cclaw-cli 1.0.0 → 3.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 (52) hide show
  1. package/dist/artifact-linter/brainstorm.js +15 -1
  2. package/dist/artifact-linter/design.js +14 -0
  3. package/dist/artifact-linter/scope.js +14 -0
  4. package/dist/artifact-linter/shared.d.ts +1 -0
  5. package/dist/artifact-linter/shared.js +32 -0
  6. package/dist/artifact-linter.js +13 -5
  7. package/dist/cli.js +2 -9
  8. package/dist/config.d.ts +11 -67
  9. package/dist/config.js +59 -649
  10. package/dist/content/hook-events.js +1 -5
  11. package/dist/content/hook-manifest.d.ts +6 -4
  12. package/dist/content/hook-manifest.js +16 -65
  13. package/dist/content/hooks.js +54 -14
  14. package/dist/content/meta-skill.js +4 -3
  15. package/dist/content/node-hooks.d.ts +0 -26
  16. package/dist/content/node-hooks.js +459 -157
  17. package/dist/content/observe.js +5 -4
  18. package/dist/content/opencode-plugin.js +1 -78
  19. package/dist/content/skills-elicitation.d.ts +1 -0
  20. package/dist/content/skills-elicitation.js +123 -0
  21. package/dist/content/skills.js +6 -4
  22. package/dist/content/stages/brainstorm.js +7 -3
  23. package/dist/content/stages/design.js +6 -2
  24. package/dist/content/stages/plan.js +2 -2
  25. package/dist/content/stages/scope.js +9 -5
  26. package/dist/content/stages/tdd.js +11 -11
  27. package/dist/content/start-command.js +4 -4
  28. package/dist/content/templates.js +21 -0
  29. package/dist/flow-state.d.ts +7 -0
  30. package/dist/flow-state.js +1 -0
  31. package/dist/gate-evidence.js +1 -5
  32. package/dist/hook-schema.js +3 -0
  33. package/dist/hook-schemas/claude-hooks.v1.json +2 -5
  34. package/dist/hook-schemas/codex-hooks.v1.json +1 -4
  35. package/dist/hook-schemas/cursor-hooks.v1.json +1 -3
  36. package/dist/install.d.ts +2 -7
  37. package/dist/install.js +32 -123
  38. package/dist/internal/advance-stage/advance.js +22 -1
  39. package/dist/internal/advance-stage/parsers.d.ts +1 -0
  40. package/dist/internal/advance-stage/parsers.js +6 -0
  41. package/dist/internal/compound-readiness.js +1 -16
  42. package/dist/internal/early-loop-status.js +1 -3
  43. package/dist/internal/runtime-integrity.js +0 -20
  44. package/dist/policy.js +6 -9
  45. package/dist/run-persistence.d.ts +1 -1
  46. package/dist/run-persistence.js +29 -2
  47. package/dist/runtime/run-hook.mjs +459 -265
  48. package/dist/tdd-verification-evidence.js +6 -18
  49. package/dist/track-heuristics.d.ts +7 -1
  50. package/dist/track-heuristics.js +12 -0
  51. package/dist/types.d.ts +0 -56
  52. package/package.json +1 -1
@@ -7405,117 +7405,6 @@ var REQUIRED_GITIGNORE_PATTERNS = [
7405
7405
  ".cursor/rules/cclaw-workflow.mdc"
7406
7406
  ];
7407
7407
 
7408
- // src/content/iron-laws.ts
7409
- var IRON_LAWS = [
7410
- {
7411
- id: "tdd-red-before-write",
7412
- title: "RED before production write",
7413
- rule: "Do not edit production code in tdd stage before a failing RED test exists for the slice.",
7414
- rationale: "Prevents implementation-first behavior and keeps RED as executable specification.",
7415
- enforcement: "PreToolUse",
7416
- severity: "hard-gate",
7417
- appliesTo: ["tdd"],
7418
- hookMatcher: {
7419
- toolPattern: "write|edit|multiedit|applypatch|shell|bash",
7420
- payloadPattern: "\\.(ts|tsx|js|jsx|py|go|java|rs|rb|php|c|cc|cpp|h|hpp)"
7421
- }
7422
- },
7423
- {
7424
- id: "plan-requires-approval",
7425
- title: "No implementation before plan approval",
7426
- rule: "Do not perform write-like actions while plan stage is pending WAIT_FOR_CONFIRM approval.",
7427
- rationale: "Locks intent before execution and reduces expensive rework from unapproved paths.",
7428
- enforcement: "PreToolUse",
7429
- severity: "hard-gate",
7430
- appliesTo: ["plan"]
7431
- },
7432
- {
7433
- id: "runtime-writes-managed-only",
7434
- title: "Runtime writes are managed",
7435
- rule: `Do not mutate ${RUNTIME_ROOT}/state, ${RUNTIME_ROOT}/hooks, or ${RUNTIME_ROOT}/skills by ad-hoc edits unless using cclaw-managed commands.`,
7436
- rationale: "Protects generated runtime integrity and avoids drift that silently breaks hooks or skills.",
7437
- enforcement: "PreToolUse",
7438
- severity: "hard-gate",
7439
- appliesTo: "all",
7440
- hookMatcher: {
7441
- toolPattern: "write|edit|multiedit|delete|applypatch|shell|bash",
7442
- payloadPattern: "\\.cclaw/(state|hooks|skills)"
7443
- }
7444
- },
7445
- {
7446
- id: "flow-state-read-fresh",
7447
- title: "Fresh flow-state read required",
7448
- rule: `Before mutating actions, a fresh read of ${RUNTIME_ROOT}/state/flow-state.json must exist within guard freshness window.`,
7449
- rationale: "Prevents stale-stage mutations after context shifts or multi-agent divergence.",
7450
- enforcement: "PreToolUse",
7451
- severity: "hard-gate",
7452
- appliesTo: "all"
7453
- },
7454
- {
7455
- id: "review-layer-order",
7456
- title: "Review layers are sequential",
7457
- rule: "Review stage must complete Layer 1 spec compliance before Layer 2 quality/security passes.",
7458
- rationale: "Stops premature quality discussion when acceptance criteria are not yet satisfied.",
7459
- enforcement: "PreToolUse",
7460
- severity: "hard-gate",
7461
- appliesTo: ["review"]
7462
- },
7463
- {
7464
- id: "review-criticals-close-before-ship",
7465
- title: "No ship with open criticals",
7466
- rule: "Ship decisions are blocked when review-army contains open Critical findings or ship blockers.",
7467
- rationale: "Enforces explicit risk closure before release finalization.",
7468
- enforcement: "PreToolUse",
7469
- severity: "hard-gate",
7470
- appliesTo: ["ship"]
7471
- },
7472
- {
7473
- id: "ship-preflight-required",
7474
- title: "Preflight required before finalization",
7475
- rule: "Do not execute release finalization actions until ship preflight gate is passed.",
7476
- rationale: "Catches regressions before irreversible release steps.",
7477
- enforcement: "PreToolUse",
7478
- severity: "hard-gate",
7479
- appliesTo: ["ship"]
7480
- },
7481
- {
7482
- id: "review-coverage-complete-before-ship",
7483
- title: "Review layer coverage before ship",
7484
- rule: "Block ship finalization when review-army does not confirm full Layer 1/2 coverage map.",
7485
- rationale: "Prevents finalization when multi-pass review evidence is incomplete or partially missing.",
7486
- enforcement: "PreToolUse",
7487
- severity: "hard-gate",
7488
- appliesTo: ["ship"]
7489
- },
7490
- {
7491
- id: "subagent-task-self-contained",
7492
- title: "Subagent tasks are self-contained",
7493
- rule: "Delegated tasks must include explicit objective, constraints, and expected output, not just references.",
7494
- rationale: "Avoids context loss and low-quality delegation in isolated worker contexts.",
7495
- enforcement: "advisory",
7496
- severity: "soft-gate",
7497
- appliesTo: "all"
7498
- },
7499
- {
7500
- id: "no-secrets-in-artifacts",
7501
- title: "Never log secrets in artifacts",
7502
- rule: "Secrets/tokens/passwords must not be written to review, ship, or runtime state artifacts.",
7503
- rationale: "Prevents accidental credential leakage through generated workflow artifacts.",
7504
- enforcement: "PostToolUse",
7505
- severity: "hard-gate",
7506
- appliesTo: "all"
7507
- },
7508
- {
7509
- id: "stop-clean-or-handoff",
7510
- title: "Stop only from clean handoff",
7511
- rule: "Do not end a session with dirty state unless the current artifact records unresolved work and blockers.",
7512
- rationale: "Protects continuity and prevents silent half-finished sessions.",
7513
- enforcement: "Stop",
7514
- severity: "hard-gate",
7515
- appliesTo: "all"
7516
- }
7517
- ];
7518
-
7519
7408
  // src/types.ts
