cclaw-cli 0.55.2 → 2.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.
- package/README.md +3 -3
- package/dist/artifact-linter/brainstorm.js +59 -1
- package/dist/artifact-linter/design.js +46 -1
- package/dist/artifact-linter/plan.js +22 -1
- package/dist/artifact-linter/review.js +35 -1
- package/dist/artifact-linter/scope.js +33 -9
- package/dist/artifact-linter/shared.d.ts +12 -10
- package/dist/artifact-linter/shared.js +102 -41
- package/dist/artifact-linter/ship.js +36 -0
- package/dist/artifact-linter/spec.js +23 -1
- package/dist/artifact-linter/tdd.js +74 -0
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/artifact-linter.js +11 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/content/closeout-guidance.d.ts +1 -1
- package/dist/content/closeout-guidance.js +10 -11
- package/dist/content/core-agents.d.ts +35 -36
- package/dist/content/core-agents.js +189 -99
- package/dist/content/diff-command.js +1 -1
- package/dist/content/examples.d.ts +0 -3
- package/dist/content/examples.js +197 -752
- package/dist/content/hook-events.js +1 -2
- package/dist/content/hook-manifest.d.ts +3 -4
- package/dist/content/hook-manifest.js +22 -25
- package/dist/content/hooks.js +54 -14
- package/dist/content/idea.d.ts +60 -0
- package/dist/content/idea.js +404 -0
- package/dist/content/learnings.d.ts +2 -4
- package/dist/content/learnings.js +10 -26
- package/dist/content/meta-skill.js +4 -3
- package/dist/content/node-hooks.js +368 -164
- package/dist/content/observe.js +3 -3
- package/dist/content/opencode-plugin.js +12 -32
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/runtime-shared-snippets.d.ts +8 -0
- package/dist/content/runtime-shared-snippets.js +80 -0
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/skills-elicitation.d.ts +1 -0
- package/dist/content/skills-elicitation.js +123 -0
- package/dist/content/skills.d.ts +1 -0
- package/dist/content/skills.js +54 -2
- package/dist/content/stage-schema.js +107 -63
- package/dist/content/stages/brainstorm.js +7 -3
- package/dist/content/stages/design.js +4 -0
- package/dist/content/stages/review.js +8 -8
- package/dist/content/stages/schema-types.d.ts +2 -2
- package/dist/content/stages/scope.js +7 -3
- package/dist/content/stages/ship.js +1 -1
- package/dist/content/start-command.js +4 -4
- package/dist/content/status-command.js +3 -3
- package/dist/content/subagent-context-skills.js +156 -1
- package/dist/content/subagents.d.ts +0 -5
- package/dist/content/subagents.js +12 -82
- package/dist/content/templates.js +108 -6
- package/dist/content/utility-skills.js +26 -97
- package/dist/flow-state.d.ts +12 -6
- package/dist/flow-state.js +5 -6
- package/dist/gate-evidence.d.ts +0 -31
- package/dist/gate-evidence.js +3 -181
- package/dist/harness-adapters.js +1 -1
- package/dist/hook-schemas/claude-hooks.v1.json +2 -3
- package/dist/hook-schemas/codex-hooks.v1.json +1 -1
- package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
- package/dist/install.js +50 -7
- package/dist/internal/advance-stage/advance.js +22 -2
- package/dist/internal/advance-stage/parsers.d.ts +1 -0
- package/dist/internal/advance-stage/parsers.js +6 -0
- package/dist/internal/advance-stage/review-loop.js +1 -10
- package/dist/knowledge-store.d.ts +2 -20
- package/dist/knowledge-store.js +43 -57
- package/dist/policy.js +3 -3
- package/dist/retro-gate.js +8 -90
- package/dist/run-archive.js +1 -4
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +43 -111
- package/dist/runtime/run-hook.entry.d.ts +3 -0
- package/dist/runtime/run-hook.entry.js +5 -0
- package/dist/runtime/run-hook.mjs +9647 -0
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- package/package.json +4 -2
- package/dist/content/hook-inline-snippets.d.ts +0 -96
- package/dist/content/hook-inline-snippets.js +0 -515
- package/dist/content/idea-command.d.ts +0 -8
- package/dist/content/idea-command.js +0 -322
- package/dist/content/idea-frames.d.ts +0 -31
- package/dist/content/idea-frames.js +0 -140
- package/dist/content/idea-ranking.d.ts +0 -25
- package/dist/content/idea-ranking.js +0 -65
- package/dist/trace-matrix.d.ts +0 -27
- 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 {
|
|
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];
|
|
@@ -83,6 +83,13 @@ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
|
|
|
83
83
|
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
|
+
const SESSION_DIGEST_SCHEMA_VERSION = 1;
|
|
87
|
+
const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
|
|
88
|
+
const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
|
|
89
|
+
const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
|
|
90
|
+
|
|
91
|
+
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
92
|
+
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
86
93
|
|
|
87
94
|
function resolveStrictness() {
|
|
88
95
|
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
@@ -427,6 +434,73 @@ async function runCclawInternal(root, args, options = {}) {
|
|
|
427
434
|
});
|
|
428
435
|
}
|
|
429
436
|
|
|
437
|
+
function compactStderr(value) {
|
|
438
|
+
const raw = typeof value === "string" ? value : "";
|
|
439
|
+
return raw.replace(/\\s+/gu, " ").trim();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function summarizeInternalFailure(operation, result) {
|
|
443
|
+
const detail = compactStderr(result && typeof result === "object" ? result.stderr : "");
|
|
444
|
+
return detail.length > 0 ? operation + ": " + detail : operation + " failed";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function parseJsonStdoutObject(result) {
|
|
448
|
+
const raw = typeof (result && result.stdout) === "string" ? result.stdout.trim() : "";
|
|
449
|
+
if (raw.length === 0) return null;
|
|
450
|
+
try {
|
|
451
|
+
return toObject(JSON.parse(raw));
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function firstStdoutLine(value) {
|
|
458
|
+
const raw = typeof value === "string" ? value : "";
|
|
459
|
+
const lines = raw
|
|
460
|
+
.split(/\\r?\\n/gu)
|
|
461
|
+
.map((line) => line.trim())
|
|
462
|
+
.filter((line) => line.length > 0);
|
|
463
|
+
return lines[0] || "";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function formatRalphLoopStatusLineFromJson(status) {
|
|
467
|
+
const redOpenSlices = Array.isArray(status.redOpenSlices)
|
|
468
|
+
? status.redOpenSlices.filter((value) => typeof value === "string")
|
|
469
|
+
: [];
|
|
470
|
+
const redOpen = redOpenSlices.length > 0 ? redOpenSlices.join(",") : "none";
|
|
471
|
+
const loopIteration =
|
|
472
|
+
typeof status.loopIteration === "number" && Number.isFinite(status.loopIteration)
|
|
473
|
+
? Math.trunc(status.loopIteration)
|
|
474
|
+
: 0;
|
|
475
|
+
const sliceCount =
|
|
476
|
+
typeof status.sliceCount === "number" && Number.isFinite(status.sliceCount)
|
|
477
|
+
? Math.trunc(status.sliceCount)
|
|
478
|
+
: 0;
|
|
479
|
+
const acClosed = Array.isArray(status.acClosed) ? status.acClosed.length : 0;
|
|
480
|
+
return "Ralph Loop: iter=" + String(loopIteration) +
|
|
481
|
+
", slices=" + String(sliceCount) +
|
|
482
|
+
", acClosed=" + String(acClosed) +
|
|
483
|
+
", redOpen=" + redOpen;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function formatEarlyLoopStatusLineFromJson(status) {
|
|
487
|
+
const stage = typeof status.stage === "string" ? status.stage : "unknown";
|
|
488
|
+
const iteration =
|
|
489
|
+
typeof status.iteration === "number" && Number.isFinite(status.iteration)
|
|
490
|
+
? Math.trunc(status.iteration)
|
|
491
|
+
: 0;
|
|
492
|
+
const maxIterations =
|
|
493
|
+
typeof status.maxIterations === "number" && Number.isFinite(status.maxIterations)
|
|
494
|
+
? Math.trunc(status.maxIterations)
|
|
495
|
+
: EARLY_LOOP_MAX_ITERATIONS;
|
|
496
|
+
const openConcerns = Array.isArray(status.openConcerns) ? status.openConcerns.length : 0;
|
|
497
|
+
const convergence = status.convergenceTripped === true ? "tripped" : "clear";
|
|
498
|
+
return "Early Loop: stage=" + stage +
|
|
499
|
+
", iter=" + String(iteration) + "/" + String(maxIterations) +
|
|
500
|
+
", open=" + String(openConcerns) +
|
|
501
|
+
", convergence=" + convergence;
|
|
502
|
+
}
|
|
503
|
+
|
|
430
504
|
function detectHarness(env) {
|
|
431
505
|
if (env.CLAUDE_PROJECT_DIR) return "claude";
|
|
432
506
|
if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
|
|
@@ -438,9 +512,10 @@ function hookEventNameForOutput(hookName) {
|
|
|
438
512
|
if (hookName === "session-start") return "SessionStart";
|
|
439
513
|
if (hookName === "prompt-guard") return "PreToolUse";
|
|
440
514
|
if (hookName === "workflow-guard") return "PreToolUse";
|
|
515
|
+
if (hookName === "pre-tool-pipeline") return "PreToolUse";
|
|
516
|
+
if (hookName === "prompt-pipeline") return "UserPromptSubmit";
|
|
441
517
|
if (hookName === "context-monitor") return "PostToolUse";
|
|
442
518
|
if (hookName === "stop-handoff") return "Stop";
|
|
443
|
-
if (hookName === "pre-compact") return "PreCompact";
|
|
444
519
|
if (hookName === "verify-current-state") return "UserPromptSubmit";
|
|
445
520
|
return "SessionStart";
|
|
446
521
|
}
|
|
@@ -839,16 +914,215 @@ async function readFlowState(root) {
|
|
|
839
914
|
// empty object. Silent fallbacks used to mask stale CLI+hook drift.
|
|
840
915
|
const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
|
|
841
916
|
const obj = toObject(parsed) || {};
|
|
842
|
-
const
|
|
917
|
+
const summary = summarizeFlowState(obj);
|
|
843
918
|
return {
|
|
844
919
|
filePath: statePath,
|
|
845
|
-
currentStage:
|
|
846
|
-
activeRunId:
|
|
847
|
-
completedCount: completed
|
|
920
|
+
currentStage: summary.stage,
|
|
921
|
+
activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
|
|
922
|
+
completedCount: summary.completed,
|
|
848
923
|
raw: obj
|
|
849
924
|
};
|
|
850
925
|
}
|
|
851
926
|
|
|
927
|
+
async function readFileMtimeMs(filePath) {
|
|
928
|
+
try {
|
|
929
|
+
const stat = await fs.stat(filePath);
|
|
930
|
+
if (!stat.isFile()) return 0;
|
|
931
|
+
return Math.trunc(stat.mtimeMs);
|
|
932
|
+
} catch {
|
|
933
|
+
return 0;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function parseNumericMs(value) {
|
|
938
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
939
|
+
? Math.trunc(value)
|
|
940
|
+
: -1;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
|
|
944
|
+
const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
945
|
+
const cache = toObject(await readJsonFile(cachePath, {})) || {};
|
|
946
|
+
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
947
|
+
const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
|
|
948
|
+
const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
|
|
949
|
+
const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
|
|
950
|
+
if (!fresh) {
|
|
951
|
+
return {
|
|
952
|
+
ralphLoopLine: "",
|
|
953
|
+
earlyLoopLine: "",
|
|
954
|
+
compoundReadinessLine: "",
|
|
955
|
+
fresh: false
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
|
|
960
|
+
earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
|
|
961
|
+
compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
|
|
962
|
+
fresh: true
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
|
|
967
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
968
|
+
let ralphLoopLine = "";
|
|
969
|
+
let earlyLoopLine = "";
|
|
970
|
+
let compoundReadinessLine = "";
|
|
971
|
+
|
|
972
|
+
if (state.currentStage === "tdd") {
|
|
973
|
+
try {
|
|
974
|
+
const internalRalph = await runCclawInternal(
|
|
975
|
+
root,
|
|
976
|
+
["tdd-loop-status", "--json", "--write"],
|
|
977
|
+
{ captureStdout: true }
|
|
978
|
+
);
|
|
979
|
+
if (internalRalph.code !== 0) {
|
|
980
|
+
throw new Error(summarizeInternalFailure("tdd-loop-status", internalRalph));
|
|
981
|
+
}
|
|
982
|
+
const ralphStatus = parseJsonStdoutObject(internalRalph);
|
|
983
|
+
if (!ralphStatus) {
|
|
984
|
+
throw new Error("tdd-loop-status returned empty or malformed JSON");
|
|
985
|
+
}
|
|
986
|
+
ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
|
|
987
|
+
} catch (err) {
|
|
988
|
+
await recordHookError(
|
|
989
|
+
root,
|
|
990
|
+
"session-start:ralph-loop",
|
|
991
|
+
err instanceof Error ? err.message : String(err)
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (
|
|
996
|
+
EARLY_LOOP_ENABLED &&
|
|
997
|
+
(state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
|
|
998
|
+
) {
|
|
999
|
+
try {
|
|
1000
|
+
const internalEarly = await runCclawInternal(
|
|
1001
|
+
root,
|
|
1002
|
+
[
|
|
1003
|
+
"early-loop-status",
|
|
1004
|
+
"--json",
|
|
1005
|
+
"--write",
|
|
1006
|
+
"--stage",
|
|
1007
|
+
state.currentStage,
|
|
1008
|
+
"--run-id",
|
|
1009
|
+
state.activeRunId
|
|
1010
|
+
],
|
|
1011
|
+
{ captureStdout: true }
|
|
1012
|
+
);
|
|
1013
|
+
if (internalEarly.code !== 0) {
|
|
1014
|
+
throw new Error(summarizeInternalFailure("early-loop-status", internalEarly));
|
|
1015
|
+
}
|
|
1016
|
+
const earlyLoopStatus = parseJsonStdoutObject(internalEarly);
|
|
1017
|
+
if (!earlyLoopStatus) {
|
|
1018
|
+
throw new Error("early-loop-status returned empty or malformed JSON");
|
|
1019
|
+
}
|
|
1020
|
+
earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
await recordHookError(
|
|
1023
|
+
root,
|
|
1024
|
+
"session-start:early-loop",
|
|
1025
|
+
err instanceof Error ? err.message : String(err)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
|
|
1032
|
+
const internalReadiness = await runCclawInternal(
|
|
1033
|
+
root,
|
|
1034
|
+
shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
|
|
1035
|
+
{ captureStdout: true }
|
|
1036
|
+
);
|
|
1037
|
+
if (internalReadiness.code !== 0) {
|
|
1038
|
+
throw new Error(summarizeInternalFailure("compound-readiness", internalReadiness));
|
|
1039
|
+
}
|
|
1040
|
+
if (shouldShowReadiness) {
|
|
1041
|
+
compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
|
|
1042
|
+
}
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
await recordHookError(
|
|
1045
|
+
root,
|
|
1046
|
+
"session-start:compound-readiness",
|
|
1047
|
+
err instanceof Error ? err.message : String(err)
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1052
|
+
await writeJsonFile(digestPath, {
|
|
1053
|
+
schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
|
|
1054
|
+
generatedAt: new Date().toISOString(),
|
|
1055
|
+
flowStateMtimeMs,
|
|
1056
|
+
currentStage: state.currentStage,
|
|
1057
|
+
activeRunId: state.activeRunId,
|
|
1058
|
+
ralphLoopLine,
|
|
1059
|
+
earlyLoopLine,
|
|
1060
|
+
compoundReadinessLine
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
|
|
1065
|
+
if (flowStateMtimeMs <= 0) return;
|
|
1066
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1067
|
+
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1068
|
+
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
1069
|
+
|
|
1070
|
+
const cache = toObject(await readJsonFile(digestPath, {})) || {};
|
|
1071
|
+
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
1072
|
+
if (cachedMtimeMs === flowStateMtimeMs) return;
|
|
1073
|
+
|
|
1074
|
+
const marker = toObject(await readJsonFile(markerPath, {})) || {};
|
|
1075
|
+
const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
|
|
1076
|
+
const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
|
|
1077
|
+
const markerFresh =
|
|
1078
|
+
markerMtimeMs === flowStateMtimeMs &&
|
|
1079
|
+
markerStartedAtMs > 0 &&
|
|
1080
|
+
Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
|
|
1081
|
+
if (markerFresh) return;
|
|
1082
|
+
|
|
1083
|
+
await writeJsonFile(markerPath, {
|
|
1084
|
+
flowStateMtimeMs,
|
|
1085
|
+
startedAtMs: Date.now(),
|
|
1086
|
+
currentStage: state.currentStage,
|
|
1087
|
+
activeRunId: state.activeRunId
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
|
|
1092
|
+
cwd: runtime.root,
|
|
1093
|
+
stdio: "ignore",
|
|
1094
|
+
windowsHide: true,
|
|
1095
|
+
detached: true,
|
|
1096
|
+
env: {
|
|
1097
|
+
...process.env,
|
|
1098
|
+
CCLAW_PROJECT_ROOT: runtime.root,
|
|
1099
|
+
CCLAW_BG_WORKER: "1"
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
child.unref();
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
1105
|
+
await recordHookError(
|
|
1106
|
+
runtime.root,
|
|
1107
|
+
"session-start:spawn-refresh",
|
|
1108
|
+
err instanceof Error ? err.message : String(err)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async function handleSessionStartRefresh(runtime) {
|
|
1114
|
+
const state = await readFlowState(runtime.root);
|
|
1115
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1116
|
+
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
1117
|
+
try {
|
|
1118
|
+
const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
|
|
1119
|
+
await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
|
|
1120
|
+
} finally {
|
|
1121
|
+
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
1122
|
+
}
|
|
1123
|
+
return 0;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
852
1126
|
|
|
853
1127
|
async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
854
1128
|
const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
@@ -857,44 +1131,16 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
|
857
1131
|
const raw = typeof prereadRaw === "string"
|
|
858
1132
|
? prereadRaw
|
|
859
1133
|
: await readTextFile(knowledgeFile, "");
|
|
860
|
-
const
|
|
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
|
-
});
|
|
1134
|
+
const digest = parseKnowledgeDigest(raw, currentStage, 6);
|
|
888
1135
|
return {
|
|
889
|
-
digestLines:
|
|
890
|
-
learningsCount
|
|
1136
|
+
digestLines: digest.lines,
|
|
1137
|
+
learningsCount: digest.learningsCount
|
|
891
1138
|
};
|
|
892
1139
|
}
|
|
893
1140
|
|
|
894
1141
|
async function readStageSupportContext(root, currentStage) {
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
if (!validStages.has(stage)) return [];
|
|
1142
|
+
if (!isKnownStageId(currentStage)) return [];
|
|
1143
|
+
const stage = currentStage;
|
|
898
1144
|
|
|
899
1145
|
const parts = [];
|
|
900
1146
|
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
@@ -906,12 +1152,7 @@ async function readStageSupportContext(root, currentStage) {
|
|
|
906
1152
|
);
|
|
907
1153
|
}
|
|
908
1154
|
|
|
909
|
-
const
|
|
910
|
-
brainstorm: "brainstorm-self-review.md",
|
|
911
|
-
scope: "scope-ceo-review.md",
|
|
912
|
-
design: "design-eng-review.md"
|
|
913
|
-
};
|
|
914
|
-
const promptName = reviewPromptByStage[stage];
|
|
1155
|
+
const promptName = reviewPromptFileName(stage);
|
|
915
1156
|
if (typeof promptName === "string") {
|
|
916
1157
|
const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
|
|
917
1158
|
const prompt = (await readTextFile(promptPath, "")).trim();
|
|
@@ -945,99 +1186,23 @@ async function handleSessionStart(runtime) {
|
|
|
945
1186
|
);
|
|
946
1187
|
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
947
1188
|
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
// Best-effort — a malformed cycle log should never break
|
|
966
|
-
// session-start. But we DO leave a breadcrumb in
|
|
967
|
-
// hook-errors.jsonl so \`npx cclaw-cli sync\` can surface chronic
|
|
968
|
-
// failures (previously this was a silent swallow).
|
|
969
|
-
await recordHookError(
|
|
970
|
-
runtime.root,
|
|
971
|
-
"session-start:ralph-loop",
|
|
972
|
-
err instanceof Error ? err.message : String(err)
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (
|
|
977
|
-
EARLY_LOOP_ENABLED &&
|
|
978
|
-
(state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
|
|
979
|
-
) {
|
|
980
|
-
try {
|
|
981
|
-
const earlyLoopStatus = await computeEarlyLoopStatusInline(
|
|
982
|
-
stateDir,
|
|
983
|
-
state.currentStage,
|
|
984
|
-
state.activeRunId,
|
|
985
|
-
EARLY_LOOP_MAX_ITERATIONS
|
|
986
|
-
);
|
|
987
|
-
await writeJsonFile(path.join(stateDir, "early-loop.json"), earlyLoopStatus);
|
|
988
|
-
earlyLoopLine = formatEarlyLoopStatusLineInline(earlyLoopStatus);
|
|
989
|
-
} catch (err) {
|
|
990
|
-
await recordHookError(
|
|
991
|
-
runtime.root,
|
|
992
|
-
"session-start:early-loop",
|
|
993
|
-
err instanceof Error ? err.message : String(err)
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Keep compound-readiness.json fresh on every session-start (cheap derived
|
|
999
|
-
// summary). Surface a one-line nudge only from review and ship stages
|
|
1000
|
-
// where lifting becomes relevant; earlier stages update the file silently.
|
|
1001
|
-
let compoundReadinessLine = "";
|
|
1002
|
-
try {
|
|
1003
|
-
let readiness = null;
|
|
1004
|
-
const internalReadiness = await runCclawInternal(
|
|
1005
|
-
runtime.root,
|
|
1006
|
-
["compound-readiness", "--json"],
|
|
1007
|
-
{ captureStdout: true }
|
|
1008
|
-
);
|
|
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);
|
|
1026
|
-
}
|
|
1027
|
-
if (state.currentStage === "review" || state.currentStage === "ship") {
|
|
1028
|
-
compoundReadinessLine = formatCompoundReadinessLineInline(toObject(readiness) || {});
|
|
1029
|
-
}
|
|
1030
|
-
} catch (err) {
|
|
1031
|
-
// Best-effort — a malformed knowledge.jsonl must never break
|
|
1032
|
-
// session-start. But we DO leave a breadcrumb in
|
|
1033
|
-
// hook-errors.jsonl so config/IO problems become visible in
|
|
1034
|
-
// \`npx cclaw-cli sync\` instead of silently degrading readiness output.
|
|
1035
|
-
await recordHookError(
|
|
1036
|
-
runtime.root,
|
|
1037
|
-
"session-start:compound-readiness",
|
|
1038
|
-
err instanceof Error ? err.message : String(err)
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1189
|
+
// Fast path: read precomputed status lines from session-digest cache.
|
|
1190
|
+
// If cache is stale, schedule a debounced background refresh so this hook
|
|
1191
|
+
// returns quickly inside harness startup.
|
|
1192
|
+
const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
|
|
1193
|
+
const forceSyncRefresh =
|
|
1194
|
+
normalizeText(process.env.CCLAW_SESSION_START_BG_SYNC).toLowerCase() === "1" ||
|
|
1195
|
+
["1", "true", "yes"].includes(normalizeText(process.env.VITEST).toLowerCase());
|
|
1196
|
+
let sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
|
|
1197
|
+
if (forceSyncRefresh && flowStateMtimeMs > 0) {
|
|
1198
|
+
await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
|
|
1199
|
+
sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
|
|
1200
|
+
} else if (!sessionDigest.fresh) {
|
|
1201
|
+
await scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs);
|
|
1202
|
+
}
|
|
1203
|
+
const ralphLoopLine = sessionDigest.ralphLoopLine;
|
|
1204
|
+
const earlyLoopLine = sessionDigest.earlyLoopLine;
|
|
1205
|
+
const compoundReadinessLine = sessionDigest.compoundReadinessLine;
|
|
1041
1206
|
|
|
1042
1207
|
const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
|
|
1043
1208
|
const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
|
|
@@ -1052,6 +1217,15 @@ async function handleSessionStart(runtime) {
|
|
|
1052
1217
|
});
|
|
1053
1218
|
const staleStages = toObject(state.raw.staleStages) || {};
|
|
1054
1219
|
const staleStageNames = Object.keys(staleStages);
|
|
1220
|
+
const interactionHints = toObject(state.raw.interactionHints) || {};
|
|
1221
|
+
const stageInteractionHint = toObject(interactionHints[state.currentStage]);
|
|
1222
|
+
const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
|
|
1223
|
+
const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
|
|
1224
|
+
? stageInteractionHint.sourceStage
|
|
1225
|
+
: "";
|
|
1226
|
+
const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
|
|
1227
|
+
? stageInteractionHint.recordedAt
|
|
1228
|
+
: "";
|
|
1055
1229
|
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
1056
1230
|
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
1057
1231
|
|
|
@@ -1063,8 +1237,8 @@ async function handleSessionStart(runtime) {
|
|
|
1063
1237
|
"/8 completed, run=" +
|
|
1064
1238
|
state.activeRunId +
|
|
1065
1239
|
"). Active artifacts: " +
|
|
1066
|
-
RUNTIME_ROOT +
|
|
1067
|
-
"
|
|
1240
|
+
activeArtifactsPathLabel(RUNTIME_ROOT) +
|
|
1241
|
+
" Learnings: " +
|
|
1068
1242
|
String(knowledge.learningsCount) +
|
|
1069
1243
|
" entries."
|
|
1070
1244
|
];
|
|
@@ -1084,6 +1258,14 @@ async function handleSessionStart(runtime) {
|
|
|
1084
1258
|
" (use npx cclaw-cli internal rewind --ack <stage> after redo)."
|
|
1085
1259
|
);
|
|
1086
1260
|
}
|
|
1261
|
+
if (skipQuestionsHintActive) {
|
|
1262
|
+
parts.push(
|
|
1263
|
+
"Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
|
|
1264
|
+
(skipQuestionsSource ? " from " + skipQuestionsSource : "") +
|
|
1265
|
+
(skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
|
|
1266
|
+
"). Draft with available context unless irreversible/security override checks still require explicit confirmation."
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1087
1269
|
if (knowledge.digestLines.length > 0) {
|
|
1088
1270
|
parts.push(
|
|
1089
1271
|
"Knowledge digest (top relevant entries):\\n" +
|
|
@@ -1169,7 +1351,7 @@ async function handleStopHandoff(runtime) {
|
|
|
1169
1351
|
const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
|
|
1170
1352
|
const closeoutContext =
|
|
1171
1353
|
state.currentStage === "ship" || shipSubstate !== "idle"
|
|
1172
|
-
? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=
|
|
1354
|
+
? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=post_ship_review -> archive; continue closeout with /cc."
|
|
1173
1355
|
: "";
|
|
1174
1356
|
|
|
1175
1357
|
const message =
|
|
@@ -1196,10 +1378,6 @@ async function handleStopHandoff(runtime) {
|
|
|
1196
1378
|
return 0;
|
|
1197
1379
|
}
|
|
1198
1380
|
|
|
1199
|
-
async function handlePreCompact(_runtime) {
|
|
1200
|
-
return 0;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
1381
|
async function handlePromptGuard(runtime) {
|
|
1204
1382
|
const mode = resolveStrictness();
|
|
1205
1383
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
@@ -1246,17 +1424,6 @@ async function handlePromptGuard(runtime) {
|
|
|
1246
1424
|
return 0;
|
|
1247
1425
|
}
|
|
1248
1426
|
|
|
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
1427
|
async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
|
|
1261
1428
|
const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
|
|
1262
1429
|
for (const line of cycleRaw.split(/\\r?\\n/gu)) {
|
|
@@ -1468,8 +1635,14 @@ async function handleWorkflowGuard(runtime) {
|
|
|
1468
1635
|
// writes that actually belonged to a new, not-yet-red S-2. Now
|
|
1469
1636
|
// we reuse the canonical Ralph Loop status: if NO slice has an
|
|
1470
1637
|
// open RED, we block.
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1638
|
+
const internalRalph = await runCclawInternal(
|
|
1639
|
+
runtime.root,
|
|
1640
|
+
["tdd-loop-status", "--json", "--no-write"],
|
|
1641
|
+
{ captureStdout: true }
|
|
1642
|
+
);
|
|
1643
|
+
const ralphStatus = parseJsonStdoutObject(internalRalph);
|
|
1644
|
+
const redOpen = internalRalph.code === 0 && ralphStatus?.redOpen === true;
|
|
1645
|
+
if (!redOpen) {
|
|
1473
1646
|
reasons.push("tdd_write_without_open_red");
|
|
1474
1647
|
}
|
|
1475
1648
|
}
|
|
@@ -1706,15 +1879,38 @@ async function handleVerifyCurrentState(runtime) {
|
|
|
1706
1879
|
return 0;
|
|
1707
1880
|
}
|
|
1708
1881
|
|
|
1882
|
+
async function handlePreToolPipeline(runtime) {
|
|
1883
|
+
const promptExitCode = await handlePromptGuard(runtime);
|
|
1884
|
+
if (promptExitCode !== 0) {
|
|
1885
|
+
return promptExitCode;
|
|
1886
|
+
}
|
|
1887
|
+
return await handleWorkflowGuard(runtime);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function handlePromptPipeline(runtime) {
|
|
1891
|
+
const promptExitCode = await handlePromptGuard(runtime);
|
|
1892
|
+
if (promptExitCode !== 0) {
|
|
1893
|
+
return promptExitCode;
|
|
1894
|
+
}
|
|
1895
|
+
const verifyExitCode = await handleVerifyCurrentState(runtime);
|
|
1896
|
+
if (verifyExitCode !== 0) {
|
|
1897
|
+
return verifyExitCode;
|
|
1898
|
+
}
|
|
1899
|
+
runtime.writeJson({ ok: true });
|
|
1900
|
+
return 0;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1709
1903
|
function normalizeHookName(rawName) {
|
|
1710
1904
|
const value = normalizeText(rawName).toLowerCase();
|
|
1711
1905
|
if (value === "session-start") return "session-start";
|
|
1906
|
+
if (value === "session-start-refresh") return "session-start-refresh";
|
|
1712
1907
|
if (value === "stop-handoff" || value === "stop") return "stop-handoff";
|
|
1713
1908
|
if (value === "stop-checkpoint") return "stop-handoff";
|
|
1714
|
-
if (value === "pre-compact" || value === "precompact") return "pre-compact";
|
|
1715
1909
|
if (value === "session-rehydrate") return "session-start";
|
|
1716
1910
|
if (value === "prompt-guard") return "prompt-guard";
|
|
1717
1911
|
if (value === "workflow-guard") return "workflow-guard";
|
|
1912
|
+
if (value === "pre-tool-pipeline" || value === "pretool-pipeline") return "pre-tool-pipeline";
|
|
1913
|
+
if (value === "prompt-pipeline" || value === "promptpipeline") return "prompt-pipeline";
|
|
1718
1914
|
if (value === "context-monitor") return "context-monitor";
|
|
1719
1915
|
if (value === "verify-current-state") return "verify-current-state";
|
|
1720
1916
|
return "";
|
|
@@ -1726,7 +1922,7 @@ async function main() {
|
|
|
1726
1922
|
process.stderr.write(
|
|
1727
1923
|
"[cclaw] run-hook: usage: node " +
|
|
1728
1924
|
RUNTIME_ROOT +
|
|
1729
|
-
"/hooks/run-hook.mjs <session-start|stop-handoff|
|
|
1925
|
+
"/hooks/run-hook.mjs <session-start|session-start-refresh|stop-handoff|prompt-guard|workflow-guard|pre-tool-pipeline|prompt-pipeline|context-monitor|verify-current-state>\\n"
|
|
1730
1926
|
);
|
|
1731
1927
|
process.exitCode = 1;
|
|
1732
1928
|
return;
|
|
@@ -1758,12 +1954,12 @@ async function main() {
|
|
|
1758
1954
|
process.exitCode = await handleSessionStart(runtime);
|
|
1759
1955
|
return;
|
|
1760
1956
|
}
|
|
1761
|
-
if (hookName === "
|
|
1762
|
-
process.exitCode = await
|
|
1957
|
+
if (hookName === "session-start-refresh") {
|
|
1958
|
+
process.exitCode = await handleSessionStartRefresh(runtime);
|
|
1763
1959
|
return;
|
|
1764
1960
|
}
|
|
1765
|
-
if (hookName === "
|
|
1766
|
-
process.exitCode = await
|
|
1961
|
+
if (hookName === "stop-handoff") {
|
|
1962
|
+
process.exitCode = await handleStopHandoff(runtime);
|
|
1767
1963
|
return;
|
|
1768
1964
|
}
|
|
1769
1965
|
if (hookName === "prompt-guard") {
|
|
@@ -1774,6 +1970,14 @@ async function main() {
|
|
|
1774
1970
|
process.exitCode = await handleWorkflowGuard(runtime);
|
|
1775
1971
|
return;
|
|
1776
1972
|
}
|
|
1973
|
+
if (hookName === "pre-tool-pipeline") {
|
|
1974
|
+
process.exitCode = await handlePreToolPipeline(runtime);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (hookName === "prompt-pipeline") {
|
|
1978
|
+
process.exitCode = await handlePromptPipeline(runtime);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1777
1981
|
if (hookName === "context-monitor") {
|
|
1778
1982
|
process.exitCode = await handleContextMonitor(runtime);
|
|
1779
1983
|
return;
|