Repository navigation
fix(cli): derive projectId from prefixed issue id on spawn - #1330
Conversation
When `ao spawn <projectId>/<issue>` is used in a multi-project config, route the spawn to the prefixed project and strip the prefix from the issue id. Previously the projectId fell back to whichever project `ao start` was running for, tagging cross-project sessions with the wrong project (and session prefix). Applies to both `ao spawn` and `ao batch-spawn`; batch-spawn errors out if issues mix multiple project prefixes. Fixes #1329
Test Coverage Report
Per-file breakdown
Uncovered lines
|
Move the issue-prefix → project routing from the CLI into a reusable core utility, `resolveSpawnTarget(projects, issueRef, fallback?)`. - Exposes routing to any spawn entry point (CLI, web API, programmatic). - Accepts either a project id or sessionPrefix as the prefix; project id wins on collision. - `ao batch-spawn` now groups issues by resolved project and preflights once per group instead of erroring on mixed prefixes. Tests: - 8 unit tests for `resolveSpawnTarget` in core. - CLI spawn.test.ts: adds a sessionPrefix routing test (23 tests total). Refs #1329
Self-review found three gaps:
- `resolveSpawnTarget` accepted `undefined` issueRef and returned
`{ issueId: "" }` with a fallback project. Dead path — both callers
guard against undefined. Drop it and make issueRef required.
- No tests for `batch-spawn`. Added two:
- routes cross-project prefixed issues to the correct project and
lists each project's sessions separately
- skips a prefixed issue when the target project already has an
active session for it
- `spawn` and `batch-spawn` help text didn't document the
`<projectId>/<issue>` or `<sessionPrefix>/<issue>` forms.
Self-reviewReviewed the diff for bugs. Found three minor issues, all fixed in 9c636f1. Issues found and fixed1. Dead 2. No tests for
3. Help text didn't document the new forms Non-issues considered and left alone
Final state
|
Second review pass found a latent bug: Plain JS objects inherit `__proto__`, `constructor`, `toString`, `hasOwnProperty`, etc. from Object.prototype. The previous `if (projects[prefix])` check entered the routing branch for any of these keys, mis-routing a user typing `ao spawn __proto__/42` with `projectId: "__proto__"`. Downstream session-manager would then receive a junk project object (Object.prototype itself) and fail with a non-obvious error. Fix: use `Object.prototype.hasOwnProperty.call(projects, prefix)` so only actual configured project ids match. Also: - Document the case-sensitive matching semantic explicitly. - Lock in the batch-spawn grouping contract by asserting exact `list()` and `spawn()` call counts in the cross-project test.
Second self-reviewRan another pass (plus an independent agent review). Found one real correctness bug and two minor tightening opportunities. All addressed in b64a672. Bug found and fixedPrototype-key match in Fix: Minor tightening
Considered and left alone
Final state
|
Third self-review (fresh eyes)Re-read the final PR state end-to-end without referring to prior reviews. No new bugs found, but two observations worth flagging for the PR record. Observations (not bugs — just behavioral changes reviewers may ask about)Branch naming changes for prefixed issues. Per-group preflight even when the group spawns nothing. Dead-code sanity check
Re-verified invariants
Final tally
No further changes recommended. Ready for review. |
- Rewrite the @aoagents/ao-core changeset to describe only what feat/1226 contributes: event enrichment with schemaVersion: 2, issueTitle persistence, executeReaction refactor, maybeDispatchCIFailureDetails, and bugbot-comments enrichment. Drop the false claims about adding spawn-target and format-automated-comments (those modules came from upstream PRs OrchestratorInc#1330 and OrchestratorInc#1334). - Delete stop-kill-all-resilience.md — its claims (kill-all loop, fd-safety in tryAcquire, allSessionPrefixes unify) are no longer in the branch after the rebase took upstream's canonical stop logic. Co-Authored-By: Claude Opus 4.7 <[email protected]>
* fix: serialize ao start and stop numbered orchestrators (#1306) * fix: restore dead orchestrators on start (#1306) * fix: harden startup lock handling (#1306) * feat(core): enrich events with PR title, description, and URL Adds PR and issue context to all event payloads sent to notifiers. External consumers (Telegram, Discord, n8n) can now display meaningful information without making additional API calls. Changes: - Add buildEventContext() helper to extract PR/issue context from session - Enrich all createEvent() calls with context data (pr, issueId, issueTitle, branch) - Store issueTitle in session metadata during spawn - Add issueTitle field to SessionMetadata interface - Update executeReaction() to accept session for context access - Add tests for event enrichment The context includes: - pr: { url, title, number, branch } when PR exists - issueId: issue identifier - issueTitle: issue title (from tracker during spawn) - branch: session branch name Events before PR creation gracefully omit PR fields (pr: null). Existing webhook consumers that ignore unknown fields are unaffected. Closes #1226 Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(core): address review comments for event enrichment - Add issueTitle to readMetadata/writeMetadata for proper persistence - Create ReactionSessionContext type for type-safe system events - Replace unsafe `as unknown as Session` cast with proper union type - Add end-to-end test verifying issueTitle persistence during spawn Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(cli): include lock file path in startup lock error message Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address review feedback — fd safety, kill-all resilience, issueTitle restore - Restore try/catch/finally in tryAcquire for fd leak prevention - Wrap stop command's sm.list()+kill in try/catch so dashboard shutdown always runs even on session listing failure - Add per-iteration error handling in kill-all loop with partial failure reporting (spinner.warn for mixed results) - Unify allSessionPrefixes derivation between start and stop commands - Propagate issueTitle through archive restore path - Add clarifying comments on agentInfo.summary fallback and intentional prNumber/prUrl duplication in event data - Add ora warn mock for stop tests - Update changeset to minor (event enrichment is a feature) and add CLI changeset for stop resilience - Add tests for kill-all error mid-loop and issueTitle archive restore Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address harsh-batheja review — title/summary split, lock grace, context namespace - Separate PR title from agent summary in EventContext: title is null until enrichment cache populates; summary is a distinct field so webhook consumers never confuse the task summary for a PR title. - Restore UNPARSEABLE_LOCK_GRACE_MS (5s mtime grace) and isStaleUnparseableLock lost during merge conflict resolution — prevents lockfile-steal race when process A just created the file but hasn't written metadata yet. - Fix orchestrator sort: extract numeric suffix instead of localeCompare so -10 sorts after -2, not before. - Namespace context under data.context instead of spreading into data to prevent field collisions with reaction-specific keys. - Add schemaVersion: 2 to all enriched events so consumers can migrate away from top-level prNumber/prUrl (kept for compat, marked for removal in v3). - Update event enrichment tests for nested context structure. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: resolve merge conflicts — use formatReviewCommentsMessage, fix batch enrichment mock - Replace formatAutomatedCommentsMessage with upstream's formatReviewCommentsMessage for automated review comment dispatch (fixes type mismatch with ReviewComment[]) - Make createMockSCM's enrichSessionsPRBatch dynamically resolve from individual method mocks so test overrides (e.g. getPRState("closed")) propagate correctly - Add explicit enrichSessionsPRBatch to merge-conflict-tracking test to avoid unexpected getMergeability calls from the dynamic mock - Remove all debug console.log statements added during troubleshooting Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: wire up maybeDispatchCIFailureDetails and record dispatch hash on transition The function was defined but never called after merge conflict resolution dropped the call site. Added it back to the Promise.allSettled alongside maybeDispatchReviewBacklog and maybeDispatchMergeConflicts. Also updated the transition-reaction early-return to record the dispatch hash, since the transition path now enriches the CI message with detailed check info from the batch cache — preventing duplicate sends on subsequent polls. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test: remove stale duplicate test from rebase Removes the orphaned numbered-orchestrator restoration test left over from feat/1226 history. Upstream's canonical model test (same name, expects "app-orchestrator" via ensureOrchestrator) supersedes it. Co-Authored-By: Claude Opus 4.7 <[email protected]> * docs: remove misleading CLI CHANGELOG entry The "Restore the most recently active dead orchestrator on ao start" entry described upstream's ensureOrchestrator behavior (#1487), not work contributed by this PR. Co-Authored-By: Claude Opus 4.7 <[email protected]> * docs(changeset): correct scope and drop stale CLI changeset - Rewrite the @aoagents/ao-core changeset to describe only what feat/1226 contributes: event enrichment with schemaVersion: 2, issueTitle persistence, executeReaction refactor, maybeDispatchCIFailureDetails, and bugbot-comments enrichment. Drop the false claims about adding spawn-target and format-automated-comments (those modules came from upstream PRs #1330 and #1334). - Delete stop-kill-all-resilience.md — its claims (kill-all loop, fd-safety in tryAcquire, allSessionPrefixes unify) are no longer in the branch after the rebase took upstream's canonical stop logic. Co-Authored-By: Claude Opus 4.7 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
…torInc#1330) * fix(cli): derive projectId from prefixed issue id on spawn When `ao spawn <projectId>/<issue>` is used in a multi-project config, route the spawn to the prefixed project and strip the prefix from the issue id. Previously the projectId fell back to whichever project `ao start` was running for, tagging cross-project sessions with the wrong project (and session prefix). Applies to both `ao spawn` and `ao batch-spawn`; batch-spawn errors out if issues mix multiple project prefixes. Fixes OrchestratorInc#1329 * refactor(core,cli): lift spawn target resolution into core Move the issue-prefix → project routing from the CLI into a reusable core utility, `resolveSpawnTarget(projects, issueRef, fallback?)`. - Exposes routing to any spawn entry point (CLI, web API, programmatic). - Accepts either a project id or sessionPrefix as the prefix; project id wins on collision. - `ao batch-spawn` now groups issues by resolved project and preflights once per group instead of erroring on mixed prefixes. Tests: - 8 unit tests for `resolveSpawnTarget` in core. - CLI spawn.test.ts: adds a sessionPrefix routing test (23 tests total). Refs OrchestratorInc#1329 * test(cli): cover batch-spawn grouping; tighten resolveSpawnTarget API Self-review found three gaps: - `resolveSpawnTarget` accepted `undefined` issueRef and returned `{ issueId: "" }` with a fallback project. Dead path — both callers guard against undefined. Drop it and make issueRef required. - No tests for `batch-spawn`. Added two: - routes cross-project prefixed issues to the correct project and lists each project's sessions separately - skips a prefixed issue when the target project already has an active session for it - `spawn` and `batch-spawn` help text didn't document the `<projectId>/<issue>` or `<sessionPrefix>/<issue>` forms. * fix(core): guard resolveSpawnTarget against prototype-key matches Second review pass found a latent bug: Plain JS objects inherit `__proto__`, `constructor`, `toString`, `hasOwnProperty`, etc. from Object.prototype. The previous `if (projects[prefix])` check entered the routing branch for any of these keys, mis-routing a user typing `ao spawn __proto__/42` with `projectId: "__proto__"`. Downstream session-manager would then receive a junk project object (Object.prototype itself) and fail with a non-obvious error. Fix: use `Object.prototype.hasOwnProperty.call(projects, prefix)` so only actual configured project ids match. Also: - Document the case-sensitive matching semantic explicitly. - Lock in the batch-spawn grouping contract by asserting exact `list()` and `spawn()` call counts in the cross-project test.
…tratorInc#1326) * fix: serialize ao start and stop numbered orchestrators (OrchestratorInc#1306) * fix: restore dead orchestrators on start (OrchestratorInc#1306) * fix: harden startup lock handling (OrchestratorInc#1306) * feat(core): enrich events with PR title, description, and URL Adds PR and issue context to all event payloads sent to notifiers. External consumers (Telegram, Discord, n8n) can now display meaningful information without making additional API calls. Changes: - Add buildEventContext() helper to extract PR/issue context from session - Enrich all createEvent() calls with context data (pr, issueId, issueTitle, branch) - Store issueTitle in session metadata during spawn - Add issueTitle field to SessionMetadata interface - Update executeReaction() to accept session for context access - Add tests for event enrichment The context includes: - pr: { url, title, number, branch } when PR exists - issueId: issue identifier - issueTitle: issue title (from tracker during spawn) - branch: session branch name Events before PR creation gracefully omit PR fields (pr: null). Existing webhook consumers that ignore unknown fields are unaffected. Closes OrchestratorInc#1226 Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(core): address review comments for event enrichment - Add issueTitle to readMetadata/writeMetadata for proper persistence - Create ReactionSessionContext type for type-safe system events - Replace unsafe `as unknown as Session` cast with proper union type - Add end-to-end test verifying issueTitle persistence during spawn Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(cli): include lock file path in startup lock error message Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address review feedback — fd safety, kill-all resilience, issueTitle restore - Restore try/catch/finally in tryAcquire for fd leak prevention - Wrap stop command's sm.list()+kill in try/catch so dashboard shutdown always runs even on session listing failure - Add per-iteration error handling in kill-all loop with partial failure reporting (spinner.warn for mixed results) - Unify allSessionPrefixes derivation between start and stop commands - Propagate issueTitle through archive restore path - Add clarifying comments on agentInfo.summary fallback and intentional prNumber/prUrl duplication in event data - Add ora warn mock for stop tests - Update changeset to minor (event enrichment is a feature) and add CLI changeset for stop resilience - Add tests for kill-all error mid-loop and issueTitle archive restore Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address harsh-batheja review — title/summary split, lock grace, context namespace - Separate PR title from agent summary in EventContext: title is null until enrichment cache populates; summary is a distinct field so webhook consumers never confuse the task summary for a PR title. - Restore UNPARSEABLE_LOCK_GRACE_MS (5s mtime grace) and isStaleUnparseableLock lost during merge conflict resolution — prevents lockfile-steal race when process A just created the file but hasn't written metadata yet. - Fix orchestrator sort: extract numeric suffix instead of localeCompare so -10 sorts after -2, not before. - Namespace context under data.context instead of spreading into data to prevent field collisions with reaction-specific keys. - Add schemaVersion: 2 to all enriched events so consumers can migrate away from top-level prNumber/prUrl (kept for compat, marked for removal in v3). - Update event enrichment tests for nested context structure. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: resolve merge conflicts — use formatReviewCommentsMessage, fix batch enrichment mock - Replace formatAutomatedCommentsMessage with upstream's formatReviewCommentsMessage for automated review comment dispatch (fixes type mismatch with ReviewComment[]) - Make createMockSCM's enrichSessionsPRBatch dynamically resolve from individual method mocks so test overrides (e.g. getPRState("closed")) propagate correctly - Add explicit enrichSessionsPRBatch to merge-conflict-tracking test to avoid unexpected getMergeability calls from the dynamic mock - Remove all debug console.log statements added during troubleshooting Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: wire up maybeDispatchCIFailureDetails and record dispatch hash on transition The function was defined but never called after merge conflict resolution dropped the call site. Added it back to the Promise.allSettled alongside maybeDispatchReviewBacklog and maybeDispatchMergeConflicts. Also updated the transition-reaction early-return to record the dispatch hash, since the transition path now enriches the CI message with detailed check info from the batch cache — preventing duplicate sends on subsequent polls. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test: remove stale duplicate test from rebase Removes the orphaned numbered-orchestrator restoration test left over from feat/1226 history. Upstream's canonical model test (same name, expects "app-orchestrator" via ensureOrchestrator) supersedes it. Co-Authored-By: Claude Opus 4.7 <[email protected]> * docs: remove misleading CLI CHANGELOG entry The "Restore the most recently active dead orchestrator on ao start" entry described upstream's ensureOrchestrator behavior (OrchestratorInc#1487), not work contributed by this PR. Co-Authored-By: Claude Opus 4.7 <[email protected]> * docs(changeset): correct scope and drop stale CLI changeset - Rewrite the @aoagents/ao-core changeset to describe only what feat/1226 contributes: event enrichment with schemaVersion: 2, issueTitle persistence, executeReaction refactor, maybeDispatchCIFailureDetails, and bugbot-comments enrichment. Drop the false claims about adding spawn-target and format-automated-comments (those modules came from upstream PRs OrchestratorInc#1330 and OrchestratorInc#1334). - Delete stop-kill-all-resilience.md — its claims (kill-all loop, fd-safety in tryAcquire, allSessionPrefixes unify) are no longer in the branch after the rebase took upstream's canonical stop logic. Co-Authored-By: Claude Opus 4.7 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
Summary
Fixes cross-project spawn tagging the session with the wrong
projectIdwhenao startis running for a different project than the issue belongs to.resolveSpawnTarget(projects, issueRef, fallback?)— parses<prefix>/<rest>and routes to the matched project. Accepts either a project id (yaml key) orsessionPrefixas the prefix; project id wins on collision. Returnsnullwhen nothing matches and no fallback is supplied.ao spawn <projectId>/<issue>— now routes to the prefixed project and strips the prefix from the issue id. PreviouslyprojectIdfell back to whichever projectao startwas running for, giving the session the wrong prefix and dashboard column.ao spawn <sessionPrefix>/<issue>— also works (e.g.xid/1routes to the project whosesessionPrefixisxid).ao batch-spawn— groups issues by resolved project and runs preflight once per group, instead of erroring on mixed prefixes./api/spawn— unchanged; it already requires an explicitprojectId, so it's immune. Any future caller that wants free-form issue refs can opt in by callingresolveSpawnTargetbeforesessionManager.spawn().SessionManager.spawn()— unchanged on purpose. Explicit{ projectId, issueId }stays authoritative; the utility is opt-in for callers that want routing.Fixes #1329
Design notes
The first commit was a CLI-only fix. The second commit lifts the routing into a core utility so any spawn entry point (CLI, web API, programmatic) can share the same behavior. The CLI now imports
resolveSpawnTargetinstead of duplicating logic.Prefix disambiguation: project-id match wins over
sessionPrefixmatch because yaml keys are unique by construction whilesessionPrefixvalues are not guaranteed to be.Test plan
pnpm --filter @aoagents/ao-core test— 800 tests pass, including 8 new unit tests forresolveSpawnTargetcovering:nullwhen nothing matches and no fallbackundefinedissueRef handledpnpm --filter @aoagents/ao-cli test— 485 tests pass, including 3 newspawn.test.tscases:x402-identity/1→projectId: x402-identity,issueId: 1xid/7viasessionPrefix→projectId: x402-identity,issueId: 7some-org/42untouched when no project or prefix matchespnpm --filter @aoagents/ao-core typecheckandpnpm --filter @aoagents/ao-cli typecheckpasspnpm lint— 0 errors (33 pre-existing warnings unchanged)ao start agent-orchestrator,ao spawn x402-identity/1→ session prefixxid-*,projectId=x402-identity, appears under x402-identity on the dashboardao batch-spawn agent-orchestrator/1 x402-identity/2→ one session per project, each routed correctly