7520
7409
  var FLOW_STAGES = [
7521
7410
  "brainstorm",
@@ -7527,9 +7416,7 @@ var FLOW_STAGES = [
7527
7416
  "review",
7528
7417
  "ship"
7529
7418
  ];
7530
- var FLOW_TRACKS = ["quick", "medium", "standard"];
7531
7419
  var HARNESS_IDS = ["claude", "cursor", "opencode", "codex"];
7532
- var LANGUAGE_RULE_PACKS = ["typescript", "python", "go"];
7533
7420
 
7534
7421
  // src/managed-resources.ts
7535
7422
  var MANAGED_RESOURCE_MANIFEST_REL_PATH = `${RUNTIME_ROOT}/state/managed-resources.json`;
@@ -7538,11 +7425,7 @@ var MANAGED_RESOURCE_HARNESSES = /* @__PURE__ */ new Set(["core", ...HARNESS_IDS
7538
7425
  // src/config.ts
7539
7426
  var CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
7540
7427
  var HARNESS_ID_SET = new Set(HARNESS_IDS);
7541
- var FLOW_TRACK_SET = new Set(FLOW_TRACKS);
7542
- var LANGUAGE_RULE_PACK_SET = new Set(LANGUAGE_RULE_PACKS);
7543
7428
  var SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
7544
- var SUPPORTED_TRACKS_TEXT = FLOW_TRACKS.join(", ");
7545
- var SUPPORTED_LANGUAGE_RULE_PACKS_TEXT = LANGUAGE_RULE_PACKS.join(", ");
7546
7429
  var DEFAULT_TDD_TEST_PATH_PATTERNS = [
7547
7430
  "**/*.test.*",
7548
7431
  "**/tests/**",
@@ -7656,10 +7539,6 @@ function reviewPromptFileName(stage) {
7656
7539
  `;
7657
7540
 
7658
7541
  // src/content/node-hooks.ts
7659
- function normalizePatterns(patterns, fallback) {
7660
- if (!patterns || patterns.length === 0) return [...fallback];
7661
- return patterns.map((value) => value.trim()).filter((value) => value.length > 0);
7662
- }
7663
7542
  function resolveCliRuntimeForGeneratedHook() {
7664
7543
  const here = fileURLToPath2(import.meta.url);
7665
7544
  const candidates = [
@@ -7679,16 +7558,19 @@ function resolveCliRuntimeForGeneratedHook() {
7679
7558
  return { entrypoint: null, argsPrefix: [] };
7680
7559
  }
7681
7560
  function nodeHookRuntimeScript(options = {}) {
7682
- const strictness = options.strictness === "strict" ? "strict" : "advisory";
7683
- const tddTestPathPatterns = normalizePatterns(options.tddTestPathPatterns, [
7561
+ void options;
7562
+ const strictness = "advisory";
7563
+ const tddTestPathPatterns = [
7684
7564
  "**/*.test.*",
7685
7565
  "**/tests/**",
7686
7566
  "**/__tests__/**"
7687
- ]);
7688
- const tddProductionPathPatterns = normalizePatterns(options.tddProductionPathPatterns, []);
7689
- const compoundRecurrenceThreshold = typeof options.compoundRecurrenceThreshold === "number" && Number.isInteger(options.compoundRecurrenceThreshold) && options.compoundRecurrenceThreshold >= 1 ? options.compoundRecurrenceThreshold : DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
7690
- const earlyLoopEnabled = options.earlyLoopEnabled !== false;
7691
- const earlyLoopMaxIterations = typeof options.earlyLoopMaxIterations === "number" && Number.isInteger(options.earlyLoopMaxIterations) && options.earlyLoopMaxIterations >= 1 ? options.earlyLoopMaxIterations : DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
7567
+ ];
7568
+ const tddProductionPathPatterns = [];
7569
+ const compoundRecurrenceThreshold = DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
7570
+ const earlyLoopEnabled = true;
7571
+ const earlyLoopMaxIterations = DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
7572
+ const defaultHookProfile = "standard";
7573
+ const defaultDisabledHooks = [];
7692
7574
  const cliRuntime = resolveCliRuntimeForGeneratedHook();
7693
7575
  return `#!/usr/bin/env node
7694
7576
  import fs from "node:fs/promises";
@@ -7715,11 +7597,135 @@ const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
7715
7597
  const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
7716
7598
  const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
7717
7599
  const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
7600
+ const DEFAULT_HOOK_PROFILE = ${JSON.stringify(defaultHookProfile)};
7601
+ const DEFAULT_DISABLED_HOOKS = ${JSON.stringify(defaultDisabledHooks)};
7602
+ const HOOK_PROFILE_VALUES = new Set(["minimal", "standard", "strict"]);
7603
+ const MINIMAL_PROFILE_ALLOWED_HOOKS = new Set([
7604
+ "session-start",
7605
+ "session-start-refresh",
7606
+ "stop-handoff"
7607
+ ]);
7608
+ const SESSION_DIGEST_SCHEMA_VERSION = 1;
7609
+ const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
7610
+ const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
7611
+ const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
7718
7612
 
7719
7613
  ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
7720
7614
  ${SHARED_STAGE_SUPPORT_SNIPPETS}
7721
7615
 
7616
+ let ACTIVE_HOOK_PROFILE = DEFAULT_HOOK_PROFILE;
7617
+
7618
+ function normalizeHookToken(value) {
7619
+ return String(value == null ? "" : value).trim().toLowerCase();
7620
+ }
7621
+
7622
+ function parseHookProfile(rawValue, fallback = "standard") {
7623
+ const normalized = normalizeHookToken(rawValue);
7624
+ if (HOOK_PROFILE_VALUES.has(normalized)) return normalized;
7625
+ return fallback;
7626
+ }
7627
+
7628
+ function parseDisabledHooksCsv(rawValue) {
7629
+ const raw = typeof rawValue === "string" ? rawValue : "";
7630
+ if (raw.trim().length === 0) return [];
7631
+ const out = [];
7632
+ for (const token of raw.split(",")) {
7633
+ const normalized = normalizeHookToken(token);
7634
+ if (normalized.length === 0) continue;
7635
+ if (!out.includes(normalized)) out.push(normalized);
7636
+ }
7637
+ return out;
7638
+ }
7639
+
7640
+ function parseInlineYamlList(rawValue) {
7641
+ const raw = typeof rawValue === "string" ? rawValue.trim() : "";
7642
+ if (!raw.startsWith("[") || !raw.endsWith("]")) return [];
7643
+ const inside = raw.slice(1, -1).trim();
7644
+ if (inside.length === 0) return [];
7645
+ return inside.split(",").map((token) => normalizeHookToken(token.replace(/^['"]|['"]$/g, ""))).filter((token) => token.length > 0);
7646
+ }
7647
+
7648
+ function parseConfigHookProfile(rawYaml) {
7649
+ if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
7650
+ return "";
7651
+ }
7652
+ const match = rawYaml.match(/^\\s*hookProfile\\s*:\\s*([A-Za-z0-9_-]+)\\s*$/m);
7653
+ if (!match || typeof match[1] !== "string") return "";
7654
+ return parseHookProfile(match[1], "");
7655
+ }
7656
+
7657
+ function parseConfigDisabledHooks(rawYaml) {
7658
+ if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
7659
+ return [];
7660
+ }
7661
+ const lines = rawYaml.split(/\\r?\\n/u);
7662
+ const out = [];
7663
+ for (let i = 0; i < lines.length; i += 1) {
7664
+ const line = lines[i];
7665
+ const inlineMatch = line.match(/^\\s*disabledHooks\\s*:\\s*(\\[[^\\]]*\\])\\s*$/u);
7666
+ if (inlineMatch) {
7667
+ for (const value of parseInlineYamlList(inlineMatch[1])) {
7668
+ if (!out.includes(value)) out.push(value);
7669
+ }
7670
+ continue;
7671
+ }
7672
+ const blockMatch = line.match(/^(\\s*)disabledHooks\\s*:\\s*$/u);
7673
+ if (!blockMatch) continue;
7674
+ const baseIndent = blockMatch[1] ? blockMatch[1].length : 0;
7675
+ for (let j = i + 1; j < lines.length; j += 1) {
7676
+ const nextLine = lines[j];
7677
+ const indent = (nextLine.match(/^(\\s*)/u)?.[1].length ?? 0);
7678
+ const trimmed = nextLine.trim();
7679
+ if (trimmed.length === 0) continue;
7680
+ if (indent <= baseIndent) break;
7681
+ const itemMatch = nextLine.match(/^\\s*-\\s*(.+?)\\s*$/u);
7682
+ if (!itemMatch) continue;
7683
+ const normalized = normalizeHookToken(itemMatch[1].replace(/^['"]|['"]$/g, ""));
7684
+ if (normalized.length === 0) continue;
7685
+ if (!out.includes(normalized)) out.push(normalized);
7686
+ }
7687
+ }
7688
+ return out;
7689
+ }
7690
+
7691
+ async function readConfigHookPolicy(root) {
7692
+ const configPath = path.join(root, RUNTIME_ROOT, "config.yaml");
7693
+ const raw = await readTextFile(configPath, "");
7694
+ const profile = parseConfigHookProfile(raw);
7695
+ const disabledHooks = parseConfigDisabledHooks(raw);
7696
+ return { profile, disabledHooks };
7697
+ }
7698
+
7699
+ async function resolveHookPolicy(root) {
7700
+ const fromConfig = await readConfigHookPolicy(root);
7701
+ const configProfile = parseHookProfile(fromConfig.profile, DEFAULT_HOOK_PROFILE);
7702
+ const configDisabledHooks = Array.isArray(fromConfig.disabledHooks) && fromConfig.disabledHooks.length > 0
7703
+ ? fromConfig.disabledHooks
7704
+ : DEFAULT_DISABLED_HOOKS;
7705
+
7706
+ const envProfileRaw = process.env.CCLAW_HOOK_PROFILE;
7707
+ const envProfile = parseHookProfile(envProfileRaw, "");
7708
+ const profile = envProfile.length > 0 ? envProfile : configProfile;
7709
+
7710
+ const envDisabledRaw = process.env.CCLAW_DISABLED_HOOKS;
7711
+ const envDisabledHooks = parseDisabledHooksCsv(envDisabledRaw);
7712
+ const disabledHooks = envDisabledHooks.length > 0 ? envDisabledHooks : configDisabledHooks;
7713
+ const disabled = new Set(disabledHooks.map((value) => normalizeHookToken(value)));
7714
+ return { profile, disabled };
7715
+ }
7716
+
7717
+ function hookDisabledByProfile(profile, hookName) {
7718
+ if (profile !== "minimal") return false;
7719
+ return !MINIMAL_PROFILE_ALLOWED_HOOKS.has(hookName);
7720
+ }
7721
+
7722
+ function isHookDisabled(policy, hookName) {
7723
+ if (policy.disabled.has(hookName)) return true;
7724
+ return hookDisabledByProfile(policy.profile, hookName);
7725
+ }
7726
+
7722
7727
  function resolveStrictness() {
7728
+ if (ACTIVE_HOOK_PROFILE === "strict") return "strict";
7723
7729
  return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
7724
7730
  }
7725
7731
 
@@ -8140,9 +8146,10 @@ function hookEventNameForOutput(hookName) {
8140
8146
  if (hookName === "session-start") return "SessionStart";
8141
8147
  if (hookName === "prompt-guard") return "PreToolUse";
8142
8148
  if (hookName === "workflow-guard") return "PreToolUse";
8149
+ if (hookName === "pre-tool-pipeline") return "PreToolUse";
8150
+ if (hookName === "prompt-pipeline") return "UserPromptSubmit";
8143
8151
  if (hookName === "context-monitor") return "PostToolUse";
8144
8152
  if (hookName === "stop-handoff") return "Stop";
8145
- if (hookName === "pre-compact") return "PreCompact";
8146
8153
  if (hookName === "verify-current-state") return "UserPromptSubmit";
8147
8154
  return "SessionStart";
8148
8155
  }
@@ -8551,78 +8558,55 @@ async function readFlowState(root) {
8551
8558
  };
8552
8559
  }
8553
8560
 
8554
-
8555
- async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
8556
- const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
8557
- // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
8558
- // Falls back to a local read if nothing is passed in.
8559
- const raw = typeof prereadRaw === "string"
8560
- ? prereadRaw
8561
- : await readTextFile(knowledgeFile, "");
8562
- const digest = parseKnowledgeDigest(raw, currentStage, 6);
8563
- return {
8564
- digestLines: digest.lines,
8565
- learningsCount: digest.learningsCount
8566
- };
8561
+ async function readFileMtimeMs(filePath) {
8562
+ try {
8563
+ const stat = await fs.stat(filePath);
8564
+ if (!stat.isFile()) return 0;
8565
+ return Math.trunc(stat.mtimeMs);
8566
+ } catch {
8567
+ return 0;
8568
+ }
8567
8569
  }
8568
8570
 
8569
- async function readStageSupportContext(root, currentStage) {
8570
- if (!isKnownStageId(currentStage)) return [];
8571
- const stage = currentStage;
8572
-
8573
- const parts = [];
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
- }
8571
+ function parseNumericMs(value) {
8572
+ return typeof value === "number" && Number.isFinite(value)
8573
+ ? Math.trunc(value)
8574
+ : -1;
8575
+ }
8582
8576
 
8583
- const promptName = reviewPromptFileName(stage);
8584
- if (typeof promptName === "string") {
8585
- const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
8586
- const prompt = (await readTextFile(promptPath, "")).trim();
8587
- if (prompt.length > 0) {
8588
- parts.push(
8589
- "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
8590
- prompt
8591
- );
8592
- }
8577
+ async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
8578
+ const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
8579
+ const cache = toObject(await readJsonFile(cachePath, {})) || {};
8580
+ const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
8581
+ const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
8582
+ const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
8583
+ const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
8584
+ if (!fresh) {
8585
+ return {
8586
+ ralphLoopLine: "",
8587
+ earlyLoopLine: "",
8588
+ compoundReadinessLine: "",
8589
+ fresh: false
8590
+ };
8593
8591
  }
8594
-
8595
- return parts;
8592
+ return {
8593
+ ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
8594
+ earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
8595
+ compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
8596
+ fresh: true
8597
+ };
8596
8598
  }
8597
8599
 
8598
- async function handleSessionStart(runtime) {
8599
- const state = await readFlowState(runtime.root);
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.
8600
+ async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
8601
+ const stateDir = path.join(root, RUNTIME_ROOT, "state");
8620
8602
  let ralphLoopLine = "";
8621
8603
  let earlyLoopLine = "";
8604
+ let compoundReadinessLine = "";
8605
+
8622
8606
  if (state.currentStage === "tdd") {
8623
8607
  try {
8624
8608
  const internalRalph = await runCclawInternal(
8625
- runtime.root,
8609
+ root,
8626
8610
  ["tdd-loop-status", "--json", "--write"],
8627
8611
  { captureStdout: true }
8628
8612
  );
@@ -8635,12 +8619,8 @@ async function handleSessionStart(runtime) {
8635
8619
  }
8636
8620
  ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
8637
8621
  } 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
8622
  await recordHookError(
8643
- runtime.root,
8623
+ root,
8644
8624
  "session-start:ralph-loop",
8645
8625
  err instanceof Error ? err.message : String(err)
8646
8626
  );
@@ -8652,7 +8632,7 @@ async function handleSessionStart(runtime) {
8652
8632
  ) {
8653
8633
  try {
8654
8634
  const internalEarly = await runCclawInternal(
8655
- runtime.root,
8635
+ root,
8656
8636
  [
8657
8637
  "early-loop-status",
8658
8638
  "--json",
@@ -8674,21 +8654,17 @@ async function handleSessionStart(runtime) {
8674
8654
  earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
8675
8655
  } catch (err) {
8676
8656
  await recordHookError(
8677
- runtime.root,
8657
+ root,
8678
8658
  "session-start:early-loop",
8679
8659
  err instanceof Error ? err.message : String(err)
8680
8660
  );
8681
8661
  }
8682
8662
  }
8683
8663
 
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
8664
  try {
8689
8665
  const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
8690
8666
  const internalReadiness = await runCclawInternal(
8691
- runtime.root,
8667
+ root,
8692
8668
  shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
8693
8669
  { captureStdout: true }
8694
8670
  );
@@ -8699,30 +8675,165 @@ async function handleSessionStart(runtime) {
8699
8675
  compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
8700
8676
  }
8701
8677
  } 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
8678
  await recordHookError(
8707
- runtime.root,
8679
+ root,
8708
8680
  "session-start:compound-readiness",
8709
8681
  err instanceof Error ? err.message : String(err)
8710
8682
  );
8711
8683
  }
8712
8684
 
8713
- const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
8714
- const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
8715
- const ironLawLines = laws
8716
- .filter((row) => row && typeof row === "object")
8717
- .slice(0, 6)
8718
- .map((row) => {
8719
- const strict = row.strict === true ? "strict" : "advisory";
8720
- const id = typeof row.id === "string" && row.id.length > 0 ? row.id : "law";
8721
- const rule = typeof row.rule === "string" ? row.rule : "";
8722
- return "- [" + strict + "] " + id + " -> " + rule;
8685
+ const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
8686
+ await writeJsonFile(digestPath, {
8687
+ schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
8688
+ generatedAt: new Date().toISOString(),
8689
+ flowStateMtimeMs,
8690
+ currentStage: state.currentStage,
8691
+ activeRunId: state.activeRunId,
8692
+ ralphLoopLine,
8693
+ earlyLoopLine,
8694
+ compoundReadinessLine
8695
+ });
8696
+ }
8697
+
8698
+ async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
8699
+ if (flowStateMtimeMs <= 0) return;
8700
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8701
+ const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
8702
+ const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
8703
+
8704
+ const cache = toObject(await readJsonFile(digestPath, {})) || {};
8705
+ const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
8706
+ if (cachedMtimeMs === flowStateMtimeMs) return;
8707
+
8708
+ const marker = toObject(await readJsonFile(markerPath, {})) || {};
8709
+ const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
8710
+ const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
8711
+ const markerFresh =
8712
+ markerMtimeMs === flowStateMtimeMs &&
8713
+ markerStartedAtMs > 0 &&
8714
+ Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
8715
+ if (markerFresh) return;
8716
+
8717
+ await writeJsonFile(markerPath, {
8718
+ flowStateMtimeMs,
8719
+ startedAtMs: Date.now(),
8720
+ currentStage: state.currentStage,
8721
+ activeRunId: state.activeRunId
8722
+ });
8723
+
8724
+ try {
8725
+ const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
8726
+ cwd: runtime.root,
8727
+ stdio: "ignore",
8728
+ windowsHide: true,
8729
+ detached: true,
8730
+ env: {
8731
+ ...process.env,
8732
+ CCLAW_PROJECT_ROOT: runtime.root,
8733
+ CCLAW_BG_WORKER: "1"
8734
+ }
8723
8735
  });
8736
+ child.unref();
8737
+ } catch (err) {
8738
+ await fs.rm(markerPath, { force: true }).catch(() => undefined);
8739
+ await recordHookError(
8740
+ runtime.root,
8741
+ "session-start:spawn-refresh",
8742
+ err instanceof Error ? err.message : String(err)
8743
+ );
8744
+ }
8745
+ }
8746
+
8747
+ async function handleSessionStartRefresh(runtime) {
8748
+ const state = await readFlowState(runtime.root);
8749
+ const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8750
+ const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
8751
+ try {
8752
+ const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
8753
+ await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
8754
+ } finally {
8755
+ await fs.rm(markerPath, { force: true }).catch(() => undefined);
8756
+ }
8757
+ return 0;
8758
+ }
8759
+
8760
+
8761
+ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
8762
+ const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
8763
+ // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
8764
+ // Falls back to a local read if nothing is passed in.
8765
+ const raw = typeof prereadRaw === "string"
8766
+ ? prereadRaw
8767
+ : await readTextFile(knowledgeFile, "");
8768
+ const digest = parseKnowledgeDigest(raw, currentStage, 6);
8769
+ return {
8770
+ digestLines: digest.lines,
8771
+ learningsCount: digest.learningsCount
8772
+ };
8773
+ }
8774
+
8775
+ async function readStageSupportContext(root, currentStage) {
8776
+ if (!isKnownStageId(currentStage)) return [];
8777
+ const stage = currentStage;
8778
+
8779
+ const parts = [];
8780
+ const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
8781
+ const contract = (await readTextFile(contractPath, "")).trim();
8782
+ if (contract.length > 0) {
8783
+ parts.push(
8784
+ "Current stage state contract (read before drafting or editing the stage artifact):\\n" +
8785
+ contract
8786
+ );
8787
+ }
8788
+
8789
+ const promptName = reviewPromptFileName(stage);
8790
+ if (typeof promptName === "string") {
8791
+ const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
8792
+ const prompt = (await readTextFile(promptPath, "")).trim();
8793
+ if (prompt.length > 0) {
8794
+ parts.push(
8795
+ "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
8796
+ prompt
8797
+ );
8798
+ }
8799
+ }
8800
+
8801
+ return parts;
8802
+ }
8803
+
8804
+ async function handleSessionStart(runtime) {
8805
+ const state = await readFlowState(runtime.root);
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
+ // Wave 21 honest-core: session-start no longer runs background helper
8822
+ // pipelines or digest caches. It rehydrates flow + knowledge only.
8823
+ const ralphLoopLine = "";
8824
+ const earlyLoopLine = "";
8825
+ const compoundReadinessLine = "";
8724
8826
  const staleStages = toObject(state.raw.staleStages) || {};
8725
8827
  const staleStageNames = Object.keys(staleStages);
8828
+ const interactionHints = toObject(state.raw.interactionHints) || {};
8829
+ const stageInteractionHint = toObject(interactionHints[state.currentStage]);
8830
+ const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
8831
+ const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
8832
+ ? stageInteractionHint.sourceStage
8833
+ : "";
8834
+ const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
8835
+ ? stageInteractionHint.recordedAt
8836
+ : "";
8726
8837
  const metaContent = (await readTextFile(metaSkillFile, "")).trim();
8727
8838
  const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
8728
8839
 
@@ -8755,6 +8866,14 @@ async function handleSessionStart(runtime) {
8755
8866
  " (use npx cclaw-cli internal rewind --ack <stage> after redo)."
8756
8867
  );
8757
8868
  }
8869
+ if (skipQuestionsHintActive) {
8870
+ parts.push(
8871
+ "Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
8872
+ (skipQuestionsSource ? " from " + skipQuestionsSource : "") +
8873
+ (skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
8874
+ "). Draft with available context unless irreversible/security override checks still require explicit confirmation."
8875
+ );
8876
+ }
8758
8877
  if (knowledge.digestLines.length > 0) {
8759
8878
  parts.push(
8760
8879
  "Knowledge digest (top relevant entries):\\n" +
@@ -8764,9 +8883,6 @@ async function handleSessionStart(runtime) {
8764
8883
  if (stageSupportContext.length > 0) {
8765
8884
  parts.push(...stageSupportContext);
8766
8885
  }
8767
- if (ironLawLines.length > 0) {
8768
- parts.push("Iron laws (enforced policy highlights):\\n" + ironLawLines.join("\\n"));
8769
- }
8770
8886
  if (metaContent.length > 0) {
8771
8887
  parts.push(metaContent);
8772
8888
  }
@@ -8805,22 +8921,80 @@ async function isGitDirty(root) {
8805
8921
  });
8806
8922
  }
8807
8923
 
8808
- function stopLawIsStrict(ironLawsObj) {
8809
- if ((ironLawsObj.mode || "advisory") === "strict") return true;
8810
- const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
8811
- return laws.some(
8812
- (row) =>
8813
- row &&
8814
- typeof row === "object" &&
8815
- (row.id === "stop-clean-or-handoff" || row.id === "stop-clean-or-checkpointed") &&
8816
- row.strict === true
8817
- );
8924
+ const STOP_BLOCK_LIMIT_PER_TRANSCRIPT = 2;
8925
+
8926
+ function asBoolean(value) {
8927
+ if (value === true || value === false) return value;
8928
+ if (typeof value === "number") return Number.isFinite(value) && value !== 0;
8929
+ if (typeof value !== "string") return false;
8930
+ const normalized = value.trim().toLowerCase();
8931
+ if (normalized.length === 0) return false;
8932
+ return ["1", "true", "yes", "on"].includes(normalized);
8933
+ }
8934
+
8935
+ function stringTokenHit(value, tokens) {
8936
+ const normalized = normalizeText(value).toLowerCase();
8937
+ if (normalized.length === 0) return false;
8938
+ return tokens.some((token) => normalized.includes(token));
8939
+ }
8940
+
8941
+ function sanitizeStopSessionKey(raw) {
8942
+ const normalized = normalizeText(raw)
8943
+ .toLowerCase()
8944
+ .replace(/[^a-z0-9._-]+/gu, "-")
8945
+ .replace(/^-+|-+$/gu, "");
8946
+ return normalized.length > 0 ? normalized.slice(0, 96) : "global";
8947
+ }
8948
+
8949
+ function extractStopSignals(input, fallbackSessionKey) {
8950
+ const event = toObject(input.event) || {};
8951
+ const session = toObject(input.session) || {};
8952
+ const contextLimit =
8953
+ asBoolean(input.context_limit) ||
8954
+ asBoolean(input.contextLimit) ||
8955
+ asBoolean(event.context_limit) ||
8956
+ asBoolean(event.contextLimit) ||
8957
+ stringTokenHit(input.reason, ["context_limit", "context limit"]) ||
8958
+ stringTokenHit(event.reason, ["context_limit", "context limit"]) ||
8959
+ stringTokenHit(input.stop_reason, ["context_limit", "context limit"]) ||
8960
+ stringTokenHit(event.stop_reason, ["context_limit", "context limit"]);
8961
+ const userAbort =
8962
+ asBoolean(input.user_abort) ||
8963
+ asBoolean(input.userAbort) ||
8964
+ asBoolean(input.user_cancelled) ||
8965
+ asBoolean(input.userCancelled) ||
8966
+ asBoolean(event.user_abort) ||
8967
+ asBoolean(event.userAbort) ||
8968
+ stringTokenHit(input.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
8969
+ stringTokenHit(event.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
8970
+ stringTokenHit(input.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
8971
+ stringTokenHit(event.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]);
8972
+ const stopHookActive =
8973
+ asBoolean(input.stop_hook_active) ||
8974
+ asBoolean(input.stopHookActive) ||
8975
+ asBoolean(event.stop_hook_active) ||
8976
+ asBoolean(event.stopHookActive);
8977
+
8978
+ const sessionKeyCandidate =
8979
+ (typeof input.transcript_id === "string" && input.transcript_id) ||
8980
+ (typeof input.transcriptId === "string" && input.transcriptId) ||
8981
+ (typeof input.session_id === "string" && input.session_id) ||
8982
+ (typeof input.sessionId === "string" && input.sessionId) ||
8983
+ (typeof session.id === "string" && session.id) ||
8984
+ fallbackSessionKey;
8985
+ const sessionKey = sanitizeStopSessionKey(sessionKeyCandidate);
8986
+
8987
+ return {
8988
+ contextLimit,
8989
+ userAbort,
8990
+ stopHookActive,
8991
+ sessionKey
8992
+ };
8818
8993
  }
8819
8994
 
8820
8995
  async function handleStopHandoff(runtime) {
8821
8996
  const state = await readFlowState(runtime.root);
8822
8997
  const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8823
- const ironLawsFile = path.join(stateDir, "iron-laws.json");
8824
8998
  const input = toObject(runtime.inputData) || {};
8825
8999
  const loopCount =
8826
9000
  typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
@@ -8828,12 +9002,40 @@ async function handleStopHandoff(runtime) {
8828
9002
  : 0;
8829
9003
 
8830
9004
  const dirtyState = await isGitDirty(runtime.root);
8831
- const strictStop = stopLawIsStrict(toObject(await readJsonFile(ironLawsFile, {})) || {});
8832
- if (dirtyState === "dirty" && strictStop) {
9005
+ const stopSignals = extractStopSignals(input, "run-" + state.activeRunId);
9006
+ const safetyBypassActive = stopSignals.stopHookActive || stopSignals.userAbort || stopSignals.contextLimit;
9007
+ if (dirtyState === "dirty" && !safetyBypassActive) {
9008
+ const stopBlocksPath = path.join(stateDir, "stop-blocks-" + stopSignals.sessionKey + ".json");
9009
+ const prior = toObject(await readJsonFile(stopBlocksPath, {})) || {};
9010
+ const priorCount =
9011
+ typeof prior.blockCount === "number" && Number.isFinite(prior.blockCount)
9012
+ ? Math.max(0, Math.trunc(prior.blockCount))
9013
+ : 0;
9014
+ if (priorCount < STOP_BLOCK_LIMIT_PER_TRANSCRIPT) {
9015
+ const nextCount = priorCount + 1;
9016
+ await writeJsonFile(stopBlocksPath, {
9017
+ schemaVersion: 1,
9018
+ sessionKey: stopSignals.sessionKey,
9019
+ blockCount: nextCount,
9020
+ updatedAt: new Date().toISOString()
9021
+ });
9022
+ process.stderr.write(
9023
+ '[cclaw] Stop blocked by iron law "stop-clean-or-handoff": working tree is dirty. Commit/revert changes or record blockers in the current artifact before ending the session.\\n'
9024
+ );
9025
+ return 1;
9026
+ }
8833
9027
  process.stderr.write(
8834
- '[cclaw] Stop blocked by iron law "stop-clean-or-handoff": working tree is dirty. Commit/revert changes or record blockers in the current artifact before ending the session.\\n'
9028
+ '[cclaw] Stop advisory: dirty working tree detected, but block limit reached for this transcript (max 2). Continuing with handoff reminder only.\\n'
9029
+ );
9030
+ } else if (dirtyState === "dirty" && safetyBypassActive) {
9031
+ const reason = stopSignals.stopHookActive
9032
+ ? "stop_hook_active"
9033
+ : stopSignals.userAbort
9034
+ ? "user_abort"
9035
+ : "context_limit";
9036
+ process.stderr.write(
9037
+ "[cclaw] Stop advisory: bypassing strict stop block due to safety rule (" + reason + ").\\n"
8835
9038
  );
8836
- return 1;
8837
9039
  }
8838
9040
 
8839
9041
  const closeoutObj = toObject(state.raw.closeout) || {};
@@ -8867,10 +9069,6 @@ async function handleStopHandoff(runtime) {
8867
9069
  return 0;
8868
9070
  }
8869
9071
 
8870
- async function handlePreCompact(_runtime) {
8871
- return 0;
8872
- }
8873
-
8874
9072
  async function handlePromptGuard(runtime) {
8875
9073
  const mode = resolveStrictness();
8876
9074
  const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
@@ -9372,17 +9570,33 @@ async function handleVerifyCurrentState(runtime) {
9372
9570
  return 0;
9373
9571
  }
9374
9572
 
9573
+ async function handlePreToolPipeline(runtime) {
9574
+ const promptExitCode = await handlePromptGuard(runtime);
9575
+ if (promptExitCode !== 0) {
9576
+ return promptExitCode;
9577
+ }
9578
+ return await handleWorkflowGuard(runtime);
9579
+ }
9580
+
9581
+ async function handlePromptPipeline(runtime) {
9582
+ const promptExitCode = await handlePromptGuard(runtime);
9583
+ if (promptExitCode !== 0) {
9584
+ return promptExitCode;
9585
+ }
9586
+ const verifyExitCode = await handleVerifyCurrentState(runtime);
9587
+ if (verifyExitCode !== 0) {
9588
+ return verifyExitCode;
9589
+ }
9590
+ runtime.writeJson({ ok: true });
9591
+ return 0;
9592
+ }
9593
+
9375
9594
  function normalizeHookName(rawName) {
9376
9595
  const value = normalizeText(rawName).toLowerCase();
9377
9596
  if (value === "session-start") return "session-start";
9378
9597
  if (value === "stop-handoff" || value === "stop") return "stop-handoff";
9379
9598
  if (value === "stop-checkpoint") return "stop-handoff";
9380
- if (value === "pre-compact" || value === "precompact") return "pre-compact";
9381
9599
  if (value === "session-rehydrate") return "session-start";
9382
- if (value === "prompt-guard") return "prompt-guard";
9383
- if (value === "workflow-guard") return "workflow-guard";
9384
- if (value === "context-monitor") return "context-monitor";
9385
- if (value === "verify-current-state") return "verify-current-state";
9386
9600
  return "";
9387
9601
  }
9388
9602
 
@@ -9392,7 +9606,7 @@ async function main() {
9392
9606
  process.stderr.write(
9393
9607
  "[cclaw] run-hook: usage: node " +
9394
9608
  RUNTIME_ROOT +
9395
- "/hooks/run-hook.mjs <session-start|stop-handoff|pre-compact|prompt-guard|workflow-guard|context-monitor|verify-current-state>\\n"
9609
+ "/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
9396
9610
  );
9397
9611
  process.exitCode = 1;
9398
9612
  return;
@@ -9428,26 +9642,6 @@ async function main() {
9428
9642
  process.exitCode = await handleStopHandoff(runtime);
9429
9643
  return;
9430
9644
  }
9431
- if (hookName === "pre-compact") {
9432
- process.exitCode = await handlePreCompact(runtime);
9433
- return;
9434
- }
9435
- if (hookName === "prompt-guard") {
9436
- process.exitCode = await handlePromptGuard(runtime);
9437
- return;
9438
- }
9439
- if (hookName === "workflow-guard") {
9440
- process.exitCode = await handleWorkflowGuard(runtime);
9441
- return;
9442
- }
9443
- if (hookName === "context-monitor") {
9444
- process.exitCode = await handleContextMonitor(runtime);
9445
- return;
9446
- }
9447
- if (hookName === "verify-current-state") {
9448
- process.exitCode = await handleVerifyCurrentState(runtime);
9449
- return;
9450
- }
9451
9645
  process.stderr.write("[cclaw] run-hook: unsupported hook " + hookName + "\\n");
9452
9646
  process.exitCode = 1;
9453
9647
  } catch (error) {