cclaw-cli 2.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.
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { readConfig } from "../config.js";
4
4
  import { RUNTIME_ROOT } from "../constants.js";
5
- import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "../codex-feature-flag.js";
6
5
  import { exists } from "../fs-utils.js";
7
6
  import { HARNESS_ADAPTERS, harnessShimFileNames, harnessShimSkillNames } from "../harness-adapters.js";
8
7
  import { validateHookDocument } from "../hook-schema.js";
@@ -220,24 +219,6 @@ async function checkHarnessShims(projectRoot, harnesses) {
220
219
  }
221
220
  return findings;
222
221
  }
223
- async function checkCodexHooksFlag(harnesses) {
224
- if (!harnesses.includes("codex")) {
225
- return warningFinding("codex_hooks_flag", true, "Codex harness is not enabled.");
226
- }
227
- const configTomlPath = codexConfigPath();
228
- let existing;
229
- try {
230
- existing = await readCodexConfig(configTomlPath);
231
- }
232
- catch (error) {
233
- return warningFinding("codex_hooks_flag", false, "Could not read Codex config.toml to validate codex_hooks.", [error instanceof Error ? error.message : String(error)]);
234
- }
235
- const state = classifyCodexHooksFlag(existing);
236
- if (state === "enabled") {
237
- return warningFinding("codex_hooks_flag", true, "Codex hooks feature flag is enabled.");
238
- }
239
- return warningFinding("codex_hooks_flag", false, "Codex hooks file is present, but [features] codex_hooks is not true in Codex config.", [`configPath: ${configTomlPath}`, `state: ${state}`]);
240
- }
241
222
  function buildReport(findings) {
242
223
  const errors = findings.filter((finding) => !finding.ok && finding.severity === "error").length;
243
224
  const warnings = findings.filter((finding) => !finding.ok && finding.severity === "warning").length;
@@ -274,7 +255,6 @@ export async function runRuntimeIntegrityCommand(projectRoot, argv, io) {
274
255
  findings.push(await checkHookDocument(projectRoot, harness));
275
256
  }
276
257
  }
277
- findings.push(await checkCodexHooksFlag(harnesses));
278
258
  const report = buildReport(findings);
