Skip to content

feat(resources)!: let a team tune a default's thresholds with a params.yaml in its folder - #1849

Merged
SaulMoro merged 18 commits into
Tencent:mainfrom
Walternater:feat/promotion-thresholds
Nov 12, 2026
Merged

SaulMoro merged 18 commits into
Tencent:mainfrom
Walternater:feat/promotion-thresholds

Conversation

@Walternater

@Walternater Walternater commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

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 the params.yaml of the default's resource folder, and a team commits only that file, with only the keys it changes:

# workflows/teamai-promote-learning/params.yaml in the team repo
min_users: 1   # every other key, and WORKFLOW.yaml, stay teamai's

One contract module owns every default's parameters and texts, and every reader goes through it:

team folder (only the files it changes) + teamai's folder   (#1836 inheritance)
  └─ DefaultFolders (src/default-folders.ts): zod schema with ranges per default, text contracts
       ├─ pull / `teamai review --branch`: refuse an out-of-range value or unknown key, keep the version pull last took
       └─ DefaultFolders.load(ref, scope): no scope reads teamai's
            ├─ teamai-promote-learning      min_confidence, min_upvoted, min_users, min_age_days
            │    └─ recall promote listing, promote-publish, dashboard
            ├─ teamai-draft-learning-updates min_recalled, max_upvoted, min_users, max_related
            │    └─ --update-quality --dry-run, stale and draft-context steps, dashboard
            ├─ teamai-finished-sessions     idle: 6h  (SessionWork.finishedAfter, the one owner)
            │    └─ session list --finished, usage-report outcomes, sharing panel, share proposals
            ├─ teamai-codebase-deep-enrich  concurrency: 5
            │    └─ codebase --deep-enrich, deep-enrich
            ├─ hooks teamai-friction-reminder, teamai-mr-hint   params.yaml instead of HOOK.yaml `with:`
            │    └─ friction reasons are texts too: reason-*.md + .zh.md
            └─ rules teamai-recall          the same contract as the others

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 of inputs and defaults.inputs. The earlier commits on this branch tried defaults.inputs. Later commits replaced that approach, as the decisions comment explains.

What else changed:

  • A team with language: zh now gets the friction reminder's reasons in Chinese. Before, they were English strings inside the Chinese sentence.
  • A team that copied the deep-enrich workflow file keeps working. Its concurrency is still passed as the same input.
  • Each new folder has a README.md and a README.zh-CN.md, in the format of the other workflow folders. The teamai-import-from-mr and teamai-import-from-repo READMEs no longer say a team folder replaces teamai's "as a whole".

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

What breaks: a team HOOK.yaml that still holds with: for a default hook, as 0.30.0 documents, is refused. Pull names params.yaml as 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 replaced teamai-friction-reminder or teamai-mr-hint; the other default hooks have no parameters.

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes
  • The test files that cover what I changed pass. The full npx vitest run passed too: 786 files, 24,001 tests. Since then, three review fixes were checked with the suites they touch (47 files, 948 tests).
  • Added/updated tests for the change

Each behaviour started as a failing test:

Test Before After
default-folders: a team params.yaml holding min_users: 1 teamai's values { ...teamai, min_users: 1 }, and the steps stay teamai's
default-folders: min_confidence: 2 after a valid version (new) refused with the file and the field, and the last version stays
maintenance-quality-update: gates a team lowers, then raises snake_case gates ignored ['shared'] → ['shared','solo'] → []
finished-sessions: a team idle: 1h, with a session idle 1.5h not listed listed; an explicit --idle 6h still overrides
default-hooks-golden, zh friction reminder 问题:you interrupted the AI twice, …。 问题:你打断了 AI 2 次,你拒绝了 1 次工具调用,…。; en and unset are byte for byte as before
default-hook-replacements: a zh team holding only an English reason-interrupt.md you interrupted the AI 2 次 you interrupted the AI twice
codebase-deep-enrich: a team concurrency: 2, on a team teamwiki and on an --output one [5, 5], then [2, 5] [2, 2]
review-branch: a default workflow's bad params.yaml (new) fails with what pull prints

The 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-1752 and contribute-check-e2e pass.

Real CLI, in a world (npm run verify:world). VERIFIED: claude × git × project scope, via teamai pull, teamai recall promote and teamai review --branch. Build 468a2c62ef63 (clean), teamai 0.22.0, sha256 412ea5c6f4b0. Ledger: CLEAN; the only thing left is project/.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 on teamai-reports.

Step Command Observed
1 teamai pull; teamai recall promote No learnings eligible for promotion yet. (teamai's min_users: 2)
2 team pushes params.yaml = min_users: 1; teamai pull; teamai recall promote Synced 1 workflow; 1 learning(s) eligible for promotion: - ready (confidence: 1.00, suggested: skills)
3 branch sets min_confidence: 2; teamai review --branch tune-confidence exit 1: ✖ [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 1
4 the same value on main; teamai pull; teamai recall promote pull warns with that line and Kept the last valid version of teamai-promote-learning; this revision was not cached; ready is still listed

Related Issues

Closes #1829

Notes for Reviewers

Surfaces walked:

  • Tools. No tool-specific path changed. The friction reasons were run through the Claude Code Stop handler. Codex, CodeBuddy and OpenCode share the same handler, and I left them to CI.
  • Scopes. User scope (unit fixtures) and project scope (world).
  • Team repo modes. git (world), self mode (default-folders fixtures). http reads the same workflow cache and was not run.
  • Checkouts. Linked worktree, submodule, workspace children and bare repo were not run separately. Readers take the scope's existing workflow cache and delivered folders, which this PR does not change.
  • Layers. The default, its replacement at the team root, and the active namespace: an active namespace's folder is read and checked the same way. A single file workflows/<name>.yaml replaces the folder whole, so teamai's parameters apply.
  • Entry points.
    • CLI: recall promote, --update-quality, session list --finished, codebase --deep-enrich, review --branch.
    • Workflow steps: promote-*, stale, draft-context.
    • Hooks: the friction reminder and the MR hint.
    • Dashboard: the maintenance view and the sharing panel. No command gives the knowledge report a team repo path, so without a scope it uses teamai's parameters.
  • Reverse states. Removing the team's params.yaml gives teamai's values back on the next pull, since the workflow cache and the delivered hook folders hold only what one pull took.
  • Runs. The first pull with a refused file leaves teamai's values; a second refused pull keeps the version the first one took. Both are tested.
  • OS. Windows did not run here. The only path change is durationMs moving to src/utils/duration.ts, unchanged.
  • Docs and skill-data/. Guides en/zh, folder READMEs en/zh, the glossary ("Default parameters"), CHANGELOG.md, and the regenerated command reference (--idle help).

Agent context and wait budget:

  • No always-loaded text grows.
  • The hook event texts change only for language: zh friction reasons.
  • No hook or pull starts a new process.
  • The friction reminder reads four more small text files, only when it fires.

Deliberately left out:

  • The Resources page's parameter table still covers hooks only. A team's workflow folder shows there as replacing the default, with its inheritance counts, and a refused one shows its refusal.
  • teamai import keeps passing deep-enrich the concurrency: 5 that teamai-import-from-repo's WORKFLOW.yaml holds. A team changes that in that file.
  • The share-learnings texts in skill-data/share stay as they are. AGENTS.md needs a maintainer's sign-off before they move.

…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 SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

I read the change at 2f3b2e47. The wiring is tidy:

  • One resolver, resolvePromotionThresholds, which goes through resolveStart the way a run resolves the workflow, feeds the listing, the promote-candidate and promote-publish steps, 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.inputs keep 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-learning is a single file, so a team that only wants min_users: 1 has 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, so teamai workflow run teamai-promote-learning --input min_users=1 (or a workflow: step passing one) is accepted, but no step reads it and the resolver only looks at defaults.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 as inputs, 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.

@SaulMoro
SaulMoro requested a review from jeff-r2026 November 12, 2026 11:56
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Surface invalid promotion gates in the dashboard — src/viz.ts:594: A replacement with min_confidence: 2 makes threshold resolution fail, but this catch converts the failure into promote: []. The dashboard consequently displays “None right now.” rather than the configuration error; only the server console receives the diagnostic. Preserve the error in the report or propagate it instead of presenting a failed eligibility check as an empty result.

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>
@Walternater

Copy link
Copy Markdown
Collaborator Author

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.

@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

  • Resolved: The earlier dashboard finding is fixed: promoteError reaches both report HTML and the dashboard’s Promote tab, with escaped diagnostics and translated warning text.
  • Testing: The PR description includes a sufficient representative real-CLI verification record. No missing-testing finding.
  • Reviewed only the diff; no PR code was run, built, or installed.

@Walternater

Copy link
Copy Markdown
Collaborator Author

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: inputsMismatch refuses only undeclared keys, and defaults.inputs requires each name to be one inputs: declares (parse.ts:1615-1680), so the declaration cannot simply be dropped — the file would not parse. The gates also cannot reach the step at run time: promote-candidate resolves the thresholds from this workflow file, not from the run's inputs. Closing the silent no-op therefore means one of:

  • refuse a start that passes one of the four — needs the engine to know which declared inputs no step reads, which it does not track today;
  • pass them as flags on the internal verb — but then a run override and the file are two sources, while the listing and dashboard still read the file.

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.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Thanks, that's a clear write-up, and the CHANGELOG line in 07dbd01c covers the upgrade note. You're right that the declaration can't simply be dropped, since defaults.inputs has to name declared inputs.

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.

@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

  • Resolved: The earlier dashboard finding is fixed. promoteError reaches both the report HTML and the dashboard’s Promote tab, with escaped diagnostics and translated warning text.
  • Testing: The PR description documents a sufficient representative real-CLI verification run. No missing-testing finding.
  • Reviewed only the specified diff; no PR code was run, built, or installed.

@SaulMoro

Copy link
Copy Markdown
Collaborator

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 (Co-authored-by). @Walternater, tell me before I push if you'd rather something go differently.

What changes and why:

  • The gates are team parameters, not workflow inputs. No step reads them, and two of their three consumers (the listing and the dashboard) are not runs. Declaring them as inputs let --inputs '{"min_users":1}' be accepted and do nothing. They move to workflows/teamai-promote-learning/params.yaml, and the four inputs and defaults.inputs go.
  • Tuning one value no longer forks the flow. teamai-promote-learning becomes a folder. A team commits only params.yaml (min_users: 1) and keeps teamai's WORKFLOW.yaml through the per-file inheritance of feat(resources)!: change one file of a teamai default, and keep any kind as a file or a folder #1836. Keys merge one by one over teamai's, as hook with: does today ([feat] Resources as folders: workflows, hooks, rules and MCP servers carry their own files #1787).
  • One contract module for every default folder that code reads. src/default-hooks.ts becomes src/default-folders.ts. For each kind (hooks, workflows, rules) and name, it holds a zod schema for the parameters and the variables and required lines of each text. The recall rule's RECALL_FILES moves there. Default scorers stay in src/default-scorers.ts, because there the resource is itself the template.
  • params.yaml in every kind. Hooks move with: out of HOOK.yaml into params.yaml. This is breaking: pull refuses a HOOK.yaml that still has with: and says where it went. The result is one spelling everywhere, and with: keeps its single meaning in workflows (what a step passes to a child workflow).
  • Refusal at the door. pull and teamai review --branch refuse an out-of-range value, naming the file and the field, and the workflow cache keeps the last valid version. That replaces the read-time resolver and promoteError.
  • The same mechanism for the other hard-coded policy that already diverges:
    • teamai-draft-learning-updates gets min_recalled, max_upvoted, min_users and related_limit. QualityUpdateOptions already exists, but no caller passes it.
    • teamai-finished-sessions gets idle: 6h. Today the value lives in four places (session-work.ts, the workflow's --idle 6h, two describe texts), so replacing the workflow changes neither the dashboard nor the texts.
    • The friction reminder's {{ reasons }} carries English prose even in .zh.md. It becomes texts with {{ count }} / {{ times }}.
    • teamai codebase --deep-enrich always passes concurrency: 5, which masks the workflow's own default. The value moves to its defaults.inputs.

Left out on purpose:

  • The describe "why" texts, because they explain teamai's own mechanics. Only their "6 hours" changes, to read idle.
  • The relay prefix and {{ command }}, because they are mechanism, not team wording.
  • Windows no team has asked to tune: the 14-day marks, the 30-day self-improvement window, prune, recall relevance.
  • skill-data/share, because making it replaceable fights the skill-data/ rule in AGENTS.md and needs a maintainer's sign-off.

@RogerAbyss, this extends your call on #1829 to every default folder. Say so if params.yaml for hooks goes a step too far.

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.

@Walternater

Copy link
Copy Markdown
Collaborator Author

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 inputs only bought a --inputs '{min_users:1}' that was accepted and did nothing. Moving them to params.yaml and refusing at the door closes that no-op by construction instead of by a check.

Two things I would ask for, in the interest of not losing what this PR already established:

  1. Please keep the failure loud. The current shape refuses with the file and the field named (workflows/teamai-promote-learning.yaml: defaults.inputs.min_confidence must be a number from 0 to 1), and that message is the thing I verified in a real CLI run. If the door-refusal lands elsewhere, keep naming the file and the field — a bare "invalid configuration" would put us back to a green that checked nothing.

  2. Consider splitting the breaking part out. params.yaml for hooks changes the contract every team that has replaced a hook is on (with: moves out of HOOK.yaml), so it is a breaking change for them, not only for us. Landing it as its own commit — or its own PR — keeps the promotion change reviewable on its own, and lets the breaking change get the rollout note it needs. If you would rather keep it all in one PR, that is your call to make; I mention it because the blast radius is wider than this PR's title.

On min_recalled / max_upvoted / min_users / related_limit for teamai-draft-learning-updates, idle for teamai-finished-sessions, the friction reminder's {{ reasons }} prose and codebase --deep-enrich's concurrency: 5: folding those into the same mechanism is the right call — one spelling everywhere beats four bespoke mechanisms, and I would rather learn the mechanism once than in four places.

Happy to re-run the real CLI record against your first slice (the world pair I ran covered main vs the change on the thresholds path: the shipped gates list one learning, the team's tuned gates list two, and an out-of-range gate refuses by name) — the world harness still exists if you want the record on the final shape rather than mine.

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.
@SaulMoro SaulMoro changed the title feat(recall): tune promotion thresholds by replacing the promote workflow feat(resources)!: let a team tune a default's thresholds with a params.yaml in its folder Nov 12, 2026
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Preserve invalid-parameter diagnostics in the dashboard — src/default-folders.ts:474: With dashboard --repo and min_confidence: 2, chooseInForce logs only at debug level and returns teamai’s parameters. The dashboard then presents candidates—or “None right now.”—without revealing that the team’s configuration was rejected. Return the diagnostic alongside the fallback and display it. The earlier dashboard finding remains unresolved in this specified diff; it contains no promoteError propagation.

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.
@SaulMoro

Copy link
Copy Markdown
Collaborator

No command reaches this path. teamai dashboard has no --repo option, and every caller of the knowledge report passes the installed scope (src/dashboard.ts:943, :963, :992). Only tests passed repo to resolveVizRoot.

With a scope, the dashboard reads what pull accepted. A refused params.yaml was already reported by pull ("Kept the last valid version…") and on the Resources page, so the lists are not a silent fallback.

1f3b1eb removes DefaultFolders.loadAt instead of carrying its refusals into the page. Without a scope, the report now uses teamai's parameters, as main did. The removed path also read ~/.teamai/workflows/ on the unconfigured fallback, where pull had checked nothing.

@github-actions

Copy link
Copy Markdown

No confident findings in the specified diff.

  • Earlier dashboard finding: Resolved for scoped reads through sync-time validation and retention of the last valid cached folder—not through promoteError propagation. Unscoped --repo reads use shipped defaults.
  • Testing: The PR description includes sufficient representative real-CLI verification. The older build record is a note, not a blocking finding.
  • Reviewed only the specified diff; no PR code was run, built, or installed.

…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.
@SaulMoro

SaulMoro commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

@Walternater both points are taken.

The failure stays loud. Pull and teamai review --branch name the file and the field. From the real-CLI run on this branch:

✖ [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 1
Kept the last valid version of teamai-promote-learning; this revision was not cached

A team HOOK.yaml that still has with: is refused the same way: HOOK.yaml: with is not a key of a default hook, whose parameters are in params.yaml. Move the keys of with to params.yaml.

The hook change is breaking for released teams, and you were right about the blast radius. 0.30.0 ships teamai-friction-reminder and teamai-mr-hint with their parameters in HOOK.yaml's with:, and I had wrongly treated the hook folders as unreleased. The CHANGELOG now lists the change under Breaking Changes, with the move to make. The 0.30.0 hook-folder entry keeps its published wording. The change stays its own commit (3ed50c12e) in this PR, with this note.

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 --output, the dashboard read without a scope, and the CHANGELOG. None of them is on the promotion path.

@github-actions

Copy link
Copy Markdown

No new confident findings in the specified diff.

  • Resolved: The earlier dashboard finding is addressed through sync-time validation and retention of the last valid cached workflow folder—not promoteError propagation. Unscoped reads use shipped defaults.
  • Testing: The PR description includes sufficient representative real-CLI verification. Its older build record is a note, not a blocking finding.
  • Reviewed only the specified diff; no PR code was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the final shape at 29bf331: the parameters in params.yaml, refusals that name the file and the field, and the hook change recorded as breaking.

@SaulMoro
SaulMoro merged commit a342c2f into Tencent:main Nov 12, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Make promotion thresholds configurable via external config

3 participants