cclaw-cli 1.0.0 → 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/dist/artifact-linter/brainstorm.js +15 -1
- package/dist/artifact-linter/design.js +14 -0
- package/dist/artifact-linter/scope.js +14 -0
- package/dist/artifact-linter/shared.d.ts +1 -0
- package/dist/artifact-linter/shared.js +32 -0
- package/dist/artifact-linter.js +11 -1
- 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/meta-skill.js +4 -3
- package/dist/content/node-hooks.js +259 -89
- package/dist/content/observe.js +3 -3
- package/dist/content/opencode-plugin.js +0 -6
- package/dist/content/skills-elicitation.d.ts +1 -0
- package/dist/content/skills-elicitation.js +123 -0
- package/dist/content/skills.js +6 -4
- package/dist/content/stages/brainstorm.js +7 -3
- package/dist/content/stages/design.js +4 -0
- package/dist/content/stages/scope.js +6 -2
- package/dist/content/start-command.js +4 -4
- package/dist/content/templates.js +21 -0
- package/dist/flow-state.d.ts +7 -0
- package/dist/flow-state.js +1 -0
- 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 +12 -3
- package/dist/internal/advance-stage/advance.js +22 -1
- package/dist/internal/advance-stage/parsers.d.ts +1 -0
- package/dist/internal/advance-stage/parsers.js +6 -0
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +29 -2
- package/dist/runtime/run-hook.mjs +259 -89
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- package/package.json +1 -1
|
@@ -7715,6 +7715,10 @@ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
|
|
|
7715
7715
|
const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
|
|
7716
7716
|
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
|
|
7717
7717
|
const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
|
|
7718
|
+
const SESSION_DIGEST_SCHEMA_VERSION = 1;
|
|
7719
|
+
const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
|
|
7720
|
+
const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
|
|
7721
|
+
const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
|
|
7718
7722
|
|
|
7719
7723
|
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
7720
7724
|
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
@@ -8140,9 +8144,10 @@ function hookEventNameForOutput(hookName) {
|
|
|
8140
8144
|
if (hookName === "session-start") return "SessionStart";
|
|
8141
8145
|
if (hookName === "prompt-guard") return "PreToolUse";
|
|
8142
8146
|
if (hookName === "workflow-guard") return "PreToolUse";
|
|
8147
|
+
if (hookName === "pre-tool-pipeline") return "PreToolUse";
|
|
8148
|
+
if (hookName === "prompt-pipeline") return "UserPromptSubmit";
|
|
8143
8149
|
if (hookName === "context-monitor") return "PostToolUse";
|
|
8144
8150
|
if (hookName === "stop-handoff") return "Stop";
|
|
8145
|
-
if (hookName === "pre-compact") return "PreCompact";
|
|
8146
8151
|
if (hookName === "verify-current-state") return "UserPromptSubmit";
|
|
8147
8152
|
return "SessionStart";
|
|
8148
8153
|
}
|
|
@@ -8551,78 +8556,55 @@ async function readFlowState(root) {
|
|
|
8551
8556
|
};
|
|
8552
8557
|
}
|
|
8553
8558
|
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
const digest = parseKnowledgeDigest(raw, currentStage, 6);
|
|
8563
|
-
return {
|
|
8564
|
-
digestLines: digest.lines,
|
|
8565
|
-
learningsCount: digest.learningsCount
|
|
8566
|
-
};
|
|
8559
|
+
async function readFileMtimeMs(filePath) {
|
|
8560
|
+
try {
|
|
8561
|
+
const stat = await fs.stat(filePath);
|
|
8562
|
+
if (!stat.isFile()) return 0;
|
|
8563
|
+
return Math.trunc(stat.mtimeMs);
|
|
8564
|
+
} catch {
|
|
8565
|
+
return 0;
|
|
8566
|
+
}
|
|
8567
8567
|
}
|
|
8568
8568
|
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
8575
|
-
const contract = (await readTextFile(contractPath, "")).trim();
|
|
8576
|
-
if (contract.length > 0) {
|
|
8577
|
-
parts.push(
|
|
8578
|
-
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
8579
|
-
contract
|
|
8580
|
-
);
|
|
8581
|
-
}
|
|
8569
|
+
function parseNumericMs(value) {
|
|
8570
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
8571
|
+
? Math.trunc(value)
|
|
8572
|
+
: -1;
|
|
8573
|
+
}
|
|
8582
8574
|
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8575
|
+
async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
|
|
8576
|
+
const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
8577
|
+
const cache = toObject(await readJsonFile(cachePath, {})) || {};
|
|
8578
|
+
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
8579
|
+
const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
|
|
8580
|
+
const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
|
|
8581
|
+
const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
|
|
8582
|
+
if (!fresh) {
|
|
8583
|
+
return {
|
|
8584
|
+
ralphLoopLine: "",
|
|
8585
|
+
earlyLoopLine: "",
|
|
8586
|
+
compoundReadinessLine: "",
|
|
8587
|
+
fresh: false
|
|
8588
|
+
};
|
|
8593
8589
|
}
|
|
8594
|
-
|
|
8595
|
-
|
|
8590
|
+
return {
|
|
8591
|
+
ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
|
|
8592
|
+
earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
|
|
8593
|
+
compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
|
|
8594
|
+
fresh: true
|
|
8595
|
+
};
|
|
8596
8596
|
}
|
|
8597
8597
|
|
|
8598
|
-
async function
|
|
8599
|
-
const
|
|
8600
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
8601
|
-
const ironLawsFile = path.join(stateDir, "iron-laws.json");
|
|
8602
|
-
const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
// Read knowledge.jsonl exactly once per session-start while holding the
|
|
8606
|
-
// SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
|
|
8607
|
-
// see a partial (mid-write) snapshot. Both the digest and
|
|
8608
|
-
// compound-readiness derive from this single read.
|
|
8609
|
-
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
8610
|
-
const knowledgeRaw = await readTextFileLocked(
|
|
8611
|
-
knowledgeLockPathInline(runtime.root),
|
|
8612
|
-
knowledgeFilePath,
|
|
8613
|
-
""
|
|
8614
|
-
);
|
|
8615
|
-
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
8616
|
-
|
|
8617
|
-
// Refresh Ralph Loop status each session-start so /cc and the model
|
|
8618
|
-
// both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
|
|
8619
|
-
// we are in tdd \u2014 other stages skip the write to keep the file stable.
|
|
8598
|
+
async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
|
|
8599
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
8620
8600
|
let ralphLoopLine = "";
|
|
8621
8601
|
let earlyLoopLine = "";
|
|
8602
|
+
let compoundReadinessLine = "";
|
|
8603
|
+
|
|
8622
8604
|
if (state.currentStage === "tdd") {
|
|
8623
8605
|
try {
|
|
8624
8606
|
const internalRalph = await runCclawInternal(
|
|
8625
|
-
|
|
8607
|
+
root,
|
|
8626
8608
|
["tdd-loop-status", "--json", "--write"],
|
|
8627
8609
|
{ captureStdout: true }
|
|
8628
8610
|
);
|
|
@@ -8635,12 +8617,8 @@ async function handleSessionStart(runtime) {
|
|
|
8635
8617
|
}
|
|
8636
8618
|
ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
|
|
8637
8619
|
} catch (err) {
|
|
8638
|
-
// Best-effort \u2014 a malformed cycle log should never break
|
|
8639
|
-
// session-start. But we DO leave a breadcrumb in
|
|
8640
|
-
// hook-errors.jsonl so \`npx cclaw-cli sync\` can surface chronic
|
|
8641
|
-
// failures (previously this was a silent swallow).
|
|
8642
8620
|
await recordHookError(
|
|
8643
|
-
|
|
8621
|
+
root,
|
|
8644
8622
|
"session-start:ralph-loop",
|
|
8645
8623
|
err instanceof Error ? err.message : String(err)
|
|
8646
8624
|
);
|
|
@@ -8652,7 +8630,7 @@ async function handleSessionStart(runtime) {
|
|
|
8652
8630
|
) {
|
|
8653
8631
|
try {
|
|
8654
8632
|
const internalEarly = await runCclawInternal(
|
|
8655
|
-
|
|
8633
|
+
root,
|
|
8656
8634
|
[
|
|
8657
8635
|
"early-loop-status",
|
|
8658
8636
|
"--json",
|
|
@@ -8674,21 +8652,17 @@ async function handleSessionStart(runtime) {
|
|
|
8674
8652
|
earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
|
|
8675
8653
|
} catch (err) {
|
|
8676
8654
|
await recordHookError(
|
|
8677
|
-
|
|
8655
|
+
root,
|
|
8678
8656
|
"session-start:early-loop",
|
|
8679
8657
|
err instanceof Error ? err.message : String(err)
|
|
8680
8658
|
);
|
|
8681
8659
|
}
|
|
8682
8660
|
}
|
|
8683
8661
|
|
|
8684
|
-
// Keep compound-readiness.json fresh on every session-start (cheap derived
|
|
8685
|
-
// summary). Surface a one-line nudge only from review and ship stages
|
|
8686
|
-
// where lifting becomes relevant; earlier stages update the file silently.
|
|
8687
|
-
let compoundReadinessLine = "";
|
|
8688
8662
|
try {
|
|
8689
8663
|
const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
|
|
8690
8664
|
const internalReadiness = await runCclawInternal(
|
|
8691
|
-
|
|
8665
|
+
root,
|
|
8692
8666
|
shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
|
|
8693
8667
|
{ captureStdout: true }
|
|
8694
8668
|
);
|
|
@@ -8699,17 +8673,169 @@ async function handleSessionStart(runtime) {
|
|
|
8699
8673
|
compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
|
|
8700
8674
|
}
|
|
8701
8675
|
} catch (err) {
|
|
8702
|
-
// Best-effort \u2014 a malformed knowledge.jsonl must never break
|
|
8703
|
-
// session-start. But we DO leave a breadcrumb in
|
|
8704
|
-
// hook-errors.jsonl so config/IO problems become visible in
|
|
8705
|
-
// \`npx cclaw-cli sync\` instead of silently degrading readiness output.
|
|
8706
8676
|
await recordHookError(
|
|
8707
|
-
|
|
8677
|
+
root,
|
|
8708
8678
|
"session-start:compound-readiness",
|
|
8709
8679
|
err instanceof Error ? err.message : String(err)
|
|
8710
8680
|
);
|
|
8711
8681
|
}
|
|
8712
8682
|
|
|
8683
|
+
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
8684
|
+
await writeJsonFile(digestPath, {
|
|
8685
|
+
schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
|
|
8686
|
+
generatedAt: new Date().toISOString(),
|
|
8687
|
+
flowStateMtimeMs,
|
|
8688
|
+
currentStage: state.currentStage,
|
|
8689
|
+
activeRunId: state.activeRunId,
|
|
8690
|
+
ralphLoopLine,
|
|
8691
|
+
earlyLoopLine,
|
|
8692
|
+
compoundReadinessLine
|
|
8693
|
+
});
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
|
|
8697
|
+
if (flowStateMtimeMs <= 0) return;
|
|
8698
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
8699
|
+
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
8700
|
+
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
8701
|
+
|
|
8702
|
+
const cache = toObject(await readJsonFile(digestPath, {})) || {};
|
|
8703
|
+
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
8704
|
+
if (cachedMtimeMs === flowStateMtimeMs) return;
|
|
8705
|
+
|
|
8706
|
+
const marker = toObject(await readJsonFile(markerPath, {})) || {};
|
|
8707
|
+
const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
|
|
8708
|
+
const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
|
|
8709
|
+
const markerFresh =
|
|
8710
|
+
markerMtimeMs === flowStateMtimeMs &&
|
|
8711
|
+
markerStartedAtMs > 0 &&
|
|
8712
|
+
Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
|
|
8713
|
+
if (markerFresh) return;
|
|
8714
|
+
|
|
8715
|
+
await writeJsonFile(markerPath, {
|
|
8716
|
+
flowStateMtimeMs,
|
|
8717
|
+
startedAtMs: Date.now(),
|
|
8718
|
+
currentStage: state.currentStage,
|
|
8719
|
+
activeRunId: state.activeRunId
|
|
8720
|
+
});
|
|
8721
|
+
|
|
8722
|
+
try {
|
|
8723
|
+
const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
|
|
8724
|
+
cwd: runtime.root,
|
|
8725
|
+
stdio: "ignore",
|
|
8726
|
+
windowsHide: true,
|
|
8727
|
+
detached: true,
|
|
8728
|
+
env: {
|
|
8729
|
+
...process.env,
|
|
8730
|
+
CCLAW_PROJECT_ROOT: runtime.root,
|
|
8731
|
+
CCLAW_BG_WORKER: "1"
|
|
8732
|
+
}
|
|
8733
|
+
});
|
|
8734
|
+
child.unref();
|
|
8735
|
+
} catch (err) {
|
|
8736
|
+
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
8737
|
+
await recordHookError(
|
|
8738
|
+
runtime.root,
|
|
8739
|
+
"session-start:spawn-refresh",
|
|
8740
|
+
err instanceof Error ? err.message : String(err)
|
|
8741
|
+
);
|
|
8742
|
+
}
|
|
8743
|
+
}
|
|
8744
|
+
|
|
8745
|
+
async function handleSessionStartRefresh(runtime) {
|
|
8746
|
+
const state = await readFlowState(runtime.root);
|
|
8747
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
8748
|
+
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
8749
|
+
try {
|
|
8750
|
+
const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
|
|
8751
|
+
await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
|
|
8752
|
+
} finally {
|
|
8753
|
+
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
8754
|
+
}
|
|
8755
|
+
return 0;
|
|
8756
|
+
}
|
|
8757
|
+
|
|
8758
|
+
|
|
8759
|
+
async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
8760
|
+
const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
8761
|
+
// Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
|
|
8762
|
+
// Falls back to a local read if nothing is passed in.
|
|
8763
|
+
const raw = typeof prereadRaw === "string"
|
|
8764
|
+
? prereadRaw
|
|
8765
|
+
: await readTextFile(knowledgeFile, "");
|
|
8766
|
+
const digest = parseKnowledgeDigest(raw, currentStage, 6);
|
|
8767
|
+
return {
|
|
8768
|
+
digestLines: digest.lines,
|
|
8769
|
+
learningsCount: digest.learningsCount
|
|
8770
|
+
};
|
|
8771
|
+
}
|
|
8772
|
+
|
|
8773
|
+
async function readStageSupportContext(root, currentStage) {
|
|
8774
|
+
if (!isKnownStageId(currentStage)) return [];
|
|
8775
|
+
const stage = currentStage;
|
|
8776
|
+
|
|
8777
|
+
const parts = [];
|
|
8778
|
+
const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
|
|
8779
|
+
const contract = (await readTextFile(contractPath, "")).trim();
|
|
8780
|
+
if (contract.length > 0) {
|
|
8781
|
+
parts.push(
|
|
8782
|
+
"Current stage state contract (read before drafting or editing the stage artifact):\\n" +
|
|
8783
|
+
contract
|
|
8784
|
+
);
|
|
8785
|
+
}
|
|
8786
|
+
|
|
8787
|
+
const promptName = reviewPromptFileName(stage);
|
|
8788
|
+
if (typeof promptName === "string") {
|
|
8789
|
+
const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
|
|
8790
|
+
const prompt = (await readTextFile(promptPath, "")).trim();
|
|
8791
|
+
if (prompt.length > 0) {
|
|
8792
|
+
parts.push(
|
|
8793
|
+
"Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
|
|
8794
|
+
prompt
|
|
8795
|
+
);
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
return parts;
|
|
8800
|
+
}
|
|
8801
|
+
|
|
8802
|
+
async function handleSessionStart(runtime) {
|
|
8803
|
+
const state = await readFlowState(runtime.root);
|
|
8804
|
+
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
8805
|
+
const ironLawsFile = path.join(stateDir, "iron-laws.json");
|
|
8806
|
+
const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
|
|
8807
|
+
|
|
8808
|
+
|
|
8809
|
+
// Read knowledge.jsonl exactly once per session-start while holding the
|
|
8810
|
+
// SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
|
|
8811
|
+
// see a partial (mid-write) snapshot. Both the digest and
|
|
8812
|
+
// compound-readiness derive from this single read.
|
|
8813
|
+
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
8814
|
+
const knowledgeRaw = await readTextFileLocked(
|
|
8815
|
+
knowledgeLockPathInline(runtime.root),
|
|
8816
|
+
knowledgeFilePath,
|
|
8817
|
+
""
|
|
8818
|
+
);
|
|
8819
|
+
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
8820
|
+
|
|
8821
|
+
// Fast path: read precomputed status lines from session-digest cache.
|
|
8822
|
+
// If cache is stale, schedule a debounced background refresh so this hook
|
|
8823
|
+
// returns quickly inside harness startup.
|
|
8824
|
+
const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
|
|
8825
|
+
const forceSyncRefresh =
|
|
8826
|
+
normalizeText(process.env.CCLAW_SESSION_START_BG_SYNC).toLowerCase() === "1" ||
|
|
8827
|
+
["1", "true", "yes"].includes(normalizeText(process.env.VITEST).toLowerCase());
|
|
8828
|
+
let sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
|
|
8829
|
+
if (forceSyncRefresh && flowStateMtimeMs > 0) {
|
|
8830
|
+
await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
|
|
8831
|
+
sessionDigest = await readSessionDigestLines(stateDir, state, flowStateMtimeMs);
|
|
8832
|
+
} else if (!sessionDigest.fresh) {
|
|
8833
|
+
await scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs);
|
|
8834
|
+
}
|
|
8835
|
+
const ralphLoopLine = sessionDigest.ralphLoopLine;
|
|
8836
|
+
const earlyLoopLine = sessionDigest.earlyLoopLine;
|
|
8837
|
+
const compoundReadinessLine = sessionDigest.compoundReadinessLine;
|
|
8838
|
+
|
|
8713
8839
|
const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
|
|
8714
8840
|
const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
|
|
8715
8841
|
const ironLawLines = laws
|
|
@@ -8723,6 +8849,15 @@ async function handleSessionStart(runtime) {
|
|
|
8723
8849
|
});
|
|
8724
8850
|
const staleStages = toObject(state.raw.staleStages) || {};
|
|
8725
8851
|
const staleStageNames = Object.keys(staleStages);
|
|
8852
|
+
const interactionHints = toObject(state.raw.interactionHints) || {};
|
|
8853
|
+
const stageInteractionHint = toObject(interactionHints[state.currentStage]);
|
|
8854
|
+
const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
|
|
8855
|
+
const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
|
|
8856
|
+
? stageInteractionHint.sourceStage
|
|
8857
|
+
: "";
|
|
8858
|
+
const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
|
|
8859
|
+
? stageInteractionHint.recordedAt
|
|
8860
|
+
: "";
|
|
8726
8861
|
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
8727
8862
|
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
8728
8863
|
|
|
@@ -8755,6 +8890,14 @@ async function handleSessionStart(runtime) {
|
|
|
8755
8890
|
" (use npx cclaw-cli internal rewind --ack <stage> after redo)."
|
|
8756
8891
|
);
|
|
8757
8892
|
}
|
|
8893
|
+
if (skipQuestionsHintActive) {
|
|
8894
|
+
parts.push(
|
|
8895
|
+
"Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
|
|
8896
|
+
(skipQuestionsSource ? " from " + skipQuestionsSource : "") +
|
|
8897
|
+
(skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
|
|
8898
|
+
"). Draft with available context unless irreversible/security override checks still require explicit confirmation."
|
|
8899
|
+
);
|
|
8900
|
+
}
|
|
8758
8901
|
if (knowledge.digestLines.length > 0) {
|
|
8759
8902
|
parts.push(
|
|
8760
8903
|
"Knowledge digest (top relevant entries):\\n" +
|
|
@@ -8867,10 +9010,6 @@ async function handleStopHandoff(runtime) {
|
|
|
8867
9010
|
return 0;
|
|
8868
9011
|
}
|
|
8869
9012
|
|
|
8870
|
-
async function handlePreCompact(_runtime) {
|
|
8871
|
-
return 0;
|
|
8872
|
-
}
|
|
8873
|
-
|
|
8874
9013
|
async function handlePromptGuard(runtime) {
|
|
8875
9014
|
const mode = resolveStrictness();
|
|
8876
9015
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
@@ -9372,15 +9511,38 @@ async function handleVerifyCurrentState(runtime) {
|
|
|
9372
9511
|
return 0;
|
|
9373
9512
|
}
|
|
9374
9513
|
|
|
9514
|
+
async function handlePreToolPipeline(runtime) {
|
|
9515
|
+
const promptExitCode = await handlePromptGuard(runtime);
|
|
9516
|
+
if (promptExitCode !== 0) {
|
|
9517
|
+
return promptExitCode;
|
|
9518
|
+
}
|
|
9519
|
+
return await handleWorkflowGuard(runtime);
|
|
9520
|
+
}
|
|
9521
|
+
|
|
9522
|
+
async function handlePromptPipeline(runtime) {
|
|
9523
|
+
const promptExitCode = await handlePromptGuard(runtime);
|
|
9524
|
+
if (promptExitCode !== 0) {
|
|
9525
|
+
return promptExitCode;
|
|
9526
|
+
}
|
|
9527
|
+
const verifyExitCode = await handleVerifyCurrentState(runtime);
|
|
9528
|
+
if (verifyExitCode !== 0) {
|
|
9529
|
+
return verifyExitCode;
|
|
9530
|
+
}
|
|
9531
|
+
runtime.writeJson({ ok: true });
|
|
9532
|
+
return 0;
|
|
9533
|
+
}
|
|
9534
|
+
|
|
9375
9535
|
function normalizeHookName(rawName) {
|
|
9376
9536
|
const value = normalizeText(rawName).toLowerCase();
|
|
9377
9537
|
if (value === "session-start") return "session-start";
|
|
9538
|
+
if (value === "session-start-refresh") return "session-start-refresh";
|
|
9378
9539
|
if (value === "stop-handoff" || value === "stop") return "stop-handoff";
|
|
9379
9540
|
if (value === "stop-checkpoint") return "stop-handoff";
|
|
9380
|
-
if (value === "pre-compact" || value === "precompact") return "pre-compact";
|
|
9381
9541
|
if (value === "session-rehydrate") return "session-start";
|
|
9382
9542
|
if (value === "prompt-guard") return "prompt-guard";
|
|
9383
9543
|
if (value === "workflow-guard") return "workflow-guard";
|
|
9544
|
+
if (value === "pre-tool-pipeline" || value === "pretool-pipeline") return "pre-tool-pipeline";
|
|
9545
|
+
if (value === "prompt-pipeline" || value === "promptpipeline") return "prompt-pipeline";
|
|
9384
9546
|
if (value === "context-monitor") return "context-monitor";
|
|
9385
9547
|
if (value === "verify-current-state") return "verify-current-state";
|
|
9386
9548
|
return "";
|
|
@@ -9392,7 +9554,7 @@ async function main() {
|
|
|
9392
9554
|
process.stderr.write(
|
|
9393
9555
|
"[cclaw] run-hook: usage: node " +
|
|
9394
9556
|
RUNTIME_ROOT +
|
|
9395
|
-
"/hooks/run-hook.mjs <session-start|stop-handoff|
|
|
9557
|
+
"/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"
|
|
9396
9558
|
);
|
|
9397
9559
|
process.exitCode = 1;
|
|
9398
9560
|
return;
|
|
@@ -9424,12 +9586,12 @@ async function main() {
|
|
|
9424
9586
|
process.exitCode = await handleSessionStart(runtime);
|
|
9425
9587
|
return;
|
|
9426
9588
|
}
|
|
9427
|
-
if (hookName === "
|
|
9428
|
-
process.exitCode = await
|
|
9589
|
+
if (hookName === "session-start-refresh") {
|
|
9590
|
+
process.exitCode = await handleSessionStartRefresh(runtime);
|
|
9429
9591
|
return;
|
|
9430
9592
|
}
|
|
9431
|
-
if (hookName === "
|
|
9432
|
-
process.exitCode = await
|
|
9593
|
+
if (hookName === "stop-handoff") {
|
|
9594
|
+
process.exitCode = await handleStopHandoff(runtime);
|
|
9433
9595
|
return;
|
|
9434
9596
|
}
|
|
9435
9597
|
if (hookName === "prompt-guard") {
|
|
@@ -9440,6 +9602,14 @@ async function main() {
|
|
|
9440
9602
|
process.exitCode = await handleWorkflowGuard(runtime);
|
|
9441
9603
|
return;
|
|
9442
9604
|
}
|
|
9605
|
+
if (hookName === "pre-tool-pipeline") {
|
|
9606
|
+
process.exitCode = await handlePreToolPipeline(runtime);
|
|
9607
|
+
return;
|
|
9608
|
+
}
|
|
9609
|
+
if (hookName === "prompt-pipeline") {
|
|
9610
|
+
process.exitCode = await handlePromptPipeline(runtime);
|
|
9611
|
+
return;
|
|
9612
|
+
}
|
|
9443
9613
|
if (hookName === "context-monitor") {
|
|
9444
9614
|
process.exitCode = await handleContextMonitor(runtime);
|
|
9445
9615
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowTrack, TrackHeuristicRule, TrackHeuristicsConfig } from "./types.js";
|
|
1
|
+
import type { FlowStage, FlowTrack, TrackHeuristicRule, TrackHeuristicsConfig } from "./types.js";
|
|
2
2
|
export interface TrackResolution {
|
|
3
3
|
track: FlowTrack;
|
|
4
4
|
reason: string;
|
|
@@ -6,6 +6,11 @@ export interface TrackResolution {
|
|
|
6
6
|
confidence: "high" | "medium" | "low";
|
|
7
7
|
overrideGuidance: string;
|
|
8
8
|
}
|
|
9
|
+
export interface QuestionBudgetHint {
|
|
10
|
+
min: number;
|
|
11
|
+
recommended: number;
|
|
12
|
+
hardCapWarning: number;
|
|
13
|
+
}
|
|
9
14
|
/**
|
|
10
15
|
* Reference implementation of the track classifier the /cc skill prose
|
|
11
16
|
* describes. Tests pin its behavior so the built-in defaults stay honest.
|
|
@@ -14,6 +19,7 @@ export interface TrackResolution {
|
|
|
14
19
|
* "advisory" language.
|
|
15
20
|
*/
|
|
16
21
|
export declare function resolveTrackFromPrompt(prompt: string, config: TrackHeuristicsConfig | undefined): TrackResolution;
|
|
22
|
+
export declare function questionBudgetHint(track: FlowTrack, stage: FlowStage): QuestionBudgetHint;
|
|
17
23
|
export declare const TRACK_HEURISTICS_DEFAULTS: {
|
|
18
24
|
readonly fallback: "standard";
|
|
19
25
|
readonly evaluationOrder: readonly ("quick" | "medium" | "standard")[];
|
package/dist/track-heuristics.js
CHANGED
|
@@ -54,6 +54,12 @@ const DEFAULT_RULES = {
|
|
|
54
54
|
// into runtime, so cclaw stopped offering the knob in v0.38.0.
|
|
55
55
|
const EVALUATION_ORDER = ["standard", "medium", "quick"];
|
|
56
56
|
const DEFAULT_FALLBACK = "standard";
|
|
57
|
+
const ADAPTIVE_ELICITATION_STAGES = new Set(["brainstorm", "scope", "design"]);
|
|
58
|
+
const QUESTION_BUDGET_HINTS_BY_TRACK = {
|
|
59
|
+
quick: { min: 2, recommended: 3, hardCapWarning: 4 },
|
|
60
|
+
medium: { min: 5, recommended: 6, hardCapWarning: 8 },
|
|
61
|
+
standard: { min: 10, recommended: 12, hardCapWarning: 14 }
|
|
62
|
+
};
|
|
57
63
|
function hasToken(promptLower, token) {
|
|
58
64
|
return promptLower.includes(token.toLowerCase());
|
|
59
65
|
}
|
|
@@ -130,6 +136,12 @@ export function resolveTrackFromPrompt(prompt, config) {
|
|
|
130
136
|
overrideGuidance: "Confirm or override before state is written; choose quick only for known low-blast-radius work, medium for known architecture with product framing, standard for uncertainty."
|
|
131
137
|
};
|
|
132
138
|
}
|
|
139
|
+
export function questionBudgetHint(track, stage) {
|
|
140
|
+
if (!ADAPTIVE_ELICITATION_STAGES.has(stage)) {
|
|
141
|
+
return { min: 0, recommended: 0, hardCapWarning: 0 };
|
|
142
|
+
}
|
|
143
|
+
return QUESTION_BUDGET_HINTS_BY_TRACK[track];
|
|
144
|
+
}
|
|
133
145
|
export const TRACK_HEURISTICS_DEFAULTS = {
|
|
134
146
|
fallback: DEFAULT_FALLBACK,
|
|
135
147
|
evaluationOrder: EVALUATION_ORDER,
|