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