cclaw-cli 0.51.25 → 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.
@@ -9,13 +9,48 @@ import { HARNESS_ADAPTERS } from "./harness-adapters.js";
9
9
  import { readFlowState } from "./runs.js";
10
10
  import { stageSchema } from "./content/stage-schema.js";
11
11
  const execFileAsync = promisify(execFile);
12
- const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived"]);
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
  }
16
45
  function delegationLockPath(projectRoot) {
17
46
  return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
18
47
  }
48
+ function delegationEventsPath(projectRoot) {
49
+ return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-events.jsonl");
50
+ }
51
+ function subagentsStatePath(projectRoot) {
52
+ return path.join(projectRoot, RUNTIME_ROOT, "state", "subagents.json");
53
+ }
19
54
  function createSpanId() {
20
55
  return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
21
56
  }
@@ -131,13 +166,16 @@ function isDelegationEntry(value) {
131
166
  const o = value;
132
167
  const modeOk = o.mode === "mandatory" || o.mode === "proactive";
133
168
  const statusOk = o.status === "scheduled" ||
169
+ o.status === "launched" ||
170
+ o.status === "acknowledged" ||
134
171
  o.status === "completed" ||
135
172
  o.status === "failed" ||
136
- o.status === "waived";
173
+ o.status === "waived" ||
174
+ o.status === "stale";
137
175
  const timestampOk = typeof o.ts === "string" ||
138
176
  typeof o.startTs === "string";
139
- const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived";
140
- const lifecycleOk = o.status !== "scheduled" || o.endTs === undefined;
177
+ const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived" || o.status === "stale";
178
+ const lifecycleOk = (o.status !== "scheduled" && o.status !== "launched" && o.status !== "acknowledged") || o.endTs === undefined;
141
179
  const terminalLifecycleOk = !terminalStatus ||
142
180
  o.endTs === undefined ||
143
181
  typeof o.endTs === "string";
@@ -166,46 +204,105 @@ function isDelegationEntry(value) {
166
204
  o.fulfillmentMode === "isolated" ||
167
205
  o.fulfillmentMode === "generic-dispatch" ||
168
206
  o.fulfillmentMode === "role-switch" ||
169
- o.fulfillmentMode === "harness-waiver") &&
207
+ o.fulfillmentMode === "harness-waiver" ||
208
+ o.fulfillmentMode === "legacy-inferred") &&
170
209
  (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
210
+ (o.dispatchId === undefined || typeof o.dispatchId === "string") &&
211
+ (o.workerRunId === undefined || typeof o.workerRunId === "string") &&
212
+ (o.dispatchSurface === undefined || isDelegationDispatchSurface(o.dispatchSurface)) &&
213
+ (o.agentDefinitionPath === undefined || typeof o.agentDefinitionPath === "string") &&
214
+ (o.ackTs === undefined || typeof o.ackTs === "string") &&
215
+ (o.launchedTs === undefined || typeof o.launchedTs === "string") &&
216
+ (o.completedTs === undefined || typeof o.completedTs === "string") &&
171
217
  (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
172
218
  retryOk &&
173
219
  (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
174
220
  (o.skill === undefined || typeof o.skill === "string") &&
175
- (o.schemaVersion === undefined || o.schemaVersion === 1));
221
+ (o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3));
222
+ }
223
+ function isDelegationDispatchSurface(value) {
224
+ return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
225
+ }
226
+ function statusTimestampPatch(entry, ts) {
227
+ const patch = { ...entry };
228
+ if (patch.status === "launched")
229
+ patch.launchedTs = patch.launchedTs ?? ts;
230
+ if (patch.status === "acknowledged")
231
+ patch.ackTs = patch.ackTs ?? ts;
232
+ if (patch.status === "completed")
233
+ patch.completedTs = patch.completedTs ?? patch.endTs ?? ts;
234
+ return patch;
235
+ }
236
+ function eventFromEntry(entry) {
237
+ const eventTs = entry.completedTs ?? entry.ackTs ?? entry.launchedTs ?? entry.endTs ?? entry.startTs ?? entry.ts ?? new Date().toISOString();
238
+ return {
239
+ ...entry,
240
+ event: entry.status,
241
+ eventTs,
242
+ schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
243
+ };
244
+ }
245
+ function isDelegationEvent(value) {
246
+ if (!isDelegationEntry(value))
247
+ return false;
248
+ const o = value;
249
+ if (o.event !== o.status || typeof o.eventTs !== "string")
250
+ return false;
251
+ return true;
176
252
  }
177
253
  function parseLedger(raw, runId) {
178
254
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
179
- return { runId, entries: [] };
255
+ return { runId, entries: [], schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION };
180
256
  }
181
257
  const o = raw;
258
+ const ledgerSchemaVersion = (o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3
259
+ ? o.schemaVersion
260
+ : undefined);
182
261
  const entriesRaw = o.entries;
183
262
  const entries = [];
184
263
  if (Array.isArray(entriesRaw)) {
185
264
  for (const item of entriesRaw) {
186
265
  if (isDelegationEntry(item)) {
187
266
  const ts = item.startTs ?? item.ts ?? new Date().toISOString();
188
- const isLegacyCompletion = item.fulfillmentMode === undefined &&
189
- 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 &&
190
283
  item.status === "completed";
191
- const inferredFulfillmentMode = item.fulfillmentMode ?? (isLegacyCompletion ? "isolated" : undefined);
284
+ const inferredFulfillmentMode = item.fulfillmentMode
285
+ ?? (looksLegacy ? "legacy-inferred" : (item.status === "completed" && item.schemaVersion === undefined ? "isolated" : undefined));
192
286
  entries.push({
193
287
  ...item,
194
288
  spanId: item.spanId ?? createSpanId(),
195
289
  startTs: ts,
196
290
  endTs: TERMINAL_DELEGATION_STATUSES.has(item.status) ? (item.endTs ?? ts) : undefined,
197
291
  ts,
292
+ launchedTs: item.launchedTs ?? (item.status === "launched" ? ts : undefined),
293
+ ackTs: item.ackTs ?? (item.status === "acknowledged" ? ts : undefined),
294
+ completedTs: item.completedTs ?? (item.status === "completed" ? (item.endTs ?? ts) : undefined),
198
295
  retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
199
296
  ? item.retryCount
200
297
  : 0,
201
298
  evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs : [],
202
299
  fulfillmentMode: inferredFulfillmentMode,
203
- schemaVersion: 1
300
+ schemaVersion: item.schemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION
204
301
  });
205
302
  }
206
303
  }
207
304
  }
208
- return { runId, entries };
305
+ return { runId, entries, schemaVersion: ledgerSchemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION };
209
306
  }
210
307
  export async function readDelegationLedger(projectRoot) {
211
308
  const { activeRunId } = await readFlowState(projectRoot);
@@ -222,6 +319,57 @@ export async function readDelegationLedger(projectRoot) {
222
319
  return { runId: activeRunId, entries: [] };
223
320
  }
224
321
  }
322
+ export async function readDelegationEvents(projectRoot) {
323
+ const filePath = delegationEventsPath(projectRoot);
324
+ if (!(await exists(filePath))) {
325
+ return { events: [], corruptLines: [] };
326
+ }
327
+ const events = [];
328
+ const corruptLines = [];
329
+ const text = await fs.readFile(filePath, "utf8").catch(() => "");
330
+ const lines = text.split(/\r?\n/gu);
331
+ for (let index = 0; index < lines.length; index += 1) {
332
+ const line = lines[index]?.trim() ?? "";
333
+ if (line.length === 0)
334
+ continue;
335
+ try {
336
+ const parsed = JSON.parse(line);
337
+ if (isDelegationEvent(parsed)) {
338
+ events.push(parsed);
339
+ }
340
+ else {
341
+ corruptLines.push(index + 1);
342
+ }
343
+ }
344
+ catch {
345
+ corruptLines.push(index + 1);
346
+ }
347
+ }
348
+ return { events, corruptLines };
349
+ }
350
+ async function appendDelegationEvent(projectRoot, event) {
351
+ const filePath = delegationEventsPath(projectRoot);
352
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
353
+ await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
354
+ }
355
+ async function writeSubagentTracker(projectRoot, entries) {
356
+ const active = entries
357
+ .filter((entry) => entry.status === "scheduled" || entry.status === "launched" || entry.status === "acknowledged")
358
+ .map((entry) => ({
359
+ spanId: entry.spanId,
360
+ dispatchId: entry.dispatchId,
361
+ workerRunId: entry.workerRunId,
362
+ stage: entry.stage,
363
+ agent: entry.agent,
364
+ status: entry.status,
365
+ dispatchSurface: entry.dispatchSurface,
366
+ agentDefinitionPath: entry.agentDefinitionPath,
367
+ startedAt: entry.startTs,
368
+ launchedAt: entry.launchedTs,
369
+ acknowledgedAt: entry.ackTs
370
+ }));
371
+ await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
372
+ }
225
373
  export async function appendDelegation(projectRoot, entry) {
226
374
  const { activeRunId } = await readFlowState(projectRoot);
227
375
  await withDirectoryLock(delegationLockPath(projectRoot), async () => {
@@ -231,17 +379,20 @@ export async function appendDelegation(projectRoot, entry) {
231
379
  if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
232
380
  throw new Error("waived delegation entries require a non-empty waiverReason");
233
381
  }
234
- const stamped = { ...entry, runId: entry.runId ?? activeRunId };
382
+ const stamped = statusTimestampPatch({ ...entry, runId: entry.runId ?? activeRunId }, startTs);
235
383
  stamped.spanId = entry.spanId ?? createSpanId();
236
384
  stamped.startTs = startTs;
237
385
  stamped.ts = startTs;
238
386
  if (TERMINAL_DELEGATION_STATUSES.has(stamped.status) && !stamped.endTs) {
239
387
  stamped.endTs = new Date().toISOString();
240
388
  }
389
+ if (stamped.status === "completed") {
390
+ stamped.completedTs = stamped.completedTs ?? stamped.endTs ?? new Date().toISOString();
391
+ }
241
392
  if (stamped.status === "scheduled") {
242
393
  delete stamped.endTs;
243
394
  }
244
- stamped.schemaVersion = 1;
395
+ stamped.schemaVersion = DELEGATION_LEDGER_SCHEMA_VERSION;
245
396
  if (stamped.retryCount === undefined ||
246
397
  !Number.isInteger(stamped.retryCount) ||
247
398
  stamped.retryCount < 0) {
@@ -268,11 +419,14 @@ export async function appendDelegation(projectRoot, entry) {
268
419
  if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
269
420
  return;
270
421
  }
422
+ await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
271
423
  const ledger = {
272
424
  runId: activeRunId,
273
- entries: [...prior.entries, stamped]
425
+ entries: [...prior.entries, stamped],
426
+ schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
274
427
  };
275
428
  await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
429
+ await writeSubagentTracker(projectRoot, ledger.entries);
276
430
  });
277
431
  }
278
432
  /**
@@ -299,6 +453,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
299
453
  const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
300
454
  const { activeRunId } = flowState;
301
455
  const ledger = await readDelegationLedger(projectRoot);
456
+ const events = await readDelegationEvents(projectRoot);
302
457
  const forStage = ledger.entries.filter((e) => e.stage === stage);
303
458
  const forRun = forStage.filter((e) => e.runId === activeRunId);
304
459
  const staleIgnored = forStage
@@ -307,6 +462,9 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
307
462
  const missing = [];
308
463
  const waived = [];
309
464
  const missingEvidence = [];
465
+ const missingDispatchProof = [];
466
+ const legacyInferredCompletions = [];
467
+ let legacyRequiresRerecord = false;
310
468
  const terminalSpanIds = new Set(forRun
311
469
  .filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
312
470
  .map((entry) => entry.spanId));
@@ -342,13 +500,57 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
342
500
  !completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
343
501
  missingEvidence.push(agent);
344
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.
510
+ for (const row of completedRows) {
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
+ }
517
+ if (mode === "isolated") {
518
+ const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
519
+ event.stage === stage &&
520
+ event.agent === agent &&
521
+ event.spanId === row.spanId);
522
+ const dispatchId = row.dispatchId ?? row.workerRunId ?? spanEvents.find((event) => event.dispatchId || event.workerRunId)?.dispatchId ?? spanEvents.find((event) => event.workerRunId)?.workerRunId;
523
+ const dispatchSurface = row.dispatchSurface ?? spanEvents.find((event) => event.dispatchSurface)?.dispatchSurface;
524
+ const agentDefinitionPath = row.agentDefinitionPath ?? spanEvents.find((event) => event.agentDefinitionPath)?.agentDefinitionPath;
525
+ const hasAck = Boolean(row.ackTs || spanEvents.some((event) => event.event === "acknowledged" && event.ackTs));
526
+ const hasCompleted = Boolean(row.completedTs || spanEvents.some((event) => event.event === "completed" && event.completedTs));
527
+ const hasDispatchProof = Boolean(row.spanId && dispatchId && dispatchSurface && agentDefinitionPath && hasAck && hasCompleted);
528
+ if (!hasDispatchProof) {
529
+ const proofEraSignal = Boolean(row.dispatchId || row.workerRunId || row.dispatchSurface || row.agentDefinitionPath || spanEvents.some((event) => event.dispatchId || event.workerRunId || event.dispatchSurface || event.agentDefinitionPath || event.event === "acknowledged" || event.event === "launched"));
530
+ if (proofEraSignal) {
531
+ missingDispatchProof.push(agent);
532
+ }
533
+ else {
534
+ legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
535
+ }
536
+ }
537
+ }
538
+ }
345
539
  }
346
540
  return {
347
- satisfied: missing.length === 0 && missingEvidence.length === 0 && staleWorkers.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,
348
547
  missing,
349
548
  waived,
350
549
  staleIgnored,
351
550
  missingEvidence,
551
+ missingDispatchProof,
552
+ legacyInferredCompletions,
553
+ corruptEventLines: events.corruptLines,
352
554
  staleWorkers,
353
555
  expectedMode
354
556
  };
@@ -107,6 +107,15 @@ const RULES = [
107
107
  docRef: ref("harnesses.md")
108
108
  }
109
109
  },
110
+ {
111
+ test: /^harness:reality:/,
112
+ metadata: {
113
+ severity: "info",
114
+ summary: "Harness reality label for dispatch/proof support.",
115
+ fix: "No action required; use this label to interpret native/generic/role-switch proof requirements.",
116
+ docRef: ref("harnesses.md")
117
+ }
118
+ },
110
119
  {
111
120
  test: /^delegation:/,
112
121
  metadata: {
package/dist/doctor.js CHANGED
@@ -13,7 +13,7 @@ import { policyChecks } from "./policy.js";
13
13
  import { CorruptFlowStateError, readFlowState } from "./runs.js";
14
14
  import { createInitialFlowState, skippedStagesForTrack } from "./flow-state.js";
15
15
  import { FLOW_STAGES, TRACK_STAGES } from "./types.js";
16
- import { checkMandatoryDelegations } from "./delegation.js";
16
+ import { checkMandatoryDelegations, readDelegationEvents } from "./delegation.js";
17
17
  import { buildTraceMatrix } from "./trace-matrix.js";
18
18
  import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
19
19
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
@@ -305,6 +305,20 @@ function normalizeOpenCodePluginEntry(entry) {
305
305
  }
306
306
  return null;
307
307
  }
308
+ function generatedAgentShape(content, kind, agentName) {
309
+ if (kind === "opencode") {
310
+ return content.includes("mode: subagent") && content.includes(`# ${agentName}`) && content.includes("STRICT_RETURN_SCHEMA");
311
+ }
312
+ return content.includes(`name = "${agentName}"`) && content.includes("developer_instructions") && content.includes("STRICT_RETURN_SCHEMA");
313
+ }
314
+ function harnessRealityLabel(harness) {
315
+ const adapter = HARNESS_ADAPTERS[harness];
316
+ const declaredSupport = adapter.capabilities.nativeSubagentDispatch;
317
+ const runtimeLaunch = harness === "opencode" || harness === "codex" ? "prompt-level launch" : declaredSupport === "generic" ? "generic Task launch" : "native tool launch";
318
+ const proofRequired = adapter.capabilities.subagentFallback === "native" ? "dispatchId+spanId+ack" : "evidenceRefs";
319
+ const proofSource = harness === "opencode" ? ".opencode/agents + delegation-events.jsonl" : harness === "codex" ? ".codex/agents + delegation-events.jsonl" : ".cclaw/state/delegation-log.json";
320
+ return `declaredSupport=${declaredSupport}; runtimeLaunch=${runtimeLaunch}; proofRequired=${proofRequired}; proofSource=${proofSource}`;
321
+ }
308
322
  const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
309
323
  function opencodeConfigCandidates(projectRoot) {
310
324
  return [
@@ -780,6 +794,14 @@ export async function doctorChecks(projectRoot, options = {}) {
780
794
  }
781
795
  }
782
796
  }
797
+ for (const harness of configuredHarnesses) {
798
+ checks.push({
799
+ name: `harness:reality:${harness}`,
800
+ ok: true,
801
+ severity: "info",
802
+ details: harnessRealityLabel(harness)
803
+ });
804
+ }
783
805
  const agentsFile = path.join(projectRoot, "AGENTS.md");
784
806
  let agentsBlockOk = false;
785
807
  if (await exists(agentsFile)) {
@@ -884,12 +906,39 @@ export async function doctorChecks(projectRoot, options = {}) {
884
906
  details: agentPath
885
907
  });
886
908
  }
909
+ for (const agent of CCLAW_AGENTS) {
910
+ if (configuredHarnesses.includes("opencode")) {
911
+ const agentPath = path.join(projectRoot, ".opencode", "agents", `${agent.name}.md`);
912
+ let ok = false;
913
+ if (await exists(agentPath)) {
914
+ ok = generatedAgentShape(await fs.readFile(agentPath, "utf8"), "opencode", agent.name);
915
+ }
916
+ checks.push({
917
+ name: `agent:opencode:${agent.name}:shape`,
918
+ ok,
919
+ details: `${agentPath} must be a generated OpenCode subagent with mode: subagent and strict return schema`
920
+ });
921
+ }
922
+ if (configuredHarnesses.includes("codex")) {
923
+ const agentPath = path.join(projectRoot, ".codex", "agents", `${agent.name}.toml`);
924
+ let ok = false;
925
+ if (await exists(agentPath)) {
926
+ ok = generatedAgentShape(await fs.readFile(agentPath, "utf8"), "codex", agent.name);
927
+ }
928
+ checks.push({
929
+ name: `agent:codex:${agent.name}:shape`,
930
+ ok,
931
+ details: `${agentPath} must be a generated Codex custom agent TOML with developer_instructions and strict return schema`
932
+ });
933
+ }
934
+ }
887
935
  // Hook scripts
888
936
  for (const script of [
889
937
  "run-hook.mjs",
890
938
  "run-hook.cmd",
891
939
  "stage-complete.mjs",
892
940
  "start-flow.mjs",
941
+ "delegation-record.mjs",
893
942
  "opencode-plugin.mjs"
894
943
  ]) {
895
944
  const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
@@ -1774,6 +1823,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1774
1823
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
1775
1824
  repairFeatureSystem: false
1776
1825
  });
1826
+ const delegationEvents = await readDelegationEvents(projectRoot);
1777
1827
  const delegationSatisfiedForDoctor = currentStageUntouched || delegation.satisfied;
1778
1828
  const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
1779
1829
  ? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
@@ -1785,7 +1835,30 @@ export async function doctorChecks(projectRoot, options = {}) {
1785
1835
  ? `mandatory delegation check deferred for untouched stage "${flowState.currentStage}"; stage-complete enforces it when work begins`
1786
1836
  : delegation.satisfied
1787
1837
  ? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
1788
- : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
1838
+ : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}; missingDispatchProof=${delegation.missingDispatchProof.join(", ")}; staleWorkers=${delegation.staleWorkers.join(", ")}; corruptEventLines=${delegation.corruptEventLines.join(", ")}`
1839
+ });
1840
+ checks.push({
1841
+ name: "delegation:events:parse",
1842
+ ok: delegationEvents.corruptLines.length === 0,
1843
+ details: delegationEvents.corruptLines.length === 0
1844
+ ? `${RUNTIME_ROOT}/state/delegation-events.jsonl parsed successfully (${delegationEvents.events.length} event(s))`
1845
+ : `corrupt delegation event line(s): ${delegationEvents.corruptLines.join(", ")}`
1846
+ });
1847
+ checks.push({
1848
+ name: "delegation:proof:current_stage",
1849
+ ok: currentStageUntouched || delegation.missingDispatchProof.length === 0,
1850
+ details: currentStageUntouched
1851
+ ? `dispatch proof check deferred for untouched stage "${flowState.currentStage}"`
1852
+ : delegation.missingDispatchProof.length === 0
1853
+ ? `no dispatch proof gaps for current stage "${flowState.currentStage}"`
1854
+ : `isolated completions missing dispatchId/dispatchSurface/agentDefinitionPath/ackTs/completedTs: ${delegation.missingDispatchProof.join(", ")}`
1855
+ });
1856
+ checks.push({
1857
+ name: "warning:delegation:legacy_inferred_completions",
1858
+ ok: true,
1859
+ details: delegation.legacyInferredCompletions.length > 0
1860
+ ? `warning: legacy inferred isolated completion rows lack event-log proof: ${delegation.legacyInferredCompletions.join(", ")}`
1861
+ : "no legacy inferred isolated completions for current stage"
1789
1862
  });
1790
1863
  checks.push({
1791
1864
  name: "warning:delegation:waived",
@@ -37,6 +37,12 @@ export type SubagentFallback =
37
37
  export type ShimKind = "command" | "skill";
38
38
  export interface HarnessAdapter {
39
39
  id: HarnessId;
40
+ reality: {
41
+ declaredSupport: "full" | "generic" | "partial" | "none";
42
+ runtimeLaunch: string;
43
+ proofRequired: string;
44
+ proofSource: string;
45
+ };
40
46
  /**
41
47
  * Root directory where cclaw writes `/cc*` entry points.
42
48
  *
@@ -89,6 +95,48 @@ export declare function harnessShimFileNames(): string[];
89
95
  export declare function harnessShimSkillNames(): string[];
90
96
  export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
91
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[];
92
140
  export declare function harnessDispatchFallback(harnessId: HarnessId): string;
93
141
  export type HarnessTier = "tier1" | "tier2" | "tier3";
94
142
  export declare function harnessTier(harnessId: HarnessId): HarnessTier;