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.
- package/dist/artifact-linter.js +154 -0
- package/dist/cli.js +2 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -3
- package/dist/content/contracts.js +1 -1
- package/dist/content/doctor-references.js +7 -6
- package/dist/content/feature-command.js +54 -51
- package/dist/content/harnesses-doc.js +2 -2
- package/dist/content/hooks.js +2 -2
- package/dist/content/learnings.d.ts +1 -1
- package/dist/content/learnings.js +22 -5
- package/dist/content/meta-skill.js +2 -2
- package/dist/content/next-command.js +2 -2
- package/dist/content/observe.js +3 -2
- package/dist/content/ops-command.js +1 -3
- package/dist/content/protocols.js +6 -34
- package/dist/content/rewind-command.d.ts +0 -1
- package/dist/content/rewind-command.js +19 -33
- package/dist/content/skills.js +2 -3
- package/dist/content/stage-schema.d.ts +2 -92
- package/dist/content/stage-schema.js +10 -1379
- package/dist/content/stages/brainstorm.d.ts +2 -0
- package/dist/content/stages/brainstorm.js +136 -0
- package/dist/content/stages/design.d.ts +2 -0
- package/dist/content/stages/design.js +215 -0
- package/dist/content/stages/index.d.ts +8 -0
- package/dist/content/stages/index.js +11 -0
- package/dist/content/stages/plan.d.ts +2 -0
- package/dist/content/stages/plan.js +157 -0
- package/dist/content/stages/review.d.ts +2 -0
- package/dist/content/stages/review.js +197 -0
- package/dist/content/stages/schema-types.d.ts +94 -0
- package/dist/content/stages/schema-types.js +1 -0
- package/dist/content/stages/scope.d.ts +2 -0
- package/dist/content/stages/scope.js +194 -0
- package/dist/content/stages/ship.d.ts +2 -0
- package/dist/content/stages/ship.js +142 -0
- package/dist/content/stages/spec.d.ts +2 -0
- package/dist/content/stages/spec.js +136 -0
- package/dist/content/stages/tdd.d.ts +2 -0
- package/dist/content/stages/tdd.js +185 -0
- package/dist/content/templates.js +105 -9
- package/dist/content/utility-skills.js +1 -1
- package/dist/delegation.d.ts +33 -3
- package/dist/delegation.js +56 -3
- package/dist/doctor.js +147 -88
- package/dist/feature-system.d.ts +22 -5
- package/dist/feature-system.js +267 -126
- package/dist/install.js +4 -8
- package/dist/policy.js +3 -4
- 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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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.
|
package/dist/delegation.d.ts
CHANGED
|
@@ -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:
|
|
6
|
-
status:
|
|
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
|
|
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;
|
package/dist/delegation.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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"
|
|
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
|
-
["
|
|
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}/
|
|
1025
|
-
: `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/
|
|
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
|
|
1032
|
-
: `no feature
|
|
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:
|
|
1036
|
-
ok:
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
|
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 ||
|