| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
docs(config): clarify unreadable project scope fallback (#934) Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> | 21 小时前 | |
ci: add informational code-erosion (slop metrics) workflow (#588) * ci: add informational code-erosion (slop metrics) workflow Report SlopCodeBench verbosity/erosion metrics on every PR using the official scb-check tool, pinned to 0.2.0 (the first release with TypeScript support; 0.1.3 is Python-only). The workflow is informational and never blocks a merge: scb-check's exit code is swallowed, and the numbers are posted as a deduplicated PR comment with the run's job summary as a fallback (so fork PRs, whose token is read-only, still surface the report). Tests are excluded via scb-check.toml so metrics reflect the product surface. On TypeScript the ast-grep verbosity rule component is Python-only and contributes 0, so verbosity reflects clone + wrapper detection only; erosion is fully faithful. This caveat is documented in the bilingual docs/ci-code-erosion.{md,zh-CN.md}. * ci(code-erosion): add independent TS verbosity rule layer scb-check only runs its ast-grep rules on Python files, so on this TypeScript repo its verbosity rule component is always 0. This adds a standalone ast-grep pass with a small, hand-ported rule set to fill that gap, reported as a separate "Rule hits (TS verbosity layer)" section in the same non-blocking PR comment. Only purely structural rules are ported. Rules that hinge on truthiness or type semantics (len==0, ==True, redundant template strings) were tried and deliberately dropped: they are false positives in TypeScript, where arr.length>0 is idiomatic and x!==true is not equivalent to x===false (TS has undefined). Ported rules verified against src/ for false positives: unnecessary-else-after-return, empty-catch-block, redundant-ternary-same, if-return-boolean-literal, return-ternary-boolean-literal, duplicated-if-condition, self-assignment. Rules use severity: hint and the scan step has `|| true`, so the layer never blocks CI. Bilingual docs updated with the honest scope: this is an extra signal, not a reproduction of the paper's verbosity number. * ci(code-erosion): slim down the PR comment, defer detail to docs The comment carried long inline explanations (verbosity footnote, rule-layer paragraph). Move the prose to docs/ci-code-erosion.md and keep the comment to numbers plus a one-line pointer. Also replace the ambiguous "(informational)" tag with plain "never blocks the merge". * ci(code-erosion): drop the two repeated doc links in the comment The top line already points to docs/ci-code-erosion.md; the verbosity footnote and rule-layer note repeated the same link. Keep one pointer. | 15 天前 | |
ci: add informational code-erosion (slop metrics) workflow (#588) * ci: add informational code-erosion (slop metrics) workflow Report SlopCodeBench verbosity/erosion metrics on every PR using the official scb-check tool, pinned to 0.2.0 (the first release with TypeScript support; 0.1.3 is Python-only). The workflow is informational and never blocks a merge: scb-check's exit code is swallowed, and the numbers are posted as a deduplicated PR comment with the run's job summary as a fallback (so fork PRs, whose token is read-only, still surface the report). Tests are excluded via scb-check.toml so metrics reflect the product surface. On TypeScript the ast-grep verbosity rule component is Python-only and contributes 0, so verbosity reflects clone + wrapper detection only; erosion is fully faithful. This caveat is documented in the bilingual docs/ci-code-erosion.{md,zh-CN.md}. * ci(code-erosion): add independent TS verbosity rule layer scb-check only runs its ast-grep rules on Python files, so on this TypeScript repo its verbosity rule component is always 0. This adds a standalone ast-grep pass with a small, hand-ported rule set to fill that gap, reported as a separate "Rule hits (TS verbosity layer)" section in the same non-blocking PR comment. Only purely structural rules are ported. Rules that hinge on truthiness or type semantics (len==0, ==True, redundant template strings) were tried and deliberately dropped: they are false positives in TypeScript, where arr.length>0 is idiomatic and x!==true is not equivalent to x===false (TS has undefined). Ported rules verified against src/ for false positives: unnecessary-else-after-return, empty-catch-block, redundant-ternary-same, if-return-boolean-literal, return-ternary-boolean-literal, duplicated-if-condition, self-assignment. Rules use severity: hint and the scan step has `|| true`, so the layer never blocks CI. Bilingual docs updated with the honest scope: this is an extra signal, not a reproduction of the paper's verbosity number. * ci(code-erosion): slim down the PR comment, defer detail to docs The comment carried long inline explanations (verbosity footnote, rule-layer paragraph). Move the prose to docs/ci-code-erosion.md and keep the comment to numbers plus a one-line pointer. Also replace the ambiguous "(informational)" tag with plain "never blocks the merge". * ci(code-erosion): drop the two repeated doc links in the comment The top line already points to docs/ci-code-erosion.md; the verbosity footnote and rule-layer note repeated the same link. Keep one pointer. | 15 天前 | |
fix(source): preserve pending config during pull (#340) Keep source add/remove edits across usage-report cleanup so the next pull can deploy subscribed skills. Co-authored-by: Cursor <cursoragent@cursor.com> | 1 个月前 | |
feat(env): team-declared secrets with member-local values (#875) (#880) * refactor(entries): let a namespaced entry reader declare its own layout (#879) An entry reader took its directory, file name, activation key and failure wording from its EntryType. It can now declare them as an EntryLayout, defaulting to entryLayout(type), which gives today's values. This lets a later reader read env/secrets.yaml and env/<ns>/secrets.yaml activated by resources.env. No behaviour change: env, hooks, MCP and models resolve and report as before. Part of #875. * refactor(models): share the team values path and stdin reader with a second store getTeamValuesPath takes the store directory (defaulting to models/teams) and keeps its <team>-<hash>.json naming. The piped-stdin reader moves to utils/prompt.ts as readStdin; the --api-key-stdin checks and messages stay in the models command. No behaviour change. Refs #879 (S2), #875 * feat(env): declare team secrets in env/secrets.yaml and show their state (#879) A team repo can declare the secrets its members need, with no value, in env/secrets.yaml and env/<ns>/secrets.yaml (key, optional description and url). They resolve like env.yaml: active through resources.env, a namespace entry replaces the root entry with the same key. The declarations are absent, valid or failed; a broken file fails the secrets only, is reported in secret wording by pull, env list and doctor, and env variables are still delivered. - env list and list env show each declared secret as environment or missing, never its value, --reveal included. - doctor fails "Team secrets can be resolved" on a broken file, and its notes name env/secrets.yaml, not env/env.yaml, for an override or a key repeated in legacy mode (describeEntryNotes takes the reader's layout). - push lists a changed secrets.yaml, in single-repo mode too. - docs/designs/team-secrets.md and .zh-CN.md start here, with the #818 boundary; usage guide, product overview, multi-project, management backend and the admin reference updated. Part of #875. * feat(env): declare a team secret with env add --secret (#879) env add <key> [value] --secret [-d] [--url] [--role|--project] writes env/secrets.yaml or env/<ns>/secrets.yaml with no value; a value is rejected and never printed. env remove removes a declared secret when env.yaml does not set the key, and --secret removes only the declaration for a key both files carry. entryNamespaceFromFlags takes a layout so the --role warning names secrets.yaml. * feat(mcp): keep the entry an earlier pull wrote when a declared secret is missing (#879) The session-start pull inherits the agent's environment, which often lacks the member's shell export, so it removed the MCP entry the interactive pull had written. A server whose only missing variables are declared secrets now keeps its entry and ownership record; it is removed when it leaves mcp.yaml or by removeAll. A failed secrets declaration keeps managed MCP state. * feat(env): keep a member's value for a team secret and resolve it in MCP servers (#879) teamai env set KEY (hidden prompt, --stdin, --from-env VAR) and env unset KEY store a member's value per team repo in ~/.teamai/secrets/teams/, 0600, accepting only keys the scope declares as secrets. ${VAR} in MCP servers resolves a declared secret from that value, then from the member's own environment, which leaves out values a teamai env.sh exported (Conflict 10). A key declared as a secret and set in env.yaml resolves as the secret: its repo value leaves env.sh, the env backup, both list renderers and doctor's expected set (Conflict 13). A failed declaration leaves env.sh and the backup as they are (Conflict 14). env list shows team. * docs(env): document team secret values, storage and MCP resolution (#879) * feat(env): set one value for a team secret for every team on the machine (#879) env set/unset --global keep the value in ~/.teamai/secrets/machine.json. Resolution becomes team value > machine value > the member's environment, for MCP servers and env list (state `global`). In a scope --global still accepts only a declared secret; outside any scope it accepts any valid key and notes that no team declares it yet. * docs(env): document the machine value for team secrets (#879) * feat(mcp): keep project MCP configs with resolved values out of git (#882) A project-scope MCP config that carries a resolved ${VAR} sat untracked and unignored in the business repo, one `git add -A` from committing the token. After the reconcile writes such a file and git would track it, teamai lists its path in the clone's .git/info/exclude inside a marked block (resolved via `git rev-parse --git-path`, so linked worktrees and submodules work). The committed .gitignore is never touched; an ignored path or a config with no resolved value adds nothing; dry runs write nothing. Project-scope uninstall removes only teamai's block, and doctor reports such a file git would still commit. The hook sits after the appliers in reconcileMcpForConfig, outside desiredMcpForTarget/applyJson/applyCodex, so it merges cleanly with #880. * feat(env): name a missing team secret and the command that sets it (#879) Interactive pull, mcp list, env list and doctor print one line per declared secret with no value, naming the MCP servers that use it, `teamai env set KEY` and the declared url. doctor prints it as a note and no longer fails the MCP delivery check for a server skipped only for a missing declared secret. Pull and doctor also note a kept entry that may hold an old value and a key declared as a secret and set in env.yaml. The silent pull prints nothing. The lines come from one envAdvisories() result that later pull notices extend. * docs(env): document the missing-secret advisory in pull, doctor and the lists (#879) * fix(uninstall): count the .git/info/exclude block in the removal plan (#882) The plan now records whether the project's .git/info/exclude holds teamai's MCP config block (gitExcludeBlock). It counts toward isPlanEmpty, is listed in the summary and dry run, and gates the removal, so a plan whose only teamai leftover is the block removes it instead of reporting "Nothing to uninstall". * feat(env): run a command with the directory's team env and secrets via env exec (#879) * docs(env): document env exec, what the command can reach and the ANTHROPIC_* caveat (#879) * fix(tests): isolate Claude config dir from model tests (cherry picked from commit e567ac31e57933a93d8c74b9bcd7d7f899296dd5) * fix(env): warn when env add --secret updates a declaration an unknown key keeps undeclared (#879) * feat(env): tell agents at session start which team secrets exist and to run their CLIs through env exec (#879) * docs(env): teach agents to use team secrets through env exec and leave values to the member (#879) * feat(env): resolve plain env variables in one order, with a member override (#879) The environment no longer overrides an env.yaml variable in MCP servers, env exec or env.sh. A member sets their value for a team with `teamai env set KEY`; an interactive pull and doctor say when an export differs and is ignored. env.sh exports a literal override and leaves out a --from-env one; doctor's env delivery check expects the same. * docs(env): document the variable order and the member override (#879) * fix(mcp): keep the exclude block until every MCP config is clean (#882) - uninstall keeps a repository's .git/info/exclude block while a config in it could not be parsed and still holds teamai servers, and warns - uninstall finds and removes the block in nested repositories holding an MCP config, across every worktree - the block opens at the last start marker, so an orphaned start never pairs with a later block's end and takes the member's lines - doctor counts only servers the ownership manifest records, not a member's own server under a team name * fix(env): discount an old env.sh export in every later command (#879) A shell opened before a pull keeps the values env.sh exported then. Only the process that rewrote env.sh discounted them, so the next command read the team's old token as the member's own (Conflict 10). Each env.sh now keeps env.sh.exports.json beside it: SHA-256 of KEY=VALUE for the last 20 values per key, mode 0600, never a value. memberEnvironment discounts any recorded export, which replaces the in-memory snapshot. * fix(uninstall): remove the exclude block only once its files are proven clean (#882) A missing or unreadable managed-mcp.json made the MCP cleanup return early without reporting anything, so uninstall removed the block while .mcp.json still held the resolved token. Uninstall now inspects every path the block protects after the cleanup. The block goes only when each one is missing, or parses and holds none of the team's servers that need a resolved ${VAR}. Anything it cannot check keeps the block, with a warning naming the file. This replaces the leftInPlace report from the reconcile, which the check subsumes. * fix(mcp): protect every project MCP config holding a resolved value (#882) Pull, doctor and uninstall each skipped a case they had not inspected and treated it as safe. Now: - pull lists a config in .git/info/exclude whether or not it delivered to it this run: a disabled or undetected tool's file, a team with automatic delivery off, an unreadable mcp.yaml (any teamai entry counts), a failed write to another tool's config, and a lost ownership manifest (the resolved value found in the file) - doctor checks the same files, including one that does not parse, and counts a git error as a failure - git check-ignore failing inside a repository is no longer read as "not tracked": the path is excluded anyway, or teamai warns with git's error - uninstall also keeps the block while a file contains the value (8+ characters, not a path or the login name) of a variable still set in the environment, which finds a server since dropped from mcp.yaml * refactor(env): resolve a scope's env once per command (#879) resolveTeamEnv (src/env-resolution.ts) reads env.yaml, secrets.yaml, both value stores and the env.sh exports once. buildVarTable, the MCP reconcile, envAdvisories, env exec, doctor and pull take that result: an interactive pull resolves once per scope in its env stage and reuses it for MCP and the advisories (was up to four secrets.yaml reads). - Variable and secret values move out of resources/secrets.ts, which now holds declarations only (R3); one StoreResolution<V> and storeEntry (R2); resolveSecretDeclarations takes { active } instead of a tri-state array (R4); reportMissingSecrets lives in env-advisories (X1). - ENV_KEY_RE moves to resources/env-key.ts, so env.ts imports SECRETS_LAYOUT statically (EV1). - env list and `teamai list env` print one listing (env-listing.ts); a variable shows the value it resolves to and its source, team or env.yaml, like a secret (S1). - A failed declaration is never "no secrets": the listings show no variable value then, --reveal included, and a server skipped for a missing variable is kept (S2). - SecretState gains `unreadable` for a store that can't be read, handled with a never check (R1). * fix(mcp): mcp list reports a broken secrets.yaml and exits 1 (#879) A failed declaration read as no secrets, so every server variable came from the raw environment, another team's token included, and mcp list said 'all set' while pull kept MCP frozen. It now names the file, exits 1, and shows a server's variables as 'not resolved' (Conflict 14, C3). * fix(env): env commands outside a scope say so and exit 1 (#879) env list, add, remove, set and unset threw NotInitializedError with a stack trace outside any teamai scope. They now print its message and exit 1; env set/unset --global still work there. * fix(env): env exec refuses a command without -- before it (#879) Commander drops --, so `teamai env exec gh pr list --dry-run` read the command's flag as teamai's and ran nothing. env exec now takes what was typed after `exec`, and without -- before the command it says "Put -- before the command: teamai env exec -- <command>" and exits 2. teamai's own options may still come before --. * feat(doctor): check that the member's secret values can be read (#879) While teams/<team>.json or machine.json can't be read, every secret has no value and MCP keeps what the last pull wrote, yet the MCP check passed and only a pull warning said why. doctor now fails 'Your team secret values can be read' with the reason, for a scope whose secrets or variables read those files. * fix(env): name the unset variable a --from-env secret reads (#879) The missing-secret line told the member to run `teamai env set KEY`, which replaces the reference they chose. For a secret whose deciding entry reads an unset variable it now says: KEY reads VAR, which is not set. Set VAR, or run `teamai env set KEY [--global]` to replace the reference. * test(mcp): env unset keeps the entry, still owned, until a new value (#879) env set, pull, env unset with nothing exported, pull: the entry keeps the old value and the manifest still claims it, so the next value replaces it instead of colliding with a user-owned server. * refactor(env): expected env set/remove failures as values, one wording (#879) - secretInput returns a tagged result; an unexpected stdin or prompt failure propagates instead of printing as a user error (E1). - Every error path in env-commands uses fail() and invalidKeyMessage(), so each exits 1, env add's invalid key included (E2). - removeSecret returns removed | absent | reported: env remove of a variable env.yaml lacks still says it is not found next to a broken secrets.yaml (E3). - env add branches on --secret, not on a missing value (E4). - env unset with declarations that can't be read no longer calls the key a variable (E5). - "Secret is not declared" names the file and the next step (E6). - Messages call the machine store "global value (every team on this machine)", matching --global and the env list label (R5). * refactor(entries): EntryFailure always carries its layout (#879) An optional layout fell back to the type's, so a failure site that forgot it would word a broken secrets.yaml as env's. activeEntryNamespaces now takes the layout and every failure sets it (N1). * refactor(doctor): name entry resolution checks with their layout (#879) The check names were looked up by the layout's message label, so a wording change to a label would silently drop its doctor check. Each entry set now carries its check name (DD1). * refactor(secrets): report an unparsable value store by path only (#879) - Drop src/utils/json-position.ts, a second JSON grammar kept only to print a line and column: a parse error now reports the path alone, which is what the spec asks (never the input), and a disagreement with JSON.parse can no longer leak the parser's quote (SS3). - The unreadable-store error names what to check and the way out, and reads the error code without a cast (SS1). - SecretStore is inferred from its schema (SS2). - The design doc says an unreadable store leaves secrets 'unreadable'. * test(secrets): typed fixtures instead of casts in the new tests (#879) The LocalConfig and TeamaiConfig fixtures in env-exec, mcp-secrets and secret-values are now checked against the types, so a new required field breaks them instead of testing a shape production never has (T1). * docs(env): env set takes variables, listings show sources, exec needs -- (#879) - Usage guide: `env set` accepts an env.yaml variable without --global, not only a declared secret (D1/C2); `env exec` links the resolution order instead of "the order above", which the section never gave (C4). - team-secrets.md intro states the variable order; a --from-env variable override leaves the key out of new shells entirely. - Document the shared listing (variable source, `unreadable`, no values while the declarations fail), `mcp list`'s `not resolved`, the doctor check for an unreadable values file, and `env exec`'s exit 2 without --. * fix(env): refuse env set/unset/list when the project config can't be read (#879) Detection returned null for an unreadable project config, so env set fell back to the user scope and stored the value for that team. Report the file and why, exit 1, and write nothing, as pull and env exec do. * fix(env-exec): exit 128 + signal for a command killed by SIGPIPE or SIGUSR1 (#879) Re-raising SIGPIPE on teamai does nothing (Node ignores it) and SIGUSR1 starts the inspector, so teamai exited 0. Set the shell's exit code first and re-raise only the signals Node ends on. * fix(env-exec): don't send the command a second SIGINT on Ctrl-C (#879) The terminal sends Ctrl-C and Ctrl-\ to the whole foreground process group, so the command already has them; forwarding sent a second SIGINT, which tools such as terraform take as force-quit. Ignore SIGINT and SIGQUIT while the command runs and keep forwarding SIGTERM and SIGHUP. * docs(env): exec signal handling and env set on an unreadable project config (#879) * fix(mcp): judge MCP configs by disk and manifest, not current config (#882) Pull, doctor and uninstall still decided "clean" from the current team config in places. Now one function, resolvedValueEvidence, decides for all three: - a teamai-owned entry still in the file counts when its server has left mcp.yaml, as well as when it needs a resolved ${VAR} or mcp.yaml cannot be read (doctor no longer skips that case) - targets include the built-in location of a tool the team dropped from toolPaths or moved - doctor names a file two tools share once - exclude updates take the existing acquireLock helper, re-read the file and write it atomically, so concurrent commands keep each other's paths - uninstall inspects every worktree of each repository owning a block, including a nested repository's linked worktrees, and applies the manifest rule per worktree - the kept-block warning names each file and why, such as the variable whose value matched * fix(env): the env commands and env exec load config through --dry-run (#866) #866 threaded { dryRun } through the loaders pull, push, status and list use. The scope lookups this branch added did not take it, so `teamai --dry-run env list|set|unset|add|remove` and `env exec --dry-run` still persisted the legacy role migration, adopted a pre-#546 partition or ran the self-mode bootstrap. - resolveConfigForDir takes LoadOptions and passes them to both loaders. - scopeHere/requireScope (env commands) and commandEnvironment (env exec) forward options.dryRun. Without the flag nothing changes: env exec still runs the migrations every command runs (spec #879 Conflict 12). Five cases added to dry-run-load-path.test.ts; each failed before this change (config.yaml rewritten, partition renamed). * fix(secrets): name the team values file by the repo identity hash alone (#879) Renaming team: in teamai.yaml changed <team>-<hash>.json and orphaned every member's values. The secrets store now uses ~/.teamai/secrets/teams/<hash>.json; the models key files keep their names. The path has not shipped, so there is no migration. * fix(env): keep a __proto__ env key through the store, MCP and env exec (#879) ENV_KEY_RE accepts __proto__, but z.record dropped it from the value store, assigning it on an ordinary object hit the inherited setter, and reading it unset returned Object.prototype. The store, the MCP var table and the env exec overlay are now built without a prototype (envTable), and the member's environment and --from-env references are read as own keys (envValue). * fix(env): env list loads config with --dry-run always (#879) env list only reads, so like status and list since #866 it never migrates the config it loads, with or without --dry-run. The dry-run load-path test now runs env list without the flag, which is the case that used to migrate. * revert: drop the #890 cherry-pick from this branch 2e15c3f6 was picked only to protect the local Claude config while this branch's tests ran; #890 lands it on main on its own. * fix(env): env exec applies no team env while the declarations fail (#879) A legacy GITHUB_TOKEN in env.yaml plus a secrets.yaml that fails gave the command the repo value, though the key may be a declared secret. Like env.sh and the backup (Conflict 14), env exec now overlays nothing on a failed declaration: the command gets the inherited environment, and stderr names the failure. * fix(fs): create the atomic-write temp file with the target mode (#879) writeFileAtomic and writeJsonAtomic wrote the temp file with the umask's default mode (0644 under umask 022) and narrowed it by chmod afterwards, so a secret or model key was readable by other users until then. The temp file is now opened exclusively with the target mode; the chmod stays for the bits the umask removes. * fix(env): env set --dry-run previews without asking for the value (#879) `teamai --dry-run env set KEY` prompted for the value, or read stdin with --stdin, before printing the preview, so it failed without a terminal. The preview now comes first, and no value is read. * fix(env): serialize env set and env unset on the values file (#879) Two env set or env unset runs at once each read the store, changed it and wrote it back, so the later write dropped the other's change. Both now go through updateSecretStore, which takes <store>.lock (the acquireLock helper), re-reads the file inside it and writes the result. A --dry-run takes no lock. * fix(env): keep a secret's stored value from becoming a variable override (#879) Secrets and variable overrides share the team store. env set now records kind: secret | variable from what the scope declares; variable resolution uses only variable entries, secret resolution only secret ones, and an entry without kind counts as a secret. env list flags an entry of the other kind with the fix. * fix(mcp): write an MCP config that holds a resolved value 0600 (#879) writeJsonAtomic preserved an existing file's mode, so a 0644 .mcp.json or ~/.claude.json kept 0644 after teamai wrote a resolved secret into it. A config holding a resolved ${VAR} value (a kept entry included) is now written 0600; one without keeps its mode. * fix(mcp): create the Codex config temp file 0600 (#879) applyCodex wrote config.toml.<pid>.tmp with the umask's mode and chmodded it after, so a resolved secret was briefly readable at 0644. Both Codex writes now go through writeFileAtomic with mode 0600 (random temp name, created with the mode, symlinked targets written through). * docs(env): say env exec ignores a SIGINT or SIGQUIT sent to teamai alone (#879) * fix(mcp): tighten an unchanged config that holds a resolved value to 0600 (#879) * fix(env): mark what each env.sh exported so a value from one no scan finds is not the member's (#879) * fix(env): pass on a SIGINT or SIGQUIT sent to env exec outside the terminal's foreground (#879) * fix(env): keep marking what an env.sh exported before a rewrite dropped it (#879) * docs(env): say a SIGINT sent to a foreground env exec alone is not passed on (#879) * fix(secrets): name the team values file by the configured team repo URL, not teamai.yaml's repo: (#879) A copied or hostile team repo could claim another team's repo: and receive that team's stored values. The secrets file now hashes the URL from the member's own config, normalized so ssh, https and credentialed forms match. The models key store keeps its naming (#894). * fix(env): remove what a teamai env.sh exported from env exec while the declarations fail (#879) An invalid secrets.yaml left the inherited environment untouched, so a shell that had sourced an env.sh still passed the repo's GITHUB_TOKEN to the command. Every inherited value the member-environment rule discounts is now removed, named by key on stderr; the member's own exports stay. * fix(mcp): never write a declared secret into a project MCP config git tracks (#879) .git/info/exclude (#882) stops git add, not a file git already tracks. For such a file the server is skipped for that tool and an entry an earlier pull wrote stays as it is; pull warns, mcp list shows it as withheld and doctor fails the delivery check, each naming the file and git rm --cached. * fix(env): leave no duplicate declaration of a key env add --secret or env remove edits (#879) A key declared twice fails every read of secrets.yaml, and both commands edited only the first declaration. env add --secret now updates the first and removes the rest; env remove removes every one; both say how many. * test: keep the real normalizeRepoUrlForCompare in utils/git mocks the secrets store reaches (#879) * fix(env): keep the port in the URL that names a team's secrets file (#879) normalizeRepoUrlForCompare drops explicit ports, so two team repos on one host with different ports shared one values file and one team's secret reached the other. The file is now named by the URL's scheme family, lowercased host, non-default port and path; only credentials, the ssh user, a trailing .git and slashes are dropped. The scp form and the ssh URL of a repo still share a file; its ssh and https URLs no longer do. The utils/git mocks that kept the real normalizeRepoUrlForCompare for the store are no longer needed and are reverted. * fix(mcp): skip the .git/info/exclude write while another command holds its lock (#882) After the 2.5 s wait for the exclude file's lock, updateExclude wrote without it, so two writers could drop each other's pattern and leave a plaintext MCP config committable. It now writes nothing and reports 'locked': pull warns that the file is not excluded yet and to run `teamai pull` again (doctor's exclude check keeps reporting it meanwhile), and uninstall keeps the block and warns. * fix(uninstall): keep an exclude entry unless its MCP config is proven free of teamai's servers (#882) Uninstall judged a protected file clean from the current mcp.yaml, manifest and resolvable values, so with the manifest lost, the server gone from mcp.yaml and its value unset, a plaintext token looked like the member's own server and the exclusion went. It now fails closed and works per entry: a pattern goes only when its file is gone, holds no server, or holds none of teamai's servers with managed-mcp.json still there to say what teamai wrote. A kept entry is named with its file, why, and how to clean it by hand, since a rerun of uninstall finds no config after a full uninstall. * fix(env): keep http and https team repos in separate secrets files (#879) The store identity mapped https and http to one `http` family and dropped each default port, so `http://host/team.git` and `https://host/team.git` read one file: if the two endpoints serve different repos, one team got the other's stored secrets. The identity now keeps the scheme, still dropping 443 and 80; the ssh forms (`ssh://`, `git+ssh://`, `ssh+git://`, scp) stay one family. * fix(env): strip teamai env.sh exports in env exec when the project config can't be read (#879) With an unreadable project config, env exec passed the inherited environment unchanged, so a value another scope's env.sh exported (a legacy token, a member override) reached the command while the warning said no team values were applied. It now removes what the member-environment rule discounts (env.sh file, record or marker, the env.sh beside the unreadable config included), keeps the member's own exports, and names the removed keys, never values, as the failed-declaration path does. With no config at all the inherited environment is still passed as is. * fix(mcp): exclude a project MCP config from git before writing a resolved value into it (#882) Pull listed the file in .git/info/exclude only after writing the plaintext, and a failed exclusion only warned, so the secret-bearing file stayed eligible for git add -A. The exclusion now comes first; when it cannot be established (exclude file or .git/info not writable, lock held past the wait, file already tracked, git error) the file is left as it was and the warning names the reason and the fix. * fix(mcp): report a server withheld from a file git would commit in mcp list and doctor (#882) * fix(secrets): keep the ssh user in a team's values-file identity (#879) alice@host:team.git and bob@host:team.git can be different repos in each user's home; they no longer share one values file. The scp and ssh:// forms of one user, host, port and path still match; http(s) credentials are still dropped. * fix(env): overlay and remove env exec keys case-insensitively on Windows (#879) Windows environment names are case-insensitive, so a declared api_url left an inherited API_URL in place (Node keeps the first name of a case-folded pair) and a removed secret survived in another case. On win32 a key now replaces or removes every case variant; elsewhere nothing changes. * fix(mcp): report a tracked file on a dry run and a withheld server already installed (#882) Backports #880's merge 0d9f7fa7: a dry run (doctor, mcp list) names a tracked file before any pull has listed it, mcp list reports withheld for a server an earlier pull installed, and doctor's withheld note carries the exclusion's own fix instead of the pull --force advice. * fix(mcp): name a tracked MCP config before an unwritable .git/info/exclude (#882) A tracked file needs `git rm --cached` whatever else is wrong, so ensureExcludedFromGit checks gitTracks before the writability check, on a pull and a dry run alike, and lists nothing for it. * fix(mcp): take a project MCP config's exclude line back out once it holds no resolved value (#882) A pull that lists a config in .git/info/exclude and then writes no value into it (it does not parse, a member's server holds the team's name, the write fails) removes the line it added. After a pull or `teamai mcp remove`, a line whose configs are proven clean in every worktree, by the proof uninstall uses (moved to mcp-reconcile.ts), is removed under the lock; one not proven clean stays. A config listed before its write is listed again after it, so a concurrent uninstall that dropped the line between the check and the write does not leave the value unprotected. * fix(secrets): tell an scp path in the ssh user's home from an ssh:// path from the root (#879) git@host:acme/team is relative to the ssh user's home, ssh://git@host/acme/team is absolute; on a plain ssh host they can be different repos, yet they shared one values file. An scp path starting with neither / nor ~ is now keyed as ~/path, the path ssh://host/~/path names; host:/abs and ssh://host/abs still match. The scp form without a user (host:path) is now read as ssh too. * fix(mcp): judge a project MCP config by the manifest as it stood before the pull rewrote it (#882) A pull whose manifest was lost before it ran recreates managed-mcp.json while reconciling, so the clean-file proof read the new record and took the exclude line out of a file still holding a teamai server that left mcp.yaml with its variable unset. The proof now uses this worktree's manifest as read before the reconcile. * fix(mcp): log a rolled-back exclude line at debug level (#882) A line this pull added and took back out, because it wrote no resolved value into the file, was reported as removed although the member never saw it added. Only removing a line an earlier run added stays at info. * fix(mcp): keep a shared exclude line while another worktree's config holds a server (#882) A pull or `teamai mcp remove` judged every linked worktree's MCP config with today's definitions and values. Once a server's ${VAR} became a literal, and the value was no longer set, a pull in worktree A took worktree B's stale token-bearing entry for clean and removed the shared /.mcp.json line, so `git add -A` in B staged the token. These commands now release a line only when the current worktree's file passes the full proof and every other worktree's file is missing or holds no MCP server. `teamai uninstall` keeps its full proof in each worktree. * test(mcp): build the other worktree from the real temp path so the test checks what it names (#882) * fix(env): strip teamai env.sh exports from env exec with no config or an HTTP scope (#879) Neither path applies team values, yet both passed on whatever a sourced teamai env.sh exported, another team's credentials included. Remove every inherited value that is not the member's own, as the unreadable-config path does, and name the removed keys on stderr. * fix(secrets): name a team's values file by the full SHA-256 digest (#879) The file name is the boundary between teams, and 40 bits let a hostile team search for a repo URL whose name matches another team's file and read its values. Nothing has shipped, so no migration. * fix(env): name the env.sh provenance marker by the full SHA-256 of its data home (#879) * test(push): push publishes the secrets.yaml env add --secret leaves (#879) * fix(uninstall): list no worktrees for a project root that no longer exists (#882) buildRemovalPlan now lists every worktree to find teamai's exclude blocks, outside the MCP cleanup's try. simple-git throws synchronously for a missing directory, so a project uninstall whose root is gone crashed; main's #878 test caught it after the merge. * fix(secrets): keep the URL query and fragment in a team's values file name (#879) * fix(mcp): treat an empty, unparsable or tool-less managed-mcp.json as no record (#882) The clean-file proof took any managed-mcp.json on disk as teamai's record, so an empty or truncated one let a pull, `mcp remove` or uninstall judge a file still holding a stale secret-bearing entry clean and drop its exclude line. A file now counts as recorded only when the manifest parses and holds an entry for that tool's file. A project record teamai empties stays as [] so a file left with only the member's own servers can still be released. * fix(env): compare exported, recorded and marked keys case-insensitively on Windows (#879) * fix(uninstall): list no worktrees for a project root that no longer exists (#879) * fix(mcp): keep the exclude line of a config this pull wrote when a later step fails (#882) * fix(mcp): read a check-ignore error as unsafe unless ls-files proves the config untracked (#882) * fix(mcp): report withheld only for targets delivery would write the server to (#882) * docs(mcp): describe the .git/info/exclude block in the setup skill and the stricter git check (#882) * fix(mcp): keep the exclude line of an entry a pull wrote with a resolved value after its definition turns literal (#882) * fix(mcp): judge a nested repository's linked worktree config by its sibling's tool (#882) * feat(mcp): record the project MCP configs a pull wrote a resolved value to in managed-mcp-files.json (#882) * fix(mcp): keep protecting a config a pull wrote under a toolPaths mapping the team has since changed (#882) * fix(mcp): keep a config's exclude line past the pull that rebuilt its lost record (#882) * test(mcp): pin today's exclude rules for a missing, corrupt or locked managed-mcp-files.json (#882) * docs(mcp): describe managed-mcp-files.json and the configs it keeps protected (#882) * refactor(mcp): keep the #882 record edits off the lines #880 changes (#882) * test(mcp): keep excluding a config whose entry a pull kept for a missing declared secret (#875, #882) * fix(mcp): protect a config an older teamai wrote under a mapping an earlier teamai.yaml made (#882) A teamai from before managed-mcp-files.json kept no record of the path it wrote a resolved value to. Once the team changed that toolPaths mapping, no pull visited the file. The first pull on this version now reads every mcpProject path the team repo's history of teamai.yaml mapped, once per worktree: a file under the project root that no current mapping or record reaches, and that holds a resolved value, is listed in .git/info/exclude and recorded. A git error leaves the read for the next pull; a shallow clone reads the history it has. * fix(mcp): keep a rebuilt record from persisting without its note of the file's other servers (#882) When a pull rebuilt a lost managed-mcp.json and could not note the other servers in the file (managed-mcp-files.json locked, an I/O error), it still wrote the rebuilt record, so no later pull knew the record was rebuilt and a stale server's line could go. The same manifest write now marks those records unnoted: the file counts as having no record, so it keeps its line while it holds a server, and the next pull notes them and clears the mark. * fix(mcp): take back a managed-mcp-files.json record for a config the pull then did not write (#882) A pull records a config before writing a resolved value to it. When the write failed or did not happen (the file does not parse), the record stayed, and once the mapping changed a config of the member's own at that path was kept excluded while it held any server. The pull now takes back a record it added for a file it did not write, as it does the file's exclude line; the settle after records it again if the file holds a resolved value anyway. * docs(mcp): describe the teamai.yaml history read, the unnoted rebuilt record and the record a failed write takes back (#882) * refactor(mcp): keep the r3 edits off the lines #880 changes (#882) * fix(mcp): also protect a config an older teamai wrote under a built-in default it has since changed (#882) * fix(mcp): replace a symlinked Codex config instead of writing into the file it links to (#875, #882) * fix(env): drop what a teamai env.sh exported from env exec when env.yaml or the values file fails (#875) * fix(env): on Windows, match a declared secret to an env.yaml variable in any case (#875) * fix(mcp): judge a project MCP config under a symlinked directory where the write lands (#882) The appliers replace the file itself (tmp + rename) but follow its directories. Every git check now judges realFilePath(file), the one resolver the release keying already used: a directory linked out of any repository no longer withholds the servers on git's "not a git repository", and a tracked file there is named with both paths, with a git rm --cached that works (git refuses the path through the link). * docs(mcp): describe how a config under a symlinked directory is kept out of git (#882) * refactor(mcp): keep realFilePath next to existingAncestor, without an import cycle (#882) * refactor(mcp): key each worktree's targets with realFilePath, the one rule for where a write lands (#882) * fix(mcp): judge a config under an earlier teamai.yaml mapping as a recorded file, not by today's records (#882) * fix(mcp): have doctor check the configs earlier teamai.yaml mappings reach until a pull reads them (#882) * docs(mcp): describe how a config under an earlier teamai.yaml mapping is judged, and doctor's check of it (#882) * fix(mcp): record a config under an earlier teamai.yaml mapping that git tracks, and judge it once git no longer does (#882) * fix(mcp): keep judging a recorded config for a tool the team moved while another tool still maps it (#882) * docs(mcp): describe the tracked config an earlier mapping reached, and a moved tool's config another tool still maps (#882) * fix(mcp): find a config an older teamai wrote under an earlier mapping another tool maps today, and judge it by that tool's records (#882) * docs(mcp): describe the history read's configs another tool maps today (#882) * fix(mcp): prove a shared config clean only while every tool that wrote a resolved value there has its record (#882) * fix(env): on Windows, set and unset a key under the name the scope declares, in any case typed (#875) * fix(mcp): judge a built-in location no mapping reaches today as an earlier-mapped file, and a shared config no pull recorded by every tool mapping it (#882) * docs(mcp): describe the built-in location of a moved or dropped tool, and a shared config no pull recorded (#882) * fix(env): on Windows, match a secret's declaration and stored value in any case (#875) * fix(mcp): name the ignore rule that re-includes a config teamai just listed, instead of saying git tracks it (#882) * fix(mcp): judge a moved tool's built-in location another tool maps for that tool too, and hold a config's line while no managed-mcp.json claims its servers (#882) * docs(mcp): describe the no-manifest rule, a moved tool's built-in location another tool maps, and a re-including ignore rule (#882) * fix(env): key a team's values by its repo URL when the configured remote is only an alias (#875) * fix(env): on Windows, recognise a secret's env.yaml value under another case of its name in the environment (#875) * fix(mcp): on Windows, keep an inherited value under another case of a team variable's name out of MCP servers (#875) * fix(env): on Windows, replace and remove every stored entry under another case of the key (#875) * fix(mcp): keep a tool's record as it was when its config does not parse, and take back each tool a write that did not happen recorded (#882) * fix(mcp): mark the records a pull with no managed-mcp.json writes as unnoted until the servers no record claims are noted (#882) * fix(mcp): have doctor judge a record marked unnoted like a missing managed-mcp.json (#882) * fix(mcp): judge a config tools of different formats share in each of their formats before releasing its line (#882) * fix(mcp): note the servers no record claims in the file of a tool whose record a pull writes first, as with no managed-mcp.json at all (#882) * fix(mcp): take back a tool managed-mcp-files.json recorded before a write unless its own records hold a resolved value there (#882) * fix(mcp): treat an installed tool's missing record as lost at every pull and in doctor, not only an empty managed-mcp.json (#882) * fix(mcp): tighten to 0600 every project config the protection pass keeps out of git, not only the ones a pull writes (#879) * fix(mcp): on Windows, fill a placeholder from the variable of the same name in another case (#875) * fix(mcp): note the unclaimed servers under every format of a shared config, and pin an uninstalled tool's leftover config (#882) * fix(mcp): count an uninstalled tool's missing record when no installed tool maps its file, and name a re-including .gitignore rule on a dry run (#882) * fix(mcp): on Windows, match a placeholder to its secret in any case in mcp list and the missing-secret notice (#875) * fix(mcp): scope a shared config's claims to the tools reading the same key, and settle its notes on every format's view (#882) * fix(env): keep a file:// repo's .git suffix in its values file identity: team and team.git are two directories (#875) * fix(env): on Windows, update and remove an env.yaml variable typed in another case (#875) * fix(mcp): keep suspect an uninstalled tool managed-mcp-files.json lists as a writer, though an installed tool maps the file (#882) | 1 天前 | |
feat(env): team-declared secrets with member-local values (#875) (#880) * refactor(entries): let a namespaced entry reader declare its own layout (#879) An entry reader took its directory, file name, activation key and failure wording from its EntryType. It can now declare them as an EntryLayout, defaulting to entryLayout(type), which gives today's values. This lets a later reader read env/secrets.yaml and env/<ns>/secrets.yaml activated by resources.env. No behaviour change: env, hooks, MCP and models resolve and report as before. Part of #875. * refactor(models): share the team values path and stdin reader with a second store getTeamValuesPath takes the store directory (defaulting to models/teams) and keeps its <team>-<hash>.json naming. The piped-stdin reader moves to utils/prompt.ts as readStdin; the --api-key-stdin checks and messages stay in the models command. No behaviour change. Refs #879 (S2), #875 * feat(env): declare team secrets in env/secrets.yaml and show their state (#879) A team repo can declare the secrets its members need, with no value, in env/secrets.yaml and env/<ns>/secrets.yaml (key, optional description and url). They resolve like env.yaml: active through resources.env, a namespace entry replaces the root entry with the same key. The declarations are absent, valid or failed; a broken file fails the secrets only, is reported in secret wording by pull, env list and doctor, and env variables are still delivered. - env list and list env show each declared secret as environment or missing, never its value, --reveal included. - doctor fails "Team secrets can be resolved" on a broken file, and its notes name env/secrets.yaml, not env/env.yaml, for an override or a key repeated in legacy mode (describeEntryNotes takes the reader's layout). - push lists a changed secrets.yaml, in single-repo mode too. - docs/designs/team-secrets.md and .zh-CN.md start here, with the #818 boundary; usage guide, product overview, multi-project, management backend and the admin reference updated. Part of #875. * feat(env): declare a team secret with env add --secret (#879) env add <key> [value] --secret [-d] [--url] [--role|--project] writes env/secrets.yaml or env/<ns>/secrets.yaml with no value; a value is rejected and never printed. env remove removes a declared secret when env.yaml does not set the key, and --secret removes only the declaration for a key both files carry. entryNamespaceFromFlags takes a layout so the --role warning names secrets.yaml. * feat(mcp): keep the entry an earlier pull wrote when a declared secret is missing (#879) The session-start pull inherits the agent's environment, which often lacks the member's shell export, so it removed the MCP entry the interactive pull had written. A server whose only missing variables are declared secrets now keeps its entry and ownership record; it is removed when it leaves mcp.yaml or by removeAll. A failed secrets declaration keeps managed MCP state. * feat(env): keep a member's value for a team secret and resolve it in MCP servers (#879) teamai env set KEY (hidden prompt, --stdin, --from-env VAR) and env unset KEY store a member's value per team repo in ~/.teamai/secrets/teams/, 0600, accepting only keys the scope declares as secrets. ${VAR} in MCP servers resolves a declared secret from that value, then from the member's own environment, which leaves out values a teamai env.sh exported (Conflict 10). A key declared as a secret and set in env.yaml resolves as the secret: its repo value leaves env.sh, the env backup, both list renderers and doctor's expected set (Conflict 13). A failed declaration leaves env.sh and the backup as they are (Conflict 14). env list shows team. * docs(env): document team secret values, storage and MCP resolution (#879) * feat(env): set one value for a team secret for every team on the machine (#879) env set/unset --global keep the value in ~/.teamai/secrets/machine.json. Resolution becomes team value > machine value > the member's environment, for MCP servers and env list (state `global`). In a scope --global still accepts only a declared secret; outside any scope it accepts any valid key and notes that no team declares it yet. * docs(env): document the machine value for team secrets (#879) * feat(mcp): keep project MCP configs with resolved values out of git (#882) A project-scope MCP config that carries a resolved ${VAR} sat untracked and unignored in the business repo, one `git add -A` from committing the token. After the reconcile writes such a file and git would track it, teamai lists its path in the clone's .git/info/exclude inside a marked block (resolved via `git rev-parse --git-path`, so linked worktrees and submodules work). The committed .gitignore is never touched; an ignored path or a config with no resolved value adds nothing; dry runs write nothing. Project-scope uninstall removes only teamai's block, and doctor reports such a file git would still commit. The hook sits after the appliers in reconcileMcpForConfig, outside desiredMcpForTarget/applyJson/applyCodex, so it merges cleanly with #880. * feat(env): name a missing team secret and the command that sets it (#879) Interactive pull, mcp list, env list and doctor print one line per declared secret with no value, naming the MCP servers that use it, `teamai env set KEY` and the declared url. doctor prints it as a note and no longer fails the MCP delivery check for a server skipped only for a missing declared secret. Pull and doctor also note a kept entry that may hold an old value and a key declared as a secret and set in env.yaml. The silent pull prints nothing. The lines come from one envAdvisories() result that later pull notices extend. * docs(env): document the missing-secret advisory in pull, doctor and the lists (#879) * fix(uninstall): count the .git/info/exclude block in the removal plan (#882) The plan now records whether the project's .git/info/exclude holds teamai's MCP config block (gitExcludeBlock). It counts toward isPlanEmpty, is listed in the summary and dry run, and gates the removal, so a plan whose only teamai leftover is the block removes it instead of reporting "Nothing to uninstall". * feat(env): run a command with the directory's team env and secrets via env exec (#879) * docs(env): document env exec, what the command can reach and the ANTHROPIC_* caveat (#879) * fix(tests): isolate Claude config dir from model tests (cherry picked from commit e567ac31e57933a93d8c74b9bcd7d7f899296dd5) * fix(env): warn when env add --secret updates a declaration an unknown key keeps undeclared (#879) * feat(env): tell agents at session start which team secrets exist and to run their CLIs through env exec (#879) * docs(env): teach agents to use team secrets through env exec and leave values to the member (#879) * feat(env): resolve plain env variables in one order, with a member override (#879) The environment no longer overrides an env.yaml variable in MCP servers, env exec or env.sh. A member sets their value for a team with `teamai env set KEY`; an interactive pull and doctor say when an export differs and is ignored. env.sh exports a literal override and leaves out a --from-env one; doctor's env delivery check expects the same. * docs(env): document the variable order and the member override (#879) * fix(mcp): keep the exclude block until every MCP config is clean (#882) - uninstall keeps a repository's .git/info/exclude block while a config in it could not be parsed and still holds teamai servers, and warns - uninstall finds and removes the block in nested repositories holding an MCP config, across every worktree - the block opens at the last start marker, so an orphaned start never pairs with a later block's end and takes the member's lines - doctor counts only servers the ownership manifest records, not a member's own server under a team name * fix(env): discount an old env.sh export in every later command (#879) A shell opened before a pull keeps the values env.sh exported then. Only the process that rewrote env.sh discounted them, so the next command read the team's old token as the member's own (Conflict 10). Each env.sh now keeps env.sh.exports.json beside it: SHA-256 of KEY=VALUE for the last 20 values per key, mode 0600, never a value. memberEnvironment discounts any recorded export, which replaces the in-memory snapshot. * fix(uninstall): remove the exclude block only once its files are proven clean (#882) A missing or unreadable managed-mcp.json made the MCP cleanup return early without reporting anything, so uninstall removed the block while .mcp.json still held the resolved token. Uninstall now inspects every path the block protects after the cleanup. The block goes only when each one is missing, or parses and holds none of the team's servers that need a resolved ${VAR}. Anything it cannot check keeps the block, with a warning naming the file. This replaces the leftInPlace report from the reconcile, which the check subsumes. * fix(mcp): protect every project MCP config holding a resolved value (#882) Pull, doctor and uninstall each skipped a case they had not inspected and treated it as safe. Now: - pull lists a config in .git/info/exclude whether or not it delivered to it this run: a disabled or undetected tool's file, a team with automatic delivery off, an unreadable mcp.yaml (any teamai entry counts), a failed write to another tool's config, and a lost ownership manifest (the resolved value found in the file) - doctor checks the same files, including one that does not parse, and counts a git error as a failure - git check-ignore failing inside a repository is no longer read as "not tracked": the path is excluded anyway, or teamai warns with git's error - uninstall also keeps the block while a file contains the value (8+ characters, not a path or the login name) of a variable still set in the environment, which finds a server since dropped from mcp.yaml * refactor(env): resolve a scope's env once per command (#879) resolveTeamEnv (src/env-resolution.ts) reads env.yaml, secrets.yaml, both value stores and the env.sh exports once. buildVarTable, the MCP reconcile, envAdvisories, env exec, doctor and pull take that result: an interactive pull resolves once per scope in its env stage and reuses it for MCP and the advisories (was up to four secrets.yaml reads). - Variable and secret values move out of resources/secrets.ts, which now holds declarations only (R3); one StoreResolution<V> and storeEntry (R2); resolveSecretDeclarations takes { active } instead of a tri-state array (R4); reportMissingSecrets lives in env-advisories (X1). - ENV_KEY_RE moves to resources/env-key.ts, so env.ts imports SECRETS_LAYOUT statically (EV1). - env list and `teamai list env` print one listing (env-listing.ts); a variable shows the value it resolves to and its source, team or env.yaml, like a secret (S1). - A failed declaration is never "no secrets": the listings show no variable value then, --reveal included, and a server skipped for a missing variable is kept (S2). - SecretState gains `unreadable` for a store that can't be read, handled with a never check (R1). * fix(mcp): mcp list reports a broken secrets.yaml and exits 1 (#879) A failed declaration read as no secrets, so every server variable came from the raw environment, another team's token included, and mcp list said 'all set' while pull kept MCP frozen. It now names the file, exits 1, and shows a server's variables as 'not resolved' (Conflict 14, C3). * fix(env): env commands outside a scope say so and exit 1 (#879) env list, add, remove, set and unset threw NotInitializedError with a stack trace outside any teamai scope. They now print its message and exit 1; env set/unset --global still work there. * fix(env): env exec refuses a command without -- before it (#879) Commander drops --, so `teamai env exec gh pr list --dry-run` read the command's flag as teamai's and ran nothing. env exec now takes what was typed after `exec`, and without -- before the command it says "Put -- before the command: teamai env exec -- <command>" and exits 2. teamai's own options may still come before --. * feat(doctor): check that the member's secret values can be read (#879) While teams/<team>.json or machine.json can't be read, every secret has no value and MCP keeps what the last pull wrote, yet the MCP check passed and only a pull warning said why. doctor now fails 'Your team secret values can be read' with the reason, for a scope whose secrets or variables read those files. * fix(env): name the unset variable a --from-env secret reads (#879) The missing-secret line told the member to run `teamai env set KEY`, which replaces the reference they chose. For a secret whose deciding entry reads an unset variable it now says: KEY reads VAR, which is not set. Set VAR, or run `teamai env set KEY [--global]` to replace the reference. * test(mcp): env unset keeps the entry, still owned, until a new value (#879) env set, pull, env unset with nothing exported, pull: the entry keeps the old value and the manifest still claims it, so the next value replaces it instead of colliding with a user-owned server. * refactor(env): expected env set/remove failures as values, one wording (#879) - secretInput returns a tagged result; an unexpected stdin or prompt failure propagates instead of printing as a user error (E1). - Every error path in env-commands uses fail() and invalidKeyMessage(), so each exits 1, env add's invalid key included (E2). - removeSecret returns removed | absent | reported: env remove of a variable env.yaml lacks still says it is not found next to a broken secrets.yaml (E3). - env add branches on --secret, not on a missing value (E4). - env unset with declarations that can't be read no longer calls the key a variable (E5). - "Secret is not declared" names the file and the next step (E6). - Messages call the machine store "global value (every team on this machine)", matching --global and the env list label (R5). * refactor(entries): EntryFailure always carries its layout (#879) An optional layout fell back to the type's, so a failure site that forgot it would word a broken secrets.yaml as env's. activeEntryNamespaces now takes the layout and every failure sets it (N1). * refactor(doctor): name entry resolution checks with their layout (#879) The check names were looked up by the layout's message label, so a wording change to a label would silently drop its doctor check. Each entry set now carries its check name (DD1). * refactor(secrets): report an unparsable value store by path only (#879) - Drop src/utils/json-position.ts, a second JSON grammar kept only to print a line and column: a parse error now reports the path alone, which is what the spec asks (never the input), and a disagreement with JSON.parse can no longer leak the parser's quote (SS3). - The unreadable-store error names what to check and the way out, and reads the error code without a cast (SS1). - SecretStore is inferred from its schema (SS2). - The design doc says an unreadable store leaves secrets 'unreadable'. * test(secrets): typed fixtures instead of casts in the new tests (#879) The LocalConfig and TeamaiConfig fixtures in env-exec, mcp-secrets and secret-values are now checked against the types, so a new required field breaks them instead of testing a shape production never has (T1). * docs(env): env set takes variables, listings show sources, exec needs -- (#879) - Usage guide: `env set` accepts an env.yaml variable without --global, not only a declared secret (D1/C2); `env exec` links the resolution order instead of "the order above", which the section never gave (C4). - team-secrets.md intro states the variable order; a --from-env variable override leaves the key out of new shells entirely. - Document the shared listing (variable source, `unreadable`, no values while the declarations fail), `mcp list`'s `not resolved`, the doctor check for an unreadable values file, and `env exec`'s exit 2 without --. * fix(env): refuse env set/unset/list when the project config can't be read (#879) Detection returned null for an unreadable project config, so env set fell back to the user scope and stored the value for that team. Report the file and why, exit 1, and write nothing, as pull and env exec do. * fix(env-exec): exit 128 + signal for a command killed by SIGPIPE or SIGUSR1 (#879) Re-raising SIGPIPE on teamai does nothing (Node ignores it) and SIGUSR1 starts the inspector, so teamai exited 0. Set the shell's exit code first and re-raise only the signals Node ends on. * fix(env-exec): don't send the command a second SIGINT on Ctrl-C (#879) The terminal sends Ctrl-C and Ctrl-\ to the whole foreground process group, so the command already has them; forwarding sent a second SIGINT, which tools such as terraform take as force-quit. Ignore SIGINT and SIGQUIT while the command runs and keep forwarding SIGTERM and SIGHUP. * docs(env): exec signal handling and env set on an unreadable project config (#879) * fix(mcp): judge MCP configs by disk and manifest, not current config (#882) Pull, doctor and uninstall still decided "clean" from the current team config in places. Now one function, resolvedValueEvidence, decides for all three: - a teamai-owned entry still in the file counts when its server has left mcp.yaml, as well as when it needs a resolved ${VAR} or mcp.yaml cannot be read (doctor no longer skips that case) - targets include the built-in location of a tool the team dropped from toolPaths or moved - doctor names a file two tools share once - exclude updates take the existing acquireLock helper, re-read the file and write it atomically, so concurrent commands keep each other's paths - uninstall inspects every worktree of each repository owning a block, including a nested repository's linked worktrees, and applies the manifest rule per worktree - the kept-block warning names each file and why, such as the variable whose value matched * fix(env): the env commands and env exec load config through --dry-run (#866) #866 threaded { dryRun } through the loaders pull, push, status and list use. The scope lookups this branch added did not take it, so `teamai --dry-run env list|set|unset|add|remove` and `env exec --dry-run` still persisted the legacy role migration, adopted a pre-#546 partition or ran the self-mode bootstrap. - resolveConfigForDir takes LoadOptions and passes them to both loaders. - scopeHere/requireScope (env commands) and commandEnvironment (env exec) forward options.dryRun. Without the flag nothing changes: env exec still runs the migrations every command runs (spec #879 Conflict 12). Five cases added to dry-run-load-path.test.ts; each failed before this change (config.yaml rewritten, partition renamed). * fix(secrets): name the team values file by the repo identity hash alone (#879) Renaming team: in teamai.yaml changed <team>-<hash>.json and orphaned every member's values. The secrets store now uses ~/.teamai/secrets/teams/<hash>.json; the models key files keep their names. The path has not shipped, so there is no migration. * fix(env): keep a __proto__ env key through the store, MCP and env exec (#879) ENV_KEY_RE accepts __proto__, but z.record dropped it from the value store, assigning it on an ordinary object hit the inherited setter, and reading it unset returned Object.prototype. The store, the MCP var table and the env exec overlay are now built without a prototype (envTable), and the member's environment and --from-env references are read as own keys (envValue). * fix(env): env list loads config with --dry-run always (#879) env list only reads, so like status and list since #866 it never migrates the config it loads, with or without --dry-run. The dry-run load-path test now runs env list without the flag, which is the case that used to migrate. * revert: drop the #890 cherry-pick from this branch 2e15c3f6 was picked only to protect the local Claude config while this branch's tests ran; #890 lands it on main on its own. * fix(env): env exec applies no team env while the declarations fail (#879) A legacy GITHUB_TOKEN in env.yaml plus a secrets.yaml that fails gave the command the repo value, though the key may be a declared secret. Like env.sh and the backup (Conflict 14), env exec now overlays nothing on a failed declaration: the command gets the inherited environment, and stderr names the failure. * fix(fs): create the atomic-write temp file with the target mode (#879) writeFileAtomic and writeJsonAtomic wrote the temp file with the umask's default mode (0644 under umask 022) and narrowed it by chmod afterwards, so a secret or model key was readable by other users until then. The temp file is now opened exclusively with the target mode; the chmod stays for the bits the umask removes. * fix(env): env set --dry-run previews without asking for the value (#879) `teamai --dry-run env set KEY` prompted for the value, or read stdin with --stdin, before printing the preview, so it failed without a terminal. The preview now comes first, and no value is read. * fix(env): serialize env set and env unset on the values file (#879) Two env set or env unset runs at once each read the store, changed it and wrote it back, so the later write dropped the other's change. Both now go through updateSecretStore, which takes <store>.lock (the acquireLock helper), re-reads the file inside it and writes the result. A --dry-run takes no lock. * fix(env): keep a secret's stored value from becoming a variable override (#879) Secrets and variable overrides share the team store. env set now records kind: secret | variable from what the scope declares; variable resolution uses only variable entries, secret resolution only secret ones, and an entry without kind counts as a secret. env list flags an entry of the other kind with the fix. * fix(mcp): write an MCP config that holds a resolved value 0600 (#879) writeJsonAtomic preserved an existing file's mode, so a 0644 .mcp.json or ~/.claude.json kept 0644 after teamai wrote a resolved secret into it. A config holding a resolved ${VAR} value (a kept entry included) is now written 0600; one without keeps its mode. * fix(mcp): create the Codex config temp file 0600 (#879) applyCodex wrote config.toml.<pid>.tmp with the umask's mode and chmodded it after, so a resolved secret was briefly readable at 0644. Both Codex writes now go through writeFileAtomic with mode 0600 (random temp name, created with the mode, symlinked targets written through). * docs(env): say env exec ignores a SIGINT or SIGQUIT sent to teamai alone (#879) * fix(mcp): tighten an unchanged config that holds a resolved value to 0600 (#879) * fix(env): mark what each env.sh exported so a value from one no scan finds is not the member's (#879) * fix(env): pass on a SIGINT or SIGQUIT sent to env exec outside the terminal's foreground (#879) * fix(env): keep marking what an env.sh exported before a rewrite dropped it (#879) * docs(env): say a SIGINT sent to a foreground env exec alone is not passed on (#879) * fix(secrets): name the team values file by the configured team repo URL, not teamai.yaml's repo: (#879) A copied or hostile team repo could claim another team's repo: and receive that team's stored values. The secrets file now hashes the URL from the member's own config, normalized so ssh, https and credentialed forms match. The models key store keeps its naming (#894). * fix(env): remove what a teamai env.sh exported from env exec while the declarations fail (#879) An invalid secrets.yaml left the inherited environment untouched, so a shell that had sourced an env.sh still passed the repo's GITHUB_TOKEN to the command. Every inherited value the member-environment rule discounts is now removed, named by key on stderr; the member's own exports stay. * fix(mcp): never write a declared secret into a project MCP config git tracks (#879) .git/info/exclude (#882) stops git add, not a file git already tracks. For such a file the server is skipped for that tool and an entry an earlier pull wrote stays as it is; pull warns, mcp list shows it as withheld and doctor fails the delivery check, each naming the file and git rm --cached. * fix(env): leave no duplicate declaration of a key env add --secret or env remove edits (#879) A key declared twice fails every read of secrets.yaml, and both commands edited only the first declaration. env add --secret now updates the first and removes the rest; env remove removes every one; both say how many. * test: keep the real normalizeRepoUrlForCompare in utils/git mocks the secrets store reaches (#879) * fix(env): keep the port in the URL that names a team's secrets file (#879) normalizeRepoUrlForCompare drops explicit ports, so two team repos on one host with different ports shared one values file and one team's secret reached the other. The file is now named by the URL's scheme family, lowercased host, non-default port and path; only credentials, the ssh user, a trailing .git and slashes are dropped. The scp form and the ssh URL of a repo still share a file; its ssh and https URLs no longer do. The utils/git mocks that kept the real normalizeRepoUrlForCompare for the store are no longer needed and are reverted. * fix(mcp): skip the .git/info/exclude write while another command holds its lock (#882) After the 2.5 s wait for the exclude file's lock, updateExclude wrote without it, so two writers could drop each other's pattern and leave a plaintext MCP config committable. It now writes nothing and reports 'locked': pull warns that the file is not excluded yet and to run `teamai pull` again (doctor's exclude check keeps reporting it meanwhile), and uninstall keeps the block and warns. * fix(uninstall): keep an exclude entry unless its MCP config is proven free of teamai's servers (#882) Uninstall judged a protected file clean from the current mcp.yaml, manifest and resolvable values, so with the manifest lost, the server gone from mcp.yaml and its value unset, a plaintext token looked like the member's own server and the exclusion went. It now fails closed and works per entry: a pattern goes only when its file is gone, holds no server, or holds none of teamai's servers with managed-mcp.json still there to say what teamai wrote. A kept entry is named with its file, why, and how to clean it by hand, since a rerun of uninstall finds no config after a full uninstall. * fix(env): keep http and https team repos in separate secrets files (#879) The store identity mapped https and http to one `http` family and dropped each default port, so `http://host/team.git` and `https://host/team.git` read one file: if the two endpoints serve different repos, one team got the other's stored secrets. The identity now keeps the scheme, still dropping 443 and 80; the ssh forms (`ssh://`, `git+ssh://`, `ssh+git://`, scp) stay one family. * fix(env): strip teamai env.sh exports in env exec when the project config can't be read (#879) With an unreadable project config, env exec passed the inherited environment unchanged, so a value another scope's env.sh exported (a legacy token, a member override) reached the command while the warning said no team values were applied. It now removes what the member-environment rule discounts (env.sh file, record or marker, the env.sh beside the unreadable config included), keeps the member's own exports, and names the removed keys, never values, as the failed-declaration path does. With no config at all the inherited environment is still passed as is. * fix(mcp): exclude a project MCP config from git before writing a resolved value into it (#882) Pull listed the file in .git/info/exclude only after writing the plaintext, and a failed exclusion only warned, so the secret-bearing file stayed eligible for git add -A. The exclusion now comes first; when it cannot be established (exclude file or .git/info not writable, lock held past the wait, file already tracked, git error) the file is left as it was and the warning names the reason and the fix. * fix(mcp): report a server withheld from a file git would commit in mcp list and doctor (#882) * fix(secrets): keep the ssh user in a team's values-file identity (#879) alice@host:team.git and bob@host:team.git can be different repos in each user's home; they no longer share one values file. The scp and ssh:// forms of one user, host, port and path still match; http(s) credentials are still dropped. * fix(env): overlay and remove env exec keys case-insensitively on Windows (#879) Windows environment names are case-insensitive, so a declared api_url left an inherited API_URL in place (Node keeps the first name of a case-folded pair) and a removed secret survived in another case. On win32 a key now replaces or removes every case variant; elsewhere nothing changes. * fix(mcp): report a tracked file on a dry run and a withheld server already installed (#882) Backports #880's merge 0d9f7fa7: a dry run (doctor, mcp list) names a tracked file before any pull has listed it, mcp list reports withheld for a server an earlier pull installed, and doctor's withheld note carries the exclusion's own fix instead of the pull --force advice. * fix(mcp): name a tracked MCP config before an unwritable .git/info/exclude (#882) A tracked file needs `git rm --cached` whatever else is wrong, so ensureExcludedFromGit checks gitTracks before the writability check, on a pull and a dry run alike, and lists nothing for it. * fix(mcp): take a project MCP config's exclude line back out once it holds no resolved value (#882) A pull that lists a config in .git/info/exclude and then writes no value into it (it does not parse, a member's server holds the team's name, the write fails) removes the line it added. After a pull or `teamai mcp remove`, a line whose configs are proven clean in every worktree, by the proof uninstall uses (moved to mcp-reconcile.ts), is removed under the lock; one not proven clean stays. A config listed before its write is listed again after it, so a concurrent uninstall that dropped the line between the check and the write does not leave the value unprotected. * fix(secrets): tell an scp path in the ssh user's home from an ssh:// path from the root (#879) git@host:acme/team is relative to the ssh user's home, ssh://git@host/acme/team is absolute; on a plain ssh host they can be different repos, yet they shared one values file. An scp path starting with neither / nor ~ is now keyed as ~/path, the path ssh://host/~/path names; host:/abs and ssh://host/abs still match. The scp form without a user (host:path) is now read as ssh too. * fix(mcp): judge a project MCP config by the manifest as it stood before the pull rewrote it (#882) A pull whose manifest was lost before it ran recreates managed-mcp.json while reconciling, so the clean-file proof read the new record and took the exclude line out of a file still holding a teamai server that left mcp.yaml with its variable unset. The proof now uses this worktree's manifest as read before the reconcile. * fix(mcp): log a rolled-back exclude line at debug level (#882) A line this pull added and took back out, because it wrote no resolved value into the file, was reported as removed although the member never saw it added. Only removing a line an earlier run added stays at info. * fix(mcp): keep a shared exclude line while another worktree's config holds a server (#882) A pull or `teamai mcp remove` judged every linked worktree's MCP config with today's definitions and values. Once a server's ${VAR} became a literal, and the value was no longer set, a pull in worktree A took worktree B's stale token-bearing entry for clean and removed the shared /.mcp.json line, so `git add -A` in B staged the token. These commands now release a line only when the current worktree's file passes the full proof and every other worktree's file is missing or holds no MCP server. `teamai uninstall` keeps its full proof in each worktree. * test(mcp): build the other worktree from the real temp path so the test checks what it names (#882) * fix(env): strip teamai env.sh exports from env exec with no config or an HTTP scope (#879) Neither path applies team values, yet both passed on whatever a sourced teamai env.sh exported, another team's credentials included. Remove every inherited value that is not the member's own, as the unreadable-config path does, and name the removed keys on stderr. * fix(secrets): name a team's values file by the full SHA-256 digest (#879) The file name is the boundary between teams, and 40 bits let a hostile team search for a repo URL whose name matches another team's file and read its values. Nothing has shipped, so no migration. * fix(env): name the env.sh provenance marker by the full SHA-256 of its data home (#879) * test(push): push publishes the secrets.yaml env add --secret leaves (#879) * fix(uninstall): list no worktrees for a project root that no longer exists (#882) buildRemovalPlan now lists every worktree to find teamai's exclude blocks, outside the MCP cleanup's try. simple-git throws synchronously for a missing directory, so a project uninstall whose root is gone crashed; main's #878 test caught it after the merge. * fix(secrets): keep the URL query and fragment in a team's values file name (#879) * fix(mcp): treat an empty, unparsable or tool-less managed-mcp.json as no record (#882) The clean-file proof took any managed-mcp.json on disk as teamai's record, so an empty or truncated one let a pull, `mcp remove` or uninstall judge a file still holding a stale secret-bearing entry clean and drop its exclude line. A file now counts as recorded only when the manifest parses and holds an entry for that tool's file. A project record teamai empties stays as [] so a file left with only the member's own servers can still be released. * fix(env): compare exported, recorded and marked keys case-insensitively on Windows (#879) * fix(uninstall): list no worktrees for a project root that no longer exists (#879) * fix(mcp): keep the exclude line of a config this pull wrote when a later step fails (#882) * fix(mcp): read a check-ignore error as unsafe unless ls-files proves the config untracked (#882) * fix(mcp): report withheld only for targets delivery would write the server to (#882) * docs(mcp): describe the .git/info/exclude block in the setup skill and the stricter git check (#882) * fix(mcp): keep the exclude line of an entry a pull wrote with a resolved value after its definition turns literal (#882) * fix(mcp): judge a nested repository's linked worktree config by its sibling's tool (#882) * feat(mcp): record the project MCP configs a pull wrote a resolved value to in managed-mcp-files.json (#882) * fix(mcp): keep protecting a config a pull wrote under a toolPaths mapping the team has since changed (#882) * fix(mcp): keep a config's exclude line past the pull that rebuilt its lost record (#882) * test(mcp): pin today's exclude rules for a missing, corrupt or locked managed-mcp-files.json (#882) * docs(mcp): describe managed-mcp-files.json and the configs it keeps protected (#882) * refactor(mcp): keep the #882 record edits off the lines #880 changes (#882) * test(mcp): keep excluding a config whose entry a pull kept for a missing declared secret (#875, #882) * fix(mcp): protect a config an older teamai wrote under a mapping an earlier teamai.yaml made (#882) A teamai from before managed-mcp-files.json kept no record of the path it wrote a resolved value to. Once the team changed that toolPaths mapping, no pull visited the file. The first pull on this version now reads every mcpProject path the team repo's history of teamai.yaml mapped, once per worktree: a file under the project root that no current mapping or record reaches, and that holds a resolved value, is listed in .git/info/exclude and recorded. A git error leaves the read for the next pull; a shallow clone reads the history it has. * fix(mcp): keep a rebuilt record from persisting without its note of the file's other servers (#882) When a pull rebuilt a lost managed-mcp.json and could not note the other servers in the file (managed-mcp-files.json locked, an I/O error), it still wrote the rebuilt record, so no later pull knew the record was rebuilt and a stale server's line could go. The same manifest write now marks those records unnoted: the file counts as having no record, so it keeps its line while it holds a server, and the next pull notes them and clears the mark. * fix(mcp): take back a managed-mcp-files.json record for a config the pull then did not write (#882) A pull records a config before writing a resolved value to it. When the write failed or did not happen (the file does not parse), the record stayed, and once the mapping changed a config of the member's own at that path was kept excluded while it held any server. The pull now takes back a record it added for a file it did not write, as it does the file's exclude line; the settle after records it again if the file holds a resolved value anyway. * docs(mcp): describe the teamai.yaml history read, the unnoted rebuilt record and the record a failed write takes back (#882) * refactor(mcp): keep the r3 edits off the lines #880 changes (#882) * fix(mcp): also protect a config an older teamai wrote under a built-in default it has since changed (#882) * fix(mcp): replace a symlinked Codex config instead of writing into the file it links to (#875, #882) * fix(env): drop what a teamai env.sh exported from env exec when env.yaml or the values file fails (#875) * fix(env): on Windows, match a declared secret to an env.yaml variable in any case (#875) * fix(mcp): judge a project MCP config under a symlinked directory where the write lands (#882) The appliers replace the file itself (tmp + rename) but follow its directories. Every git check now judges realFilePath(file), the one resolver the release keying already used: a directory linked out of any repository no longer withholds the servers on git's "not a git repository", and a tracked file there is named with both paths, with a git rm --cached that works (git refuses the path through the link). * docs(mcp): describe how a config under a symlinked directory is kept out of git (#882) * refactor(mcp): keep realFilePath next to existingAncestor, without an import cycle (#882) * refactor(mcp): key each worktree's targets with realFilePath, the one rule for where a write lands (#882) * fix(mcp): judge a config under an earlier teamai.yaml mapping as a recorded file, not by today's records (#882) * fix(mcp): have doctor check the configs earlier teamai.yaml mappings reach until a pull reads them (#882) * docs(mcp): describe how a config under an earlier teamai.yaml mapping is judged, and doctor's check of it (#882) * fix(mcp): record a config under an earlier teamai.yaml mapping that git tracks, and judge it once git no longer does (#882) * fix(mcp): keep judging a recorded config for a tool the team moved while another tool still maps it (#882) * docs(mcp): describe the tracked config an earlier mapping reached, and a moved tool's config another tool still maps (#882) * fix(mcp): find a config an older teamai wrote under an earlier mapping another tool maps today, and judge it by that tool's records (#882) * docs(mcp): describe the history read's configs another tool maps today (#882) * fix(mcp): prove a shared config clean only while every tool that wrote a resolved value there has its record (#882) * fix(env): on Windows, set and unset a key under the name the scope declares, in any case typed (#875) * fix(mcp): judge a built-in location no mapping reaches today as an earlier-mapped file, and a shared config no pull recorded by every tool mapping it (#882) * docs(mcp): describe the built-in location of a moved or dropped tool, and a shared config no pull recorded (#882) * fix(env): on Windows, match a secret's declaration and stored value in any case (#875) * fix(mcp): name the ignore rule that re-includes a config teamai just listed, instead of saying git tracks it (#882) * fix(mcp): judge a moved tool's built-in location another tool maps for that tool too, and hold a config's line while no managed-mcp.json claims its servers (#882) * docs(mcp): describe the no-manifest rule, a moved tool's built-in location another tool maps, and a re-including ignore rule (#882) * fix(env): key a team's values by its repo URL when the configured remote is only an alias (#875) * fix(env): on Windows, recognise a secret's env.yaml value under another case of its name in the environment (#875) * fix(mcp): on Windows, keep an inherited value under another case of a team variable's name out of MCP servers (#875) * fix(env): on Windows, replace and remove every stored entry under another case of the key (#875) * fix(mcp): keep a tool's record as it was when its config does not parse, and take back each tool a write that did not happen recorded (#882) * fix(mcp): mark the records a pull with no managed-mcp.json writes as unnoted until the servers no record claims are noted (#882) * fix(mcp): have doctor judge a record marked unnoted like a missing managed-mcp.json (#882) * fix(mcp): judge a config tools of different formats share in each of their formats before releasing its line (#882) * fix(mcp): note the servers no record claims in the file of a tool whose record a pull writes first, as with no managed-mcp.json at all (#882) * fix(mcp): take back a tool managed-mcp-files.json recorded before a write unless its own records hold a resolved value there (#882) * fix(mcp): treat an installed tool's missing record as lost at every pull and in doctor, not only an empty managed-mcp.json (#882) * fix(mcp): tighten to 0600 every project config the protection pass keeps out of git, not only the ones a pull writes (#879) * fix(mcp): on Windows, fill a placeholder from the variable of the same name in another case (#875) * fix(mcp): note the unclaimed servers under every format of a shared config, and pin an uninstalled tool's leftover config (#882) * fix(mcp): count an uninstalled tool's missing record when no installed tool maps its file, and name a re-including .gitignore rule on a dry run (#882) * fix(mcp): on Windows, match a placeholder to its secret in any case in mcp list and the missing-secret notice (#875) * fix(mcp): scope a shared config's claims to the tools reading the same key, and settle its notes on every format's view (#882) * fix(env): keep a file:// repo's .git suffix in its values file identity: team and team.git are two directories (#875) * fix(env): on Windows, update and remove an env.yaml variable typed in another case (#875) * fix(mcp): keep suspect an uninstalled tool managed-mcp-files.json lists as a writer, though an installed tool maps the file (#882) | 1 天前 | |
feat(init): let a member choose the git provider with --provider (#789) (#844) * feat(init): let a member choose the git provider with --provider (#789) A member of a team on self-hosted GitLab had to configure GITLAB_TOKEN even when they only sync and never need the CLI to open merge requests. `teamai init <repo> --provider <name>` now uses the named provider instead of detecting one, and records it in the member's local config. PR/MR creation and doctor's provider checks prefer it over the team's teamai.yaml, which stays unchanged, so other members keep detection. With `git`, push pushes the branch and says the MR must be opened by hand, as it already does for a provider: git team repo. * fix(init): address review — guard --provider gitlab and keep --provider git out of teamai.yaml --provider gitlab on a host with no configured GitLab instance would send the token to gitlab.com (the API base defaults there); stop with a hint to set GITLAB_URL or use --provider git. A teamai.yaml that init creates now records the provider detected from the URL instead of a member's git override, matching the docs. * fix(init): address review — do not record git as the team provider on an unconfigured GitLab With --provider git, a teamai.yaml that init creates (empty team repo or first self-mode init) recorded detectProvider(url), which skips the self-hosted GitLab probe. On an unconfigured instance that wrote `provider: git` and cost every teammate automatic merge requests. Init now resolves the team provider as it would without the flag, including the probe, and stops with a GITLAB_URL hint when the probe finds GitLab. * fix(gitlab): address review — refuse a TEAMAI_GITLAB_HOST that disagrees with GITLAB_URL Repos on TEAMAI_GITLAB_HOST were detected as GitLab while the API base, token included, came from GITLAB_URL. Stop before any request when the two name different hosts, and let gitlabWhoami surface the configuration error instead of reporting a failed login. | 5 天前 | |
fix(config): honor CODEX_HOME through toolRoots (#942) * fix(config): honor CODEX_HOME through toolRoots Codex keeps its home in $CODEX_HOME, but teamai resolved every Codex path under ~/.codex, so a member who exports the variable got no skills, hooks, agents or MCP in the directory Codex reads, and doctor stayed green. Extend the toolRoots relocation built for CLAUDE_CONFIG_DIR (#725) to Codex. A RELOCATABLE_TOOLS table maps each tool to its variable; init records CODEX_HOME into toolRoots.codex, a re-init that moves the root releases the old hooks and managed MCP servers, and doctor adds a "Codex root matches CODEX_HOME" check. The two Codex writes outside toolPaths now follow the root: the co-author setting in config.toml and the skill-existence probe used by usage tracking. The doctor check compares the paths each root resolves to rather than requiring a record, so an unrecorded CODEX_HOME=~/.codex passes while an unrecorded CLAUDE_CONFIG_DIR=~/.claude still fails (it moves .claude.json). Closes #939 * fix(config): address review on CODEX_HOME relocation Derive the Codex co-author file from the scoped paths, list relocatable skill roots from RELOCATABLE_TOOLS, and skip the root-release warning for a tool the member does not sync or whose old root does not exist. * fix(coauthor): resolve the Codex config root, not the skills mapping A team mapping toolPaths.codex.skills to .agents/skills sent the Codex co-author setting to ~/.agents/config.toml. Resolve each Codex's own root (toolRoots.codex or ~/.codex, ~/.tcodex, ~/.codex-internal) instead. | 20 小时前 | |
fix(config): honor CODEX_HOME through toolRoots (#942) * fix(config): honor CODEX_HOME through toolRoots Codex keeps its home in $CODEX_HOME, but teamai resolved every Codex path under ~/.codex, so a member who exports the variable got no skills, hooks, agents or MCP in the directory Codex reads, and doctor stayed green. Extend the toolRoots relocation built for CLAUDE_CONFIG_DIR (#725) to Codex. A RELOCATABLE_TOOLS table maps each tool to its variable; init records CODEX_HOME into toolRoots.codex, a re-init that moves the root releases the old hooks and managed MCP servers, and doctor adds a "Codex root matches CODEX_HOME" check. The two Codex writes outside toolPaths now follow the root: the co-author setting in config.toml and the skill-existence probe used by usage tracking. The doctor check compares the paths each root resolves to rather than requiring a record, so an unrecorded CODEX_HOME=~/.codex passes while an unrecorded CLAUDE_CONFIG_DIR=~/.claude still fails (it moves .claude.json). Closes #939 * fix(config): address review on CODEX_HOME relocation Derive the Codex co-author file from the scoped paths, list relocatable skill roots from RELOCATABLE_TOOLS, and skip the root-release warning for a tool the member does not sync or whose old root does not exist. * fix(coauthor): resolve the Codex config root, not the skills mapping A team mapping toolPaths.codex.skills to .agents/skills sent the Codex co-author setting to ~/.agents/config.toml. Resolve each Codex's own root (toolRoots.codex or ~/.codex, ~/.tcodex, ~/.codex-internal) instead. | 20 小时前 | |
fix(hooks): give the detached hook child the bundled gits on PATH (#801) The session-start pull is spawned through the WMI service to escape the host's job object, and a WMI-created process inherits the provider's environment, not the caller's. On a machine with no system git - every WorkBuddy client - the PATH that ran `teamai` therefore never reaches the pull: bare-name `git` lookups (simple-git, providers, mr-hint) fail with `spawn git ENOENT`, the failure is reported only on the console spinner (a detached pull has no console), and the clone silently stops advancing while the postPull script - spawned by absolute path - keeps deploying a stale tree. Resolve the bundled git at startup instead, one resolver per host in the git counterpart of BUNDLED_SHELLS: a qualifying root's `<root>/cmd` goes on PATH first, with the msys dirs appended for the tools git itself shells out to. A machine that already resolves git is left untouched, and both outcomes are logged. The pull's outcome and the divergence notice are persisted too - the spinner is console-only and log.warn never reaches debug.log, which is why a stale clone had no explanation anywhere. The suite builds its "no git on PATH" fixtures as empty dirs under the per-test home, never a host directory like /usr/bin, which really holds a git on macOS and Linux - the win32 simulation drops the execute-bit requirement, so such a path silently satisfies the gate and the case proves nothing. Co-authored-by: flowjzh <flowjzh@users.noreply.github.com> | 7 天前 | |
fix(hooks): give the detached hook child the bundled gits on PATH (#801) The session-start pull is spawned through the WMI service to escape the host's job object, and a WMI-created process inherits the provider's environment, not the caller's. On a machine with no system git - every WorkBuddy client - the PATH that ran `teamai` therefore never reaches the pull: bare-name `git` lookups (simple-git, providers, mr-hint) fail with `spawn git ENOENT`, the failure is reported only on the console spinner (a detached pull has no console), and the clone silently stops advancing while the postPull script - spawned by absolute path - keeps deploying a stale tree. Resolve the bundled git at startup instead, one resolver per host in the git counterpart of BUNDLED_SHELLS: a qualifying root's `<root>/cmd` goes on PATH first, with the msys dirs appended for the tools git itself shells out to. A machine that already resolves git is left untouched, and both outcomes are logged. The pull's outcome and the divergence notice are persisted too - the spinner is console-only and log.warn never reaches debug.log, which is why a stale clone had no explanation anywhere. The suite builds its "no git on PATH" fixtures as empty dirs under the per-test home, never a host directory like /usr/bin, which really holds a git on macOS and Linux - the win32 simulation drops the execute-bit requirement, so such a path silently satisfies the gate and the case proves nothing. Co-authored-by: flowjzh <flowjzh@users.noreply.github.com> | 7 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 21 小时前 | ||
| 15 天前 | ||
| 15 天前 | ||
| 1 个月前 | ||
| 1 天前 | ||
| 1 天前 | ||
| 5 天前 | ||
| 20 小时前 | ||
| 20 小时前 | ||
| 7 天前 | ||
| 7 天前 |