cclaw-cli 2.0.0 → 4.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 +13 -2
- package/dist/artifact-linter/design.js +14 -3
- package/dist/artifact-linter/scope.js +20 -33
- package/dist/artifact-linter/shared.d.ts +48 -7
- package/dist/artifact-linter/shared.js +130 -55
- package/dist/artifact-linter.d.ts +11 -1
- package/dist/artifact-linter.js +30 -16
- package/dist/cli.js +2 -9
- package/dist/config.d.ts +11 -67
- package/dist/config.js +59 -649
- package/dist/content/examples.js +8 -0
- package/dist/content/hook-events.js +0 -3
- package/dist/content/hook-manifest.d.ts +5 -2
- package/dist/content/hook-manifest.js +18 -64
- package/dist/content/hooks.js +2 -1
- package/dist/content/node-hooks.d.ts +0 -26
- package/dist/content/node-hooks.js +237 -105
- package/dist/content/observe.js +2 -1
- package/dist/content/opencode-plugin.js +1 -72
- package/dist/content/review-prompts.js +3 -3
- package/dist/content/skills-elicitation.js +58 -20
- package/dist/content/skills.js +19 -6
- package/dist/content/stage-schema.js +36 -18
- package/dist/content/stages/brainstorm.js +3 -3
- package/dist/content/stages/design.js +4 -4
- package/dist/content/stages/plan.js +3 -3
- package/dist/content/stages/schema-types.d.ts +9 -0
- package/dist/content/stages/scope.js +8 -8
- package/dist/content/stages/tdd.js +11 -11
- package/dist/content/templates.d.ts +8 -1
- package/dist/content/templates.js +80 -18
- package/dist/gate-evidence.d.ts +25 -1
- package/dist/gate-evidence.js +35 -8
- package/dist/harness-adapters.js +8 -0
- package/dist/hook-schema.js +3 -0
- package/dist/hook-schemas/claude-hooks.v1.json +0 -2
- package/dist/hook-schemas/codex-hooks.v1.json +0 -3
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -2
- package/dist/install.d.ts +2 -7
- package/dist/install.js +42 -131
- package/dist/internal/advance-stage/advance.d.ts +1 -0
- package/dist/internal/advance-stage/advance.js +5 -2
- 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/runtime/run-hook.mjs +237 -213
- package/dist/tdd-verification-evidence.js +6 -18
- 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,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
|
-
//
|
|
8822
|
-
//
|
|
8823
|
-
|
|
8824
|
-
const
|
|
8825
|
-
const
|
|
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
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
return
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
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
|
|
8975
|
-
|
|
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
|
|
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|
|
|
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) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { readConfig } from "./config.js";
|
|
3
2
|
import { exists } from "./fs-utils.js";
|
|
4
3
|
export const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|npm run test(?::[\w:-]+)?|pnpm test|pnpm [\w:-]*test[\w:-]*|yarn test|yarn [\w:-]*test[\w:-]*|bun test|bun run test(?::[\w:-]+)?|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|\.\/gradlew test|dotnet test)\b/iu;
|
|
5
4
|
export const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
@@ -8,10 +7,8 @@ export const NO_VCS_ATTESTATION_PATTERN = /\b(?:no[-_ ]?vcs|no git|not a git rep
|
|
|
8
7
|
export const NO_VCS_HASH_PATTERN = /\b(?:content|artifact)[-_ ]?hash\s*[:=]\s*(?:sha256:)?[0-9a-f]{16,64}\b|\bsha256\s*[:=]\s*[0-9a-f]{16,64}\b/iu;
|
|
9
8
|
export async function validateTddVerificationEvidence(projectRoot, evidence, options = {}) {
|
|
10
9
|
const normalized = evidence.trim();
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const configuredVcs = config.vcs ?? "git-local-only";
|
|
14
|
-
const gitPresent = configuredVcs !== "none" && await exists(path.join(projectRoot, ".git"));
|
|
10
|
+
const mode = "auto";
|
|
11
|
+
const gitPresent = await exists(path.join(projectRoot, ".git"));
|
|
15
12
|
const issues = [];
|
|
16
13
|
if (options.requireCommand !== false && !TEST_COMMAND_HINT_PATTERN.test(normalized)) {
|
|
17
14
|
issues.push("GREEN repair needed: include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).");
|
|
@@ -21,23 +18,14 @@ export async function validateTddVerificationEvidence(projectRoot, evidence, opt
|
|
|
21
18
|
}
|
|
22
19
|
const hasSha = SHA_WITH_LABEL_PATTERN.test(normalized);
|
|
23
20
|
const hasNoVcs = NO_VCS_ATTESTATION_PATTERN.test(normalized);
|
|
24
|
-
|
|
25
|
-
if (mode !== "disabled" && configuredVcs === "none") {
|
|
26
|
-
if (!hasNoVcs) {
|
|
27
|
-
issues.push("NO_VCS_MODE repair needed: include an explicit no-VCS reason because `vcs` is `none`.");
|
|
28
|
-
}
|
|
29
|
-
if (!hasNoVcsHash) {
|
|
30
|
-
issues.push("NO_VCS_MODE repair needed: include a content/artifact hash for no-VCS TDD evidence (for example `artifact-hash: sha256:<hash>`).");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else if (mode === "required" && !hasSha) {
|
|
34
|
-
issues.push("must include a commit SHA token prefixed with `sha` or `commit` because `tdd.verificationRef` is `required`.");
|
|
35
|
-
}
|
|
36
|
-
else if (mode === "auto" && gitPresent && !hasSha) {
|
|
21
|
+
if (mode === "auto" && gitPresent && !hasSha) {
|
|
37
22
|
issues.push("must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).");
|
|
38
23
|
}
|
|
39
24
|
else if (mode === "auto" && !gitPresent && !hasSha && !hasNoVcs) {
|
|
40
25
|
issues.push("must include either a commit SHA or an explicit no-VCS attestation (for example `no-vcs: project has no .git directory`).");
|
|
41
26
|
}
|
|
27
|
+
else if (mode === "auto" && !gitPresent && hasNoVcs && !NO_VCS_HASH_PATTERN.test(normalized)) {
|
|
28
|
+
issues.push("NO_VCS_MODE repair needed: include a content/artifact hash for no-VCS TDD evidence (for example `artifact-hash: sha256:<hash>`).");
|
|
29
|
+
}
|
|
42
30
|
return { ok: issues.length === 0, issues, mode, gitPresent };
|
|
43
31
|
}
|