Skip to content

[feat] Read every member setting in one order: environment, this scope, this machine, the team #1459

Closed

Description

@SaulMoro

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 config

What 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

Acceptance

  • In a workspace-only install, ~/.teamai/config.yaml holding only ai: { tool: codex } makes import --dir, recall promote, and a workflow start --driver local prompt step with no workflows.tools entry run on codex. status, doctor and pull print no Invalid local config and 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-mr in a bare checkout run on codex (the one AI command that runs without teamai init).
  • With ai.tool: claude in that workspace's config as well, those run on claude there and on codex in another workspace.
  • With user and workspace installs, recallEnabled: true in ~/.teamai/config.yaml turns recall on in a workspace whose config does not set it. recall disable in that workspace writes recallEnabled: false to its config, and a later pull keeps recall off.
  • A save by any other command leaves the workspace config without the machine's settings.
  • codexTrustEnabled: false in a settings-only machine file stops init of a new workspace from trusting its Codex hooks.
  • recall disable in a workspace whose machine file already says recallEnabled: false still writes the key to the workspace config.
  • codexTrustEnabled: false in a settings-only machine file stops init --http from trusting the local agent's Codex hooks.
  • A bad value at level 3 is warned about, and the next level decides. A bad workflows.allow value fails the workflow command.
  • init --scope user over a settings-only file, with no TTY and no --force, installs user scope and keeps the file's member settings.
  • doctor lists each member setting with its level, file and the levels below. With usageReportEnabled: false only in the machine file, its usage-reporting note names the machine file.
  • A key added to LocalConfigSchema fails 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.yaml and "local config" before the PR.
  • CHANGELOG.md, ### Changed: member settings in ~/.teamai/config.yaml now apply in every workspace and self scope whose own config does not set them, also for workspaces of other teams. updatePolicy and autoCompactTokens follow the deciding-scope rule above. A member with a user install who set one there, for example recallEnabled, coAuthorEnabled, usageReportEnabled or workflows.*, 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's Member settings and one AI call on the machine's tool; then the workspace override; then recall disable and a pull.

Activity

  1. SaulMoro commented on Oct 27, 2026

    @SaulMoro
    CollaboratorAuthor

    Suggested order, one PR each, so every step is usable and verifiable on its own:

    1. A settings-only machine file is valid. The one install check (repo key in ~/.teamai/config.yaml) replaces the nine file-existence checks, and the loaders stop printing Invalid local config for a file without repo. Done when a workspace-only install with a settings-only ~/.teamai/config.yaml runs status, doctor and pull with no config error, and init --scope user over that file keeps its settings.
    2. The overlay. Declare the member settings once, apply level 3 below a workspace or self scope where the role assignment is applied today, keep saves from writing machine values, and add the classification test. Start from the ai.tool resolver 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 for recallEnabled, codexTrustEnabled and the bad-value warning.
    3. Which level won. doctor and doctor --json list each member setting with its level, and the existing texts that name a file for a value name the level instead.
    4. Docs and CHANGELOG from the Definition of done, if not already carried by the steps above.

    #1456 is independent and can land before or after any of these.

  2. SaulMoro commented on Oct 27, 2026

    @SaulMoro
    CollaboratorAuthor

    Status after #1444 (merged, edebc6c):

    • ai.tool is read per scope: TEAMAI_AI_TOOL, then the scope config, then ~/.teamai/config.yaml, through aiToolSettings (src/config.ts). callTool and both workflow paths use it. The body now marks that row and those readers done.
    • Step 1 of the suggested order is partly done. hasUserScopeInstall is the one install check, and a machine file holding only ai is no install and prints no config error. What is left is widening its rule from "only member-setting keys" (MEMBER_SETTING_KEYS, today ai) to "no repo key", plus init --scope user keeping that file's settings.
  3. 66 remaining items

  4. SaulMoro commented on Oct 28, 2026

    @SaulMoro
    CollaboratorAuthor

    This was generated by AI during triage.

    Two leftovers after #1477 and #1483.

    sessions is a member setting, scoring is an install key. #1483 made sessions a member setting, so a workspace takes the machine's value. scoring, whose scoring.sessions turns the same member's session judging on or off per mode, stays in LOCAL_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 scoring out 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 machine scoring.sessions: false (or daily_limit: 0) applies below any scope that does not set it, while on/auto stays per scope. Otherwise the docs should say that scoring must be set in each workspace.

    Declared once. The key-ownership test (src/__tests__/types.test.ts:86-97) now fails when a LocalConfigSchema key is in neither list, as the body asks. The value schemas are still written twice: recallEnabled, codexTrustEnabled and modelAliases appear in both MemberSettingsSchema (src/types.ts:843, :849, :851) and LocalConfigSchema (:926, :947, :985), with nothing that fails if one changes and the other does not. Composing MemberSettingsSchema.shape into LocalConfigSchema would remove the second copy.

    Checked on main at 299ce8e.

  5. added a commit that references this issue on Oct 29, 2026
    d33a911
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions