cclaw-cli 0.32.0 → 0.33.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 CHANGED
@@ -277,9 +277,8 @@ The `tdd` stage is not prose guidance. It requires:
277
277
  - optional **REFACTOR** pass with coverage preservation
278
278
 
279
279
  `/cc-next` will not advance past `tdd` until the delegation log shows the
280
- subagent as `completed` or explicitly `waived` (for harnesses without
281
- native subagent dispatch, such as Codex — see
282
- [Harness support](#harness-support)).
280
+ subagent as `completed` (or, on Codex / OpenCode, role-switched with
281
+ `evidenceRefs` see [Harness support](#harness-support)).
283
282
 
284
283
  ---
285
284
 
@@ -320,21 +319,50 @@ to: `/cc-next` is the only command.
320
319
 
321
320
  ## Harness support
322
321
 
323
- cclaw is honest about which harnesses give you full automation and which
324
- need small manual bridges. See
325
- [`docs/harnesses.md`](./docs/harnesses.md) for the full matrix.
326
-
327
- | Harness | Subagent dispatch | Hook surface | Structured ask | Status |
328
- |---|---|---|---|---|
329
- | Claude Code | native | full | `AskUserQuestion` | full parity |
330
- | Cursor | partial | full | `AskQuestion` | parity gap: subagent dispatch |
331
- | OpenCode | partial | plugin | plain-text | parity gap: plugin hooks |
332
- | OpenAI Codex | none (waiver) | full | plain-text | parity gap: no subagent |
333
-
334
- Capability gaps are captured in `.cclaw/state/harness-gaps.json`. Where
335
- native dispatch is missing, cclaw emits a **structured waiver** rather
336
- than pretending the delegation happened. Closing these gaps is an
337
- ongoing kinetic effort see the harness tracking doc above.
322
+ cclaw is honest about what each harness can and cannot do, and it
323
+ closes every real gap with a documented fallback — not a silent waiver.
324
+
325
+ | Harness | Dispatch | Fallback | Hook surface | Structured ask | Playbook |
326
+ |---|---|---|---|---|---|
327
+ | Claude Code | full (named subagents) | `native` | full | `AskUserQuestion` | [`claude-playbook.md`](./src/content/harness-playbooks.ts) |
328
+ | Cursor | generic Task dispatcher | `generic-dispatch` | full | `AskQuestion` | `cursor-playbook.md` |
329
+ | OpenCode | plugin / in-session | `role-switch` | plugin | plain-text | `opencode-playbook.md` |
330
+ | OpenAI Codex | in-session only | `role-switch` (evidenceRefs required) | full | plain-text | `codex-playbook.md` |
331
+
332
+ What the fallbacks mean:
333
+
334
+ - `native` Claude runs mandatory delegations in isolated subagent
335
+ workers; cclaw records them with `fulfillmentMode: "isolated"`.
336
+ - `generic-dispatch`Cursor has a real Task tool with a fixed
337
+ vocabulary of `subagent_type`s (`explore`, `generalPurpose`, …).
338
+ cclaw maps each named agent (planner / reviewer / test-author /
339
+ security-reviewer / doc-updater) onto the generic dispatcher with a
340
+ structured role prompt. Per-agent mapping lives in the Cursor
341
+ playbook.
342
+ - `role-switch` — OpenCode and Codex lack an isolated worker primitive.
343
+ The agent announces the role in-session, performs the work, and
344
+ records a delegation row with `fulfillmentMode: "role-switch"` and at
345
+ least one `evidenceRef` pointing at the artifact section that
346
+ captures the output. Under role-switch, a `completed` row **without**
347
+ evidenceRefs is classified as `missingEvidence` by `cclaw doctor` and
348
+ blocks stage completion.
349
+ - `waiver` — reserved. Only fires auto-waivers if every installed
350
+ harness declares it. Currently unused — v0.33 removed the old
351
+ Codex-only auto-waiver path.
352
+
353
+ The full capability matrix lives in
354
+ [`docs/harnesses.md`](./docs/harnesses.md). Per-harness playbooks are
355
+ generated into `.cclaw/references/harnesses/` on every install and
356
+ upgrade; stage skills cite them by path.
357
+
358
+ Runtime state:
359
+
360
+ - `.cclaw/state/harness-gaps.json` (schema v2) — per-harness list of
361
+ missing capabilities, missing hook events, the declared fallback, the
362
+ playbook path, and a `remediation[]` list you can act on.
363
+ - `cclaw doctor` — asserts every installed harness has its playbook on
364
+ disk and surfaces the expected fulfillment mode inside the
365
+ `delegation:mandatory:current_stage` check.
338
366
 
339
367
  ---
340
368
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-harness parity playbooks.
3
+ *
4
+ * cclaw's subagent contracts (planner / reviewer / security-reviewer /
5
+ * test-author / doc-updater) assume Claude-style isolated workers. On
6
+ * harnesses without that primitive, the agent has to fulfil the role via a
7
+ * documented fallback (generic Task dispatch, role-switch in-session, …).
8
+ *
9
+ * Each playbook is:
10
+ * 1. short (≤ ~150 lines markdown),
11
+ * 2. executable — reproducible by an agent without reading the whole repo,
12
+ * 3. evidence-first — always records a delegation-log entry with
13
+ * `fulfillmentMode` and `evidenceRefs` so `cclaw doctor` can tell the
14
+ * role was actually performed.
15
+ *
16
+ * Playbooks are materialised at
17
+ * `.cclaw/references/harnesses/<harness>-playbook.md` by install/sync/upgrade.
18
+ */
19
+ import type { HarnessId } from "../types.js";
20
+ export declare const HARNESS_PLAYBOOKS_DIR = "references/harnesses";
21
+ export declare function harnessPlaybookRelativePath(harness: HarnessId): string;
22
+ export declare function harnessPlaybookFileName(harness: HarnessId): string;
23
+ export declare function harnessPlaybookMarkdown(harness: HarnessId): string;
24
+ export declare function harnessPlaybooksIndexMarkdown(): string;
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Per-harness parity playbooks.
3
+ *
4
+ * cclaw's subagent contracts (planner / reviewer / security-reviewer /
5
+ * test-author / doc-updater) assume Claude-style isolated workers. On
6
+ * harnesses without that primitive, the agent has to fulfil the role via a
7
+ * documented fallback (generic Task dispatch, role-switch in-session, …).
8
+ *
9
+ * Each playbook is:
10
+ * 1. short (≤ ~150 lines markdown),
11
+ * 2. executable — reproducible by an agent without reading the whole repo,
12
+ * 3. evidence-first — always records a delegation-log entry with
13
+ * `fulfillmentMode` and `evidenceRefs` so `cclaw doctor` can tell the
14
+ * role was actually performed.
15
+ *
16
+ * Playbooks are materialised at
17
+ * `.cclaw/references/harnesses/<harness>-playbook.md` by install/sync/upgrade.
18
+ */
19
+ import { HARNESS_ADAPTERS } from "../harness-adapters.js";
20
+ export const HARNESS_PLAYBOOKS_DIR = "references/harnesses";
21
+ export function harnessPlaybookRelativePath(harness) {
22
+ return `${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`;
23
+ }
24
+ export function harnessPlaybookFileName(harness) {
25
+ return `${harness}-playbook.md`;
26
+ }
27
+ const CLAUDE_PLAYBOOK = `---
28
+ harness: claude
29
+ fallback: native
30
+ description: "Claude Code has real isolated subagent workers with user-defined named types. No fallback required — this playbook is reference-only."
31
+ ---
32
+
33
+ # Claude Code — Parity Playbook
34
+
35
+ **Status: native.** Claude Code supports isolated subagent workers via the
36
+ \`Task\` tool with user-defined \`subagent_type\` (\`planner\`, \`reviewer\`,
37
+ \`security-reviewer\`, \`test-author\`, \`doc-updater\`). Each dispatch runs in
38
+ its own context and produces a return message visible only to the parent
39
+ agent.
40
+
41
+ This playbook exists so the harness matrix has one reference shape; Claude
42
+ itself has no parity gap to close.
43
+
44
+ ## Dispatch pattern
45
+
46
+ 1. Pick the \`subagent_type\` matching the cclaw agent (e.g. \`reviewer\`).
47
+ 2. Provide a specific, self-contained \`prompt\` — the subagent cannot see
48
+ prior assistant turns.
49
+ 3. Record a delegation entry before dispatch:
50
+
51
+ \`\`\`json
52
+ {
53
+ "stage": "review",
54
+ "agent": "reviewer",
55
+ "mode": "mandatory",
56
+ "status": "scheduled",
57
+ "fulfillmentMode": "isolated",
58
+ "spanId": "dspan-..."
59
+ }
60
+ \`\`\`
61
+
62
+ 4. After the subagent returns, update the entry to \`status: "completed"\`
63
+ and attach \`evidenceRefs\` pointing at the artifact section that
64
+ captures the subagent's output.
65
+
66
+ ## Verification
67
+
68
+ \`cclaw doctor\` will pass the \`delegation:mandatory:current_stage\` check
69
+ when each mandatory agent has a \`completed\` row for the active run.
70
+ `;
71
+ const CURSOR_PLAYBOOK = `---
72
+ harness: cursor
73
+ fallback: generic-dispatch
74
+ description: "Cursor has a generic Task dispatcher with subagent_type (generalPurpose, explore, shell, …) but no user-defined named subagents. cclaw maps planner/reviewer/test-author/… onto generic dispatch with a structured role prompt."
75
+ ---
76
+
77
+ # Cursor — Parity Playbook
78
+
79
+ **Fallback: generic-dispatch.** Cursor's \`Task\` tool supports
80
+ \`subagent_type\` from a fixed vocabulary (\`generalPurpose\`, \`explore\`,
81
+ \`shell\`, \`browser-use\`, …). Real isolation, but no user-defined agent
82
+ names. cclaw closes the gap by mapping each named cclaw agent onto the
83
+ generic dispatcher with a strict role prompt.
84
+
85
+ ## Named-agent → Cursor subagent_type map
86
+
87
+ | cclaw agent | Cursor \`subagent_type\` | Readonly? | Rationale |
88
+ |----------------------|-------------------------|-----------|-----------|
89
+ | \`planner\` | \`explore\` | yes | Pure research, no writes. |
90
+ | \`reviewer\` | \`explore\` | yes | Reads diff + context, emits findings. |
91
+ | \`security-reviewer\`| \`explore\` | yes | Reads code, produces report; no fixes. |
92
+ | \`test-author\` | \`generalPurpose\` | no | Writes tests, runs them, iterates. |
93
+ | \`doc-updater\` | \`generalPurpose\` | no | Edits docs, re-runs build. |
94
+
95
+ ## Dispatch pattern
96
+
97
+ 1. Pick the mapped \`subagent_type\` from the table above.
98
+ 2. Build the \`prompt\` from the cclaw agent contract in
99
+ \`.cclaw/agents/<agent>.md\`, prefaced with a single line naming the
100
+ cclaw role (\`You are the cclaw <agent>. Follow the contract below.\`).
101
+ 3. Set \`readonly: true\` when the table says yes — Cursor enforces it.
102
+ 4. Before dispatch, append a delegation row:
103
+
104
+ \`\`\`json
105
+ {
106
+ "stage": "tdd",
107
+ "agent": "test-author",
108
+ "mode": "mandatory",
109
+ "status": "scheduled",
110
+ "fulfillmentMode": "generic-dispatch",
111
+ "spanId": "dspan-..."
112
+ }
113
+ \`\`\`
114
+
115
+ 5. After dispatch returns, transition the row to \`completed\` with
116
+ \`evidenceRefs\` citing the artifact anchor where the result landed.
117
+
118
+ ## Why not upgrade Cursor to a full tier-1?
119
+
120
+ Cursor has dispatch + hooks + \`AskQuestion\`. The missing piece is
121
+ **user-defined named subagents**. Semantically this is the difference
122
+ between Claude's \`test-author\` (a distinct runtime worker registered by
123
+ cclaw) and Cursor's \`generalPurpose\` worker that cclaw *asks* to act as a
124
+ test-author. Good enough for parity; different enough to keep the labels
125
+ honest.
126
+
127
+ ## Verification
128
+
129
+ \`cclaw doctor\` passes when the delegation row exists with
130
+ \`fulfillmentMode: "generic-dispatch"\` (or \`completed\` rows for the
131
+ mandatory agents in general). No evidenceRef requirement applies here —
132
+ Cursor dispatch is real isolation.
133
+ `;
134
+ const OPENCODE_PLAYBOOK = `---
135
+ harness: opencode
136
+ fallback: role-switch
137
+ description: "OpenCode has plugin-based dispatch hooks but no isolated subagent worker primitive. cclaw uses an in-session role-switch with a delegation-log entry + evidenceRefs."
138
+ ---
139
+
140
+ # OpenCode — Parity Playbook
141
+
142
+ **Fallback: role-switch.** OpenCode exposes tool/session event hooks via a
143
+ plugin but does not provide an isolated subagent worker. cclaw closes the
144
+ delegation gate by role-switching inside the same session: the agent
145
+ announces the role, performs the work against the contract, and records
146
+ evidence.
147
+
148
+ ## Role-switch protocol
149
+
150
+ 1. Announce the role explicitly in a single message:
151
+
152
+ > Acting as cclaw **<agent>** per \`.cclaw/agents/<agent>.md\`. No other
153
+ > role may be assumed until the delegation row is closed.
154
+
155
+ 2. Execute the role's contract. Do NOT interleave other roles' work.
156
+ 3. Write the result into the stage artifact (e.g. TDD work lands in
157
+ \`.cclaw/artifacts/06-tdd.md\`).
158
+ 4. Append a delegation row:
159
+
160
+ \`\`\`json
161
+ {
162
+ "stage": "tdd",
163
+ "agent": "test-author",
164
+ "mode": "mandatory",
165
+ "status": "completed",
166
+ "fulfillmentMode": "role-switch",
167
+ "evidenceRefs": [
168
+ ".cclaw/artifacts/06-tdd.md#red-run",
169
+ ".cclaw/artifacts/06-tdd.md#green-run"
170
+ ],
171
+ "spanId": "dspan-..."
172
+ }
173
+ \`\`\`
174
+
175
+ 5. \`evidenceRefs\` **must** point at concrete artifact anchors — not
176
+ placeholder text. \`cclaw doctor\` will report \`missingEvidence\` if
177
+ the array is empty under a role-switch fallback.
178
+
179
+ ## Exception: OpenCode plugin dispatch
180
+
181
+ If the project configures a plugin-based dispatch path (e.g. a tool that
182
+ spawns a worker process), set \`fulfillmentMode: "generic-dispatch"\`
183
+ instead of \`role-switch\` and omit the role-announce step. evidenceRefs
184
+ remain optional but recommended.
185
+
186
+ ## Verification
187
+
188
+ \`cclaw doctor\` passes when every mandatory agent for the active stage
189
+ has either a \`completed\` row with evidenceRefs (role-switch) or a
190
+ \`completed\` row under plugin dispatch.
191
+ `;
192
+ const CODEX_PLAYBOOK = `---
193
+ harness: codex
194
+ fallback: role-switch
195
+ description: "OpenAI Codex has no subagent dispatch primitive. cclaw uses role-switch with evidenceRefs; silent auto-waiver is explicitly disabled."
196
+ ---
197
+
198
+ # OpenAI Codex — Parity Playbook
199
+
200
+ **Fallback: role-switch.** Codex has no subagent dispatch — neither named
201
+ nor generic. cclaw used to silently auto-waive mandatory delegations on
202
+ Codex; v0.33 disables that shortcut. The agent must role-switch in-session
203
+ and record evidence, or the delegation gate blocks stage completion.
204
+
205
+ ## Role-switch protocol
206
+
207
+ Identical to OpenCode. Key requirements:
208
+
209
+ 1. **Explicit announce.** Before performing the role, emit a single
210
+ message naming the role and citing \`.cclaw/agents/<agent>.md\`.
211
+ 2. **No role interleaving.** Do not mix, for example, reviewer and
212
+ test-author work into the same turn — close one delegation before
213
+ opening another.
214
+ 3. **EvidenceRefs are mandatory.** Under Codex's role-switch fallback a
215
+ \`completed\` row without \`evidenceRefs\` is treated as
216
+ \`missingEvidence\` by \`cclaw doctor\` and blocks the gate.
217
+
218
+ ## Stage-specific role maps
219
+
220
+ | Stage | Mandatory roles | Artifact to cite in evidenceRefs |
221
+ |------------|----------------------------------|--------------------------------------|
222
+ | scope | \`planner\` | \`.cclaw/artifacts/02-scope.md\` |
223
+ | design | \`planner\` | \`.cclaw/artifacts/03-design.md\` |
224
+ | plan | \`planner\` | \`.cclaw/artifacts/05-plan.md\` |
225
+ | tdd | \`test-author\` | \`.cclaw/artifacts/06-tdd.md\` |
226
+ | review | \`reviewer\`, \`security-reviewer\` | \`.cclaw/artifacts/07-review.md\` |
227
+ | ship | \`doc-updater\` | \`.cclaw/artifacts/08-ship.md\` |
228
+
229
+ ## Why no auto-waiver anymore?
230
+
231
+ Silent auto-waiver on Codex let entire stages complete without any
232
+ reviewer or test-author work. That defeats cclaw's hard gates. v0.33
233
+ replaces it with an explicit role-switch obligation: the agent still gets
234
+ a path forward, but the path is visible in the delegation log.
235
+
236
+ If a team genuinely wants to skip a delegation on Codex, they must
237
+ manually append a \`status: "waived"\` row with a one-line
238
+ \`waiverReason\` — the same audit trail any Claude/Cursor install would
239
+ need.
240
+
241
+ ## Verification
242
+
243
+ \`cclaw doctor\` passes when every mandatory agent for the active stage
244
+ has a \`completed\` row with \`fulfillmentMode: "role-switch"\` and at
245
+ least one \`evidenceRef\`.
246
+ `;
247
+ const PLAYBOOK_BY_HARNESS = {
248
+ claude: CLAUDE_PLAYBOOK,
249
+ cursor: CURSOR_PLAYBOOK,
250
+ opencode: OPENCODE_PLAYBOOK,
251
+ codex: CODEX_PLAYBOOK
252
+ };
253
+ export function harnessPlaybookMarkdown(harness) {
254
+ const body = PLAYBOOK_BY_HARNESS[harness];
255
+ if (!body) {
256
+ throw new Error(`No playbook defined for harness "${harness}".`);
257
+ }
258
+ return body;
259
+ }
260
+ export function harnessPlaybooksIndexMarkdown() {
261
+ const rows = Object.keys(HARNESS_ADAPTERS)
262
+ .map((h) => {
263
+ const fallback = HARNESS_ADAPTERS[h].capabilities.subagentFallback;
264
+ return `| \`${h}\` | ${fallback} | [\`${harnessPlaybookFileName(h)}\`](./${harnessPlaybookFileName(h)}) |`;
265
+ })
266
+ .join("\n");
267
+ return `# Harness parity playbooks
268
+
269
+ Each playbook describes the concrete pattern cclaw expects when the
270
+ harness does not natively satisfy a mandatory delegation contract.
271
+
272
+ | Harness | Fallback | Playbook |
273
+ |---|---|---|
274
+ ${rows}
275
+
276
+ ## How cclaw uses these files
277
+
278
+ - \`cclaw doctor\` verifies that every installed harness has its playbook
279
+ present under \`.cclaw/references/harnesses/\`.
280
+ - Stage skills (TDD, review, ship) cite the active harness's playbook
281
+ instead of inlining the fallback pattern.
282
+ - The \`delegation:mandatory:current_stage\` check expects
283
+ \`fulfillmentMode\` to match the harness's declared \`subagentFallback\`
284
+ (\`isolated\`, \`generic-dispatch\`, or \`role-switch\`).
285
+
286
+ ## When to edit
287
+
288
+ Playbooks are generated by \`cclaw upgrade\`. Local edits are overwritten.
289
+ To customise the parity pattern for a specific repository, override the
290
+ skill that cites the playbook, not the playbook itself.
291
+ `;
292
+ }
@@ -1,5 +1,6 @@
1
1
  import { HARNESS_ADAPTERS, harnessTier } from "../harness-adapters.js";
2
2
  import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./hook-events.js";
3
+ import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName } from "./harness-playbooks.js";
3
4
  function harnessTitle(harness) {
4
5
  switch (harness) {
5
6
  case "claude":
@@ -25,7 +26,9 @@ export function harnessIntegrationDocMarkdown() {
25
26
  .map((harness) => {
26
27
  const adapter = HARNESS_ADAPTERS[harness];
27
28
  const tier = harnessTier(harness);
28
- return `| ${harnessTitle(harness)} | \`${harness}\` | \`${tier}\` (${tierDescription(tier)}) | ${adapter.capabilities.nativeSubagentDispatch} | ${adapter.capabilities.hookSurface} | ${adapter.capabilities.structuredAsk} |`;
29
+ const caps = adapter.capabilities;
30
+ const playbook = `\`${HARNESS_PLAYBOOKS_DIR}/${harnessPlaybookFileName(harness)}\``;
31
+ return `| ${harnessTitle(harness)} | \`${harness}\` | \`${tier}\` (${tierDescription(tier)}) | ${caps.nativeSubagentDispatch} | ${caps.subagentFallback} | ${caps.hookSurface} | ${caps.structuredAsk} | ${playbook} |`;
29
32
  })
30
33
  .join("\n");
31
34
  const hookRows = HOOK_SEMANTIC_EVENTS.map((eventName) => {
@@ -43,10 +46,17 @@ Generated from \`src/harness-adapters.ts\` capabilities and hook event mappings.
43
46
 
44
47
  ## Capability tiers
45
48
 
46
- | Harness | ID | Tier | Native subagent dispatch | Hook surface | Structured ask |
47
- |---|---|---|---|---|---|
49
+ | Harness | ID | Tier | Native dispatch | Fallback | Hook surface | Structured ask | Playbook |
50
+ |---|---|---|---|---|---|---|---|
48
51
  ${capabilityRows}
49
52
 
53
+ Fallback legend:
54
+
55
+ - \`native\` — first-class named subagent dispatch (Claude).
56
+ - \`generic-dispatch\` — generic Task dispatcher mapped to cclaw roles (Cursor).
57
+ - \`role-switch\` — in-session role announce + delegation-log entry with evidenceRefs (OpenCode, Codex).
58
+ - \`waiver\` — no parity path; reserved for harnesses that cannot role-switch (none shipped).
59
+
50
60
  ## Semantic hook event coverage
51
61
 
52
62
  | Event | Claude | Cursor | OpenCode | Codex |
@@ -43,14 +43,20 @@ Human input remains mandatory only at explicit approval gates (plan approval, us
43
43
 
44
44
  ### Harness routing
45
45
 
46
- | Harness | Delegation tool | Structured ask tool | Routing note |
47
- |---|---|---|---|
48
- | Claude | Task/delegate | AskUserQuestion | Preferred for rich multi-step delegation + explicit approvals. |
49
- | Cursor | Task | AskQuestion | Use option-based asks for mode/waiver decisions; keep subagent payloads concise. |
50
- | Codex | Task (if available) | None native | Use numbered choices in chat for approvals; keep prompts fully self-contained. |
51
- | OpenCode | Task (if available) | None native | Log delegation outcomes in artifacts/state explicitly; do not assume built-in ask workflows. |
52
-
53
- If delegation tooling is unavailable in the active harness, run the same controller protocol in-thread and record a delegation waiver with reason \`harness_limitation\`.
46
+ | Harness | Fallback | Delegation tool | Structured ask | Parity playbook |
47
+ |---|---|---|---|---|
48
+ | Claude | \`native\` | Task (named subagent_type) | AskUserQuestion | \`.cclaw/references/harnesses/claude-playbook.md\` |
49
+ | Cursor | \`generic-dispatch\` | Task (generic subagent_type: explore/generalPurpose/…) | AskQuestion | \`.cclaw/references/harnesses/cursor-playbook.md\` |
50
+ | OpenCode | \`role-switch\` | plugin dispatch _or_ in-session role-switch | plain-text options | \`.cclaw/references/harnesses/opencode-playbook.md\` |
51
+ | Codex | \`role-switch\` | in-session role-switch (mandatory evidenceRefs) | plain-text options | \`.cclaw/references/harnesses/codex-playbook.md\` |
52
+
53
+ **Dispatch rules driven by \`subagentFallback\`:**
54
+
55
+ - \`native\` — use the harness's own named subagent primitive; delegation entry uses \`fulfillmentMode: "isolated"\`.
56
+ - \`generic-dispatch\` — map each cclaw agent onto the generic dispatcher via the harness playbook; delegation entry uses \`fulfillmentMode: "generic-dispatch"\`.
57
+ - \`role-switch\` — announce the role in-session, perform the work, append a delegation row with \`fulfillmentMode: "role-switch"\` and ≥1 \`evidenceRef\`. Without evidenceRefs the \`delegation:mandatory:current_stage\` check reports \`missingEvidence\` and blocks stage completion.
58
+
59
+ The only time a \`harness_limitation\` waiver fires automatically is when every installed harness declares \`subagentFallback: "waiver"\`. cclaw 0.33 no longer maps Codex onto auto-waiver — the agent must role-switch with evidence.
54
60
 
55
61
  ### Model routing
56
62
 
@@ -1,6 +1,17 @@
1
+ import { type SubagentFallback } from "./harness-adapters.js";
1
2
  import type { FlowStage } from "./types.js";
2
3
  export type DelegationMode = "mandatory" | "proactive" | "conditional";
3
4
  export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
5
+ /**
6
+ * How a delegation was actually fulfilled. Advisory — mirrors the harness
7
+ * `subagentFallback` that was in effect when the entry was recorded.
8
+ *
9
+ * - `isolated` — Claude-style isolated subagent worker.
10
+ * - `generic-dispatch` — Cursor-style Task dispatch mapped to a named role.
11
+ * - `role-switch` — performed in-session with explicit role announce.
12
+ * - `harness-waiver` — auto-waived due to missing dispatch capability.
13
+ */
14
+ export type DelegationFulfillmentMode = "isolated" | "generic-dispatch" | "role-switch" | "harness-waiver";
4
15
  export interface DelegationTokenUsage {
5
16
  input: number;
6
17
  output: number;
@@ -45,6 +56,12 @@ export type DelegationEntry = {
45
56
  retryCount?: number;
46
57
  /** Optional references to evidence anchors in artifacts. */
47
58
  evidenceRefs?: string[];
59
+ /**
60
+ * Fulfillment mode this entry was executed under. Omitted on legacy rows
61
+ * (treated as `"isolated"` for Claude, otherwise inferred from the active
62
+ * harness).
63
+ */
64
+ fulfillmentMode?: DelegationFulfillmentMode;
48
65
  /** Schema version marker for span-compatible delegation logs. */
49
66
  schemaVersion?: 1;
50
67
  };
@@ -54,10 +71,21 @@ export type DelegationLedger = {
54
71
  };
55
72
  export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
56
73
  export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
74
+ /**
75
+ * Aggregate the fulfillment mode cclaw expects for the active harness set.
76
+ * Priority native > generic-dispatch > role-switch > waiver — the best
77
+ * available mode wins so mixed installs (e.g. claude + codex) inherit the
78
+ * strongest guarantee.
79
+ */
80
+ export declare function expectedFulfillmentMode(fallbacks: SubagentFallback[]): DelegationFulfillmentMode;
57
81
  export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage): Promise<{
58
82
  satisfied: boolean;
59
83
  missing: string[];
60
84
  waived: string[];
61
85
  autoWaived: string[];
62
86
  staleIgnored: string[];
87
+ /** Delegation rows missing required evidence under a role-switch fallback. */
88
+ missingEvidence: string[];
89
+ /** Expected fulfillment mode for the active harness set. */
90
+ expectedMode: DelegationFulfillmentMode;
63
91
  }>;
@@ -54,6 +54,11 @@ function isDelegationEntry(value) {
54
54
  (o.taskId === undefined || typeof o.taskId === "string") &&
55
55
  (o.waiverReason === undefined || typeof o.waiverReason === "string") &&
56
56
  (o.runId === undefined || typeof o.runId === "string") &&
57
+ (o.fulfillmentMode === undefined ||
58
+ o.fulfillmentMode === "isolated" ||
59
+ o.fulfillmentMode === "generic-dispatch" ||
60
+ o.fulfillmentMode === "role-switch" ||
61
+ o.fulfillmentMode === "harness-waiver") &&
57
62
  (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
58
63
  (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
59
64
  retryOk &&
@@ -128,6 +133,23 @@ export async function appendDelegation(projectRoot, entry) {
128
133
  await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`);
129
134
  });
130
135
  }
136
+ /**
137
+ * Aggregate the fulfillment mode cclaw expects for the active harness set.
138
+ * Priority native > generic-dispatch > role-switch > waiver — the best
139
+ * available mode wins so mixed installs (e.g. claude + codex) inherit the
140
+ * strongest guarantee.
141
+ */
142
+ export function expectedFulfillmentMode(fallbacks) {
143
+ if (fallbacks.length === 0)
144
+ return "isolated";
145
+ if (fallbacks.some((f) => f === "native"))
146
+ return "isolated";
147
+ if (fallbacks.some((f) => f === "generic-dispatch"))
148
+ return "generic-dispatch";
149
+ if (fallbacks.some((f) => f === "role-switch"))
150
+ return "role-switch";
151
+ return "harness-waiver";
152
+ }
131
153
  export async function checkMandatoryDelegations(projectRoot, stage) {
132
154
  const mandatory = stageSchema(stage).mandatoryDelegations;
133
155
  const { activeRunId } = await readFlowState(projectRoot);
@@ -140,15 +162,21 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
140
162
  const missing = [];
141
163
  const waived = [];
142
164
  const autoWaived = [];
165
+ const missingEvidence = [];
143
166
  const config = await readConfig(projectRoot).catch(() => null);
144
167
  const harnesses = config?.harnesses ?? [];
145
- const nativeDelegationUnavailable = harnesses.length > 0 &&
146
- harnesses.every((harness) => HARNESS_ADAPTERS[harness].capabilities.nativeSubagentDispatch === "none");
168
+ const fallbacks = harnesses.map((h) => HARNESS_ADAPTERS[h].capabilities.subagentFallback);
169
+ const expectedMode = expectedFulfillmentMode(fallbacks);
170
+ const onlyWaiverFallback = harnesses.length > 0 && fallbacks.every((f) => f === "waiver");
147
171
  for (const agent of mandatory) {
148
172
  const rows = forRun.filter((e) => e.agent === agent);
149
- const ok = rows.some((e) => e.status === "completed" || e.status === "waived");
173
+ const completedRows = rows.filter((e) => e.status === "completed");
174
+ const waivedRows = rows.filter((e) => e.status === "waived");
175
+ const hasCompleted = completedRows.length > 0;
176
+ const hasWaived = waivedRows.length > 0;
177
+ const ok = hasCompleted || hasWaived;
150
178
  if (!ok) {
151
- if (nativeDelegationUnavailable) {
179
+ if (onlyWaiverFallback) {
152
180
  const existingHarnessWaiver = rows.some((e) => e.status === "waived" && e.waiverReason === "harness_limitation");
153
181
  if (!existingHarnessWaiver) {
154
182
  await appendDelegation(projectRoot, {
@@ -157,6 +185,7 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
157
185
  mode: "mandatory",
158
186
  status: "waived",
159
187
  waiverReason: "harness_limitation",
188
+ fulfillmentMode: "harness-waiver",
160
189
  ts: new Date().toISOString(),
161
190
  runId: activeRunId
162
191
  });
@@ -167,16 +196,27 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
167
196
  else {
168
197
  missing.push(agent);
169
198
  }
199
+ continue;
170
200
  }
171
- else if (rows.some((e) => e.status === "waived")) {
201
+ if (hasWaived) {
172
202
  waived.push(agent);
173
203
  }
204
+ // Under role-switch fallback, a `completed` row is only credible if it
205
+ // carries at least one evidenceRef — otherwise the agent might have
206
+ // claimed role-switch satisfaction without showing its work.
207
+ if (hasCompleted &&
208
+ expectedMode === "role-switch" &&
209
+ !completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
210
+ missingEvidence.push(agent);
211
+ }
174
212
  }
175
213
  return {
176
- satisfied: missing.length === 0,
214
+ satisfied: missing.length === 0 && missingEvidence.length === 0,
177
215
  missing,
178
216
  waived,
179
217
  autoWaived,
180
- staleIgnored
218
+ staleIgnored,
219
+ missingEvidence,
220
+ expectedMode
181
221
  };
182
222
  }
package/dist/doctor.js CHANGED
@@ -23,6 +23,7 @@ import { doctorCheckMetadata } from "./doctor-registry.js";
23
23
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
24
24
  import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
25
25
  import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
26
+ import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName } from "./content/harness-playbooks.js";
26
27
  import { validateHookDocument } from "./hook-schema.js";
27
28
  const execFileAsync = promisify(execFile);
28
29
  async function isGitRepo(projectRoot) {
@@ -375,6 +376,12 @@ export async function doctorChecks(projectRoot, options = {}) {
375
376
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "references", "harnesses.md")),
376
377
  details: `${RUNTIME_ROOT}/references/harnesses.md`
377
378
  });
379
+ const playbookDir = path.join(projectRoot, RUNTIME_ROOT, ...HARNESS_PLAYBOOKS_DIR.split("/"));
380
+ checks.push({
381
+ name: "harness_ref:playbooks_index",
382
+ ok: await exists(path.join(playbookDir, "README.md")),
383
+ details: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/README.md`
384
+ });
378
385
  const doctorRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "doctor");
379
386
  for (const fileName of Object.keys(DOCTOR_REFERENCE_MARKDOWN)) {
380
387
  const refPath = path.join(doctorRefDir, fileName);
@@ -475,6 +482,12 @@ export async function doctorChecks(projectRoot, options = {}) {
475
482
  details: shimPath
476
483
  });
477
484
  }
485
+ const playbookFile = path.join(projectRoot, RUNTIME_ROOT, ...HARNESS_PLAYBOOKS_DIR.split("/"), harnessPlaybookFileName(harness));
486
+ checks.push({
487
+ name: `harness_ref:playbook:${harness}`,
488
+ ok: await exists(playbookFile),
489
+ details: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/${harnessPlaybookFileName(harness)}`
490
+ });
478
491
  }
479
492
  const agentsFile = path.join(projectRoot, "AGENTS.md");
480
493
  let agentsBlockOk = false;
@@ -1298,12 +1311,15 @@ export async function doctorChecks(projectRoot, options = {}) {
1298
1311
  details: `${RUNTIME_ROOT}/runs must exist for archived feature snapshots`
1299
1312
  });
1300
1313
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
1314
+ const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
1315
+ ? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
1316
+ : "";
1301
1317
  checks.push({
1302
1318
  name: "delegation:mandatory:current_stage",
1303
1319
  ok: delegation.satisfied,
1304
1320
  details: delegation.satisfied
1305
- ? `All mandatory delegations satisfied for stage "${flowState.currentStage}"`
1306
- : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}`
1321
+ ? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
1322
+ : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
1307
1323
  });
1308
1324
  checks.push({
1309
1325
  name: "warning:delegation:waived",
@@ -1,19 +1,58 @@
1
1
  import type { HarnessId } from "./types.js";
2
2
  export declare const CCLAW_MARKER_START = "<!-- cclaw-start -->";
3
3
  export declare const CCLAW_MARKER_END = "<!-- cclaw-end -->";
4
+ export type SubagentFallback =
5
+ /** Harness has real, isolated subagent dispatch; no fallback needed. */
6
+ "native"
7
+ /**
8
+ * Harness has generic dispatch (e.g. Cursor's Task tool with
9
+ * `subagent_type`) but not user-defined named subagents; cclaw maps each
10
+ * named agent to the generic dispatcher with a structured role prompt.
11
+ */
12
+ | "generic-dispatch"
13
+ /**
14
+ * No isolated dispatch — the agent performs the named subagent's role
15
+ * in-session with an explicit role announce + delegation-log entry
16
+ * carrying evidenceRefs. Accepted as `completed` in delegation checks.
17
+ */
18
+ | "role-switch"
19
+ /**
20
+ * No meaningful fallback — mandatory delegations can only be waived
21
+ * under `waiverReason: "harness_limitation"`.
22
+ */
23
+ | "waiver";
4
24
  export interface HarnessAdapter {
5
25
  id: HarnessId;
6
26
  commandDir: string;
7
27
  capabilities: {
8
- nativeSubagentDispatch: "full" | "partial" | "none";
28
+ /**
29
+ * Level of native subagent dispatch:
30
+ * - `full` — isolated workers + user-defined named subagents (Claude).
31
+ * - `generic` — generic dispatcher (Task) without named agents (Cursor).
32
+ * - `partial` — plugin-based dispatch, not a first-class primitive
33
+ * (OpenCode).
34
+ * - `none` — no dispatch primitive at all (Codex).
35
+ */
36
+ nativeSubagentDispatch: "full" | "generic" | "partial" | "none";
9
37
  hookSurface: "full" | "plugin" | "limited" | "none";
10
38
  structuredAsk: "AskUserQuestion" | "AskQuestion" | "plain-text";
39
+ /**
40
+ * Declared fallback pattern used when the harness cannot satisfy a
41
+ * mandatory delegation natively. Drives `checkMandatoryDelegations`
42
+ * and the generated playbook per harness.
43
+ */
44
+ subagentFallback: SubagentFallback;
11
45
  };
12
46
  }
13
47
  export declare function harnessShimFileNames(): string[];
14
48
  export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
15
49
  export type HarnessTier = "tier1" | "tier2" | "tier3";
16
50
  export declare function harnessTier(harnessId: HarnessId): HarnessTier;
51
+ /**
52
+ * Harness IDs ordered from best (tier1) to least-capable. Stable sort — same
53
+ * tier preserves declaration order.
54
+ */
55
+ export declare function harnessesByTier(): HarnessId[];
17
56
  /** Removes the cclaw AGENTS.md block. */
18
57
  export declare function stripCclawBlock(content: string): string;
19
58
  export declare function removeCclawFromAgentsMd(projectRoot: string): Promise<void>;
@@ -54,16 +54,22 @@ export const HARNESS_ADAPTERS = {
54
54
  capabilities: {
55
55
  nativeSubagentDispatch: "full",
56
56
  hookSurface: "full",
57
- structuredAsk: "AskUserQuestion"
57
+ structuredAsk: "AskUserQuestion",
58
+ subagentFallback: "native"
58
59
  }
59
60
  },
60
61
  cursor: {
61
62
  id: "cursor",
62
63
  commandDir: ".cursor/commands",
63
64
  capabilities: {
64
- nativeSubagentDispatch: "partial",
65
+ // Cursor has a real Task tool with subagent_type (generalPurpose,
66
+ // explore, shell, browser-use, …) but no user-defined named
67
+ // subagents. cclaw maps each named agent (planner/reviewer/…) onto
68
+ // generic dispatch with a role prompt — see the cursor playbook.
69
+ nativeSubagentDispatch: "generic",
65
70
  hookSurface: "full",
66
- structuredAsk: "AskQuestion"
71
+ structuredAsk: "AskQuestion",
72
+ subagentFallback: "generic-dispatch"
67
73
  }
68
74
  },
69
75
  opencode: {
@@ -72,7 +78,8 @@ export const HARNESS_ADAPTERS = {
72
78
  capabilities: {
73
79
  nativeSubagentDispatch: "partial",
74
80
  hookSurface: "plugin",
75
- structuredAsk: "plain-text"
81
+ structuredAsk: "plain-text",
82
+ subagentFallback: "role-switch"
76
83
  }
77
84
  },
78
85
  codex: {
@@ -81,7 +88,8 @@ export const HARNESS_ADAPTERS = {
81
88
  capabilities: {
82
89
  nativeSubagentDispatch: "none",
83
90
  hookSurface: "full",
84
- structuredAsk: "plain-text"
91
+ structuredAsk: "plain-text",
92
+ subagentFallback: "role-switch"
85
93
  }
86
94
  }
87
95
  };
@@ -94,11 +102,22 @@ export function harnessTier(harnessId) {
94
102
  }
95
103
  if (capabilities.hookSurface === "full" ||
96
104
  capabilities.hookSurface === "plugin" ||
105
+ capabilities.nativeSubagentDispatch === "generic" ||
97
106
  capabilities.nativeSubagentDispatch === "partial") {
98
107
  return "tier2";
99
108
  }
100
109
  return "tier3";
101
110
  }
111
+ /**
112
+ * Harness IDs ordered from best (tier1) to least-capable. Stable sort — same
113
+ * tier preserves declaration order.
114
+ */
115
+ export function harnessesByTier() {
116
+ return Object.keys(HARNESS_ADAPTERS).sort((a, b) => {
117
+ const tierOrder = { tier1: 0, tier2: 1, tier3: 2 };
118
+ return tierOrder[harnessTier(a)] - tierOrder[harnessTier(b)];
119
+ });
120
+ }
102
121
  function agentsMdBlock() {
103
122
  return `${CCLAW_MARKER_START}
104
123
  ## Cclaw — Workflow Adapter
package/dist/install.js CHANGED
@@ -37,6 +37,7 @@ import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
37
37
  import { HARNESS_TOOL_REFS_DIR, HARNESS_TOOL_REFS_INDEX_MD, harnessToolRefMarkdown } from "./content/harness-tool-refs.js";
38
38
  import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
39
39
  import { harnessIntegrationDocMarkdown } from "./content/harnesses-doc.js";
40
+ import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName, harnessPlaybookMarkdown, harnessPlaybooksIndexMarkdown } from "./content/harness-playbooks.js";
40
41
  import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./content/hook-events.js";
41
42
  import { createInitialFlowState } from "./flow-state.js";
42
43
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
@@ -293,6 +294,15 @@ async function writeSkills(projectRoot, config) {
293
294
  await writeFileSafe(runtimePath(projectRoot, ...doctorRefsDir, fileName), markdown);
294
295
  }
295
296
  await writeFileSafe(runtimePath(projectRoot, "references", "harnesses.md"), harnessIntegrationDocMarkdown());
297
+ // Per-harness parity playbooks. Generated for every supported harness
298
+ // regardless of which harnesses the project installed — the index always
299
+ // resolves, and doctor only asserts presence of the installed harnesses'
300
+ // playbooks (see runtime-integrity checks).
301
+ const playbookDirSegments = HARNESS_PLAYBOOKS_DIR.split("/");
302
+ await writeFileSafe(runtimePath(projectRoot, ...playbookDirSegments, "README.md"), harnessPlaybooksIndexMarkdown());
303
+ for (const harness of harnessIds) {
304
+ await writeFileSafe(runtimePath(projectRoot, ...playbookDirSegments, harnessPlaybookFileName(harness)), harnessPlaybookMarkdown(harness));
305
+ }
296
306
  }
297
307
  async function writeUtilityCommands(projectRoot) {
298
308
  await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
@@ -948,15 +958,40 @@ async function writeHarnessGapsState(projectRoot, harnesses) {
948
958
  if (capabilities.structuredAsk === "plain-text") {
949
959
  missingCapabilities.push("structuredAsk:none");
950
960
  }
961
+ const remediation = [];
962
+ switch (capabilities.subagentFallback) {
963
+ case "native":
964
+ // nothing to remediate — harness has first-class dispatch
965
+ break;
966
+ case "generic-dispatch":
967
+ remediation.push(`subagent dispatch → map named cclaw agents onto generic Task subagent_type per ${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`);
968
+ break;
969
+ case "role-switch":
970
+ remediation.push(`subagent dispatch → role-switch in-session with evidenceRefs per ${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`);
971
+ break;
972
+ case "waiver":
973
+ remediation.push(`subagent dispatch → record explicit harness_limitation waiver; no parity path available`);
974
+ break;
975
+ }
976
+ if (capabilities.structuredAsk === "plain-text") {
977
+ remediation.push("structured ask → fall back to a numbered plain-text list; first option is default");
978
+ }
979
+ for (const event of missingHookEvents) {
980
+ remediation.push(`hook event ${event} → schedule the corresponding script manually or accept reduced observability`);
981
+ }
951
982
  return {
952
983
  harness,
953
984
  tier: harnessTier(harness),
985
+ subagentFallback: capabilities.subagentFallback,
986
+ playbookPath: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/${harness}-playbook.md`,
954
987
  missingCapabilities,
955
- missingHookEvents
988
+ missingHookEvents,
989
+ remediation
956
990
  };
957
991
  });
958
992
  await writeFileSafe(runtimePath(projectRoot, "state", "harness-gaps.json"), `${JSON.stringify({
959
993
  generatedAt: new Date().toISOString(),
994
+ schemaVersion: 2,
960
995
  harnesses: report
961
996
  }, null, 2)}\n`);
962
997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {