cclaw-cli 0.51.14 → 0.51.16
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 +31 -1
- package/dist/content/node-hooks.js +39 -0
- package/dist/content/opencode-plugin.js +55 -3
- package/dist/content/skills.js +3 -0
- package/dist/content/stages/review.js +2 -1
- package/dist/content/templates.js +10 -1
- package/dist/gate-evidence.js +63 -0
- package/dist/internal/advance-stage.js +129 -3
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -604,11 +604,15 @@ function validateApproachesTaxonomy(sectionBody) {
|
|
|
604
604
|
const roleIndex = columnIndex(header, "role");
|
|
605
605
|
const upsideIndex = columnIndex(header, "upside");
|
|
606
606
|
if (roleIndex < 0 || upsideIndex < 0) {
|
|
607
|
+
const firstColumnTokens = rows.map((row) => normalizeTableToken(row[0] ?? ""));
|
|
608
|
+
const appearsTransposed = firstColumnTokens.includes("role") || firstColumnTokens.includes("upside");
|
|
607
609
|
return {
|
|
608
610
|
rowCount: rows.length,
|
|
609
611
|
roleUpsideOk: false,
|
|
610
612
|
challengerOk: false,
|
|
611
|
-
details:
|
|
613
|
+
details: appearsTransposed
|
|
614
|
+
? "Approaches table appears transposed: `Role`/`Upside` are rows, but must be columns. Use `| Approach | Role | Upside | ... |` with one approach per row."
|
|
615
|
+
: "Approaches table must include canonical `Role` and `Upside` columns (Role: baseline | challenger | wild-card; Upside: low | modest | high | higher)."
|
|
612
616
|
};
|
|
613
617
|
}
|
|
614
618
|
let challengerRows = 0;
|
|
@@ -649,6 +653,21 @@ function validateApproachesTaxonomy(sectionBody) {
|
|
|
649
653
|
: `Approaches table must include exactly one challenger row with Upside high or higher. Found ${challengerRows} challenger row(s).`
|
|
650
654
|
};
|
|
651
655
|
}
|
|
656
|
+
function validateCalibratedSelfReview(sectionBody) {
|
|
657
|
+
const hasStatus = /^\s*-\s*Status:\s*(?:Approved|Issues Found)\s*$/imu.test(sectionBody);
|
|
658
|
+
const hasPatches = /^\s*-\s*Patches applied:\s*$/imu.test(sectionBody);
|
|
659
|
+
const hasConcerns = /^\s*-\s*Remaining concerns:\s*$/imu.test(sectionBody);
|
|
660
|
+
if (!hasStatus || !hasPatches || !hasConcerns) {
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
details: "Self-Review Notes must use the calibrated review prompt format: `- Status: Approved | Issues Found`, `- Patches applied:`, and `- Remaining concerns:`."
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
details: "Self-Review Notes use the calibrated review prompt format."
|
|
669
|
+
};
|
|
670
|
+
}
|
|
652
671
|
function validateRequirementsTaxonomy(sectionBody) {
|
|
653
672
|
const header = tableHeaderCells(sectionBody);
|
|
654
673
|
if (!header) {
|
|
@@ -1786,6 +1805,17 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
1786
1805
|
});
|
|
1787
1806
|
}
|
|
1788
1807
|
}
|
|
1808
|
+
const selfReviewBody = sectionBodyByName(sections, "Self-Review Notes");
|
|
1809
|
+
if (selfReviewBody !== null) {
|
|
1810
|
+
const selfReview = validateCalibratedSelfReview(selfReviewBody);
|
|
1811
|
+
findings.push({
|
|
1812
|
+
section: "Calibrated Self-Review Format",
|
|
1813
|
+
required: true,
|
|
1814
|
+
rule: "When Self-Review Notes are present, they must use the calibrated review prompt output shape.",
|
|
1815
|
+
found: selfReview.ok,
|
|
1816
|
+
details: selfReview.details
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1789
1819
|
}
|
|
1790
1820
|
if (stage === "design") {
|
|
1791
1821
|
const tierResolution = await resolveDesignDiagramTier(projectRoot, track, raw);
|
|
@@ -879,6 +879,41 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
|
879
879
|
};
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
+
async function readStageSupportContext(root, currentStage) {
|
|
883
|
+
const stage = typeof currentStage === "string" ? currentStage : "";
|
|
884
|
+
const validStages = new Set(["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"]);
|
|
885
|
+
if (!validStages.has(stage)) return [];
|
|
886
|
+
|
|
887
|
+
const parts = [];
|
|
888
|
+
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
889
|
+
const contract = (await readTextFile(contractPath, "")).trim();
|
|
890
|
+
if (contract.length > 0) {
|
|
891
|
+
parts.push(
|
|
892
|
+
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
893
|
+
contract
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const reviewPromptByStage = {
|
|
898
|
+
brainstorm: "brainstorm-self-review.md",
|
|
899
|
+
scope: "scope-ceo-review.md",
|
|
900
|
+
design: "design-eng-review.md"
|
|
901
|
+
};
|
|
902
|
+
const promptName = reviewPromptByStage[stage];
|
|
903
|
+
if (typeof promptName === "string") {
|
|
904
|
+
const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
|
|
905
|
+
const prompt = (await readTextFile(promptPath, "")).trim();
|
|
906
|
+
if (prompt.length > 0) {
|
|
907
|
+
parts.push(
|
|
908
|
+
"Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
|
|
909
|
+
prompt
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return parts;
|
|
915
|
+
}
|
|
916
|
+
|
|
882
917
|
async function handleSessionStart(runtime) {
|
|
883
918
|
const state = await readFlowState(runtime.root);
|
|
884
919
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
@@ -984,6 +1019,7 @@ async function handleSessionStart(runtime) {
|
|
|
984
1019
|
const staleStages = toObject(state.raw.staleStages) || {};
|
|
985
1020
|
const staleStageNames = Object.keys(staleStages);
|
|
986
1021
|
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
1022
|
+
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
987
1023
|
|
|
988
1024
|
const parts = [
|
|
989
1025
|
"cclaw loaded. Flow: stage=" +
|
|
@@ -1017,6 +1053,9 @@ async function handleSessionStart(runtime) {
|
|
|
1017
1053
|
knowledge.digestLines.join("\\n")
|
|
1018
1054
|
);
|
|
1019
1055
|
}
|
|
1056
|
+
if (stageSupportContext.length > 0) {
|
|
1057
|
+
parts.push(...stageSupportContext);
|
|
1058
|
+
}
|
|
1020
1059
|
if (ironLawLines.length > 0) {
|
|
1021
1060
|
parts.push("Iron laws (enforced policy highlights):\\n" + ironLawLines.join("\\n"));
|
|
1022
1061
|
}
|
|
@@ -4,7 +4,7 @@ export function opencodePluginJs(_options = {}) {
|
|
|
4
4
|
return `// cclaw OpenCode plugin — generated by npx cclaw-cli sync
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { readFile, stat } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
8
|
|
|
9
9
|
export default function cclawPlugin(ctx) {
|
|
10
10
|
const root = ctx.directory || process.cwd();
|
|
@@ -16,6 +16,13 @@ export default function cclawPlugin(ctx) {
|
|
|
16
16
|
const flowStatePath = join(stateDir, "flow-state.json");
|
|
17
17
|
const knowledgePath = join(runtimeDir, "knowledge.jsonl");
|
|
18
18
|
const metaSkillPath = join(runtimeDir, "skills/${META_SKILL_NAME}/SKILL.md");
|
|
19
|
+
const STAGE_IDS = ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"];
|
|
20
|
+
const REVIEW_PROMPT_BY_STAGE = {
|
|
21
|
+
brainstorm: "brainstorm-self-review.md",
|
|
22
|
+
scope: "scope-ceo-review.md",
|
|
23
|
+
design: "design-eng-review.md"
|
|
24
|
+
};
|
|
25
|
+
const REVIEW_PROMPT_FILES = Object.values(REVIEW_PROMPT_BY_STAGE);
|
|
19
26
|
|
|
20
27
|
function ensureRuntimeDirs() {
|
|
21
28
|
try {
|
|
@@ -83,6 +90,29 @@ export default function cclawPlugin(ctx) {
|
|
|
83
90
|
return readTailLines(knowledgePath, 12);
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
async function readStageSupportContext(stage) {
|
|
94
|
+
if (typeof stage !== "string" || !STAGE_IDS.includes(stage)) return [];
|
|
95
|
+
const parts = [];
|
|
96
|
+
const contract = (await readFileText(join(runtimeDir, "templates/state-contracts", stage + ".json"))).trim();
|
|
97
|
+
if (contract.length > 0) {
|
|
98
|
+
parts.push(
|
|
99
|
+
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
100
|
+
contract
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const reviewPromptName = REVIEW_PROMPT_BY_STAGE[stage];
|
|
104
|
+
if (reviewPromptName) {
|
|
105
|
+
const prompt = (await readFileText(join(runtimeDir, "skills/review-prompts", reviewPromptName))).trim();
|
|
106
|
+
if (prompt.length > 0) {
|
|
107
|
+
parts.push(
|
|
108
|
+
"Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
|
|
109
|
+
prompt
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return parts;
|
|
114
|
+
}
|
|
115
|
+
|
|
86
116
|
const BOOTSTRAP_MARKER = "<!-- cclaw-bootstrap-v1 -->";
|
|
87
117
|
|
|
88
118
|
async function buildBootstrap() {
|
|
@@ -97,6 +127,9 @@ export default function cclawPlugin(ctx) {
|
|
|
97
127
|
const knowledge = await readKnowledgeDigest();
|
|
98
128
|
if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
|
|
99
129
|
|
|
130
|
+
const stageSupport = await readStageSupportContext(flow.stage);
|
|
131
|
+
if (stageSupport.length > 0) parts.push(...stageSupport);
|
|
132
|
+
|
|
100
133
|
parts.push(
|
|
101
134
|
"If you discover a non-obvious rule or pattern during stage work, add it to the current artifact ## Learnings section; stage-complete harvests it into .cclaw/knowledge.jsonl. Direct JSONL append is only for explicit manual learnings operations."
|
|
102
135
|
);
|
|
@@ -112,7 +145,9 @@ export default function cclawPlugin(ctx) {
|
|
|
112
145
|
const BOOTSTRAP_SOURCE_PATHS = [
|
|
113
146
|
flowStatePath,
|
|
114
147
|
knowledgePath,
|
|
115
|
-
metaSkillPath
|
|
148
|
+
metaSkillPath,
|
|
149
|
+
...STAGE_IDS.map((stage) => join(runtimeDir, "templates/state-contracts", stage + ".json")),
|
|
150
|
+
...REVIEW_PROMPT_FILES.map((file) => join(runtimeDir, "skills/review-prompts", file))
|
|
116
151
|
];
|
|
117
152
|
|
|
118
153
|
async function readMtimeMs(filePath) {
|
|
@@ -244,6 +279,23 @@ export default function cclawPlugin(ctx) {
|
|
|
244
279
|
return false;
|
|
245
280
|
}
|
|
246
281
|
|
|
282
|
+
function resolveNodeExecutable() {
|
|
283
|
+
const override = typeof process.env.CCLAW_NODE_EXECUTABLE === "string"
|
|
284
|
+
? process.env.CCLAW_NODE_EXECUTABLE.trim()
|
|
285
|
+
: "";
|
|
286
|
+
if (override.length > 0) return override;
|
|
287
|
+
|
|
288
|
+
const execName = basename(process.execPath || "").toLowerCase();
|
|
289
|
+
if (execName === "node" || execName === "node.exe") {
|
|
290
|
+
return process.execPath;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// OpenCode can host plugins from its own CLI binary, making
|
|
294
|
+
// process.execPath point at opencode instead of Node. Fall back to the
|
|
295
|
+
// user's Node on PATH so generated cclaw hooks execute as JavaScript.
|
|
296
|
+
return "node";
|
|
297
|
+
}
|
|
298
|
+
|
|
247
299
|
async function runHookScript(hookName, payload = {}) {
|
|
248
300
|
const { spawn } = await import("node:child_process");
|
|
249
301
|
const hookRuntimePath = join(root, "${RUNTIME_ROOT}/hooks/run-hook.mjs");
|
|
@@ -260,7 +312,7 @@ export default function cclawPlugin(ctx) {
|
|
|
260
312
|
|
|
261
313
|
let child;
|
|
262
314
|
try {
|
|
263
|
-
child = spawn(
|
|
315
|
+
child = spawn(resolveNodeExecutable(), [hookRuntimePath, hookName], {
|
|
264
316
|
cwd: root,
|
|
265
317
|
stdio: ["pipe", "ignore", "pipe"]
|
|
266
318
|
});
|
package/dist/content/skills.js
CHANGED
|
@@ -135,6 +135,9 @@ This is the gate function for completion claims. No "done", "all good", or
|
|
|
135
135
|
"tests pass" unless fresh evidence from this turn proves it.
|
|
136
136
|
|
|
137
137
|
- Run verification commands (tests/build/lint/type-check) for the changed scope.
|
|
138
|
+
- Before \`tdd -> review\` and \`review -> ship\`, discover the real test command
|
|
139
|
+
from repo config (package scripts, pytest/go/cargo/maven/gradle signals) and
|
|
140
|
+
cite that exact command in the gate evidence.
|
|
138
141
|
- Confirm output directly; do not infer success from prior runs or green memories.
|
|
139
142
|
- If this is a bug fix, capture RED -> GREEN evidence for the regression path.
|
|
140
143
|
- If a command fails, report the failure as diagnostic evidence and stop before completion.
|
|
@@ -72,7 +72,7 @@ export const REVIEW = {
|
|
|
72
72
|
{ id: "review_layer_coverage_complete", description: "Layer coverage map in 07-review-army.json confirms spec/correctness/security/performance/architecture/external-safety tags were considered." },
|
|
73
73
|
{ id: "review_criticals_resolved", description: "No unresolved critical blockers remain." },
|
|
74
74
|
{ id: "review_army_json_valid", description: "07-review-army.json passes schema validation (validateReviewArmy)." },
|
|
75
|
-
{ id: "review_trace_matrix_clean", description: "Trace matrix has no orphaned criteria/tasks/test slices for the active run." }
|
|
75
|
+
{ id: "review_trace_matrix_clean", description: "Trace matrix has no orphaned criteria/tasks/test slices for the active run, and evidence cites a discovered real test command before ship handoff." }
|
|
76
76
|
],
|
|
77
77
|
requiredEvidence: [
|
|
78
78
|
"Artifact written to `.cclaw/artifacts/07-review.md`.",
|
|
@@ -82,6 +82,7 @@ export const REVIEW = {
|
|
|
82
82
|
"Layer 2 sections completed with findings.",
|
|
83
83
|
"Severity log includes critical/important/suggestion buckets.",
|
|
84
84
|
"Explicit final verdict: APPROVED, APPROVED_WITH_CONCERNS, or BLOCKED.",
|
|
85
|
+
"Fresh verification command discovery recorded, and the command cited in `review_trace_matrix_clean` evidence before ship handoff.",
|
|
85
86
|
"If BLOCKED: include explicit remediation route (`ROUTE_BACK_TO_TDD`) with blocking finding IDs."
|
|
86
87
|
],
|
|
87
88
|
inputs: ["implementation diff", "spec and plan artifacts", "test/build evidence"],
|
|
@@ -96,7 +96,11 @@ ${SEED_SHELF_SECTION}
|
|
|
96
96
|
- (compact ASCII/Mermaid diagram for medium+ complexity, or one-line justification for omission.)
|
|
97
97
|
|
|
98
98
|
## Self-Review Notes
|
|
99
|
-
-
|
|
99
|
+
- Status: Approved | Issues Found
|
|
100
|
+
- Patches applied:
|
|
101
|
+
- None
|
|
102
|
+
- Remaining concerns:
|
|
103
|
+
- None
|
|
100
104
|
|
|
101
105
|
## Assumptions and Open Questions
|
|
102
106
|
- **Assumptions:**
|
|
@@ -754,6 +758,11 @@ Execution rule: complete and verify each batch before starting the next batch.
|
|
|
754
758
|
- Orphaned tests: 0
|
|
755
759
|
- Evidence ref:
|
|
756
760
|
|
|
761
|
+
## Verification Command Discovery
|
|
762
|
+
| Source | Discovered command | Result | Evidence ref |
|
|
763
|
+
|---|---|---|---|
|
|
764
|
+
| package.json / pytest / go.mod / Cargo.toml / pom.xml / gradle | | PASS/FAIL | |
|
|
765
|
+
|
|
757
766
|
## Blocked Route
|
|
758
767
|
- ROUTE_BACK_TO_TDD: only when Final Verdict = BLOCKED
|
|
759
768
|
- Target stage: tdd
|
package/dist/gate-evidence.js
CHANGED
|
@@ -44,6 +44,64 @@ function sameStringArray(a, b) {
|
|
|
44
44
|
return false;
|
|
45
45
|
return a.every((value, index) => value === b[index]);
|
|
46
46
|
}
|
|
47
|
+
async function readJsonFile(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
50
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
51
|
+
? parsed
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function discoverRealTestCommands(projectRoot) {
|
|
59
|
+
const commands = [];
|
|
60
|
+
const packageJson = await readJsonFile(path.join(projectRoot, "package.json"));
|
|
61
|
+
const scripts = packageJson?.scripts;
|
|
62
|
+
if (scripts && typeof scripts === "object" && !Array.isArray(scripts)) {
|
|
63
|
+
const scriptNames = Object.keys(scripts).filter((name) => {
|
|
64
|
+
const value = scripts[name];
|
|
65
|
+
return typeof value === "string" && (name === "test" || name.startsWith("test:"));
|
|
66
|
+
});
|
|
67
|
+
for (const name of scriptNames.sort()) {
|
|
68
|
+
commands.push(name === "test" ? "npm test" : `npm run ${name}`);
|
|
69
|
+
commands.push(name === "test" ? "pnpm test" : `pnpm ${name}`);
|
|
70
|
+
commands.push(name === "test" ? "yarn test" : `yarn ${name}`);
|
|
71
|
+
commands.push(name === "test" ? "bun test" : `bun run ${name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (await exists(path.join(projectRoot, "pyproject.toml")))
|
|
75
|
+
commands.push("pytest");
|
|
76
|
+
if (await exists(path.join(projectRoot, "pytest.ini")))
|
|
77
|
+
commands.push("pytest");
|
|
78
|
+
if (await exists(path.join(projectRoot, "go.mod")))
|
|
79
|
+
commands.push("go test ./...");
|
|
80
|
+
if (await exists(path.join(projectRoot, "Cargo.toml")))
|
|
81
|
+
commands.push("cargo test");
|
|
82
|
+
if (await exists(path.join(projectRoot, "pom.xml")))
|
|
83
|
+
commands.push("mvn test");
|
|
84
|
+
if (await exists(path.join(projectRoot, "build.gradle")) ||
|
|
85
|
+
await exists(path.join(projectRoot, "build.gradle.kts"))) {
|
|
86
|
+
commands.push("gradle test", "./gradlew test");
|
|
87
|
+
}
|
|
88
|
+
return unique(commands);
|
|
89
|
+
}
|
|
90
|
+
async function verifyDiscoveredCommandEvidence(projectRoot, stage, gateId, flowState) {
|
|
91
|
+
if (!(stage === "tdd" && gateId === "tdd_verified_before_complete") &&
|
|
92
|
+
!(stage === "review" && gateId === "review_trace_matrix_clean")) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const commands = await discoverRealTestCommands(projectRoot);
|
|
96
|
+
if (commands.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
const evidence = flowState.guardEvidence[gateId];
|
|
99
|
+
const normalizedEvidence = typeof evidence === "string" ? evidence.toLowerCase() : "";
|
|
100
|
+
const matched = commands.some((command) => normalizedEvidence.includes(command.toLowerCase()));
|
|
101
|
+
if (matched)
|
|
102
|
+
return null;
|
|
103
|
+
return `${stage} verification gate blocked (${gateId}): guard evidence must cite one discovered real test command: ${commands.join(", ")}.`;
|
|
104
|
+
}
|
|
47
105
|
const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
|
|
48
106
|
const RECONCILIATION_NOTICES_SCHEMA_VERSION = 1;
|
|
49
107
|
const DESIGN_RESEARCH_REQUIRED_SECTIONS = [
|
|
@@ -203,6 +261,11 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
203
261
|
const evidence = flowState.guardEvidence[gateId];
|
|
204
262
|
if (typeof evidence !== "string" || evidence.trim().length === 0) {
|
|
205
263
|
issues.push(`passed gate "${gateId}" is missing guardEvidence entry.`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const discoveredCommandIssue = await verifyDiscoveredCommandEvidence(projectRoot, stage, gateId, flowState);
|
|
267
|
+
if (discoveredCommandIssue) {
|
|
268
|
+
issues.push(discoveredCommandIssue);
|
|
206
269
|
}
|
|
207
270
|
}
|
|
208
271
|
for (const gateId of catalog.blocked) {
|
|
@@ -4,8 +4,8 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { resolveArtifactPath } from "../artifact-paths.js";
|
|
6
6
|
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
7
|
-
import { stageSchema } from "../content/stage-schema.js";
|
|
8
|
-
import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
|
|
7
|
+
import { stageAutoSubagentDispatch, stageSchema } from "../content/stage-schema.js";
|
|
8
|
+
import { appendDelegation, checkMandatoryDelegations, readDelegationLedger } from "../delegation.js";
|
|
9
9
|
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
10
10
|
import { extractMarkdownSectionBody, parseLearningsSection } from "../artifact-linter.js";
|
|
11
11
|
import { getAvailableTransitions, getTransitionGuards, isFlowTrack, createInitialFlowState } from "../flow-state.js";
|
|
@@ -203,6 +203,15 @@ const GATE_EVIDENCE_VALIDATORS = {
|
|
|
203
203
|
}
|
|
204
204
|
return null;
|
|
205
205
|
},
|
|
206
|
+
"review:review_trace_matrix_clean": (evidence) => {
|
|
207
|
+
if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
|
|
208
|
+
return "must include the fresh verification command that was run before ship handoff (for example `npm test`, `pytest`, `go test`, or equivalent).";
|
|
209
|
+
}
|
|
210
|
+
if (!PASS_STATUS_PATTERN.test(evidence)) {
|
|
211
|
+
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
},
|
|
206
215
|
"ship:ship_finalization_executed": (evidence) => {
|
|
207
216
|
if (!SHIP_FINALIZATION_MODE_PATTERN.test(evidence)) {
|
|
208
217
|
return `must name the finalization mode that ran (for example ${SHIP_FINALIZATION_MODE_HINT}).`;
|
|
@@ -818,6 +827,7 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
818
827
|
nextGuardEvidence[gateId] = provided.trim();
|
|
819
828
|
}
|
|
820
829
|
}
|
|
830
|
+
await ensureProactiveDelegationTrace(projectRoot, args.stage);
|
|
821
831
|
const nextStageCatalog = {
|
|
822
832
|
required: [...catalog.required],
|
|
823
833
|
recommended: [...catalog.recommended],
|
|
@@ -959,6 +969,121 @@ function firstIncompleteStageForTrack(track, completedStages) {
|
|
|
959
969
|
const stages = TRACK_STAGES[track];
|
|
960
970
|
return stages.find((stage) => !completed.has(stage)) ?? stages[stages.length - 1] ?? "brainstorm";
|
|
961
971
|
}
|
|
972
|
+
async function ensureProactiveDelegationTrace(projectRoot, stage) {
|
|
973
|
+
const proactiveRules = stageAutoSubagentDispatch(stage).filter((rule) => rule.mode === "proactive");
|
|
974
|
+
if (proactiveRules.length === 0)
|
|
975
|
+
return;
|
|
976
|
+
const ledger = await readDelegationLedger(projectRoot);
|
|
977
|
+
const currentRunEntries = ledger.entries.filter((entry) => entry.runId === ledger.runId);
|
|
978
|
+
for (const rule of proactiveRules) {
|
|
979
|
+
const alreadyRecorded = currentRunEntries.some((entry) => entry.stage === stage && entry.agent === rule.agent && entry.mode === "proactive");
|
|
980
|
+
if (alreadyRecorded)
|
|
981
|
+
continue;
|
|
982
|
+
await appendDelegation(projectRoot, {
|
|
983
|
+
stage,
|
|
984
|
+
agent: rule.agent,
|
|
985
|
+
mode: "proactive",
|
|
986
|
+
status: "waived",
|
|
987
|
+
waiverReason: "auto-recorded: proactive delegation was not explicitly triggered before stage completion",
|
|
988
|
+
conditionTrigger: rule.when,
|
|
989
|
+
skill: rule.skill,
|
|
990
|
+
ts: new Date().toISOString()
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async function pathExists(projectRoot, relPath) {
|
|
995
|
+
try {
|
|
996
|
+
await fs.stat(path.join(projectRoot, relPath));
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function listExistingFiles(projectRoot, relPaths) {
|
|
1004
|
+
const matches = [];
|
|
1005
|
+
for (const relPath of relPaths) {
|
|
1006
|
+
try {
|
|
1007
|
+
const stat = await fs.stat(path.join(projectRoot, relPath));
|
|
1008
|
+
if (stat.isFile())
|
|
1009
|
+
matches.push(relPath);
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
// continue
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return matches;
|
|
1016
|
+
}
|
|
1017
|
+
async function listFilesUnder(projectRoot, relDir, limit = 20) {
|
|
1018
|
+
const root = path.join(projectRoot, relDir);
|
|
1019
|
+
const out = [];
|
|
1020
|
+
async function walk(absDir) {
|
|
1021
|
+
if (out.length >= limit)
|
|
1022
|
+
return;
|
|
1023
|
+
let entries;
|
|
1024
|
+
try {
|
|
1025
|
+
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
for (const entry of entries) {
|
|
1031
|
+
if (out.length >= limit)
|
|
1032
|
+
return;
|
|
1033
|
+
if (entry.name.startsWith("."))
|
|
1034
|
+
continue;
|
|
1035
|
+
const abs = path.join(absDir, entry.name);
|
|
1036
|
+
if (entry.isDirectory()) {
|
|
1037
|
+
await walk(abs);
|
|
1038
|
+
}
|
|
1039
|
+
else if (entry.isFile()) {
|
|
1040
|
+
out.push(path.relative(projectRoot, abs).split(path.sep).join("/"));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
await walk(root);
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
1047
|
+
async function discoverStartFlowContext(projectRoot) {
|
|
1048
|
+
const lines = [];
|
|
1049
|
+
const seedFiles = (await listFilesUnder(projectRoot, path.join(RUNTIME_ROOT, "seeds"), 10))
|
|
1050
|
+
.filter((relPath) => /^\.cclaw\/seeds\/SEED-.*\.md$/u.test(relPath));
|
|
1051
|
+
lines.push(seedFiles.length > 0
|
|
1052
|
+
? `- Seed shelf scanned: ${seedFiles.join(", ")}.`
|
|
1053
|
+
: "- Seed shelf scanned: no `.cclaw/seeds/SEED-*.md` files found.");
|
|
1054
|
+
const originDirs = ["docs/prd", "docs/rfcs", "docs/adr", "docs/design", "specs", "prd", "rfc", "design"];
|
|
1055
|
+
const originRootFiles = ["PRD.md", "SPEC.md", "DESIGN.md", "REQUIREMENTS.md", "ROADMAP.md"];
|
|
1056
|
+
const originFiles = [
|
|
1057
|
+
...(await listExistingFiles(projectRoot, originRootFiles)),
|
|
1058
|
+
...(await Promise.all(originDirs.map((dir) => listFilesUnder(projectRoot, dir, 6)))).flat()
|
|
1059
|
+
].slice(0, 20);
|
|
1060
|
+
lines.push(originFiles.length > 0
|
|
1061
|
+
? `- Origin docs scanned: found ${originFiles.join(", ")}.`
|
|
1062
|
+
: "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
|
|
1063
|
+
const stackMarkers = await listExistingFiles(projectRoot, [
|
|
1064
|
+
"package.json",
|
|
1065
|
+
"pyproject.toml",
|
|
1066
|
+
"requirements.txt",
|
|
1067
|
+
"requirements-dev.txt",
|
|
1068
|
+
".python-version",
|
|
1069
|
+
"go.mod",
|
|
1070
|
+
"Cargo.toml",
|
|
1071
|
+
"pom.xml",
|
|
1072
|
+
"build.gradle",
|
|
1073
|
+
"build.gradle.kts",
|
|
1074
|
+
"Dockerfile",
|
|
1075
|
+
"docker-compose.yml",
|
|
1076
|
+
"docker-compose.yaml",
|
|
1077
|
+
".gitlab-ci.yml"
|
|
1078
|
+
]);
|
|
1079
|
+
if (await pathExists(projectRoot, ".github/workflows")) {
|
|
1080
|
+
stackMarkers.push(".github/workflows/");
|
|
1081
|
+
}
|
|
1082
|
+
lines.push(stackMarkers.length > 0
|
|
1083
|
+
? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
|
|
1084
|
+
: "- Stack markers scanned: no root stack markers found.");
|
|
1085
|
+
return lines;
|
|
1086
|
+
}
|
|
962
1087
|
async function appendIdeaArtifact(projectRoot, args, previous) {
|
|
963
1088
|
const artifactPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
|
|
964
1089
|
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
@@ -975,6 +1100,7 @@ async function appendIdeaArtifact(projectRoot, args, previous) {
|
|
|
975
1100
|
await fs.appendFile(artifactPath, entry, "utf8");
|
|
976
1101
|
return;
|
|
977
1102
|
}
|
|
1103
|
+
const discoveredContext = await discoverStartFlowContext(projectRoot);
|
|
978
1104
|
const body = [
|
|
979
1105
|
"# Idea",
|
|
980
1106
|
`Class: ${args.className || "unspecified"}`,
|
|
@@ -985,7 +1111,7 @@ async function appendIdeaArtifact(projectRoot, args, previous) {
|
|
|
985
1111
|
args.prompt || "(not provided)",
|
|
986
1112
|
"",
|
|
987
1113
|
"## Discovered context",
|
|
988
|
-
|
|
1114
|
+
...discoveredContext
|
|
989
1115
|
].join("\n") + "\n";
|
|
990
1116
|
await fs.writeFile(artifactPath, body, "utf8");
|
|
991
1117
|
}
|