cclaw-cli 0.51.30 → 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 (160) hide show
  1. package/README.md +24 -18
  2. package/dist/artifact-linter/brainstorm.d.ts +2 -0
  3. package/dist/artifact-linter/brainstorm.js +289 -0
  4. package/dist/artifact-linter/design.d.ts +2 -0
  5. package/dist/artifact-linter/design.js +354 -0
  6. package/dist/artifact-linter/plan.d.ts +2 -0
  7. package/dist/artifact-linter/plan.js +183 -0
  8. package/dist/artifact-linter/review-army.d.ts +24 -0
  9. package/dist/artifact-linter/review-army.js +365 -0
  10. package/dist/artifact-linter/review.d.ts +2 -0
  11. package/dist/artifact-linter/review.js +99 -0
  12. package/dist/artifact-linter/scope.d.ts +2 -0
  13. package/dist/artifact-linter/scope.js +125 -0
  14. package/dist/artifact-linter/shared.d.ts +247 -0
  15. package/dist/artifact-linter/shared.js +1517 -0
  16. package/dist/artifact-linter/ship.d.ts +2 -0
  17. package/dist/artifact-linter/ship.js +82 -0
  18. package/dist/artifact-linter/spec.d.ts +2 -0
  19. package/dist/artifact-linter/spec.js +130 -0
  20. package/dist/artifact-linter/tdd.d.ts +2 -0
  21. package/dist/artifact-linter/tdd.js +198 -0
  22. package/dist/artifact-linter.d.ts +4 -76
  23. package/dist/artifact-linter.js +56 -2949
  24. package/dist/cli.d.ts +1 -6
  25. package/dist/cli.js +4 -159
  26. package/dist/codex-feature-flag.d.ts +1 -1
  27. package/dist/codex-feature-flag.js +1 -1
  28. package/dist/config.d.ts +3 -2
  29. package/dist/config.js +67 -3
  30. package/dist/constants.d.ts +1 -7
  31. package/dist/constants.js +10 -15
  32. package/dist/content/cancel-command.js +2 -2
  33. package/dist/content/closeout-guidance.d.ts +1 -1
  34. package/dist/content/closeout-guidance.js +15 -13
  35. package/dist/content/core-agents.d.ts +46 -29
  36. package/dist/content/core-agents.js +216 -82
  37. package/dist/content/decision-protocol.d.ts +1 -1
  38. package/dist/content/decision-protocol.js +1 -1
  39. package/dist/content/diff-command.js +1 -1
  40. package/dist/content/examples.d.ts +0 -3
  41. package/dist/content/examples.js +197 -752
  42. package/dist/content/harness-doc.js +20 -2
  43. package/dist/content/hook-manifest.d.ts +2 -2
  44. package/dist/content/hook-manifest.js +2 -2
  45. package/dist/content/hooks.d.ts +1 -0
  46. package/dist/content/hooks.js +32 -137
  47. package/dist/content/idea.d.ts +60 -0
  48. package/dist/content/idea.js +404 -0
  49. package/dist/content/iron-laws.d.ts +0 -1
  50. package/dist/content/iron-laws.js +31 -16
  51. package/dist/content/learnings.d.ts +2 -4
  52. package/dist/content/learnings.js +11 -27
  53. package/dist/content/meta-skill.js +7 -7
  54. package/dist/content/node-hooks.d.ts +10 -0
  55. package/dist/content/node-hooks.js +163 -95
  56. package/dist/content/opencode-plugin.js +15 -29
  57. package/dist/content/reference-patterns.js +2 -2
  58. package/dist/content/runtime-shared-snippets.d.ts +8 -0
  59. package/dist/content/runtime-shared-snippets.js +80 -0
  60. package/dist/content/session-hooks.js +1 -1
  61. package/dist/content/skills.d.ts +1 -0
  62. package/dist/content/skills.js +69 -7
  63. package/dist/content/stage-schema.js +147 -61
  64. package/dist/content/stages/_lint-metadata/index.js +26 -2
  65. package/dist/content/stages/brainstorm.js +13 -7
  66. package/dist/content/stages/design.js +16 -11
  67. package/dist/content/stages/plan.js +7 -4
  68. package/dist/content/stages/review.js +12 -12
  69. package/dist/content/stages/schema-types.d.ts +2 -2
  70. package/dist/content/stages/scope.js +15 -12
  71. package/dist/content/stages/ship.js +3 -3
  72. package/dist/content/stages/spec.js +9 -3
  73. package/dist/content/stages/tdd.js +14 -4
  74. package/dist/content/start-command.js +11 -10
  75. package/dist/content/status-command.js +5 -5
  76. package/dist/content/subagent-context-skills.js +156 -1
  77. package/dist/content/subagents.d.ts +0 -5
  78. package/dist/content/subagents.js +65 -81
  79. package/dist/content/templates.d.ts +1 -1
  80. package/dist/content/templates.js +187 -154
  81. package/dist/content/tree-command.js +2 -2
  82. package/dist/content/utility-skills.d.ts +2 -2
  83. package/dist/content/utility-skills.js +28 -99
  84. package/dist/content/view-command.js +4 -2
  85. package/dist/delegation.d.ts +2 -0
  86. package/dist/delegation.js +2 -1
  87. package/dist/early-loop.d.ts +66 -0
  88. package/dist/early-loop.js +275 -0
  89. package/dist/flow-state.d.ts +5 -6
  90. package/dist/flow-state.js +4 -6
  91. package/dist/gate-evidence.d.ts +0 -23
  92. package/dist/gate-evidence.js +111 -153
  93. package/dist/harness-adapters.d.ts +2 -2
  94. package/dist/harness-adapters.js +48 -19
  95. package/dist/install.js +190 -32
  96. package/dist/internal/advance-stage/advance.d.ts +50 -0
  97. package/dist/internal/advance-stage/advance.js +479 -0
  98. package/dist/internal/advance-stage/cancel-run.d.ts +8 -0
  99. package/dist/internal/advance-stage/cancel-run.js +19 -0
  100. package/dist/internal/advance-stage/flow-state-coercion.d.ts +3 -0
  101. package/dist/internal/advance-stage/flow-state-coercion.js +81 -0
  102. package/dist/internal/advance-stage/helpers.d.ts +14 -0
  103. package/dist/internal/advance-stage/helpers.js +145 -0
  104. package/dist/internal/advance-stage/hook.d.ts +8 -0
  105. package/dist/internal/advance-stage/hook.js +40 -0
  106. package/dist/internal/advance-stage/parsers.d.ts +54 -0
  107. package/dist/internal/advance-stage/parsers.js +307 -0
  108. package/dist/internal/advance-stage/review-loop.d.ts +7 -0
  109. package/dist/internal/advance-stage/review-loop.js +161 -0
  110. package/dist/internal/advance-stage/rewind.d.ts +14 -0
  111. package/dist/internal/advance-stage/rewind.js +108 -0
  112. package/dist/internal/advance-stage/start-flow.d.ts +11 -0
  113. package/dist/internal/advance-stage/start-flow.js +136 -0
  114. package/dist/internal/advance-stage/verify.d.ts +29 -0
  115. package/dist/internal/advance-stage/verify.js +225 -0
  116. package/dist/internal/advance-stage.js +21 -1470
  117. package/dist/internal/compound-readiness.d.ts +1 -1
  118. package/dist/internal/compound-readiness.js +2 -2
  119. package/dist/internal/early-loop-status.d.ts +7 -0
  120. package/dist/internal/early-loop-status.js +90 -0
  121. package/dist/internal/runtime-integrity.d.ts +7 -0
  122. package/dist/internal/runtime-integrity.js +288 -0
  123. package/dist/internal/tdd-red-evidence.js +1 -1
  124. package/dist/knowledge-store.d.ts +5 -28
  125. package/dist/knowledge-store.js +57 -84
  126. package/dist/managed-resources.js +24 -2
  127. package/dist/policy.js +7 -9
  128. package/dist/retro-gate.js +8 -90
  129. package/dist/run-archive.d.ts +1 -1
  130. package/dist/run-archive.js +13 -16
  131. package/dist/run-persistence.js +20 -15
  132. package/dist/runtime/run-hook.entry.d.ts +3 -0
  133. package/dist/runtime/run-hook.entry.js +5 -0
  134. package/dist/runtime/run-hook.mjs +9477 -0
  135. package/dist/tdd-cycle.d.ts +3 -3
  136. package/dist/tdd-cycle.js +1 -1
  137. package/dist/types.d.ts +18 -10
  138. package/package.json +4 -2
  139. package/dist/content/hook-inline-snippets.d.ts +0 -83
  140. package/dist/content/hook-inline-snippets.js +0 -302
  141. package/dist/content/ideate-command.d.ts +0 -8
  142. package/dist/content/ideate-command.js +0 -315
  143. package/dist/content/ideate-frames.d.ts +0 -31
  144. package/dist/content/ideate-frames.js +0 -140
  145. package/dist/content/ideate-ranking.d.ts +0 -25
  146. package/dist/content/ideate-ranking.js +0 -65
  147. package/dist/content/next-command.d.ts +0 -20
  148. package/dist/content/next-command.js +0 -298
  149. package/dist/content/seed-shelf.d.ts +0 -36
  150. package/dist/content/seed-shelf.js +0 -301
  151. package/dist/content/stage-common-guidance.d.ts +0 -1
  152. package/dist/content/stage-common-guidance.js +0 -106
  153. package/dist/doctor-registry.d.ts +0 -10
  154. package/dist/doctor-registry.js +0 -186
  155. package/dist/doctor.d.ts +0 -17
  156. package/dist/doctor.js +0 -2201
  157. package/dist/internal/hook-manifest.d.ts +0 -16
  158. package/dist/internal/hook-manifest.js +0 -77
  159. package/dist/trace-matrix.d.ts +0 -27
  160. package/dist/trace-matrix.js +0 -226
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD } from "../config.js";
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 } 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];
@@ -51,6 +51,12 @@ export function nodeHookRuntimeScript(options = {}) {
51
51
  options.compoundRecurrenceThreshold >= 1
52
52
  ? options.compoundRecurrenceThreshold
53
53
  : DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
54
+ const earlyLoopEnabled = options.earlyLoopEnabled !== false;
55
+ const earlyLoopMaxIterations = typeof options.earlyLoopMaxIterations === "number" &&
56
+ Number.isInteger(options.earlyLoopMaxIterations) &&
57
+ options.earlyLoopMaxIterations >= 1
58
+ ? options.earlyLoopMaxIterations
59
+ : DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
54
60
  const cliRuntime = resolveCliRuntimeForGeneratedHook();
55
61
  return `#!/usr/bin/env node
56
62
  import fs from "node:fs/promises";
@@ -73,9 +79,14 @@ const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = ${JSON.stringify(tddProductionPathP
73
79
  const COMPOUND_RECURRENCE_THRESHOLD = ${JSON.stringify(compoundRecurrenceThreshold)};
74
80
  const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD)};
75
81
  const SMALL_PROJECT_RECURRENCE_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_RECURRENCE_THRESHOLD)};
82
+ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
83
+ const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
76
84
  const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
77
85
  const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
78
86
 
87
+ ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
88
+ ${SHARED_STAGE_SUPPORT_SNIPPETS}
89
+
79
90
  function resolveStrictness() {
80
91
  return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
81
92
  }
@@ -253,7 +264,7 @@ async function readJsonFile(filePath, fallback = {}, options = {}) {
253
264
  } catch (parseErr) {
254
265
  // Emit a diagnostic breadcrumb instead of silently returning fallback.
255
266
  // The hook must still continue (soft-fail), but the corruption is
256
- // now visible in \`state/hook-errors.jsonl\` and to \`cclaw doctor\`.
267
+ // now visible in \`state/hook-errors.jsonl\` and to \`npx cclaw-cli sync\`.
257
268
  if (options.root) {
258
269
  await recordHookError(
259
270
  options.root,
@@ -419,6 +430,73 @@ async function runCclawInternal(root, args, options = {}) {
419
430
  });
420
431
  }
421
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
+
422
500
  function detectHarness(env) {
423
501
  if (env.CLAUDE_PROJECT_DIR) return "claude";
424
502
  if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
@@ -684,7 +762,6 @@ function detectTargetStage(payloadLower) {
684
762
  }
685
763
 
686
764
  function isFlowProgressionCommand(payloadLower) {
687
- if (/(\\/cc-next|cc-next)([^a-z0-9_-]|$)/u.test(payloadLower)) return true;
688
765
  return /\\/cc([^a-z0-9_-]|$)/u.test(payloadLower);
689
766
  }
690
767
 
@@ -832,12 +909,12 @@ async function readFlowState(root) {
832
909
  // empty object. Silent fallbacks used to mask stale CLI+hook drift.
833
910
  const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
834
911
  const obj = toObject(parsed) || {};
835
- const completed = Array.isArray(obj.completedStages) ? obj.completedStages : [];
912
+ const summary = summarizeFlowState(obj);
836
913
  return {
837
914
  filePath: statePath,
838
- currentStage: typeof obj.currentStage === "string" ? obj.currentStage : "none",
839
- activeRunId: typeof obj.activeRunId === "string" ? obj.activeRunId : "active",
840
- completedCount: completed.length,
915
+ currentStage: summary.stage,
916
+ activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
917
+ completedCount: summary.completed,
841
918
  raw: obj
842
919
  };
843
920
  }
@@ -850,44 +927,16 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
850
927
  const raw = typeof prereadRaw === "string"
851
928
  ? prereadRaw
852
929
  : await readTextFile(knowledgeFile, "");
853
- const lines = raw.split(/\\r?\\n/gu).map((line) => line.trim()).filter((line) => line.length > 0);
854
- let learningsCount = 0;
855
- const parsedRows = [];
856
- for (const line of lines) {
857
- if (line.startsWith("{")) learningsCount += 1;
858
- try {
859
- const parsed = JSON.parse(line);
860
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
861
- parsedRows.push(parsed);
862
- } catch {
863
- // ignore malformed knowledge line in digest
864
- }
865
- }
866
- const relevant = parsedRows
867
- .filter((row) => {
868
- const stage = typeof row.stage === "string" ? row.stage : null;
869
- return stage === null || stage === currentStage;
870
- })
871
- .slice(-6)
872
- .reverse()
873
- .map((row) => {
874
- const confidence = typeof row.confidence === "string" ? row.confidence : "unknown";
875
- const stage = typeof row.stage === "string" ? row.stage : "global";
876
- const domain = typeof row.domain === "string" ? row.domain : "general";
877
- const trigger = typeof row.trigger === "string" ? row.trigger : "trigger";
878
- const action = typeof row.action === "string" ? row.action : "action";
879
- return "- [" + confidence + " • " + stage + " • " + domain + "] " + trigger + " -> " + action;
880
- });
930
+ const digest = parseKnowledgeDigest(raw, currentStage, 6);
881
931
  return {
882
- digestLines: relevant,
883
- learningsCount
932
+ digestLines: digest.lines,
933
+ learningsCount: digest.learningsCount
884
934
  };
885
935
  }
886
936
 
887
937
  async function readStageSupportContext(root, currentStage) {
888
- const stage = typeof currentStage === "string" ? currentStage : "";
889
- const validStages = new Set(["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"]);
890
- if (!validStages.has(stage)) return [];
938
+ if (!isKnownStageId(currentStage)) return [];
939
+ const stage = currentStage;
891
940
 
892
941
  const parts = [];
893
942
  const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
@@ -899,12 +948,7 @@ async function readStageSupportContext(root, currentStage) {
899
948
  );
900
949
  }
901
950
 
902
- const reviewPromptByStage = {
903
- brainstorm: "brainstorm-self-review.md",
904
- scope: "scope-ceo-review.md",
905
- design: "design-eng-review.md"
906
- };
907
- const promptName = reviewPromptByStage[stage];
951
+ const promptName = reviewPromptFileName(stage);
908
952
  if (typeof promptName === "string") {
909
953
  const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
910
954
  const prompt = (await readTextFile(promptPath, "")).trim();
@@ -942,21 +986,26 @@ async function handleSessionStart(runtime) {
942
986
  // both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
943
987
  // we are in tdd — other stages skip the write to keep the file stable.
944
988
  let ralphLoopLine = "";
989
+ let earlyLoopLine = "";
945
990
  if (state.currentStage === "tdd") {
946
991
  try {
947
- const ralphStatus = await computeRalphLoopStatusInline(stateDir, state.activeRunId);
948
- await writeJsonFile(path.join(stateDir, "ralph-loop.json"), ralphStatus);
949
- const redOpen = ralphStatus.redOpenSlices.length > 0
950
- ? ralphStatus.redOpenSlices.join(",")
951
- : "none";
952
- ralphLoopLine = "Ralph Loop: iter=" + String(ralphStatus.loopIteration) +
953
- ", slices=" + String(ralphStatus.sliceCount) +
954
- ", acClosed=" + String(ralphStatus.acClosed.length) +
955
- ", 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);
956
1005
  } catch (err) {
957
1006
  // Best-effort — a malformed cycle log should never break
958
1007
  // session-start. But we DO leave a breadcrumb in
959
- // hook-errors.jsonl so \`cclaw doctor\` can surface chronic
1008
+ // hook-errors.jsonl so \`npx cclaw-cli sync\` can surface chronic
960
1009
  // failures (previously this was a silent swallow).
961
1010
  await recordHookError(
962
1011
  runtime.root,
@@ -965,44 +1014,63 @@ async function handleSessionStart(runtime) {
965
1014
  );
966
1015
  }
967
1016
  }
1017
+ if (
1018
+ EARLY_LOOP_ENABLED &&
1019
+ (state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
1020
+ ) {
1021
+ try {
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 }
1034
+ );
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);
1043
+ } catch (err) {
1044
+ await recordHookError(
1045
+ runtime.root,
1046
+ "session-start:early-loop",
1047
+ err instanceof Error ? err.message : String(err)
1048
+ );
1049
+ }
1050
+ }
968
1051
 
969
1052
  // Keep compound-readiness.json fresh on every session-start (cheap derived
970
1053
  // summary). Surface a one-line nudge only from review and ship stages
971
1054
  // where lifting becomes relevant; earlier stages update the file silently.
972
1055
  let compoundReadinessLine = "";
973
1056
  try {
974
- let readiness = null;
1057
+ const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
975
1058
  const internalReadiness = await runCclawInternal(
976
1059
  runtime.root,
977
- ["compound-readiness", "--json"],
1060
+ shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
978
1061
  { captureStdout: true }
979
1062
  );
980
- if (internalReadiness.code === 0 && internalReadiness.stdout.trim().length > 0) {
981
- try {
982
- const parsed = JSON.parse(internalReadiness.stdout);
983
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
984
- readiness = parsed;
985
- }
986
- } catch {
987
- readiness = null;
988
- }
1063
+ if (internalReadiness.code !== 0) {
1064
+ throw new Error(summarizeInternalFailure("compound-readiness", internalReadiness));
989
1065
  }
990
- if (!readiness) {
991
- const archivedRunsCount = await countArchivedRunsInline(runtime.root);
992
- readiness = await computeCompoundReadinessInline(runtime.root, {
993
- prereadRaw: knowledgeRaw,
994
- ...(typeof archivedRunsCount === "number" ? { archivedRunsCount } : {})
995
- });
996
- await writeJsonFile(path.join(stateDir, "compound-readiness.json"), readiness);
997
- }
998
- if (state.currentStage === "review" || state.currentStage === "ship") {
999
- compoundReadinessLine = formatCompoundReadinessLineInline(toObject(readiness) || {});
1066
+ if (shouldShowReadiness) {
1067
+ compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
1000
1068
  }
1001
1069
  } catch (err) {
1002
1070
  // Best-effort — a malformed knowledge.jsonl must never break
1003
1071
  // session-start. But we DO leave a breadcrumb in
1004
1072
  // hook-errors.jsonl so config/IO problems become visible in
1005
- // \`cclaw doctor\` instead of silently degrading readiness output.
1073
+ // \`npx cclaw-cli sync\` instead of silently degrading readiness output.
1006
1074
  await recordHookError(
1007
1075
  runtime.root,
1008
1076
  "session-start:compound-readiness",
@@ -1034,14 +1102,17 @@ async function handleSessionStart(runtime) {
1034
1102
  "/8 completed, run=" +
1035
1103
  state.activeRunId +
1036
1104
  "). Active artifacts: " +
1037
- RUNTIME_ROOT +
1038
- "/artifacts/. Learnings: " +
1105
+ activeArtifactsPathLabel(RUNTIME_ROOT) +
1106
+ " Learnings: " +
1039
1107
  String(knowledge.learningsCount) +
1040
1108
  " entries."
1041
1109
  ];
1042
1110
  if (ralphLoopLine.length > 0) {
1043
1111
  parts.push(ralphLoopLine);
1044
1112
  }
1113
+ if (earlyLoopLine.length > 0) {
1114
+ parts.push(earlyLoopLine);
1115
+ }
1045
1116
  if (compoundReadinessLine.length > 0) {
1046
1117
  parts.push(compoundReadinessLine);
1047
1118
  }
@@ -1049,7 +1120,7 @@ async function handleSessionStart(runtime) {
1049
1120
  parts.push(
1050
1121
  "Stale stages pending acknowledgement: " +
1051
1122
  staleStageNames.join(", ") +
1052
- " (use cclaw internal rewind --ack <stage> after redo)."
1123
+ " (use npx cclaw-cli internal rewind --ack <stage> after redo)."
1053
1124
  );
1054
1125
  }
1055
1126
  if (knowledge.digestLines.length > 0) {
@@ -1137,7 +1208,7 @@ async function handleStopHandoff(runtime) {
1137
1208
  const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
1138
1209
  const closeoutContext =
1139
1210
  state.currentStage === "ship" || shipSubstate !== "idle"
1140
- ? " 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."
1141
1212
  : "";
1142
1213
 
1143
1214
  const message =
@@ -1214,15 +1285,6 @@ async function handlePromptGuard(runtime) {
1214
1285
  return 0;
1215
1286
  }
1216
1287
 
1217
- // Inline mirrors of canonical CLI computations (compound-readiness,
1218
- // ralph-loop) are factored into src/content/hook-inline-snippets.ts so
1219
- // this 2000+-line file no longer owns their bodies. Each snippet carries
1220
- // an explicit "mirrors X, parity enforced by Y" comment in the snippets
1221
- // module. Parity is enforced by tests/unit/ralph-loop-parity.test.ts.
1222
- ${HOOK_INLINE_SHARED_HELPERS}
1223
- ${COMPOUND_READINESS_INLINE_SOURCE}
1224
- ${RALPH_LOOP_INLINE_SOURCE}
1225
-
1226
1288
  async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1227
1289
  const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
1228
1290
  for (const line of cycleRaw.split(/\\r?\\n/gu)) {
@@ -1401,7 +1463,7 @@ async function handleWorkflowGuard(runtime) {
1401
1463
  /^(read|readfile|open|view|cat|shell|runcommand|run_command|execcommand|exec_command|terminal)$/u.test(
1402
1464
  toolLower
1403
1465
  ) &&
1404
- /(\\.cclaw\\/state\\/flow-state\\.json|cclaw doctor|cclaw sync)/u.test(payloadLower);
1466
+ /(\\.cclaw\\/state\\/flow-state\\.json|npx cclaw-cli sync|npx cclaw-cli sync|npx cclaw-cli sync|cclaw sync)/u.test(payloadLower);
1405
1467
  if (shouldRecordFlowRead) {
1406
1468
  await writeJsonFile(guardStateFile, {
1407
1469
  ...guardState,
@@ -1434,8 +1496,14 @@ async function handleWorkflowGuard(runtime) {
1434
1496
  // writes that actually belonged to a new, not-yet-red S-2. Now
1435
1497
  // we reuse the canonical Ralph Loop status: if NO slice has an
1436
1498
  // open RED, we block.
1437
- const ralphStatus = await computeRalphLoopStatusInline(stateDir, currentRun);
1438
- 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) {
1439
1507
  reasons.push("tdd_write_without_open_red");
1440
1508
  }
1441
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,19 +105,19 @@ 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);
131
117
  if (stageSupport.length > 0) parts.push(...stageSupport);
132
118
 
133
119
  parts.push(
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. If this plugin does not load, run \`cclaw sync\`, verify opencode.json(.c) includes the cclaw plugin registration, then run \`cclaw doctor --explain\`. Direct JSONL append is only for explicit manual learnings operations."
120
+ "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. If this plugin does not load, run \`npx cclaw-cli sync\`, verify opencode.json(.c) includes the cclaw plugin registration, then run \`npx cclaw-cli sync\`. Direct JSONL append is only for explicit manual learnings operations."
135
121
  );
136
122
 
137
123
  const meta = (await readFileText(metaSkillPath)).trim();
@@ -510,7 +496,7 @@ export default function cclawPlugin(ctx) {
510
496
  notInitializedAdvised = true;
511
497
  logToFile(
512
498
  "guards skipped: cclaw is not initialized in this project. " +
513
- "Run \`cclaw init\` in " + root + " to activate flow enforcement."
499
+ "Run \`npx cclaw-cli init\` in " + root + " to activate flow enforcement."
514
500
  );
515
501
  }
516
502
 
@@ -697,7 +683,7 @@ export default function cclawPlugin(ctx) {
697
683
  throw new Error(
698
684
  "cclaw " + failed + " blocked tool.execute.before.\\n" +
699
685
  "Reason: " + detail + "\\n" +
700
- "Diagnose: run \`npx cclaw-cli doctor\` in project root.\\n" +
686
+ "Diagnose: run \`npx cclaw-cli sync\` in project root.\\n" +
701
687
  "Bypass (temporary): export CCLAW_DISABLE=1 before starting OpenCode,\\n" +
702
688
  "or set \`strictness: advisory\` in .cclaw/config.yaml."
703
689
  );
@@ -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;