cclaw-cli 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +54 -82
  2. package/dist/artifact-linter.d.ts +4 -0
  3. package/dist/artifact-linter.js +24 -3
  4. package/dist/cli.d.ts +1 -19
  5. package/dist/cli.js +49 -491
  6. package/dist/constants.d.ts +2 -13
  7. package/dist/constants.js +1 -43
  8. package/dist/content/closeout-guidance.d.ts +14 -0
  9. package/dist/content/closeout-guidance.js +42 -0
  10. package/dist/content/core-agents.js +51 -9
  11. package/dist/content/decision-protocol.d.ts +12 -0
  12. package/dist/content/decision-protocol.js +20 -0
  13. package/dist/content/diff-command.d.ts +1 -2
  14. package/dist/content/diff-command.js +8 -94
  15. package/dist/content/examples.d.ts +4 -10
  16. package/dist/content/examples.js +10 -20
  17. package/dist/content/hook-events.js +2 -2
  18. package/dist/content/hook-inline-snippets.d.ts +5 -2
  19. package/dist/content/hook-inline-snippets.js +33 -1
  20. package/dist/content/hook-manifest.d.ts +3 -4
  21. package/dist/content/hook-manifest.js +11 -12
  22. package/dist/content/hooks.js +2 -0
  23. package/dist/content/ideate-command.d.ts +2 -0
  24. package/dist/content/ideate-command.js +31 -25
  25. package/dist/content/iron-laws.d.ts +5 -5
  26. package/dist/content/iron-laws.js +5 -5
  27. package/dist/content/learnings.d.ts +3 -4
  28. package/dist/content/learnings.js +24 -50
  29. package/dist/content/meta-skill.js +31 -21
  30. package/dist/content/next-command.js +38 -38
  31. package/dist/content/node-hooks.js +17 -343
  32. package/dist/content/opencode-plugin.js +2 -100
  33. package/dist/content/research-playbooks.js +14 -14
  34. package/dist/content/review-loop.d.ts +2 -0
  35. package/dist/content/review-loop.js +8 -0
  36. package/dist/content/session-hooks.js +14 -46
  37. package/dist/content/skills.d.ts +0 -5
  38. package/dist/content/skills.js +53 -128
  39. package/dist/content/stage-common-guidance.d.ts +0 -1
  40. package/dist/content/stage-common-guidance.js +15 -14
  41. package/dist/content/stage-schema.d.ts +26 -1
  42. package/dist/content/stage-schema.js +121 -40
  43. package/dist/content/stages/_lint-metadata/index.js +9 -15
  44. package/dist/content/stages/brainstorm.js +22 -43
  45. package/dist/content/stages/design.js +37 -57
  46. package/dist/content/stages/plan.js +22 -13
  47. package/dist/content/stages/review.js +24 -27
  48. package/dist/content/stages/scope.js +34 -46
  49. package/dist/content/stages/ship.js +7 -4
  50. package/dist/content/stages/spec.js +20 -9
  51. package/dist/content/stages/tdd.js +64 -44
  52. package/dist/content/start-command.js +10 -12
  53. package/dist/content/status-command.d.ts +2 -7
  54. package/dist/content/status-command.js +19 -146
  55. package/dist/content/subagents.d.ts +0 -5
  56. package/dist/content/subagents.js +47 -28
  57. package/dist/content/templates.d.ts +1 -1
  58. package/dist/content/templates.js +126 -135
  59. package/dist/content/track-render-context.d.ts +17 -0
  60. package/dist/content/track-render-context.js +44 -0
  61. package/dist/content/tree-command.d.ts +1 -2
  62. package/dist/content/tree-command.js +4 -87
  63. package/dist/content/utility-skills.d.ts +2 -29
  64. package/dist/content/utility-skills.js +2 -1534
  65. package/dist/content/view-command.js +29 -11
  66. package/dist/delegation.d.ts +1 -1
  67. package/dist/delegation.js +5 -15
  68. package/dist/doctor-registry.js +20 -21
  69. package/dist/doctor.js +88 -344
  70. package/dist/flow-state.d.ts +3 -0
  71. package/dist/flow-state.js +2 -0
  72. package/dist/harness-adapters.d.ts +1 -1
  73. package/dist/harness-adapters.js +48 -57
  74. package/dist/install.js +128 -358
  75. package/dist/internal/advance-stage.js +3 -9
  76. package/dist/internal/compound-readiness.d.ts +1 -1
  77. package/dist/internal/compound-readiness.js +1 -1
  78. package/dist/internal/tdd-loop-status.d.ts +1 -1
  79. package/dist/internal/tdd-loop-status.js +1 -1
  80. package/dist/knowledge-store.d.ts +16 -10
  81. package/dist/knowledge-store.js +51 -15
  82. package/dist/policy.js +16 -105
  83. package/dist/run-archive.d.ts +4 -6
  84. package/dist/run-archive.js +15 -20
  85. package/dist/run-persistence.d.ts +2 -2
  86. package/dist/run-persistence.js +3 -9
  87. package/package.json +1 -2
  88. package/dist/content/archive-command.d.ts +0 -2
  89. package/dist/content/archive-command.js +0 -124
  90. package/dist/content/compound-command.d.ts +0 -5
  91. package/dist/content/compound-command.js +0 -193
  92. package/dist/content/contexts.d.ts +0 -18
  93. package/dist/content/contexts.js +0 -24
  94. package/dist/content/contracts.d.ts +0 -2
  95. package/dist/content/contracts.js +0 -51
  96. package/dist/content/doctor-references.d.ts +0 -2
  97. package/dist/content/doctor-references.js +0 -150
  98. package/dist/content/eval-scaffold.d.ts +0 -15
  99. package/dist/content/eval-scaffold.js +0 -370
  100. package/dist/content/feature-command.d.ts +0 -2
  101. package/dist/content/feature-command.js +0 -123
  102. package/dist/content/flow-map.d.ts +0 -23
  103. package/dist/content/flow-map.js +0 -134
  104. package/dist/content/harness-doc.d.ts +0 -2
  105. package/dist/content/harness-doc.js +0 -202
  106. package/dist/content/harness-playbooks.d.ts +0 -24
  107. package/dist/content/harness-playbooks.js +0 -393
  108. package/dist/content/harness-tool-refs.d.ts +0 -20
  109. package/dist/content/harness-tool-refs.js +0 -268
  110. package/dist/content/ops-command.d.ts +0 -2
  111. package/dist/content/ops-command.js +0 -71
  112. package/dist/content/protocols.d.ts +0 -7
  113. package/dist/content/protocols.js +0 -215
  114. package/dist/content/retro-command.d.ts +0 -2
  115. package/dist/content/retro-command.js +0 -165
  116. package/dist/content/rewind-command.d.ts +0 -2
  117. package/dist/content/rewind-command.js +0 -106
  118. package/dist/content/tdd-log-command.d.ts +0 -2
  119. package/dist/content/tdd-log-command.js +0 -85
  120. package/dist/eval/agents/single-shot.d.ts +0 -27
  121. package/dist/eval/agents/single-shot.js +0 -79
  122. package/dist/eval/agents/with-tools.d.ts +0 -44
  123. package/dist/eval/agents/with-tools.js +0 -261
  124. package/dist/eval/agents/workflow.d.ts +0 -31
  125. package/dist/eval/agents/workflow.js +0 -155
  126. package/dist/eval/baseline.d.ts +0 -38
  127. package/dist/eval/baseline.js +0 -282
  128. package/dist/eval/config-loader.d.ts +0 -14
  129. package/dist/eval/config-loader.js +0 -395
  130. package/dist/eval/corpus.d.ts +0 -30
  131. package/dist/eval/corpus.js +0 -330
  132. package/dist/eval/cost-guard.d.ts +0 -102
  133. package/dist/eval/cost-guard.js +0 -190
  134. package/dist/eval/diff.d.ts +0 -64
  135. package/dist/eval/diff.js +0 -323
  136. package/dist/eval/llm-client.d.ts +0 -176
  137. package/dist/eval/llm-client.js +0 -267
  138. package/dist/eval/mode.d.ts +0 -28
  139. package/dist/eval/mode.js +0 -61
  140. package/dist/eval/progress.d.ts +0 -83
  141. package/dist/eval/progress.js +0 -59
  142. package/dist/eval/report.d.ts +0 -11
  143. package/dist/eval/report.js +0 -181
  144. package/dist/eval/rubric-loader.d.ts +0 -20
  145. package/dist/eval/rubric-loader.js +0 -143
  146. package/dist/eval/runner.d.ts +0 -81
  147. package/dist/eval/runner.js +0 -746
  148. package/dist/eval/runs.d.ts +0 -41
  149. package/dist/eval/runs.js +0 -114
  150. package/dist/eval/sandbox.d.ts +0 -38
  151. package/dist/eval/sandbox.js +0 -137
  152. package/dist/eval/tools/glob.d.ts +0 -2
  153. package/dist/eval/tools/glob.js +0 -163
  154. package/dist/eval/tools/grep.d.ts +0 -2
  155. package/dist/eval/tools/grep.js +0 -152
  156. package/dist/eval/tools/index.d.ts +0 -7
  157. package/dist/eval/tools/index.js +0 -35
  158. package/dist/eval/tools/read.d.ts +0 -2
  159. package/dist/eval/tools/read.js +0 -122
  160. package/dist/eval/tools/types.d.ts +0 -49
  161. package/dist/eval/tools/types.js +0 -41
  162. package/dist/eval/tools/write.d.ts +0 -2
  163. package/dist/eval/tools/write.js +0 -92
  164. package/dist/eval/types.d.ts +0 -561
  165. package/dist/eval/types.js +0 -47
  166. package/dist/eval/verifiers/judge.d.ts +0 -40
  167. package/dist/eval/verifiers/judge.js +0 -256
  168. package/dist/eval/verifiers/rules.d.ts +0 -24
  169. package/dist/eval/verifiers/rules.js +0 -218
  170. package/dist/eval/verifiers/structural.d.ts +0 -14
  171. package/dist/eval/verifiers/structural.js +0 -171
  172. package/dist/eval/verifiers/traceability.d.ts +0 -23
  173. package/dist/eval/verifiers/traceability.js +0 -84
  174. package/dist/eval/verifiers/workflow-consistency.d.ts +0 -21
  175. package/dist/eval/verifiers/workflow-consistency.js +0 -225
  176. package/dist/eval/workflow-corpus.d.ts +0 -7
  177. package/dist/eval/workflow-corpus.js +0 -207
  178. package/dist/feature-system.d.ts +0 -42
  179. package/dist/feature-system.js +0 -432
  180. package/dist/internal/knowledge-digest.d.ts +0 -7
  181. package/dist/internal/knowledge-digest.js +0 -93