279
259
  if (!args.quiet) {
280
260
  if (args.json) {
package/dist/policy.js CHANGED
@@ -96,11 +96,8 @@ export async function policyChecks(projectRoot, options = {}) {
96
96
  { file: runtimeFile("skills/session/SKILL.md"), needle: "## Session Resume Protocol", name: "utility_skill:session:resume" },
97
97
  { file: runtimeFile("skills/brainstorm/SKILL.md"), needle: "## Shared Stage Guidance", name: "stage_skill:shared_guidance_inline" },
98
98
  { file: runtimeFile("hooks/run-hook.mjs"), needle: "activeRunId", name: "hooks:session_start:active_run" },
99
- { file: runtimeFile("hooks/run-hook.mjs"), needle: "write_to_cclaw_runtime", name: "hooks:guard:risky_write_advisory" },
100
- { file: runtimeFile("hooks/run-hook.mjs"), needle: "stage_invocation_without_recent_flow_read", name: "hooks:workflow_guard:flow_read_reason" },
101
- { file: runtimeFile("hooks/run-hook.mjs"), needle: "stage_jump_", name: "hooks:workflow_guard:stage_jump_reason" },
102
- { file: runtimeFile("hooks/run-hook.mjs"), needle: "tdd_write_without_open_red", name: "hooks:workflow_guard:tdd_red_first" },
103
- { file: runtimeFile("hooks/run-hook.mjs"), needle: "context remaining is", name: "hooks:context:threshold_warning" },
99
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "session-start", name: "hooks:session_start:wired" },
100
+ { file: runtimeFile("hooks/run-hook.mjs"), needle: "stop-handoff", name: "hooks:stop_handoff:wired" },
104
101
  { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "activeRunId", name: "hooks:opencode:active_run" },
105
102
  { file: runtimeFile("hooks/run-hook.mjs"), needle: "Knowledge digest", name: "hooks:session_start:knowledge_digest" },
106
103
  { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "Knowledge digest", name: "hooks:opencode:knowledge_digest" }
@@ -108,13 +105,13 @@ export async function policyChecks(projectRoot, options = {}) {
108
105
  if (activeHarnesses.has("opencode")) {
109
106
  utilitySkillChecks.push({
110
107
  file: ".opencode/plugins/cclaw-plugin.mjs",
111
- needle: "\"tool.execute.before\"",
112
- name: "hooks:opencode:deployed_tool_hook"
108
+ needle: "session-start",
109
+ name: "hooks:opencode:deployed_session_start_hook"
113
110
  });
114
111
  utilitySkillChecks.push({
115
112
  file: ".opencode/plugins/cclaw-plugin.mjs",
116
- needle: "workflow-guard",
117
- name: "hooks:opencode:deployed_workflow_guard"
113
+ needle: "stop-handoff",
114
+ name: "hooks:opencode:deployed_stop_handoff_hook"
118
115
  });
119
116
  }
120
117
  if (activeHarnesses.has("cursor")) {
@@ -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,6 +7597,14 @@ 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
+ ]);
7718
7608
  const SESSION_DIGEST_SCHEMA_VERSION = 1;
7719
7609
  const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
7720
7610
  const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
@@ -7723,7 +7613,119 @@ const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
7723
7613
  ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
7724
7614
  ${SHARED_STAGE_SUPPORT_SNIPPETS}
7725
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
+
7726
7727
  function resolveStrictness() {
7728
+ if (ACTIVE_HOOK_PROFILE === "strict") return "strict";
7727
7729
  return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
7728
7730
  }
7729
7731
 
@@ -8801,8 +8803,6 @@ async function readStageSupportContext(root, currentStage) {
8801
8803
 
8802
8804
  async function handleSessionStart(runtime) {
8803
8805
  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
8806
  const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
8807
8807
 
8808
8808
 
@@ -8818,35 +8818,11 @@ async function handleSessionStart(runtime) {
8818
8818
  );
8819
8819
  const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
8820
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
-
8839
- const ironLawsObj = toObject(await readJsonFile(ironLawsFile, {})) || {};
8840
- const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
8841
- const ironLawLines = laws
8842
- .filter((row) => row && typeof row === "object")
8843
- .slice(0, 6)
8844
- .map((row) => {
8845
- const strict = row.strict === true ? "strict" : "advisory";
8846
- const id = typeof row.id === "string" && row.id.length > 0 ? row.id : "law";
8847
- const rule = typeof row.rule === "string" ? row.rule : "";
8848
- return "- [" + strict + "] " + id + " -> " + rule;
8849
- });
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 = "";
8850
8826
  const staleStages = toObject(state.raw.staleStages) || {};
8851
8827
  const staleStageNames = Object.keys(staleStages);
8852
8828
  const interactionHints = toObject(state.raw.interactionHints) || {};
@@ -8907,9 +8883,6 @@ async function handleSessionStart(runtime) {
8907
8883
  if (stageSupportContext.length > 0) {
8908
8884
  parts.push(...stageSupportContext);
8909
8885
  }
8910
- if (ironLawLines.length > 0) {
8911
- parts.push("Iron laws (enforced policy highlights):\\n" + ironLawLines.join("\\n"));
8912
- }
8913
8886
  if (metaContent.length > 0) {
8914
8887
  parts.push(metaContent);
8915
8888
  }
@@ -8948,22 +8921,80 @@ async function isGitDirty(root) {
8948
8921
  });
8949
8922
  }
8950
8923
 
8951
- function stopLawIsStrict(ironLawsObj) {
8952
- if ((ironLawsObj.mode || "advisory") === "strict") return true;
8953
- const laws = Array.isArray(ironLawsObj.laws) ? ironLawsObj.laws : [];
8954
- return laws.some(
8955
- (row) =>
8956
- row &&
8957
- typeof row === "object" &&
8958
- (row.id === "stop-clean-or-handoff" || row.id === "stop-clean-or-checkpointed") &&
8959
- row.strict === true
8960
- );
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
+ };
8961
8993
  }
8962
8994
 
8963
8995
  async function handleStopHandoff(runtime) {
8964
8996
  const state = await readFlowState(runtime.root);
8965
8997
  const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
8966
- const ironLawsFile = path.join(stateDir, "iron-laws.json");
8967
8998
  const input = toObject(runtime.inputData) || {};
8968
8999
  const loopCount =
8969
9000
  typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
@@ -8971,12 +9002,40 @@ async function handleStopHandoff(runtime) {
8971
9002
  : 0;
8972
9003
 
8973
9004
  const dirtyState = await isGitDirty(runtime.root);
8974
- const strictStop = stopLawIsStrict(toObject(await readJsonFile(ironLawsFile, {})) || {});
8975
- 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
+ }
8976
9027
  process.stderr.write(
8977
- '[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"
8978
9038
  );
8979
- return 1;
8980
9039
  }
8981
9040
 
8982
9041
  const closeoutObj = toObject(state.raw.closeout) || {};
@@ -9535,16 +9594,9 @@ async function handlePromptPipeline(runtime) {
9535
9594
  function normalizeHookName(rawName) {
9536
9595
  const value = normalizeText(rawName).toLowerCase();
9537
9596
  if (value === "session-start") return "session-start";
9538
- if (value === "session-start-refresh") return "session-start-refresh";
9539
9597
  if (value === "stop-handoff" || value === "stop") return "stop-handoff";
9540
9598
  if (value === "stop-checkpoint") return "stop-handoff";
9541
9599
  if (value === "session-rehydrate") return "session-start";
9542
- if (value === "prompt-guard") return "prompt-guard";
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";
9546
- if (value === "context-monitor") return "context-monitor";
9547
- if (value === "verify-current-state") return "verify-current-state";
9548
9600
  return "";
9549
9601
  }
9550
9602
 
@@ -9554,7 +9606,7 @@ async function main() {
9554
9606
  process.stderr.write(
9555
9607
  "[cclaw] run-hook: usage: node " +
9556
9608
  RUNTIME_ROOT +
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"
9609
+ "/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
9558
9610
  );
9559
9611
  process.exitCode = 1;
9560
9612
  return;
@@ -9586,38 +9638,10 @@ async function main() {
9586
9638
  process.exitCode = await handleSessionStart(runtime);
9587
9639
  return;
9588
9640
  }
9589
- if (hookName === "session-start-refresh") {
9590
- process.exitCode = await handleSessionStartRefresh(runtime);
9591
- return;
9592
- }
9593
9641
  if (hookName === "stop-handoff") {
9594
9642
  process.exitCode = await handleStopHandoff(runtime);
9595
9643
  return;
9596
9644
  }
9597
- if (hookName === "prompt-guard") {
9598
- process.exitCode = await handlePromptGuard(runtime);
9599
- return;
9600
- }
9601
- if (hookName === "workflow-guard") {
9602
- process.exitCode = await handleWorkflowGuard(runtime);
9603
- return;
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
- }
9613
- if (hookName === "context-monitor") {
9614
- process.exitCode = await handleContextMonitor(runtime);
9615
- return;
9616
- }
9617
- if (hookName === "verify-current-state") {
9618
- process.exitCode = await handleVerifyCurrentState(runtime);
9619
- return;
9620
- }
9621
9645
  process.stderr.write("[cclaw] run-hook: unsupported hook " + hookName + "\\n");
9622
9646
  process.exitCode = 1;
9623
9647
  } catch (error) {