cclaw-cli 0.11.0 → 0.13.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 (67) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.d.ts +8 -0
  3. package/dist/cli.js +311 -10
  4. package/dist/config.js +19 -0
  5. package/dist/constants.d.ts +2 -2
  6. package/dist/constants.js +13 -1
  7. package/dist/content/core-agents.d.ts +44 -0
  8. package/dist/content/core-agents.js +225 -0
  9. package/dist/content/diff-command.d.ts +2 -0
  10. package/dist/content/diff-command.js +83 -0
  11. package/dist/content/doctor-references.d.ts +2 -0
  12. package/dist/content/doctor-references.js +144 -0
  13. package/dist/content/examples.js +1 -1
  14. package/dist/content/feature-command.d.ts +2 -0
  15. package/dist/content/feature-command.js +120 -0
  16. package/dist/content/harnesses-doc.d.ts +1 -0
  17. package/dist/content/harnesses-doc.js +103 -0
  18. package/dist/content/hook-events.d.ts +4 -0
  19. package/dist/content/hook-events.js +42 -0
  20. package/dist/content/hooks.js +47 -1
  21. package/dist/content/meta-skill.js +3 -2
  22. package/dist/content/next-command.js +8 -6
  23. package/dist/content/observe.d.ts +5 -1
  24. package/dist/content/observe.js +134 -2
  25. package/dist/content/protocols.js +34 -6
  26. package/dist/content/research-playbooks.d.ts +8 -0
  27. package/dist/content/research-playbooks.js +135 -0
  28. package/dist/content/retro-command.d.ts +2 -0
  29. package/dist/content/retro-command.js +77 -0
  30. package/dist/content/rewind-command.d.ts +3 -0
  31. package/dist/content/rewind-command.js +120 -0
  32. package/dist/content/skills.js +20 -0
  33. package/dist/content/stage-schema.d.ts +3 -1
  34. package/dist/content/stage-schema.js +20 -51
  35. package/dist/content/status-command.js +43 -35
  36. package/dist/content/subagents.d.ts +1 -1
  37. package/dist/content/subagents.js +23 -38
  38. package/dist/content/tdd-log-command.d.ts +2 -0
  39. package/dist/content/tdd-log-command.js +75 -0
  40. package/dist/content/templates.d.ts +1 -1
  41. package/dist/content/templates.js +84 -16
  42. package/dist/content/tree-command.d.ts +2 -0
  43. package/dist/content/tree-command.js +91 -0
  44. package/dist/delegation.d.ts +1 -0
  45. package/dist/delegation.js +27 -1
  46. package/dist/doctor-registry.d.ts +8 -0
  47. package/dist/doctor-registry.js +127 -0
  48. package/dist/doctor.d.ts +5 -0
  49. package/dist/doctor.js +261 -7
  50. package/dist/feature-system.d.ts +18 -0
  51. package/dist/feature-system.js +247 -0
  52. package/dist/flow-state.d.ts +25 -0
  53. package/dist/flow-state.js +8 -1
  54. package/dist/harness-adapters.d.ts +7 -0
  55. package/dist/harness-adapters.js +127 -13
  56. package/dist/init-detect.d.ts +2 -0
  57. package/dist/init-detect.js +45 -0
  58. package/dist/install.js +98 -3
  59. package/dist/policy.js +27 -0
  60. package/dist/runs.d.ts +33 -1
  61. package/dist/runs.js +365 -6
  62. package/dist/tdd-cycle.d.ts +22 -0
  63. package/dist/tdd-cycle.js +82 -0
  64. package/dist/types.d.ts +4 -0
  65. package/package.json +2 -1
  66. package/dist/content/agents.d.ts +0 -48
  67. package/dist/content/agents.js +0 -411
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
+ import { readConfig } from "./config.js";
4
5
  import { exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
6
+ import { HARNESS_ADAPTERS } from "./harness-adapters.js";
5
7
  import { readFlowState } from "./runs.js";
6
8
  import { stageSchema } from "./content/stage-schema.js";
7
9
  function delegationLogPath(projectRoot) {
@@ -84,11 +86,34 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
84
86
  .map((e) => `${e.agent}(runId=${e.runId ?? "unknown"})`);
85
87
  const missing = [];
86
88
  const waived = [];
89
+ const autoWaived = [];
90
+ const config = await readConfig(projectRoot).catch(() => null);
91
+ const harnesses = config?.harnesses ?? [];
92
+ const nativeDelegationUnavailable = harnesses.length > 0 &&
93
+ harnesses.every((harness) => HARNESS_ADAPTERS[harness].capabilities.nativeSubagentDispatch === "none");
87
94
  for (const agent of mandatory) {
88
95
  const rows = forRun.filter((e) => e.agent === agent);
89
96
  const ok = rows.some((e) => e.status === "completed" || e.status === "waived");
90
97
  if (!ok) {
91
- missing.push(agent);
98
+ if (nativeDelegationUnavailable) {
99
+ const existingHarnessWaiver = rows.some((e) => e.status === "waived" && e.waiverReason === "harness_limitation");
100
+ if (!existingHarnessWaiver) {
101
+ await appendDelegation(projectRoot, {
102
+ stage,
103
+ agent,
104
+ mode: "mandatory",
105
+ status: "waived",
106
+ waiverReason: "harness_limitation",
107
+ ts: new Date().toISOString(),
108
+ runId: activeRunId
109
+ });
110
+ }
111
+ waived.push(agent);
112
+ autoWaived.push(agent);
113
+ }
114
+ else {
115
+ missing.push(agent);
116
+ }
92
117
  }
93
118
  else if (rows.some((e) => e.status === "waived")) {
94
119
  waived.push(agent);
@@ -98,6 +123,7 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
98
123
  satisfied: missing.length === 0,
99
124
  missing,
100
125
  waived,
126
+ autoWaived,
101
127
  staleIgnored
102
128
  };
103
129
  }
@@ -0,0 +1,8 @@
1
+ export type DoctorSeverity = "error" | "warning" | "info";
2
+ export interface DoctorCheckMetadata {
3
+ severity: DoctorSeverity;
4
+ summary: string;
5
+ fix: string;
6
+ docRef?: string;
7
+ }
8
+ export declare function doctorCheckMetadata(checkName: string): DoctorCheckMetadata;
@@ -0,0 +1,127 @@
1
+ import { DOCTOR_REFERENCE_DIR } from "./content/doctor-references.js";
2
+ function ref(fileName) {
3
+ return `${DOCTOR_REFERENCE_DIR}/${fileName}`;
4
+ }
5
+ const RULES = [
6
+ {
7
+ test: /^gates:reconcile:writeback$/,
8
+ metadata: {
9
+ severity: "info",
10
+ summary: "Gate reconciliation status update.",
11
+ fix: "No action required unless subsequent gate checks fail.",
12
+ docRef: ref("state-and-gates.md")
13
+ }
14
+ },
15
+ {
16
+ test: /^warning:/,
17
+ metadata: {
18
+ severity: "warning",
19
+ summary: "Advisory signal; runtime can continue with caution.",
20
+ fix: "Address when possible to prevent future drift or degraded behavior.",
21
+ docRef: ref("README.md")
22
+ }
23
+ },
24
+ {
25
+ test: /^skill:.*:(max_lines|min_lines|canonical_sections)$/,
26
+ metadata: {
27
+ severity: "warning",
28
+ summary: "Stage skill quality guardrail check.",
29
+ fix: "Tune generated stage skill content and re-run `cclaw sync`.",
30
+ docRef: ref("runtime-layout.md")
31
+ }
32
+ },
33
+ {
34
+ test: /^capability:runtime:json_parser$/,
35
+ metadata: {
36
+ severity: "warning",
37
+ summary: "Optional JSON fallback parser availability.",
38
+ fix: "Install at least one of `python3` or `jq` for resilient fallback parsing.",
39
+ docRef: ref("tooling-capabilities.md")
40
+ }
41
+ },
42
+ {
43
+ test: /^capability:required:/,
44
+ metadata: {
45
+ severity: "error",
46
+ summary: "Required runtime tooling availability check.",
47
+ fix: "Install the missing required tool and re-run `cclaw doctor`.",
48
+ docRef: ref("tooling-capabilities.md")
49
+ }
50
+ },
51
+ {
52
+ test: /^(dir:|command:|utility_command:|skill:|utility_skill:|agent:|harness_tool_ref:|harness_ref:|stage_examples_ref:|doctor_ref:)/,
53
+ metadata: {
54
+ severity: "error",
55
+ summary: "Generated runtime surface presence check.",
56
+ fix: "Run `cclaw sync` to regenerate runtime files, then re-run doctor.",
57
+ docRef: ref("runtime-layout.md")
58
+ }
59
+ },
60
+ {
61
+ test: /^(hook:|lifecycle:|git_hooks:)/,
62
+ metadata: {
63
+ severity: "error",
64
+ summary: "Hook wiring and lifecycle integration check.",
65
+ fix: "Repair hook/plugin wiring (usually via `cclaw sync`) and validate harness config.",
66
+ docRef: ref("hooks-and-lifecycle.md")
67
+ }
68
+ },
69
+ {
70
+ test: /^(shim:|agents:cclaw_block|rules:cursor:workflow)/,
71
+ metadata: {
72
+ severity: "error",
73
+ summary: "Harness shim and routing file consistency check.",
74
+ fix: "Regenerate harness adapters via `cclaw sync`; confirm enabled harness list.",
75
+ docRef: ref("harness-and-routing.md")
76
+ }
77
+ },
78
+ {
79
+ test: /^(flow_state:|state:|contexts:|gates:)/,
80
+ metadata: {
81
+ severity: "error",
82
+ summary: "Flow state and gate evidence consistency check.",
83
+ fix: "Repair flow-state artifacts and gate evidence, then run `cclaw doctor --reconcile-gates`.",
84
+ docRef: ref("state-and-gates.md")
85
+ }
86
+ },
87
+ {
88
+ test: /^delegation:/,
89
+ metadata: {
90
+ severity: "error",
91
+ summary: "Mandatory delegation completion check.",
92
+ fix: "Complete or explicitly waive missing mandatory delegations in delegation log.",
93
+ docRef: ref("delegation-and-preamble.md")
94
+ }
95
+ },
96
+ {
97
+ test: /^trace:/,
98
+ metadata: {
99
+ severity: "error",
100
+ summary: "Cross-artifact traceability integrity check.",
101
+ fix: "Restore criterion/task/test ID mappings across spec, plan, and tdd artifacts.",
102
+ docRef: ref("traceability.md")
103
+ }
104
+ },
105
+ {
106
+ test: /^(config:|rules:policy_schema|language_rule_pack:|gitignore:|git:)/,
107
+ metadata: {
108
+ severity: "error",
109
+ summary: "Config or policy schema consistency check.",
110
+ fix: "Fix config/rules drift, then run `cclaw sync` and re-run doctor.",
111
+ docRef: ref("config-and-policy.md")
112
+ }
113
+ }
114
+ ];
115
+ export function doctorCheckMetadata(checkName) {
116
+ for (const rule of RULES) {
117
+ if (rule.test.test(checkName)) {
118
+ return { ...rule.metadata };
119
+ }
120
+ }
121
+ return {
122
+ severity: "error",
123
+ summary: "Doctor runtime integrity check.",
124
+ fix: "Inspect check details, apply the suggested remediation, and re-run `cclaw doctor`.",
125
+ docRef: ref("README.md")
126
+ };
127
+ }
package/dist/doctor.d.ts CHANGED
@@ -1,7 +1,12 @@
1
+ import type { DoctorSeverity } from "./doctor-registry.js";
1
2
  export interface DoctorCheck {
2
3
  name: string;
3
4
  ok: boolean;
4
5
  details: string;
6
+ severity: DoctorSeverity;
7
+ summary: string;
8
+ fix: string;
9
+ docRef?: string;
5
10
  }
6
11
  export interface DoctorOptions {
7
12
  /** When true, normalize current-stage gate catalog and persist reconciliation before checks. */
package/dist/doctor.js CHANGED
@@ -4,7 +4,7 @@ import { execFile } from "node:child_process";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { promisify } from "node:util";
6
6
  import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
7
- import { CCLAW_AGENTS } from "./content/agents.js";
7
+ import { CCLAW_AGENTS } from "./content/core-agents.js";
8
8
  import { readConfig } from "./config.js";
9
9
  import { exists } from "./fs-utils.js";
10
10
  import { gitignoreHasRequiredPatterns } from "./gitignore.js";
@@ -14,13 +14,18 @@ import { readFlowState } from "./runs.js";
14
14
  import { skippedStagesForTrack } from "./flow-state.js";
15
15
  import { TRACK_STAGES } from "./types.js";
16
16
  import { checkMandatoryDelegations } from "./delegation.js";
17
+ import { ensureFeatureSystem, featureRootPath, listFeatures, readActiveFeature } from "./feature-system.js";
17
18
  import { buildTraceMatrix } from "./trace-matrix.js";
18
19
  import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
20
+ import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
19
21
  import { stageSkillFolder } from "./content/skills.js";
22
+ import { doctorCheckMetadata } from "./doctor-registry.js";
20
23
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
21
24
  import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
25
+ import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
22
26
  import { validateHookDocument } from "./hook-schema.js";
23
27
  const execFileAsync = promisify(execFile);
28
+ const PREAMBLE_COOLDOWN_MS = 15 * 60 * 1000;
24
29
  async function isGitRepo(projectRoot) {
25
30
  try {
26
31
  await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectRoot });
@@ -345,6 +350,20 @@ export async function doctorChecks(projectRoot, options = {}) {
345
350
  details: refPath
346
351
  });
347
352
  }
353
+ checks.push({
354
+ name: "harness_ref:matrix",
355
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "references", "harnesses.md")),
356
+ details: `${RUNTIME_ROOT}/references/harnesses.md`
357
+ });
358
+ const doctorRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "doctor");
359
+ for (const fileName of Object.keys(DOCTOR_REFERENCE_MARKDOWN)) {
360
+ const refPath = path.join(doctorRefDir, fileName);
361
+ checks.push({
362
+ name: `doctor_ref:${fileName.replace(/\.md$/, "")}`,
363
+ ok: await exists(refPath),
364
+ details: refPath
365
+ });
366
+ }
348
367
  checks.push({
349
368
  name: "gitignore:required_patterns",
350
369
  ok: await gitignoreHasRequiredPatterns(projectRoot),
@@ -428,7 +447,19 @@ export async function doctorChecks(projectRoot, options = {}) {
428
447
  });
429
448
  continue;
430
449
  }
431
- for (const shim of ["cc.md", "cc-next.md", "cc-learn.md"]) {
450
+ for (const shim of [
451
+ "cc.md",
452
+ "cc-next.md",
453
+ "cc-learn.md",
454
+ "cc-status.md",
455
+ "cc-tree.md",
456
+ "cc-diff.md",
457
+ "cc-feature.md",
458
+ "cc-tdd-log.md",
459
+ "cc-retro.md",
460
+ "cc-rewind.md",
461
+ "cc-rewind-ack.md"
462
+ ]) {
432
463
  const shimPath = path.join(projectRoot, adapter.commandDir, shim);
433
464
  checks.push({
434
465
  name: `shim:${harness}:${shim.replace(".md", "")}`,
@@ -445,10 +476,32 @@ export async function doctorChecks(projectRoot, options = {}) {
445
476
  const hasCcCommand = content.includes("/cc");
446
477
  const hasCcNext = content.includes("/cc-next");
447
478
  const hasCcLearn = content.includes("/cc-learn");
479
+ const hasCcStatus = content.includes("/cc-status");
480
+ const hasCcTree = content.includes("/cc-tree");
481
+ const hasCcDiff = content.includes("/cc-diff");
482
+ const hasCcFeature = content.includes("/cc-feature");
483
+ const hasCcTddLog = content.includes("/cc-tdd-log");
484
+ const hasCcRetro = content.includes("/cc-retro");
485
+ const hasCcRewind = content.includes("/cc-rewind");
486
+ const hasCcRewindAck = content.includes("/cc-rewind-ack");
448
487
  const hasVerification = content.includes("Verification Discipline");
449
488
  const hasMinimalMarker = content.includes("intentionally minimal for cross-project use");
450
489
  const hasMetaSkillPointer = content.includes(".cclaw/skills/using-cclaw/SKILL.md");
451
- agentsBlockOk = hasMarkers && hasCcCommand && hasCcNext && hasCcLearn && hasVerification && hasMinimalMarker && hasMetaSkillPointer;
490
+ agentsBlockOk = hasMarkers
491
+ && hasCcCommand
492
+ && hasCcNext
493
+ && hasCcLearn
494
+ && hasCcStatus
495
+ && hasCcTree
496
+ && hasCcDiff
497
+ && hasCcFeature
498
+ && hasCcTddLog
499
+ && hasCcRetro
500
+ && hasCcRewind
501
+ && hasCcRewindAck
502
+ && hasVerification
503
+ && hasMinimalMarker
504
+ && hasMetaSkillPointer;
452
505
  }
453
506
  checks.push({
454
507
  name: "agents:cclaw_block",
@@ -456,7 +509,7 @@ export async function doctorChecks(projectRoot, options = {}) {
456
509
  details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
457
510
  });
458
511
  // Utility commands
459
- for (const cmd of ["learn"]) {
512
+ for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind", "rewind-ack"]) {
460
513
  const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
461
514
  checks.push({
462
515
  name: `utility_command:${cmd}`,
@@ -467,6 +520,12 @@ export async function doctorChecks(projectRoot, options = {}) {
467
520
  // Utility skills
468
521
  for (const [folder, label] of [
469
522
  ["learnings", "learnings"],
523
+ ["flow-tree", "flow-tree"],
524
+ ["flow-diff", "flow-diff"],
525
+ ["feature-workspaces", "feature-workspaces"],
526
+ ["tdd-cycle-log", "tdd-cycle-log"],
527
+ ["flow-retro", "flow-retro"],
528
+ ["flow-rewind", "flow-rewind"],
470
529
  ["subagent-dev", "sdd"],
471
530
  ["parallel-dispatch", "parallel-agents"],
472
531
  ["session", "session"],
@@ -810,6 +869,11 @@ export async function doctorChecks(projectRoot, options = {}) {
810
869
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
811
870
  details: `${RUNTIME_ROOT}/state/suggestion-memory.json must exist for proactive suggestion memory`
812
871
  });
872
+ checks.push({
873
+ name: "state:harness_gaps_exists",
874
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "harness-gaps.json")),
875
+ details: `${RUNTIME_ROOT}/state/harness-gaps.json must exist for tiered harness capability tracking`
876
+ });
813
877
  const contextModeStatePath = path.join(projectRoot, RUNTIME_ROOT, "state", "context-mode.json");
814
878
  checks.push({
815
879
  name: "state:context_mode_exists",
@@ -840,6 +904,83 @@ export async function doctorChecks(projectRoot, options = {}) {
840
904
  details: modePath
841
905
  });
842
906
  }
907
+ const preambleLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "preamble-log.jsonl");
908
+ const preambleLogExists = await exists(preambleLogPath);
909
+ checks.push({
910
+ name: "state:preamble_log_exists",
911
+ ok: preambleLogExists,
912
+ details: `${RUNTIME_ROOT}/state/preamble-log.jsonl must exist for preamble budget tracking`
913
+ });
914
+ if (preambleLogExists) {
915
+ let duplicateHits = 0;
916
+ let parsedEntries = 0;
917
+ let malformedEntries = 0;
918
+ try {
919
+ const now = Date.now();
920
+ const byKey = new Map();
921
+ const raw = await fs.readFile(preambleLogPath, "utf8");
922
+ const lines = raw
923
+ .split("\n")
924
+ .map((line) => line.trim())
925
+ .filter((line) => line.length > 0);
926
+ for (const line of lines) {
927
+ try {
928
+ const parsed = JSON.parse(line);
929
+ const tsRaw = parsed.ts;
930
+ const stageRaw = parsed.stage;
931
+ const triggerRaw = parsed.trigger;
932
+ const hashRaw = parsed.hash;
933
+ if (typeof tsRaw !== "string" ||
934
+ typeof stageRaw !== "string" ||
935
+ typeof triggerRaw !== "string" ||
936
+ typeof hashRaw !== "string") {
937
+ malformedEntries += 1;
938
+ continue;
939
+ }
940
+ const stamp = Date.parse(tsRaw);
941
+ if (!Number.isFinite(stamp)) {
942
+ malformedEntries += 1;
943
+ continue;
944
+ }
945
+ if (now - stamp > 24 * 60 * 60 * 1000) {
946
+ continue;
947
+ }
948
+ parsedEntries += 1;
949
+ const key = `${stageRaw}|${triggerRaw}|${hashRaw}`;
950
+ const bucket = byKey.get(key) ?? [];
951
+ bucket.push(stamp);
952
+ byKey.set(key, bucket);
953
+ }
954
+ catch {
955
+ malformedEntries += 1;
956
+ }
957
+ }
958
+ for (const stamps of byKey.values()) {
959
+ stamps.sort((a, b) => a - b);
960
+ for (let i = 1; i < stamps.length; i += 1) {
961
+ if (stamps[i] - stamps[i - 1] < PREAMBLE_COOLDOWN_MS) {
962
+ duplicateHits += 1;
963
+ }
964
+ }
965
+ }
966
+ }
967
+ catch {
968
+ malformedEntries += 1;
969
+ }
970
+ checks.push({
971
+ name: "warning:preamble:dedup",
972
+ ok: true,
973
+ details: duplicateHits > 0
974
+ ? `warning: detected ${duplicateHits} repeated preamble emission(s) inside ${Math.floor(PREAMBLE_COOLDOWN_MS / 60000)}m cooldown window`
975
+ : parsedEntries > 0
976
+ ? `preamble budget healthy (${parsedEntries} recent preamble entry/entries checked)`
977
+ : malformedEntries > 0
978
+ ? `warning: preamble log exists but entries are malformed (${malformedEntries} line(s) ignored)`
979
+ : "preamble log is empty; no recent preamble emissions recorded"
980
+ });
981
+ }
982
+ await ensureFeatureSystem(projectRoot);
983
+ const activeFeature = await readActiveFeature(projectRoot);
843
984
  let flowState = await readFlowState(projectRoot);
844
985
  if (options.reconcileCurrentStageGates === true) {
845
986
  const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
@@ -894,6 +1035,108 @@ export async function doctorChecks(projectRoot, options = {}) {
894
1035
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "artifacts")),
895
1036
  details: `${RUNTIME_ROOT}/artifacts must exist as the active artifact root`
896
1037
  });
1038
+ const features = await listFeatures(projectRoot);
1039
+ checks.push({
1040
+ name: "state:active_feature_meta",
1041
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json")),
1042
+ details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
1043
+ });
1044
+ checks.push({
1045
+ name: "state:active_feature_exists",
1046
+ ok: features.includes(activeFeature),
1047
+ details: features.includes(activeFeature)
1048
+ ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/features`
1049
+ : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/features`
1050
+ });
1051
+ checks.push({
1052
+ name: "state:features_nonempty",
1053
+ ok: features.length > 0,
1054
+ details: features.length > 0
1055
+ ? `${features.length} feature snapshot(s): ${features.join(", ")}`
1056
+ : `no feature snapshots found under ${RUNTIME_ROOT}/features`
1057
+ });
1058
+ checks.push({
1059
+ name: "state:active_feature_snapshot_dirs",
1060
+ ok: await exists(path.join(featureRootPath(projectRoot, activeFeature), "artifacts")) &&
1061
+ await exists(path.join(featureRootPath(projectRoot, activeFeature), "state")),
1062
+ details: `${RUNTIME_ROOT}/features/${activeFeature}/artifacts and /state must exist`
1063
+ });
1064
+ const staleStages = Object.keys(flowState.staleStages).filter((value) => COMMAND_FILE_ORDER.includes(value));
1065
+ checks.push({
1066
+ name: "state:stale_stages_resolved",
1067
+ ok: staleStages.length === 0,
1068
+ details: staleStages.length === 0
1069
+ ? "no stale stages pending acknowledgement"
1070
+ : `stale stages must be acknowledged via /cc-rewind-ack: ${staleStages.join(", ")}`
1071
+ });
1072
+ const retroRequired = flowState.completedStages.includes("ship");
1073
+ const retroComplete = !retroRequired ||
1074
+ (typeof flowState.retro.completedAt === "string" && flowState.retro.compoundEntries > 0);
1075
+ checks.push({
1076
+ name: "state:retro_gate",
1077
+ ok: retroComplete,
1078
+ details: retroComplete
1079
+ ? retroRequired
1080
+ ? `retro gate complete (${flowState.retro.compoundEntries} compound entries)`
1081
+ : "retro gate not required yet (ship not completed)"
1082
+ : "retro gate incomplete: run /cc-retro and record at least one compound knowledge entry"
1083
+ });
1084
+ const flowSnapshotPath = path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.snapshot.json");
1085
+ const flowSnapshotExists = await exists(flowSnapshotPath);
1086
+ let flowSnapshotValid = flowSnapshotExists;
1087
+ if (flowSnapshotExists) {
1088
+ try {
1089
+ JSON.parse(await fs.readFile(flowSnapshotPath, "utf8"));
1090
+ flowSnapshotValid = true;
1091
+ }
1092
+ catch {
1093
+ flowSnapshotValid = false;
1094
+ }
1095
+ }
1096
+ checks.push({
1097
+ name: "state:flow_snapshot",
1098
+ ok: flowSnapshotExists && flowSnapshotValid,
1099
+ details: flowSnapshotExists
1100
+ ? flowSnapshotValid
1101
+ ? `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists and is valid JSON`
1102
+ : `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists but is invalid JSON`
1103
+ : `${RUNTIME_ROOT}/state/flow-state.snapshot.json is missing`
1104
+ });
1105
+ const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
1106
+ const tddLogExists = await exists(tddLogPath);
1107
+ checks.push({
1108
+ name: "state:tdd_cycle_log_exists",
1109
+ ok: tddLogExists,
1110
+ details: `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl must exist`
1111
+ });
1112
+ const tddCompleted = flowState.completedStages.includes("tdd")
1113
+ || (flowState.currentStage === "review" || flowState.currentStage === "ship");
1114
+ if (tddLogExists) {
1115
+ const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
1116
+ const parsedCycles = parseTddCycleLog(tddLogRaw);
1117
+ const validation = validateTddCycleOrder(parsedCycles, { runId: activeRunId || undefined });
1118
+ const hasCoverage = validation.sliceCount > 0;
1119
+ checks.push({
1120
+ name: "state:tdd_cycle_order",
1121
+ ok: validation.ok && (!tddCompleted || hasCoverage),
1122
+ details: validation.ok
1123
+ ? tddCompleted && !hasCoverage
1124
+ ? "tdd stage complete but no RED/GREEN cycle evidence logged"
1125
+ : `tdd cycle log valid (${validation.sliceCount} slice(s), open_red=${validation.openRedSlices.length})`
1126
+ : `tdd cycle order issues: ${validation.issues.join("; ")}${validation.openRedSlices.length > 0
1127
+ ? ` | open red slices: ${validation.openRedSlices.join(", ")}`
1128
+ : ""}`
1129
+ });
1130
+ }
1131
+ else {
1132
+ checks.push({
1133
+ name: "state:tdd_cycle_order",
1134
+ ok: !tddCompleted,
1135
+ details: tddCompleted
1136
+ ? "tdd stage complete but tdd-cycle-log.jsonl is missing"
1137
+ : "tdd cycle order deferred until tdd stage evidence is generated"
1138
+ });
1139
+ }
897
1140
  checks.push({
898
1141
  name: "runs:archive_root",
899
1142
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
@@ -911,7 +1154,9 @@ export async function doctorChecks(projectRoot, options = {}) {
911
1154
  name: "warning:delegation:waived",
912
1155
  ok: true,
913
1156
  details: delegation.waived.length > 0
914
- ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}`
1157
+ ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}${delegation.autoWaived.length > 0
1158
+ ? ` (auto-waived due to harness limitation: ${delegation.autoWaived.join(", ")})`
1159
+ : ""}`
915
1160
  : "no waived mandatory delegations for current stage"
916
1161
  });
917
1162
  checks.push({
@@ -1025,8 +1270,17 @@ export async function doctorChecks(projectRoot, options = {}) {
1025
1270
  });
1026
1271
  const policy = await policyChecks(projectRoot, { harnesses: configuredHarnesses });
1027
1272
  checks.push(...policy);
1028
- return checks;
1273
+ return checks.map((check) => {
1274
+ const metadata = doctorCheckMetadata(check.name);
1275
+ return {
1276
+ ...check,
1277
+ severity: check.severity ?? metadata.severity,
1278
+ summary: check.summary ?? metadata.summary,
1279
+ fix: check.fix ?? metadata.fix,
1280
+ docRef: check.docRef ?? metadata.docRef
1281
+ };
1282
+ });
1029
1283
  }
1030
1284
  export function doctorSucceeded(checks) {
1031
- return checks.every((check) => check.ok);
1285
+ return checks.every((check) => check.ok || check.severity !== "error");
1032
1286
  }
@@ -0,0 +1,18 @@
1
+ export interface ActiveFeatureMeta {
2
+ activeFeature: string;
3
+ updatedAt: string;
4
+ }
5
+ export declare function activeFeatureMetaPath(projectRoot: string): string;
6
+ export declare function featureRootPath(projectRoot: string, featureId: string): string;
7
+ export declare function featureArtifactsPath(projectRoot: string, featureId: string): string;
8
+ export declare function featureStatePath(projectRoot: string, featureId: string): string;
9
+ export declare function readActiveFeature(projectRoot: string): Promise<string>;
10
+ export declare function listFeatures(projectRoot: string): Promise<string[]>;
11
+ export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
12
+ export declare function syncActiveFeatureSnapshot(projectRoot: string): Promise<void>;
13
+ export declare function switchActiveFeature(projectRoot: string, featureId: string): Promise<ActiveFeatureMeta>;
14
+ export interface CreateFeatureOptions {
15
+ cloneActive?: boolean;
16
+ switchTo?: boolean;
17
+ }
18
+ export declare function createFeature(projectRoot: string, rawFeatureId: string, options?: CreateFeatureOptions): Promise<string>;