cclaw-cli 0.55.2 → 1.0.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.
Files changed (72) hide show
  1. package/README.md +3 -3
  2. package/dist/artifact-linter/brainstorm.js +45 -1
  3. package/dist/artifact-linter/design.js +32 -1
  4. package/dist/artifact-linter/plan.js +22 -1
  5. package/dist/artifact-linter/review.js +35 -1
  6. package/dist/artifact-linter/scope.js +19 -9
  7. package/dist/artifact-linter/shared.d.ts +11 -10
  8. package/dist/artifact-linter/shared.js +70 -41
  9. package/dist/artifact-linter/ship.js +36 -0
  10. package/dist/artifact-linter/spec.js +23 -1
  11. package/dist/artifact-linter/tdd.js +74 -0
  12. package/dist/artifact-linter.d.ts +1 -1
  13. package/dist/constants.d.ts +1 -1
  14. package/dist/constants.js +1 -0
  15. package/dist/content/closeout-guidance.d.ts +1 -1
  16. package/dist/content/closeout-guidance.js +10 -11
  17. package/dist/content/core-agents.d.ts +35 -36
  18. package/dist/content/core-agents.js +189 -99
  19. package/dist/content/diff-command.js +1 -1
  20. package/dist/content/examples.d.ts +0 -3
  21. package/dist/content/examples.js +197 -752
  22. package/dist/content/idea.d.ts +60 -0
  23. package/dist/content/idea.js +404 -0
  24. package/dist/content/learnings.d.ts +2 -4
  25. package/dist/content/learnings.js +10 -26
  26. package/dist/content/node-hooks.js +131 -97
  27. package/dist/content/opencode-plugin.js +12 -26
  28. package/dist/content/reference-patterns.js +2 -2
  29. package/dist/content/runtime-shared-snippets.d.ts +8 -0
  30. package/dist/content/runtime-shared-snippets.js +80 -0
  31. package/dist/content/session-hooks.js +1 -1
  32. package/dist/content/skills.d.ts +1 -0
  33. package/dist/content/skills.js +50 -0
  34. package/dist/content/stage-schema.js +107 -63
  35. package/dist/content/stages/review.js +8 -8
  36. package/dist/content/stages/schema-types.d.ts +2 -2
  37. package/dist/content/stages/scope.js +1 -1
  38. package/dist/content/stages/ship.js +1 -1
  39. package/dist/content/status-command.js +3 -3
  40. package/dist/content/subagent-context-skills.js +156 -1
  41. package/dist/content/subagents.d.ts +0 -5
  42. package/dist/content/subagents.js +12 -82
  43. package/dist/content/templates.js +87 -6
  44. package/dist/content/utility-skills.js +26 -97
  45. package/dist/flow-state.d.ts +5 -6
  46. package/dist/flow-state.js +4 -6
  47. package/dist/gate-evidence.d.ts +0 -31
  48. package/dist/gate-evidence.js +3 -181
  49. package/dist/harness-adapters.js +1 -1
  50. package/dist/install.js +38 -4
  51. package/dist/internal/advance-stage/advance.js +0 -1
  52. package/dist/internal/advance-stage/review-loop.js +1 -10
  53. package/dist/knowledge-store.d.ts +2 -20
  54. package/dist/knowledge-store.js +43 -57
  55. package/dist/policy.js +3 -3
  56. package/dist/retro-gate.js +8 -90
  57. package/dist/run-archive.js +1 -4
  58. package/dist/run-persistence.js +14 -109
  59. package/dist/runtime/run-hook.entry.d.ts +3 -0
  60. package/dist/runtime/run-hook.entry.js +5 -0
  61. package/dist/runtime/run-hook.mjs +9477 -0
  62. package/package.json +4 -2
  63. package/dist/content/hook-inline-snippets.d.ts +0 -96
  64. package/dist/content/hook-inline-snippets.js +0 -515
  65. package/dist/content/idea-command.d.ts +0 -8
  66. package/dist/content/idea-command.js +0 -322
  67. package/dist/content/idea-frames.d.ts +0 -31
  68. package/dist/content/idea-frames.js +0 -140
  69. package/dist/content/idea-ranking.d.ts +0 -25
  70. package/dist/content/idea-ranking.js +0 -65
  71. package/dist/trace-matrix.d.ts +0 -27
  72. package/dist/trace-matrix.js +0 -226
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD, DEFAULT_EARLY_LOOP_MAX_ITERATIONS } from "../config.js";
5
5
  import { RUNTIME_ROOT } from "../constants.js";
6
6
  import { SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD, SMALL_PROJECT_RECURRENCE_THRESHOLD } from "../knowledge-store.js";
7
- import { HOOK_INLINE_SHARED_HELPERS, COMPOUND_READINESS_INLINE_SOURCE, RALPH_LOOP_INLINE_SOURCE, EARLY_LOOP_INLINE_SOURCE } from "./hook-inline-snippets.js";
7
+ import { SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS, SHARED_STAGE_SUPPORT_SNIPPETS } from "./runtime-shared-snippets.js";
8
8
  function normalizePatterns(patterns, fallback) {
9
9
  if (!patterns || patterns.length === 0)
10
10
  return [...fallback];
@@ -84,6 +84,9 @@ const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
84
84
  const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
85
85
  const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
86
86
 
87
+ ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
88
+ ${SHARED_STAGE_SUPPORT_SNIPPETS}
89
+
87
90
  function resolveStrictness() {
88
91
  return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
89
92
  }
@@ -427,6 +430,73 @@ async function runCclawInternal(root, args, options = {}) {
427
430
  });
428
431
  }
429
432
 
433
+ function compactStderr(value) {
434
+ const raw = typeof value === "string" ? value : "";
435
+ return raw.replace(/\\s+/gu, " ").trim();
436
+ }
437
+
438
+ function summarizeInternalFailure(operation, result) {
439
+ const detail = compactStderr(result && typeof result === "object" ? result.stderr : "");
440
+ return detail.length > 0 ? operation + ": " + detail : operation + " failed";
441
+ }
442
+
443
+ function parseJsonStdoutObject(result) {
444
+ const raw = typeof (result && result.stdout) === "string" ? result.stdout.trim() : "";
445
+ if (raw.length === 0) return null;
446
+ try {
447
+ return toObject(JSON.parse(raw));
448
+ } catch {
449
+ return null;
450
+ }
451
+ }
452
+
453
+ function firstStdoutLine(value) {
454
+ const raw = typeof value === "string" ? value : "";
455
+ const lines = raw
456
+ .split(/\\r?\\n/gu)
457
+ .map((line) => line.trim())
458
+ .filter((line) => line.length > 0);
459
+ return lines[0] || "";
460
+ }
461
+
462
+ function formatRalphLoopStatusLineFromJson(status) {
463
+ const redOpenSlices = Array.isArray(status.redOpenSlices)
464
+ ? status.redOpenSlices.filter((value) => typeof value === "string")
465
+ : [];
466
+ const redOpen = redOpenSlices.length > 0 ? redOpenSlices.join(",") : "none";
467
+ const loopIteration =
468
+ typeof status.loopIteration === "number" && Number.isFinite(status.loopIteration)
469
+ ? Math.trunc(status.loopIteration)
470
+ : 0;
471
+ const sliceCount =
472
+ typeof status.sliceCount === "number" && Number.isFinite(status.sliceCount)
473
+ ? Math.trunc(status.sliceCount)
474
+ : 0;
475
+ const acClosed = Array.isArray(status.acClosed) ? status.acClosed.length : 0;
476
+ return "Ralph Loop: iter=" + String(loopIteration) +
477
+ ", slices=" + String(sliceCount) +
478
+ ", acClosed=" + String(acClosed) +
479
+ ", redOpen=" + redOpen;
480
+ }
481
+
482
+ function formatEarlyLoopStatusLineFromJson(status) {
483
+ const stage = typeof status.stage === "string" ? status.stage : "unknown";
484
+ const iteration =
485
+ typeof status.iteration === "number" && Number.isFinite(status.iteration)
486
+ ? Math.trunc(status.iteration)
487
+ : 0;
488
+ const maxIterations =
489
+ typeof status.maxIterations === "number" && Number.isFinite(status.maxIterations)
490
+ ? Math.trunc(status.maxIterations)
491
+ : EARLY_LOOP_MAX_ITERATIONS;
492
+ const openConcerns = Array.isArray(status.openConcerns) ? status.openConcerns.length : 0;
493
+ const convergence = status.convergenceTripped === true ? "tripped" : "clear";
494
+ return "Early Loop: stage=" + stage +
495
+ ", iter=" + String(iteration) + "/" + String(maxIterations) +
496
+ ", open=" + String(openConcerns) +
497
+ ", convergence=" + convergence;
498
+ }
499
+
430
500
  function detectHarness(env) {
431
501
  if (env.CLAUDE_PROJECT_DIR) return "claude";
432
502
  if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
@@ -839,12 +909,12 @@ async function readFlowState(root) {
839
909
  // empty object. Silent fallbacks used to mask stale CLI+hook drift.
840
910
  const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
841
911
  const obj = toObject(parsed) || {};
842
- const completed = Array.isArray(obj.completedStages) ? obj.completedStages : [];
912
+ const summary = summarizeFlowState(obj);
843
913
  return {
844
914
  filePath: statePath,
845
- currentStage: typeof obj.currentStage === "string" ? obj.currentStage : "none",
846
- activeRunId: typeof obj.activeRunId === "string" ? obj.activeRunId : "active",
847
- completedCount: completed.length,
915
+ currentStage: summary.stage,
916
+ activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
917
+ completedCount: summary.completed,
848
918
  raw: obj
849
919
  };
850
920
  }
@@ -857,44 +927,16 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
857
927
  const raw = typeof prereadRaw === "string"
858
928
  ? prereadRaw
859
929
  : await readTextFile(knowledgeFile, "");
860
- const lines = raw.split(/\\r?\\n/gu).map((line) => line.trim()).filter((line) => line.length > 0);
861
- let learningsCount = 0;
862
- const parsedRows = [];
863
- for (const line of lines) {
864
- if (line.startsWith("{")) learningsCount += 1;
865
- try {
866
- const parsed = JSON.parse(line);
867
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
868
- parsedRows.push(parsed);
869
- } catch {
870
- // ignore malformed knowledge line in digest
871
- }
872
- }
873
- const relevant = parsedRows
874
- .filter((row) => {
875
- const stage = typeof row.stage === "string" ? row.stage : null;
876
- return stage === null || stage === currentStage;
877
- })
878
- .slice(-6)
879
- .reverse()
880
- .map((row) => {
881
- const confidence = typeof row.confidence === "string" ? row.confidence : "unknown";
882
- const stage = typeof row.stage === "string" ? row.stage : "global";
883
- const domain = typeof row.domain === "string" ? row.domain : "general";
884
- const trigger = typeof row.trigger === "string" ? row.trigger : "trigger";
885
- const action = typeof row.action === "string" ? row.action : "action";
886
- return "- [" + confidence + " • " + stage + " • " + domain + "] " + trigger + " -> " + action;
887
- });
930
+ const digest = parseKnowledgeDigest(raw, currentStage, 6);
888
931
  return {
889
- digestLines: relevant,
890
- learningsCount
932
+ digestLines: digest.lines,
933
+ learningsCount: digest.learningsCount
891
934
  };
892
935
  }
893
936
 
894
937
  async function readStageSupportContext(root, currentStage) {
895
- const stage = typeof currentStage === "string" ? currentStage : "";
896
- const validStages = new Set(["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"]);
897
- if (!validStages.has(stage)) return [];
938
+ if (!isKnownStageId(currentStage)) return [];
939
+ const stage = currentStage;
898
940
 
899
941
  const parts = [];
900
942
  const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
@@ -906,12 +948,7 @@ async function readStageSupportContext(root, currentStage) {
906
948
  );
907
949
  }
908
950
 
909
- const reviewPromptByStage = {
910
- brainstorm: "brainstorm-self-review.md",
911
- scope: "scope-ceo-review.md",
912
- design: "design-eng-review.md"
913
- };
914
- const promptName = reviewPromptByStage[stage];
951
+ const promptName = reviewPromptFileName(stage);
915
952
  if (typeof promptName === "string") {
916
953
  const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
917
954
  const prompt = (await readTextFile(promptPath, "")).trim();
@@ -952,15 +989,19 @@ async function handleSessionStart(runtime) {
952
989
  let earlyLoopLine = "";
953
990
  if (state.currentStage === "tdd") {
954
991
  try {
955
- const ralphStatus = await computeRalphLoopStatusInline(stateDir, state.activeRunId);
956
- await writeJsonFile(path.join(stateDir, "ralph-loop.json"), ralphStatus);
957
- const redOpen = ralphStatus.redOpenSlices.length > 0
958
- ? ralphStatus.redOpenSlices.join(",")
959
- : "none";
960
- ralphLoopLine = "Ralph Loop: iter=" + String(ralphStatus.loopIteration) +
961
- ", slices=" + String(ralphStatus.sliceCount) +
962
- ", acClosed=" + String(ralphStatus.acClosed.length) +
963
- ", redOpen=" + redOpen;
992
+ const internalRalph = await runCclawInternal(
993
+ runtime.root,
994
+ ["tdd-loop-status", "--json", "--write"],
995
+ { captureStdout: true }
996
+ );
997
+ if (internalRalph.code !== 0) {
998
+ throw new Error(summarizeInternalFailure("tdd-loop-status", internalRalph));
999
+ }
1000
+ const ralphStatus = parseJsonStdoutObject(internalRalph);
1001
+ if (!ralphStatus) {
1002
+ throw new Error("tdd-loop-status returned empty or malformed JSON");
1003
+ }
1004
+ ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
964
1005
  } catch (err) {
965
1006
  // Best-effort — a malformed cycle log should never break
966
1007
  // session-start. But we DO leave a breadcrumb in
@@ -978,14 +1019,27 @@ async function handleSessionStart(runtime) {
978
1019
  (state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
979
1020
  ) {
980
1021
  try {
981
- const earlyLoopStatus = await computeEarlyLoopStatusInline(
982
- stateDir,
983
- state.currentStage,
984
- state.activeRunId,
985
- EARLY_LOOP_MAX_ITERATIONS
1022
+ const internalEarly = await runCclawInternal(
1023
+ runtime.root,
1024
+ [
1025
+ "early-loop-status",
1026
+ "--json",
1027
+ "--write",
1028
+ "--stage",
1029
+ state.currentStage,
1030
+ "--run-id",
1031
+ state.activeRunId
1032
+ ],
1033
+ { captureStdout: true }
986
1034
  );
987
- await writeJsonFile(path.join(stateDir, "early-loop.json"), earlyLoopStatus);
988
- earlyLoopLine = formatEarlyLoopStatusLineInline(earlyLoopStatus);
1035
+ if (internalEarly.code !== 0) {
1036
+ throw new Error(summarizeInternalFailure("early-loop-status", internalEarly));
1037
+ }
1038
+ const earlyLoopStatus = parseJsonStdoutObject(internalEarly);
1039
+ if (!earlyLoopStatus) {
1040
+ throw new Error("early-loop-status returned empty or malformed JSON");
1041
+ }
1042
+ earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
989
1043
  } catch (err) {
990
1044
  await recordHookError(
991
1045
  runtime.root,
@@ -1000,32 +1054,17 @@ async function handleSessionStart(runtime) {
1000
1054
  // where lifting becomes relevant; earlier stages update the file silently.
1001
1055
  let compoundReadinessLine = "";
1002
1056
  try {
1003
- let readiness = null;
1057
+ const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
1004
1058
  const internalReadiness = await runCclawInternal(
1005
1059
  runtime.root,
1006
- ["compound-readiness", "--json"],
1060
+ shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
1007
1061
  { captureStdout: true }
1008
1062
  );
1009
- if (internalReadiness.code === 0 && internalReadiness.stdout.trim().length > 0) {
1010
- try {
1011
- const parsed = JSON.parse(internalReadiness.stdout);
1012
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1013
- readiness = parsed;
1014
- }
1015
- } catch {
1016
- readiness = null;
1017
- }
1018
- }
1019
- if (!readiness) {
1020
- const archivedRunsCount = await countArchivedRunsInline(runtime.root);
1021
- readiness = await computeCompoundReadinessInline(runtime.root, {
1022
- prereadRaw: knowledgeRaw,
1023
- ...(typeof archivedRunsCount === "number" ? { archivedRunsCount } : {})
1024
- });
1025
- await writeJsonFile(path.join(stateDir, "compound-readiness.json"), readiness);
1063
+ if (internalReadiness.code !== 0) {
1064
+ throw new Error(summarizeInternalFailure("compound-readiness", internalReadiness));
1026
1065
  }
1027
- if (state.currentStage === "review" || state.currentStage === "ship") {
1028
- compoundReadinessLine = formatCompoundReadinessLineInline(toObject(readiness) || {});
1066
+ if (shouldShowReadiness) {
1067
+ compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
1029
1068
  }
1030
1069
  } catch (err) {
1031
1070
  // Best-effort — a malformed knowledge.jsonl must never break
@@ -1063,8 +1102,8 @@ async function handleSessionStart(runtime) {
1063
1102
  "/8 completed, run=" +
1064
1103
  state.activeRunId +
1065
1104
  "). Active artifacts: " +
1066
- RUNTIME_ROOT +
1067
- "/artifacts/. Learnings: " +
1105
+ activeArtifactsPathLabel(RUNTIME_ROOT) +
1106
+ " Learnings: " +
1068
1107
  String(knowledge.learningsCount) +
1069
1108
  " entries."
1070
1109
  ];
@@ -1169,7 +1208,7 @@ async function handleStopHandoff(runtime) {
1169
1208
  const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
1170
1209
  const closeoutContext =
1171
1210
  state.currentStage === "ship" || shipSubstate !== "idle"
1172
- ? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=retro -> compound -> archive; continue closeout with /cc."
1211
+ ? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=post_ship_review -> archive; continue closeout with /cc."
1173
1212
  : "";
1174
1213
 
1175
1214
  const message =
@@ -1246,17 +1285,6 @@ async function handlePromptGuard(runtime) {
1246
1285
  return 0;
1247
1286
  }
1248
1287
 
1249
- // Inline mirrors of canonical CLI computations (compound-readiness,
1250
- // ralph-loop, early-loop) are factored into
1251
- // src/content/hook-inline-snippets.ts so
1252
- // this 2000+-line file no longer owns their bodies. Each snippet carries
1253
- // an explicit "mirrors X, parity enforced by Y" comment in the snippets
1254
- // module. Parity is enforced by tests/unit/ralph-loop-parity.test.ts.
1255
- ${HOOK_INLINE_SHARED_HELPERS}
1256
- ${COMPOUND_READINESS_INLINE_SOURCE}
1257
- ${RALPH_LOOP_INLINE_SOURCE}
1258
- ${EARLY_LOOP_INLINE_SOURCE}
1259
-
1260
1288
  async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1261
1289
  const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
1262
1290
  for (const line of cycleRaw.split(/\\r?\\n/gu)) {
@@ -1468,8 +1496,14 @@ async function handleWorkflowGuard(runtime) {
1468
1496
  // writes that actually belonged to a new, not-yet-red S-2. Now
1469
1497
  // we reuse the canonical Ralph Loop status: if NO slice has an
1470
1498
  // open RED, we block.
1471
- const ralphStatus = await computeRalphLoopStatusInline(stateDir, currentRun);
1472
- if (!ralphStatus.redOpen) {
1499
+ const internalRalph = await runCclawInternal(
1500
+ runtime.root,
1501
+ ["tdd-loop-status", "--json", "--no-write"],
1502
+ { captureStdout: true }
1503
+ );
1504
+ const ralphStatus = parseJsonStdoutObject(internalRalph);
1505
+ const redOpen = internalRalph.code === 0 && ralphStatus?.redOpen === true;
1506
+ if (!redOpen) {
1473
1507
  reasons.push("tdd_write_without_open_red");
1474
1508
  }
1475
1509
  }
@@ -1,5 +1,6 @@
1
1
  import { RUNTIME_ROOT } from "../constants.js";
2
2
  import { META_SKILL_NAME } from "./meta-skill.js";
3
+ import { SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS, SHARED_STAGE_SUPPORT_SNIPPETS } from "./runtime-shared-snippets.js";
3
4
  export function opencodePluginJs(_options = {}) {
4
5
  return `// cclaw OpenCode plugin — generated by npx cclaw-cli sync
5
6
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
@@ -16,13 +17,8 @@ export default function cclawPlugin(ctx) {
16
17
  const flowStatePath = join(stateDir, "flow-state.json");
17
18
  const knowledgePath = join(runtimeDir, "knowledge.jsonl");
18
19
  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);
20
+ ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
21
+ ${SHARED_STAGE_SUPPORT_SNIPPETS}
26
22
 
27
23
  function ensureRuntimeDirs() {
28
24
  try {
@@ -61,14 +57,9 @@ export default function cclawPlugin(ctx) {
61
57
  async function readFlowState() {
62
58
  try {
63
59
  const raw = await readFile(flowStatePath, "utf8");
64
- const state = JSON.parse(raw);
65
- return {
66
- stage: typeof state.currentStage === "string" ? state.currentStage : "none",
67
- completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
68
- activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
69
- };
60
+ return summarizeFlowState(JSON.parse(raw));
70
61
  } catch {
71
- return { stage: "none", completed: 0, activeRunId: "none" };
62
+ return summarizeFlowState({});
72
63
  }
73
64
  }
74
65
 
@@ -80,18 +71,13 @@ export default function cclawPlugin(ctx) {
80
71
  }
81
72
  }
82
73
 
83
- async function readTailLines(filePath, maxLines) {
84
- const text = (await readFileText(filePath)).trim();
85
- if (!text) return [];
86
- return text.split(/\\r?\\n/).slice(-maxLines);
87
- }
88
-
89
- async function readKnowledgeDigest() {
90
- return readTailLines(knowledgePath, 12);
74
+ async function readKnowledgeDigest(stage) {
75
+ const raw = await readFileText(knowledgePath);
76
+ return parseKnowledgeDigest(raw, stage, 6).lines;
91
77
  }
92
78
 
93
79
  async function readStageSupportContext(stage) {
94
- if (typeof stage !== "string" || !STAGE_IDS.includes(stage)) return [];
80
+ if (!isKnownStageId(stage)) return [];
95
81
  const parts = [];
96
82
  const contract = (await readFileText(join(runtimeDir, "templates/state-contracts", stage + ".json"))).trim();
97
83
  if (contract.length > 0) {
@@ -100,7 +86,7 @@ export default function cclawPlugin(ctx) {
100
86
  contract
101
87
  );
102
88
  }
103
- const reviewPromptName = REVIEW_PROMPT_BY_STAGE[stage];
89
+ const reviewPromptName = reviewPromptFileName(stage);
104
90
  if (reviewPromptName) {
105
91
  const prompt = (await readFileText(join(runtimeDir, "skills/review-prompts", reviewPromptName))).trim();
106
92
  if (prompt.length > 0) {
@@ -119,12 +105,12 @@ export default function cclawPlugin(ctx) {
119
105
  const flow = await readFlowState();
120
106
  const parts = [
121
107
  BOOTSTRAP_MARKER,
122
- \`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
108
+ \`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: \${activeArtifactsPathLabel("${RUNTIME_ROOT}")}\`
123
109
  ];
124
110
 
125
111
 
126
112
 
127
- const knowledge = await readKnowledgeDigest();
113
+ const knowledge = await readKnowledgeDigest(flow.stage);
128
114
  if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
129
115
 
130
116
  const stageSupport = await readStageSupportContext(flow.stage);
@@ -201,7 +201,7 @@ export const REFERENCE_PATTERNS = [
201
201
  "Review source-item coverage by vertical slice, not by file count alone.",
202
202
  "A slice is review-ready only when RED, GREEN, REFACTOR, and verification evidence all line up."
203
203
  ],
204
- artifactSections: ["Completeness Snapshot", "Trace Matrix Check"]
204
+ artifactSections: ["Completeness Snapshot", "Coverage Check"]
205
205
  }
206
206
  ]
207
207
  },
@@ -348,7 +348,7 @@ export const REFERENCE_PATTERNS = [
348
348
  {
349
349
  stage: "review",
350
350
  guidance: [
351
- "Victory Detector: Layer 1, Layer 2, security sweep, structured findings, and trace evidence are complete with no unresolved criticals unless verdict is BLOCKED.",
351
+ "Victory Detector: Layer 1, Layer 2, security sweep, structured findings, and acceptance/reproduction coverage evidence are complete with no unresolved criticals unless verdict is BLOCKED.",
352
352
  "If the detector fails, iterate findings or route back to TDD; do not say LGTM."
353
353
  ],
354
354
  artifactSections: ["Review Readiness Snapshot", "Final Verdict"]
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared runtime snippets interpolated into generated hook/plugin scripts.
3
+ *
4
+ * Keep these string helpers minimal and dependency-free so both runtimes
5
+ * (node hooks and OpenCode plugin) stay in sync without duplicating constants.
6
+ */
7
+ export declare const SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS = "\nfunction summarizeFlowState(rawState) {\n const state =\n rawState && typeof rawState === \"object\" && !Array.isArray(rawState)\n ? rawState\n : {};\n return {\n stage: typeof state.currentStage === \"string\" ? state.currentStage : \"none\",\n completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,\n activeRunId: typeof state.activeRunId === \"string\" ? state.activeRunId : \"none\"\n };\n}\n\nfunction parseKnowledgeDigest(rawKnowledge, currentStage, maxRows = 6) {\n const text = typeof rawKnowledge === \"string\" ? rawKnowledge : \"\";\n if (text.trim().length === 0) {\n return { learningsCount: 0, lines: [] };\n }\n const rows = text\n .split(/\\r?\\n/gu)\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n let learningsCount = 0;\n const parsedRows = [];\n for (const line of rows) {\n if (line.startsWith(\"{\")) learningsCount += 1;\n try {\n const parsed = JSON.parse(line);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n parsedRows.push(parsed);\n } catch {\n // ignore malformed knowledge line in digest\n }\n }\n const lines = parsedRows\n .filter((row) => {\n const stage = typeof row.stage === \"string\" ? row.stage : null;\n return stage === null || stage === currentStage;\n })\n .slice(-maxRows)\n .reverse()\n .map((row) => {\n const confidence = typeof row.confidence === \"string\" ? row.confidence : \"unknown\";\n const stage = typeof row.stage === \"string\" ? row.stage : \"global\";\n const trigger = typeof row.trigger === \"string\" ? row.trigger : \"trigger\";\n const action = typeof row.action === \"string\" ? row.action : \"action\";\n return \"- [\" + confidence + \" \u2022 \" + stage + \"] \" + trigger + \" -> \" + action;\n });\n return { learningsCount, lines };\n}\n\nfunction activeArtifactsPathLabel(runtimeRoot) {\n return String(runtimeRoot || \".cclaw\") + \"/artifacts/\";\n}\n";
8
+ export declare const SHARED_STAGE_SUPPORT_SNIPPETS = "\nconst STAGE_IDS = [\"brainstorm\", \"scope\", \"design\", \"spec\", \"plan\", \"tdd\", \"review\", \"ship\"];\nconst REVIEW_PROMPT_BY_STAGE = {\n brainstorm: \"brainstorm-self-review.md\",\n scope: \"scope-ceo-review.md\",\n design: \"design-eng-review.md\"\n};\nconst REVIEW_PROMPT_FILES = Object.values(REVIEW_PROMPT_BY_STAGE);\n\nfunction isKnownStageId(stage) {\n return typeof stage === \"string\" && STAGE_IDS.includes(stage);\n}\n\nfunction reviewPromptFileName(stage) {\n if (!isKnownStageId(stage)) return null;\n const name = REVIEW_PROMPT_BY_STAGE[stage];\n return typeof name === \"string\" ? name : null;\n}\n";
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared runtime snippets interpolated into generated hook/plugin scripts.
3
+ *
4
+ * Keep these string helpers minimal and dependency-free so both runtimes
5
+ * (node hooks and OpenCode plugin) stay in sync without duplicating constants.
6
+ */
7
+ export const SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS = `
8
+ function summarizeFlowState(rawState) {
9
+ const state =
10
+ rawState && typeof rawState === "object" && !Array.isArray(rawState)
11
+ ? rawState
12
+ : {};
13
+ return {
14
+ stage: typeof state.currentStage === "string" ? state.currentStage : "none",
15
+ completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
16
+ activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
17
+ };
18
+ }
19
+
20
+ function parseKnowledgeDigest(rawKnowledge, currentStage, maxRows = 6) {
21
+ const text = typeof rawKnowledge === "string" ? rawKnowledge : "";
22
+ if (text.trim().length === 0) {
23
+ return { learningsCount: 0, lines: [] };
24
+ }
25
+ const rows = text
26
+ .split(/\\r?\\n/gu)
27
+ .map((line) => line.trim())
28
+ .filter((line) => line.length > 0);
29
+ let learningsCount = 0;
30
+ const parsedRows = [];
31
+ for (const line of rows) {
32
+ if (line.startsWith("{")) learningsCount += 1;
33
+ try {
34
+ const parsed = JSON.parse(line);
35
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
36
+ parsedRows.push(parsed);
37
+ } catch {
38
+ // ignore malformed knowledge line in digest
39
+ }
40
+ }
41
+ const lines = parsedRows
42
+ .filter((row) => {
43
+ const stage = typeof row.stage === "string" ? row.stage : null;
44
+ return stage === null || stage === currentStage;
45
+ })
46
+ .slice(-maxRows)
47
+ .reverse()
48
+ .map((row) => {
49
+ const confidence = typeof row.confidence === "string" ? row.confidence : "unknown";
50
+ const stage = typeof row.stage === "string" ? row.stage : "global";
51
+ const trigger = typeof row.trigger === "string" ? row.trigger : "trigger";
52
+ const action = typeof row.action === "string" ? row.action : "action";
53
+ return "- [" + confidence + " • " + stage + "] " + trigger + " -> " + action;
54
+ });
55
+ return { learningsCount, lines };
56
+ }
57
+
58
+ function activeArtifactsPathLabel(runtimeRoot) {
59
+ return String(runtimeRoot || ".cclaw") + "/artifacts/";
60
+ }
61
+ `;
62
+ export const SHARED_STAGE_SUPPORT_SNIPPETS = `
63
+ const STAGE_IDS = ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"];
64
+ const REVIEW_PROMPT_BY_STAGE = {
65
+ brainstorm: "brainstorm-self-review.md",
66
+ scope: "scope-ceo-review.md",
67
+ design: "design-eng-review.md"
68
+ };
69
+ const REVIEW_PROMPT_FILES = Object.values(REVIEW_PROMPT_BY_STAGE);
70
+
71
+ function isKnownStageId(stage) {
72
+ return typeof stage === "string" && STAGE_IDS.includes(stage);
73
+ }
74
+
75
+ function reviewPromptFileName(stage) {
76
+ if (!isKnownStageId(stage)) return null;
77
+ const name = REVIEW_PROMPT_BY_STAGE[stage];
78
+ return typeof name === "string" ? name : null;
79
+ }
80
+ `;
@@ -66,7 +66,7 @@ When resuming work after a break:
66
66
 
67
67
  ### Optional session-history scan for compound
68
68
 
69
- During post-ship \`compound_review\`, ask before scanning external session history. If the user opts in, inspect only relevant Cursor/Claude/Codex transcripts for repeated failures or process lessons, summarize matches, and then apply the same overlap/refresh/supersede rules before touching \`.cclaw/knowledge.jsonl\`.
69
+ During post-ship \`post_ship_review\`, ask before scanning external session history. If the user opts in, inspect only relevant Cursor/Claude/Codex transcripts for repeated failures or process lessons, summarize matches, and then apply the same overlap/refresh/supersede rules before touching \`.cclaw/knowledge.jsonl\`.
70
70
 
71
71
  ## Context Management
72
72
 
@@ -10,3 +10,4 @@ export declare function noPlaceholdersBlock(): string;
10
10
  export declare function watchedFailProofBlock(): string;
11
11
  export declare function stageSkillFolder(stage: FlowStage): string;
12
12
  export declare function stageSkillMarkdown(stage: FlowStage, track?: FlowTrack): string;
13
+ export declare function executingWavesSkillMarkdown(): string;
@@ -607,3 +607,53 @@ ${reviewSectionsBlock(reviewLens.reviewSections)}
607
607
  - Keep decisions explicit: context, options, chosen option, rationale, risk, and rollback.
608
608
  `;
609
609
  }
610
+ export function executingWavesSkillMarkdown() {
611
+ return `---
612
+ name: executing-waves
613
+ description: "Execute multi-wave work using existing cclaw run resume + verify-current-state — no new CLI needed."
614
+ ---
615
+
616
+ # Executing Waves (Persistent Multi-Wave Work)
617
+
618
+ ## Overview
619
+
620
+ Long-form work (large refactors, multi-stage uplifts) often spans many waves.
621
+ This skill documents how the controller persists work across waves WITHOUT new
622
+ CLI commands, using existing \`cclaw run resume\` and \`internal verify-current-state\`.
623
+
624
+ ## When to Use
625
+
626
+ - Work spans 2+ commits / waves with cohesion concerns between waves.
627
+ - Each wave has its own stage cycle (brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship).
628
+ - User wants explicit per-wave verification before the next wave starts.
629
+ - Risk of cross-wave drift exists.
630
+
631
+ ## Anti-Pattern
632
+
633
+ - Running many waves linearly without verification between them, accumulating drift.
634
+ - Treating a wave as only a commit boundary without re-verifying upstream decisions.
635
+
636
+ ## Process
637
+
638
+ 1. **Wave Start**: author wave plan as \`.cclaw/wave-plans/<wave-n>.md\` referencing previous wave's ship artifact.
639
+ 2. **Carry-forward Audit**: at brainstorm of the next wave, re-read previous wave ship artifact and explicitly record:
640
+ - Carrying forward: <locked decisions still valid>
641
+ - Drift detected: <decisions no longer valid + reason>
642
+ - Re-scope needed: <yes/no>
643
+ 3. **Resume Path**: if a wave was interrupted mid-stage, \`cclaw run resume\` restores state. Run \`internal verify-current-state\` before continuing.
644
+ 4. **Wave End**: at ship, architect cross-stage verification runs from dispatch matrix. If \`DRIFT_DETECTED\`, fix before ship.
645
+ 5. **Next Wave Trigger**: launch new \`/cc <topic>\` for next wave and reference previous wave ship artifact in upstream handoff.
646
+
647
+ ## Status Markers
648
+
649
+ - \`wave-status: in-progress\` — current stage incomplete.
650
+ - \`wave-status: blocked-by-prev\` — waiting on previous wave verification.
651
+ - \`wave-status: shipped\` — wave shipped, next wave can start.
652
+ - \`wave-status: rolled-back\` — previous wave invalidated, current wave needs rebase.
653
+
654
+ ## Linter Hooks
655
+
656
+ - If multi-wave work is detected (>1 wave-plan files in \`.cclaw/wave-plans/\`), current brainstorm artifact MUST contain \`## Wave Carry-forward\` section with drift audit.
657
+ - If carry-forward drift is missing in multi-wave context, emit \`[P1] wave.drift_unaddressed\`.
658
+ `;
659
+ }