Repository navigation
[feat] Read every member setting in one order: environment, this scope, this machine, the team #1459
Description
Example
A member set up two workspaces with the default teamai init and never installed user scope. Today they cannot set anything for the whole machine. The docs send them to ~/.teamai/config.yaml. With a file that holds only settings, every command prints ✖ Invalid local config: repo: Required; username: Required (7 times on status, 29 on doctor), and the settings do nothing. With a user install as well, most readers in a workspace still ignore that file, because they read the workspace's own config.yaml.
After this issue, the member writes the machine file and overrides it in one workspace:
# ~/.teamai/config.yaml: this machine. No repo or username needed
ai:
tool: codex
recallEnabled: true
workflows:
tools: { default: codex }# ~/.teamai/projects/<slug>/config.yaml: the workspace ~/work/a
ai:
tool: claude
recallEnabled: false$ cd ~/work/a && teamai doctor
…
ℹ Member settings:
ai.tool: claude, from the workspace config ~/.teamai/projects/<slug>/config.yaml
recallEnabled: false, from the workspace config ~/.teamai/projects/<slug>/config.yaml (the machine says true, the team false)
workflows.tools.default: codex, from the machine config ~/.teamai/config.yaml
$ cd ~/work/b && teamai import --dir . # runs on codex, from the machine configWhat changes
Each member setting is read in this order. The first level that sets it wins:
1. environment or flag where the key has one (TEAMAI_AI_TOOL, --tools, …), at the places that read it today
2. this scope the config.yaml of the scope the command runs in (workspace, self or user)
3. this machine ~/.teamai/config.yaml
4. the team teamai.yaml of this scope's team, where a team key exists
5. default
In user scope, levels 2 and 3 are the same file. Levels 1, 4 and 5 stay where they are today, in each key's resolver (isRecallEnabled, isContributeHintEnabled, resolveCoAuthor, … in src/types.ts:282-360) and at the flags. This issue adds level 3 below a workspace or self scope, and level 2 where a reader skips it.
| Key | 1 env or flag | 2 this scope | 3 this machine | 4 team |
|---|---|---|---|---|
ai.tool (#1444) |
TEAMAI_AI_TOOL |
yes (#1444) | yes | - |
workflows.tools.<agent>, workflows.max_parallel, workflows.auto_approve, workflows.sandbox, workflows.allow.<key> |
--tools, --auto-approve |
yes | new | - |
recallEnabled |
TEAMAI_RECALL_DISABLED (hooks) |
yes | new | sharing.recall.enabled |
contributeHintEnabled |
TEAMAI_CONTRIBUTE_HINT_DISABLED |
yes | new | sharing.contributeHint.enabled |
mrHintEnabled |
TEAMAI_MR_HINT_DISABLED |
yes | new | sharing.mrHint.enabled |
coAuthorEnabled |
- | yes | new | sharing.coAuthor.enabled |
usageReportEnabled |
- | yes | new | usageReport |
codexTrustEnabled |
- | yes | new | - |
modelAliases.<alias> |
- | new | yes | - |
updatePolicy, autoCompactTokens |
- | see below | new | autoUpdate, sharing.autoCompact.tokens |
A key is a path to one value. In a map (workflows.tools, workflows.allow, modelAliases) each entry is a key of its own, so a workspace that sets workflows.tools.implementer still takes workflows.tools.default from the machine.
updatePolicy and autoCompactTokens act on what the whole machine shares: the installed CLI, and each tool's own config file. One scope decides them: the user scope when its config can be read, else the scope that runs. #1456 makes both readers follow that rule. This issue adds only level 3 when no user scope is installed. A team's autoUpdate: false or sharing.autoCompact.tokens therefore holds on a member's machine only when that team's scope decides.
updatePolicy from the machine file takes effect once #1456 is fixed, since doUpdate reads only ~/.teamai/config.yaml as an install today.
toolRoots keeps its own rule: this scope, else this machine, as a whole map (resolveMemberToolRoots, src/config.ts:609-617), and init still copies it into a new workspace config (src/init.ts:812-815).
One declaration, one overlay
The member settings above are declared once, as a schema of their own composed into LocalConfigSchema (src/types.ts:810). Every other key of LocalConfigSchema is outside this overlay: repo, username, scope, projectRoot, provider, primaryRole, additionalRoles, projects, subscribedTags, excludedSkills, enabledAgents, disabledAgents, inheritUserScope, resourceProfileVersion, gitExcludeEnabled (one checkout's .git/info/exclude, #915), toolRoots (its own rule, above), and scoring (per data home: a machine value would start paid session judging in every workspace). A test fails when a key of LocalConfigSchema is in neither list.
One function reads the machine settings from ~/.teamai/config.yaml, whether or not that file is an install. The overlay fills its values into a workspace or self config wherever the team's role assignment is applied today (#1186): when a partition config is loaded (src/config.ts:437, :759), and in refreshRoleAssignment (src/config.ts:188), which init and pull run on a config they built or hold in memory (src/init.ts:798, :1415, :2310, src/pull.ts:1433). A load runs it more than once, so applying it twice gives the same config, as the role overlay's guard does (src/config.ts:116-119). So init's first hook reconciliation, on the git, self and HTTP paths alike, already sees a machine codexTrustEnabled: false, and every reader that takes a LocalConfig gets the resolved value without a change of its own.
Saving must not copy a machine value into the scope's file, and must keep what a command set. The overlay keeps the machine values it applied, runtime-only. serializeLocalConfig (src/config.ts:304) leaves out a key while it still holds the applied value, unless a command set it. recall enable and recall disable (src/recall-toggle.ts:107, :122), the only commands that write a member setting, mark the key as set, so recall disable in a workspace persists recallEnabled: false even when the machine already says false.
The readers that read ~/.teamai/config.yaml directly change as follows:
| Reader (#1444 unless noted) | Today | After |
|---|---|---|
callTool (src/tool-launch/call.ts:85) |
done in #1444: aiToolSettings (src/config.ts) reads ai.tool from the scope config, then the machine file; the overlay replaces that reader |
resolveConfigForDir(process.cwd()) by default, since no caller holds a config today; a caller acting for another scope passes its config. With no scope, the machine settings; a scope config that cannot be read, or a repository git cannot read, is an error, not a fallback (resolveConfigForDir returns nothing in all three cases, and only its onUnreadable callback tells them apart, src/config.ts:579-597). The per-process pick cache (call.ts:97-98) is keyed by the resolved ai.tool |
workflow start (src/workflow-commands.ts:172) and the local driver (src/workflow/drivers/local/local-driver.ts:336) |
done in #1444 for ai.tool, through aiToolSettings; workflows.* still loadLocalConfig() |
the config the workflow already selected (readyStartConfig, runOwner, src/workflow-commands.ts:104-125) |
price-table aliases (src/hook-handlers.ts:440-449, main) |
loadLocalConfig() |
the config the hook dispatcher resolved |
the HTTP local agent's Codex trust (src/local-agent.ts:582, :588, main) |
loadLocalConfig(); the agent has no scope config |
the machine settings |
ci extract-mr keeps running with no teamai init (#1389): with no scope and no machine file, every key is at its default.
A missing or partial machine file
~/.teamai/config.yaml |
User scope | Level 3 |
|---|---|---|
| missing | not installed | empty |
no repo key |
not installed. Today this is the "could not be read" path (src/config.ts:388-396) and logs Invalid local config on every load. After, it is "not initialized", with no error |
its member settings |
| an install | installed, as today | its member settings |
| an install that does not validate | not usable, as today: The teamai config at … could not be read |
its member settings that validate |
Level 3 reads each member setting on its own, from the file as YAML. A value of the wrong type (recallEnabled: "yes") is skipped, the next level decides, and each command that reads it warns once: recallEnabled in ~/.teamai/config.yaml is "yes", not true or false, so it is ignored. That is how scorers/scoring.yaml treats a bad key (src/scorers/settings.ts:39-79). workflows.allow is the exception: a bad value there fails the workflow command and names the key, so a typo never lifts a restriction. A key in a settings-only file that is not a member setting (excludedSkills, …) is not read, and doctor says so. In a scope's own config, a bad value still makes that config unreadable, as today, because that file is also the install record.
Whether user scope is installed is one exported check, used everywhere it is asked: ~/.teamai/config.yaml holds a repo key. #1444 added that check as hasUserScopeInstall (src/config.ts:300) and already uses it in throwMissingOrInvalid, the env loader's user row, Hermes' global rules and init's re-init guard, owner check and set-aside recovery; both loaders that print Invalid local config stay silent for the same files. Its rule is narrower: a file holding only member-setting keys (MEMBER_SETTING_KEYS, today only ai) is not an install. This issue widens that rule to "no repo key" rather than adding a second check. An install that fails validation still has its repo key, so it still counts as installed, as rules.ts:967 wants.
init --scope user (git or --http) over a settings-only file takes the machine settings as the new config's own values, beside what it carries today (carriedConfig, src/init.ts:1689-1691), before its first hook reconciliation, with no "already initialized" prompt. Today it stops with teamai is already initialized for user scope, and with --force moves the file to config.yaml.previous as another install's config.
Which level won
doctor and doctor --json add Member settings: each member setting set at any level, with its value, the level and file (or variable) that set it, and the values of the levels below it, the team's included. In user scope the file is named once, as "the user config, also this machine's". For updatePolicy and autoCompactTokens it shows the value of the scope that decides, and says when this scope's own value is not used. Text that already names a file for a setting's value names the level that set it instead: #1444's ai.tool error (src/tool-launch/call.ts:71, :77) and doctor's usage-reporting note (src/doctor.ts:516), which today name ~/.teamai/config.yaml and the scope's own config.yaml whatever set the value.
Out of scope
- A team default or per-namespace default for keys that have no team key today (
ai.tool,workflows.*,codexTrustEnabled). No team has asked, and level 4 has room for one later. - Per-namespace values for the existing team keys.
scorers/<ns>/scoring.yamlstays the only per-namespace setting file. - The install keys listed above,
scoring.*included. - A command to set these keys. Members edit the files, as the docs say today.
- Env and secret values per workspace ([feat] Workspace-scoped env overrides, so one machine can point each repo at its own database #1429).
- Bugs filed on their own: [bug] Auto-update and auto-compact are decided by whichever scope runs, not by one rule #1456 (which scope decides
updatePolicyandautoCompactTokens), [bug]TEAMAI_RECALL_DISABLED=1does not stoppullinstalling recall #1457 (TEAMAI_RECALL_DISABLEDnot stoppingpull) and [bug]init --forcein an existing workspace drops the member's own settings #1458 (init --forcedropping member settings; its fix carries the scope file's own values, never overlaid ones). The docs that send members to~/.teamai/config.yamlfor keys a workspace reads from its own config are fixed here (Definition of done). uninstall. A user-scope uninstall that removes~/.teamai/config.yamltakes the machine settings with it, as today.
Acceptance
- In a workspace-only install,
~/.teamai/config.yamlholding onlyai: { tool: codex }makesimport --dir,recall promote, and aworkflow start --driver localprompt step with noworkflows.toolsentry run on codex.status,doctorandpullprint noInvalid local configand no warning about the user scope, and the env registry gets no user row. - With no install at all, the same file makes
teamai ci extract-mrin a bare checkout run on codex (the one AI command that runs withoutteamai init). - With
ai.tool: claudein that workspace's config as well, those run on claude there and on codex in another workspace. - With user and workspace installs,
recallEnabled: truein~/.teamai/config.yamlturns recall on in a workspace whose config does not set it.recall disablein that workspace writesrecallEnabled: falseto its config, and a laterpullkeeps recall off. - A save by any other command leaves the workspace config without the machine's settings.
codexTrustEnabled: falsein a settings-only machine file stopsinitof a new workspace from trusting its Codex hooks.recall disablein a workspace whose machine file already saysrecallEnabled: falsestill writes the key to the workspace config.codexTrustEnabled: falsein a settings-only machine file stopsinit --httpfrom trusting the local agent's Codex hooks.- A bad value at level 3 is warned about, and the next level decides. A bad
workflows.allowvalue fails the workflow command. init --scope userover a settings-only file, with no TTY and no--force, installs user scope and keeps the file's member settings.doctorlists each member setting with its level, file and the levels below. WithusageReportEnabled: falseonly in the machine file, its usage-reporting note names the machine file.- A key added to
LocalConfigSchemafails a test until it is declared a member setting or an install key.
Definition of done
- Docs, English and
zh-CN:docs/guide/reference.md(the config section explains the levels and lists the member settings), and the lines that name a file for a key:knowledge.md:38,:82,:92,member-guide.md:117,:140,diagnostics.md:28-36,reporting.md:14-18,workflow.md:154,workflow-advanced.md:57,:620. Grep~/.teamai/config.yamland "local config" before the PR. CHANGELOG.md,### Changed: member settings in~/.teamai/config.yamlnow apply in every workspace and self scope whose own config does not set them, also for workspaces of other teams.updatePolicyandautoCompactTokensfollow the deciding-scope rule above. A member with a user install who set one there, for examplerecallEnabled,coAuthorEnabled,usageReportEnabledorworkflows.*, for user scope only sees it apply in their workspaces too.- Real-CLI record: a workspace-only install with a settings-only machine file, showing
doctor'sMember settingsand one AI call on the machine's tool; then the workspace override; thenrecall disableand apull.
Activity
SaulMoro commentedon Oct 27, 2026
Suggested order, one PR each, so every step is usable and verifiable on its own:
repokey in~/.teamai/config.yaml) replaces the nine file-existence checks, and the loaders stop printingInvalid local configfor a file withoutrepo. Done when a workspace-only install with a settings-only~/.teamai/config.yamlrunsstatus,doctorandpullwith no config error, andinit --scope userover that file keeps its settings.ai.toolresolver feat(tool-launch): one launcher for every AI call, from the tool descriptor (#1389) #1444 adds rather than a second reader. Done with the acceptance items forrecallEnabled,codexTrustEnabledand the bad-value warning.doctoranddoctor --jsonlist each member setting with its level, and the existing texts that name a file for a value name the level instead.#1456 is independent and can land before or after any of these.
SaulMoro commentedon Oct 27, 2026
Status after #1444 (merged, edebc6c):
ai.toolis read per scope:TEAMAI_AI_TOOL, then the scope config, then~/.teamai/config.yaml, throughaiToolSettings(src/config.ts).callTooland both workflow paths use it. The body now marks that row and those readers done.hasUserScopeInstallis the one install check, and a machine file holding onlyaiis no install and prints no config error. What is left is widening its rule from "only member-setting keys" (MEMBER_SETTING_KEYS, todayai) to "norepokey", plusinit --scope userkeeping that file's settings.feat(tool-launch): read ai.tool through the scope's loaded config
fix(hooks): price a stop with the model aliases of the hook's scope
fix(workflow): refuse to run while this machine's workflows.allow is …
test(config): fail when a local config key is neither a member settin…
feat(doctor): print the member settings, and name the level that set …
docs: explain the member setting levels and the machine config
fix(workflow): refuse every workflow command, run ids included, while…
feat(config): accept settings-only machine files and overlay member s…
feat(tool-launch): read ai.tool through the scope's loaded config
fix(hooks): price a stop with the model aliases of the hook's scope
fix(workflow): refuse to run while this machine's workflows.allow is …
66 remaining items
fix(workflow): refuse every workflow command, run ids included, while…
test(doctor): follow the provenance report's runtime values and per-a…
fix(doctor): name the user config for a setting it decides in a works…
docs: list sessions among the member settings
fix(doctor): say a workspace value the user scope overrules is not used
fix(doctor): report the session publishing policy of each run mode
fix(doctor): say the machine's session policy is not used where a wor…
fix(doctor): name the HTTP team repo for session publishing it turns off
feat(config): AI calls, stop pricing, workflow policy and doctor foll…
SaulMoro commentedon Oct 28, 2026
Two leftovers after #1477 and #1483.
sessionsis a member setting,scoringis an install key. #1483 madesessionsa member setting, so a workspace takes the machine's value.scoring, whosescoring.sessionsturns the same member's session judging on or off per mode, stays inLOCAL_CONFIG_INSTALL_KEYS(src/types.ts:1005-1009). So a machine-level opt-out of publishing sessions reaches every workspace, and a machine-level opt-out of judging them does not.The body keeps
scoringout of the overlay because a machine value would start paid judging in every workspace. That reason covers turning it on, not off. A smaller rule would close the gap: a machinescoring.sessions: false(ordaily_limit: 0) applies below any scope that does not set it, whileon/autostays per scope. Otherwise the docs should say thatscoringmust be set in each workspace.Declared once. The key-ownership test (
src/__tests__/types.test.ts:86-97) now fails when aLocalConfigSchemakey is in neither list, as the body asks. The value schemas are still written twice:recallEnabled,codexTrustEnabledandmodelAliasesappear in bothMemberSettingsSchema(src/types.ts:843,:849,:851) andLocalConfigSchema(:926,:947,:985), with nothing that fails if one changes and the other does not. ComposingMemberSettingsSchema.shapeintoLocalConfigSchemawould remove the second copy.Checked on main at 299ce8e.
docs(changelog): say which scope decides updatePolicy and autoCompact…
Screenshots of the Judging row in this workspace (Tencent#1459)
docs(changelog): say which scope decides updatePolicy and autoCompact…
feat(scoring, dashboard): let ~/.teamai/config.yaml limit session jud…