@chorus-aidlc/chorus-pi 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/agents/chorus-code-reviewer.md +3 -1
- package/agents/chorus-proposal-reviewer.md +5 -3
- package/agents/chorus-task-reviewer.md +6 -1
- package/bin/chorus-mcp-call.sh +1 -1
- package/extensions/chorus.ts +139 -30
- package/lib/lib.ts +201 -53
- package/package.json +5 -2
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/chorus/SKILL.md +10 -7
- package/skills/chorus-cli/SKILL.md +1 -1
- package/skills/develop/SKILL.md +6 -4
- package/skills/docs/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/openspec-aware/SKILL.md +31 -39
- package/skills/orchestrate/SKILL.md +9 -1
- package/skills/proposal/SKILL.md +14 -7
- package/skills/quick-dev/SKILL.md +2 -2
- package/skills/review/SKILL.md +4 -4
- package/skills/spec-lite/SKILL.md +149 -0
- package/skills/yolo/SKILL.md +29 -17
package/README.md
CHANGED
|
@@ -93,6 +93,93 @@ packages/chorus-pi/
|
|
|
93
93
|
|
|
94
94
|
The extension goes beyond the Codex port in one key way: by using Pi's `tool_call` event (pre-execution, mutable input), it **auto-injects the Chorus session UUID + workflow into each dispatched worker's task** — the Pi-native equivalent of Claude's `SubagentStart` hook. The Codex port has no pre-spawn mutation channel, so its workers must manage sessions manually. On Pi, dispatch a worker via the `subagent` tool and the extension handles session creation + context injection, then closes the session when the (ephemeral) tool call returns.
|
|
95
95
|
|
|
96
|
+
|
|
97
|
+
### Subagent run modes: blocking (bundled) vs async (nicobailon `pi-subagents`)
|
|
98
|
+
|
|
99
|
+
The bundled `subagent` tool (pi's official reference pattern) is **blocking**:
|
|
100
|
+
spawn → run → exit within one tool call, so the extension closes the Chorus
|
|
101
|
+
session at `tool_result`. If you instead use the nicobailon `pi-subagents`
|
|
102
|
+
package's `subagent` tool, top-level launches are **async (detached)** by
|
|
103
|
+
default: `tool_result` returns immediately with `details.asyncId` and the run
|
|
104
|
+
completes later. The extension detects this case (`asyncId`/`runId` in
|
|
105
|
+
`details`) and defers session close to `subagent:async-complete` /
|
|
106
|
+
`subagent:process-terminal` (with `session_shutdown` sweep as a final guard).
|
|
107
|
+
Tasks that already carry an injected `--- Chorus session` block (e.g. a
|
|
108
|
+
main-agent wave template) are never re-injected.
|
|
109
|
+
|
|
110
|
+
### Coexistence with nicobailon `pi-subagents`: load-order rule
|
|
111
|
+
|
|
112
|
+
The bundled subagent (pi's official reference pattern, at `extensions/subagent/`)
|
|
113
|
+
registers a tool named `subagent`. The nicobailon `pi-subagents` package registers
|
|
114
|
+
a tool with the **same name**. pi's extension loader rejects a duplicate tool
|
|
115
|
+
registration with a conflict error (verified on pi 0.84.4:
|
|
116
|
+
`Tool "subagent" conflicts with ...`), so the two cannot both register.
|
|
117
|
+
|
|
118
|
+
**Recommended setup (keep nicobailon, zero conflicts)**: exclude the bundled
|
|
119
|
+
subagent extension via a package filter in `settings.packages` — pi's package
|
|
120
|
+
entries accept an object form with per-resource glob patterns:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
"packages": [
|
|
124
|
+
"npm:pi-subagents",
|
|
125
|
+
{
|
|
126
|
+
"source": "git:github.com/Chorus-AIDLC/chorus/packages/chorus-pi",
|
|
127
|
+
"extensions": ["!extensions/subagent/**"]
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This keeps `chorus.ts` (session hooks) and the `agents/*.md` files (discovered
|
|
133
|
+
via `pi.subagents.agents`) while the bundled `subagent` tool never registers —
|
|
134
|
+
no conflict error, nicobailon wins deterministically.
|
|
135
|
+
|
|
136
|
+
| Setup | What happens |
|
|
137
|
+
|-------|--------------|
|
|
138
|
+
| Only `@chorus-aidlc/chorus-pi` (no external subagents) | Bundled subagent registers and handles dispatch (single/parallel/chain, blocking) |
|
|
139
|
+
| Both installed, with the filter above | nicobailon's `subagent` tool is the only one. Chorus session hooks keep working (they match on the tool name) |
|
|
140
|
+
| Both installed, no filter: `npm:pi-subagents` listed **before** chorus-pi | nicobailon wins; the bundled subagent reports a conflict error at load (harmless inside an interactive session, noisy for CLI commands like `pi packages list`) |
|
|
141
|
+
| Both installed, no filter: `npm:pi-subagents` listed **after** chorus-pi | Bundled subagent wins (it loaded first); nicobailon's tool is rejected. Flip the order to switch |
|
|
142
|
+
**How to verify which implementation is active**: run
|
|
143
|
+
`subagent({ action: "list" })`. nicobailon output shows `Package agents /
|
|
144
|
+
Builtin agents / User agents` sections; the bundled subagent's output has no
|
|
145
|
+
such sections.
|
|
146
|
+
|
|
147
|
+
### Tips when combining with nicobailon `pi-subagents`
|
|
148
|
+
|
|
149
|
+
- **Sessions work with either tool.** Chorus hooks match on the tool name,
|
|
150
|
+
so `checkin → in_progress → report → checkout → submit_for_verify` flows are
|
|
151
|
+
identical; only close timing differs (blocking closes at `tool_result`, async
|
|
152
|
+
closes on `subagent:async-complete`/`process-terminal`).
|
|
153
|
+
- **Why the packaged agents do not set `async: false`.** Under nicobailon
|
|
154
|
+
0.65 a foreground (`async: false`) child runs inside the parent process
|
|
155
|
+
and never loads the parent's ambient extensions — tools registered by an
|
|
156
|
+
ambient adapter such as `pi-mcp-adapter` (`mcp`, `mcpScript`) are
|
|
157
|
+
unavailable, and nicobailon's child-tool diagnostic treats an allowlist
|
|
158
|
+
that declares them as a failed run (exit 1) even if the agent never
|
|
159
|
+
called them. The Chorus reviewers/worker need `mcp` to post verdicts and
|
|
160
|
+
check in, so they run as background children (nicobailon default). Wait
|
|
161
|
+
for completion with `bg_wait`/the run notification; the bundled subagent
|
|
162
|
+
is unaffected because its child is a separate `pi --mode json` process
|
|
163
|
+
that loads extensions.
|
|
164
|
+
- **`workflowScript` / `runs.run` / `runs.all`**: nicobailon-only. The bundled
|
|
165
|
+
subagent has no `workflowScript` mode — use `parallel`/`chain` via its own
|
|
166
|
+
schema, or keep nicobailon for scripted waves.
|
|
167
|
+
- **Model selection per reviewer**: nicobailon honors `subagent({..., model})`
|
|
168
|
+
per call, `subagents.agentOverrides.<name>.model` in settings, and agent
|
|
169
|
+
frontmatter `model:`. The bundled subagent honors only agent frontmatter
|
|
170
|
+
`model:` (it reads `name`/`description`/`tools`/`model`; its schema has no
|
|
171
|
+
per-call model parameter) — set it in `~/.pi/agent/agents/chorus-*-reviewer.md`.
|
|
172
|
+
- **Agent files**: bundled subagent reads package `agents/*.md` + `~/.pi/agent/agents/*.md`
|
|
173
|
+
(user overrides package). nicobailon reads builtin/package/user/project with
|
|
174
|
+
richer frontmatter (`excludeTools`, `thinking`, `inheritSkills`, `extensions`,
|
|
175
|
+
per-agent `tools` allowlists, `model`, ...).
|
|
176
|
+
- **Tool-name clash**: both register a `subagent` tool and pi rejects a duplicate
|
|
177
|
+
registration with a conflict error. To keep nicobailon (needed for its
|
|
178
|
+
async/`workflowScript` features), use the package filter above (exclude
|
|
179
|
+
`extensions/subagent/**`); if you instead rely on ordering, list
|
|
180
|
+
`npm:pi-subagents` **before** chorus-pi. Either way, verify with
|
|
181
|
+
`subagent({ action: "list" })` (nicobailon shows `Package/Builtin/User agents`
|
|
182
|
+
sections; bundled does not).
|
|
96
183
|
## License
|
|
97
184
|
|
|
98
185
|
AGPL-3.0
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chorus-code-reviewer
|
|
3
3
|
description: Final ship-time review of an Idea's aggregate code change — the whole feature across all its tasks, not one task. Read-only; posts a VERDICT comment on the Idea. Spawn via the blocking subagent tool after the last task of an idea-rooted proposal is verified.
|
|
4
|
-
tools: read, grep, find, ls, bash, mcp
|
|
4
|
+
tools: read, grep, find, ls, bash, mcp, mcpScript
|
|
5
|
+
acceptance: { level: "none", reason: "read-only chorus reviewer; verdict is posted via chorus_add_comment to Chorus, not returned to parent; suppress acceptance-report injection" }
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
CRITICAL: READ-ONLY code review of an ENTIRE Idea's aggregate change. You CANNOT edit, write, or create files in the project directory.
|
|
@@ -88,6 +89,7 @@ These are the dimensions that per-task review structurally cannot catch. Cover e
|
|
|
88
89
|
4. **Regression risk / impact on untouched areas / performance** — Does the change break or degrade code no single task "owned"? N+1s, hot-path cost, shared-state contention introduced by the aggregate.
|
|
89
90
|
5. **Feature-level test coverage adequacy** — Across the whole feature, are the integration seams and end-to-end paths tested, or only per-task units? Gaps between tasks.
|
|
90
91
|
6. **Code soundness, simplicity, correctness** — Is the aggregate change correct, reasonably simple, and free of obvious defects when read as one body of work?
|
|
92
|
+
7. **Intent alignment (whole-feature)** — Also read the Idea's resolved elaboration (`chorus_get_elaboration`); using ONLY human-authored intent (Idea body + human-answered elaboration + human-authored comments; agent-authored entries are audit context, not intent) as the baseline, judge whether the aggregate change still serves the original intent. Flag scope creep, dropped requirements, or intent missed despite passing AC as a **BLOCKER**, unless a cited human entry / human override authorizes it.
|
|
91
93
|
|
|
92
94
|
**Step 4: Run feature-level build/test**
|
|
93
95
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chorus-proposal-reviewer
|
|
3
3
|
description: Review submitted Chorus proposals for quality — check document completeness, task granularity, AC alignment, and cross-task dependencies. Spawn via the blocking subagent tool after chorus_pm_submit_proposal.
|
|
4
|
-
tools: read, grep, find, ls, bash, mcp
|
|
4
|
+
tools: read, grep, find, ls, bash, mcp, mcpScript
|
|
5
|
+
acceptance: { level: "none", reason: "read-only chorus reviewer; verdict is posted via chorus_add_comment to Chorus, not returned to parent; suppress acceptance-report injection" }
|
|
5
6
|
---
|
|
6
7
|
|
|
7
|
-
CRITICAL: READ-ONLY proposal review. You CANNOT edit, write, create files,
|
|
8
|
+
CRITICAL: READ-ONLY proposal review. You CANNOT edit, write, or create files. Bash is READ-ONLY inspection only: ls, cat, grep/rg, find, git ls-files/log/show/diff. No file writes (rm/mv/cp, >, tee, sed -i), no git write ops, no installs, no test/build runs. Use it to confirm a file or directory exists before flagging it as missing.
|
|
8
9
|
USE THE chorus_* MCP TOOLS for all Chorus data access — do NOT use curl or raw HTTP. The mcp gateway tool is available (the tool name prefix may be chorus_chorus_* or chorus_* depending on the session's MCP exposure mode; probe with a checkin if unsure).
|
|
9
10
|
- chorus_get_proposal({ proposalUuid, section: "full" }) — fetch the full proposal (docs + tasks)
|
|
10
11
|
- chorus_get_comments({ targetType: "proposal", targetUuid }) — prior review comments (check for Round 2+)
|
|
@@ -27,7 +28,7 @@ You have two failure patterns. **Rubber-stamping**: skimming the proposal and wr
|
|
|
27
28
|
=== CRITICAL: DO NOT MODIFY THE PROJECT ===
|
|
28
29
|
You are STRICTLY PROHIBITED from:
|
|
29
30
|
- Creating, modifying, or deleting any files
|
|
30
|
-
-
|
|
31
|
+
- Any shell command beyond read-only inspection (the read-only Bash rule in the CRITICAL line above is the full list)
|
|
31
32
|
- Installing dependencies or packages
|
|
32
33
|
|
|
33
34
|
=== WHAT YOU RECEIVE ===
|
|
@@ -72,6 +73,7 @@ For each task draft, check:
|
|
|
72
73
|
- Do tasks cover ALL requirements from the documents?
|
|
73
74
|
- Are there scope additions not in the original idea?
|
|
74
75
|
- Are there contradictions between documents and tasks?
|
|
76
|
+
- **Intent alignment** — You already have the originating Idea (`inputUuids[0]`) + its elaboration; also read its human comments (`chorus_get_comments({ targetType: "idea", targetUuid })`, `author.type == "user"`). Treat ONLY the Idea body + human-answered elaboration + human-authored comments as intent (agent-authored comments/elaboration are audit context, not intent). Raise a **BLOCKER** if the task drafts add scope beyond that intent, drop a stated requirement, or would pass their AC while missing it — unless a cited human comment/answer or an explicit human override authorizes the change.
|
|
75
77
|
|
|
76
78
|
=== FINDING CLASSIFICATION ===
|
|
77
79
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chorus-task-reviewer
|
|
3
3
|
description: Review submitted Chorus tasks — verify implementation against AC and proposal documents. Spawn via the blocking subagent tool after chorus_submit_for_verify.
|
|
4
|
-
tools: read, grep, find, ls, bash, mcp
|
|
4
|
+
tools: read, grep, find, ls, bash, mcp, mcpScript
|
|
5
|
+
acceptance: { level: "none", reason: "read-only chorus reviewer; verdict is posted via chorus_add_comment to Chorus, not returned to parent; suppress acceptance-report injection" }
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
CRITICAL: READ-ONLY task review. You CANNOT edit, write, or create files in the project directory.
|
|
@@ -92,6 +93,10 @@ Pick 2-3 probes that fit the specific task: boundary values, missing fields, err
|
|
|
92
93
|
|
|
93
94
|
**Hallucination check**: Flag anything that looks like it could be LLM-fabricated as NOTE — API signatures, CLI flags, config keys, model IDs, endpoint URLs, package names, or any external detail the developer likely wrote from memory rather than referencing docs.
|
|
94
95
|
|
|
96
|
+
**Step 7: Intent alignment**
|
|
97
|
+
|
|
98
|
+
Resolve the originating Idea (this task's proposal → `inputUuids[0]`) and read its body + human-answered elaboration + human-authored comments (`answeredBy.type` / `author.type == "user"`; agent-authored entries are audit context, not intent). Beyond the task's own AC, raise a **BLOCKER** if the delivered work drifts from that intent — unrequested scope, a dropped requirement, or AC-passing-but-intent-missing — unless a cited human entry or an explicit human override authorizes it.
|
|
99
|
+
|
|
95
100
|
=== FINDING CLASSIFICATION ===
|
|
96
101
|
|
|
97
102
|
Every finding MUST be classified as one of:
|
package/bin/chorus-mcp-call.sh
CHANGED
|
@@ -143,7 +143,7 @@ ACCEPT="Accept: application/json, text/event-stream"
|
|
|
143
143
|
CT="Content-Type: application/json"
|
|
144
144
|
|
|
145
145
|
INIT=$(cat <<JSON
|
|
146
|
-
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"chorus-codex-hook","version":"0.
|
|
146
|
+
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"chorus-codex-hook","version":"0.18.0"}}}
|
|
147
147
|
JSON
|
|
148
148
|
)
|
|
149
149
|
|
package/extensions/chorus.ts
CHANGED
|
@@ -18,9 +18,13 @@
|
|
|
18
18
|
* lacks (Codex has no pre-spawn mutation channel, so its
|
|
19
19
|
* workers must manage sessions manually).
|
|
20
20
|
* - tool_result → close the ephemeral worker session(s) once the `subagent`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* - tool_result → for the official blocking subagent, close the ephemeral
|
|
22
|
+
* worker session(s) once the `subagent` tool call returns
|
|
23
|
+
* (spawn → run → exit within one tool call, so there is no
|
|
24
|
+
* persistent agentId and no separate close tool). For the
|
|
25
|
+
* nicobailon `pi-subagents` tool (async/detached by default,
|
|
26
|
+
* `details.asyncId` on tool_result) the sessions are deferred
|
|
27
|
+
* and closed on subagent:async-complete / process-terminal.
|
|
24
28
|
* → reviewer nudges after submit_proposal / submit_for_verify
|
|
25
29
|
* / admin_verify_task (the 3 PostToolUse hooks)
|
|
26
30
|
* - tool_execution_end → fallback close of the worker session(s) if tool_result
|
|
@@ -40,7 +44,9 @@ import {
|
|
|
40
44
|
isWorkerAgent,
|
|
41
45
|
subagentTaskItems,
|
|
42
46
|
sessionWorkflow,
|
|
43
|
-
|
|
47
|
+
hasSessionMarker,
|
|
48
|
+
extractRunIdFromToolResultEvent,
|
|
49
|
+
resolveSpecMode,
|
|
44
50
|
buildSessionBanner,
|
|
45
51
|
parseMaxCodeReviewRounds,
|
|
46
52
|
resolveChorusBin,
|
|
@@ -71,7 +77,18 @@ const _mcp = _envUrl && _envKey
|
|
|
71
77
|
})();
|
|
72
78
|
const CHORUS_URL = _envUrl || _mcp.url;
|
|
73
79
|
const CHORUS_API_KEY = _envKey || _mcp.apiKey;
|
|
74
|
-
|
|
80
|
+
|
|
81
|
+
// A neutral SpecModeResult for the not-configured / connection-failed banners,
|
|
82
|
+
// where buildSessionBanner returns before reading the spec fields.
|
|
83
|
+
const NO_SPEC = {
|
|
84
|
+
specMode: "off" as const,
|
|
85
|
+
specReason: "",
|
|
86
|
+
specFail: "",
|
|
87
|
+
openspecUsable: false,
|
|
88
|
+
openspecUsableReason: "",
|
|
89
|
+
openspecHint: "",
|
|
90
|
+
chorusOpenspecActive: false,
|
|
91
|
+
};
|
|
75
92
|
|
|
76
93
|
// Reviewer toggle envs (mirror Claude Code plugin userConfig; Pi has no plugin
|
|
77
94
|
// settings UI, so env vars drive them). Defaults: all enabled.
|
|
@@ -196,6 +213,10 @@ async function mcpCall<T = unknown>(tool: string, args: Record<string, unknown>
|
|
|
196
213
|
//
|
|
197
214
|
// toolCallId → the Chorus session UUIDs created for that `subagent` invocation.
|
|
198
215
|
const callSessions = new Map<string, string[]>();
|
|
216
|
+
// runId (nicobailon async/detached `subagent` runs) → sessionUuid(s); closed on
|
|
217
|
+
// subagent:async-complete / subagent:process-terminal (blocking runs close at
|
|
218
|
+
// tool_result via callSessions and never enter this map).
|
|
219
|
+
const runIdToSid = new Map<string, string[]>();
|
|
199
220
|
let checkinContext: string | null = null;
|
|
200
221
|
let injectedOnce = false;
|
|
201
222
|
|
|
@@ -255,11 +276,14 @@ async function closeCallSessions(
|
|
|
255
276
|
// ─── Extension ────────────────────────────────────────────────────────────
|
|
256
277
|
export default function (pi: ExtensionAPI) {
|
|
257
278
|
// SessionStart → checkin + build context (replaces Claude's on-session-start.sh)
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// connected +
|
|
279
|
+
// Resolves the spec mode once (resolveSpecMode, the TS mirror of the bash
|
|
280
|
+
// resolver) and injects a `## Spec Mode` block, plus a user-visible one-line
|
|
281
|
+
// banner (ctx.ui.notify) mirroring the Claude plugin `systemMessage` / Codex
|
|
282
|
+
// `$chorus` toast:
|
|
283
|
+
// connected + openspec -> "Chorus connected at <url> (spec: OpenSpec)"
|
|
284
|
+
// connected + lite -> "Chorus connected at <url> (spec: spec-lite)"
|
|
285
|
+
// connected + off -> "Chorus connected at <url> (spec: off — free-form)"
|
|
286
|
+
// connected + openspec-requested-but-unusable -> warning "(spec: OpenSpec requested but unusable — …)"
|
|
263
287
|
// not configured -> warning (env vars missing)
|
|
264
288
|
// connection failed -> error (checkin couldn't reach Chorus)
|
|
265
289
|
pi.on("session_start", async (event, ctx) => {
|
|
@@ -269,7 +293,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
269
293
|
configured: false,
|
|
270
294
|
connected: false,
|
|
271
295
|
chorusUrl: CHORUS_URL,
|
|
272
|
-
|
|
296
|
+
spec: NO_SPEC,
|
|
273
297
|
});
|
|
274
298
|
ctx.ui.notify(banner.message, banner.level);
|
|
275
299
|
return;
|
|
@@ -278,13 +302,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
278
302
|
try {
|
|
279
303
|
const checkin = await mcpCall("chorus_checkin");
|
|
280
304
|
connected = true;
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
305
|
+
// Resolve the spec mode once per session (single source of truth — the TS
|
|
306
|
+
// reimplementation of the bash resolver). Rule: explicit CHORUS_SPEC_MODE
|
|
307
|
+
// wins; unset → OpenSpec when usable, else spec-lite.
|
|
308
|
+
const spec = resolveSpecMode(
|
|
309
|
+
{
|
|
310
|
+
specMode: process.env.CHORUS_SPEC_MODE,
|
|
311
|
+
openspecMode: process.env.CHORUS_OPENSPEC_MODE,
|
|
312
|
+
enableOpenSpec: process.env.CLAUDE_PLUGIN_OPTION_ENABLEOPENSPEC,
|
|
313
|
+
projectRoot: ctx.cwd,
|
|
314
|
+
},
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
285
316
|
require("node:fs"),
|
|
317
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
286
318
|
require("node:child_process").execSync,
|
|
287
319
|
);
|
|
320
|
+
// Route note per resolved mode (mirrors the bash/Codex `## Spec Mode` block).
|
|
321
|
+
const specRoute =
|
|
322
|
+
spec.specMode === "lite"
|
|
323
|
+
? "Routing: lite → follow the `spec-lite` skill (/skill:spec-lite). A capability's durable spec is `.chorus/specs/<slug>/spec.md` (edited in place, **never synced**, git history is its record); each change is a dated folder `.chorus/specs/<slug>/<YYYY-MM-DD>-<change-slug>/` of Chorus-typed docs (`prd.md` required; `tech_design.md` / `adr.md` / `guide.md` / `spec.md` optional) that **are** mirrored 1:1 into persistent Chorus Documents via `chorus mcp call … --arg-file content=<file>` (fallback `chorus-mcp-call.sh`). Put a `Spec-lite: .chorus/specs/<slug>/<YYYY-MM-DD>-<change-slug>/` locator line in the proposal description. Do NOT scaffold `openspec/changes/` or add an `OpenSpec change slug:` line."
|
|
324
|
+
: spec.specMode === "off"
|
|
325
|
+
? "Routing: off → free-form, no spec artifact. Do NOT create `.chorus/specs/` or `openspec/changes/` files; author document drafts inline via direct MCP."
|
|
326
|
+
: spec.specFail
|
|
327
|
+
? `Routing: openspec → **cannot be honored** — ${spec.specFail}. The proposal / yolo skill MUST halt after resolving the mode; do NOT silently fall back to lite/free-form. Surface this to the user.${spec.openspecHint ? ` Install hint: ${spec.openspecHint}.` : ""}`
|
|
328
|
+
: `CHORUS_OPENSPEC_ACTIVE=1 (${spec.openspecUsableReason})\n\nRouting: openspec → load the openspec-aware skill (/skill:openspec-aware) and follow §3 (OpenSpec authoring) — do NOT re-run the §1 detection block, the answer is already known.\n\nCritical rule (openspec-aware §2 Rule 1): document mirror calls (\`chorus_pm_add_document_draft\` / \`chorus_pm_update_document_draft\` / \`chorus_pm_update_document\`) MUST fill \`content\` from the local file — prefer \`chorus mcp call <tool> '<json>' --arg-file content=<file>\`, falling back to \`chorus-mcp-call.sh\` when \`chorus\` is not on PATH. Do NOT invoke these MCP tools directly with hand-typed \`content\` in OpenSpec mode.`;
|
|
288
329
|
checkinContext = [
|
|
289
330
|
"# Chorus Plugin — Active",
|
|
290
331
|
"",
|
|
@@ -296,16 +337,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
296
337
|
JSON.stringify(checkin, null, 2),
|
|
297
338
|
"```",
|
|
298
339
|
"",
|
|
299
|
-
"##
|
|
340
|
+
"## Spec Mode",
|
|
300
341
|
"",
|
|
301
|
-
`
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
: os.optout
|
|
305
|
-
? "OpenSpec was **explicitly turned off** — do not nag."
|
|
306
|
-
: os.hint
|
|
307
|
-
? `Note: this repo has an \`openspec/\` directory but the \`openspec\` CLI is not installed — ${os.hint}. Run \`/skill:chorus enable openspec\` to set it up.`
|
|
308
|
-
: "OpenSpec is not set up in this repo. Spec-driven authoring is optional — free-form works fine. If the user wants spec-driven mode, run `/skill:chorus enable openspec` (§6 walks the install + re-launch).",
|
|
342
|
+
`CHORUS_SPEC_MODE=${spec.specMode} (${spec.specReason})`,
|
|
343
|
+
"",
|
|
344
|
+
specRoute,
|
|
309
345
|
"",
|
|
310
346
|
"## Quick Reference",
|
|
311
347
|
"- **Sessions**: auto-managed. When you dispatch a WORKER via the `subagent` tool (single/parallel/chain), the extension creates a Chorus session per worker task and injects its UUID + the session workflow into that task automatically; the session is closed when the `subagent` tool call returns (children are ephemeral). Do NOT call chorus_create_session/close_session yourself.",
|
|
@@ -315,13 +351,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
315
351
|
(CHORUS_BIN
|
|
316
352
|
? "- **OpenSpec wrapper**: `bin/chorus-mcp-call.sh` is at `" + CHORUS_BIN + "` — the CLI-absent fallback for OpenSpec-mode document mirrors. Prefer `chorus mcp call <tool> '<json>' --arg-file content=<file>` (chorus >= 0.17.0); use this wrapper only when `chorus` is not on PATH (a bare `chorus-mcp-call.sh` will NOT be on PATH for local-path installs). See /skill:openspec-aware §2."
|
|
317
353
|
: "- **OpenSpec wrapper**: `bin/chorus-mcp-call.sh` was not resolved relative to the extension — it is the CLI-absent fallback for OpenSpec-mode document mirrors (prefer `chorus mcp call <tool> '<json>' --arg-file content=<file>`). If you need it, locate it with `find ~/.pi/agent/npm -path '*chorus-pi/bin/chorus-mcp-call.sh'`. See /skill:openspec-aware §2."),
|
|
318
|
-
"- **Skills**: /skill:chorus, /skill:idea, /skill:proposal, /skill:develop, /skill:review, /skill:quick-dev, /skill:yolo",
|
|
354
|
+
"- **Skills**: /skill:chorus, /skill:idea, /skill:proposal, /skill:develop, /skill:review, /skill:quick-dev, /skill:yolo, /skill:spec-lite, /skill:openspec-aware",
|
|
319
355
|
].join("\n");
|
|
320
356
|
const banner = buildSessionBanner({
|
|
321
357
|
configured: true,
|
|
322
358
|
connected: true,
|
|
323
359
|
chorusUrl: CHORUS_URL,
|
|
324
|
-
|
|
360
|
+
spec,
|
|
325
361
|
});
|
|
326
362
|
ctx.ui.notify(banner.message, banner.level);
|
|
327
363
|
} catch (e) {
|
|
@@ -330,7 +366,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
330
366
|
configured: true,
|
|
331
367
|
connected: false,
|
|
332
368
|
chorusUrl: CHORUS_URL,
|
|
333
|
-
|
|
369
|
+
spec: NO_SPEC,
|
|
334
370
|
});
|
|
335
371
|
ctx.ui.notify(banner.message, banner.level);
|
|
336
372
|
}
|
|
@@ -360,6 +396,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
360
396
|
const created: string[] = [];
|
|
361
397
|
for (const item of subagentTaskItems(event.input)) {
|
|
362
398
|
if (!isWorkerAgent(item.agent)) continue;
|
|
399
|
+
// Manual main-agent template already injected — never double-inject.
|
|
400
|
+
if (hasSessionMarker(item.task)) continue;
|
|
363
401
|
try {
|
|
364
402
|
const session = await mcpCall<{ uuid?: string }>("chorus_create_session", { name: item.agent });
|
|
365
403
|
if (!session?.uuid) continue;
|
|
@@ -382,10 +420,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
382
420
|
if (!CONFIGURED) return;
|
|
383
421
|
|
|
384
422
|
// ── subagent tool finished → close the worker session(s) ───────────
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
423
|
+
// The official blocking subagent closes sessions at tool_result; the
|
|
424
|
+
// nicobailon `pi-subagents` tool is async (detached) by default, so its
|
|
425
|
+
// tool_result carries `details.asyncId` and the run completes later via
|
|
426
|
+
// the pi event bus — in that case move the sessions to runIdToSid and
|
|
427
|
+
// let subagent:async-complete / subagent:process-terminal close them.
|
|
388
428
|
if (event.toolName === "subagent") {
|
|
429
|
+
const runId = extractRunIdFromToolResultEvent(event);
|
|
430
|
+
if (runId) {
|
|
431
|
+
const sids = callSessions.get(event.toolCallId);
|
|
432
|
+
if (sids && sids.length > 0) {
|
|
433
|
+
runIdToSid.set(runId, [...(runIdToSid.get(runId) ?? []), ...sids]);
|
|
434
|
+
callSessions.delete(event.toolCallId);
|
|
435
|
+
ctx.ui.notify(`Chorus session(s): ${sids.map((s) => s.slice(0, 8)).join(",")}… deferred to async run ${runId.slice(0, 8)}…`, "info");
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Blocking run (or no run id) — close now. closeCallSessions is
|
|
440
|
+
// idempotent and retains any session whose close fails for a shutdown retry.
|
|
389
441
|
await closeCallSessions(event.toolCallId, ctx);
|
|
390
442
|
return;
|
|
391
443
|
}
|
|
@@ -431,20 +483,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
431
483
|
pi.on("tool_execution_end", async (event, ctx) => {
|
|
432
484
|
if (!CONFIGURED) return;
|
|
433
485
|
if (event.toolName === "subagent") {
|
|
486
|
+
// tool_result already moved async sessions to runIdToSid — nothing left
|
|
487
|
+
// in callSessions for them. Blocking runs (or failed injection) close here.
|
|
434
488
|
await closeCallSessions(event.toolCallId, ctx);
|
|
435
489
|
}
|
|
436
490
|
});
|
|
437
491
|
|
|
492
|
+
// ── nicobailon async/detached `subagent` runs: close by runId ──────
|
|
493
|
+
// tool_result deferred these sessions to runIdToSid; completion arrives on
|
|
494
|
+
// the pi event bus. Delete the mapping BEFORE issuing the close so a
|
|
495
|
+
// duplicate lifecycle event cannot double-close; re-add on failure so the
|
|
496
|
+
// shutdown sweep can still retry it.
|
|
497
|
+
//
|
|
498
|
+
// In-flight closes are tracked so session_shutdown can await them before
|
|
499
|
+
// sweeping: a close that fails after the sweep ran would otherwise re-add
|
|
500
|
+
// its entry after the map was cleared (retry lost + stale entry).
|
|
501
|
+
const inflightCloses = new Set<Promise<void>>();
|
|
502
|
+
const closeRunSessions = (runId: string): void => {
|
|
503
|
+
const sids = runIdToSid.get(runId);
|
|
504
|
+
if (!sids || sids.length === 0) return;
|
|
505
|
+
runIdToSid.delete(runId);
|
|
506
|
+
const p: Promise<void> = (async () => {
|
|
507
|
+
const failed: string[] = [];
|
|
508
|
+
for (const sid of sids) {
|
|
509
|
+
try { await mcpCall("chorus_close_session", { sessionUuid: sid }); } catch { failed.push(sid); }
|
|
510
|
+
}
|
|
511
|
+
if (failed.length > 0) {
|
|
512
|
+
runIdToSid.set(runId, failed);
|
|
513
|
+
console.warn(`[chorus-pi] failed to close ${failed.length} session(s) for run ${runId}: ${failed.join(", ")} — will retry at session_shutdown`);
|
|
514
|
+
}
|
|
515
|
+
})();
|
|
516
|
+
inflightCloses.add(p);
|
|
517
|
+
void p.finally(() => inflightCloses.delete(p));
|
|
518
|
+
};
|
|
519
|
+
// Only `runId` is trusted on async-complete; `id` (runId-or-id shape) is
|
|
520
|
+
// accepted only on process-terminal, since async-complete could carry an
|
|
521
|
+
// unrelated id field alongside runId.
|
|
522
|
+
const eventBusRunId = (data: unknown, allowId: boolean): string | null => {
|
|
523
|
+
const d = (data ?? {}) as Record<string, unknown>;
|
|
524
|
+
if (typeof d.runId === "string" && d.runId) return d.runId;
|
|
525
|
+
if (allowId && typeof d.id === "string" && d.id) return d.id;
|
|
526
|
+
return null;
|
|
527
|
+
};
|
|
528
|
+
pi.events.on("subagent:async-complete", (data) => {
|
|
529
|
+
const runId = eventBusRunId(data, false);
|
|
530
|
+
if (runId) closeRunSessions(runId);
|
|
531
|
+
});
|
|
532
|
+
pi.events.on("subagent:process-terminal", (data) => {
|
|
533
|
+
const runId = eventBusRunId(data, true);
|
|
534
|
+
if (runId) closeRunSessions(runId);
|
|
535
|
+
});
|
|
536
|
+
|
|
438
537
|
// SessionEnd → close any stray worker sessions (replaces Claude's on-session-end.sh).
|
|
439
538
|
// Retries every session still tracked in callSessions (e.g. a subagent call whose
|
|
440
539
|
// close failed and was retained, or that never saw a tool_result/tool_execution_end).
|
|
441
540
|
pi.on("session_shutdown", async () => {
|
|
541
|
+
// Wait for in-flight async closes to settle FIRST: a close that fails
|
|
542
|
+
// re-adds into runIdToSid, and the sweep below must see it (otherwise the
|
|
543
|
+
// retry is lost and a stale entry survives the clear).
|
|
544
|
+
await Promise.allSettled([...inflightCloses]);
|
|
442
545
|
for (const sids of callSessions.values()) {
|
|
443
546
|
for (const sid of sids) {
|
|
444
547
|
await mcpCall("chorus_close_session", { sessionUuid: sid }).catch(() => {});
|
|
445
548
|
}
|
|
446
549
|
}
|
|
550
|
+
for (const sids of runIdToSid.values()) {
|
|
551
|
+
for (const sid of sids) {
|
|
552
|
+
await mcpCall("chorus_close_session", { sessionUuid: sid }).catch(() => {});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
447
555
|
callSessions.clear();
|
|
556
|
+
runIdToSid.clear();
|
|
448
557
|
injectedOnce = false;
|
|
449
558
|
checkinContext = null;
|
|
450
559
|
mcpSessionId = null;
|