cclaw-cli 0.15.0 → 0.18.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 (51) hide show
  1. package/dist/artifact-linter.js +154 -0
  2. package/dist/cli.js +2 -1
  3. package/dist/constants.d.ts +2 -2
  4. package/dist/constants.js +2 -3
  5. package/dist/content/contracts.js +1 -1
  6. package/dist/content/doctor-references.js +7 -6
  7. package/dist/content/feature-command.js +54 -51
  8. package/dist/content/harnesses-doc.js +2 -2
  9. package/dist/content/hooks.js +2 -2
  10. package/dist/content/learnings.d.ts +1 -1
  11. package/dist/content/learnings.js +22 -5
  12. package/dist/content/meta-skill.js +2 -2
  13. package/dist/content/next-command.js +2 -2
  14. package/dist/content/observe.js +3 -2
  15. package/dist/content/ops-command.js +1 -3
  16. package/dist/content/protocols.js +6 -34
  17. package/dist/content/rewind-command.d.ts +0 -1
  18. package/dist/content/rewind-command.js +19 -33
  19. package/dist/content/skills.js +2 -3
  20. package/dist/content/stage-schema.d.ts +2 -92
  21. package/dist/content/stage-schema.js +10 -1379
  22. package/dist/content/stages/brainstorm.d.ts +2 -0
  23. package/dist/content/stages/brainstorm.js +136 -0
  24. package/dist/content/stages/design.d.ts +2 -0
  25. package/dist/content/stages/design.js +215 -0
  26. package/dist/content/stages/index.d.ts +8 -0
  27. package/dist/content/stages/index.js +11 -0
  28. package/dist/content/stages/plan.d.ts +2 -0
  29. package/dist/content/stages/plan.js +157 -0
  30. package/dist/content/stages/review.d.ts +2 -0
  31. package/dist/content/stages/review.js +197 -0
  32. package/dist/content/stages/schema-types.d.ts +94 -0
  33. package/dist/content/stages/schema-types.js +1 -0
  34. package/dist/content/stages/scope.d.ts +2 -0
  35. package/dist/content/stages/scope.js +194 -0
  36. package/dist/content/stages/ship.d.ts +2 -0
  37. package/dist/content/stages/ship.js +142 -0
  38. package/dist/content/stages/spec.d.ts +2 -0
  39. package/dist/content/stages/spec.js +136 -0
  40. package/dist/content/stages/tdd.d.ts +2 -0
  41. package/dist/content/stages/tdd.js +185 -0
  42. package/dist/content/templates.js +105 -9
  43. package/dist/content/utility-skills.js +1 -1
  44. package/dist/delegation.d.ts +33 -3
  45. package/dist/delegation.js +56 -3
  46. package/dist/doctor.js +147 -88
  47. package/dist/feature-system.d.ts +22 -5
  48. package/dist/feature-system.js +267 -126
  49. package/dist/install.js +4 -8
  50. package/dist/policy.js +3 -4
  51. package/package.json +1 -1
@@ -1,7 +1,16 @@
1
1
  import { COMMAND_FILE_ORDER } from "../constants.js";
2
2
  import { orderedStageSchemas } from "./stage-schema.js";
3
3
  export const ARTIFACT_TEMPLATES = {
4
- "01-brainstorm.md": `# Brainstorm Artifact
4
+ "01-brainstorm.md": `---
5
+ stage: brainstorm
6
+ schema_version: 1
7
+ version: 0.18.0
8
+ feature: <feature-id>
9
+ locked_decisions: []
10
+ inputs_hash: sha256:pending
11
+ ---
12
+
13
+ # Brainstorm Artifact
5
14
 
6
15
  ## Context
7
16
  - **Project state:**
@@ -37,7 +46,16 @@ export const ARTIFACT_TEMPLATES = {
37
46
  - **Assumptions:**
38
47
  - **Open questions (or "None"):**
39
48
  `,
40
- "02-scope.md": `# Scope Artifact
49
+ "02-scope.md": `---
50
+ stage: scope
51
+ schema_version: 1
52
+ version: 0.18.0
53
+ feature: <feature-id>
54
+ locked_decisions: []
55
+ inputs_hash: sha256:pending
56
+ ---
57
+
58
+ # Scope Artifact
41
59
 
42
60
  ## Prime Directives
43
61
  - Zero silent failures:
@@ -94,6 +112,11 @@ export const ARTIFACT_TEMPLATES = {
94
112
  > is later dropped, keep the row and mark Priority \`DROPPED\`; if a new one is
95
113
  > added mid-flow, append with the next free R-number — do NOT reuse numbers.
96
114
 
115
+ ## Locked Decisions (D-XX)
116
+ | Decision ID | Decision | Why locked now | Downstream impact |
117
+ |---|---|---|---|
118
+ | D-01 | | | |
119
+
97
120
  ## In Scope / Out of Scope
98
121
 
99
122
  ### In Scope
@@ -126,7 +149,16 @@ export const ARTIFACT_TEMPLATES = {
126
149
  - Deferred:
127
150
  - Explicitly excluded:
128
151
  `,
129
- "03-design.md": `# Design Artifact
152
+ "03-design.md": `---
153
+ stage: design
154
+ schema_version: 1
155
+ version: 0.18.0
156
+ feature: <feature-id>
157
+ locked_decisions: []
158
+ inputs_hash: sha256:pending
159
+ ---
160
+
161
+ # Design Artifact
130
162
 
131
163
  ## Codebase Investigation
132
164
  | File | Current responsibility | Patterns discovered |
@@ -210,7 +242,16 @@ export const ARTIFACT_TEMPLATES = {
210
242
 
211
243
  **Decisions made:** 0 | **Unresolved:** 0
212
244
  `,
213
- "04-spec.md": `# Specification Artifact
245
+ "04-spec.md": `---
246
+ stage: spec
247
+ schema_version: 1
248
+ version: 0.18.0
249
+ feature: <feature-id>
250
+ locked_decisions: []
251
+ inputs_hash: sha256:pending
252
+ ---
253
+
254
+ # Specification Artifact
214
255
 
215
256
  ## Acceptance Criteria
216
257
  | ID | Requirement Ref (R#) | Criterion (observable/measurable/falsifiable) | Design Decision Ref |
@@ -254,7 +295,16 @@ export const ARTIFACT_TEMPLATES = {
254
295
  - Approved by:
255
296
  - Date:
256
297
  `,
257
- "05-plan.md": `# Plan Artifact
298
+ "05-plan.md": `---
299
+ stage: plan
300
+ schema_version: 1
301
+ version: 0.18.0
302
+ feature: <feature-id>
303
+ locked_decisions: []
304
+ inputs_hash: sha256:pending
305
+ ---
306
+
307
+ # Plan Artifact
258
308
 
259
309
  ## Dependency Graph
260
310
  -
@@ -282,6 +332,7 @@ Execution rule: complete and verify each wave before starting the next wave.
282
332
  **Rules (apply before writing rows):**
283
333
  - Every task fits the **2-5 minute budget**. If \`[~Nm]\` is >5, split the task.
284
334
  - **No placeholders.** Forbidden tokens anywhere in this table: \`TODO\`, \`TBD\`, \`FIXME\`, \`<fill-in>\`, \`<your-*-here>\`, \`xxx\`, bare ellipsis. Every file path, test, and verification command must be copy-pasteable as written.
335
+ - **No silent scope reduction.** Forbidden phrasing when locked decisions exist: \`v1\`, \`for now\`, \`later\`, \`temporary\`, \`placeholder\`, \`mock for now\`, \`hardcoded for now\`, \`will improve later\`.
285
336
  - If an estimate is genuinely uncertain (new library, unfamiliar subsystem), add a **spike task in wave 0** to de-risk — do NOT hide the uncertainty inside a large estimate.
286
337
 
287
338
  | Task ID | Description | Acceptance criterion | Verification command | Effort (S/M/L) | Minutes |
@@ -293,6 +344,11 @@ Execution rule: complete and verify each wave before starting the next wave.
293
344
  |---|---|
294
345
  | AC-1 | T-1 |
295
346
 
347
+ ## Locked Decision Coverage
348
+ | Decision ID | Source section | Plan tasks implementing decision | Status |
349
+ |---|---|---|---|
350
+ | D-01 | 02-scope.md > Locked Decisions | T-1 | covered |
351
+
296
352
  ## Risk Assessment
297
353
  | Task/Wave | Risk | Likelihood | Impact | Mitigation |
298
354
  |---|---|---|---|---|
@@ -307,11 +363,24 @@ Execution rule: complete and verify each wave before starting the next wave.
307
363
  - Scanned tokens: \`TODO\`, \`TBD\`, \`FIXME\`, \`<fill-in>\`, \`<your-*-here>\`, \`xxx\`, bare ellipsis in task rows.
308
364
  - Hits: 0 (required for WAIT_FOR_CONFIRM to resolve).
309
365
 
366
+ ## No Scope Reduction Language Scan
367
+ - Scanned phrases: \`v1\`, \`for now\`, \`later\`, \`temporary\`, \`placeholder\`, \`mock for now\`, \`hardcoded for now\`, \`will improve later\`.
368
+ - Hits: 0 (required when Locked Decisions section is non-empty).
369
+
310
370
  ## WAIT_FOR_CONFIRM
311
371
  - Status: pending
312
372
  - Confirmed by:
313
373
  `,
314
- "06-tdd.md": `# TDD Artifact
374
+ "06-tdd.md": `---
375
+ stage: tdd
376
+ schema_version: 1
377
+ version: 0.18.0
378
+ feature: <feature-id>
379
+ locked_decisions: []
380
+ inputs_hash: sha256:pending
381
+ ---
382
+
383
+ # TDD Artifact
315
384
 
316
385
  ## RED Evidence
317
386
  | Slice | Test name | Command | Failure output summary |
@@ -366,7 +435,16 @@ Execution rule: complete and verify each wave before starting the next wave.
366
435
  |---|---|---|---|---|
367
436
  | S-1 | | | | |
368
437
  `,
369
- "07-review.md": `# Review Artifact
438
+ "07-review.md": `---
439
+ stage: review
440
+ schema_version: 1
441
+ version: 0.18.0
442
+ feature: <feature-id>
443
+ locked_decisions: []
444
+ inputs_hash: sha256:pending
445
+ ---
446
+
447
+ # Review Artifact
370
448
 
371
449
  ## Layer 1 Verdict
372
450
  | Criterion | Verdict | Evidence |
@@ -444,7 +522,16 @@ Execution rule: complete and verify each wave before starting the next wave.
444
522
  }
445
523
  }
446
524
  `,
447
- "08-ship.md": `# Ship Artifact
525
+ "08-ship.md": `---
526
+ stage: ship
527
+ schema_version: 1
528
+ version: 0.18.0
529
+ feature: <feature-id>
530
+ locked_decisions: []
531
+ inputs_hash: sha256:pending
532
+ ---
533
+
534
+ # Ship Artifact
448
535
 
449
536
  ## Preflight Results
450
537
  - Review verdict:
@@ -485,7 +572,16 @@ Execution rule: complete and verify each wave before starting the next wave.
485
572
  - Retro artifact path: \`.cclaw/artifacts/09-retro.md\`
486
573
  - Archive remains blocked until retro gate is complete.
487
574
  `,
488
- "09-retro.md": `# Retro Artifact
575
+ "09-retro.md": `---
576
+ stage: retro
577
+ schema_version: 1
578
+ version: 0.18.0
579
+ feature: <feature-id>
580
+ locked_decisions: []
581
+ inputs_hash: sha256:pending
582
+ ---
583
+
584
+ # Retro Artifact
489
585
 
490
586
  ## Run Summary
491
587
  - Flow track:
@@ -1271,7 +1271,7 @@ For each lens, write either a knowledge entry **or** the explicit string
1271
1271
  ## Output protocol
1272
1272
 
1273
1273
  For every harvested insight, append one strict-schema JSON line to
1274
- \`.cclaw/knowledge.jsonl\` (fields: \`type, trigger, action, confidence, domain, stage, created, project\`).
1274
+ \`.cclaw/knowledge.jsonl\` (fields: \`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`).
1275
1275
  See the \`learnings\` skill for the canonical shape. Choose \`type\`:
1276
1276
 
1277
1277
  - \`compound\` for process/speed accelerators.
@@ -1,12 +1,34 @@
1
1
  import type { FlowStage } from "./types.js";
2
+ export type DelegationMode = "mandatory" | "proactive" | "conditional";
3
+ export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
4
+ export interface DelegationTokenUsage {
5
+ input: number;
6
+ output: number;
7
+ model: string;
8
+ }
2
9
  export type DelegationEntry = {
3
10
  stage: string;
4
11
  agent: string;
5
- mode: "mandatory" | "proactive" | "conditional";
6
- status: "scheduled" | "completed" | "failed" | "waived";
12
+ mode: DelegationMode;
13
+ status: DelegationStatus;
14
+ /**
15
+ * Span identifier for this delegation unit. Multiple status transitions for
16
+ * the same delegated unit should reuse the same spanId.
17
+ */
18
+ spanId?: string;
19
+ /** Parent span id when this delegation was spawned from another span. */
20
+ parentSpanId?: string;
21
+ /** ISO timestamp when the delegation span started. */
22
+ startTs?: string;
23
+ /** ISO timestamp when the delegation span ended (for terminal statuses). */
24
+ endTs?: string;
25
+ /**
26
+ * Legacy timestamp used by historical ledgers. New writers set both `ts` and
27
+ * `startTs` for backward compatibility.
28
+ */
7
29
  taskId?: string;
8
30
  waiverReason?: string;
9
- ts: string;
31
+ ts?: string;
10
32
  /**
11
33
  * Run id the entry belongs to. Older ledgers written before 0.5.17 may omit this;
12
34
  * consumers treat missing runId as unscoped (conservatively excluded from current-run checks).
@@ -17,6 +39,14 @@ export type DelegationEntry = {
17
39
  * Recorded for audit so reviewers can see why the second pass was required.
18
40
  */
19
41
  conditionTrigger?: string;
42
+ /** Optional token usage captured from the delegated run. */
43
+ tokens?: DelegationTokenUsage;
44
+ /** Number of retries attempted for this span. */
45
+ retryCount?: number;
46
+ /** Optional references to evidence anchors in artifacts. */
47
+ evidenceRefs?: string[];
48
+ /** Schema version marker for span-compatible delegation logs. */
49
+ schemaVersion?: 1;
20
50
  };
21
51
  export type DelegationLedger = {
22
52
  runId: string;
@@ -12,6 +12,20 @@ function delegationLogPath(projectRoot) {
12
12
  function delegationLockPath(projectRoot) {
13
13
  return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
14
14
  }
15
+ function createSpanId() {
16
+ return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
17
+ }
18
+ function isDelegationTokenUsage(value) {
19
+ if (!value || typeof value !== "object" || Array.isArray(value))
20
+ return false;
21
+ const o = value;
22
+ return (typeof o.input === "number" &&
23
+ Number.isFinite(o.input) &&
24
+ typeof o.output === "number" &&
25
+ Number.isFinite(o.output) &&
26
+ typeof o.model === "string" &&
27
+ o.model.trim().length > 0);
28
+ }
15
29
  function isDelegationEntry(value) {
16
30
  if (!value || typeof value !== "object" || Array.isArray(value))
17
31
  return false;
@@ -21,15 +35,30 @@ function isDelegationEntry(value) {
21
35
  o.status === "completed" ||
22
36
  o.status === "failed" ||
23
37
  o.status === "waived";
38
+ const timestampOk = typeof o.ts === "string" ||
39
+ typeof o.startTs === "string";
40
+ const retryOk = o.retryCount === undefined ||
41
+ (typeof o.retryCount === "number" &&
42
+ Number.isFinite(o.retryCount) &&
43
+ Number.isInteger(o.retryCount) &&
44
+ o.retryCount >= 0);
24
45
  return (typeof o.stage === "string" &&
25
46
  typeof o.agent === "string" &&
26
47
  modeOk &&
27
48
  statusOk &&
28
- typeof o.ts === "string" &&
49
+ timestampOk &&
50
+ (o.spanId === undefined || typeof o.spanId === "string") &&
51
+ (o.parentSpanId === undefined || typeof o.parentSpanId === "string") &&
52
+ (o.startTs === undefined || typeof o.startTs === "string") &&
53
+ (o.endTs === undefined || typeof o.endTs === "string") &&
29
54
  (o.taskId === undefined || typeof o.taskId === "string") &&
30
55
  (o.waiverReason === undefined || typeof o.waiverReason === "string") &&
31
56
  (o.runId === undefined || typeof o.runId === "string") &&
32
- (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string"));
57
+ (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
58
+ (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
59
+ retryOk &&
60
+ (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
61
+ (o.schemaVersion === undefined || o.schemaVersion === 1));
33
62
  }
34
63
  function parseLedger(raw, runId) {
35
64
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -41,7 +70,18 @@ function parseLedger(raw, runId) {
41
70
  if (Array.isArray(entriesRaw)) {
42
71
  for (const item of entriesRaw) {
43
72
  if (isDelegationEntry(item)) {
44
- entries.push(item);
73
+ const ts = item.startTs ?? item.ts ?? new Date().toISOString();
74
+ entries.push({
75
+ ...item,
76
+ spanId: item.spanId ?? createSpanId(),
77
+ startTs: ts,
78
+ ts,
79
+ retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
80
+ ? item.retryCount
81
+ : 0,
82
+ evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs : [],
83
+ schemaVersion: 1
84
+ });
45
85
  }
46
86
  }
47
87
  }
@@ -67,7 +107,20 @@ export async function appendDelegation(projectRoot, entry) {
67
107
  await withDirectoryLock(delegationLockPath(projectRoot), async () => {
68
108
  const filePath = delegationLogPath(projectRoot);
69
109
  const prior = await readDelegationLedger(projectRoot);
110
+ const startTs = entry.startTs ?? entry.ts ?? new Date().toISOString();
70
111
  const stamped = { ...entry, runId: entry.runId ?? activeRunId };
112
+ stamped.spanId = entry.spanId ?? createSpanId();
113
+ stamped.startTs = startTs;
114
+ stamped.ts = startTs;
115
+ stamped.schemaVersion = 1;
116
+ if (stamped.retryCount === undefined ||
117
+ !Number.isInteger(stamped.retryCount) ||
118
+ stamped.retryCount < 0) {
119
+ stamped.retryCount = 0;
120
+ }
121
+ if (!Array.isArray(stamped.evidenceRefs)) {
122
+ stamped.evidenceRefs = [];
123
+ }
71
124
  const ledger = {
72
125
  runId: activeRunId,
73
126
  entries: [...prior.entries, stamped]
package/dist/doctor.js CHANGED
@@ -14,7 +14,7 @@ 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
+ import { ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
18
18
  import { buildTraceMatrix } from "./trace-matrix.js";
19
19
  import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
20
20
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
@@ -25,7 +25,6 @@ import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
25
25
  import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
26
26
  import { validateHookDocument } from "./hook-schema.js";
27
27
  const execFileAsync = promisify(execFile);
28
- const PREAMBLE_COOLDOWN_MS = 15 * 60 * 1000;
29
28
  async function isGitRepo(projectRoot) {
30
29
  try {
31
30
  await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectRoot });
@@ -485,7 +484,7 @@ export async function doctorChecks(projectRoot, options = {}) {
485
484
  details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
486
485
  });
487
486
  // Utility commands
488
- for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind", "rewind-ack"]) {
487
+ for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind"]) {
489
488
  const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
490
489
  checks.push({
491
490
  name: `utility_command:${cmd}`,
@@ -498,7 +497,7 @@ export async function doctorChecks(projectRoot, options = {}) {
498
497
  ["learnings", "learnings"],
499
498
  ["flow-tree", "flow-tree"],
500
499
  ["flow-diff", "flow-diff"],
501
- ["feature-workspaces", "feature-workspaces"],
500
+ ["using-git-worktrees", "using-git-worktrees"],
502
501
  ["tdd-cycle-log", "tdd-cycle-log"],
503
502
  ["flow-retro", "flow-retro"],
504
503
  ["flow-rewind", "flow-rewind"],
@@ -830,6 +829,72 @@ export async function doctorChecks(projectRoot, options = {}) {
830
829
  ? `legacy ${RUNTIME_ROOT}/knowledge.md must be removed — cclaw is JSONL-native`
831
830
  : `no legacy markdown store present`
832
831
  });
832
+ const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
833
+ if (await exists(knowledgePath)) {
834
+ let malformedKnowledgeLines = 0;
835
+ let missingSchemaV2Fields = 0;
836
+ let parsedKnowledgeLines = 0;
837
+ const requiredV2Fields = [
838
+ "type",
839
+ "trigger",
840
+ "action",
841
+ "confidence",
842
+ "domain",
843
+ "stage",
844
+ "origin_stage",
845
+ "origin_feature",
846
+ "frequency",
847
+ "universality",
848
+ "maturity",
849
+ "created",
850
+ "first_seen_ts",
851
+ "last_seen_ts",
852
+ "project"
853
+ ];
854
+ try {
855
+ const raw = await fs.readFile(knowledgePath, "utf8");
856
+ const lines = raw
857
+ .split("\n")
858
+ .map((line) => line.trim())
859
+ .filter((line) => line.length > 0);
860
+ for (const line of lines) {
861
+ try {
862
+ const parsed = JSON.parse(line);
863
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
864
+ malformedKnowledgeLines += 1;
865
+ continue;
866
+ }
867
+ parsedKnowledgeLines += 1;
868
+ const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
869
+ if (missing) {
870
+ missingSchemaV2Fields += 1;
871
+ }
872
+ }
873
+ catch {
874
+ malformedKnowledgeLines += 1;
875
+ }
876
+ }
877
+ }
878
+ catch {
879
+ malformedKnowledgeLines += 1;
880
+ }
881
+ checks.push({
882
+ name: "knowledge:jsonl_parseable",
883
+ ok: malformedKnowledgeLines === 0,
884
+ details: malformedKnowledgeLines === 0
885
+ ? "knowledge.jsonl lines parse as JSON objects"
886
+ : `knowledge.jsonl contains ${malformedKnowledgeLines} malformed line(s)`
887
+ });
888
+ checks.push({
889
+ name: "warning:knowledge:schema_v2_fields",
890
+ ok: true,
891
+ details: parsedKnowledgeLines === 0
892
+ ? "knowledge.jsonl is empty"
893
+ : missingSchemaV2Fields === 0
894
+ ? `all ${parsedKnowledgeLines} knowledge line(s) include schema v2 fields`
895
+ : `warning: ${missingSchemaV2Fields}/${parsedKnowledgeLines} knowledge line(s) miss schema v2 fields (origin/maturity/frequency metadata)`
896
+ });
897
+ }
833
898
  checks.push({
834
899
  name: "state:checkpoint_exists",
835
900
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "checkpoint.json")),
@@ -840,6 +905,54 @@ export async function doctorChecks(projectRoot, options = {}) {
840
905
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl")),
841
906
  details: `${RUNTIME_ROOT}/state/stage-activity.jsonl must exist`
842
907
  });
908
+ const stageActivityPath = path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl");
909
+ if (await exists(stageActivityPath)) {
910
+ let malformedActivityLines = 0;
911
+ let missingSchemaVersion = 0;
912
+ let parsedActivityLines = 0;
913
+ try {
914
+ const raw = await fs.readFile(stageActivityPath, "utf8");
915
+ const lines = raw
916
+ .split("\n")
917
+ .map((line) => line.trim())
918
+ .filter((line) => line.length > 0);
919
+ for (const line of lines) {
920
+ try {
921
+ const parsed = JSON.parse(line);
922
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
923
+ malformedActivityLines += 1;
924
+ continue;
925
+ }
926
+ parsedActivityLines += 1;
927
+ if (parsed.schemaVersion !== 1) {
928
+ missingSchemaVersion += 1;
929
+ }
930
+ }
931
+ catch {
932
+ malformedActivityLines += 1;
933
+ }
934
+ }
935
+ }
936
+ catch {
937
+ malformedActivityLines += 1;
938
+ }
939
+ checks.push({
940
+ name: "state:stage_activity_jsonl_parseable",
941
+ ok: malformedActivityLines === 0,
942
+ details: malformedActivityLines === 0
943
+ ? "stage-activity.jsonl lines parse as JSON objects"
944
+ : `stage-activity.jsonl contains ${malformedActivityLines} malformed line(s)`
945
+ });
946
+ checks.push({
947
+ name: "warning:state:stage_activity_schema_version",
948
+ ok: true,
949
+ details: parsedActivityLines === 0
950
+ ? "stage-activity.jsonl is empty"
951
+ : missingSchemaVersion === 0
952
+ ? `all ${parsedActivityLines} stage-activity line(s) include schemaVersion=1`
953
+ : `warning: ${missingSchemaVersion}/${parsedActivityLines} stage-activity line(s) missing schemaVersion=1`
954
+ });
955
+ }
843
956
  checks.push({
844
957
  name: "state:suggestion_memory_exists",
845
958
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
@@ -880,81 +993,6 @@ export async function doctorChecks(projectRoot, options = {}) {
880
993
  details: modePath
881
994
  });
882
995
  }
883
- const preambleLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "preamble-log.jsonl");
884
- const preambleLogExists = await exists(preambleLogPath);
885
- checks.push({
886
- name: "state:preamble_log_exists",
887
- ok: preambleLogExists,
888
- details: `${RUNTIME_ROOT}/state/preamble-log.jsonl must exist for preamble budget tracking`
889
- });
890
- if (preambleLogExists) {
891
- let duplicateHits = 0;
892
- let parsedEntries = 0;
893
- let malformedEntries = 0;
894
- try {
895
- const now = Date.now();
896
- const byKey = new Map();
897
- const raw = await fs.readFile(preambleLogPath, "utf8");
898
- const lines = raw
899
- .split("\n")
900
- .map((line) => line.trim())
901
- .filter((line) => line.length > 0);
902
- for (const line of lines) {
903
- try {
904
- const parsed = JSON.parse(line);
905
- const tsRaw = parsed.ts;
906
- const stageRaw = parsed.stage;
907
- const triggerRaw = parsed.trigger;
908
- const hashRaw = parsed.hash;
909
- if (typeof tsRaw !== "string" ||
910
- typeof stageRaw !== "string" ||
911
- typeof triggerRaw !== "string" ||
912
- typeof hashRaw !== "string") {
913
- malformedEntries += 1;
914
- continue;
915
- }
916
- const stamp = Date.parse(tsRaw);
917
- if (!Number.isFinite(stamp)) {
918
- malformedEntries += 1;
919
- continue;
920
- }
921
- if (now - stamp > 24 * 60 * 60 * 1000) {
922
- continue;
923
- }
924
- parsedEntries += 1;
925
- const key = `${stageRaw}|${triggerRaw}|${hashRaw}`;
926
- const bucket = byKey.get(key) ?? [];
927
- bucket.push(stamp);
928
- byKey.set(key, bucket);
929
- }
930
- catch {
931
- malformedEntries += 1;
932
- }
933
- }
934
- for (const stamps of byKey.values()) {
935
- stamps.sort((a, b) => a - b);
936
- for (let i = 1; i < stamps.length; i += 1) {
937
- if (stamps[i] - stamps[i - 1] < PREAMBLE_COOLDOWN_MS) {
938
- duplicateHits += 1;
939
- }
940
- }
941
- }
942
- }
943
- catch {
944
- malformedEntries += 1;
945
- }
946
- checks.push({
947
- name: "warning:preamble:dedup",
948
- ok: true,
949
- details: duplicateHits > 0
950
- ? `warning: detected ${duplicateHits} repeated preamble emission(s) inside ${Math.floor(PREAMBLE_COOLDOWN_MS / 60000)}m cooldown window`
951
- : parsedEntries > 0
952
- ? `preamble budget healthy (${parsedEntries} recent preamble entry/entries checked)`
953
- : malformedEntries > 0
954
- ? `warning: preamble log exists but entries are malformed (${malformedEntries} line(s) ignored)`
955
- : "preamble log is empty; no recent preamble emissions recorded"
956
- });
957
- }
958
996
  await ensureFeatureSystem(projectRoot);
959
997
  const activeFeature = await readActiveFeature(projectRoot);
960
998
  let flowState = await readFlowState(projectRoot);
@@ -1012,30 +1050,51 @@ export async function doctorChecks(projectRoot, options = {}) {
1012
1050
  details: `${RUNTIME_ROOT}/artifacts must exist as the active artifact root`
1013
1051
  });
1014
1052
  const features = await listFeatures(projectRoot);
1053
+ const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot);
1054
+ const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
1055
+ const activeFeatureWorkspacePath = activeFeatureEntry
1056
+ ? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
1057
+ : "";
1015
1058
  checks.push({
1016
1059
  name: "state:active_feature_meta",
1017
1060
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json")),
1018
1061
  details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
1019
1062
  });
1063
+ checks.push({
1064
+ name: "state:worktree_registry_exists",
1065
+ ok: await exists(worktreeRegistryPath(projectRoot)),
1066
+ details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
1067
+ });
1020
1068
  checks.push({
1021
1069
  name: "state:active_feature_exists",
1022
1070
  ok: features.includes(activeFeature),
1023
1071
  details: features.includes(activeFeature)
1024
- ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/features`
1025
- : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/features`
1072
+ ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/state/worktrees.json`
1073
+ : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/state/worktrees.json`
1026
1074
  });
1027
1075
  checks.push({
1028
1076
  name: "state:features_nonempty",
1029
1077
  ok: features.length > 0,
1030
1078
  details: features.length > 0
1031
- ? `${features.length} feature snapshot(s): ${features.join(", ")}`
1032
- : `no feature snapshots found under ${RUNTIME_ROOT}/features`
1079
+ ? `${features.length} registered feature workspace(s): ${features.join(", ")}`
1080
+ : `no feature workspaces found in ${RUNTIME_ROOT}/state/worktrees.json`
1081
+ });
1082
+ checks.push({
1083
+ name: "state:active_feature_workspace_path",
1084
+ ok: activeFeatureEntry ? await exists(activeFeatureWorkspacePath) : false,
1085
+ details: activeFeatureEntry
1086
+ ? `active feature "${activeFeature}" maps to workspace path ${activeFeatureEntry.path} (${activeFeatureEntry.source})`
1087
+ : `active feature "${activeFeature}" has no worktree registry entry`
1033
1088
  });
1089
+ const legacyWorkspaceEntries = worktreeRegistry.entries
1090
+ .filter((entry) => entry.source === "legacy-snapshot")
1091
+ .map((entry) => entry.featureId);
1034
1092
  checks.push({
1035
- name: "state:active_feature_snapshot_dirs",
1036
- ok: await exists(path.join(featureRootPath(projectRoot, activeFeature), "artifacts")) &&
1037
- await exists(path.join(featureRootPath(projectRoot, activeFeature), "state")),
1038
- details: `${RUNTIME_ROOT}/features/${activeFeature}/artifacts and /state must exist`
1093
+ name: "warning:state:legacy_feature_snapshots",
1094
+ ok: legacyWorkspaceEntries.length === 0,
1095
+ details: legacyWorkspaceEntries.length === 0
1096
+ ? "no legacy .cclaw/features snapshot entries remain"
1097
+ : `legacy snapshot entries still present (read-only): ${legacyWorkspaceEntries.join(", ")}`
1039
1098
  });
1040
1099
  const staleStages = Object.keys(flowState.staleStages).filter((value) => COMMAND_FILE_ORDER.includes(value));
1041
1100
  checks.push({
@@ -1043,7 +1102,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1043
1102
  ok: staleStages.length === 0,
1044
1103
  details: staleStages.length === 0
1045
1104
  ? "no stale stages pending acknowledgement"
1046
- : `stale stages must be acknowledged via /cc-ops rewind-ack: ${staleStages.join(", ")}`
1105
+ : `stale stages must be acknowledged via /cc-ops rewind --ack <stage>: ${staleStages.join(", ")}`
1047
1106
  });
1048
1107
  const retroRequired = flowState.completedStages.includes("ship");
1049
1108
  const retroComplete = !retroRequired ||