cclaw-cli 0.51.19 → 0.51.22

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 (44) hide show
  1. package/dist/artifact-linter.js +89 -6
  2. package/dist/config.d.ts +8 -1
  3. package/dist/config.js +9 -6
  4. package/dist/content/examples.js +1 -0
  5. package/dist/content/hook-events.js +1 -5
  6. package/dist/content/hook-manifest.d.ts +2 -4
  7. package/dist/content/hook-manifest.js +4 -3
  8. package/dist/content/meta-skill.js +7 -9
  9. package/dist/content/next-command.js +2 -2
  10. package/dist/content/node-hooks.js +15 -16
  11. package/dist/content/observe.js +2 -4
  12. package/dist/content/opencode-plugin.js +5 -6
  13. package/dist/content/review-loop.js +15 -5
  14. package/dist/content/review-prompts.js +1 -1
  15. package/dist/content/skills.js +3 -2
  16. package/dist/content/stage-schema.d.ts +0 -1
  17. package/dist/content/stage-schema.js +2 -5
  18. package/dist/content/stages/brainstorm.js +3 -3
  19. package/dist/content/stages/design.js +18 -17
  20. package/dist/content/stages/plan.js +2 -1
  21. package/dist/content/stages/review.js +10 -10
  22. package/dist/content/stages/scope.js +13 -13
  23. package/dist/content/stages/spec.js +7 -5
  24. package/dist/content/stages/tdd.js +2 -2
  25. package/dist/content/start-command.d.ts +4 -3
  26. package/dist/content/start-command.js +21 -17
  27. package/dist/content/templates.d.ts +1 -1
  28. package/dist/content/templates.js +49 -29
  29. package/dist/content/view-command.js +3 -1
  30. package/dist/delegation.d.ts +0 -1
  31. package/dist/delegation.js +29 -11
  32. package/dist/doctor.js +148 -24
  33. package/dist/gate-evidence.js +19 -7
  34. package/dist/harness-adapters.js +1 -5
  35. package/dist/install.js +111 -24
  36. package/dist/internal/advance-stage.js +90 -11
  37. package/dist/knowledge-store.d.ts +4 -1
  38. package/dist/knowledge-store.js +24 -14
  39. package/dist/retro-gate.d.ts +1 -0
  40. package/dist/retro-gate.js +9 -9
  41. package/dist/run-archive.js +19 -1
  42. package/dist/run-persistence.js +12 -5
  43. package/dist/tdd-cycle.js +6 -3
  44. package/package.json +1 -1
@@ -143,6 +143,7 @@ ${SEED_SHELF_SECTION}
143
143
  | Reversibility cost? | | |
144
144
 
145
145
  ## Dream State Mapping
146
+ - Deep/optional only; omit for compact scope.
146
147
  - CURRENT STATE:
147
148
  - THIS PLAN:
148
149
  - 12-MONTH IDEAL:
@@ -156,6 +157,7 @@ ${SEED_SHELF_SECTION}
156
157
  | C (optional) | | | | | | |
157
158
 
158
159
  ## Temporal Interrogation
160
+ - Deep/optional only; omit for compact scope.
159
161
  | Time slice | Likely decision pressure | Lock now or defer? | Reason |
160
162
  |---|---|---|---|
161
163
  | HOUR 1 (foundations) | | | |
@@ -170,12 +172,11 @@ ${SEED_SHELF_SECTION}
170
172
  - [ ] SCOPE REDUCTION — strip to the smallest useful wedge when risk/blast radius is too high.
171
173
 
172
174
  ## Mode-Specific Analysis
173
- - **Selected mode:**
174
- - **Analysis:**
175
- - (SCOPE EXPANSION: 10-star opportunities, delight features)
176
- - (SELECTIVE EXPANSION: hold-scope baseline, cherry-picked expansions)
177
- - (HOLD SCOPE: approved slice with maximum rigor)
178
- - (SCOPE REDUCTION: ruthless cuts, follow-up split)
175
+ | Selected mode | Rationale | Depth |
176
+ |---|---|---|
177
+ | | | default / deep |
178
+
179
+ > Default path: one selected-mode row plus rationale. Deep/high-risk scope may expand below with mode-specific analysis.
179
180
 
180
181
  ## Requirements (stable IDs)
181
182
  | ID | Requirement (observable outcome) | Priority | Source (origin doc / prompt line) |
@@ -224,7 +225,7 @@ ${SEED_SHELF_SECTION}
224
225
  |---|---|---|---|---|
225
226
  | F-1 | premise_fit | | accept/reject/defer | |
226
227
 
227
- ## Spec Review Loop
228
+ ## Scope Outside Voice Loop
228
229
  | Iteration | Quality Score | Findings | Stop decision |
229
230
  |---|---|---|---|
230
231
  | 1 | 0.00 | 0 | continue/stop |
@@ -288,8 +289,13 @@ ${SEED_SHELF_SECTION}
288
289
 
289
290
  # Design Artifact
290
291
 
292
+ ## Compact-First Scaffold
293
+ - Default to the compact design spine unless risk requires Standard/Deep add-ons.
294
+ - Compact required spine: Codebase Investigation, Architecture Boundaries, Architecture Diagram, Data Flow, Failure Mode Table, Test Strategy, and Completion Dashboard.
295
+ - Mark optional Standard/Deep sections as \`Omitted - compact design\` when they do not apply; do not expand the scaffold just to fill empty tables.
296
+
291
297
  ## Upstream Handoff
292
- - Source artifacts: \`02-scope-<slug>.md\`, \`02a-research.md\` when present
298
+ - Source artifacts: \`02-scope-<slug>.md\`, \`02a-research.md\` only when present for deep/high-risk research
293
299
  - Decisions carried forward:
294
300
  - Constraints carried forward:
295
301
  - Open questions:
@@ -308,12 +314,11 @@ ${SEED_SHELF_SECTION}
308
314
  | Layer 3 | | |
309
315
 
310
316
  ## Research Fleet Synthesis
311
- | Lens | Key findings | Design impact | Evidence |
317
+ | Lens actually run | Key findings | Design impact | Evidence |
312
318
  |---|---|---|---|
313
- | stack-researcher | | | |
314
- | features-researcher | | | |
315
- | architecture-researcher | | | |
316
- | pitfalls-researcher | | | |
319
+ | compact inline synthesis | | | |
320
+
321
+ > Default path: compact inline synthesis here. Deep/high-risk work may also write \`.cclaw/artifacts/02a-research.md\`.
317
322
 
318
323
  ## Architecture Boundaries
319
324
  | Component | Responsibility | Requirement Refs (R#) | Decision Refs (LD#hash) | Owner |
@@ -329,12 +334,14 @@ ${MARKDOWN_CODE_FENCE}
329
334
  ${MARKDOWN_CODE_FENCE}
330
335
 
331
336
  ## Data-Flow Shadow Paths
337
+ - Standard/Deep add-on; omit when compact design does not need a shadow path.
332
338
  <!-- diagram: data-flow-shadow-paths -->
333
339
  | Path | Trigger | Fallback/Degrade behavior |
334
340
  |---|---|---|
335
341
  | | | |
336
342
 
337
343
  ## Error Flow Diagram
344
+ - Standard/Deep add-on; omit when the Failure Mode Table is sufficient.
338
345
 
339
346
  <!-- diagram: error-flow -->
340
347
 
@@ -343,6 +350,7 @@ ${MARKDOWN_CODE_FENCE}
343
350
  ${MARKDOWN_CODE_FENCE}
344
351
 
345
352
  ## State Machine Diagram
353
+ - Deep add-on; omit for compact design.
346
354
 
347
355
  <!-- diagram: state-machine -->
348
356
 
@@ -351,6 +359,7 @@ ${MARKDOWN_CODE_FENCE}
351
359
  ${MARKDOWN_CODE_FENCE}
352
360
 
353
361
  ## Rollback Flowchart
362
+ - Deep add-on; omit for compact design.
354
363
 
355
364
  <!-- diagram: rollback-flowchart -->
356
365
 
@@ -359,6 +368,7 @@ ${MARKDOWN_CODE_FENCE}
359
368
  ${MARKDOWN_CODE_FENCE}
360
369
 
361
370
  ## Deployment Sequence Diagram
371
+ - Deep add-on; omit for compact design.
362
372
 
363
373
  <!-- diagram: deployment-sequence -->
364
374
 
@@ -426,7 +436,7 @@ ${MARKDOWN_CODE_FENCE}
426
436
  |---|---|---|---|---|
427
437
  | F-1 | architecture_fit | | accept/reject/defer | |
428
438
 
429
- ## Spec Review Loop
439
+ ## Design Outside Voice Loop
430
440
  | Iteration | Quality Score | Findings | Stop decision |
431
441
  |---|---|---|---|
432
442
  | 1 | 0.00 | 0 | continue/stop |
@@ -439,6 +449,7 @@ ${MARKDOWN_CODE_FENCE}
439
449
  -
440
450
 
441
451
  ## Parallelization Strategy
452
+ - Standard/Deep add-on when multi-module; omit for compact sequential work.
442
453
  - Parallel lanes:
443
454
  - Conflict risks:
444
455
 
@@ -448,11 +459,13 @@ ${MARKDOWN_CODE_FENCE}
448
459
  | | | |
449
460
 
450
461
  ## Interface Contracts
462
+ - Standard/Deep add-on when module boundaries or APIs change; omit for compact local changes.
451
463
  | Module | Produces | Consumes |
452
464
  |---|---|---|
453
465
  | | | |
454
466
 
455
467
  ## Unresolved Decisions
468
+ - Standard/Deep add-on; use \`None\` for compact design with no unresolved decisions.
456
469
  | Decision | Missing info | Owner | Default |
457
470
  |---|---|---|---|
458
471
  | | | | |
@@ -481,7 +494,7 @@ ${SEED_SHELF_SECTION}
481
494
  # Specification Artifact
482
495
 
483
496
  ## Upstream Handoff
484
- - Source artifacts: \`02-scope-<slug>.md\`, \`03-design-<slug>.md\`
497
+ - Source artifacts: standard uses \`02-scope-<slug>.md\` + \`03-design-<slug>.md\`; medium uses \`01-brainstorm-<slug>.md\` when present; quick uses \`00-idea.md\` plus reproduction context.
485
498
  - Decisions carried forward:
486
499
  - Constraints carried forward:
487
500
  - Open questions:
@@ -492,9 +505,14 @@ ${SEED_SHELF_SECTION}
492
505
  |---|---|---|---|
493
506
  | AC-1 | R1 | | |
494
507
 
495
- > Every AC must reference at least one \`R#\` from \`02-scope.md\`. ACs are
496
- > stable (never renumber): dropped ACs stay with Priority \`DROPPED\`; new
497
- > ones append with the next free \`AC-#\`.
508
+ > Standard ACs reference at least one \`R#\` from \`02-scope.md\`. Quick-track ACs may instead put \`Quick Reproduction Contract\` / bug-slice refs in the Requirement Ref column and \`N/A\` for Design Decision Ref. ACs are stable (never renumber): dropped ACs stay with Priority \`DROPPED\`; new ones append with the next free \`AC-#\`.
509
+
510
+ ## Quick Reproduction Contract
511
+ > Required for quick bug-fix specs; use \`N/A\` for non-bugfix or standard/medium tracks. TDD turns this contract into the RED reproduction test.
512
+
513
+ | Bug slice | Symptom | Repro steps | Expected RED test behavior | Linked acceptance criterion |
514
+ |---|---|---|---|---|
515
+ | QS-1 | | | | AC-1 |
498
516
 
499
517
  ## Edge Cases
500
518
  | Criterion ID | Boundary case | Error case |
@@ -510,7 +528,7 @@ ${SEED_SHELF_SECTION}
510
528
  |---|---|---|---|
511
529
  | | | | accepted/rejected/open |
512
530
 
513
- ## Testability Map
531
+ ## Acceptance Mapping
514
532
  | Criterion ID | Verification approach | Command/manual steps |
515
533
  |---|---|---|
516
534
  | AC-1 | | |
@@ -627,7 +645,7 @@ Execution rule: complete and verify each batch before starting the next batch.
627
645
  # TDD Artifact
628
646
 
629
647
  ## Upstream Handoff
630
- - Source artifacts: \`04-spec.md\`, \`05-plan.md\`
648
+ - Source artifacts: \`04-spec.md\`; \`05-plan.md\` when present. Quick track uses spec acceptance items / bug reproduction slices instead of nonexistent plan tasks.
631
649
  - Decisions carried forward:
632
650
  - Constraints carried forward:
633
651
  - Open questions:
@@ -654,9 +672,11 @@ Execution rule: complete and verify each batch before starting the next batch.
654
672
  | S-1 | | | |
655
673
 
656
674
  ## Acceptance Mapping
657
- | Slice | Plan task ID | Spec criterion ID |
675
+ | Slice | Plan task ID or quick source | Spec criterion ID |
658
676
  |---|---|---|
659
- | S-1 | T-1 | AC-1 |
677
+ | S-1 | T-1 / QS-1 | AC-1 |
678
+
679
+ > On quick track, map to the \`Quick Reproduction Contract\` bug slice or spec acceptance item. Do not invent a plan task just to satisfy this table.
660
680
 
661
681
  ## Failure Analysis
662
682
  | Slice | Expected missing behavior | Actual failure reason |
@@ -709,7 +729,7 @@ Execution rule: complete and verify each batch before starting the next batch.
709
729
  # Review Artifact
710
730
 
711
731
  ## Upstream Handoff
712
- - Source artifacts: \`04-spec.md\`, \`05-plan.md\`, \`06-tdd.md\`
732
+ - Source artifacts: \`04-spec.md\`, \`06-tdd.md\`; \`05-plan.md\` only when present. Quick track reviews spec acceptance items / bug reproduction slices without requiring plan-task coverage.
713
733
  - Decisions carried forward:
714
734
  - Constraints carried forward:
715
735
  - Open questions:
@@ -723,7 +743,7 @@ Execution rule: complete and verify each batch before starting the next batch.
723
743
  ## Layer 2 Findings
724
744
  | ID | Severity | Category | Description | Status |
725
745
  |---|---|---|---|---|
726
- | R-1 | Critical/Important/Suggestion | correctness/security/performance/architecture | | open/resolved |
746
+ | R-1 | Critical/Important/Suggestion | correctness/security/performance/architecture/external-safety | | open/resolved |
727
747
  - NO_CHANGE_ATTESTATION: <required when Category=security has no entries; explain why no security-relevant changes were detected>
728
748
 
729
749
  ## Incoming Feedback Queue
@@ -746,15 +766,15 @@ Execution rule: complete and verify each batch before starting the next batch.
746
766
 
747
767
  ## Completeness Snapshot
748
768
  - AC coverage: <N>/<M> (<percent>%)
749
- - Task coverage (tasks backed by ≥1 test slice): <N>/<M>
750
- - Slice coverage (slices linked to ≥1 AC): <N>/<M>
769
+ - Task coverage (tasks backed by ≥1 test slice): <N>/<M> or \`N/A - quick track has no plan artifact\`
770
+ - Slice coverage (slices linked to ≥1 AC or bug reproduction slice): <N>/<M>
751
771
  - Adversarial review: not triggered | pass | fail
752
772
  - Overall: complete | concerns | blocked
753
773
 
754
774
  ## Trace Matrix Check
755
- - Command: \`cclaw internal trace-matrix\`
775
+ - Command: \`cclaw internal trace-matrix\` when plan artifacts exist or the active track enforces it; quick track may record direct AC/reproduction-slice coverage instead.
756
776
  - Orphaned criteria: 0
757
- - Orphaned tasks: 0
777
+ - Orphaned tasks: 0 or \`N/A - quick track\`
758
778
  - Orphaned tests: 0
759
779
  - Evidence ref:
760
780
 
@@ -955,7 +975,7 @@ Track-specific skips are allowed only when \`flow-state.track\` + \`skippedStage
955
975
  | Class | Route |
956
976
  |---|---|
957
977
  | non-trivial software work | \`/cc <idea>\` |
958
- | trivial software fix | \`/cc <idea>\` (quick or medium track) |
978
+ | trivial software fix | \`/cc <idea>\` (quick track) |
959
979
  | bugfix with repro | \`/cc <idea>\` and enforce RED-first in tdd |
960
980
  | pure question / non-software | direct answer (no stage flow) |
961
981
 
@@ -37,9 +37,11 @@ ${conversationLanguagePolicyMarkdown()}
37
37
  For machine orchestration, emit one JSON envelope:
38
38
 
39
39
  \`\`\`json
40
- {"version":"1","kind":"stage-output","stage":"non-flow","payload":{"command":"/cc-view","subcommand":"status","summary":"<short>"},"emittedAt":"<ISO-8601>"}
40
+ {"version":"1","kind":"stage-output","stage":"non-flow","payload":{"command":"/cc-view <status|tree|diff>","subcommand":"<status|tree|diff>","summary":"<short>"},"emittedAt":"<ISO-8601>"}
41
41
  \`\`\`
42
42
 
43
+ Use the parsed/defaulted subcommand in both \`payload.command\` and \`payload.subcommand\`; do not collapse \`tree\` or \`diff\` responses to \`status\`.
44
+
43
45
  Validate envelopes with:
44
46
  \`cclaw internal envelope-validate --stdin\`
45
47
 
@@ -93,7 +93,6 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
93
93
  satisfied: boolean;
94
94
  missing: string[];
95
95
  waived: string[];
96
- autoWaived: string[];
97
96
  staleIgnored: string[];
98
97
  /** Delegation rows missing required evidence under a role-switch fallback. */
99
98
  missingEvidence: string[];
@@ -103,6 +103,9 @@ async function detectReviewTriggers(projectRoot) {
103
103
  return empty;
104
104
  }
105
105
  }
106
+ function hasValidWaiverReason(value) {
107
+ return typeof value === "string" && value.trim().length > 0;
108
+ }
106
109
  function isDelegationTokenUsage(value) {
107
110
  if (!value || typeof value !== "object" || Array.isArray(value))
108
111
  return false;
@@ -130,6 +133,7 @@ function isDelegationEntry(value) {
130
133
  Number.isFinite(o.retryCount) &&
131
134
  Number.isInteger(o.retryCount) &&
132
135
  o.retryCount >= 0);
136
+ const waiverOk = o.status !== "waived" || hasValidWaiverReason(o.waiverReason);
133
137
  return (typeof o.stage === "string" &&
134
138
  typeof o.agent === "string" &&
135
139
  modeOk &&
@@ -141,6 +145,7 @@ function isDelegationEntry(value) {
141
145
  (o.endTs === undefined || typeof o.endTs === "string") &&
142
146
  (o.taskId === undefined || typeof o.taskId === "string") &&
143
147
  (o.waiverReason === undefined || typeof o.waiverReason === "string") &&
148
+ waiverOk &&
144
149
  (o.runId === undefined || typeof o.runId === "string") &&
145
150
  (o.fulfillmentMode === undefined ||
146
151
  o.fulfillmentMode === "isolated" ||
@@ -165,8 +170,10 @@ function parseLedger(raw, runId) {
165
170
  for (const item of entriesRaw) {
166
171
  if (isDelegationEntry(item)) {
167
172
  const ts = item.startTs ?? item.ts ?? new Date().toISOString();
168
- const inferredFulfillmentMode = item.fulfillmentMode
169
- ?? (item.status === "completed" ? "isolated" : undefined);
173
+ const isLegacyCompletion = item.fulfillmentMode === undefined &&
174
+ item.schemaVersion === undefined &&
175
+ item.status === "completed";
176
+ const inferredFulfillmentMode = item.fulfillmentMode ?? (isLegacyCompletion ? "isolated" : undefined);
170
177
  entries.push({
171
178
  ...item,
172
179
  spanId: item.spanId ?? createSpanId(),
@@ -205,6 +212,9 @@ export async function appendDelegation(projectRoot, entry) {
205
212
  const filePath = delegationLogPath(projectRoot);
206
213
  const prior = await readDelegationLedger(projectRoot);
207
214
  const startTs = entry.startTs ?? entry.ts ?? new Date().toISOString();
215
+ if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
216
+ throw new Error("waived delegation entries require a non-empty waiverReason");
217
+ }
208
218
  const stamped = { ...entry, runId: entry.runId ?? activeRunId };
209
219
  stamped.spanId = entry.spanId ?? createSpanId();
210
220
  stamped.startTs = startTs;
@@ -219,10 +229,19 @@ export async function appendDelegation(projectRoot, entry) {
219
229
  stamped.evidenceRefs = [];
220
230
  }
221
231
  if (stamped.status === "completed" && stamped.fulfillmentMode === undefined) {
222
- const config = await readConfig(projectRoot).catch(() => null);
223
- const harnesses = config?.harnesses ?? [];
224
- const fallbacks = harnesses.map((h) => HARNESS_ADAPTERS[h].capabilities.subagentFallback);
225
- stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
232
+ const activeFallback = process.env.CCLAW_ACTIVE_HARNESS
233
+ ? HARNESS_ADAPTERS[process.env.CCLAW_ACTIVE_HARNESS]
234
+ ?.capabilities.subagentFallback
235
+ : undefined;
236
+ if (activeFallback) {
237
+ stamped.fulfillmentMode = expectedFulfillmentMode([activeFallback]);
238
+ }
239
+ else {
240
+ const config = await readConfig(projectRoot).catch(() => null);
241
+ const harnesses = config?.harnesses ?? [];
242
+ const fallbacks = harnesses.map((h) => HARNESS_ADAPTERS[h].capabilities.subagentFallback);
243
+ stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
244
+ }
226
245
  }
227
246
  // Idempotency: if a caller (or a retried hook) tries to append a row
228
247
  // with a spanId that already exists in the ledger, treat it as a no-op
@@ -256,10 +275,11 @@ export function expectedFulfillmentMode(fallbacks) {
256
275
  return "harness-waiver";
257
276
  }
258
277
  export async function checkMandatoryDelegations(projectRoot, stage, options = {}) {
259
- const mandatory = stageSchema(stage).mandatoryDelegations;
260
- const { activeRunId } = await readFlowState(projectRoot, {
278
+ const flowState = await readFlowState(projectRoot, {
261
279
  repairFeatureSystem: options.repairFeatureSystem
262
280
  });
281
+ const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
282
+ const { activeRunId } = flowState;
263
283
  const ledger = await readDelegationLedger(projectRoot);
264
284
  const forStage = ledger.entries.filter((e) => e.stage === stage);
265
285
  const forRun = forStage.filter((e) => e.runId === activeRunId);
@@ -268,7 +288,6 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
268
288
  .map((e) => `${e.agent}(runId=${e.runId ?? "unknown"})`);
269
289
  const missing = [];
270
290
  const waived = [];
271
- const autoWaived = [];
272
291
  const missingEvidence = [];
273
292
  const config = await readConfig(projectRoot).catch(() => null);
274
293
  const harnesses = config?.harnesses ?? [];
@@ -277,7 +296,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
277
296
  for (const agent of mandatory) {
278
297
  const rows = forRun.filter((e) => e.agent === agent);
279
298
  const completedRows = rows.filter((e) => e.status === "completed");
280
- const waivedRows = rows.filter((e) => e.status === "waived");
299
+ const waivedRows = rows.filter((e) => e.status === "waived" && e.mode === "mandatory");
281
300
  const hasCompleted = completedRows.length >= 1;
282
301
  const hasWaived = waivedRows.length > 0;
283
302
  const ok = hasWaived || hasCompleted;
@@ -301,7 +320,6 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
301
320
  satisfied: missing.length === 0 && missingEvidence.length === 0,
302
321
  missing,
303
322
  waived,
304
- autoWaived,
305
323
  staleIgnored,
306
324
  missingEvidence,
307
325
  expectedMode
package/dist/doctor.js CHANGED
@@ -25,6 +25,7 @@ import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_
25
25
  import { validateHookDocument } from "./hook-schema.js";
26
26
  import { validateKnowledgeEntry } from "./knowledge-store.js";
27
27
  import { readSeedShelf } from "./content/seed-shelf.js";
28
+ import { evaluateRetroGate } from "./retro-gate.js";
28
29
  const execFileAsync = promisify(execFile);
29
30
  async function isGitRepo(projectRoot) {
30
31
  try {
@@ -149,18 +150,30 @@ function knowledgeRoutingSurfaceIsDiscoverable(content) {
149
150
  return ["trigger", "action", "origin_run"].every((term) => normalized.includes(term));
150
151
  }
151
152
  async function commandAvailable(command) {
153
+ const version = await commandVersion(command);
154
+ return version.available;
155
+ }
156
+ async function commandVersion(command, args = ["--version"]) {
152
157
  try {
153
158
  if (process.platform === "win32") {
154
159
  await execFileAsync("where", [command]);
155
- return true;
156
160
  }
157
- await execFileAsync(command, ["--version"]);
158
- return true;
161
+ const { stdout, stderr } = await execFileAsync(command, args);
162
+ return { available: true, output: `${stdout}${stderr}`.trim() };
159
163
  }
160
164
  catch {
161
- return false;
165
+ return { available: false, output: "" };
162
166
  }
163
167
  }
168
+ function parseNodeMajor(versionOutput) {
169
+ const match = /v?(\d+)\./u.exec(versionOutput);
170
+ if (!match)
171
+ return null;
172
+ return Number(match[1]);
173
+ }
174
+ function gitVersionLooksUsable(versionOutput) {
175
+ return /git version \d+\.\d+/iu.test(versionOutput);
176
+ }
164
177
  function stripJsonCommentsOutsideStrings(input) {
165
178
  let out = "";
166
179
  let i = 0;
@@ -308,6 +321,87 @@ async function opencodeRegistrationCheck(projectRoot) {
308
321
  }
309
322
  return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
310
323
  }
324
+ async function initRecoveryCheck(projectRoot) {
325
+ const sentinelPath = path.join(projectRoot, RUNTIME_ROOT, "state", ".init-in-progress");
326
+ if (!(await exists(sentinelPath))) {
327
+ return { ok: true, details: "no partial init/sync sentinel found" };
328
+ }
329
+ let summary = `${RUNTIME_ROOT}/state/.init-in-progress sentinel present`;
330
+ try {
331
+ const parsed = JSON.parse(await fs.readFile(sentinelPath, "utf8"));
332
+ const operation = typeof parsed.operation === "string" ? parsed.operation : "unknown";
333
+ const startedAt = typeof parsed.startedAt === "string" ? parsed.startedAt : "unknown";
334
+ summary = `${summary} (operation=${operation}, startedAt=${startedAt})`;
335
+ }
336
+ catch {
337
+ summary = `${summary} (unreadable sentinel payload)`;
338
+ }
339
+ return {
340
+ ok: false,
341
+ details: `${summary}. Fix: inspect generated runtime files, then rerun cclaw sync or remove the sentinel only after confirming the runtime is complete.`
342
+ };
343
+ }
344
+ async function archiveIntegrityCheck(projectRoot) {
345
+ const runsDir = path.join(projectRoot, RUNTIME_ROOT, "runs");
346
+ if (!(await exists(runsDir))) {
347
+ return { ok: true, details: `${RUNTIME_ROOT}/runs is absent; no archives to inspect yet` };
348
+ }
349
+ let entries;
350
+ try {
351
+ entries = await fs.readdir(runsDir, { withFileTypes: true });
352
+ }
353
+ catch (error) {
354
+ const reason = error instanceof Error ? error.message : String(error);
355
+ return { ok: false, details: `unable to inspect ${RUNTIME_ROOT}/runs (${reason})` };
356
+ }
357
+ const problems = [];
358
+ for (const entry of entries) {
359
+ if (!entry.isDirectory())
360
+ continue;
361
+ const runId = entry.name;
362
+ const runPath = path.join(runsDir, runId);
363
+ const relRunPath = `${RUNTIME_ROOT}/runs/${runId}`;
364
+ if (await exists(path.join(runPath, ".archive-in-progress"))) {
365
+ problems.push(`${relRunPath}/.archive-in-progress sentinel present`);
366
+ }
367
+ const manifestPath = path.join(runPath, "archive-manifest.json");
368
+ if (!(await exists(manifestPath))) {
369
+ problems.push(`${relRunPath} missing archive-manifest.json`);
370
+ continue;
371
+ }
372
+ let manifest;
373
+ try {
374
+ manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
375
+ }
376
+ catch (error) {
377
+ const reason = error instanceof Error ? error.message : String(error);
378
+ problems.push(`${relRunPath}/archive-manifest.json unreadable (${reason})`);
379
+ continue;
380
+ }
381
+ const stateFiles = Array.isArray(manifest.snapshottedStateFiles)
382
+ ? manifest.snapshottedStateFiles.filter((value) => typeof value === "string")
383
+ : [];
384
+ const stateDir = path.join(runPath, "state");
385
+ if (stateFiles.length > 0 && !(await exists(stateDir))) {
386
+ problems.push(`${relRunPath} manifest lists state snapshot files but state/ is missing`);
387
+ continue;
388
+ }
389
+ for (const stateFile of stateFiles) {
390
+ if (stateFile.endsWith("/"))
391
+ continue;
392
+ if (!(await exists(path.join(stateDir, stateFile)))) {
393
+ problems.push(`${relRunPath}/state missing ${stateFile} listed in manifest`);
394
+ }
395
+ }
396
+ }
397
+ if (problems.length === 0) {
398
+ return { ok: true, details: "no partial archive sentinels or incomplete archive snapshots found" };
399
+ }
400
+ return {
401
+ ok: false,
402
+ details: `${problems.join("; ")}. Fix: inspect the archive directory, retry archive if the active run was restored, or recover/rollback artifacts and state from the snapshot before removing the sentinel.`
403
+ };
404
+ }
311
405
  async function opencodePluginRuntimeShapeCheck(projectRoot) {
312
406
  const pluginPath = path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs");
313
407
  if (!(await exists(pluginPath))) {
@@ -981,11 +1075,36 @@ export async function doctorChecks(projectRoot, options = {}) {
981
1075
  details: registration.details
982
1076
  });
983
1077
  }
984
- const hasNode = await commandAvailable("node");
1078
+ const nodeVersion = await commandVersion("node");
1079
+ const nodeMajor = parseNodeMajor(nodeVersion.output);
985
1080
  checks.push({
986
1081
  name: "capability:required:node",
987
- ok: hasNode,
988
- details: "node is required for cclaw runtime scripts and CLI wiring"
1082
+ ok: nodeVersion.available,
1083
+ details: nodeVersion.available
1084
+ ? `node binary available (${nodeVersion.output || "version unknown"})`
1085
+ : "node is required for cclaw runtime scripts and CLI wiring"
1086
+ });
1087
+ checks.push({
1088
+ name: "capability:required:node_version",
1089
+ ok: nodeVersion.available && nodeMajor !== null && nodeMajor >= 20,
1090
+ details: nodeVersion.available
1091
+ ? `node >=20 required; detected ${nodeVersion.output || "unknown version"}`
1092
+ : "node version check skipped because node binary is unavailable"
1093
+ });
1094
+ const gitVersion = await commandVersion("git");
1095
+ checks.push({
1096
+ name: "capability:required:git",
1097
+ ok: gitVersion.available,
1098
+ details: gitVersion.available
1099
+ ? `git binary available (${gitVersion.output || "version unknown"})`
1100
+ : "git is required for repository detection, hook setup, and doctor checks"
1101
+ });
1102
+ checks.push({
1103
+ name: "capability:required:git_version",
1104
+ ok: gitVersion.available && gitVersionLooksUsable(gitVersion.output),
1105
+ details: gitVersion.available
1106
+ ? `git version output: ${gitVersion.output || "unknown version"}`
1107
+ : "git version check skipped because git binary is unavailable"
989
1108
  });
990
1109
  const windowsHookConfigCandidates = [
991
1110
  path.join(projectRoot, ".claude/hooks/hooks.json"),
@@ -1083,12 +1202,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1083
1202
  const key = `${trigger} => ${action}`;
1084
1203
  triggerActionCounts.set(key, (triggerActionCounts.get(key) ?? 0) + 1);
1085
1204
  }
1086
- const missing = requiredV2Fields.some((field) => {
1087
- if (field === "origin_run" && Object.prototype.hasOwnProperty.call(parsed, "origin_feature")) {
1088
- return false;
1089
- }
1090
- return !Object.prototype.hasOwnProperty.call(parsed, field);
1091
- });
1205
+ const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
1092
1206
  if (missing) {
1093
1207
  missingSchemaV2Fields += 1;
1094
1208
  }
@@ -1388,17 +1502,17 @@ export async function doctorChecks(projectRoot, options = {}) {
1388
1502
  ? "no stale stages pending acknowledgement"
1389
1503
  : `stale stages pending acknowledgement: ${staleStages.join(", ")}`
1390
1504
  });
1391
- const retroRequired = flowState.completedStages.includes("ship");
1392
- const retroComplete = !retroRequired ||
1393
- (typeof flowState.retro.completedAt === "string" && flowState.retro.compoundEntries > 0);
1505
+ const retroGateStatus = await evaluateRetroGate(projectRoot, flowState);
1394
1506
  checks.push({
1395
1507
  name: "state:retro_gate",
1396
- ok: retroComplete,
1397
- details: retroComplete
1398
- ? retroRequired
1399
- ? `retro gate complete (${flowState.retro.compoundEntries} compound entries)`
1508
+ ok: retroGateStatus.completed,
1509
+ details: retroGateStatus.completed
1510
+ ? retroGateStatus.required
1511
+ ? retroGateStatus.skipped
1512
+ ? "retro gate complete (retro skipped with recorded closeout decision)"
1513
+ : `retro gate complete (${retroGateStatus.compoundEntries} compound entries)`
1400
1514
  : "retro gate not required yet (ship not completed)"
1401
- : "retro gate incomplete: ship flow requires recorded retrospective evidence."
1515
+ : "retro gate incomplete: ship flow requires recorded retrospective evidence or an explicit retro skip."
1402
1516
  });
1403
1517
  const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
1404
1518
  const tddLogExists = await exists(tddLogPath);
@@ -1444,6 +1558,18 @@ export async function doctorChecks(projectRoot, options = {}) {
1444
1558
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
1445
1559
  details: `${RUNTIME_ROOT}/runs must exist for archived run snapshots`
1446
1560
  });
1561
+ const initRecovery = await initRecoveryCheck(projectRoot);
1562
+ checks.push({
1563
+ name: "state:init_recovery",
1564
+ ok: initRecovery.ok,
1565
+ details: initRecovery.details
1566
+ });
1567
+ const archiveIntegrity = await archiveIntegrityCheck(projectRoot);
1568
+ checks.push({
1569
+ name: "runs:archive_integrity",
1570
+ ok: archiveIntegrity.ok,
1571
+ details: archiveIntegrity.details
1572
+ });
1447
1573
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
1448
1574
  repairFeatureSystem: false
1449
1575
  });
@@ -1461,9 +1587,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1461
1587
  name: "warning:delegation:waived",
1462
1588
  ok: true,
1463
1589
  details: delegation.waived.length > 0
1464
- ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}${delegation.autoWaived.length > 0
1465
- ? ` (auto-waived due to harness limitation: ${delegation.autoWaived.join(", ")})`
1466
- : ""}`
1590
+ ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}`
1467
1591
  : "no waived mandatory delegations for current stage"
1468
1592
  });
1469
1593
  checks.push({
@@ -336,11 +336,27 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
336
336
  if (stage === "design") {
337
337
  const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
338
338
  if (researchGateRequired) {
339
+ const designMarkdown = await readArtifactMarkdown(projectRoot, "03-design.md");
340
+ const inlineResearchBody = designMarkdown
341
+ ? extractMarkdownSectionBody(designMarkdown, "Research Fleet Synthesis")
342
+ : null;
343
+ const inlineResearchLines = inlineResearchBody
344
+ ? inlineResearchBody
345
+ .split(/\r?\n/gu)
346
+ .map((line) => line.trim())
347
+ .filter((line) => line.length > 0)
348
+ .filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line))
349
+ .filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
350
+ !/<fill-in>/iu.test(line) &&
351
+ !/^>\s*Default path:/iu.test(line) &&
352
+ !/^\|\s*compact inline synthesis\s*\|\s*\|\s*\|\s*\|?\s*$/iu.test(line))
353
+ : [];
354
+ const inlineResearchComplete = inlineResearchLines.length > 0;
339
355
  const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
340
- if (!researchMarkdown) {
341
- issues.push("design research gate blocked (design_research_complete): missing `.cclaw/artifacts/02a-research.md`.");
356
+ if (!inlineResearchComplete && !researchMarkdown) {
357
+ issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in `.cclaw/artifacts/03-design.md`, or write `.cclaw/artifacts/02a-research.md` for deep/high-risk research.");
342
358
  }
343
- else {
359
+ else if (researchMarkdown) {
344
360
  const missingSections = [];
345
361
  for (const section of DESIGN_RESEARCH_REQUIRED_SECTIONS) {
346
362
  const body = extractMarkdownSectionBody(researchMarkdown, section);
@@ -353,10 +369,6 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
353
369
  .map((line) => line.trim())
354
370
  .filter((line) => line.length > 0)
355
371
  .filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
356
- // `<fill-in>` needs its own check because `\b` does not match
357
- // around `<`/`>` (non-word characters), so the previous combined
358
- // pattern `\b(?:...|<fill-in>)\b` silently never matched placeholder
359
- // templates that used angle-bracket form.
360
372
  const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
361
373
  !/<fill-in>/iu.test(line));
362
374
  if (nonPlaceholder.length === 0) {