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.
@@ -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,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(process.execPath, [hookRuntimePath, hookName], {
315
+ child = spawn(resolveNodeExecutable(), [hookRuntimePath, hookName], {
264
316
  cwd: root,
265
317
  stdio: ["pipe", "ignore", "pipe"]
266
318
  });
@@ -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
- - (list patches applied to this artifact during self-review, or \`- None.\`)
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
@@ -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
- "- None recorded by managed start-flow."
1114
+ ...discoveredContext
989
1115
  ].join("\n") + "\n";
990
1116
  await fs.writeFile(artifactPath, body, "utf8");
991
1117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.14",
3
+ "version": "0.51.16",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {