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