cclaw-cli 0.51.26 → 0.51.27

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.
@@ -10,6 +10,35 @@ import { readFlowState } from "./runs.js";
10
10
  import { stageSchema } from "./content/stage-schema.js";
11
11
  const execFileAsync = promisify(execFile);
12
12
  const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
13
+ export const DELEGATION_DISPATCH_SURFACES = [
14
+ "claude-task",
15
+ "cursor-task",
16
+ "opencode-agent",
17
+ "codex-agent",
18
+ "generic-task",
19
+ "role-switch",
20
+ "manual"
21
+ ];
22
+ /**
23
+ * Per-surface allowed agent-definition path prefixes. Used by the generated
24
+ * `.cclaw/hooks/delegation-record.mjs` helper to reject mismatched
25
+ * `--agent-definition-path` values without inspecting any harness state.
26
+ *
27
+ * The list is intentionally structural: each surface maps to one or more
28
+ * repo-relative path prefixes that must be a parent of the supplied path.
29
+ * `role-switch` and `manual` accept any path because the agent-definition
30
+ * is intentionally not a generated artifact for those surfaces.
31
+ */
32
+ export const DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES = {
33
+ "claude-task": [".claude/agents/", ".cclaw/agents/"],
34
+ "cursor-task": [".cursor/agents/", ".cclaw/agents/"],
35
+ "opencode-agent": [".opencode/agents/", ".cclaw/agents/"],
36
+ "codex-agent": [".codex/agents/", ".cclaw/agents/"],
37
+ "generic-task": [".cclaw/agents/"],
38
+ "role-switch": [],
39
+ "manual": []
40
+ };
41
+ export const DELEGATION_LEDGER_SCHEMA_VERSION = 3;
13
42
  function delegationLogPath(projectRoot) {
14
43
  return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json");
15
44
  }
@@ -175,7 +204,8 @@ function isDelegationEntry(value) {
175
204
  o.fulfillmentMode === "isolated" ||
176
205
  o.fulfillmentMode === "generic-dispatch" ||
177
206
  o.fulfillmentMode === "role-switch" ||
178
- o.fulfillmentMode === "harness-waiver") &&
207
+ o.fulfillmentMode === "harness-waiver" ||
208
+ o.fulfillmentMode === "legacy-inferred") &&
179
209
  (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
180
210
  (o.dispatchId === undefined || typeof o.dispatchId === "string") &&
181
211
  (o.workerRunId === undefined || typeof o.workerRunId === "string") &&
@@ -188,16 +218,10 @@ function isDelegationEntry(value) {
188
218
  retryOk &&
189
219
  (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
190
220
  (o.skill === undefined || typeof o.skill === "string") &&
191
- (o.schemaVersion === undefined || o.schemaVersion === 1));
221
+ (o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3));
192
222
  }
193
223
  function isDelegationDispatchSurface(value) {
194
- return (value === "claude-task" ||
195
- value === "cursor-task" ||
196
- value === "opencode-agent" ||
197
- value === "codex-agent" ||
198
- value === "generic-task" ||
199
- value === "role-switch" ||
200
- value === "manual");
224
+ return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
201
225
  }
202
226
  function statusTimestampPatch(entry, ts) {
203
227
  const patch = { ...entry };
@@ -215,30 +239,50 @@ function eventFromEntry(entry) {
215
239
  ...entry,
216
240
  event: entry.status,
217
241
  eventTs,
218
- schemaVersion: 1
242
+ schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
219
243
  };
220
244
  }
221
245
  function isDelegationEvent(value) {
222
246
  if (!isDelegationEntry(value))
223
247
  return false;
224
248
  const o = value;
225
- return o.event === o.status && typeof o.eventTs === "string";
249
+ if (o.event !== o.status || typeof o.eventTs !== "string")
250
+ return false;
251
+ return true;
226
252
  }
227
253
  function parseLedger(raw, runId) {
228
254
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
229
- return { runId, entries: [] };
255
+ return { runId, entries: [], schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION };
230
256
  }
231
257
  const o = raw;
258
+ const ledgerSchemaVersion = (o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3
259
+ ? o.schemaVersion
260
+ : undefined);
232
261
  const entriesRaw = o.entries;
233
262
  const entries = [];
234
263
  if (Array.isArray(entriesRaw)) {
235
264
  for (const item of entriesRaw) {
236
265
  if (isDelegationEntry(item)) {
237
266
  const ts = item.startTs ?? item.ts ?? new Date().toISOString();
238
- const isLegacyCompletion = item.fulfillmentMode === undefined &&
239
- item.schemaVersion === undefined &&
267
+ // A row is "pre-v3 legacy" when the file format predates the
268
+ // dispatch-proof contract: schemaVersion is missing on both ledger
269
+ // and entry, the entry has no fulfillmentMode, and there is no
270
+ // dispatch-surface or dispatch-id evidence on the row. We honor
271
+ // that by tagging fulfillmentMode = "legacy-inferred" so callers
272
+ // (stage-complete, doctor) can require an explicit `--rerecord`
273
+ // before the row counts as proof-era.
274
+ const ledgerHasNoVersion = ledgerSchemaVersion === undefined || ledgerSchemaVersion === 1;
275
+ const entryHasNoVersion = item.schemaVersion === undefined || item.schemaVersion === 1;
276
+ const looksLegacy = ledgerHasNoVersion &&
277
+ entryHasNoVersion &&
278
+ item.fulfillmentMode === undefined &&
279
+ item.dispatchSurface === undefined &&
280
+ item.dispatchId === undefined &&
281
+ item.workerRunId === undefined &&
282
+ item.agentDefinitionPath === undefined &&
240
283
  item.status === "completed";
241
- const inferredFulfillmentMode = item.fulfillmentMode ?? (isLegacyCompletion ? "isolated" : undefined);
284
+ const inferredFulfillmentMode = item.fulfillmentMode
285
+ ?? (looksLegacy ? "legacy-inferred" : (item.status === "completed" && item.schemaVersion === undefined ? "isolated" : undefined));
242
286
  entries.push({
243
287
  ...item,
244
288
  spanId: item.spanId ?? createSpanId(),
@@ -253,12 +297,12 @@ function parseLedger(raw, runId) {
253
297
  : 0,
254
298
  evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs : [],
255
299
  fulfillmentMode: inferredFulfillmentMode,
256
- schemaVersion: 1
300
+ schemaVersion: item.schemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION
257
301
  });
258
302
  }
259
303
  }
260
304
  }
261
- return { runId, entries };
305
+ return { runId, entries, schemaVersion: ledgerSchemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION };
262
306
  }