package/dist/doctor.js CHANGED
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { promisify } from "node:util";
6
- import { REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
6
+ import { REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
7
7
  import { CCLAW_AGENTS } from "./content/core-agents.js";
8
8
  import { detectAdvancedKeys, InvalidConfigError, readConfig } from "./config.js";
9
9
  import { exists } from "./fs-utils.js";
@@ -14,7 +14,6 @@ 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
16
  import { checkMandatoryDelegations } from "./delegation.js";
17
- import { activeFeatureMetaPath, ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
18
17
  import { buildTraceMatrix } from "./trace-matrix.js";
19
18
  import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
20
19
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
@@ -22,11 +21,10 @@ import { stageSkillFolder } from "./content/skills.js";
22
21
  import { doctorCheckMetadata } from "./doctor-registry.js";
23
22
  import { resolveTrackFromPrompt } from "./track-heuristics.js";
24
23
  import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
25
- import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
26
- import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
27
- import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
28
- import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName } from "./content/harness-playbooks.js";
24
+ import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
29
25
  import { validateHookDocument } from "./hook-schema.js";
26
+ import { validateKnowledgeEntry } from "./knowledge-store.js";
27
+ import { readSeedShelf } from "./content/seed-shelf.js";
30
28
  const execFileAsync = promisify(execFile);
31
29
  async function isGitRepo(projectRoot) {
32
30
  try {
@@ -37,27 +35,6 @@ async function isGitRepo(projectRoot) {
37
35
  return false;
38
36
  }
39
37
  }
40
- async function gitWorktreePaths(projectRoot) {
41
- try {
42
- const { stdout } = await execFileAsync("git", ["worktree", "list", "--porcelain"], {
43
- cwd: projectRoot
44
- });
45
- const out = new Set();
46
- for (const line of stdout.split(/\r?\n/)) {
47
- const trimmed = line.trim();
48
- if (!trimmed.startsWith("worktree "))
49
- continue;
50
- const rawPath = trimmed.slice("worktree ".length).trim();
51
- if (!rawPath)
52
- continue;
53
- out.add(path.resolve(rawPath));
54
- }
55
- return out;
56
- }
57
- catch {
58
- return new Set();
59
- }
60
- }
61
38
  async function resolveGitHooksDir(projectRoot) {
62
39
  try {
63
40
  const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], { cwd: projectRoot });
@@ -108,9 +85,21 @@ function extractUserPromptFromIdeaArtifact(markdown) {
108
85
  const body = (nextHeadingIndex >= 0 ? tail.slice(0, nextHeadingIndex) : tail).trim();
109
86
  return body.length > 0 ? body : null;
110
87
  }
88
+ function knowledgeRoutingSurfaceIsDiscoverable(content) {
89
+ const normalized = content.toLowerCase();
90
+ if (!normalized.includes(".cclaw/knowledge.jsonl"))
91
+ return false;
92
+ if (!/\b(rule|pattern|lesson|compound)\b/u.test(normalized))
93
+ return false;
94
+ return ["trigger", "action", "origin_run"].every((term) => normalized.includes(term));
95
+ }
111
96
  async function commandAvailable(command) {
112
97
  try {
113
- await execFileAsync("bash", ["-lc", `command -v ${command} >/dev/null 2>&1`]);
98
+ if (process.platform === "win32") {
99
+ await execFileAsync("where", [command]);
100
+ return true;
101
+ }
102
+ await execFileAsync(command, ["--version"]);
114
103
  return true;
115
104
  }
116
105
  catch {
@@ -321,12 +310,6 @@ export async function doctorChecks(projectRoot, options = {}) {
321
310
  });
322
311
  }
323
312
  for (const stage of FLOW_STAGES) {
324
- const commandPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${stage}.md`);
325
- checks.push({
326
- name: `command:${stage}`,
327
- ok: await exists(commandPath),
328
- details: commandPath
329
- });
330
313
  const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", stageSkillFolder(stage), "SKILL.md");
331
314
  const skillExists = await exists(skillPath);
332
315
  checks.push({
@@ -341,7 +324,7 @@ export async function doctorChecks(projectRoot, options = {}) {
341
324
  // Soft max tightened from 650 → 500 after externalising the TDD
342
325
  // batch-execution walkthrough and collapsing the duplicate "what
343
326
  // goes wrong" lists. Stage skills beyond 500 lines drift into unread
344
- // bloat; long-form content belongs under `.cclaw/references/` instead.
327
+ // bloat; long-form content belongs in shared guidance sections instead.
345
328
  const MAX_SKILL_LINES = 500;
346
329
  checks.push({
347
330
  name: `skill:${stage}:min_lines`,
@@ -360,7 +343,6 @@ export async function doctorChecks(projectRoot, options = {}) {
360
343
  { id: "checklist", pattern: /^## Checklist$/m, label: "## Checklist" },
361
344
  { id: "completion_parameters", pattern: /^## Completion Parameters$/m, label: "## Completion Parameters" },
362
345
  { id: "shared_guidance", pattern: /^## Shared Stage Guidance$/m, label: "## Shared Stage Guidance" },
363
- { id: "good_vs_bad", pattern: /Good vs Bad/i, label: "Good vs Bad examples" },
364
346
  { id: "anti_patterns", pattern: /^## Anti-Patterns & Red Flags$/m, label: "## Anti-Patterns & Red Flags" }
365
347
  ];
366
348
  const missingSections = canonicalSections
@@ -386,7 +368,7 @@ export async function doctorChecks(projectRoot, options = {}) {
386
368
  { id: "routing_flow", pattern: /Routing flow/i, label: "Routing flow" },
387
369
  { id: "task_classification", pattern: /Task classification/i, label: "Task classification" },
388
370
  { id: "stage_map", pattern: /Stage quick map/i, label: "Stage quick map" },
389
- { id: "protocol_refs", pattern: /Protocol references/i, label: "Protocol references" },
371
+ { id: "protocol_behavior", pattern: /Protocol Behavior/i, label: "Protocol Behavior" },
390
372
  { id: "knowledge_guidance", pattern: /Knowledge guidance/i, label: "Knowledge guidance" },
391
373
  { id: "failure_guardrails", pattern: /Failure guardrails/i, label: "Failure guardrails" }
392
374
  ];
@@ -401,50 +383,6 @@ export async function doctorChecks(projectRoot, options = {}) {
401
383
  : `${metaSkillPath} missing signals: ${missingMeta.join(", ")}`
402
384
  });
403
385
  }
404
- // Harness tool-map references (A.1#4) must always be present — stage skills
405
- // cite the paths by name.
406
- const harnessRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "harness-tools");
407
- const harnessRefFiles = ["README.md", "claude.md", "cursor.md", "opencode.md", "codex.md"];
408
- for (const fileName of harnessRefFiles) {
409
- const refPath = path.join(harnessRefDir, fileName);
410
- checks.push({
411
- name: `harness_tool_ref:${fileName.replace(/\.md$/, "")}`,
412
- ok: await exists(refPath),
413
- details: refPath
414
- });
415
- }
416
- // Per-stage example references (A.2#8, progressive disclosure). Each stage
417
- // skill's Examples section points here; the file MUST exist or the pointer
418
- // is a dangling link.
419
- const stageRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "stages");
420
- for (const stage of FLOW_STAGES) {
421
- const refPath = path.join(stageRefDir, `${stage}-examples.md`);
422
- checks.push({
423
- name: `stage_examples_ref:${stage}`,
424
- ok: await exists(refPath),
425
- details: refPath
426
- });
427
- }
428
- checks.push({
429
- name: "harness_ref:matrix",
430
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "references", "harnesses.md")),
431
- details: `${RUNTIME_ROOT}/references/harnesses.md`
432
- });
433
- const playbookDir = path.join(projectRoot, RUNTIME_ROOT, ...HARNESS_PLAYBOOKS_DIR.split("/"));
434
- checks.push({
435
- name: "harness_ref:playbooks_index",
436
- ok: await exists(path.join(playbookDir, "README.md")),
437
- details: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/README.md`
438
- });
439
- const doctorRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "doctor");
440
- for (const fileName of Object.keys(DOCTOR_REFERENCE_MARKDOWN)) {
441
- const refPath = path.join(doctorRefDir, fileName);
442
- checks.push({
443
- name: `doctor_ref:${fileName.replace(/\.md$/, "")}`,
444
- ok: await exists(refPath),
445
- details: refPath
446
- });
447
- }
448
386
  checks.push({
449
387
  name: "gitignore:required_patterns",
450
388
  ok: await gitignoreHasRequiredPatterns(projectRoot),
@@ -553,12 +491,6 @@ export async function doctorChecks(projectRoot, options = {}) {
553
491
  });
554
492
  }
555
493
  }
556
- const playbookFile = path.join(projectRoot, RUNTIME_ROOT, ...HARNESS_PLAYBOOKS_DIR.split("/"), harnessPlaybookFileName(harness));
557
- checks.push({
558
- name: `harness_ref:playbook:${harness}`,
559
- ok: await exists(playbookFile),
560
- details: `${RUNTIME_ROOT}/${HARNESS_PLAYBOOKS_DIR}/${harnessPlaybookFileName(harness)}`
561
- });
562
494
  }
563
495
  const agentsFile = path.join(projectRoot, "AGENTS.md");
564
496
  let agentsBlockOk = false;
@@ -569,7 +501,6 @@ export async function doctorChecks(projectRoot, options = {}) {
569
501
  const hasCcNext = content.includes("/cc-next");
570
502
  const hasCcIdeate = content.includes("/cc-ideate");
571
503
  const hasCcView = content.includes("/cc-view");
572
- const hasCcOps = content.includes("/cc-ops");
573
504
  const hasVerification = content.includes("Verification Discipline");
574
505
  const hasMinimalMarker = content.includes("intentionally minimal for cross-project use");
575
506
  const hasMetaSkillPointer = content.includes(".cclaw/skills/using-cclaw/SKILL.md");
@@ -578,7 +509,6 @@ export async function doctorChecks(projectRoot, options = {}) {
578
509
  && hasCcNext
579
510
  && hasCcIdeate
580
511
  && hasCcView
581
- && hasCcOps
582
512
  && hasVerification
583
513
  && hasMinimalMarker
584
514
  && hasMetaSkillPointer;
@@ -588,8 +518,8 @@ export async function doctorChecks(projectRoot, options = {}) {
588
518
  ok: agentsBlockOk,
589
519
  details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
590
520
  });
591
- // Utility commands keep in sync with UTILITY_COMMANDS (src/constants.ts)
592
- for (const cmd of UTILITY_COMMANDS) {
521
+ // User-facing entry commands only. Stage and view subcommands live in skills.
522
+ for (const cmd of ["start", "next", "ideate", "view"]) {
593
523
  const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
594
524
  checks.push({
595
525
  name: `utility_command:${cmd}`,
@@ -601,15 +531,7 @@ export async function doctorChecks(projectRoot, options = {}) {
601
531
  for (const [folder, label] of [
602
532
  ["learnings", "learnings"],
603
533
  ["flow-ideate", "flow-ideate"],
604
- ["flow-tree", "flow-tree"],
605
- ["flow-diff", "flow-diff"],
606
- ["using-git-worktrees", "using-git-worktrees"],
607
- ["tdd-cycle-log", "tdd-cycle-log"],
608
- ["flow-retro", "flow-retro"],
609
- ["flow-compound", "flow-compound"],
610
- ["flow-rewind", "flow-rewind"],
611
- ["verification-before-completion", "verification-before-completion"],
612
- ["finishing-a-development-branch", "finishing-a-development-branch"],
534
+ ["flow-view", "flow-view"],
613
535
  ["subagent-dev", "sdd"],
614
536
  ["parallel-dispatch", "parallel-agents"],
615
537
  ["session", "session"],
@@ -622,15 +544,6 @@ export async function doctorChecks(projectRoot, options = {}) {
622
544
  details: skillPath
623
545
  });
624
546
  }
625
- // Extended utility skills generated from utility skill map.
626
- for (const folder of UTILITY_SKILL_FOLDERS) {
627
- const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
628
- checks.push({
629
- name: `utility_skill:${folder}`,
630
- ok: await exists(skillPath),
631
- details: skillPath
632
- });
633
- }
634
547
  // Opt-in language rule packs: only check presence for packs the user enabled.
635
548
  // Canonical location is .cclaw/rules/lang/<pack>.md.
636
549
  for (const pack of parsedConfig?.languageRulePacks ?? []) {
@@ -775,11 +688,11 @@ export async function doctorChecks(projectRoot, options = {}) {
775
688
  preCommands.some((cmd) => cmd.includes("prompt-guard")) &&
776
689
  preCommands.some((cmd) => cmd.includes("workflow-guard")) &&
777
690
  postCommands.some((cmd) => cmd.includes("context-monitor")) &&
778
- stopCommands.some((cmd) => cmd.includes("stop-checkpoint"));
691
+ stopCommands.some((cmd) => cmd.includes("stop-handoff"));
779
692
  checks.push({
780
693
  name: "hook:wiring:claude",
781
694
  ok: wiringOk,
782
- details: `${file} must wire session-start/prompt-guard/workflow-guard/context-monitor/stop-checkpoint`
695
+ details: `${file} must wire session-start/prompt-guard/workflow-guard/context-monitor/stop-handoff`
783
696
  });
784
697
  }
785
698
  if (configuredHarnesses.includes("cursor")) {
@@ -808,11 +721,11 @@ export async function doctorChecks(projectRoot, options = {}) {
808
721
  preCommands.some((cmd) => cmd.includes("prompt-guard")) &&
809
722
  preCommands.some((cmd) => cmd.includes("workflow-guard")) &&
810
723
  postCommands.some((cmd) => cmd.includes("context-monitor")) &&
811
- stopCommands.some((cmd) => cmd.includes("stop-checkpoint"));
724
+ stopCommands.some((cmd) => cmd.includes("stop-handoff"));
812
725
  checks.push({
813
726
  name: "hook:wiring:cursor",
814
727
  ok: wiringOk,
815
- details: `${file} must wire session-start/prompt-guard/workflow-guard/context-monitor/stop-checkpoint`
728
+ details: `${file} must wire session-start/prompt-guard/workflow-guard/context-monitor/stop-handoff`
816
729
  });
817
730
  const cursorRulePath = path.join(projectRoot, ".cursor/rules/cclaw-workflow.mdc");
818
731
  let cursorRuleOk = false;
@@ -878,11 +791,11 @@ export async function doctorChecks(projectRoot, options = {}) {
878
791
  codexPreCmds.some((cmd) => cmd.includes("prompt-guard")) &&
879
792
  codexPreCmds.some((cmd) => cmd.includes("workflow-guard")) &&
880
793
  codexPostCmds.some((cmd) => cmd.includes("context-monitor")) &&
881
- codexStopCmds.some((cmd) => cmd.includes("stop-checkpoint"));
794
+ codexStopCmds.some((cmd) => cmd.includes("stop-handoff"));
882
795
  checks.push({
883
796
  name: "hook:wiring:codex",
884
797
  ok: codexWiringOk,
885
- details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/workflow/verify-current-state), PreToolUse(prompt/workflow), PostToolUse(context-monitor), and Stop(stop-checkpoint). PreToolUse/PostToolUse run Bash-only in Codex v0.114+`
798
+ details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/workflow/verify-current-state), PreToolUse(prompt/workflow), PostToolUse(context-monitor), and Stop(stop-handoff). PreToolUse/PostToolUse run Bash-only in Codex v0.114+`
886
799
  });
887
800
  // Feature flag warning: Codex ignores `.codex/hooks.json` unless the
888
801
  // user has `[features] codex_hooks = true` in `~/.codex/config.toml`.
@@ -981,7 +894,7 @@ export async function doctorChecks(projectRoot, options = {}) {
981
894
  checks.push({
982
895
  name: "lifecycle:opencode:rehydration_events",
983
896
  ok,
984
- details: `${file} must include event lifecycle handler, session.created/updated/resumed/cleared/compacted rehydration, tool.execute.before/after with prompt/workflow/context hooks, session.idle checkpoint, and transform rehydration`
897
+ details: `${file} must include event lifecycle handler, session.created/updated/resumed/cleared/compacted rehydration, tool.execute.before/after with prompt/workflow/context hooks, session.idle handoff, and transform rehydration`
985
898
  });
986
899
  checks.push({
987
900
  name: "hook:opencode:single_tool_handler_path",
@@ -989,7 +902,7 @@ export async function doctorChecks(projectRoot, options = {}) {
989
902
  details: `${file} must route tool.execute.before/after through dedicated handlers exactly once (no duplicate event() branches).`
990
903
  });
991
904
  checks.push({
992
- name: "hook:opencode:precompact_digest",
905
+ name: "hook:opencode:precompact_compat",
993
906
  ok: precompactHookOk,
994
907
  details: `${file} must run pre-compact on session.compacted before bootstrap refresh.`
995
908
  });
@@ -1022,7 +935,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1022
935
  if (!(await exists(candidate)))
1023
936
  continue;
1024
937
  const content = await fs.readFile(candidate, "utf8");
1025
- if (/bash\s+\.cclaw\/hooks\/|\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh/u.test(content)) {
938
+ if (/bash\s+\.cclaw\/hooks\/|\.cclaw\/hooks\/(?:session-start|stop-handoff|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh/u.test(content)) {
1026
939
  legacyDispatchFiles.push(path.relative(projectRoot, candidate));
1027
940
  }
1028
941
  }
@@ -1039,11 +952,6 @@ export async function doctorChecks(projectRoot, options = {}) {
1039
952
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl")),
1040
953
  details: `${RUNTIME_ROOT}/knowledge.jsonl must exist`
1041
954
  });
1042
- checks.push({
1043
- name: "knowledge:digest_exists",
1044
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "knowledge-digest.md")),
1045
- details: `${RUNTIME_ROOT}/state/knowledge-digest.md must exist`
1046
- });
1047
955
  // There must be NO legacy markdown knowledge store — JSONL is the only store.
1048
956
  const legacyKnowledgeMdPath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.md");
1049
957
  const legacyExists = await exists(legacyKnowledgeMdPath);
@@ -1061,6 +969,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1061
969
  let parsedKnowledgeLines = 0;
1062
970
  let lowConfidenceLines = 0;
1063
971
  let staleRawEntries = 0;
972
+ const schemaErrors = [];
1064
973
  const triggerActionCounts = new Map();
1065
974
  // Stale threshold for raw entries: ~90 days with no re-observation.
1066
975
  // Chosen to match the compound drift checklist language; anything newer is
@@ -1075,7 +984,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1075
984
  "domain",
1076
985
  "stage",
1077
986
  "origin_stage",
1078
- "origin_feature",
987
+ "origin_run",
1079
988
  "frequency",
1080
989
  "universality",
1081
990
  "maturity",
@@ -1098,6 +1007,10 @@ export async function doctorChecks(projectRoot, options = {}) {
1098
1007
  continue;
1099
1008
  }
1100
1009
  parsedKnowledgeLines += 1;
1010
+ const validation = validateKnowledgeEntry(parsed);
1011
+ if (!validation.ok) {
1012
+ schemaErrors.push(`line ${parsedKnowledgeLines}: ${validation.errors.slice(0, 3).join(" ")}`);
1013
+ }
1101
1014
  const confidence = typeof parsed.confidence === "string" ? parsed.confidence.toLowerCase() : "";
1102
1015
  if (confidence === "low") {
1103
1016
  lowConfidenceLines += 1;
@@ -1108,7 +1021,12 @@ export async function doctorChecks(projectRoot, options = {}) {
1108
1021
  const key = `${trigger} => ${action}`;
1109
1022
  triggerActionCounts.set(key, (triggerActionCounts.get(key) ?? 0) + 1);
1110
1023
  }
1111
- const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
1024
+ const missing = requiredV2Fields.some((field) => {
1025
+ if (field === "origin_run" && Object.prototype.hasOwnProperty.call(parsed, "origin_feature")) {
1026
+ return false;
1027
+ }
1028
+ return !Object.prototype.hasOwnProperty.call(parsed, field);
1029
+ });
1112
1030
  if (missing) {
1113
1031
  missingSchemaV2Fields += 1;
1114
1032
  }
@@ -1145,6 +1063,15 @@ export async function doctorChecks(projectRoot, options = {}) {
1145
1063
  ? `all ${parsedKnowledgeLines} knowledge line(s) include schema v2 fields`
1146
1064
  : `warning: ${missingSchemaV2Fields}/${parsedKnowledgeLines} knowledge line(s) miss schema v2 fields (origin/maturity/frequency metadata)`
1147
1065
  });
1066
+ checks.push({
1067
+ name: "warning:knowledge:current_schema",
1068
+ ok: schemaErrors.length === 0,
1069
+ details: parsedKnowledgeLines === 0
1070
+ ? "knowledge.jsonl is empty"
1071
+ : schemaErrors.length === 0
1072
+ ? `all ${parsedKnowledgeLines} knowledge line(s) match the current strict schema`
1073
+ : `warning: ${schemaErrors.length}/${parsedKnowledgeLines} knowledge line(s) fail current schema validation (${schemaErrors.slice(0, 3).join("; ")})`
1074
+ });
1148
1075
  const lowConfidenceRatio = parsedKnowledgeLines === 0 ? 0 : lowConfidenceLines / parsedKnowledgeLines;
1149
1076
  checks.push({
1150
1077
  name: "warning:knowledge:low_confidence_density",
@@ -1153,7 +1080,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1153
1080
  ? "knowledge.jsonl is empty"
1154
1081
  : lowConfidenceRatio <= 0.35
1155
1082
  ? `low-confidence entries: ${lowConfidenceLines}/${parsedKnowledgeLines}`
1156
- : `warning: low-confidence entries are high (${lowConfidenceLines}/${parsedKnowledgeLines}, ${(lowConfidenceRatio * 100).toFixed(1)}%). Consider /cc-learn curate before adding more.`
1083
+ : `warning: low-confidence entries are high (${lowConfidenceLines}/${parsedKnowledgeLines}, ${(lowConfidenceRatio * 100).toFixed(1)}%). Consider a learnings curation pass before adding more.`
1157
1084
  });
1158
1085
  const repeatedClusters = [...triggerActionCounts.entries()].filter(([, count]) => count >= 3);
1159
1086
  checks.push({
@@ -1161,7 +1088,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1161
1088
  ok: true,
1162
1089
  details: repeatedClusters.length === 0
1163
1090
  ? "no high-frequency repeated trigger/action clusters detected"
1164
- : `warning: ${repeatedClusters.length} repeated learning cluster(s) detected (>=3 repeats). Consider /cc-ops compound to lift them into rules/skills.`
1091
+ : `warning: ${repeatedClusters.length} repeated learning cluster(s) detected (>=3 repeats). Consider curating knowledge lifts into durable rules/skills.`
1165
1092
  });
1166
1093
  checks.push({
1167
1094
  name: "warning:knowledge:stale_raw_entries",
@@ -1170,101 +1097,37 @@ export async function doctorChecks(projectRoot, options = {}) {
1170
1097
  ? "knowledge.jsonl is empty"
1171
1098
  : staleRawEntries === 0
1172
1099
  ? `no raw knowledge entries older than 90 days`
1173
- : `warning: ${staleRawEntries} raw knowledge entry(ies) have last_seen_ts older than 90 days. Run /cc-learn curate or append a superseding entry before the next /cc-ops compound pass.`
1100
+ : `warning: ${staleRawEntries} raw knowledge entry(ies) have last_seen_ts older than 90 days. Run a learnings curation pass or append a superseding entry before the next compound pass.`
1174
1101
  });
1175
1102
  }
1176
- checks.push({
1177
- name: "state:checkpoint_exists",
1178
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "checkpoint.json")),
1179
- details: `${RUNTIME_ROOT}/state/checkpoint.json must exist`
1180
- });
1181
- checks.push({
1182
- name: "state:stage_activity_exists",
1183
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl")),
1184
- details: `${RUNTIME_ROOT}/state/stage-activity.jsonl must exist`
1185
- });
1186
- const stageActivityPath = path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl");
1187
- if (await exists(stageActivityPath)) {
1188
- let malformedActivityLines = 0;
1189
- let missingSchemaVersion = 0;
1190
- let parsedActivityLines = 0;
1191
- try {
1192
- const raw = await fs.readFile(stageActivityPath, "utf8");
1193
- const lines = raw
1194
- .split("\n")
1195
- .map((line) => line.trim())
1196
- .filter((line) => line.length > 0);
1197
- for (const line of lines) {
1198
- try {
1199
- const parsed = JSON.parse(line);
1200
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1201
- malformedActivityLines += 1;
1202
- continue;
1203
- }
1204
- parsedActivityLines += 1;
1205
- if (parsed.schemaVersion !== 1) {
1206
- missingSchemaVersion += 1;
1207
- }
1208
- }
1209
- catch {
1210
- malformedActivityLines += 1;
1211
- }
1212
- }
1213
- }
1214
- catch {
1215
- malformedActivityLines += 1;
1103
+ const routingKnowledgeSurfaces = [];
1104
+ for (const routingFileName of ["AGENTS.md", "CLAUDE.md"]) {
1105
+ const routingFilePath = path.join(projectRoot, routingFileName);
1106
+ if (!(await exists(routingFilePath)))
1107
+ continue;
1108
+ const content = await fs.readFile(routingFilePath, "utf8");
1109
+ if (knowledgeRoutingSurfaceIsDiscoverable(content)) {
1110
+ routingKnowledgeSurfaces.push(routingFileName);
1216
1111
  }
1217
- checks.push({
1218
- name: "state:stage_activity_jsonl_parseable",
1219
- ok: malformedActivityLines === 0,
1220
- details: malformedActivityLines === 0
1221
- ? "stage-activity.jsonl lines parse as JSON objects"
1222
- : `stage-activity.jsonl contains ${malformedActivityLines} malformed line(s)`
1223
- });
1224
- checks.push({
1225
- name: "warning:state:stage_activity_schema_version",
1226
- ok: true,
1227
- details: parsedActivityLines === 0
1228
- ? "stage-activity.jsonl is empty"
1229
- : missingSchemaVersion === 0
1230
- ? `all ${parsedActivityLines} stage-activity line(s) include schemaVersion=1`
1231
- : `warning: ${missingSchemaVersion}/${parsedActivityLines} stage-activity line(s) missing schemaVersion=1`
1232
- });
1233
1112
  }
1234
1113
  checks.push({
1235
- name: "state:suggestion_memory_exists",
1236
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
1237
- details: `${RUNTIME_ROOT}/state/suggestion-memory.json must exist for proactive suggestion memory`
1114
+ name: "warning:knowledge:discoverability",
1115
+ ok: routingKnowledgeSurfaces.length > 0,
1116
+ details: routingKnowledgeSurfaces.length > 0
1117
+ ? `knowledge store schema is discoverable from ${routingKnowledgeSurfaces.join(", ")}`
1118
+ : "warning: AGENTS.md or CLAUDE.md should mention .cclaw/knowledge.jsonl and its type/trigger/action/origin_run usage"
1238
1119
  });
1120
+ const seedEntries = await readSeedShelf(projectRoot);
1121
+ const orphanSeeds = seedEntries.filter((seed) => seed.sourceArtifact === null || seed.triggerWhen.length === 0 || seed.action === null || seed.action.trim().length === 0);
1239
1122
  checks.push({
1240
- name: "state:harness_gaps_exists",
1241
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "harness-gaps.json")),
1242
- details: `${RUNTIME_ROOT}/state/harness-gaps.json must exist for tiered harness capability tracking`
1123
+ name: "warning:knowledge:orphan_seeds",
1124
+ ok: orphanSeeds.length === 0,
1125
+ details: seedEntries.length === 0
1126
+ ? "no seed shelf entries present"
1127
+ : orphanSeeds.length === 0
1128
+ ? `all ${seedEntries.length} seed shelf entr${seedEntries.length === 1 ? "y is" : "ies are"} discoverable`
1129
+ : `warning: ${orphanSeeds.length}/${seedEntries.length} seed shelf entr${seedEntries.length === 1 ? "y is" : "ies are"} missing source_artifact, trigger_when, or action (${orphanSeeds.slice(0, 3).map((seed) => seed.relPath).join(", ")})`
1243
1130
  });
1244
- const contextModeStatePath = path.join(projectRoot, RUNTIME_ROOT, "state", "context-mode.json");
1245
- checks.push({
1246
- name: "state:context_mode_exists",
1247
- ok: await exists(contextModeStatePath),
1248
- details: `${RUNTIME_ROOT}/state/context-mode.json must exist for context mode switching`
1249
- });
1250
- if (await exists(contextModeStatePath)) {
1251
- let contextModeOk = false;
1252
- try {
1253
- const parsed = JSON.parse(await fs.readFile(contextModeStatePath, "utf8"));
1254
- const activeMode = typeof parsed.activeMode === "string" ? parsed.activeMode : "";
1255
- contextModeOk = activeMode.length > 0 && Object.prototype.hasOwnProperty.call(CONTEXT_MODES, activeMode);
1256
- }
1257
- catch {
1258
- contextModeOk = false;
1259
- }
1260
- checks.push({
1261
- name: "state:context_mode_valid",
1262
- ok: contextModeOk,
1263
- details: `${RUNTIME_ROOT}/state/context-mode.json must reference one of: ${Object.keys(CONTEXT_MODES).join(", ")} (default=${DEFAULT_CONTEXT_MODE})`
1264
- });
1265
- }
1266
- await ensureFeatureSystem(projectRoot, { repair: false });
1267
- const activeFeature = await readActiveFeature(projectRoot, { repair: false });
1268
1131
  let flowState = createInitialFlowState();
1269
1132
  let flowStateCorruptError = null;
1270
1133
  try {
@@ -1320,8 +1183,6 @@ export async function doctorChecks(projectRoot, options = {}) {
1320
1183
  path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.json"),
1321
1184
  path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json"),
1322
1185
  path.join(projectRoot, RUNTIME_ROOT, "state", "reconciliation-notices.json"),
1323
- path.join(projectRoot, RUNTIME_ROOT, "state", "worktrees.json"),
1324
- path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json"),
1325
1186
  path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl")
1326
1187
  ];
1327
1188
  const permissiveStateFiles = [];
@@ -1457,113 +1318,13 @@ export async function doctorChecks(projectRoot, options = {}) {
1457
1318
  ? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
1458
1319
  : `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
1459
1320
  });
1460
- const activeMetaStatus = await readJsonObjectStatus(activeFeatureMetaPath(projectRoot));
1461
- const worktreeRegistryStatus = await readJsonObjectStatus(worktreeRegistryPath(projectRoot));
1462
- const features = await listFeatures(projectRoot, { repair: false });
1463
- const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot, { repair: false });
1464
- const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
1465
- const activeFeatureWorkspacePath = activeFeatureEntry
1466
- ? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
1467
- : "";
1468
- checks.push({
1469
- name: "state:active_feature_meta",
1470
- ok: activeMetaStatus.exists,
1471
- details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
1472
- });
1473
- checks.push({
1474
- name: "state:active_feature_meta_valid_json",
1475
- ok: activeMetaStatus.ok,
1476
- details: activeMetaStatus.ok
1477
- ? `${RUNTIME_ROOT}/state/active-feature.json parsed successfully`
1478
- : `${RUNTIME_ROOT}/state/active-feature.json is invalid: ${activeMetaStatus.error ?? "unknown error"}`
1479
- });
1480
- checks.push({
1481
- name: "state:worktree_registry_exists",
1482
- ok: worktreeRegistryStatus.exists,
1483
- details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
1484
- });
1485
- checks.push({
1486
- name: "state:worktree_registry_valid_json",
1487
- ok: worktreeRegistryStatus.ok,
1488
- details: worktreeRegistryStatus.ok
1489
- ? `${RUNTIME_ROOT}/state/worktrees.json parsed successfully`
1490
- : `${RUNTIME_ROOT}/state/worktrees.json is invalid: ${worktreeRegistryStatus.error ?? "unknown error"}`
1491
- });
1492
- checks.push({
1493
- name: "state:active_feature_exists",
1494
- ok: features.includes(activeFeature),
1495
- details: features.includes(activeFeature)
1496
- ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/state/worktrees.json`
1497
- : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/state/worktrees.json`
1498
- });
1499
- checks.push({
1500
- name: "state:features_nonempty",
1501
- ok: features.length > 0,
1502
- details: features.length > 0
1503
- ? `${features.length} registered feature workspace(s): ${features.join(", ")}`
1504
- : `no feature workspaces found in ${RUNTIME_ROOT}/state/worktrees.json`
1505
- });
1506
- checks.push({
1507
- name: "state:active_feature_workspace_path",
1508
- ok: activeFeatureEntry ? await exists(activeFeatureWorkspacePath) : false,
1509
- details: activeFeatureEntry
1510
- ? `active feature "${activeFeature}" maps to workspace path ${activeFeatureEntry.path} (${activeFeatureEntry.source})`
1511
- : `active feature "${activeFeature}" has no worktree registry entry`
1512
- });
1513
- const missingRegistryPaths = [];
1514
- for (const entry of worktreeRegistry.entries) {
1515
- const workspacePath = resolveFeatureWorkspacePath(projectRoot, entry);
1516
- if (!(await exists(workspacePath))) {
1517
- missingRegistryPaths.push(`${entry.featureId}:${entry.path}`);
1518
- }
1519
- }
1520
- checks.push({
1521
- name: "state:worktree_registry_paths_exist",
1522
- ok: missingRegistryPaths.length === 0,
1523
- details: missingRegistryPaths.length === 0
1524
- ? "all worktree registry entries resolve to existing paths"
1525
- : `missing worktree paths for registry entries: ${missingRegistryPaths.join(", ")}`
1526
- });
1527
- const gitTrackedPaths = await gitWorktreePaths(projectRoot);
1528
- const registryGitPaths = worktreeRegistry.entries
1529
- .filter((entry) => entry.source === "git-worktree")
1530
- .map((entry) => resolveFeatureWorkspacePath(projectRoot, entry));
1531
- const missingFromGitList = registryGitPaths.filter((workspacePath) => !gitTrackedPaths.has(path.resolve(workspacePath)));
1532
- checks.push({
1533
- name: "warning:state:worktree_registry_git_drift",
1534
- ok: true,
1535
- details: missingFromGitList.length === 0
1536
- ? "git-worktree registry entries align with `git worktree list`"
1537
- : `warning: ${missingFromGitList.length} registry worktree path(s) are missing from \`git worktree list\`: ${missingFromGitList.join(", ")}`
1538
- });
1539
- const managedWorktreeRoot = path.join(projectRoot, RUNTIME_ROOT, "worktrees");
1540
- const unregisteredManagedWorktrees = [...gitTrackedPaths]
1541
- .filter((workspacePath) => workspacePath.startsWith(path.resolve(managedWorktreeRoot)))
1542
- .filter((workspacePath) => !registryGitPaths.some((registeredPath) => path.resolve(registeredPath) === workspacePath));
1543
- checks.push({
1544
- name: "warning:state:worktree_unregistered_paths",
1545
- ok: true,
1546
- details: unregisteredManagedWorktrees.length === 0
1547
- ? "no unmanaged git worktrees under .cclaw/worktrees"
1548
- : `warning: unregistered git worktree paths detected: ${unregisteredManagedWorktrees.map((value) => path.relative(projectRoot, value)).join(", ")}`
1549
- });
1550
- const legacyWorkspaceEntries = worktreeRegistry.entries
1551
- .filter((entry) => entry.source === "legacy-snapshot")
1552
- .map((entry) => entry.featureId);
1553
- checks.push({
1554
- name: "warning:state:legacy_feature_snapshots",
1555
- ok: legacyWorkspaceEntries.length === 0,
1556
- details: legacyWorkspaceEntries.length === 0
1557
- ? "no legacy .cclaw/features snapshot entries remain"
1558
- : `legacy snapshot entries still present (read-only): ${legacyWorkspaceEntries.join(", ")}`
1559
- });
1560
1321
  const staleStages = Object.keys(flowState.staleStages).filter((value) => FLOW_STAGES.includes(value));
1561
1322
  checks.push({
1562
1323
  name: "state:stale_stages_resolved",
1563
1324
  ok: staleStages.length === 0,
1564
1325
  details: staleStages.length === 0
1565
1326
  ? "no stale stages pending acknowledgement"
1566
- : `stale stages must be acknowledged via /cc-ops rewind --ack <stage>: ${staleStages.join(", ")}`
1327
+ : `stale stages pending acknowledgement: ${staleStages.join(", ")}`
1567
1328
  });
1568
1329
  const retroRequired = flowState.completedStages.includes("ship");
1569
1330
  const retroComplete = !retroRequired ||
@@ -1575,38 +1336,21 @@ export async function doctorChecks(projectRoot, options = {}) {
1575
1336
  ? retroRequired
1576
1337
  ? `retro gate complete (${flowState.retro.compoundEntries} compound entries)`
1577
1338
  : "retro gate not required yet (ship not completed)"
1578
- : "retro gate incomplete: run /cc-ops retro and record at least one compound knowledge entry"
1579
- });
1580
- const flowSnapshotPath = path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.snapshot.json");
1581
- const flowSnapshotExists = await exists(flowSnapshotPath);
1582
- let flowSnapshotValid = flowSnapshotExists;
1583
- if (flowSnapshotExists) {
1584
- try {
1585
- JSON.parse(await fs.readFile(flowSnapshotPath, "utf8"));
1586
- flowSnapshotValid = true;
1587
- }
1588
- catch {
1589
- flowSnapshotValid = false;
1590
- }
1591
- }
1592
- checks.push({
1593
- name: "state:flow_snapshot",
1594
- ok: flowSnapshotExists && flowSnapshotValid,
1595
- details: flowSnapshotExists
1596
- ? flowSnapshotValid
1597
- ? `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists and is valid JSON`
1598
- : `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists but is invalid JSON`
1599
- : `${RUNTIME_ROOT}/state/flow-state.snapshot.json is missing`
1339
+ : "retro gate incomplete: ship flow requires recorded retrospective evidence."
1600
1340
  });
1601
1341
  const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
1602
1342
  const tddLogExists = await exists(tddLogPath);
1343
+ const tddCompleted = flowState.completedStages.includes("tdd")
1344
+ || (flowState.currentStage === "review" || flowState.currentStage === "ship");
1603
1345
  checks.push({
1604
1346
  name: "state:tdd_cycle_log_exists",
1605
- ok: tddLogExists,
1606
- details: `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl must exist`
1347
+ ok: tddLogExists || !tddCompleted,
1348
+ details: tddLogExists
1349
+ ? `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl exists`
1350
+ : tddCompleted
1351
+ ? `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl must exist once TDD is complete`
1352
+ : `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl will be created when TDD evidence is generated`
1607
1353
  });
1608
- const tddCompleted = flowState.completedStages.includes("tdd")
1609
- || (flowState.currentStage === "review" || flowState.currentStage === "ship");
1610
1354
  if (tddLogExists) {
1611
1355
  const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
1612
1356
  const parsedCycles = parseTddCycleLog(tddLogRaw);
@@ -1636,7 +1380,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1636
1380
  checks.push({
1637
1381
  name: "runs:archive_root",
1638
1382
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
1639
- details: `${RUNTIME_ROOT}/runs must exist for archived feature snapshots`
1383
+ details: `${RUNTIME_ROOT}/runs must exist for archived run snapshots`
1640
1384
  });
1641
1385
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
1642
1386
  repairFeatureSystem: false