cclaw-cli 0.51.15 → 0.51.17

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.
@@ -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: "Approaches table must include canonical `Role` and `Upside` columns (Role: baseline | challenger | wild-card; Upside: low | modest | high | higher)."
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,41 @@ 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 statusLineMatch = /^\s*-\s*Status:\s*(.*)$/imu.exec(sectionBody);
658
+ const statusValue = statusLineMatch ? statusLineMatch[1].trim() : "";
659
+ const mentionsApproved = /\bApproved\b/iu.test(statusValue);
660
+ const mentionsIssuesFound = /\bIssues Found\b/iu.test(statusValue);
661
+ const statusPickedExactlyOne = statusLineMatch !== null && (mentionsApproved !== mentionsIssuesFound);
662
+ const hasPatchesHeader = /^\s*-\s*Patches applied:/imu.test(sectionBody);
663
+ const hasConcernsHeader = /^\s*-\s*Remaining concerns:/imu.test(sectionBody);
664
+ if (statusPickedExactlyOne && hasPatchesHeader && hasConcernsHeader) {
665
+ return {
666
+ ok: true,
667
+ details: "Self-Review Notes use the calibrated review prompt format."
668
+ };
669
+ }
670
+ const problems = [];
671
+ if (!statusLineMatch) {
672
+ problems.push("missing `- Status:` line");
673
+ }
674
+ else if (!mentionsApproved && !mentionsIssuesFound) {
675
+ problems.push("`- Status:` must include `Approved` or `Issues Found`");
676
+ }
677
+ else if (mentionsApproved && mentionsIssuesFound) {
678
+ problems.push("`- Status:` must pick exactly one of `Approved` or `Issues Found` (the placeholder `Approved | Issues Found` is not a decision)");
679
+ }
680
+ if (!hasPatchesHeader)
681
+ problems.push("missing `- Patches applied:` line");
682
+ if (!hasConcernsHeader)
683
+ problems.push("missing `- Remaining concerns:` line");
684
+ return {
685
+ ok: false,
686
+ details: "Self-Review Notes must use the calibrated review prompt format: `- Status: Approved` (or `Issues Found`), `- Patches applied:` (inline note or sub-bullets), and `- Remaining concerns:` (inline note or sub-bullets). Issues: " +
687
+ problems.join("; ") +
688
+ "."
689
+ };
690
+ }
652
691
  function validateRequirementsTaxonomy(sectionBody) {
653
692
  const header = tableHeaderCells(sectionBody);
654
693
  if (!header) {
@@ -1786,6 +1825,17 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1786
1825
  });
1787
1826
  }
1788
1827
  }
1828
+ const selfReviewBody = sectionBodyByName(sections, "Self-Review Notes");
1829
+ if (selfReviewBody !== null) {
1830
+ const selfReview = validateCalibratedSelfReview(selfReviewBody);
1831
+ findings.push({
1832
+ section: "Calibrated Self-Review Format",
1833
+ required: true,
1834
+ rule: "When Self-Review Notes are present, they must use the calibrated review prompt output shape.",
1835
+ found: selfReview.ok,
1836
+ details: selfReview.details
1837
+ });
1838
+ }
1789
1839
  }
1790
1840
  if (stage === "design") {
1791
1841
  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(process.execPath, [hookRuntimePath, hookName], {
315
+ child = spawn(resolveNodeExecutable(), [hookRuntimePath, hookName], {
264
316
  cwd: root,
265
317
  stdio: ["pipe", "ignore", "pipe"]
266
318
  });
@@ -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
- - (list patches applied to this artifact during self-review, or \`- None.\`)
99
+ - Status: Approved
100
+ - Patches applied:
101
+ - None
102
+ - Remaining concerns:
103
+ - None
100
104
 
101
105
  ## Assumptions and Open Questions
102
106
  - **Assumptions:**
@@ -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";
@@ -827,6 +827,7 @@ async function runAdvanceStage(projectRoot, args, io) {
827
827
  nextGuardEvidence[gateId] = provided.trim();
828
828
  }
829
829
  }
830
+ await ensureProactiveDelegationTrace(projectRoot, args.stage);
830
831
  const nextStageCatalog = {
831
832
  required: [...catalog.required],
832
833
  recommended: [...catalog.recommended],
@@ -968,6 +969,121 @@ function firstIncompleteStageForTrack(track, completedStages) {
968
969
  const stages = TRACK_STAGES[track];
969
970
  return stages.find((stage) => !completed.has(stage)) ?? stages[stages.length - 1] ?? "brainstorm";
970
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
+ }
971
1087
  async function appendIdeaArtifact(projectRoot, args, previous) {
972
1088
  const artifactPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
973
1089
  await fs.mkdir(path.dirname(artifactPath), { recursive: true });
@@ -984,6 +1100,7 @@ async function appendIdeaArtifact(projectRoot, args, previous) {
984
1100
  await fs.appendFile(artifactPath, entry, "utf8");
985
1101
  return;
986
1102
  }
1103
+ const discoveredContext = await discoverStartFlowContext(projectRoot);
987
1104
  const body = [
988
1105
  "# Idea",
989
1106
  `Class: ${args.className || "unspecified"}`,
@@ -994,7 +1111,7 @@ async function appendIdeaArtifact(projectRoot, args, previous) {
994
1111
  args.prompt || "(not provided)",
995
1112
  "",
996
1113
  "## Discovered context",
997
- "- None recorded by managed start-flow."
1114
+ ...discoveredContext
998
1115
  ].join("\n") + "\n";
999
1116
  await fs.writeFile(artifactPath, body, "utf8");
1000
1117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.15",
3
+ "version": "0.51.17",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {