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.
- 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 +13 -5
- package/dist/cli.js +2 -9
- package/dist/config.d.ts +11 -67
- package/dist/config.js +59 -649
- package/dist/content/hook-events.js +1 -5
- package/dist/content/hook-manifest.d.ts +6 -4
- package/dist/content/hook-manifest.js +16 -65
- package/dist/content/hooks.js +54 -14
- package/dist/content/meta-skill.js +4 -3
- package/dist/content/node-hooks.d.ts +0 -26
- package/dist/content/node-hooks.js +459 -157
- package/dist/content/observe.js +5 -4
- package/dist/content/opencode-plugin.js +1 -78
- 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 +6 -2
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +9 -5
- package/dist/content/stages/tdd.js +11 -11
- 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/gate-evidence.js +1 -5
- package/dist/hook-schema.js +3 -0
- package/dist/hook-schemas/claude-hooks.v1.json +2 -5
- package/dist/hook-schemas/codex-hooks.v1.json +1 -4
- package/dist/hook-schemas/cursor-hooks.v1.json +1 -3
- package/dist/install.d.ts +2 -7
- package/dist/install.js +32 -123
- 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/internal/compound-readiness.js +1 -16
- package/dist/internal/early-loop-status.js +1 -3
- package/dist/internal/runtime-integrity.js +0 -20
- package/dist/policy.js +6 -9
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +29 -2
- package/dist/runtime/run-hook.mjs +459 -265
- package/dist/tdd-verification-evidence.js +6 -18
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- package/dist/types.d.ts +0 -56
- 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
|
-
|
|
7683
|
-
const
|
|
7561
|
+
void options;
|
|
7562
|
+
const strictness = "advisory";
|
|
7563
|
+
const tddTestPathPatterns = [
|
|
7684
7564
|
"**/*.test.*",
|
|
7685
7565
|
"**/tests/**",
|
|
7686
7566
|
"**/__tests__/**"
|
|
7687
|
-
]
|
|
7688
|
-
const tddProductionPathPatterns =
|
|
7689
|
-
const compoundRecurrenceThreshold =
|
|
7690
|
-
const earlyLoopEnabled =
|
|
7691
|
-
const earlyLoopMaxIterations =
|
|
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
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
8571
|
+
function parseNumericMs(value) {
|
|
8572
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
8573
|
+
? Math.trunc(value)
|
|
8574
|
+
: -1;
|
|
8575
|
+
}
|
|
8582
8576
|
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
.
|
|
8717
|
-
|
|
8718
|
-
.
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
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
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
return
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
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
|
|
8832
|
-
|
|
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
|
|
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
|
|
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) {
|