263
307
  export async function readDelegationLedger(projectRoot) {
264
308
  const { activeRunId } = await readFlowState(projectRoot);
@@ -348,7 +392,7 @@ export async function appendDelegation(projectRoot, entry) {
348
392
  if (stamped.status === "scheduled") {
349
393
  delete stamped.endTs;
350
394
  }
351
- stamped.schemaVersion = 1;
395
+ stamped.schemaVersion = DELEGATION_LEDGER_SCHEMA_VERSION;
352
396
  if (stamped.retryCount === undefined ||
353
397
  !Number.isInteger(stamped.retryCount) ||
354
398
  stamped.retryCount < 0) {
@@ -378,7 +422,8 @@ export async function appendDelegation(projectRoot, entry) {
378
422
  await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
379
423
  const ledger = {
380
424
  runId: activeRunId,
381
- entries: [...prior.entries, stamped]
425
+ entries: [...prior.entries, stamped],
426
+ schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
382
427
  };
383
428
  await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
384
429
  await writeSubagentTracker(projectRoot, ledger.entries);
@@ -419,6 +464,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
419
464
  const missingEvidence = [];
420
465
  const missingDispatchProof = [];
421
466
  const legacyInferredCompletions = [];
467
+ let legacyRequiresRerecord = false;
422
468
  const terminalSpanIds = new Set(forRun
423
469
  .filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
424
470
  .map((entry) => entry.spanId));
@@ -454,8 +500,20 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
454
500
  !completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
455
501
  missingEvidence.push(agent);
456
502
  }
503
+ // legacyInferredCompletions has two sources, split by `legacyTagged`:
504
+ // - legacyTagged === true : the row was *parsed* as legacy-inferred
505
+ // from a pre-v3 ledger file. Requires `delegation-record.mjs
506
+ // --rerecord` and BLOCKS satisfied.
507
+ // - legacyTagged === false: in-check inference for minimally-spec'd
508
+ // isolated rows that lack proof-era signals. Advisory only —
509
+ // preserves backward-compatible behavior for existing API callers.
457
510
  for (const row of completedRows) {
458
511
  const mode = row.fulfillmentMode ?? "isolated";
512
+ if (mode === "legacy-inferred") {
513
+ legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
514
+ legacyRequiresRerecord = true;
515
+ continue;
516
+ }
459
517
  if (mode === "isolated") {
460
518
  const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
461
519
  event.stage === stage &&
@@ -480,7 +538,12 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
480
538
  }
481
539
  }
482
540
  return {
483
- satisfied: missing.length === 0 && missingEvidence.length === 0 && missingDispatchProof.length === 0 && staleWorkers.length === 0 && events.corruptLines.length === 0,
541
+ satisfied: missing.length === 0 &&
542
+ missingEvidence.length === 0 &&
543
+ missingDispatchProof.length === 0 &&
544
+ !legacyRequiresRerecord &&
545
+ staleWorkers.length === 0 &&
546
+ events.corruptLines.length === 0,
484
547
  missing,
485
548
  waived,
486
549
  staleIgnored,
@@ -95,6 +95,48 @@ export declare function harnessShimFileNames(): string[];
95
95
  export declare function harnessShimSkillNames(): string[];
96
96
  export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
97
97
  export declare function harnessDispatchSurface(harnessId: HarnessId): string;
98
+ export interface HarnessDelegationRecipe {
99
+ harnessId: HarnessId;
100
+ dispatchSurface: "claude-task" | "cursor-task" | "opencode-agent" | "codex-agent";
101
+ agentDefinitionDirectory: string;
102
+ agentDefinitionExample: string;
103
+ invocationLine: string;
104
+ fulfillmentMode: "isolated" | "generic-dispatch";
105
+ /**
106
+ * Step-by-step lifecycle commands rendered with structural placeholders only:
107
+ * `<agent-name>`, `<stage>`, `<run-id>`, `<span-id>`, `<dispatch-id>`,
108
+ * `<agent-def-path>`, `<iso-ts>`. No domain/example values.
109
+ */
110
+ lifecycleCommands: string[];
111
+ }
112
+ /**
113
+ * Per-harness lifecycle recipe used by skills and harness docs to render the
114
+ * canonical scheduled -> launched -> acknowledged -> completed sequence in
115
+ * structural form. The recipe never embeds task-specific or domain-specific
116
+ * placeholders — only neutral angle-bracket tokens (`<agent-name>`, `<stage>`,
117
+ * `<span-id>`, `<dispatch-id>`, `<agent-def-path>`, `<iso-ts>`).
118
+ *
119
+ * This function returns the **canonical primary recipe** for each shipped
120
+ * harness — the dispatch surface that maps 1:1 onto the harness's vendor-
121
+ * native subagent surface:
122
+ *
123
+ * - `claude` -> `claude-task` (isolated)
124
+ * - `cursor` -> `cursor-task` (generic-dispatch)
125
+ * - `opencode` -> `opencode-agent` (isolated)
126
+ * - `codex` -> `codex-agent` (isolated)
127
+ *
128
+ * The remaining `--dispatch-surface` enum values (`generic-task`,
129
+ * `role-switch`, `manual`) are universal fallback paths available to any
130
+ * harness when the canonical surface is unavailable; they are documented in
131
+ * the dispatch-surface table in `docs/harnesses.md` rather than per-harness
132
+ * here, because their lifecycle commands are structurally identical except
133
+ * for the surface token. No shipped harness has a non-canonical *primary*
134
+ * surface, so this function only needs to enumerate the four canonical
135
+ * recipes above.
136
+ */
137
+ export declare function harnessDelegationRecipe(harnessId: HarnessId): HarnessDelegationRecipe;
138
+ /** All four harness recipes in tier-stable order. */
139
+ export declare function harnessDelegationRecipes(): HarnessDelegationRecipe[];
98
140
  export declare function harnessDispatchFallback(harnessId: HarnessId): string;
99
141
  export type HarnessTier = "tier1" | "tier2" | "tier3";
100
142
  export declare function harnessTier(harnessId: HarnessId): HarnessTier;
@@ -184,6 +184,101 @@ export function harnessDispatchSurface(harnessId) {
184
184
  return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
185
185
  }
186
186
  }
187
+ /**
188
+ * Per-harness lifecycle recipe used by skills and harness docs to render the
189
+ * canonical scheduled -> launched -> acknowledged -> completed sequence in
190
+ * structural form. The recipe never embeds task-specific or domain-specific
191
+ * placeholders — only neutral angle-bracket tokens (`<agent-name>`, `<stage>`,
192
+ * `<span-id>`, `<dispatch-id>`, `<agent-def-path>`, `<iso-ts>`).
193
+ *
194
+ * This function returns the **canonical primary recipe** for each shipped
195
+ * harness — the dispatch surface that maps 1:1 onto the harness's vendor-
196
+ * native subagent surface:
197
+ *
198
+ * - `claude` -> `claude-task` (isolated)
199
+ * - `cursor` -> `cursor-task` (generic-dispatch)
200
+ * - `opencode` -> `opencode-agent` (isolated)
201
+ * - `codex` -> `codex-agent` (isolated)
202
+ *
203
+ * The remaining `--dispatch-surface` enum values (`generic-task`,
204
+ * `role-switch`, `manual`) are universal fallback paths available to any
205
+ * harness when the canonical surface is unavailable; they are documented in
206
+ * the dispatch-surface table in `docs/harnesses.md` rather than per-harness
207
+ * here, because their lifecycle commands are structurally identical except
208
+ * for the surface token. No shipped harness has a non-canonical *primary*
209
+ * surface, so this function only needs to enumerate the four canonical
210
+ * recipes above.
211
+ */
212
+ export function harnessDelegationRecipe(harnessId) {
213
+ const helper = "node .cclaw/hooks/delegation-record.mjs";
214
+ const common = "--stage=<stage> --agent=<agent-name> --mode=mandatory --span-id=<span-id> --dispatch-id=<dispatch-id>";
215
+ switch (harnessId) {
216
+ case "claude":
217
+ return {
218
+ harnessId,
219
+ dispatchSurface: "claude-task",
220
+ agentDefinitionDirectory: ".claude/agents/",
221
+ agentDefinitionExample: ".claude/agents/<agent-name>.md",
222
+ invocationLine: "Call Task with subagent_type=<agent-name> and prompt body that paraphrases the stage skill role.",
223
+ fulfillmentMode: "isolated",
224
+ lifecycleCommands: [
225
+ `${helper} ${common} --status=scheduled --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --json`,
226
+ `${helper} ${common} --status=launched --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --launched-ts=<iso-ts> --json`,
227
+ `${helper} ${common} --status=acknowledged --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --ack-ts=<iso-ts> --json`,
228
+ `${helper} ${common} --status=completed --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --completed-ts=<iso-ts> --json`
229
+ ]
230
+ };
231
+ case "cursor":
232
+ return {
233
+ harnessId,
234
+ dispatchSurface: "cursor-task",
235
+ agentDefinitionDirectory: ".cclaw/agents/",
236
+ agentDefinitionExample: ".cclaw/agents/<agent-name>.md",
237
+ invocationLine: "Call Task with a generic subagent_type and paste the cclaw role prompt; capture worker output as evidenceRefs in the artifact.",
238
+ fulfillmentMode: "generic-dispatch",
239
+ lifecycleCommands: [
240
+ `${helper} ${common} --status=scheduled --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --json`,
241
+ `${helper} ${common} --status=launched --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --launched-ts=<iso-ts> --json`,
242
+ `${helper} ${common} --status=acknowledged --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --ack-ts=<iso-ts> --json`,
243
+ `${helper} ${common} --status=completed --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --completed-ts=<iso-ts> --evidence-ref=<artifact-anchor> --json`
244
+ ]
245
+ };
246
+ case "opencode":
247
+ return {
248
+ harnessId,
249
+ dispatchSurface: "opencode-agent",
250
+ agentDefinitionDirectory: ".opencode/agents/",
251
+ agentDefinitionExample: ".opencode/agents/<agent-name>.md",
252
+ invocationLine: "Invoke the generated agent via Task or `@<agent-name>`; the agent body lives in `.opencode/agents/<agent-name>.md`.",
253
+ fulfillmentMode: "isolated",
254
+ lifecycleCommands: [
255
+ `${helper} ${common} --status=scheduled --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --json`,
256
+ `${helper} ${common} --status=launched --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --launched-ts=<iso-ts> --json`,
257
+ `${helper} ${common} --status=acknowledged --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --ack-ts=<iso-ts> --json`,
258
+ `${helper} ${common} --status=completed --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --completed-ts=<iso-ts> --json`
259
+ ]
260
+ };
261
+ case "codex":
262
+ return {
263
+ harnessId,
264
+ dispatchSurface: "codex-agent",
265
+ agentDefinitionDirectory: ".codex/agents/",
266
+ agentDefinitionExample: ".codex/agents/<agent-name>.toml",
267
+ invocationLine: "Ask Codex to spawn the named custom agent; the agent definition lives in `.codex/agents/<agent-name>.toml`.",
268
+ fulfillmentMode: "isolated",
269
+ lifecycleCommands: [
270
+ `${helper} ${common} --status=scheduled --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --json`,
271
+ `${helper} ${common} --status=launched --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --launched-ts=<iso-ts> --json`,
272
+ `${helper} ${common} --status=acknowledged --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --ack-ts=<iso-ts> --json`,
273
+ `${helper} ${common} --status=completed --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --completed-ts=<iso-ts> --json`
274
+ ]
275
+ };
276
+ }
277
+ }
278
+ /** All four harness recipes in tier-stable order. */
279
+ export function harnessDelegationRecipes() {
280
+ return harnessesByTier().map((id) => harnessDelegationRecipe(id));
281
+ }
187
282
  export function harnessDispatchFallback(harnessId) {
188
283
  const adapter = HARNESS_ADAPTERS[harnessId];
189
284
  if (adapter.capabilities.subagentFallback !== "role-switch") {
@@ -6,7 +6,7 @@ import { resolveArtifactPath } from "../artifact-paths.js";
6
6
  import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
7
7
  import { ensureDir } from "../fs-utils.js";
8
8
  import { stageAutoSubagentDispatch, stageSchema } from "../content/stage-schema.js";
9
- import { appendDelegation, checkMandatoryDelegations, readDelegationLedger } from "../delegation.js";
9
+ import { appendDelegation, checkMandatoryDelegations, readDelegationEvents, readDelegationLedger } from "../delegation.js";
10
10
  import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
11
11
  import { extractMarkdownSectionBody, parseLearningsSection } from "../artifact-linter.js";
12
12
  import { getAvailableTransitions, getTransitionGuards, isFlowTrack, createInitialFlowState } from "../flow-state.js";
@@ -669,6 +669,9 @@ async function buildValidationReport(projectRoot, flowState, options = {}) {
669
669
  missing: delegation.missing,
670
670
  waived: delegation.waived,
671
671
  missingEvidence: delegation.missingEvidence,
672
+ missingDispatchProof: delegation.missingDispatchProof,
673
+ legacyInferredCompletions: delegation.legacyInferredCompletions,
674
+ corruptEventLines: delegation.corruptEventLines,
672
675
  staleWorkers: delegation.staleWorkers,
673
676
  expectedMode: delegation.expectedMode
674
677
  },
@@ -897,6 +900,48 @@ async function runAdvanceStage(projectRoot, args, io) {
897
900
  allowBlockedReviewRoute: blockedReviewRoute
898
901
  });
899
902
  if (!validation.ok) {
903
+ const ledgerForDiag = await readDelegationLedger(projectRoot).catch(() => ({ entries: [] }));
904
+ const eventsForDiag = await readDelegationEvents(projectRoot).catch(() => ({ events: [], corruptLines: [] }));
905
+ const ledgerEntriesText = await fs.readFile(path.join(projectRoot, ".cclaw/state/delegation-events.jsonl"), "utf8").catch(() => "");
906
+ const corruptSnippets = (() => {
907
+ if (validation.delegation.corruptEventLines.length === 0)
908
+ return [];
909
+ const lines = ledgerEntriesText.split(/\r?\n/u);
910
+ return validation.delegation.corruptEventLines.slice(0, 3).map((lineNo) => {
911
+ const line = lines[lineNo - 1] ?? "";
912
+ const sample = line.length > 120 ? `${line.slice(0, 117)}...` : line;
913
+ return `line ${lineNo}: ${sample}`;
914
+ });
915
+ })();
916
+ const dispatchProofDetails = validation.delegation.missingDispatchProof.flatMap((agent) => {
917
+ const rows = ledgerForDiag.entries.filter((entry) => entry.agent === agent && entry.status === "completed");
918
+ return rows.map((row) => `${agent}(spanId=${row.spanId ?? "unknown"})`);
919
+ });
920
+ const nextActions = [];
921
+ if (validation.delegation.missing.length > 0) {
922
+ nextActions.push(`Complete or waive mandatory delegation(s): ${validation.delegation.missing.join(", ")}. Helper: \`node .cclaw/hooks/stage-complete.mjs ${args.stage} --waive-delegation=${validation.delegation.missing.join(",")} --waiver-reason="<why safe>"\`.`);
923
+ }
924
+ if (validation.delegation.missingEvidence.length > 0) {
925
+ nextActions.push(`Role-switch fallback completion needs --evidence-ref or escalate to a real isolated dispatch surface.`);
926
+ }
927
+ if (validation.delegation.missingDispatchProof.length > 0) {
928
+ nextActions.push(`Isolated completion(s) ${dispatchProofDetails.join(", ") || validation.delegation.missingDispatchProof.join(", ")} lack matching dispatch proof; run the helper lifecycle scheduled -> launched -> acknowledged -> completed with --span-id, --dispatch-id, --dispatch-surface and --agent-definition-path before advancing.`);
929
+ }
930
+ if (validation.delegation.legacyInferredCompletions.length > 0) {
931
+ nextActions.push(`Pre-v3 ledger entries found: ${validation.delegation.legacyInferredCompletions.join(", ")}. Run \`node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path>\` to upgrade the row to dispatch-proof shape.`);
932
+ }
933
+ if (validation.delegation.corruptEventLines.length > 0) {
934
+ nextActions.push(`delegation-events.jsonl has ${validation.delegation.corruptEventLines.length} corrupt line(s) at ${validation.delegation.corruptEventLines.slice(0, 3).join(", ")}${validation.delegation.corruptEventLines.length > 3 ? ", ..." : ""}; remove or fix them before advancing.`);
935
+ }
936
+ if (validation.delegation.staleWorkers.length > 0) {
937
+ nextActions.push(`Stale scheduled delegations ${validation.delegation.staleWorkers.join(", ")} have no terminal row sharing the same spanId; emit launched/acknowledged/completed (or failed/stale) before advancing.`);
938
+ }
939
+ if (validation.gates.issues.length > 0) {
940
+ nextActions.push("Fix the artifact/gate issue shown in gates.issues, then rerun stage-complete.");
941
+ }
942
+ if (validation.completedStages.issues.length > 0) {
943
+ nextActions.push("Repair previously completed stage gate closure before advancing.");
944
+ }
900
945
  if (args.json) {
901
946
  io.stdout.write(`${JSON.stringify({
902
947
  ok: false,
@@ -906,23 +951,12 @@ async function runAdvanceStage(projectRoot, args, io) {
906
951
  delegation: validation.delegation,
907
952
  gates: validation.gates,
908
953
  completedStages: validation.completedStages,
909
- nextActions: [
910
- ...(validation.delegation.missing.length > 0
911
- ? [`Complete or waive mandatory delegation(s): ${validation.delegation.missing.join(", ")}.`]
912
- : []),
913
- ...(validation.delegation.missingEvidence.length > 0
914
- ? ["Add evidenceRefs for role-switch delegation completion or use an explicit waiver reason."]
915
- : []),
916
- ...(validation.delegation.staleWorkers.length > 0
917
- ? ["Resolve scheduled delegation span(s) without terminal lifecycle evidence before advancing."]
918
- : []),
919
- ...(validation.gates.issues.length > 0
920
- ? ["Fix the artifact/gate issue shown in gates.issues, then rerun stage-complete."]
921
- : []),
922
- ...(validation.completedStages.issues.length > 0
923
- ? ["Repair previously completed stage gate closure before advancing."]
924
- : [])
925
- ]
954
+ diagnostics: {
955
+ dispatchProofRows: dispatchProofDetails,
956
+ corruptEventSamples: corruptSnippets,
957
+ unawareEvents: eventsForDiag.corruptLines.length
958
+ },
959
+ nextActions
926
960
  })}\n`);
927
961
  }
928
962
  io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}".\n`);
@@ -932,9 +966,25 @@ async function runAdvanceStage(projectRoot, args, io) {
932
966
  }
933
967
  if (validation.delegation.missingEvidence.length > 0) {
934
968
  io.stderr.write(`- role-switch evidence missing: ${validation.delegation.missingEvidence.join(", ")}\n`);
969
+ io.stderr.write(` next action: include --evidence-ref=<artifact#anchor> when emitting the completed event, or escalate to a true isolated dispatch surface.\n`);
970
+ }
971
+ if (validation.delegation.missingDispatchProof.length > 0) {
972
+ io.stderr.write(`- isolated completion lacks dispatch proof: ${dispatchProofDetails.join(", ") || validation.delegation.missingDispatchProof.join(", ")}\n`);
973
+ io.stderr.write(` next action: emit scheduled -> launched -> acknowledged -> completed with --span-id, --dispatch-id, --dispatch-surface, --agent-definition-path before advancing.\n`);
974
+ }
975
+ if (validation.delegation.legacyInferredCompletions.length > 0) {
976
+ io.stderr.write(`- legacy-inferred completions need rerecord: ${validation.delegation.legacyInferredCompletions.join(", ")}\n`);
977
+ io.stderr.write(` next action: \`node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path>\`.\n`);
978
+ }
979
+ if (validation.delegation.corruptEventLines.length > 0) {
980
+ io.stderr.write(`- corrupt delegation-events.jsonl line(s): ${validation.delegation.corruptEventLines.slice(0, 3).join(", ")}${validation.delegation.corruptEventLines.length > 3 ? `, ... (+${validation.delegation.corruptEventLines.length - 3})` : ""}\n`);
981
+ for (const snippet of corruptSnippets) {
982
+ io.stderr.write(` sample: ${snippet}\n`);
983
+ }
935
984
  }
936
985
  if (validation.delegation.staleWorkers.length > 0) {
937
986
  io.stderr.write(`- stale scheduled delegations: ${validation.delegation.staleWorkers.join(", ")}\n`);
987
+ io.stderr.write(` next action: emit a terminal row (completed/failed/stale) for the same span before advancing.\n`);
938
988
  }
939
989
  if (validation.gates.issues.length > 0) {
940
990
  io.stderr.write(`- gate issues: ${validation.gates.issues.join(" | ")}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.26",
3
+ "version": "0.51.27",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {