cclaw-cli 0.51.24 → 0.51.26

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.
Files changed (45) hide show
  1. package/README.md +135 -414
  2. package/dist/artifact-linter.js +10 -6
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +28 -3
  5. package/dist/content/core-agents.d.ts +110 -0
  6. package/dist/content/core-agents.js +255 -3
  7. package/dist/content/examples.js +8 -5
  8. package/dist/content/harness-doc.d.ts +1 -0
  9. package/dist/content/harness-doc.js +3 -0
  10. package/dist/content/hooks.d.ts +1 -0
  11. package/dist/content/hooks.js +189 -0
  12. package/dist/content/next-command.js +10 -6
  13. package/dist/content/reference-patterns.d.ts +18 -0
  14. package/dist/content/reference-patterns.js +391 -0
  15. package/dist/content/skills.js +42 -36
  16. package/dist/content/stage-common-guidance.js +19 -3
  17. package/dist/content/stage-schema.d.ts +12 -0
  18. package/dist/content/stage-schema.js +184 -28
  19. package/dist/content/stages/_lint-metadata/index.js +3 -2
  20. package/dist/content/stages/brainstorm.js +7 -3
  21. package/dist/content/stages/design.js +12 -3
  22. package/dist/content/stages/review.js +7 -5
  23. package/dist/content/stages/schema-types.d.ts +9 -2
  24. package/dist/content/stages/scope.js +8 -2
  25. package/dist/content/stages/ship.js +3 -2
  26. package/dist/content/stages/tdd.js +18 -13
  27. package/dist/content/start-command.js +3 -2
  28. package/dist/content/status-command.js +17 -6
  29. package/dist/content/subagents.js +286 -40
  30. package/dist/content/templates.js +64 -3
  31. package/dist/content/tree-command.js +7 -1
  32. package/dist/delegation.d.ts +34 -1
  33. package/dist/delegation.js +168 -8
  34. package/dist/doctor-registry.js +9 -0
  35. package/dist/doctor.js +121 -6
  36. package/dist/gate-evidence.js +25 -2
  37. package/dist/harness-adapters.d.ts +6 -0
  38. package/dist/harness-adapters.js +28 -4
  39. package/dist/install.js +5 -10
  40. package/dist/internal/advance-stage.js +179 -26
  41. package/dist/run-persistence.js +21 -3
  42. package/dist/tdd-verification-evidence.d.ts +17 -0
  43. package/dist/tdd-verification-evidence.js +43 -0
  44. package/dist/types.d.ts +10 -0
  45. package/package.json +1 -1
@@ -29,6 +29,11 @@ export const ARTIFACT_TEMPLATES = {
29
29
  ### Discovered context
30
30
  - (paths, prior artifacts, seeds, prompt fragments — referenced by downstream stages, or \`- None.\`)
31
31
 
32
+ ## Reference Pattern Candidates
33
+ | Pattern / source | Reusable invariant | Disposition (accept/reject/defer) | Why |
34
+ |---|---|---|---|
35
+ | | | | |
36
+
32
37
  ## Problem Decision Record
33
38
  - **Depth:** lite | standard | deep
34
39
  - **Frame type:** product | technical-maintenance
@@ -60,6 +65,12 @@ export const ARTIFACT_TEMPLATES = {
60
65
  ## How Might We
61
66
  - *How might we …?* — one line naming the user, the desired outcome, and the binding constraint.
62
67
 
68
+ ## Clarity Gate
69
+ - Ambiguity score (0.00-1.00):
70
+ - Decision boundaries (what this stage will decide):
71
+ - Reaffirmed non-goals:
72
+ - Residual-risk handoff to scope:
73
+
63
74
  ## Sharpening Questions
64
75
  > Ask one decision-changing question at a time. For concrete early exits, record \`None - early exit\` with rationale.
65
76
  | # | Question | Answer / Assumption | Decision impact |
@@ -81,7 +92,7 @@ export const ARTIFACT_TEMPLATES = {
81
92
  - Scope handoff:
82
93
 
83
94
  ## Approaches
84
- | Approach | Role | Upside | Architecture | Trade-offs | Reuses | Recommendation |
95
+ | Approach | Role | Upside | Architecture | Trade-offs | Reuses / reference pattern | Recommendation |
85
96
  |---|---|---|---|---|---|---|
86
97
  | A | baseline | modest | | | | |
87
98
  | B | challenger | high | | | | |
@@ -195,6 +206,20 @@ ${SEED_SHELF_SECTION}
195
206
  - **Success definition:**
196
207
  - **Design handoff:**
197
208
 
209
+ ## Decision Drivers
210
+ | Driver | Weight (1-5) | Option A | Option B | Option C | Notes |
211
+ |---|---|---|---|---|---|
212
+ | Value impact | | | | | |
213
+ | Risk reduction | | | | | |
214
+ | Reversibility | | | | | |
215
+ | Delivery effort | | | | | |
216
+ | Timeline fit | | | | | |
217
+
218
+ ## Scope Completeness Score
219
+ - Score (0.00-1.00):
220
+ - What is still uncertain:
221
+ - Blockers requiring escalation:
222
+
198
223
  ## Scope Mode
199
224
  - [ ] SCOPE EXPANSION — explore ambitious alternatives; user explicitly opts into the larger product slice.
200
225
  - [ ] SELECTIVE EXPANSION — hold baseline scope and cherry-pick one high-leverage addition.
@@ -214,6 +239,11 @@ ${SEED_SHELF_SECTION}
214
239
  ## Taste Calibration
215
240
  - Optional quality-bar references from in-repo modules/files.
216
241
 
242
+ ## Reference Pattern Registry
243
+ | Pattern / source | Invariant to preserve | Disposition (accepted/rejected/deferred) | Scope boundary impact |
244
+ |---|---|---|---|
245
+ | | | | |
246
+
217
247
  ## Reference Pull
218
248
  - Optional evidence from \`/Users/zuevrs/Downloads/references\`; list accepted/rejected ideas or \`Not needed - compact scope\`.
219
249
 
@@ -359,6 +389,11 @@ ${SEED_SHELF_SECTION}
359
389
  |---|---|---|---|---|---|---|
360
390
  | | | | | | | |
361
391
 
392
+ ## Architecture Decision Record (ADR)
393
+ | ADR ID | Context | Decision | Alternatives considered | Consequences | Reversal trigger |
394
+ |---|---|---|---|---|---|
395
+ | ADR-1 | | | | | |
396
+
362
397
  ## Search Before Building
363
398
  | Layer | Label | What to reuse first |
364
399
  |---|---|---|
@@ -466,11 +501,21 @@ ${MARKDOWN_CODE_FENCE}
466
501
  |---|---|---|---|
467
502
  | | | | |
468
503
 
504
+ ## Pre-mortem
505
+ | Scenario | Earliest warning signal | Mitigation owner | Containment action |
506
+ |---|---|---|---|
507
+ | | | | |
508
+
469
509
  ## Test Strategy
470
510
  - Unit:
471
511
  - Integration:
472
512
  - E2E:
473
513
 
514
+ ## Test-Diagram Mapping
515
+ | Critical flow | Test coverage (ID/command) | Diagram anchor | Gap status |
516
+ |---|---|---|---|
517
+ | | | | covered/gap |
518
+
474
519
  ## Performance Budget
475
520
  | Critical path | Metric | Target | Measurement method |
476
521
  |---|---|---|---|
@@ -530,6 +575,11 @@ ${MARKDOWN_CODE_FENCE}
530
575
  |---|---|---|
531
576
  | | | |
532
577
 
578
+ ## Reference-Grade Contracts
579
+ | Pattern / source | Reusable invariant | Local adaptation | Rejection boundary | Verification signal |
580
+ |---|---|---|---|---|
581
+ | | | | | |
582
+
533
583
  ## Interface Contracts
534
584
  - Standard/Deep add-on when module boundaries or APIs change; omit for compact local changes.
535
585
  | Module | Produces | Consumes |
@@ -558,6 +608,9 @@ ${SEED_SHELF_SECTION}
558
608
 
559
609
  **Decisions made:** 0 | **Unresolved:** 0
560
610
 
611
+ ## Learning Capture Hint
612
+ For meaningful design work, replace the Learnings sentinel with 1-3 JSON learning bullets, for example: \`- {"type":"lesson","trigger":"when design chooses a risky fallback path","action":"record the switch trigger and rollback signal in Spec Handoff","confidence":"medium","domain":"architecture","stage":"design"}\`
613
+
561
614
  ## Learnings
562
615
  - None this stage.
563
616
  `,
@@ -735,7 +788,7 @@ Execution rule: complete and verify each batch before starting the next batch.
735
788
 
736
789
  ## Execution Posture
737
790
  - Posture: sequential | dependency-batched | blocked
738
- - RED/GREEN/REFACTOR checkpoint plan:
791
+ - Vertical-slice RED/GREEN/REFACTOR checkpoint plan:
739
792
  - Incremental commits: yes/no/deferred because
740
793
 
741
794
  ## RED Evidence
@@ -744,7 +797,7 @@ Execution rule: complete and verify each batch before starting the next batch.
744
797
  | S-1 | | | |
745
798
 
746
799
  ## Acceptance Mapping
747
- | Slice | Source item ID | Spec criterion ID |
800
+ | Vertical slice | Source item ID | Spec criterion ID |
748
801
  |---|---|---|
749
802
  | S-1 | SRC-1 | AC-1 |
750
803
 
@@ -793,6 +846,9 @@ Execution rule: complete and verify each batch before starting the next batch.
793
846
  |---|---|---|---|---|
794
847
  | S-1 | | | | |
795
848
 
849
+ ## Learning Capture Hint
850
+ For meaningful TDD work, replace the Learnings sentinel with 1-3 JSON learning bullets, for example: \`- {"type":"pattern","trigger":"when a regression only fails after state rewind","action":"keep the RED fixture and add a cycle-log assertion before GREEN","confidence":"medium","domain":"testing","stage":"tdd"}\`
851
+
796
852
  ## Learnings
797
853
  - None this stage.
798
854
  `,
@@ -853,6 +909,7 @@ Execution rule: complete and verify each batch before starting the next batch.
853
909
 
854
910
  ## Review Readiness Snapshot
855
911
 
912
+ - Victory Detector: pass | fail (Layer 1, Layer 2, security sweep, structured findings, trace evidence, unresolved-critical status)
856
913
  - Completed checks: Layer 1, Layer 2 tags, security sweep, schema validation
857
914
  - Delegation log: \`.cclaw/state/delegation-log.json\` required/completed/waived/pending
858
915
  - Staleness signal: commit at last review pass vs current commit
@@ -893,6 +950,9 @@ Execution rule: complete and verify each batch before starting the next batch.
893
950
  ## Final Verdict
894
951
  - APPROVED | APPROVED_WITH_CONCERNS | BLOCKED
895
952
 
953
+ ## Learning Capture Hint
954
+ For meaningful review work, replace the Learnings sentinel with 1-3 JSON learning bullets, for example: \`- {"type":"lesson","trigger":"when security sweep finds no issues but touches trust boundaries","action":"record NO_SECURITY_IMPACT with inspected surfaces and rationale","confidence":"medium","domain":"security","stage":"review"}\`
955
+
896
956
  ## Learnings
897
957
  - None this stage.
898
958
  `,
@@ -961,6 +1021,7 @@ ${SHIP_FINALIZATION_ENUM_LINES}
961
1021
  - NO_VCS handoff target + artifact path (if FINALIZE_NO_VCS):
962
1022
 
963
1023
  ## Completion Status
1024
+ - Victory Detector: pass | fail (review verdict valid, preflight fresh, rollback ready, one finalization enum selected, execution result present)
964
1025
  - SHIPPED | SHIPPED_WITH_EXCEPTIONS | BLOCKED
965
1026
  - Exceptions (if any):
966
1027
 
@@ -5,6 +5,12 @@ function flowStatePath() {
5
5
  function delegationLogPath() {
6
6
  return `${RUNTIME_ROOT}/state/delegation-log.json`;
7
7
  }
8
+ function delegationEventsPath() {
9
+ return `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
10
+ }
11
+ function subagentsPath() {
12
+ return `${RUNTIME_ROOT}/state/subagents.json`;
13
+ }
8
14
  function artifactsPath() {
9
15
  return `${RUNTIME_ROOT}/artifacts`;
10
16
  }
@@ -30,7 +36,7 @@ Do not modify state in this command. It is a pure read/render operation.
30
36
  - stage marker: passed/current/pending/skipped/stale,
31
37
  - gates summary,
32
38
  - artifact summary,
33
- - delegation branch for current stage with fulfillmentMode labels,
39
+ - delegation branch for current stage with fulfillmentMode, dispatchSurface, proof status, and active tracker labels,
34
40
  6. When \`closeout.shipSubstate !== "idle"\` or \`currentStage === "ship"\`, add
35
41
  a closeout sub-tree:
36
42
  - \`retro:\` line derived from \`closeout.retroDraftedAt\` /
@@ -1,7 +1,9 @@
1
1
  import { type SubagentFallback } from "./harness-adapters.js";
2
2
  import type { FlowStage } from "./types.js";
3
3
  export type DelegationMode = "mandatory" | "proactive";
4
- export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
4
+ export type DelegationStatus = "scheduled" | "launched" | "acknowledged" | "completed" | "failed" | "waived" | "stale";
5
+ export type DelegationDispatchSurface = "claude-task" | "cursor-task" | "opencode-agent" | "codex-agent" | "generic-task" | "role-switch" | "manual";
6
+ export type DelegationEventType = DelegationStatus;
5
7
  /**
6
8
  * How a delegation was actually fulfilled. Advisory — mirrors the harness
7
9
  * `subagentFallback` that was in effect when the entry was recorded.
@@ -53,6 +55,20 @@ export type DelegationEntry = {
53
55
  retryCount?: number;
54
56
  /** Optional references to evidence anchors in artifacts. */
55
57
  evidenceRefs?: string[];
58
+ /** Dispatch proof id from the parent/controller side. */
59
+ dispatchId?: string;
60
+ /** Worker-reported run id or task id returned by the harness. */
61
+ workerRunId?: string;
62
+ /** Concrete runtime surface used to launch the worker. */
63
+ dispatchSurface?: DelegationDispatchSurface;
64
+ /** Path to the generated or canonical agent definition used for dispatch. */
65
+ agentDefinitionPath?: string;
66
+ /** ISO timestamp when the worker was acknowledged by the harness/worker. */
67
+ ackTs?: string;
68
+ /** ISO timestamp when the worker was launched. */
69
+ launchedTs?: string;
70
+ /** ISO timestamp when the worker completed. */
71
+ completedTs?: string;
56
72
  /** Optional skill marker used for role-specific mandatory checks. */
57
73
  skill?: string;
58
74
  /**
@@ -68,6 +84,11 @@ export type DelegationLedger = {
68
84
  runId: string;
69
85
  entries: DelegationEntry[];
70
86
  };
87
+ export type DelegationEvent = DelegationEntry & {
88
+ event: DelegationEventType;
89
+ eventTs: string;
90
+ schemaVersion: 1;
91
+ };
71
92
  /**
72
93
  * Heuristic: does a changed file path strongly imply a trust-boundary
73
94
  * surface? Used by tests and prompt guidance for risk-triggered review.
@@ -79,6 +100,10 @@ export type DelegationLedger = {
79
100
  */
80
101
  export declare function isTrustBoundaryPath(filePath: string): boolean;
81
102
  export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
103
+ export declare function readDelegationEvents(projectRoot: string): Promise<{
104
+ events: DelegationEvent[];
105
+ corruptLines: number[];
106
+ }>;
82
107
  export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
83
108
  /**
84
109
  * Aggregate the fulfillment mode cclaw expects for the active harness set.
@@ -96,6 +121,14 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
96
121
  staleIgnored: string[];
97
122
  /** Delegation rows missing required evidence under a role-switch fallback. */
98
123
  missingEvidence: string[];
124
+ /** Native isolated completion rows that lack dispatch proof. */
125
+ missingDispatchProof: string[];
126
+ /** Legacy inferred isolated completions accepted only as migration warnings. */
127
+ legacyInferredCompletions: string[];
128
+ /** Current-run event log lines that could not be parsed. */
129
+ corruptEventLines: number[];
130
+ /** Current-run scheduled rows with no terminal row sharing the same spanId. */
131
+ staleWorkers: string[];
99
132
  /** Expected fulfillment mode for the active harness set. */
100
133
  expectedMode: DelegationFulfillmentMode;
101
134
  }>;
@@ -9,12 +9,19 @@ 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", "stale"]);
12
13
  function delegationLogPath(projectRoot) {
13
14
  return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json");
14
15
  }
15
16
  function delegationLockPath(projectRoot) {
16
17
  return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
17
18
  }
19
+ function delegationEventsPath(projectRoot) {
20
+ return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-events.jsonl");
21
+ }
22
+ function subagentsStatePath(projectRoot) {
23
+ return path.join(projectRoot, RUNTIME_ROOT, "state", "subagents.json");
24
+ }
18
25
  function createSpanId() {
19
26
  return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
20
27
  }
@@ -130,11 +137,19 @@ function isDelegationEntry(value) {
130
137
  const o = value;
131
138
  const modeOk = o.mode === "mandatory" || o.mode === "proactive";
132
139
  const statusOk = o.status === "scheduled" ||
140
+ o.status === "launched" ||
141
+ o.status === "acknowledged" ||
133
142
  o.status === "completed" ||
134
143
  o.status === "failed" ||
135
- o.status === "waived";
144
+ o.status === "waived" ||
145
+ o.status === "stale";
136
146
  const timestampOk = typeof o.ts === "string" ||
137
147
  typeof o.startTs === "string";
148
+ const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived" || o.status === "stale";
149
+ const lifecycleOk = (o.status !== "scheduled" && o.status !== "launched" && o.status !== "acknowledged") || o.endTs === undefined;
150
+ const terminalLifecycleOk = !terminalStatus ||
151
+ o.endTs === undefined ||
152
+ typeof o.endTs === "string";
138
153
  const retryOk = o.retryCount === undefined ||
139
154
  (typeof o.retryCount === "number" &&
140
155
  Number.isFinite(o.retryCount) &&
@@ -146,6 +161,8 @@ function isDelegationEntry(value) {
146
161
  modeOk &&
147
162
  statusOk &&
148
163
  timestampOk &&
164
+ lifecycleOk &&
165
+ terminalLifecycleOk &&
149
166
  (o.spanId === undefined || typeof o.spanId === "string") &&
150
167
  (o.parentSpanId === undefined || typeof o.parentSpanId === "string") &&
151
168
  (o.startTs === undefined || typeof o.startTs === "string") &&
@@ -160,12 +177,53 @@ function isDelegationEntry(value) {
160
177
  o.fulfillmentMode === "role-switch" ||
161
178
  o.fulfillmentMode === "harness-waiver") &&
162
179
  (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
180
+ (o.dispatchId === undefined || typeof o.dispatchId === "string") &&
181
+ (o.workerRunId === undefined || typeof o.workerRunId === "string") &&
182
+ (o.dispatchSurface === undefined || isDelegationDispatchSurface(o.dispatchSurface)) &&
183
+ (o.agentDefinitionPath === undefined || typeof o.agentDefinitionPath === "string") &&
184
+ (o.ackTs === undefined || typeof o.ackTs === "string") &&
185
+ (o.launchedTs === undefined || typeof o.launchedTs === "string") &&
186
+ (o.completedTs === undefined || typeof o.completedTs === "string") &&
163
187
  (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
164
188
  retryOk &&
165
189
  (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
166
190
  (o.skill === undefined || typeof o.skill === "string") &&
167
191
  (o.schemaVersion === undefined || o.schemaVersion === 1));
168
192
  }
193
+ 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");
201
+ }
202
+ function statusTimestampPatch(entry, ts) {
203
+ const patch = { ...entry };
204
+ if (patch.status === "launched")
205
+ patch.launchedTs = patch.launchedTs ?? ts;
206
+ if (patch.status === "acknowledged")
207
+ patch.ackTs = patch.ackTs ?? ts;
208
+ if (patch.status === "completed")
209
+ patch.completedTs = patch.completedTs ?? patch.endTs ?? ts;
210
+ return patch;
211
+ }
212
+ function eventFromEntry(entry) {
213
+ const eventTs = entry.completedTs ?? entry.ackTs ?? entry.launchedTs ?? entry.endTs ?? entry.startTs ?? entry.ts ?? new Date().toISOString();
214
+ return {
215
+ ...entry,
216
+ event: entry.status,
217
+ eventTs,
218
+ schemaVersion: 1
219
+ };
220
+ }
221
+ function isDelegationEvent(value) {
222
+ if (!isDelegationEntry(value))
223
+ return false;
224
+ const o = value;
225
+ return o.event === o.status && typeof o.eventTs === "string";
226
+ }
169
227
  function parseLedger(raw, runId) {
170
228
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
171
229
  return { runId, entries: [] };
@@ -185,7 +243,11 @@ function parseLedger(raw, runId) {
185
243
  ...item,
186
244
  spanId: item.spanId ?? createSpanId(),
187
245
  startTs: ts,
246
+ endTs: TERMINAL_DELEGATION_STATUSES.has(item.status) ? (item.endTs ?? ts) : undefined,
188
247
  ts,
248
+ launchedTs: item.launchedTs ?? (item.status === "launched" ? ts : undefined),
249
+ ackTs: item.ackTs ?? (item.status === "acknowledged" ? ts : undefined),
250
+ completedTs: item.completedTs ?? (item.status === "completed" ? (item.endTs ?? ts) : undefined),
189
251
  retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
190
252
  ? item.retryCount
191
253
  : 0,
@@ -213,6 +275,57 @@ export async function readDelegationLedger(projectRoot) {
213
275
  return { runId: activeRunId, entries: [] };
214
276
  }
215
277
  }
278
+ export async function readDelegationEvents(projectRoot) {
279
+ const filePath = delegationEventsPath(projectRoot);
280
+ if (!(await exists(filePath))) {
281
+ return { events: [], corruptLines: [] };
282
+ }
283
+ const events = [];
284
+ const corruptLines = [];
285
+ const text = await fs.readFile(filePath, "utf8").catch(() => "");
286
+ const lines = text.split(/\r?\n/gu);
287
+ for (let index = 0; index < lines.length; index += 1) {
288
+ const line = lines[index]?.trim() ?? "";
289
+ if (line.length === 0)
290
+ continue;
291
+ try {
292
+ const parsed = JSON.parse(line);
293
+ if (isDelegationEvent(parsed)) {
294
+ events.push(parsed);
295
+ }
296
+ else {
297
+ corruptLines.push(index + 1);
298
+ }
299
+ }
300
+ catch {
301
+ corruptLines.push(index + 1);
302
+ }
303
+ }
304
+ return { events, corruptLines };
305
+ }
306
+ async function appendDelegationEvent(projectRoot, event) {
307
+ const filePath = delegationEventsPath(projectRoot);
308
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
309
+ await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
310
+ }
311
+ async function writeSubagentTracker(projectRoot, entries) {
312
+ const active = entries
313
+ .filter((entry) => entry.status === "scheduled" || entry.status === "launched" || entry.status === "acknowledged")
314
+ .map((entry) => ({
315
+ spanId: entry.spanId,
316
+ dispatchId: entry.dispatchId,
317
+ workerRunId: entry.workerRunId,
318
+ stage: entry.stage,
319
+ agent: entry.agent,
320
+ status: entry.status,
321
+ dispatchSurface: entry.dispatchSurface,
322
+ agentDefinitionPath: entry.agentDefinitionPath,
323
+ startedAt: entry.startTs,
324
+ launchedAt: entry.launchedTs,
325
+ acknowledgedAt: entry.ackTs
326
+ }));
327
+ await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
328
+ }
216
329
  export async function appendDelegation(projectRoot, entry) {
217
330
  const { activeRunId } = await readFlowState(projectRoot);
218
331
  await withDirectoryLock(delegationLockPath(projectRoot), async () => {
@@ -222,10 +335,19 @@ export async function appendDelegation(projectRoot, entry) {
222
335
  if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
223
336
  throw new Error("waived delegation entries require a non-empty waiverReason");
224
337
  }
225
- const stamped = { ...entry, runId: entry.runId ?? activeRunId };
338
+ const stamped = statusTimestampPatch({ ...entry, runId: entry.runId ?? activeRunId }, startTs);
226
339
  stamped.spanId = entry.spanId ?? createSpanId();
227
340
  stamped.startTs = startTs;
228
341
  stamped.ts = startTs;
342
+ if (TERMINAL_DELEGATION_STATUSES.has(stamped.status) && !stamped.endTs) {
343
+ stamped.endTs = new Date().toISOString();
344
+ }
345
+ if (stamped.status === "completed") {
346
+ stamped.completedTs = stamped.completedTs ?? stamped.endTs ?? new Date().toISOString();
347
+ }
348
+ if (stamped.status === "scheduled") {
349
+ delete stamped.endTs;
350
+ }
229
351
  stamped.schemaVersion = 1;
230
352
  if (stamped.retryCount === undefined ||
231
353
  !Number.isInteger(stamped.retryCount) ||
@@ -247,18 +369,19 @@ export async function appendDelegation(projectRoot, entry) {
247
369
  stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
248
370
  }
249
371
  }
250
- // Idempotency: if a caller (or a retried hook) tries to append a row
251
- // with a spanId that already exists in the ledger, treat it as a no-op
252
- // instead of growing the log with duplicate entries that subsequent
253
- // delegation checks would mis-count.
254
- if (prior.entries.some((existing) => existing.spanId === stamped.spanId)) {
372
+ // Idempotency: a retried hook may replay the same lifecycle row. Allow a
373
+ // terminal row to close an existing scheduled span, but drop exact same
374
+ // span/status duplicates so checks do not mis-count repeated writes.
375
+ if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
255
376
  return;
256
377
  }
378
+ await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
257
379
  const ledger = {
258
380
  runId: activeRunId,
259
381
  entries: [...prior.entries, stamped]
260
382
  };
261
383
  await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
384
+ await writeSubagentTracker(projectRoot, ledger.entries);
262
385
  });
263
386
  }
264
387
  /**
@@ -285,6 +408,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
285
408
  const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
286
409
  const { activeRunId } = flowState;
287
410
  const ledger = await readDelegationLedger(projectRoot);
411
+ const events = await readDelegationEvents(projectRoot);
288
412
  const forStage = ledger.entries.filter((e) => e.stage === stage);
289
413
  const forRun = forStage.filter((e) => e.runId === activeRunId);
290
414
  const staleIgnored = forStage
@@ -293,6 +417,14 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
293
417
  const missing = [];
294
418
  const waived = [];
295
419
  const missingEvidence = [];
420
+ const missingDispatchProof = [];
421
+ const legacyInferredCompletions = [];
422
+ const terminalSpanIds = new Set(forRun
423
+ .filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
424
+ .map((entry) => entry.spanId));
425
+ const staleWorkers = forRun
426
+ .filter((entry) => entry.status === "scheduled" && entry.spanId && !terminalSpanIds.has(entry.spanId))
427
+ .map((entry) => `${entry.agent}(spanId=${entry.spanId})`);
296
428
  const config = await readConfig(projectRoot).catch(() => null);
297
429
  const harnesses = config?.harnesses ?? [];
298
430
  const configuredFallbacks = harnesses.map((h) => HARNESS_ADAPTERS[h].capabilities.subagentFallback);
@@ -322,13 +454,41 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
322
454
  !completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
323
455
  missingEvidence.push(agent);
324
456
  }
457
+ for (const row of completedRows) {
458
+ const mode = row.fulfillmentMode ?? "isolated";
459
+ if (mode === "isolated") {
460
+ const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
461
+ event.stage === stage &&
462
+ event.agent === agent &&
463
+ event.spanId === row.spanId);
464
+ const dispatchId = row.dispatchId ?? row.workerRunId ?? spanEvents.find((event) => event.dispatchId || event.workerRunId)?.dispatchId ?? spanEvents.find((event) => event.workerRunId)?.workerRunId;
465
+ const dispatchSurface = row.dispatchSurface ?? spanEvents.find((event) => event.dispatchSurface)?.dispatchSurface;
466
+ const agentDefinitionPath = row.agentDefinitionPath ?? spanEvents.find((event) => event.agentDefinitionPath)?.agentDefinitionPath;
467
+ const hasAck = Boolean(row.ackTs || spanEvents.some((event) => event.event === "acknowledged" && event.ackTs));
468
+ const hasCompleted = Boolean(row.completedTs || spanEvents.some((event) => event.event === "completed" && event.completedTs));
469
+ const hasDispatchProof = Boolean(row.spanId && dispatchId && dispatchSurface && agentDefinitionPath && hasAck && hasCompleted);
470
+ if (!hasDispatchProof) {
471
+ 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"));
472
+ if (proofEraSignal) {
473
+ missingDispatchProof.push(agent);
474
+ }
475
+ else {
476
+ legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
477
+ }
478
+ }
479
+ }
480
+ }
325
481
  }
326
482
  return {
327
- satisfied: missing.length === 0 && missingEvidence.length === 0,
483
+ satisfied: missing.length === 0 && missingEvidence.length === 0 && missingDispatchProof.length === 0 && staleWorkers.length === 0 && events.corruptLines.length === 0,
328
484
  missing,
329
485
  waived,
330
486
  staleIgnored,
331
487
  missingEvidence,
488
+ missingDispatchProof,
489
+ legacyInferredCompletions,
490
+ corruptEventLines: events.corruptLines,
491
+ staleWorkers,
332
492
  expectedMode
333
493
  };
334
494
  }
@@ -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: {