cclaw-cli 0.31.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.
@@ -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",
@@ -28,6 +28,35 @@ export interface RetroState {
28
28
  completedAt?: string;
29
29
  compoundEntries: number;
30
30
  }
31
+ /**
32
+ * Ship closeout substate machine.
33
+ *
34
+ * After ship completes, cclaw auto-chains retro → compound → archive.
35
+ * Each step is interruptible: `/cc-next` reads `shipSubstate` and resumes
36
+ * from the correct step even across sessions.
37
+ *
38
+ * - `idle` — ship not complete, or closeout not yet started.
39
+ * - `retro_review` — 09-retro.md draft exists; awaiting user edit/accept/skip.
40
+ * - `compound_review` — retro accepted; compound pass awaiting execution
41
+ * (or user skip).
42
+ * - `ready_to_archive` — retro + compound done; archive is the next
43
+ * automatic step.
44
+ * - `archived` — archive completed in this session (transient — archive
45
+ * resets flow-state so this value does not persist between runs).
46
+ */
47
+ export declare const SHIP_SUBSTATES: readonly ["idle", "retro_review", "compound_review", "ready_to_archive", "archived"];
48
+ export type ShipSubstate = (typeof SHIP_SUBSTATES)[number];
49
+ export interface CloseoutState {
50
+ shipSubstate: ShipSubstate;
51
+ retroDraftedAt?: string;
52
+ retroAcceptedAt?: string;
53
+ retroSkipped?: boolean;
54
+ retroSkipReason?: string;
55
+ compoundCompletedAt?: string;
56
+ compoundSkipped?: boolean;
57
+ compoundPromoted: number;
58
+ }
59
+ export declare function createInitialCloseoutState(): CloseoutState;
31
60
  export interface FlowState {
32
61
  activeRunId: string;
33
62
  currentStage: FlowStage;
@@ -44,6 +73,8 @@ export interface FlowState {
44
73
  rewinds: RewindRecord[];
45
74
  /** Mandatory retrospective gate status before archive. */
46
75
  retro: RetroState;
76
+ /** Ship → retro → compound → archive substate for resumable closeout. */
77
+ closeout: CloseoutState;
47
78
  }
48
79
  export interface InitialFlowStateOptions {
49
80
  activeRunId?: string;
@@ -2,6 +2,41 @@ import { COMMAND_FILE_ORDER } from "./constants.js";
2
2
  import { buildTransitionRules, orderedStageSchemas, stageConditionalGateIds, stageGateIds, stageRecommendedGateIds } from "./content/stage-schema.js";
3
3
  import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
4
4
  export const TRANSITION_RULES = buildTransitionRules();
5
+ /**
6
+ * Ship closeout substate machine.
7
+ *
8
+ * After ship completes, cclaw auto-chains retro → compound → archive.
9
+ * Each step is interruptible: `/cc-next` reads `shipSubstate` and resumes
10
+ * from the correct step even across sessions.
11
+ *
12
+ * - `idle` — ship not complete, or closeout not yet started.
13
+ * - `retro_review` — 09-retro.md draft exists; awaiting user edit/accept/skip.
14
+ * - `compound_review` — retro accepted; compound pass awaiting execution
15
+ * (or user skip).
16
+ * - `ready_to_archive` — retro + compound done; archive is the next
17
+ * automatic step.
18
+ * - `archived` — archive completed in this session (transient — archive
19
+ * resets flow-state so this value does not persist between runs).
20
+ */
21
+ export const SHIP_SUBSTATES = [
22
+ "idle",
23
+ "retro_review",
24
+ "compound_review",
25
+ "ready_to_archive",
26
+ "archived"
27
+ ];
28
+ export function createInitialCloseoutState() {
29
+ return {
30
+ shipSubstate: "idle",
31
+ retroDraftedAt: undefined,
32
+ retroAcceptedAt: undefined,
33
+ retroSkipped: undefined,
34
+ retroSkipReason: undefined,
35
+ compoundCompletedAt: undefined,
36
+ compoundSkipped: undefined,
37
+ compoundPromoted: 0
38
+ };
39
+ }
5
40
  export function isFlowTrack(value) {
6
41
  return typeof value === "string" && FLOW_TRACKS.includes(value);
7
42
  }
@@ -48,7 +83,8 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
48
83
  required: false,
49
84
  completedAt: undefined,
50
85
  compoundEntries: 0
51
- }
86
+ },
87
+ closeout: createInitialCloseoutState()
52
88
  };
53
89
  }
54
90
  export function canTransition(from, to) {
@@ -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/dist/runs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
4
- import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
4
+ import { canTransition, createInitialCloseoutState, createInitialFlowState, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
5
5
  import { ensureFeatureSystem, readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
6
6
  import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
7
7
  export class InvalidStageTransitionError extends Error {
@@ -272,6 +272,37 @@ function sanitizeRetroState(value) {
272
272
  compoundEntries
273
273
  };
274
274
  }
275
+ function isShipSubstate(value) {
276
+ return typeof value === "string" && SHIP_SUBSTATES.includes(value);
277
+ }
278
+ function sanitizeCloseoutState(value) {
279
+ const fallback = createInitialCloseoutState();
280
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
281
+ return fallback;
282
+ }
283
+ const typed = value;
284
+ const shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
285
+ const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
286
+ const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
287
+ const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
288
+ const retroSkipReason = typeof typed.retroSkipReason === "string" ? typed.retroSkipReason : undefined;
289
+ const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
290
+ const compoundSkipped = typeof typed.compoundSkipped === "boolean" ? typed.compoundSkipped : undefined;
291
+ const promotedRaw = typed.compoundPromoted;
292
+ const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
293
+ ? Math.floor(promotedRaw)
294
+ : 0;
295
+ return {
296
+ shipSubstate,
297
+ retroDraftedAt,
298
+ retroAcceptedAt,
299
+ retroSkipped,
300
+ retroSkipReason,
301
+ compoundCompletedAt,
302
+ compoundSkipped,
303
+ compoundPromoted
304
+ };
305
+ }
275
306
  function coerceFlowState(parsed) {
276
307
  const track = coerceTrack(parsed.track);
277
308
  const next = createInitialFlowState("active", track);
@@ -289,7 +320,8 @@ function coerceFlowState(parsed) {
289
320
  skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
290
321
  staleStages: sanitizeStaleStages(parsed.staleStages),
291
322
  rewinds: sanitizeRewinds(parsed.rewinds),
292
- retro: sanitizeRetroState(parsed.retro)
323
+ retro: sanitizeRetroState(parsed.retro),
324
+ closeout: sanitizeCloseoutState(parsed.closeout)
293
325
  };
294
326
  }
295
327
  function toArchiveDate(date = new Date()) {
@@ -536,9 +568,12 @@ export async function archiveRun(projectRoot, featureName, options = {}) {
536
568
  if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
537
569
  throw new Error("archive --skip-retro requires --retro-reason=<text>.");
538
570
  }
539
- if (retroGate.required && !retroGate.completed && !skipRetro) {
571
+ const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
572
+ typeof sourceState.closeout.retroSkipReason === "string" &&
573
+ sourceState.closeout.retroSkipReason.trim().length > 0;
574
+ if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
540
575
  throw new Error("Archive blocked: retro gate is required after ship completion. " +
541
- "Run /cc-ops retro and append at least one compound knowledge entry, or re-run /cc-ops archive with --skip-retro and --retro-reason.");
576
+ "Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
542
577
  }
543
578
  if (retroGate.completed) {
544
579
  const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {