mirror of
https://github.com/Tencent/teamai-cli.git
synced 2026-10-02 03:14:40 +08:00
main
979
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
85ff72c775 |
feat(recall): link an OMP subagent's session to its parent for adoption (#935)
* feat(recall): link an OMP subagent's session to its parent for adoption OMP gives a subagent a session of its own, so a recall the teamai-recall subagent ran never credited the main agent's later reads. OMP writes a subagent's session to <parent>/<agent id>.jsonl beside the parent's <parent>.jsonl, whose header names the parent session. The generated extension now reads that header on a session's first tool_result and sends the session_link the OpenCode plugin already sends, so the reducer credits the root session. A session kept only in memory has no file and still gets no link. * fix(recall): keep looking up an OMP subagent's parent until it is linked (PR review) A session was marked as looked up before its parent was found, so a parent session file not yet on disk lost the link for the whole session. Also: skill-data troubleshooting lists OMP's subagent path, the docs name the no-link case (--no-session), a row shows a linked subagent's own read still scores 0, and the header read limit is named. * fix(recall): read an OMP parent's session header after its title slot (PR review) OMP writes a padded title slot line before the session header, so the parent lookup parsed the title line and never linked a subagent. The test harness now writes OMP's real layout; verified in a live OMP 18.4.8 session. |
||
|
|
9bcd53e915 |
fix(rules): give Codex the team rules: session hooks in a project, its own AGENTS.md in user scope (#940)
* fix(rules): inline rules without frontmatter, naming path-scoped globs hermesRulesText becomes inlinedRulesText, the one renderer for tools that read rules from a single instructions file. It strips frontmatter and leads a path-scoped rule with 'Applies to files matching: <globs>'. Hermes SOUL.md and doctor's Hermes check use it. Part of #938. * fix(rules): inline team rules into Codex AGENTS.md in project scope Codex reads instructions from AGENTS.md, not from .codex/rules, so the copies pull wrote there never reached it (#938). The codex defaults drop rules and gain claudemd (AGENTS.md, ~/.codex/AGENTS.md at user scope). pullAllRules syncs a [teamai:team-rules] block into that file once per path while an enabled, installed Codex-family tool maps it, and removes it otherwise. syncManagedInstructions probes rules ?? settings, so a machine without Codex gets no ~/.codex. * fix(rules): render the Codex team-rules block with inlinedRulesText Follows the T3 rename, so a path-scoped rule reaches AGENTS.md without frontmatter, after its Applies to line. * test(rules): cover Codex AGENTS.md in user scope, culture, shared instructions and recall Codex now reaches AGENTS.md through its default claudemd path; these tests pin user scope (~/.codex/AGENTS.md), a full project-scope pull writing all four teamai blocks into one AGENTS.md, and recall on/off. Also corrects the injectRecallBlockIntoTools comment, which listed Codex as skipped. * fix(rules): remove Codex team rules and their old copies on uninstall Uninstall strips the [teamai:team-rules] block and scans the legacy .codex/rules directory for teamai's copies. A shared instruction file now keeps only the blocks a remaining enabled, installed tool would write, so --agent codex drops the team-rules block while Pi keeps its own. * fix(doctor): check the team-rules block in Codex AGENTS.md Codex reads no rules directory, so doctor now compares the team-rules block in each AGENTS.md an enabled, installed Codex-family tool maps with what pull renders. It fails on a missing or stale block, on an AGENTS.override.md that Codex reads instead, and on an entry with no claudemd path. The file list comes from RulesHandler, the same resolver the pull writes through. * fix(rules): remove the .codex/rules copies earlier pulls delivered Codex never read the <rule>.md copies teamai wrote to .codex/rules (and .codex-internal/rules, .tcodex/rules). Every rules sync now removes the ones that still hold what teamai delivered, including teamai-recall.md, keeps edited copies and names them, and leaves *.rules alone. * docs(rules): document where Codex reads team rules (AGENTS.md) * fix(rules): give Codex its AGENTS.md rules on an already-synced pull A CLI upgrade with the team repo unchanged takes the "Already synced" fast path, which never ran the rules sync. So a member upgrading from 0.22.0 got no team-rules block in AGENTS.md and kept the old .codex/rules copies until the repo moved or they ran pull --force. The fast path now runs the Codex part of the rules sync (block + reclaim) and nothing else. * fix(doctor): tell a Codex member a plain pull restores the team-rules block The already-synced pull now rewrites the block, so the fix text no longer sends the member to pull --force. * fix(rules): give codex-internal and tcodex the codex instructions layout codex-internal and tcodex drop their rules path and read team rules from AGENTS.md like codex. Codex-family instruction writers probe rules ?? settings ?? skills, so a team entry with neither rules nor settings no longer counts as installed. writesInstructionBlock is the one answer to which blocks a tool gets, shared by pull's writers and uninstall. * fix(rules): keep an empty AGENTS.md that held no team-rules block removeClaudeMdSection now reports whether it removed a section and can delete a file left holding only whitespace. The team-rules sync and recall off use it, so a pull that removed nothing no longer deletes a member's own empty AGENTS.md. * fix(doctor): pass the Codex team-rules check when no rule has a body When every team rule is frontmatter only, pull writes no team-rules block, so a missing block is correct and an AGENTS.override.md shadows nothing. Also note that an empty override shadows AGENTS.md too. * fix(doctor): a plain pull applies the Codex claudemd toolPaths fix Editing teamai.yaml moves the team repo, so a plain pull does the full sync; --force is not needed. Matches the other Codex block checks. * fix(uninstall): name the instruction file uninstall cleans The summary and result lines said CLAUDE.md for every instruction file, including Codex's AGENTS.md. They now print the file's path under an 'Instruction-file blocks' heading. * refactor(recall): gate the recall block on writesInstructionBlock recall on now asks the same question as pull's recall writer and uninstall, so the three cannot drift. No behavior change. * fix(rules): reclaim .codex/rules copies of rules the team removed The tombstone cleanup walks toolPath.rules, which Codex no longer has, so a copy of a rule tombstoned after the member's last pre-#938 pull stayed forever. The legacy reclaim now removes tombstoned names too, keeping and naming a copy the member changed since delivery (#822). * test(rules): cover codex-internal and tcodex in reclaim, doctor and uninstall Parametrizes the project-scope legacy reclaim, the four Codex doctor failure cases and the full uninstall over the Codex family. * test(pull): cover the project-scope Codex fast path after an upgrade * test(local-agent): pin the prompt sync into Codex's AGENTS.md With Codex's default claudemd (#938), an HTTP prompt command reported by Codex writes the shared-instructions block into ~/.codex/AGENTS.md when ~/.codex exists, and creates nothing when it does not. * refactor(rules): move rulePaths into rule-format It reads a tool-neutral rule's paths: frontmatter, which both the Copilot renderer and the inlined-rules renderer use; it no longer lives in the Copilot module. * fix(doctor): ask nothing of a Codex entry that delivers no rules to it A team toolPaths entry such as codex: { agents: .codex/agents } has neither rules nor claudemd: the team sends Codex no rules on purpose, like any tool without a rules path. Doctor failed it on every member's machine (E2E doctor-delivery-cli). It now fails only an entry that still lists the pre-#938 rules path without claudemd. Part of #938. * fix(rules): keep a teamai-recall.md the member edited since the last pull The legacy Codex reclaim removed any .codex/rules/teamai-recall.md that opened with the recall heading, so text a member appended after the last pull that wrote it was deleted. The built-in rule is not in the delivery ledger, so match the file against the exact bodies teamai deployed instead; an edited copy is kept and named like other legacy copies. * fix(rules): keep a member's empty AGENTS.md when the team-rules block goes Removing the last block deleted any instructions file it left empty, so an empty AGENTS.md a repository tracked was deleted once Codex was disabled or the team had no rules left. injectClaudeMdSection now creates a file as just the block, and deleteIfEmpty deletes only a file that opens with it; a member's empty file is written back empty. Also describe the per-block cleanup of shared instruction files in the deployed uninstall skill. * fix(rules): address review on uninstall, remove, doctor and markers - uninstall and the legacy [teamai:rules] strip remove blocks through removeClaudeMdSection, so a member's empty AGENTS.md survives them as it does pull. Removing a block at the top of a file keeps the next one there, so a file teamai created still goes with its last block. - `remove rules` refreshes with the rules this member's pull delivers (role, project and tag selection) instead of every rule in the repo. - doctor reports a Codex team-rules block left behind when the team has no rules, and nothing else in that case. - teamRulesBlock strips the block markers wherever they appear in a rule, not only as whole lines, since the block is located by substring. * refactor(rules): split legacy Codex rule-copy ownership out of the reclaim legacyRuleCopies classifies each .codex/rules copy (and the codex-internal and tcodex dirs) as teamai's or edited, on the ledger, team-render, tombstone and shipped-recall proofs reclaimLegacyRuleCopies used inline. Read-only and public so uninstall can apply the same check. No behavior change. * fix(uninstall): keep edited legacy Codex rule copies, remove removed rules' copies Uninstall picked every .md in a legacy rules dir whose name matched a current team rule or a built-in, so it deleted copies a pull had kept as the member's edits, and left copies of rules the team had tombstoned. It now takes the legacy dirs from RulesHandler.legacyRuleCopies, the check pull reclaims by: owned copies go, edited ones stay and are named in one English warning. The tool's current rules dir is unchanged. Addresses the codex-review findings on #940. * fix(rules): give Codex the team rules through its session-start hook The project AGENTS.md the earlier revision wrote the rules into is read by Cursor, Copilot, OpenCode and Claude too, which already get the rules in their own format. The Codex family now gets them from the teamai session-start hook instead, and pull writes no rule file for it. - A team-rules handler adds the user-scope rules, then the cwd project's, as pull resolves them, skipping a scope that does not enable the tool. It adds nothing on resume, whose history already holds them. - A SubagentStart entry gives a fresh subagent the same rules. - Both entries set additionalContextLimit: 0; past 2,500 tokens Codex keeps only the start and end of a hook's context. - doctor checks the limit on the session-start entry; the AGENTS.md block check, its markers and its uninstall handling are gone. The block never shipped. - Culture, shared instructions and recall still go to AGENTS.md; the old .codex/rules cleanup and the remove-rules role filter are unchanged. For #938. * test(tool-roots): expect Codex's instruction file under CODEX_HOME #942 pinned Codex's default paths before #938 moved its rules to the session-start hook: no rules path, and an AGENTS.md that follows the root in user scope. * fix(rules): keep Codex's project content out of AGENTS.md (#945) The project AGENTS.md is the owners' file, and Cursor, Copilot, OpenCode and Claude read it too. The Codex family now takes its project content from the session hooks and its user content from its own AGENTS.md: - Project scope: no `claudemd` in the Codex-family defaults, so pull writes nothing to the project AGENTS.md. The session-start and subagent-start hooks add the project's rules plus the culture, claudemd/ and recall blocks, leaving out a block another tool already wrote into the project AGENTS.md. - User scope: the team rules go into a team-rules block of ~/.codex/AGENTS.md (and the variants' homes), beside the other blocks; the hook adds nothing outside a project. The "Already synced" pull writes the block too. - doctor checks that block in user scope, and both hook entries' limit in a project. uninstall removes the block. For #938. * fix(rules): keep tombstoned Codex copies without delivery records |
||
|
|
bec06b3d2b |
test(code-knowledge): make the assignment case pin the binding fix (#943)
* test(code-knowledge): make the assignment case pin the binding fix `ast-swift-module-scope.test.ts` asserted 2 edges for `alias = work()`. The name on the right sits in a call callee, a position both walkers skip -- so the case produced the same count before and after the fix it guards, and passed on the base commit as well. The fixture becomes `alias = work` followed by `work()`, and the expectation 1 edge. Measured on the two walkers, one variable: base |
||
|
|
b2d3598b38 |
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. |
||
|
|
6db9144fc2 |
fix(webhook): preview test endpoints without sending (#900) (#941)
* fix(webhook): honor --dry-run for test * fix(webhook): preview zero matching endpoints --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
daa624a73a |
fix(mcp): honor --dry-run for remove (#900) (#937)
* fix(mcp): honor --dry-run for remove * docs(setup): document MCP removal preview --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
9d8ef7d81f | fix(tests): clear agent root env vars once in the vitest setup file (#936) | ||
|
|
8322869256 |
docs(config): clarify unreadable project scope fallback (#934)
Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
2fcc00b1d3 |
fix(code-knowledge): collect Swift shadowing from binding positions (#928)
Follow-up to #847 (merged as |
||
|
|
3030ffd8a8 |
fix(test): give each unit test file its own HOME (#927)
Commands resolve ~/.teamai from HOME, so a unit test that reached the real home shared the sync lock with every teamai session on the machine. push-env.test.ts failed while another session held ~/.teamai/.sync-lock (#924). A setup file, listed first so module-level home paths see it too, now points HOME/USERPROFILE at an empty temp dir per test file. The e2e config keeps the real home, which CI prepares. |
||
|
|
fe5c3c5a2b |
feat(mcp): add Pi Coding Agent MCP support (#926)
* feat(mcp): add Pi Coding Agent support Deliver stdio and HTTP servers to Pi user and project configs, preserve native codemode exposure, convert timeout units, and skip unsupported SSE. Cover reconciliation and local-agent delivery and update all affected documentation. * docs(pi): place MCP delivery under the MCP section |
||
|
|
7bb06525ef |
feat(init): prompt for optional projects during interactive setup (#757) (#910)
* feat(init): prompt for optional projects during interactive setup * docs(setup): ask before selecting init projects --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
75a7782f74 |
fix(recall): honor --dry-run for enable and disable (#900) (#907)
Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
6b1c9ed2c2 |
fix(exclude): honor --dry-run for skill exclude add/remove (#900) (#906)
* fix(exclude): honor dry-run when changing exclusions * docs(exclude): show dry-run previews before changes --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
7e6ebae2a4 |
fix(search): isolate IDF statistics by domain (#902)
* fix(search): isolate IDF statistics by domain * test(search): make ranking fixtures cross-platform * fix(recall): normalize all search candidates before limiting --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
83eb8a8083 |
fix(review): honor dry-run for apply, reject and batch decisions (#930)
* fix(review): keep dry-run decisions read-only * docs(review): derive preview guidance from CLI help |
||
|
|
df940cdd18 |
fix(config): refuse unreadable project scope fallback (#899)
Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
fb9f5a2c28 |
feat(mcp): keep project MCP configs with resolved tokens out of git (#882) (#886)
* 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. * 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". * 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(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 * 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(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(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(mcp): report a tracked file on a dry run and a withheld server already installed (#882) Backports #880's merge |
||
|
|
260f1945e8 |
feat(agents): model aliases so one agent works in every tool (#830) (#924)
* refactor(agents): each tool reads its own tool_extras key (#830) Qoder, Qoder CN, ZCode and OMP rendered tool_extras.claude while push wrote their edits to their own key, so an edit never reached them. tclaude and tcodex now read their own key over claude and codex; push writes only the values that differ from the base and reports an edit that removes an inherited field. Reverse parsing keys extras by tool, so a new native agent's extras land where its renderer reads them. * feat(agents): resolve team model aliases on pull for Claude and Codex (#830) An agent can set model: strong, model: fast, or an alias the team defines in models/aliases.yaml, which maps each alias per tool to that tool's own model value and an optional effort. Pull writes the mapped model into each tool's agent file, with effort as `effort` for the Claude family and `model_reasoning_effort` for the Codex family. A tool the alias does not map gets no model field; tool_extras.<tool>.model skips the alias. Push compares deployed copies with the same resolved rendering, so a pulled alias agent is not reported as edited. A non-string model fails to parse, a legacy .md agent with an alias model is copied with a warning, and an unreadable aliases file holds the agents that depend on it. * feat(agents): resolve model aliases for every agent tool (#830) * feat(agents): record alias resolution and redeploy on the pull fast path (#830) * feat(agents): apply the member's local model alias override (#830) * feat(agents): let tools switched to a model profile inherit their model (#830) * feat(agents): keep model aliases intact on push and report drift (#830) * feat(agents): hold alias agents on invalid alias files and drop bad entries (#830) * feat(agents): namespaced model alias files (#830) * feat(doctor): show how each agent's model alias resolved (#830) * fix(agents): deliver agents held by a broken aliases file on an ordinary pull (#830) * test(agents): stateful acceptance for model aliases and adoption docs (#830) * fix(agents): keep push from promoting alias-owned or delivered fields (#830) * fix(agents): deliver held agents and quiet the pull fast path (#830) A pull that holds an agent for model resolution no longer records the team revision, so a local fix is delivered by the next pull. The fast path skips copies the member edited, re-renders untouched copies an older CLI rendered differently, and names what it wrote. Records carry the alias name, so removing an alias that gave a tool no model warns. A changed model no longer fails doctor's delivery check, and a switched tool gets no extras effort. * refactor(agents): dedupe alias helpers (#830) One extras base-tool table (EXTRAS_BASE_TOOL) for render and push, one model-record comparison for pull and doctor, and the per-tool renderers renderForTool no longer used are removed, their tests moved to renderForTool. * test(agents): cover delivering held agents after the cause is fixed (#830) * fix(agents): scope local override failures to alias agents and warn on inactive-only aliases (#830) * fix(agents): reject aliases files without an aliases key, report fast-path holds, keep extras pins out of adoption (#830) * docs(agents): note that an extras model pin never adopts an alias on push (#830) * fix(pull): preview model holds in dry-run and do not report a held pull as complete (#830) * fix(agents): hold only tools that fail resolution and keep resolved values out of alias adoption (#830) * fix(agents): name the held tools when a broken aliases file holds only some of an agent's tools (#830) * fix(agents): write a model edit over a tool's own extras pin back to that pin on push (#830) * fix(agents): deliver a deleted recorded copy on the fast path, adopt an alias only for a changed model, keep unchanged own extras keys (#830) * test(agents): cover push of a tclaude hand edit under each kind of model pin (#830) * fix(pull): name an edited agent copy the fast path keeps for a changed model (#830) |
||
|
|
5fbaa377c7 |
feat(recall): attribute recall and adoption to the agent session in every agent (#925)
* refactor(usage): extract the locked JSONL store from the usage file (#884, ticket 01) Move the usage file's lock, side-record, fold and rewrite protocol into src/utils/jsonl-store.ts so the recall log can reuse it. The usage file keeps its behavior: same waits, mode, side-file names and gitignore healing. The store adds what the recall log needs: reads that include unfolded side records, owner-only files by default, and a newline before an append when a killed writer left a torn last line. * feat(recall): attribute a Claude direct recall to the doc it opens (#884, ticket 02) recall prints run=<id> on the region start line and appends a run record (session from the environment, each doc's vote key, scope, printed path and eligibility; never the query) to the active scope's recall log. A new PostToolUse handler appends a claim for a Bash call that ran teamai recall, and evidence for a Read under the knowledge roots. votes-sync now credits through the recall-log reducer instead of the transcript parser and no longer needs transcript_path. * feat(recall): exclude the recall subagent's own reads, mark its runs with --caller (#884, ticket 03) Claims and evidence carry the actor (session, agent_id, agent_type). A run is marked as the teamai-recall subagent's by its hidden --caller or its claim's agent_type; reads by that actor never count for it, while reads by the main agent or any other subagent do. The recall agent passes --caller teamai-recall on every recall and drops the unread referenced-doc-ids instruction. * feat(recall): settle each run by a valid claim or an unambiguous env session (#884, ticket 04) A run now records its agent family, via (env or none) and whether the environment held a single session id. The hook records a claim for every run id a shell call printed, noting whether its command ran teamai recall itself (parsed, so a quoted mention does not count); it also reads Codex's string tool_response. The reducer applies only the earliest direct claim for a run in the log, else the env session when unambiguous; unsettled runs never vote. agentSessionIdFromEnv callers are unchanged. * feat(recall): count a shell reader's read of a recalled doc, the Codex path (#884, ticket 05) Add the tool-call classifier (src/utils/tool-call.ts): category, paths read and status per PostToolUse, replacing recordToolCall's Bash and Read branches. The shell tokenizer moves to src/utils/shell-command.ts and now returns each simple command's operator and redirects; invokesRecall and the reader check share it. A read is one reader command (cat, bat, batcat, less, more, head, tail, nl, sed -n printing lines) alone or at the head of a pipeline; any ;, &&, || or & makes the call no read. A failed read is not recorded; a read of unknown status (Codex's string tool_response) counts only when simple. * feat(recall): count a search that shows a recalled doc's lines, never a listing or a count (#884, ticket 06) A search tool in content mode, or a shell grep/rg/ag/ack/git grep, records evidence for each file under the knowledge roots whose lines its output shows: a line that starts with the file's path and ':', under a searched root, or the lone operand when the output is non-empty. The hook only scans the output it already holds; it logs resolved paths, never the command or the output. Listings (Glob, ls, find, fd, tree, rg --files, git ls-files, grep -l/-L, a Grep file list) and counts (-c, --count, count mode) record nothing. OMP's and Cursor's search tools produce no evidence yet. * feat(recall): count Windows reads: PowerShell readers, drive paths, Git Bash /c/ (#884, ticket 07) PowerShell (Claude, CodeBuddy, Copilot) is a shell tool. Get-Content, gc, type and cat read their -Path, -LiteralPath and positional files. Paths are resolved and compared by the new agent-path module, independent of the host: drive-letter case, either separator and Git Bash /c/ name one file, and a search line's drive colon is part of its path. * fix(recall): compare Windows paths without case, look up shell verbs by own key (#884, ticket 07 follow-up) Windows paths ignore case, so a drive-lettered path's comparison key is now lowercased whole, as isUnderRoots did through path.win32.relative before ticket 07. A verb such as `constructor` no longer resolves to an Object.prototype member in the reader and search tables. * feat(recall): count Cursor, Copilot, CodeBuddy, WorkBuddy, Qoder and ZCode reads (#884, ticket 08) Session derivation reads Cursor's conversation_id after session_id and sessionId. The classifier reads Cursor's tool_output JSON string and Copilot's tool_result (text_result_for_llm, result_type), a read's path from file_path, filePath or path, and the names view, ReadFile, bash, Shell and run_in_terminal. Unverified payload shapes are marked in the row names. * feat(recall): OpenCode bridge sends its session, output and status, and links task children to their parent (#884, ticket 09) * feat(recall): Pi and OMP bridges send their session, output and status; OMP subagents their agent id and type (#884, ticket 10) * feat(recall): credit late reads at SubagentStop and pull, and prune the recall log at pull (#884, ticket 11) - Register SubagentStop for Claude Code, Codex, CodeBuddy and Qoder (and their internal builds); votes-sync runs the reducer there, without the end-of-turn summary. - `teamai pull` credits sessions whose evidence is still pending, then prunes the log under its lock: 30 days, then 5000 lines, never dropping pending evidence younger than 24 h or the run, claim, credited reads and links it needs to vote. Evidence and its consumed lines go together. - A run's doc is credited once, so a late read drained after the ledger's 24 h window cannot vote twice. - The store gains appendJsonlBatch (one lock, one write, one side record); a hook call and a reducer pass each make at most one write. Folds now work per line. * refactor(recall): the upvote judge skips docs in the session's ledger, and the transcript parser loses its adoption logic (#884, ticket 12) The opt-in judge (TEAMAI_UPVOTE_JUDGE) excluded the parser's adoptedDocIds on top of the session's upvote ledger. Since votes-sync credits from the recall log, a doc the parser counted but the hook path did not (a Glob listing, say) was neither credited nor judged. The judge now excludes only the ledger, and the parser keeps its recalled-doc detection, region parsing and recalled-doc-ids comment for the judge and older-format sessions. * fix(recall): the upvote judge credits the turn before reading the ledger (#884, ticket 12 follow-up) The dispatcher starts the detached judge before the foreground votes-sync credits the turn, so a doc opened in that turn was sent to the judge CLI once. The judge now runs the same reducer pass first; the ledger and the consumed marks make a second pass a no-op. * feat(stats): a recall section lists the last 10 sessions' runs, recalled and adopted docs (#884, ticket 13) * fix(stats): name the agent whose hook claimed a recall run (#884, ticket 13 follow-up) A claim now records the dispatch tool id of the hook that sent it, and the recall section prefers it over the run's environment family. A Codex run started from a Claude shell, or an OMP run, showed '-' before. * docs(recall): one adoption section with the per-agent table and known limits (#884, ticket 14) The usage guides gather adoption, the recall log, run ids, run ownership, subagents, vote timing and the stats section under "Recall adoption and upvotes", with a direct-recall / subagent-path table per agent and the known limits. The core skill's troubleshooting reference says where a recalled doc gets its upvote, and the recall subagent no longer says the Stop hook parses its doc-id comment for adoption. * docs(skill): CodeBuddy and WorkBuddy hooks are installed, not skipped (#884) The core skill's hooks table said CodeBuddy and WorkBuddy hooks are skipped by design, which contradicts the settings.json injection in src/hooks.ts and the per-agent adoption list this PR adds to the same file. * test(hooks): Claude settings carry the SubagentStop hook (#884, ticket 11 follow-up) Ticket 11 registers votes-sync on SubagentStop for Claude; the hooks e2e still expected four events. Cursor keeps four. * fix(recall): a search line counts only from a file-name prefix, and the log keeps only .md paths (#884, review finding 1) A one-file search's lines carry no path, so text before a colon (Cause:, a -n false Grep line, Pi's basename) no longer becomes <doc>/<text> in the recall log. Other lines need a prefix ending in a file name, then :<line>:, or : in grep/rg path:text and OpenCode's header. The recorder drops any evidence path that is not .md. * fix(recall): a one-file search counts only when it shows a line, not a tool's no-match text (#884, review finding 2) OpenCode's No files found and Pi's and CodeBuddy's No matches found made a grep of the doc that found nothing credit it. The target now needs a line that is neither blank nor a search tool's status line. Shell searches print none, so their lines are never taken for one. * fix(recall): the upvote judge keys each doc by the key its run recorded, not the parser's basename (#884, review finding 3) The transcript parser names learnings/setup and a skill's SKILL.md by their basename, so a doc the hook path had credited was judged again and upvoted under another doc's key. recalledKeyOf maps the printed path to the run's key for the ledger check and the upvote; a path no run printed keeps the parser's id. * fix(recall): SubagentStop credits locally and leaves the vote push to the next Stop (#884, review finding 4) The main agent waits on SubagentStop, so the git round trip of the reports push (and the ledger prune) blocked it mid-turn, up to the handler budget, after every subagent while deltas were pending. Stop and pull push what it credited. Stop is unchanged. * docs(hooks): state only what is known about Codex and an unknown hook event key (#884, review finding 6) Codex 0.159 knows SubagentStop, and Codex main's HookEventsToml has no deny_unknown_fields. How older hook-capable builds parse an unknown key is unverified. * docs(skill): restore the Claude hooks row to its main wording (#884, review finding 7) The CodeBuddy/WorkBuddy row keeps its fix, which the adoption section below needs; the Claude row's rewording was unrelated to #884. * refactor(recall): the adoption window and retention constants are module-private (#884, review finding 8) Nothing imports them, tests included. * fix(recall): an unquoted # that starts a word begins a shell comment (#884, PR review finding 1) `cat other.md # /kb/doc.md` reads only other.md, yet the doc counted as an operand. The comment runs to the end of the line; `a#b` and a quoted `#` stay literal. A claim with a trailing comment still claims. * fix(recall): an unquoted backslash escapes a space or shell character, and keeps a Windows path whole (#884, PR review finding 2) `cat /kb/my\ doc.md` was split into two operands. Outside quotes a backslash now escapes whitespace and ' " \ $ ` ( ) & ; | < > * ? # !; before any other character it stays literal, so an unquoted `C:\kb\learnings\x.md` is unchanged. Double quotes already followed POSIX for \" and \\, and single quotes stay literal. * fix(recall): a claim skips the root program's options before recall (#884, PR review finding 3) `teamai -v recall "q"` is a valid recall, but its claim was marked indirect, so a run with no session variable (OMP, via: none) never settled. The root options now live in one table that index.ts registers on the program and the claim parser reads through Commander's Option, so it also skips the value of an option that takes one. `npx teamai-cli[@v]` is handled as before. * fix(recall): with no status, a shell call that printed only its command's errors failed (#884, PR review finding 4) Codex sends no status, so `grep needle /kb/doc.md` answering "grep: /kb/doc.md: Permission denied" counted as the doc's content and upvoted it; `cat doc` answering "cat: doc: No such file or directory" did the same. When the status is unknown, output lines that start with the command word (as written or by name) and ": " are errors, not content. A read or search that printed nothing else is a failure and records no evidence. Both usage guides say so. * fix(recall): Copilot's SessionEnd credits and pushes the session's votes (#884, PR review round 2 item 1) Copilot's last turn can end with SessionEnd and no Stop, so votes-sync now runs on session-end too: foreground, git only, with Stop's timeout. It pushes as Stop does and prints no adopted summary. The Copilot rows end with the real SessionEnd payload instead of a synthetic stop. * fix(recall): a search with no hits prints its run id, so its shell call can claim it (#884, PR review round 2 item 2) The no-hit line now ends in run=<id>, and the claim pattern reads it after the logger's glyph. An OMP recall with no hits and no session variable settles through its bash call's claim and shows in teamai stats. --check still prints no run id and records no run. * fix(recall): type and gc read only in PowerShell or for a Windows path, and a shell's own diagnostic is an error line (#884, PR review round 2 item 3) In bash, type is a builtin that prints what a name is, so Codex's type /posix/doc.md under Linux gave a false upvote with no status. type and gc now count under a PowerShell tool, or when every file is a Windows path. Get-Content counts everywhere. With no status, bash:, sh: and zsh: diagnostics are error lines too. * fix(recall): with no status, a read drops a file its command's error names (#884, PR review round 2 item 4) In cat a.md doc.md where only doc.md errors, doc.md still counted. An error line <verb>: <file>: … now removes that file from the read's paths, matched as written and as resolved against the cwd; the other files still count. |
||
|
|
128844fe6c |
fix(pull): name the skills a pull removes because they are no longer delivered (#911) (#917)
* fix(pull): name the skills a pull removes because they are no longer delivered (#911) * fix(pull): keep the tags subscribe hint when the removed skill copy is byte-identical to its inactive namespace source (#917 review) |
||
|
|
d4d1c6418e |
fix(projects): honor dry-run when selecting projects (#905)
Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
7c929a996b |
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 |
||
|
|
4a87fe5d26 |
test: resolve temp roots to the long path on native Windows (#870)
* test: resolve temp roots to the long path on native Windows * test: canonicalize the local-agent temp root as well --------- Co-authored-by: Jay0130-a <309071492+Jay0130-a@users.noreply.github.com> |
||
|
|
2aaf3db543 |
fix(code-knowledge): resolve Swift symbols across files in the same module (#847)
* fix(code-knowledge): resolve Swift symbols across files in the same module Swift declarations are module-scoped, but the AST track resolved calls and conformances with a file-scoped model: declared in this file, or reachable through a resolved import. That model is correct for TS/Go/Python, where a cross-file symbol has to be imported. Swift needs no import inside a module, so a conformance or a call to a symbol in a sibling file emitted nothing. Add a third, Swift-only level of lookup after the existing two: a declaration elsewhere in the same module, with the module boundary read from the layout SwiftPM mandates (Sources/<Target>/, Tests/<Target>/). Outside that layout no scope is claimed, and a name declared more than once in the module emits nothing rather than picking one arbitrarily. Fixes #843 * fix(code-knowledge): index only module-visible Swift declarations Addresses the three P1 findings the automated review raised on the first revision of #847. All three were real. The fallback widened the candidate set without narrowing eligibility. The candidate set is "every declaration in the module"; what a bare name can actually reach is strictly smaller, and that has to be decided where the declaration and its modifiers are still in hand. - module-scope.ts: the module key now keeps the package root, so Packages/A/Sources/App and Packages/B/Sources/App stay two modules instead of merging into Sources/App. - walk.ts: a declaration enters the module index only when it is top-level and not private/fileprivate. Neither fact survives into AstSymbol, so it is computed in the walker and surfaced as FileWalkResult.swiftModuleSymbols. - index.ts: feeds that subset to the module index. queries.ts and the shared AstSymbol type are untouched, and the tie rule agreed in #843 is unchanged: only module-visible declarations can tie. Verification on the merge result (main |
||
|
|
a8ab138b0a |
feat(models): support Pi Coding Agent (#918)
Pi becomes the sixth agent `teamai models switch` can point at a team
gateway. TeamAI writes one provider into `~/.pi/agent/models.json`, holding
every catalog model; a member's other providers in that file are never
touched.
- **Key is the profile ref** (`team:<id>` / `local:<id>`), not the bare `id`
— an `id` is unique only within one catalog file, so a root and a namespace
profile may both be `tokenhub`, and a bare id would make the second switch
silently overwrite the first gateway. Pi shows the profile's `name`.
- **One provider covers all three protocols.** Pi resolves the api and URL
per model: the first in Chat Completions, Responses, Anthropic order
becomes the provider's own, and a model reached through another carries
`api` and `baseUrl` itself. A model served both ways is registered once,
preferring an OpenAI one; an `anthropic`-only group pins a Claude model to
Anthropic Messages.
- **An environment key is written as `$VAR`**, and `settings.json` is left
alone, so the default model stays the member's choice.
`PI_CODING_AGENT_DIR` is honored the way `CODEX_HOME` is.
- **Restore** removes the key the last switch created, even after the catalog
re-points the profile at a different ref. Restoring without `--agent` now
covers Pi too.
Rebased onto current `origin/main` (
|
||
|
|
3a9a24a6f4 |
fix(env,hooks,mcp,status): name the entries that are not delivered (#822) (#851)
* fix(env,hooks,mcp,status): name the entries that are not delivered (#822) env list, mcp list, hooks list, status and list <env|hooks|mcp> resolve the entry types to show what reaches this directory, but dropped the resolution notices: an entry an unknown key (a mistyped role:) or a removed key (roles: on env, projects:) takes out of the delivered set was silently missing from the list, and status counted around it. pull and doctor report these; now the list commands do too, via reportEntryResolution. env add on a variable carrying a removed per-entry key kept reporting plain 'Updated' — #833 taught it to warn for keys the schema does not know, but a removed key is in the shape on purpose (so it can be detected), so it stayed silent. Warn the same way for those. * test(e2e): match the delivered DEVOPS_ONLY form, not its name in the notice env list now reports the withheld per-entry `roles:` variable by name (#822), so the whole-output not.toContain('DEVOPS_ONLY') assertion tripped on the delivery notice itself. The variable stays out of the delivered list; match the listed form `DEVOPS_ONLY=` instead. * fix(env): point env add at the namespace file, not a key drop The review of #851 found the update-path warning told users to remove a per-entry `roles:`/`projects:` key in place, which delivers a root-scoped secret to the whole team. The remediation now reuses `moveTo`, the same remedy pull's notice names, so it points at the namespace file to move the entry into (with the manifest declaration to add when nothing declares it). `moveTo` and `TargetFiles` move from module-private to exported for this. The review also found skill-data/setup/references/manage-admin.md still said only pull and doctor report undelivered entries, while this branch made the list commands and status report them too. It now names them, as docs/usage-guide.md does. * fix(entries): report notices alongside resolution failures * fix(entries): scope list warnings and document env updates * docs(entries): align changelog with list and env warnings --------- Co-authored-by: ydflow <ydflow@users.noreply.github.com> Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
ed95357fe8 |
fix(dry-run): let --dry-run reach recall maintenance and recall promote (#900) (#903)
* fix(dry-run): let --dry-run reach recall maintenance and recall promote (#900) The root program declares --dry-run, and Commander gives it to the root wherever it is written, so these two actions, which read only their own options, never saw it: --prune --dry-run pruned and published, --update-quality --dry-run wrote AI drafts, and promote --dry-run promoted and published. Both actions now merge program.opts() as the other actions do, and load with { dryRun }. --confidence-writeback, which ignored the flag, now reports what it would write and publishes nothing. * fix(dry-run): create the promotion target directory only when promoting (#900) executePromotion created <repo>/<category>/ before its dry-run return, so recall promote --dry-run left an empty directory in the team repo. * refactor(promote): drop the ensureDir that writeFile already does (#900) |
||
|
|
92e6e2f060 |
fix(models): key team values by repo identity, migrating legacy slug names (#894) (#895)
* fix(models): key team values by repo identity, migrating legacy slug names (#894) * fix(models): address review on the repo-identity values scheme (#894) - never migrate the legacy values file in a dry run: `pull --dry-run` and `models switch --dry-run` thread { dryRun } into migrateTeamValuesPath - match the legacy digest the old implementation actually keyed on — the repo: claim in teamai.yaml overrode the identity there, so an SSH remote beside an HTTPS repo: no longer orphans keys and switches - when several legacy files share a digest, migrate the newest one, so keys re-entered after a team rename win over stale copies - sync docs/designs/model-profiles.zh-CN.md with the hash-only file name 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): preview migration in dry runs, stop alias collisions, race-safe link - dry run returns the newest legacy file's path so it reads exactly the keys the real run would migrate and read, instead of reporting a key missing from a not-yet-existing hash-only file - a bare remote alias (`fork`) no longer names the values file: two checkouts sharing the alias hashed to one file and could read each other's keys, so hashing falls back to the URL, then the local path - migration links the legacy file to the target instead of renaming: a concurrent migration that links first makes ours fail with EEXIST, so stale keys can never overwrite fresh ones on POSIX 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): narrow the path digest to path-only configs, copy when links fail - the legacy local-path digest is a candidate only when the old implementation would have keyed on the path itself (no repo: claim, no configured remote, no URL): a default-path checkout re-initialized for another team with a URL no longer adopts the previous team keys - when a filesystem rejects hard links (EPERM, ENOTSUP, ...), migration falls back to an exclusive, never-overwriting copy (open wx mode, partial writes removed) instead of skipping every candidate and returning a path with no file behind it 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): publish the copy fallback atomically via a temp file The copy fallback wrote the target in place after an exclusive open, so a concurrent reader could observe the empty or partially written values file and fail to parse it. The content is now written to a unique temp file and linked into place: until the publish link succeeds the target does not exist, so a concurrent reader sees either the complete previous file or none, and two migrations still cannot overwrite each other (the loser publish link fails and its temp file is removed). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * refactor(models): read legacy values in place instead of migrating files Rename-and-migrate had to be atomic, no-clobber, link-optional, and partial-write-free all at once, and each fix round surfaced another way the migration could strand or clobber a secrets file. Drop the machine: while the hash-only file does not exist, findTeamValuesPath returns the newest legacy file and it is read where it lies; the next save writes the hash-only name, which then shadows the legacy file. Reads no longer mutate anything — a dry run needs no special casing — and no link, rename, or copy stands between the keys and their reader. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): identify alias-digest legacy files by slug as well Two checkouts that shared a bare non-origin alias (`fork`) hashed the same legacy digest, so findTeamValuesPath could read one team's values for the other. A repository-bound digest (URL, repo: claim, local path) still matches by digest alone, since the slug drifts on team renames; a file keyed by an alias digest is adopted only when its slug is this checkout's too, the discriminator the old scheme kept those files apart by. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): align switch-record matching with file matching, mirror old precedence - sameTeamIdentity applied the alias slug check only when reading a values file, not when matching a switch record: two teams sharing remote fork could treat each other managed switches as their own. The slug requirement now covers every non-repository-bound digest. - legacyTeamValueHashes mirrored the old precedence too loosely: with a repo claim present, the old implementation hashed only the claim, so the remote, URL, and path candidates never applied. A replaced checkout at the same path can no longer adopt a path-digest record the old code would never have produced for it. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): prefer the repo: claim over the local path for the values file name A config with remote origin and no repo.url hashed the checkout path, so reusing that path for another team shared the first team's hash-only values file and its identity: team B read team A's API keys and claimed its managed switches. The claim is the remaining repository identity before the path, so it now wins whenever the remote and the URL name no repository. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): qualify provider-relative identities with the provider A path-shaped identity like owner/repo names a different repository on each provider, so two teams with the same repo value on GitHub and GitCode hashed to one values file and one identity: opening team B read team A keys and accepted its managed-switch identity. The provider now qualifies path-shaped identities; host-bearing ones (URLs) already carry the host and stay as they are. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): use the effective provider and slug-check legacy claim digests - the values file identity qualified a path-shaped source with the local provider override, normally absent, so two teams declaring the same repo: value on GitHub and GitCode still hashed to one file; the teamai.yaml provider now wins for its own claim, and the local override applies only when the claim carries none - legacy candidates marked a path-shaped claim digest repository-bound, so sameTeamIdentity adopted another team's switches and findTeamValuesPath read its file under the matching digest; a URL-shaped claim stays repository-bound while a path-shaped one is matched under this team's slug, like an alias - also fixes the candidate guard inverted while landing the above: with a claim present the remote, URL, and path candidates were dropped, and without one they were duplicated 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): read legacy claim files by digest and slug-guard path-only records Path-shaped repo claims now adopt their legacy values files by digest alone, so a team rename no longer orphans its keys; the slug check moves to switch-record matching, where a claim digest names no single repository. Path-only configs mix the team slug into the hash-only identity, so a checkout path reused by a differently named team can no longer read the previous teams keys. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * docs(models): describe the path-only slug-fallback hash in both languages 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(models): slug-guard provider-ambiguous legacy files, repair renames by gateway origin A legacy file's digest hashes the bare path-shaped claim name with no provider, so GitHub team Alpha and GitCode team Beta can produce the same old digest. The previous fix read such files by digest across renames, letting the second provider adopt the first's file and API key; slug-guarding records instead orphaned a renamed team's own switches. Gate both on the one evidence that survives both a rename and a provider change: the gateway origin each stored key is bound to under `<team:<id>@<origin>`. Path-shaped claim digests now require this team's slug for files and switch records, and are re-admitted under a different slug only when the legacy file provably stores keys bound to this team's gateways — which a rename keeps and another provider cannot claim. Keys still in a beta-era `team:<id>` form (no origin) are not evidence, so their files are left unread rather than guessed at. Callers thread the resolved team profiles through `findTeamValuesPath`, `sameTeamIdentity`, `switchedGatewayOrigins`, `activeAgentsFor`, and the pull re-apply. * fix(models): prove rename-repaired legacy files from this machine's own switch history Gateway-origin key binding is not a team identity: two teams sharing a profile id and gateway URL behind the same path-shaped claim would pass the provenance check, letting the second read the first's file and claim its switches. But refusing differ-slug files entirely orphans a renamed team, whose legacy `<slug>-<hash>.json` carries its own old slug. The trustworthy, machine-local evidence is managed.json: this checkout records the exact identity each past `team:` switch used. A differ-slug provider-ambiguous file or switch record is re-admitted only when that identity appears in this checkout's own switch history — a renamed team's file, recognized without trusting a gateway a foreign team could share, a key binding another team's file may also carry, or a slug a rename just moved. Foreign teams hold only their own slug in managed.json and are refused; beta-era unbound keys need no binding to be recognized this way. Provider-relative remotes (`owner/repo`) also get the provider qualification already given to path-shaped claims, so two providers using the same checkout path and slug no longer share one values file. Design docs synced in both languages. * fix(models): never auto-adopt provider-ambiguous legacy values — adopt explicitly Two review rounds proved no silent rule can attribute a legacy file or switch record whose digest never encoded the provider: - a same-slug provider-ambiguous file is read across providers (a GitHub and a GitCode 'Alpha' on the claim 'acme/widgets' hash the same file; the slug is not provenance), and - machine switch history (managed.json) is global and self-validating: one file is shared by every checkout on the machine, so the record being proved sits in the same store a foreign team's checkout reads, letting a differently named team claim a same-digest record or file. Provider-ambiguous legacy values files (path-shaped claim, provider-relative remote, bare alias, path-only) are now never read by a silent rule, same slug or not. The CLI lists them once (unadoptedLegacyFiles) and the user explicitly adopts the exact `<slug>-<digest>` identity for this checkout; the read then happens where the file lies and the next save migrates the keys to the provider-qualified hash-only name, which shadows the legacy file forever (no re-ask, no ambiguity left). Non-interactive and --dry-run runs never adopt: they report the file and leave it unread. Differ-slug provider-ambiguous switch records are refused — no machine-global proof is ever consulted again. URL-bound legacy identities still match by digest alone: the host is in the identity. Design docs synced in both languages. Tests: same-slug cross-provider never auto-reads, foreign file/record refusal, explicit-adoption read + unbound-key migration, alias digest under this team's adoption. * fix(models): do not offer a legacy file for adoption once its target exists A past adoption and migration is the permanent record: the provider-qualified hash-only target shadows every legacy name forever, so unadoptedLegacyFiles returns no candidates when getTeamValuesPath exists. Without this, the in-memory adoption set (which resets per invocation) combined with the intentionally-kept legacy file re-prompted on every later interactive command and misled non-interactive/--dry-run runs with a stale warning. Test: after migration the file is shadowed and never offered again. * fix(models): scope legacy adoption to the provider-qualified team and config Two review findings on the adoption layer: 1. adoptedLegacyValues was global, keyed only by the legacy basename. In a pull with inheritUserScope, user scope runs first; a same-slug, same-claim team on a DIFFERENT provider resolves to the same adoption key, so confirming the user-scope file also silently authorized the project scope to read and migrate that key in its own provider space. Adoption is now keyed `${target}::<slug>-<digest>` — the adopting scope's own provider-qualified file path — so a confirmation scopes to exactly that team identity, and any other scope/provider must opt in separately. 2. adoptMigrated was a process-global boolean ("any adoption happened"). After adopting a user-scope file, declining a project-scope candidate still set it true, so the project loaded {} from its nonexistent target and wrote an empty hash-only file, permanently shadowing its legacy keys and killing future prompts. The write is now decided per config: only when readFrom differs from the target (a real legacy read) is the migration saved; a declined candidate is read nothing, written nothing, and re-offered next run. Tests: cross-provider scope regression (GitHub adoption under its target never admits the file for the GitCode scope; same-scope adoption of the same basename does), namespaced adoption keys everywhere, migration-shadow check. * fix(models): refuse shadowing writes while adoption pending; never claim ambiguous records by slug Two review findings: 1. A non-interactive `models configure team:<id> --from-env X` reads {} past an unadopted legacy file, then writes ONLY the configured key to the hash-only target — permanently shadowing the legacy file (the target's existence silences every future adoption prompt) and orphaning its other API keys without a warning anyone can act on. configure and switch now refuse the write while unadopted legacy files remain, naming the interactive adoption path. A pull is unaffected: its read-time save is the migration itself and already requires a real legacy read. 2. A same-slug provider-ambiguous switch record matched Team B's checkout. The old name never encoded the provider, so a GitHub and a GitCode team both named `Alpha` on the bare claim `acme/widgets` share the identical `alpha-<digest>` form: with Team B holding its own hash-only key, pulling B claimed an agent switched to Team A and overwrote it. A legacy record under a provider-ambiguous digest never matches now — slug equality is not ownership across providers, consistent with the differ-slug refusal. Repository-bound records still match by digest (the host is in the identity). Adoption of the values file re-establishes the team; its switched agents are re-recorded by the next `models switch`. Docs synced (en + zh-CN). Tests: path-only/alias/claim records all refused even under the exact slug, cross-provider same-slug record regression, and the 4 prior adoption-scoping expectations. * fix(models): a provider-relative remote is the top identity; never a Windows drive as a URL Two review findings on identity derivation: 1. A provider-relative remote never got its documented precedence. isRepoReference('owner/repo') is false, so the selector picked the URL or teamai.yaml claim instead; two checkouts with DIFFERENT remotes (acme/team-a vs acme/team-b) but the same claim/provider therefore shared one hash-only secrets file and could consume each other's keys. A configured non-origin remote now names the repository with the highest precedence whether URL-shaped or provider-relative (which names one repo together with its provider); only a bare alias (no slash, no scheme) falls through. Different remotes -> different files, always. 2. new URL('C:\\teams\\repo') accepts c: as a scheme, so Windows drive paths were hashed as phantom URLs and a path-only config skipped the provider-and-team-slug fallback; replacing that checkout with a differently named team reused the file. URL parsing now rejects single-letter schemes, keeping drive paths in the path form whose slug separates teams at one checkout. Tests: two different provider-relative remotes with a shared claim collide no more; same remote+provider names one repo across paths; bare aliases and drive paths never hash as URLs. Full suite 5285 passed. * fix(models): use the teamai.yaml provider for provider-relative remotes; bare aliases never key the file Two more identity findings: 1. A provider-relative remote ignored the provider declared in teamai.yaml when no local override existed, so GitHub and GitCode teams sharing `remote: owner/repo` both hashed `tgit:owner/repo`. The effective provider now consults the teamai.yaml `provider:` (via the claim or, when the claim is absent, directly) before the local override and the tgit default — the same qualification the path-shaped claim already applied. Provider survives a team rename, so the file stays bound to the repository. 2. A bare remote alias (`remote: fork`, no URL, no claim) was still hashed directly as `tgit:fork`, so two checkouts sharing the alias shared one hash-only secrets file. A bare alias names no repository, so it now falls through to the provider/slug/path identity, whose slug keeps teams sharing a checkout path apart. Legacy `<slug>-<digest>` files for alias-keyed checkouts remain migration candidates. Tests: each checkout declares its own teamai.yaml provider and stays distinct; a bare alias no longer changes the file over the path-only form and two teams sharing it stay separated by the slug. Full suite 5285 passed. * fix(models): partial adoption never orphans a declined legacy file; adoption merges all adopted keys Two defects in the read-time migration: 1. Declining one of several matching provider-ambiguous legacy files then saving the hash-only target stranded the declined file forever: the very existence of the target silences every future adoption candidate, so keys unique to the declined file became unreachable with no further prompt possible. The migration save now happens only when NO candidate remains for this team (adopted or gone); a declined file stays a candidate, keeps its keys reachable, and is re-offered on the next run. 2. A single-file read kept only the newest adopted identity's keys. When several files shared this checkout's digest, adopting all of them still dropped every non-newest identity's unique keys before the target shadowed them. Adoption now merges the keys of every adopted identity into the migrated target (new mergeModelInputs; the later map wins). Tests: with two matching files, adopting one still surfaces the other to the migration guard; adopting both leaves no candidates and the merge keeps both identities' keys. Full suite 5286 passed. * fix(models): member provider overrides the team's; declines are durable and no longer block migration Two review findings, one shared root: the migration's consent model could not distinguish "not yet decided" from "deliberately declined". 1. The effective provider put teamai.yaml's provider before the member's own, so a team declaring GitHub with the same bare claim made a member initialized with --provider gitlab share the GitHub team's hash-only file. The member's own provider now wins (localConfig.provider ?? team provider ?? tgit), as everywhere else in the CLI. 2. Requiring every same-digest file to be adopted blocked migration behind a foreign team's file: declining GitHub Alpha to adopt GitCode Beta left an unadopted candidate forever, so no target could ever be created. Declining now records a durable, target-scoped ${target}::<slug>-<digest> refusal in the models manifest. A declined identity is neither re-offered on every run (no re-prompt loop) nor silently shadowed when migration proceeds (the choice is explicit, visible state, reversible by removing the key or the file) and does not count toward the migration guard — adopting only your own team's file no longer forces you to merge a foreign team's keys. unadoptedLegacyFiles and findTeamValuesPath now take { adopted, declined }; findTeamValuesPath reads an adopted identity and skips a declined one. Tests: found the regression itself (adopt one of two, decline the other leaves no candidate and reads the adopted file only), the provider-override case, and a refuseLegacyValue/refusedLegacyValueKeys manifest round-trip with a fail-closed schema check. Full suite 5288 passed. * fix(models): serialize the migration target against a concurrent writer by merging before save The migration write into the new hash-only target is the one place a values file is created from a different file, so it must not clobber a target a concurrent process wrote between the legacy read and the save: writeJsonAtomic prevents torn files, not lost updates. A pull reading the legacy file while a configure creates the target could otherwise overwrite the newly configured key with its stale snapshot. The migration now re-reads the target and merges before saving (the concurrent content wins collisions). The merge runs only in the migration branch (readFrom !== target). When the run merely re-saves the file it already read, the target holds the same content this bind just consumed — merging would re-inject the raw legacy entries the bind renamed, so there is nothing to guard against there. Full suite 5288 passed. * fix(models): hold the team values lock across the whole read-modify-write Re-reading the migration target and saving it were still two separate operations, so the race the review flagged only shrank: a pull could re-read the target, a concurrent configure could save a new key, and the pull could then overwrite it with its stale merged snapshot. Team values writers now share an advisory lock on the target file, and each of them runs its entire read-modify-write cycle inside it: - the migration's was already read-merge-save; it now runs that whole cycle under the lock instead of only the merge step; - models configure and the models switch first-use prompt re-read the target inside the lock, so their write cannot clobber a migration or a sibling configure that landed in the same window. withTeamValuesLock reuses the existing owner-verified acquireLock/releaseLock primitive (dead-owner stale locks are reclaimed), so a crashed holder does not block pulls or configures. Local values writes are untouched; no legacy migration can race them. Full suite 5288 passed. * fix(models): resolve the profile from the key saved by the first interactive switch The lock round-trip saved the first-use key into the freshly re-read snapshot (current), but the rest of the command resolved the profile against the original in-memory values, which had no key yet — so the switch stored the key and then failed with "no API key", demanding a rerun. Apply the same key to the in-memory copy after the locked write, and add a regression test for the first interactive team switch (ask, save, and resolve in one run) so the behaviour cannot silently split again. Full suite 5289 passed. * fix(models): save the awaited key with the first interactive local switch Moving the getStoredApiKey update after the branch left the local path saving values.json without the key the prompt returned: the switch succeeded from the later in-memory copy, but the next invocation found values.json missing the key and reported it not configured. Put the key in the in-memory copy before the branch, so the local save already carries it; regression tests now cover the first interactive switch for both the personal and the team profile. Full suite 5290 passed. --------- Co-authored-by: Codebuff <noreply@codebuff.com> |
||
|
|
1d945b6f36 |
fix(init): seed a custom agent's configured root dir on regular init, not just self-mode (#867) (#873)
* fix(init): seed a custom agent's configured root dir on regular init, not just self-mode
Problem: in a multi-repo setup, `teamai init --agent AA` with `AA` a
custom agent whose skills/rules root is configured only through
`teamai.yaml`'s `toolPaths` (e.g. `AA: { skills: 'a/skills' }`) silently
delivers nothing, forever. `pull`'s `isToolInstalled` treats a missing
root directory as "not installed" and skips the tool — correct for a
real third-party tool the user has to install themselves, but wrong for
a purely teamai-managed directory convention that nothing else will ever
create. `--agent AA` is the only "installation" such an agent has (#867).
First approach, reverted: generalizing `isToolInstalledForConfig`'s
existing Copilot-only `enabledAgents` short-circuit to any tool broke
`doctor`'s own "`<tool> is installed`" diagnostic (#598), which reuses
the exact same function (via `skillsDirForTool`) to deliberately flag a
tool the user *claimed* via `enabledAgents` but never actually
installed — precisely the scenario that check exists to catch, not
paper over. Confirmed by running the full suite with each version: the
broad fix broke 3 `doctor.test.ts` cases ("buildChecks — a tool enabled
but not installed") that assert exactly this.
Actual fix: single-repo mode already solves the identical "the tool's
root doesn't exist yet, but the user explicitly asked for it" problem
via `seedSelfModeToolDirs`, called before hook injection specifically
because "a teammate's fresh clone has no `<repo>/.claude` yet, so
nothing would ever inject." That function is not actually self-mode
specific (it only uses `resolveBaseDir` + `enabledAgents`, neither of
which is self-mode-only) — it was just never called anywhere else.
Added the same call, in the same position (before hook injection), to
both the regular git-mode `init` and `initHttp`, and updated its
docstring to describe the now-dual motivation. `doctor`'s diagnostic is
untouched and still correctly flags a genuinely-uninstalled built-in
tool.
Test plan: new unit test in self-mode-agents.test.ts proving
`seedSelfModeToolDirs` seeds a custom agent's root outside self mode
(user scope, `toolPaths`-only entry). Real e2e verification via a
scratch script against the actual built CLI: confirmed `pull` silently
skips delivery and `doctor` correctly reports "AA is installed: false"
before the fix; after seeding, `pull` delivers both a skill and a rule
into the custom root for real, and `doctor` reports the check passing.
Full regression suite unchanged from the pre-existing baseline (48
failed files / 166 failed tests / 5072 passed) plus the one new test
passing (5073 passed) — no new failures.
Fixes #867.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(init): address automated review findings on custom-agent seeding
The codex-review bot on PR #873 found three real gaps in the previous
fix (seedSelfModeToolDirs reused wholesale for regular/HTTP init):
1. It seeded *every* enabledAgents entry, including built-in tools. On
a machine without Claude installed, `teamai init --agent claude`
would silently create ~/.claude/skills, defeating doctor's "is
installed" diagnostic (#598) — the whole point of which is to catch
a claimed-but-absent tool instead of manufacturing a fake root for
it. Fixed: outside self mode, only agents not in KNOWN_AGENTS (true
custom agents) get seeded; built-in tools must already exist on
disk, same as before.
2. It read teamConfig.toolPaths directly instead of resolving through
scopedToolPaths, so a custom agent with a userScope path override
would have its default path seeded while pull/doctor resolved the
user-scope path — permanently mismatched. Fixed: use
scopedToolPaths(teamConfig, localConfig), the same resolver every
other call site (doctor, hooks, local-agent) already uses.
3. It only ever probed the `skills` path, so a custom agent configured
with only e.g. `rules` (no `skills`) was never seeded and stayed
"not installed" forever. Fixed: probe skills ?? rules ?? agents ??
settings ?? hooks, the same order doctor.ts's buildEnabledToolChecks
already uses for its own probe.
Also updates the --agent help text (src/index.ts,
skill-data/core/references/commands.md) and the FAQ in
docs/usage-guide.md (+ zh-CN) to describe the custom-agent seeding
exception, per the bot's P2 finding.
Test plan: added 3 tests to self-mode-agents.test.ts covering each
fixed scenario (built-in tool not seeded outside self mode, userScope
override honored, rules-only custom agent seeded) — 25/25 passing.
Full regression suite: 48 failed files / 166 failed tests / 5076
passed / 8 skipped (5250 total), exactly matching the pre-existing
baseline (166 failed, all pre-verified unrelated) plus these 3 new
passing tests. Real-CLI e2e whitelist test (#510) still passes
unchanged, confirming a built-in tool named in enabledAgents is still
gated on actually being installed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(init): seed the tool root, not the resource path, for file-valued fields
Second round of codex-review findings on PR #873:
1. settings/hooks/claudemd are FILE paths in ToolPathsSchema (e.g.
"a/settings.json", "a/AGENTS.md"), unlike skills/rules/agents which
are directories. The previous fix called ensureDir on whichever
probe path matched first, so a custom agent configured with only
`settings: "a/settings.json"` got a directory literally named
settings.json created — hook reconciliation then can't write the
real file there, so init reports success while installing nothing.
2. claudemd was missing from the probe order entirely (copied verbatim
from doctor.ts's own probe, which has the same gap), so a custom
agent configured with only `claudemd` was never seeded and stayed
"not installed" forever.
Fixed: skills/rules/agents (directories) still seed the full resource
path as before; settings/hooks/claudemd (files) now seed only their
parent tool root via toolInstallRoot(), added claudemd to the probe.
Test plan: 2 new tests (settings-only and claudemd-only custom agents)
— 27/27 passing in self-mode-agents.test.ts. Typecheck clean, real-CLI
e2e whitelist test (#510) still passes. Full regression suite: 48
failed files / 166 failed tests / 5078 passed / 8 skipped (5252
total), exactly matching the pre-existing baseline (166 failed, all
pre-verified unrelated) plus these 2 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(init): seed every configured root, including the HOME hook-scope root
Third round of codex-review findings on PR #873:
1. seedToolRoots (formerly inline) picked only one directory path via
nullish coalescing (skills ?? rules ?? agents), so a custom agent
configured with both e.g. skills and rules at different roots only
got its skills root created — rules stayed "not installed" forever.
Fixed: collect every distinct configured root into a Set and
ensureDir each one.
2. A bare root-level file path (no "/", e.g. claudemd: "AGENTS.md")
made toolInstallRoot return the path unchanged, so the previous fix
would ensureDir a bogus directory literally named "AGENTS.md".
Fixed: skip a file path whose toolInstallRoot equals itself — there
is no parent directory to create for it.
3. Non-self project scope injects hooks into HOME, not the project
root (resolveHookScope, #264 — `~/.claude` always exists for a
built-in tool, so that gate passes without help). Seeding only
under the project root left a custom agent's HOME root missing, so
`init --scope project --agent <custom>` created its resource dirs
correctly but silently skipped its session-start auto-pull hook.
Fixed: seedSelfModeToolDirs now also seeds the hook-scope root when
it differs from the config's own base dir.
Test plan: 3 new tests (multi-root seeding, bare-file-path no bogus
dir, HOME hook-scope root in project scope) — 30/30 passing. Typecheck
clean, real-CLI e2e whitelist test (#510) still passes, targeted
hooks-cmd/hook-dispatch-scope/init/doctor tests match their
pre-existing baselines exactly (verified via git stash comparison).
Full regression suite: 48 failed files / 166 failed tests / 5081
passed / 8 skipped (5255 total), exactly matching the pre-existing
baseline (166 failed, all pre-verified unrelated) plus these 3 new
passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(init): seed bare settings/hooks files, restrict HOME pass to hook paths
Fourth round of codex-review findings on PR #873:
1. Skipping a bare root-level settings/hooks path (no parent directory
to create) left hook installation permanently broken instead of
fixed: reconcileHooksToAllTools's gate is
`pathExists(join(baseDir, toolInstallRoot(paths.settings)))`, and
toolInstallRoot degenerates to the path unchanged when there is no
"/" — so the gate checks whether the settings FILE itself exists,
which seeding never created. reconcileHooks already treats a
missing settings/hooks file as `{}` (readJson(...) ?? {}), so this
now seeds an empty JSON object at the bare path instead of skipping
— satisfies the gate and gives it something valid to merge into.
claudemd is deliberately NOT included: its own bare-root "installed"
check in local-agent.ts keys off a `.${tool}` directory convention
unrelated to the claudemd path itself, so a bare claudemd path is
still left alone (extending seeding there would guess at a
convention no custom agent is guaranteed to follow).
2. The HOME hook-scope second pass (added last round for #264) was
seeding every resource root — skills/rules/agents/claudemd — under
HOME, not just what hook installation reads. A custom agent with
only `skills` configured (no settings-based hook surface at all)
got a meaningless `~/.aa` created for nothing. Extracted the
settings/hooks-only logic into seedHookInstallRoot and restricted
the HOME pass to just that, leaving the full seedToolRoots pass for
the config's own base dir only.
Test plan: 2 new tests (bare settings path seeds `{}`, not a
directory; HOME pass skips skills-only custom agents) — 32/32 passing.
Typecheck clean, real-CLI e2e whitelist test (#510) still passes. Full
regression suite: 48 failed files / 166 failed tests / 5083 passed / 8
skipped (5257 total), exactly matching the pre-existing baseline (166
failed, all pre-verified unrelated) plus these 2 new passing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(init): scope the custom-agent --agent seeding claim to git-backed init
Fifth round of codex-review findings on PR #873 (P2 non-blocking):
seedSelfModeToolDirs cannot seed a custom agent in HTTP init. initHttp
writes a local teamai.yaml stub with no toolPaths at all (Step 2,
'sharing: {}' only) — HTTP mode deliberately never clones a real
teamai.yaml; delivery works entirely through its own report/sync/ack
mechanism instead (refreshTeamRepo's http branch: "no repo tree to
pull here"). So `--agent AA` in HTTP mode records AA in enabledAgents
but seedSelfModeToolDirs finds no configured paths for it and creates
nothing.
Fetching a custom agent's remote paths at HTTP init time would need a
new API call HTTP mode doesn't have today — real work disproportionate
to a P2, and orthogonal to #867 (a git/self-mode custom-agent-root
bug). Took the bot's own suggested alternative instead: narrowed the
"(any mode)" claim in the --agent help text (src/index.ts,
skill-data/core/references/commands.md) and the FAQ
(docs/usage-guide.md + zh-CN) to say plainly that custom-agent seeding
is git-backed-init only, and HTTP init only ever seeds built-in tools
that are already installed.
Docs/help-text only — no logic changed. Typecheck clean, build clean,
self-mode-agents.test.ts still 32/32 (unaffected).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
f8335ec40d |
fix(dry-run): stop dry-run and read-only commands persisting config migrations (#893) (#901)
* fix(dry-run): load mcp inject and mcp list without persisting a migration (#893) mcp inject loaded its scope bare, so --dry-run still saved a pending role migration, partition rename or self-mode bootstrap. It now forwards dryRun; mcp list is read-only and loads with dryRun: true unconditionally, as status and list do since #866. * fix(dry-run): forward dryRun to the loader in roles init/add/remove/update (#893) roles list is read-only and loads with dryRun: true unconditionally. * fix(dry-run): forward dryRun to the loader in projects add/update/remove (#893) projects list and projects members are read-only and load with dryRun: true. * fix(dry-run): forward dryRun to the loader in tags add/remove (#893) tags list is read-only and loads with dryRun: true. * fix(dry-run): forward dryRun to the loader in source add/remove/add-http (#893) source list and source browse are read-only and load with dryRun: true. source list also reached a second bare load through loadLocalAgentConfig, whose HTTP backfill reads only repo.kind and repo.url; it now loads with dryRun: true, and the migration persists on the next command that writes. * fix(dry-run): forward dryRun to the loader in remove and uninstall (#893) * fix(dry-run): forward dryRun to the loader in packages install (#893) * fix(dry-run): forward dryRun to the loader in import --from-iwiki/mr/claude/repo (#893) --from-repo-list and --from-org reach the same load through importFromRepo. * fix(dry-run): forward dryRun to the codebase loader; --lint and --status load read-only (#893) * fix(dry-run): forward dryRun to the loader in models switch; models list loads read-only (#893) * fix(dry-run): load read-only commands without persisting a migration (#893) hooks list, members, exclude list, recall status and doctor load with dryRun: true unconditionally, as status and list do since #866. * docs(designs): name which commands take the dry-run detection path (#893) * test(dry-run): pin that every loader on a dry-run or read-only path migrates nothing (#893) LOAD_ONLY_COMMANDS gains the read-only commands and the previews that stay clean on the legacy-role fixture. PREVIEWS covers the commands that fail past the loader on that fixture: it asserts only config.yaml, with the same call without --dry-run as the positive control that the load is on the path. * fix(dry-run): forward dryRun through resolveMemberToolRoots for import --from-claude (#893) scanCandidates resolves Claude's tool root before the loader import.ts already fixed, through a bare load in resolveMemberToolRoots, so the dry run still saved the migration. resolveConfigForDir and findUnreadableProjectConfig take the same optional LoadOptions for the read-only callers below; hook and usage callers pass nothing and behave as before. * fix(dry-run): pass dryRun into loadLocalAgentConfig instead of forcing it (#893) Forcing dryRun there made real hook runs print the [dry-run] migration preview and stop persisting it. source list now passes it through describeLocalAgent; every other caller is unchanged. source add-http forwards { dryRun: options.dryRun } like the rest of the file. * fix(dry-run): load skill list/show, webhook list, stats and digest read-only (#893) detectTeam takes optional LoadOptions; the Stop-hook share gate passes none. * docs(designs): name read-only commands by example, not as every list subcommand (#893) * test(dry-run): prove each row reached the load, and cover the remaining changed sites (#893) Every dry-run row now asserts the loader logged its migration preview, so an early return cannot pass. Adds source browse, codebase --lint, skill list/show, webhook list, digest, projects update/remove, import --from-claude, and a config-only table for members, projects members and stats. * fix(dry-run): keep the pre-command migration from adopting a partition on a dry run (#893) The preAction hook passes dryRun to maybeMigrate, but planMigration and queueKeptInCheckout resolved the partition bare, so pull/push --dry-run still renamed a pre-#546 partition. Both now take the option. * fix(dry-run): load skill get/path read-only through the share gate (#893) blockReason fell back to shareGate(), a bare load, when no team was passed; only skill get, skill path and the catalog reach that fallback. The Stop hook still asks shareGate directly and is unchanged. * refactor(dry-run): give loadWebhookConfig the option instead of copying its branch (#893) Also note in loadLocalAgentConfig that dryRun covers only its config.yaml load. * revert(dry-run): leave stats out of this change (#893) stats also loads bare per session through the dashboard scope helpers on the pull/report path; a partial fix would claim more than it does. Tracked in the follow-up issue with the other dry-run gaps. * test(dry-run): cover the pre-command migration and skill get/path share (#893) * refactor(dry-run): give shareGate the option instead of repeating it in blockReason (#893) Also correct the test comment on when the pre-command migration runs. * fix(dry-run): keep loadLocalAgentConfig from writing config.json under dryRun (#893) The option reached only the config.yaml load. The legacy group-binding cleanup, the binding-key canonicalization and the HTTP backfill still saved config.json, so `teamai source list` could rewrite or create it. Under dryRun they now stay in memory, and the cleanup prints a `[dry-run] Would remove` preview instead of `Removed`. * fix(dry-run): keep models list from saving a re-bound beta key (#893) models list loaded the scope read-only but read the team keys through loadTeamValues without the option, so a key a 0.26.0 beta stored under the profile id alone was bound to its gateway and the values file rewritten. It now binds the key in memory only; the next write command saves it. |
||
|
|
417e70427c |
fix(pull): keep a skill, rule or agent you edited instead of overwriting it (#822) (#865)
* fix(pull): keep a skill, rule or agent you edited instead of overwriting it (#822) Pull records the sha256 of the bytes it writes at each skill, rule and agent destination in the checkout's record (`delivered`), and the pre-push sync records its writes too. On a full sync, a copy that no longer matches the record is kept and named, with a warning when the team version moved since; push repeats that warning. Tombstone cleanup and the rules sweep keep an edited copy the same way. `--force` keeps edits, `--dry-run` prints `Would keep`, and `doctor` does not fail on a kept copy. Without a record (first pull on this version, a new worktree) pull overwrites as before. * fix(rules): drop a copy the rules reclaim removes from the delivered record (#822) #815 removes an unedited copy of a team rule when none reaches the directory, but the pull's delivered record kept its hash. Forget it there, as the stale-rule sweep does, so the record matches what is on disk. |
||
|
|
671f509f70 |
fix(dry-run): thread { dryRun } into the queue lock, so a preview writes nothing (#896)
`--dry-run` promises no changes made. On a fresh self-mode clone one write still gets through: an empty `<home>/.teamai/locks/` is created and left behind. It is only the directory, not a lock file, but it is a real filesystem change and the suite can see it -- `dry-run-load-path.test.ts` declares it as a tolerated entry for `pull` today, which is the honest way of saying the preview is not clean. The mechanism is already on main: #866 gave `acquireLock` an `options.dryRun` that reads the lock state instead of taking it, and `pull.ts:1873`, `push.ts:784` and `push.ts:839` pass it. The queue lock does not: learnings-publish.ts:75 syncLock === null || await acquireLock(syncLock) <- not passed learnings-publish.ts:93 listPendingForInstall(localConfig) <- not passed pending-learnings.ts:77 withQueueLock(...) -> acquireQueueLock(home) <- not passed pending-learnings.ts:66 if (await acquireLock(lockPath)) <- takes the real lock `pull` counts the queue so it can report how many learnings it would publish, and counting takes the queue lock, because the queue owns it. Taking that lock is a write: `acquireLock` ensures the lock parent (`update.ts:404`) and `releaseLock` removes the lock FILE but not that directory (`update.ts:464`), so the directory outlives the command. This threads the flag down. Three signatures gain an optional `{ dryRun?: boolean } = {}` and forward it; existing callers (`migrate.ts:413`, `migrate.ts:891`, and the `withQueueLock` inside `savePendingLearning`) send `{ dryRun: undefined }` and are unchanged. `readPendingForInstall` deliberately does not get the parameter: its only caller is behind `if (locked && !dryRun)` (`learnings-publish.ts:94`), so a preview never reaches it. The test gets stricter -- `pull`'s allowed list goes from `[PULL_LOCK_DIR]` to `[]`. `snapshotTree` records directories as `"<rel>/" = 'dir'`, so an empty directory is counted; that is why the entry had to be written at all. Verification: - before: `vitest run src/__tests__/dry-run-load-path.test.ts` -> 22 passed - after: same -> 22 passed, with `pull`'s allowed list empty - mutation: reverting `acquireLock(lockPath, options)` to `acquireLock(lockPath)` turns the case red and names the cause, "appeared": ["home\\.teamai\\locks/"] - no collateral: `lock-atomic` 22/22, `pull-post-checks` 13/13, `pull-placement-reconcile` 2/2; `tsc --noEmit` rc=0 and `oxlint --deny-warnings` rc=0 - the locally-failing files (`pending-learnings`, `git-kind-learnings`, `checkout-refusal-silent`) fail identically on the unmodified base: POSIX path assertions on Windows and EBUSY from git-worktree fixtures in workers |
||
|
|
5fddf0c5d1 |
fix(test): keep CI validate from timing out on the usage lock and an unborn HEAD (#897)
A busy event loop stretched the usage rewrite wait past its 5s budget because the wait counted sleeps. Bound it by wall time. The viz fixture also cloned a bare repo whose HEAD stayed on master under git 2.39, so worktree add failed with "invalid reference: HEAD". Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
cd3a914112 |
fix(env): keep the user scope's env block when a project pulls (#878)
* fix(env): make doctor and uninstall act on their own scope's env block (#876) A shell profile can carry a user-scope block and a project-scope block, but doctor, uninstall and the active-profile resolver only ever read the first `# [teamai:env:start]` pair. Doctor in a project then blamed the #661 backslash for the user scope's block, uninstall's plan missed a second block of its own, and its cleanup step removed the first block whatever scope it belonged to. Add findEnvBlocks/findEnvBlockFor, which pick a block by the env.sh it sources (the ownership test uninstall already used), and use them in all three consumers. With only another scope's block present, doctor now says the profile carries no block for this env.sh. The ownership test also accepts the all-backslash spelling of the path, so a #661 block of this scope keeps its diagnosis. The marker format and how pull writes the profile are unchanged. * fix(env): keep the user scope's env block when a project pulls (#876) Each pull replaced the first `# [teamai:env:start]` block in the shell profile, whichever scope it belonged to, so a member with a user and a project scope got only the last-pulled scope's env in new shells. Injection now picks the block by the env.sh it sources. A scope replaces its own block. Otherwise a project takes over another project's block, never the user scope's (recognised by the env.sh the user-scope config resolves to), and a user scope goes in right before the project block. The user block therefore precedes the project block, so a project value wins on a shared key, and the project block stays last-wins. Other blocks are left alone; the marker format and the skip-when-unchanged write are unchanged. * fix(env): address #876 review findings on env block ownership - Drop the all-backslash spelling from candidateSpellings; the #661 doctor tests now use a Windows-form data home instead. - envBlockReferencesDataHome requires a path boundary before a raw spelling, so /home/me/.teamai/env.sh no longer claims a block that sources /data/home/me/.teamai/env.sh. - resolveActiveShellProfile falls back to the first file along the sourcing chain that holds any teamai block when none holds this scope's, so a first user pull or a second project pull on the Git Bash chain lands next to the existing block instead of after the .bashrc source line. - injectShellProfile uses one "user scope's block" predicate for both scopes; a block that sources no env.sh (pre-env.sh inline exports) is the user scope's, so a project pull no longer takes it over. - Tests build well-formed blocks with EnvHandler.generateShellBlock. Refs #876 * fix(env): match an env.sh path only up to its end (#876) * fix(env): keep the user block when the user config does not parse (#876) |
||
|
|
f19f9de23a |
ci(lint): enable type-aware linting with no-floating-promises and no-misused-promises (#836) (#863)
* ci(lint): enable type-aware linting with no-floating-promises and no-misused-promises tsc does not report a promise nobody awaits or an async callback handed to an API that ignores its result. In a CLI that is an error nobody sees. oxlint's --type-aware mode checks both through oxlint-tsgolint. - package.json: add oxlint-tsgolint 7.0.2003 (oxlint 1.85.0 needs >=7.0.2001) and pass --type-aware to `npm run lint`. The package ships prebuilt binaries as optional dependencies with no install script, so CI's `npm ci --ignore-scripts` needs no change. - .oxlintrc.json: turn on typescript/no-floating-promises and typescript/no-misused-promises. Under src/__tests__/**, no-misused-promises skips checksVoidReturn.arguments: vi.spyOn(fse, ...) types the spy from fs-extra's last overload, the callback one that returns void, so every correct async mockImplementation is flagged. - --type-aware also turns on oxlint's default type-aware rules. The ones with hits on main stay off, since each is its own evaluation under #836. The rest of the defaults have no hits and stay on. Fixes, all behavior-neutral: - dashboard.ts: the SSE debounce and the PID check become named async functions called with `void`; both already catch everything they throw. The HTTP handler becomes handleRequest, and createServer catches its rejection, logs it and answers 500. - import-repo-list.ts: track in-flight imports in a Set, so the unused array splice returned is gone. - scripts/mock-teamai-server.mjs: same handler wrapper as the dashboard, so a malformed JSON body answers 500 instead of an unhandled rejection. - ai-client.test.ts: schedule the mock process events with queueMicrotask. Refs #836. * ci(lint): turn on typescript/no-useless-default-assignment 1 prod hit, type-only: syncTeamUpdatesToLocal's last parameter was `placedRules: Record<string, string> | undefined = undefined`; it is now `placedRules?: Record<string, string>`. Refs #836. * ci(lint): list only the type-aware rules that would otherwise run as off consistent-return, no-unnecessary-boolean-literal-compare, no-unnecessary-type-arguments, no-unnecessary-type-assertion, no-unnecessary-type-conversion, no-unnecessary-type-parameters and no-unsafe-type-assertion are not default rules, so listing them as off changed nothing. The config keeps off only the nine default type-aware rules that --type-aware would turn on. `oxlint --type-aware --print-config` gives the same set of active rules before and after. Refs #836. |
||
|
|
facc6104f6 |
fix: keep self-update off npm link checkouts, and report hook injection only on change (#871) (#874)
* fix(update): leave an npm link checkout alone instead of replacing it with the registry package When the running CLI is not under node_modules (an npm link checkout), resolveInstallPrefix returns null and doUpdate ran a prefix-less npm install -g. That lands in npm's default global prefix, where npm link put its symlink, so the next hook run swapped the checkout for the published package. Skip the install and say how to update instead. * fix(hooks): report OMP, OpenCode and Pi hook injection only when the file changes These injectors rewrote their file and logged success on every pull, so an up-to-date pull listed them as injected while Claude and Codex, which already compare before writing, printed nothing. Use writeIfChanged and log at debug level when the file is current. * fix(update): refuse an unsupported install before prompting, and keep the skip in debug.log Review follow-ups: resolve the install target before the prompt policy so nobody confirms an update that is then skipped; persist the skip warning because hooks discard stderr; word it for every null target, not only a link; tell the user to pull before rebuilding; pass --registry in the suggested install command. * fix(hooks): report Hermes and OpenClaw hook injection only when something changes Both are reached by the pull reconcile and still logged success on every run. OpenClaw now writes its two files with writeIfChanged; Hermes also reports whether its config.yaml entry and allowlist were written. * fix(update): persist the vendored-install skip to debug.log too Adversarial review follow-up: the Stop hook discards stderr, so the vendored-layout refusal left no trace while the linked-checkout refusal next to it is persisted. |
||
|
|
c7874434c9 | fix(dashboard): keep SSE live after event log compaction (#888) | ||
|
|
a5b36a8a73 | fix(hooks): release stdin after read timeout (#889) | ||
|
|
4284918b83 | fix(tests): isolate Claude config dir from model tests (#890) | ||
|
|
479f811b52 | fix(recall): normalize scores across knowledge sources (#891) | ||
|
|
11657bcfab | fix(recall): read each agent's session variable so recall quality joins its session (#887) | ||
|
|
f836db4230 | fix(push): publish the env files env add leaves in a standalone clone (#881) (#885) | ||
|
|
732c42cb6d | fix(rules): reclaim delivered team rules when none is selected (#802) (#815) | ||
|
|
b3b3a0b03c |
fix(dry-run): thread { dryRun } through the loaders pull, push, status and list use (#866)
* fix(dry-run): thread { dryRun } through the loaders pull, push, status and list use
`--dry-run` is documented as previewing without making changes, and #837 made
that true for `tags subscribe`, `tags unsubscribe` and `roles set`. `pull`,
`push` and `status` still wrote: each runs its scope-detection block before any
dry-run guard, and that block called config loaders that were never given the
flag. A preview could therefore persist the legacy role migration, and in a git
repo adopt a pre-#546 partition or run the single-repo self-heal bootstrap.
The loaders already take LoadOptions — #853 threaded them through
`loadLocalConfigForScope` for contribute / session save / recall. These
commands simply did not supply the flag.
- pull.ts: both loaders take { dryRun: options.dryRun }.
- push.ts: autoDetectInit takes it.
- status.ts and list: { dryRun: true } unconditionally, because both are
read-only and should never migrate, adopt a partition or bootstrap.
Callers that pass nothing behave as before, the same compatibility promise
#837 made. The one observable change is that the preview path logs, so
`status`/`list` now surface a "[dry-run] Would ..." line where a migration or
bootstrap is pending; the PR description asks for a decision on that label.
Verification: seven new command-level cases, each failing on unmodified main
with the identical test file (the project-scope three need a git project with
a pre-#546 partition name, which the existing user-scope fixture never
reaches); and the issue's own real-CLI reproduction, 6/6 — the control writes
the reported fields, this change writes nothing.
oxlint --deny-warnings and tsc --noEmit: rc=0 here and rc=0 on unmodified main.
Closes #850
* fix(pull,push): a --dry-run must leave a fresh self-mode clone alone
Resolves both P1 findings on this PR.
pull.ts:1891 - the previewed self-mode config reached lockScope(), whose
acquireLock() calls ensureDir() on the lock's parent. On a fresh clone the
partition does not exist yet, so the directory was created and stayed:
releaseLock removes the lock file, not its parent. The guard sits inside
lockScope(), the one choke point all three call sites share.
push.ts:731 - the same previewed config ran the whole self-mode setup before
pushCore reached its own dry-run guard at push.ts:1577: the sync-lock,
migrateSelfModeGitignore(), and the disposable knowledge worktree.
Both guards are deliberately narrow. A blanket early return before the
git-mode branch would also skip resetToCleanMaster/pullRepo (push.ts:934),
which a dry run performs on purpose so it can name the destination the real
command would use. Only writes that outlive the command are gated.
The preview still reads the uncommitted teamai.yaml that pushCore receives
as initialPendingTeamConfig; that block is now pendingSelfTeamConfig(), the
identical read, so the preview keeps describing the config edit it exists to
describe.
Fixture gap, also flagged: dry-run-load-path.test.ts already had a
fresh-self-mode-clone fixture, but only tags/roles ran against it. pull and
push ran at user scope, or on a project partition that already exists - never
on the one shape where acquireLock has something new to create. Two cases
added there; the unfixed tree fails them at
fs.existsSync(<HOME>/.teamai/projects) with "expected true to be false".
Not fixed here, and named in the PR description: pull --dry-run on a fresh
clone still creates an empty <HOME>/.teamai/locks/, via listPendingForInstall
in utils/pending-learnings.ts. That call is unchanged by this PR and the file
is outside its scope; the test declares and counts the entry, so anything
else appearing still fails.
* test(pull): pin the loader call shape the widened signature produces
Fixes the red Lint & Test on the previous head (all four matrix entries).
pull-scope-isolation.test.ts asserted the exact argument list of
loadLocalConfigForScope, which this change widens to carry LoadOptions:
expect(loadLocalConfigForScope).toHaveBeenCalledWith('user');
received ['user', undefined, { dryRun: undefined }]
The shape is not a free choice: recall.ts:464 and recall.ts:515 already pass
the same third argument, landed with #853, so pull.ts matches the merged
precedent. recall-scope-isolation.test.ts never asserts the argument list,
which is why the same change left it green.
The affected set is derived from the changed symbols rather than from the
topic: every test file that mentions loadLocalConfigForScope,
detectProjectConfig or autoDetectInit (76 files, 1221 tests).
* fix(update): a --dry-run asks the lock for its state instead of taking it
`acquireLock` is not a read. It `ensureDir`s the lock's parent, which on a
fresh self-mode clone is a `<getDataHome>` partition that does not exist yet —
and `releaseLock` removes the lock FILE, not that directory, so the directory
outlives the command. Any preview that calls it therefore writes, which is the
defect this PR is about (#866).
The new `options.dryRun` returns what the preview actually owes its caller: the
ANSWER the real run would get. `lockState` already separates `live` (a holder
is running) from `stale` and `missing`, and the real run reclaims either of the
latter and wins, so `acquireLock(path, { dryRun: true })` is exactly that
verdict — no mkdir, no lock file, no reclaim sentinel.
Nothing is recorded in `heldLockOwners`, which is what makes the preview safe
alongside the existing `releaseLock` calls: it returns at its first line when
it holds no owner token for the path, so a preview cannot delete a lock another
process owns.
No caller passes `dryRun` yet; this commit is the primitive only.
* fix(pull,push): acquire the preview's locks read-only, and stop hiding what it reports
Supersedes the guards added in
|
||
|
|
e2fd0abf11 |
fix(remove): remove a root MCP server named 'ambiguous' instead of refusing it silently (#862) (#864)
mcpRemovalTarget returned string | 'ambiguous' | null, so a root server named 'ambiguous' came back as its own name and the caller read it as the refusal sentinel: nothing removed, exit 1, no reason printed. Return a tagged result and switch on its kind. |
||
|
|
53c096e02f |
fix(git): spawn git by its resolved path so macOS skips a PATH search per call (#869)
* fix(git): spawn git by its resolved path so macOS skips a PATH search per call On macOS, a bare-name spawn of git costs a few milliseconds for every PATH entry ahead of git's directory: 30-67 ms per call with a typical PATH, while git itself takes about 8 ms. createGit now passes simple-git the absolute path of the git that lookup would pick, resolved once per PATH value. Refs #868 * fix(git): leave unreadable git candidates to the spawn's own lookup A stat error other than ENOENT/ENOTDIR (a symlink loop) now keeps the bare name instead of moving on to a later git. Narrow the CHANGELOG to the simple-git paths and correct two comments. Refs #868 * fix(git): keep the bare name when the first git on PATH does not start A bare-name spawn moves past a PATH entry whose spawn fails, such as a script whose interpreter is gone, while a spawn by path just fails. gitBinary now spawns the candidate once (git --version) and keeps the bare name unless it starts. POSIX-only gitBinary cases skip on win32. * fix(git): look PATH up again when the resolved git is gone or not executable A bare-name spawn walks PATH on every call, so a git removed or chmod'ed mid-process is skipped. gitBinary's cache kept the stale absolute path. One access(2) per call now drops a cached path that is no longer executable. |
||
|
|
5fb316c7b8 |
fix(code-knowledge): keep tree-sitter grammars off V8's optimizing Wasm tier on Node 24 (#860) (#861)
On Node 24, V8's Turboshaft Wasm compiler exhausts its Zone memory while tiering up tree-sitter grammar code and aborts the process with "Fatal process out of memory: Zone" (nodejs/node#63421). The Swift grammar added in #842 hits it right after its first parse, so `teamai codebase --extract` on a repo with .swift files died with exit 133 and no fallback. Set --wasm-tier-up-filter to an index no grammar has before the WASM runtime starts, on Node 24 and later only. --liftoff-only has the same effect but Node 24 ignores it when set at runtime. Node 20 and 22 keep tier-up, which parses about 1.6x faster there. Add Node 24 to the CI matrix so the regression test runs where it can fail. Closes #860 Refs #842 |
||
|
|
e9368701a7 |
fix(stats): split Windows cwds on the backslash in attributeRepo (#858)
* fix(stats): split Windows cwds on the backslash in attributeRepo `attributeRepo` split a filesystem cwd on `/` only, so on Windows every `cwd` stayed one segment: `C:\Users\dev\work\teamai-cli` was attributed to a repo literally named `C:\Users\dev\work\teamai-cli` instead of `teamai-cli`. The `NON_REPO_LEAVES` check (`home`, `data`, `tmp`, ...) never fired either, so a session started in `C:\src\data` opened its own row instead of merging into `no_repo` like `/src/data` does. `repoLabel` masked most of it on Windows, because `path.basename` handles both separators and the repo-directory branch of `nameOf` runs first. The gap shows wherever the path is not a live repo directory: a worktree removed after the fact (#810), a session recorded outside git, or a dashboard event whose anchor no longer exists. A drive root (`C:\`) also left the bare drive letter as the leaf, where `/` on POSIX yields `no_repo`. Split on both separators and treat a bare drive letter as no project, so Windows cwds attribute the way POSIX ones already do. This also fixes `repoLabel`'s "a directory named like workspace is still no_repo" test on Windows, which failed on `origin/main` for the same reason. * test(e2e): retry the #810 sandbox removal past a detached hook child `E2E (fork-safe, no credentials)` fails this suite on Linux with Error: ENOTEMPTY: directory not empty, rmdir '/tmp/teamai-issue810-e2e-*/home/.teamai' after all 12 tests pass, so the run reports `1 failed | 56 passed` suites with `356 passed | 26 skipped` tests and exit code 1. Every hook here goes through `hook-dispatch`, which spawns a detached child for the background-only handlers (SessionEnd, Stop) — `spawnPlainDetached` in src/hook-dispatch-cli.ts. The suite waits for the events that child writes, and never for the child itself to exit, so `afterAll`'s removal runs while it may still be creating a file under `.teamai`. One `rmdir` then loses the race and the suite is reported failed with nothing actually wrong. Add the retry options `git-kind-learnings.test.ts` already uses for the same `git gc --auto` race. Node's rm retries EBUSY, EMFILE, ENFILE, ENOTEMPTY and EPERM with a linear backoff, which covers both this and the Windows `EPERM` this suite also hits locally. Unrelated to the attributeRepo change in the parent commit: the suite never calls it, and the diff is an identity transform on every POSIX-shaped cwd the suite produces (verified over 35 inputs, including each `teamai-issue810-e2e-*` path). The Windows `EPERM` failure is pre-existing — it reproduces identically on a clean origin/main checkout — and persists here because it is a persistent condition, not a transient one; this change does not make it worse. --------- Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com> |
||
|
|
2169f2e7e7 |
fix(iwiki): stop fetchAllPages from rejecting every space walk before it starts (#849)
The 'initial queue is empty' guard ran after tryDrain(), which had already dispatched the root request: tryDrain shifts the only queue entry, so the queue is always empty by then and the promise rejected with 'fetchAllPages: rootId 队列为空' on every call, before any response could arrive. Zero-latency mocks hid it — the first response beat the rejection and populated allPages, so the catch guard (allPages.length === 0) swallowed the error. Check the guard before tryDrain() and seed the queue only for a non-empty rootId, so it rejects exactly when there is nothing to walk. Also drop the dead '!stopped' from the drain loop (tryDrain already returns when stopped, and stopped cannot change inside the synchronous loop — oxlint no-unmodified-loop-condition, the hit #836 flags), and write the file's user-facing messages in English per the repo rule. Co-authored-by: ydflow <ydflow@users.noreply.github.com> |