Repository navigation
feat(resources)!: let a team tune a default's thresholds with a params.yaml in its folder - #1849
Conversation
…flow The four gates findPromotionCandidates applied were constants, so every team lived with the bar a large org needs: a small team can never reach MIN_USERS tuned for many members, and tuning meant editing source. The gates now live in workflows/teamai-promote-learning.yaml's defaults.inputs (min_confidence, min_upvoted, min_users, min_age_days), shipped at the values the constants held. A team tunes its bar by replacing that workflow file, keeping the defaults it does not change. One shared schema (PromotionThresholdsSchema) parses the resolved workflow's defaults, refusing an out-of-range gate with the file and the field named instead of falling back silently. The listing, the promote-candidate and promote-publish steps, and the dashboard all resolve the same workflow through resolvePromotionThresholds, so a listing never offers a learning a step then rejects. Closes Tencent#1829 Co-Authored-By: Claude Code <noreply@anthropic.com>
SaulMoro
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
I read the change at 2f3b2e47. The wiring is tidy:
- One resolver,
resolvePromotionThresholds, which goes throughresolveStartthe way a run resolves the workflow, feeds the listing, thepromote-candidateandpromote-publishsteps, and the dashboard. They can't disagree. - The four gates have one zod contract.
- A gate out of range refuses with the file and the field named, instead of falling back.
- The shipped
defaults.inputskeep the old constants, so nothing moves for a team that changes nothing.
The before and after world runs in the description show the tuned min_age_days taking effect, and an out-of-range min_confidence refusing.
Notes, none blocking:
- [P2 non-blocking] Tuning a gate forks the whole promote flow.
teamai-promote-learningis a single file, so a team that only wantsmin_users: 1has to copy every step, prompt and threshold. From then on it stops getting teamai's fixes to the promote steps. #1836 just made default folders inherit per file, which avoids exactly this when the workflow is a folder. If the gates are meant to be a team setting, it's worth deciding whether this workflow becomes a folder, or whether the gates live where a team can override them without taking the steps. - [P2 non-blocking] A per-run override is accepted and ignored. The four names are now declared
inputs, soteamai workflow run teamai-promote-learning --input min_users=1(or aworkflow:step passing one) is accepted, but no step reads it and the resolver only looks atdefaults.inputs. The description says this is deliberate. A member gets no sign it did nothing, though, which is the silent-fallback shape the refusal above avoids. Not declaring them asinputs, or refusing a run that passes one, would close it. - [P3 nit] The CHANGELOG line doesn't say that a member on an older teamai keeps the hard-coded gates, so two members can see different listings until everyone upgrades.
@jeff-r2026 your call on this one. It's a new team-facing tuning surface (#1829), and the open question is where it should live: in a single workflow file a team must fork, or somewhere a team can change the gates without taking over the steps.
The PR description includes a sufficient representative real-CLI verification record. No missing-testing finding. No PR code was run, built, or installed. |
Codex review on this PR ([P2] on src/viz.ts): a replacement workflow with an out-of-range gate made threshold resolution fail, and the dashboard caught the error into an empty promote list, displaying 'None right now.' — a green that checked nothing, the same misconfiguration shape the CLI side refuses loudly. The error now rides the maintenance payload (VizData.maintenance .promoteError, into buildImprovement's output) and renders as a warning in both report HTML and the dashboard's Promote tab, with the message escaped and the label translated. Empty-because-nothing-eligible and empty-because-the-gates-are-broken no longer look alike. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Codex review's [P2] (surface invalid promotion gates in the dashboard) is addressed in 8f50c3b: the resolution error now rides the maintenance payload and renders as a warning in the report HTML and the Promote tab, with the message escaped and the label translated — an empty promote list now says which of the two kinds of empty it is. |
|
No new findings in the specified diff.
|
|
On the three notes: Tuning a gate forks the flow. The trade-off is real. Two shapes I can see:
If you want the folder shape, I can convert it in this PR; otherwise I'd file it as its own follow-up and keep this one to the single file. A per-run override is accepted and ignored. Verified against the code:
I'd take the first, as its own change rather than this PR. Say the word and I'll open it. CHANGELOG. Added in 07dbd01: a member on an older teamai keeps the hard-coded gates, so upgrade every member before tuning them, or two members see different listings. |
Thanks, that's a clear write-up, and the CHANGELOG line in Both open choices are about where this tuning surface should live: converting to a folder here or as a follow-up, and refusing a start that passes a gate. So I'll leave them to @jeff-r2026 rather than pick for you. |
|
No new findings in the specified diff.
|
|
I'll take this forward on this branch, in this PR, as commits on top of yours. The squash merge keeps you as co-author ( What changes and why:
Left out on purpose:
@RogerAbyss, this extends your call on #1829 to every default folder. Say so if Each slice lands as its own commit, green on its own, with its CHANGELOG line and docs. The PR description will carry the record of the real CLI run. |
|
No objection from my side — go ahead and push on this branch. The argument that the gates are team parameters rather than workflow inputs is the part I got wrong: no step reads them, and two of the three consumers are not runs at all, so declaring them as Two things I would ask for, in the interest of not losing what this PR already established:
On Happy to re-run the real CLI record against your first slice (the world pair I ran covered |
A default hook's parameters lived in HOOK.yaml's `with:`, so the spelling a team tunes a default with would differ for every other kind of default folder, where the marker is the resource itself (WORKFLOW.yaml, RULE.md) and cannot hold them. They move to a params.yaml beside it: a team folder holding only its params.yaml keeps teamai's HOOK.yaml and texts, and a key it leaves out keeps teamai's value, merged key by key as before. HOOK.yaml of a default hook now holds only description. Pull and `teamai review --branch` refuse one that keeps `with:` and say where its keys go. Tested: default-hook-replacements (params.yaml tunes min_tools with only that file in the team folder; out-of-range, unknown key and a leftover `with:` refused with the file named), dashboard default-hook comparison and review-branch refusal; npm run typecheck and lint.
A default hook's folder and the recall rule's folder are both defaults
whose files code reads, but each kept its own contract: DefaultHooks
for the hooks' parameters and texts, RECALL_FILES in builtin-rules for
the lines recall's texts keep. A third kind, workflows, is about to
need the same, so the contract moves to one module, DefaultFolders in
src/default-folders.ts: by kind and name, the parameters' schema and,
per text, the variables it offers and the lines it must keep. Pull,
the hints, recall and the dashboard read through it.
The recall folder now gets the hooks' checks too: it names every
missing line in one pass and refuses a `{{ }}` no text offers. The
`${TEAMAI_RULE_DIR}` refusal becomes a rule of the rules kind.
Tested: recall-folder, review-branch, dashboard-layered-resources and
every hint suite unchanged except for the recall refusal now listing
both missing lines; npm run typecheck and lint.
The hook folders and their move to params.yaml are both unreleased, so a Breaking Changes entry for HOOK.yaml's with: described a state no release had. The feature entry now says where the parameters live.
…yaml Why: the four promotion gates were constants a team could not change. The earlier version on this branch put them in the workflow's defaults.inputs, which made them per-run inputs that the listing and the dashboard, which run no workflow, had to read by parsing the workflow file, and which a caller could override per run. What: teamai-promote-learning is now a folder. Its params.yaml holds min_confidence, min_upvoted, min_users and min_age_days, and the DefaultFolders contract owns their schema and ranges. A team folder holding only params.yaml keeps teamai's WORKFLOW.yaml and its other values. The workflow cache checks the team's params.yaml at pull, so pull and `teamai review --branch` refuse a value out of range or an unknown key, and the version pull last took stays in force. The listing, promote-publish and the dashboard read the gates through DefaultFolders. With `--repo`, the dashboard reads them from that repo's folder. findPromotionCandidates takes one named object with the gates required. The inputs, defaults.inputs, promotion-thresholds.ts and the dashboard's promoteError are removed. Proof: default-folders tests cover a key set over teamai's, a refusal that keeps the last version, and a first refusal that leaves teamai's. A review-branch case covers the refusal before merge, a viz case covers the --repo folder, and maintenance-promote applies tuned gates. The unit suites touched and the recall-promote-publish and namespaced-learnings-maintenance e2e files pass, and typecheck and lint are clean.
Why: the stale gates (5 recalls, at most 1 upvote, 2 members) and the five adopted learnings a draft prompt shows were constants. A small team never reached them, and a team could not change them without forking teamai. What: teamai-draft-learning-updates is a folder. Its params.yaml holds min_recalled, max_upvoted, min_users and max_related, checked by the DefaultFolders contract at pull and by `teamai review --branch`. findStaleEntries takes the gates, and findRelatedAdoptedLearnings takes the limit. The --dry-run listing, the stale and draft-context steps and the dashboard read them through DefaultFolders. The dashboard with --repo reads that repo's folder. Both READMEs follow the other workflow folders. Proof: a maintenance-quality-update case lists an entry once a team lowers min_users and drops it once min_recalled rises. It failed before the change. The touched unit suites, the three update-quality e2e files, typecheck and lint pass.
durationMs moves from workflow/parse.ts to utils/duration.ts, unchanged, and its five importers follow. The default-folders contract, which every hook loads, is about to validate a duration parameter, and it should not load the workflow parser to do it. Typecheck and lint pass. No behaviour changes.
Why: an attended session counted as finished after six hours, written in three places that could disagree: `--idle 6h` in the workflow's steps, FINISHED_AFTER_MS for the usage report's outcomes and the dashboard, and "6 hours" in the share proposals and panel texts. A team that replaced the workflow with --idle 1h got sessions archived at one hour, while the dashboard and outcomes still waited six. What: teamai-finished-sessions is a folder whose params.yaml holds `idle: 6h`, from 10m to 7d, checked by the DefaultFolders contract. SessionWork.finishedAfter reads it, and becomes the one owner. The listing without --idle, the usage report's outcomes, the dashboard's sharing panel and the share proposals all use it. The panel and the proposals name the value. The steps no longer pass --idle, and FINISHED_AFTER_MS is gone. The dashboard's outcome texts, the guides and both new READMEs say the idle is the team's. Proof: a finished-sessions case lists a session idle 1.5h once the team's params.yaml sets 1h, and not before. An explicit --idle 6h still overrides it. The case failed before the change. A sharing-view case renders the team's 1h. The session, dashboard and default-folder suites, the score-drain and update-hook-outcomes e2e files, typecheck and lint pass.
Why: the reasons the friction reminder names (you interrupted the AI twice,
you rejected 1 tool call, ...) were English strings in code. A team with
language: zh got them inside a Chinese sentence, and no team could reword
them.
What: the folder ships reason-interrupt, reason-tool-reject,
reason-correction and reason-tool-error, in English and Chinese. They read
{{ count }}, {{ times }} and {{ tool_calls }}, worded in the language of the
file read: twice or 2 次, 1 tool call or 1 次工具调用. The reasons are joined
with a comma of that language. The contract lists the new texts and their
variables, so pull checks a team's replacement of them, and both READMEs
list them.
Proof: the default-hooks golden fixture now expects Chinese reasons for zh.
It failed on all eight reason cases before the change and passes after.
English and unset stay byte for byte what they were. The contribute-check,
hook replacement and dashboard suites, the contribute e2e file, typecheck
and lint pass.
…ams.yaml Why: `teamai codebase --deep-enrich` passed concurrency 5 from a constant in code. A team whose AI tool rate-limits it could only lower it by replacing the whole WORKFLOW.yaml and ignoring the input. What: teamai-codebase-deep-enrich ships params.yaml with concurrency: 5, from 1 to 20 in the DefaultFolders contract. The shortcut passes the team's value as the same input, so the workflow's input contract and every caller's copy keep working. teamai-import-from-repo keeps passing its own 5. The folder's READMEs gain Parameters and What pull refuses, and their "Replace it" now describes a folder holding only the files it changes, rather than a copy replacing teamai's as a whole. Proof: a codebase-deep-enrich case starts the run with concurrency 2 once the team's params.yaml says so. It failed before the change. A review-branch row shows a contracted folder holding only component.md passes and a typo fails. The touched suites, typecheck and lint pass.
The READMEs of teamai-import-from-mr and teamai-import-from-repo, and the changelog entry that introduced them, still told a team to copy the whole folder and said the copy replaces teamai's as a whole. Since a team folder inherits every file it leaves out, they now show copying only the prompt being changed, as the other default folders' READMEs do.
No shipped reason text reads {{ count }}, so nothing showed a team's
replacement gets it. The case replaces only reason-tool-reject.md and checks
the reminder words that reason with the count and keeps teamai's
reason-interrupt.md. With count emptied by hand, the case fails.
…eam's A zh team that replaced only reason-interrupt.md with English text got "you interrupted the AI 2 次": the base file reads as no language, and the team's language filled in for it. The count words now follow the picked file alone, so a base file is worded in English. A default-hook-replacements case for a zh team, holding only an English reason-interrupt.md, failed before the change and passes after; the golden Chinese reasons still pass.
…or --output The run starts in the member's config whatever teamwiki it writes, but the concurrency came from the teamwiki's team repo, so an --output teamwiki got teamai's 5 even when the team set 2. The shortcut now reads the parameter from the member's config, read without writing, and teamai's where no teamai init serves the directory. The deep-enrich case now starts both a team teamwiki and an --output one under concurrency: 2; it gave [2, 5] before the change and [2, 2] after.
The PR description documents sufficient representative real-CLI verification. No missing-testing finding. Reviewed only the specified diff; no PR code was run, built, or installed. |
No command passes a team repo path to the knowledge report, so reading that path's own default folders served no reader. Without a scope, the report used it on the ~/.teamai fallback, where a leftover workflow folder could change the lists without pull having checked it. With no scope, the report now uses teamai's parameters, as before this change.
|
No command reaches this path. With a scope, the dashboard reads what pull accepted. A refused 1f3b1eb removes |
|
No confident findings in the specified diff.
|
…s breaking 0.30.0 ships the default hook folders with their parameters in HOOK.yaml's with:, so a team that replaced one is refused after this change until it moves the keys to params.yaml. The hook folder entry keeps the wording 0.30.0 shipped, and the friction reasons as texts get their own line.
|
@Walternater both points are taken. The failure stays loud. Pull and A team The hook change is breaking for released teams, and you were right about the blast radius. 0.30.0 ships Thanks for the offer to rerun the world. The record in the description is on the final shape of the promotion path. The commits since then change the language of a friction reason, where deep-enrich reads its concurrency for |
|
No new confident findings in the specified diff.
|
Summary
A team could not change the bars teamai's defaults apply, such as the four promotion gates of #1829, the stale-entry gates of
--update-quality, or how long a session is idle before it counts as finished, without forking teamai. Some were also stated in more than one place that could disagree. Now each of these values lives in theparams.yamlof the default's resource folder, and a team commits only that file, with only the keys it changes:One contract module owns every default's parameters and texts, and every reader goes through it:
The workflow engine never reads
params.yaml. A default's parameters are per team, and code outside runs reads them too, so they stay out ofinputsanddefaults.inputs. The earlier commits on this branch trieddefaults.inputs. Later commits replaced that approach, as the decisions comment explains.What else changed:
language: zhnow gets the friction reminder's reasons in Chinese. Before, they were English strings inside the Chinese sentence.README.mdand aREADME.zh-CN.md, in the format of the other workflow folders. Theteamai-import-from-mrandteamai-import-from-repoREADMEs no longer say a team folder replaces teamai's "as a whole".Type of Change
What breaks: a team
HOOK.yamlthat still holdswith:for a default hook, as 0.30.0 documents, is refused. Pull namesparams.yamlas the place to move its keys, and until they move, teamai's folder is in force. The CHANGELOG records it under Breaking Changes. This affects a team that replacedteamai-friction-reminderorteamai-mr-hint; the other default hooks have no parameters.Test Plan
npx tsc --noEmitpassesnpm run lintpassesnpx vitest runpassed too: 786 files, 24,001 tests. Since then, three review fixes were checked with the suites they touch (47 files, 948 tests).Each behaviour started as a failing test:
default-folders: a teamparams.yamlholdingmin_users: 1{ ...teamai, min_users: 1 }, and the steps stay teamai'sdefault-folders:min_confidence: 2after a valid versionmaintenance-quality-update: gates a team lowers, then raises['shared'] → ['shared','solo'] → []finished-sessions: a teamidle: 1h, with a session idle 1.5h--idle 6hstill overridesdefault-hooks-golden, zh friction reminder问题:you interrupted the AI twice, …。问题:你打断了 AI 2 次,你拒绝了 1 次工具调用,…。; en and unset are byte for byte as beforedefault-hook-replacements: a zh team holding only an Englishreason-interrupt.mdyou interrupted the AI 2 次you interrupted the AI twicecodebase-deep-enrich: a teamconcurrency: 2, on a team teamwiki and on an--outputone[5, 5], then[2, 5][2, 2]review-branch: a default workflow's badparams.yamlThe e2e files
recall-promote-publish,namespaced-learnings-maintenance,recall-maintenance-update-quality,recall-maintenance-votes-dry-run,recall-maintenance-skills-1735,score-drain,update-hook-outcomes-1752andcontribute-check-e2epass.Real CLI, in a world (
npm run verify:world). VERIFIED: claude × git × project scope, viateamai pull,teamai recall promoteandteamai review --branch. Build468a2c62ef63(clean), teamai 0.22.0, sha256412ea5c6f4b0. Ledger: CLEAN; the only thing left isproject/.claude, known as #1808. The world was built before the three review fixes, which do not touch this path.The seed is one learning on
teamai-learnings, 30 days old, upvoted 15 of 15 times by a single member onteamai-reports.teamai pull;teamai recall promoteNo learnings eligible for promotion yet.(teamai'smin_users: 2)params.yaml=min_users: 1;teamai pull;teamai recall promoteSynced 1 workflow;1 learning(s) eligible for promotion: - ready (confidence: 1.00, suggested: skills)min_confidence: 2;teamai review --branch tune-confidence✖ [workflows] workflows/teamai-promote-learning/params.yaml:1: field "(root)": workflows/teamai-promote-learning/params.yaml: min_confidence must be a number from 0 to 1teamai pull;teamai recall promoteKept the last valid version of teamai-promote-learning; this revision was not cached;readyis still listedRelated Issues
Closes #1829
Notes for Reviewers
Surfaces walked:
git(world), self mode (default-foldersfixtures).httpreads the same workflow cache and was not run.workflows/<name>.yamlreplaces the folder whole, so teamai's parameters apply.recall promote,--update-quality,session list --finished,codebase --deep-enrich,review --branch.promote-*,stale,draft-context.params.yamlgives teamai's values back on the next pull, since the workflow cache and the delivered hook folders hold only what one pull took.durationMsmoving tosrc/utils/duration.ts, unchanged.skill-data/. Guides en/zh, folder READMEs en/zh, the glossary ("Default parameters"),CHANGELOG.md, and the regenerated command reference (--idlehelp).Agent context and wait budget:
language: zhfriction reasons.Deliberately left out:
teamai importkeeps passing deep-enrich theconcurrency: 5thatteamai-import-from-repo'sWORKFLOW.yamlholds. A team changes that in that file.skill-data/sharestay as they are. AGENTS.md needs a maintainer's sign-off before they move.