cclaw-cli 6.8.0 → 6.10.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/design.js +1 -1
- package/dist/artifact-linter/plan.js +37 -0
- package/dist/artifact-linter/shared.d.ts +48 -2
- package/dist/artifact-linter/shared.js +54 -5
- package/dist/artifact-linter/tdd.d.ts +31 -0
- package/dist/artifact-linter/tdd.js +357 -17
- package/dist/artifact-linter.js +87 -2
- package/dist/content/examples.js +9 -9
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/hooks.js +140 -3
- package/dist/content/iron-laws.js +6 -2
- package/dist/content/node-hooks.js +15 -1308
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/skills-elicitation.js +2 -2
- package/dist/content/skills.js +1 -1
- package/dist/content/stages/brainstorm.js +2 -2
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/scope.js +2 -2
- package/dist/content/stages/tdd.js +7 -8
- package/dist/content/subagents.js +20 -2
- package/dist/content/templates.js +5 -15
- package/dist/delegation.d.ts +102 -3
- package/dist/delegation.js +172 -14
- package/dist/early-loop.js +15 -1
- package/dist/gate-evidence.js +15 -23
- package/dist/harness-adapters.js +4 -2
- package/dist/install.js +37 -221
- package/dist/internal/advance-stage.js +19 -3
- package/dist/internal/detect-supply-chain-changes.d.ts +6 -0
- package/dist/internal/detect-supply-chain-changes.js +138 -0
- package/dist/internal/flow-state-repair.d.ts +7 -0
- package/dist/internal/flow-state-repair.js +57 -18
- package/dist/internal/plan-split-waves.d.ts +66 -0
- package/dist/internal/plan-split-waves.js +249 -0
- package/dist/run-persistence.d.ts +2 -0
- package/dist/run-persistence.js +62 -3
- package/dist/runtime/run-hook.mjs +44 -8729
- package/dist/tdd-slices.d.ts +90 -0
- package/dist/tdd-slices.js +375 -0
- package/package.json +1 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD, DEFAULT_EARLY_LOOP_MAX_ITERATIONS } from "../config.js";
|
|
5
4
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
6
|
-
import { SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD, SMALL_PROJECT_RECURRENCE_THRESHOLD } from "../knowledge-store.js";
|
|
7
5
|
import { SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS, SHARED_STAGE_SUPPORT_SNIPPETS } from "./runtime-shared-snippets.js";
|
|
8
6
|
function resolveCliRuntimeForGeneratedHook() {
|
|
9
7
|
const here = fileURLToPath(import.meta.url);
|
|
@@ -35,16 +33,6 @@ function resolveCliRuntimeForGeneratedHook() {
|
|
|
35
33
|
*/
|
|
36
34
|
export function nodeHookRuntimeScript(options = {}) {
|
|
37
35
|
void options;
|
|
38
|
-
const strictness = "advisory";
|
|
39
|
-
const tddTestPathPatterns = [
|
|
40
|
-
"**/*.test.*",
|
|
41
|
-
"**/tests/**",
|
|
42
|
-
"**/__tests__/**"
|
|
43
|
-
];
|
|
44
|
-
const tddProductionPathPatterns = [];
|
|
45
|
-
const compoundRecurrenceThreshold = DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
46
|
-
const earlyLoopEnabled = true;
|
|
47
|
-
const earlyLoopMaxIterations = DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
|
|
48
36
|
const defaultHookProfile = "standard";
|
|
49
37
|
const defaultDisabledHooks = [];
|
|
50
38
|
const cliRuntime = resolveCliRuntimeForGeneratedHook();
|
|
@@ -57,22 +45,6 @@ import { spawn } from "node:child_process";
|
|
|
57
45
|
|
|
58
46
|
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
59
47
|
const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
|
|
60
|
-
// Single strictness default, derived from config.strictness at install time.
|
|
61
|
-
// \`CCLAW_STRICTNESS\` env var overrides for the current process. All guards
|
|
62
|
-
// (prompt, workflow, TDD, iron-laws) route through \`resolveStrictness()\`.
|
|
63
|
-
const DEFAULT_STRICTNESS = ${JSON.stringify(strictness)};
|
|
64
|
-
const DEFAULT_TDD_TEST_PATH_PATTERNS = ${JSON.stringify(tddTestPathPatterns)};
|
|
65
|
-
const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = ${JSON.stringify(tddProductionPathPatterns)};
|
|
66
|
-
// Compound-readiness recurrence threshold. Baked from
|
|
67
|
-
// \`config.compound.recurrenceThreshold\` at install time so the hook and
|
|
68
|
-
// \`cclaw internal compound-readiness\` agree on the same number. The
|
|
69
|
-
// small-project relaxation rule (<${SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD} archived runs
|
|
70
|
-
// -> min(base, ${SMALL_PROJECT_RECURRENCE_THRESHOLD})) is applied at runtime.
|
|
71
|
-
const COMPOUND_RECURRENCE_THRESHOLD = ${JSON.stringify(compoundRecurrenceThreshold)};
|
|
72
|
-
const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD)};
|
|
73
|
-
const SMALL_PROJECT_RECURRENCE_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_RECURRENCE_THRESHOLD)};
|
|
74
|
-
const EARLY_LOOP_ENABLED = ${JSON.stringify(earlyLoopEnabled)};
|
|
75
|
-
const EARLY_LOOP_MAX_ITERATIONS = ${JSON.stringify(earlyLoopMaxIterations)};
|
|
76
48
|
const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
|
|
77
49
|
const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
|
|
78
50
|
const DEFAULT_HOOK_PROFILE = ${JSON.stringify(defaultHookProfile)};
|
|
@@ -80,19 +52,12 @@ const DEFAULT_DISABLED_HOOKS = ${JSON.stringify(defaultDisabledHooks)};
|
|
|
80
52
|
const HOOK_PROFILE_VALUES = new Set(["minimal", "standard", "strict"]);
|
|
81
53
|
const MINIMAL_PROFILE_ALLOWED_HOOKS = new Set([
|
|
82
54
|
"session-start",
|
|
83
|
-
"session-start-refresh",
|
|
84
55
|
"stop-handoff"
|
|
85
56
|
]);
|
|
86
|
-
const SESSION_DIGEST_SCHEMA_VERSION = 1;
|
|
87
|
-
const SESSION_DIGEST_CACHE_FILE = "session-digest.json";
|
|
88
|
-
const SESSION_DIGEST_REFRESH_MARKER_FILE = "session-digest.refresh.json";
|
|
89
|
-
const SESSION_DIGEST_REFRESH_STALE_MS = 30000;
|
|
90
57
|
|
|
91
58
|
${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
|
|
92
59
|
${SHARED_STAGE_SUPPORT_SNIPPETS}
|
|
93
60
|
|
|
94
|
-
let ACTIVE_HOOK_PROFILE = DEFAULT_HOOK_PROFILE;
|
|
95
|
-
|
|
96
61
|
function normalizeHookToken(value) {
|
|
97
62
|
return String(value == null ? "" : value).trim().toLowerCase();
|
|
98
63
|
}
|
|
@@ -202,11 +167,6 @@ function isHookDisabled(policy, hookName) {
|
|
|
202
167
|
return hookDisabledByProfile(policy.profile, hookName);
|
|
203
168
|
}
|
|
204
169
|
|
|
205
|
-
function resolveStrictness() {
|
|
206
|
-
if (ACTIVE_HOOK_PROFILE === "strict") return "strict";
|
|
207
|
-
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
170
|
function toObject(value) {
|
|
211
171
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
212
172
|
return value;
|
|
@@ -428,14 +388,6 @@ async function readTextFileLocked(lockPath, filePath, fallback = "") {
|
|
|
428
388
|
});
|
|
429
389
|
}
|
|
430
390
|
|
|
431
|
-
async function appendJsonLine(filePath, value) {
|
|
432
|
-
const payload = JSON.stringify(value) + "\\n";
|
|
433
|
-
await withDirectoryLockInline(lockPathFor(filePath), async () => {
|
|
434
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
435
|
-
await fs.appendFile(filePath, payload, "utf8");
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
391
|
async function readStdin() {
|
|
440
392
|
return await new Promise((resolve) => {
|
|
441
393
|
let data = "";
|
|
@@ -448,171 +400,6 @@ async function readStdin() {
|
|
|
448
400
|
});
|
|
449
401
|
}
|
|
450
402
|
|
|
451
|
-
async function runCclawInternal(root, args, options = {}) {
|
|
452
|
-
const cliEntrypoint = process.env.CCLAW_CLI_JS || CCLAW_CLI_ENTRYPOINT;
|
|
453
|
-
const cliArgsPrefix = process.env.CCLAW_CLI_JS ? [] : CCLAW_CLI_ARGS_PREFIX;
|
|
454
|
-
if (!cliEntrypoint || String(cliEntrypoint).trim().length === 0) {
|
|
455
|
-
return {
|
|
456
|
-
code: 1,
|
|
457
|
-
stdout: "",
|
|
458
|
-
stderr: "[cclaw] hook: local Node runtime entrypoint is missing. Re-run npx cclaw-cli sync or npx cclaw-cli upgrade.\\n",
|
|
459
|
-
missingBinary: true
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
const stat = await fs.stat(cliEntrypoint);
|
|
464
|
-
if (!stat.isFile()) throw new Error("not-file");
|
|
465
|
-
for (const argPath of cliArgsPrefix) {
|
|
466
|
-
if (typeof argPath !== "string" || argPath.startsWith("-")) continue;
|
|
467
|
-
const argStat = await fs.stat(argPath);
|
|
468
|
-
if (!argStat.isFile()) throw new Error("arg-not-file");
|
|
469
|
-
}
|
|
470
|
-
} catch {
|
|
471
|
-
return {
|
|
472
|
-
code: 1,
|
|
473
|
-
stdout: "",
|
|
474
|
-
stderr: "[cclaw] hook: local Node runtime entrypoint not found at " + cliEntrypoint + ". Re-run npx cclaw-cli sync or npx cclaw-cli upgrade.\\n",
|
|
475
|
-
missingBinary: true
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return await new Promise((resolve) => {
|
|
480
|
-
const captureStdout = options && options.captureStdout === true;
|
|
481
|
-
let settled = false;
|
|
482
|
-
let stdout = "";
|
|
483
|
-
let stderr = "";
|
|
484
|
-
const finalize = (value) => {
|
|
485
|
-
if (settled) return;
|
|
486
|
-
settled = true;
|
|
487
|
-
resolve(value);
|
|
488
|
-
};
|
|
489
|
-
let child;
|
|
490
|
-
try {
|
|
491
|
-
child = spawn(process.execPath, [cliEntrypoint, ...cliArgsPrefix, "internal", ...args], {
|
|
492
|
-
cwd: root,
|
|
493
|
-
env: process.env,
|
|
494
|
-
stdio: ["ignore", captureStdout ? "pipe" : "ignore", "pipe"]
|
|
495
|
-
});
|
|
496
|
-
} catch (error) {
|
|
497
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
498
|
-
finalize({
|
|
499
|
-
code: 1,
|
|
500
|
-
stdout,
|
|
501
|
-
stderr,
|
|
502
|
-
missingBinary: code === "ENOENT"
|
|
503
|
-
});
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
if (captureStdout) {
|
|
507
|
-
child.stdout?.on("data", (chunk) => {
|
|
508
|
-
stdout += String(chunk ?? "");
|
|
509
|
-
if (stdout.length > 16000) {
|
|
510
|
-
stdout = stdout.slice(-16000);
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
child.stderr?.on("data", (chunk) => {
|
|
515
|
-
stderr += String(chunk ?? "");
|
|
516
|
-
if (stderr.length > 8000) {
|
|
517
|
-
stderr = stderr.slice(-8000);
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
child.on("error", (error) => {
|
|
521
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
522
|
-
finalize({
|
|
523
|
-
code: 1,
|
|
524
|
-
stdout,
|
|
525
|
-
stderr,
|
|
526
|
-
missingBinary: code === "ENOENT"
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
child.on("close", (code, signal) => {
|
|
530
|
-
if (signal) {
|
|
531
|
-
finalize({
|
|
532
|
-
code: 1,
|
|
533
|
-
stdout,
|
|
534
|
-
stderr,
|
|
535
|
-
missingBinary: false
|
|
536
|
-
});
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
finalize({
|
|
540
|
-
code: typeof code === "number" ? code : 1,
|
|
541
|
-
stdout,
|
|
542
|
-
stderr,
|
|
543
|
-
missingBinary: false
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function compactStderr(value) {
|
|
550
|
-
const raw = typeof value === "string" ? value : "";
|
|
551
|
-
return raw.replace(/\\s+/gu, " ").trim();
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function summarizeInternalFailure(operation, result) {
|
|
555
|
-
const detail = compactStderr(result && typeof result === "object" ? result.stderr : "");
|
|
556
|
-
return detail.length > 0 ? operation + ": " + detail : operation + " failed";
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function parseJsonStdoutObject(result) {
|
|
560
|
-
const raw = typeof (result && result.stdout) === "string" ? result.stdout.trim() : "";
|
|
561
|
-
if (raw.length === 0) return null;
|
|
562
|
-
try {
|
|
563
|
-
return toObject(JSON.parse(raw));
|
|
564
|
-
} catch {
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function firstStdoutLine(value) {
|
|
570
|
-
const raw = typeof value === "string" ? value : "";
|
|
571
|
-
const lines = raw
|
|
572
|
-
.split(/\\r?\\n/gu)
|
|
573
|
-
.map((line) => line.trim())
|
|
574
|
-
.filter((line) => line.length > 0);
|
|
575
|
-
return lines[0] || "";
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function formatRalphLoopStatusLineFromJson(status) {
|
|
579
|
-
const redOpenSlices = Array.isArray(status.redOpenSlices)
|
|
580
|
-
? status.redOpenSlices.filter((value) => typeof value === "string")
|
|
581
|
-
: [];
|
|
582
|
-
const redOpen = redOpenSlices.length > 0 ? redOpenSlices.join(",") : "none";
|
|
583
|
-
const loopIteration =
|
|
584
|
-
typeof status.loopIteration === "number" && Number.isFinite(status.loopIteration)
|
|
585
|
-
? Math.trunc(status.loopIteration)
|
|
586
|
-
: 0;
|
|
587
|
-
const sliceCount =
|
|
588
|
-
typeof status.sliceCount === "number" && Number.isFinite(status.sliceCount)
|
|
589
|
-
? Math.trunc(status.sliceCount)
|
|
590
|
-
: 0;
|
|
591
|
-
const acClosed = Array.isArray(status.acClosed) ? status.acClosed.length : 0;
|
|
592
|
-
return "Ralph Loop: iter=" + String(loopIteration) +
|
|
593
|
-
", slices=" + String(sliceCount) +
|
|
594
|
-
", acClosed=" + String(acClosed) +
|
|
595
|
-
", redOpen=" + redOpen;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function formatEarlyLoopStatusLineFromJson(status) {
|
|
599
|
-
const stage = typeof status.stage === "string" ? status.stage : "unknown";
|
|
600
|
-
const iteration =
|
|
601
|
-
typeof status.iteration === "number" && Number.isFinite(status.iteration)
|
|
602
|
-
? Math.trunc(status.iteration)
|
|
603
|
-
: 0;
|
|
604
|
-
const maxIterations =
|
|
605
|
-
typeof status.maxIterations === "number" && Number.isFinite(status.maxIterations)
|
|
606
|
-
? Math.trunc(status.maxIterations)
|
|
607
|
-
: EARLY_LOOP_MAX_ITERATIONS;
|
|
608
|
-
const openConcerns = Array.isArray(status.openConcerns) ? status.openConcerns.length : 0;
|
|
609
|
-
const convergence = status.convergenceTripped === true ? "tripped" : "clear";
|
|
610
|
-
return "Early Loop: stage=" + stage +
|
|
611
|
-
", iter=" + String(iteration) + "/" + String(maxIterations) +
|
|
612
|
-
", open=" + String(openConcerns) +
|
|
613
|
-
", convergence=" + convergence;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
403
|
function detectHarness(env) {
|
|
617
404
|
if (env.CLAUDE_PROJECT_DIR) return "claude";
|
|
618
405
|
if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
|
|
@@ -620,33 +407,6 @@ function detectHarness(env) {
|
|
|
620
407
|
return "codex";
|
|
621
408
|
}
|
|
622
409
|
|
|
623
|
-
function hookEventNameForOutput(hookName) {
|
|
624
|
-
if (hookName === "session-start") return "SessionStart";
|
|
625
|
-
if (hookName === "prompt-guard") return "PreToolUse";
|
|
626
|
-
if (hookName === "workflow-guard") return "PreToolUse";
|
|
627
|
-
if (hookName === "pre-tool-pipeline") return "PreToolUse";
|
|
628
|
-
if (hookName === "prompt-pipeline") return "UserPromptSubmit";
|
|
629
|
-
if (hookName === "context-monitor") return "PostToolUse";
|
|
630
|
-
if (hookName === "stop-handoff") return "Stop";
|
|
631
|
-
if (hookName === "verify-current-state") return "UserPromptSubmit";
|
|
632
|
-
return "SessionStart";
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function emitAdvisoryContext(runtime, hookName, note) {
|
|
636
|
-
const normalized = normalizeText(note);
|
|
637
|
-
if (normalized.length === 0) return;
|
|
638
|
-
if (runtime.harness === "claude" || runtime.harness === "codex") {
|
|
639
|
-
runtime.writeJson({
|
|
640
|
-
hookSpecificOutput: {
|
|
641
|
-
hookEventName: hookEventNameForOutput(hookName),
|
|
642
|
-
additionalContext: normalized
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
runtime.writeJson({ additional_context: normalized });
|
|
648
|
-
}
|
|
649
|
-
|
|
650
410
|
async function detectRoot(env) {
|
|
651
411
|
const candidates = [
|
|
652
412
|
env.CCLAW_PROJECT_ROOT,
|
|
@@ -669,356 +429,10 @@ async function detectRoot(env) {
|
|
|
669
429
|
return { root: candidates[0] || process.cwd(), foundRuntime: false };
|
|
670
430
|
}
|
|
671
431
|
|
|
672
|
-
function toLower(value) {
|
|
673
|
-
return String(value || "").toLowerCase();
|
|
674
|
-
}
|
|
675
|
-
|
|
676
432
|
function normalizeText(value) {
|
|
677
433
|
return String(value || "").replace(/\\s+/gu, " ").trim();
|
|
678
434
|
}
|
|
679
435
|
|
|
680
|
-
// Mirrors \`src/tdd-cycle.ts::normalizeTddPath\`. Any change to
|
|
681
|
-
// canonical normalization must be updated in BOTH places; the
|
|
682
|
-
// tdd-parity test asserts matcher behavior agrees end-to-end.
|
|
683
|
-
function normalizePathForMatch(rawPath) {
|
|
684
|
-
return String(rawPath == null ? "" : rawPath)
|
|
685
|
-
.trim()
|
|
686
|
-
.replace(/\\\\/gu, "/")
|
|
687
|
-
.replace(/^\\.\\//u, "")
|
|
688
|
-
.toLowerCase();
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Mirrors \`src/tdd-cycle.ts::pathMatchesTarget\`. Use instead of raw
|
|
692
|
-
// \`===\` when checking recorded files against a target path.
|
|
693
|
-
function pathMatchesTargetInline(candidate, target) {
|
|
694
|
-
const normalizedCandidate = normalizePathForMatch(candidate);
|
|
695
|
-
const normalizedTarget = normalizePathForMatch(target);
|
|
696
|
-
if (normalizedCandidate.length === 0 || normalizedTarget.length === 0) {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
return (
|
|
700
|
-
normalizedCandidate === normalizedTarget ||
|
|
701
|
-
normalizedCandidate.endsWith("/" + normalizedTarget)
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function normalizeToolName(value) {
|
|
706
|
-
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
707
|
-
if (value && typeof value === "object") {
|
|
708
|
-
if (typeof value.name === "string" && value.name.trim().length > 0) {
|
|
709
|
-
return value.name.trim();
|
|
710
|
-
}
|
|
711
|
-
if (typeof value.id === "string" && value.id.trim().length > 0) {
|
|
712
|
-
return value.id.trim();
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
return "";
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function extractToolAndPayload(inputData, inputRaw) {
|
|
719
|
-
const root = toObject(inputData) || {};
|
|
720
|
-
const nestedInput = toObject(root.input) || {};
|
|
721
|
-
const nestedTool = toObject(root.tool) || {};
|
|
722
|
-
const nestedInputTool = toObject(nestedInput.tool) || {};
|
|
723
|
-
const candidates = [
|
|
724
|
-
root.tool_name,
|
|
725
|
-
root.tool,
|
|
726
|
-
root.toolName,
|
|
727
|
-
root.name,
|
|
728
|
-
root.id,
|
|
729
|
-
root.command,
|
|
730
|
-
nestedTool.name,
|
|
731
|
-
nestedTool.id,
|
|
732
|
-
nestedInput.tool_name,
|
|
733
|
-
nestedInput.tool,
|
|
734
|
-
nestedInput.toolName,
|
|
735
|
-
nestedInput.name,
|
|
736
|
-
nestedInput.id,
|
|
737
|
-
nestedInput.command,
|
|
738
|
-
nestedInputTool.name,
|
|
739
|
-
nestedInputTool.id
|
|
740
|
-
];
|
|
741
|
-
let tool = "unknown";
|
|
742
|
-
for (const candidate of candidates) {
|
|
743
|
-
const next = normalizeToolName(candidate);
|
|
744
|
-
if (next.length > 0) {
|
|
745
|
-
tool = next;
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
const payload =
|
|
750
|
-
root.tool_input ??
|
|
751
|
-
root.input ??
|
|
752
|
-
root.arguments ??
|
|
753
|
-
root.params ??
|
|
754
|
-
root.payload ??
|
|
755
|
-
{};
|
|
756
|
-
let payloadText = "";
|
|
757
|
-
try {
|
|
758
|
-
payloadText = JSON.stringify(payload);
|
|
759
|
-
} catch {
|
|
760
|
-
payloadText = "";
|
|
761
|
-
}
|
|
762
|
-
if (payloadText.length === 0) {
|
|
763
|
-
payloadText = typeof inputRaw === "string" ? inputRaw : "";
|
|
764
|
-
}
|
|
765
|
-
return { tool, payload, payloadText };
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function collectPaths(value, bucket = new Set()) {
|
|
769
|
-
if (Array.isArray(value)) {
|
|
770
|
-
for (const item of value) collectPaths(item, bucket);
|
|
771
|
-
return bucket;
|
|
772
|
-
}
|
|
773
|
-
if (!value || typeof value !== "object") {
|
|
774
|
-
return bucket;
|
|
775
|
-
}
|
|
776
|
-
const obj = value;
|
|
777
|
-
for (const key of ["path", "file_path", "filepath"]) {
|
|
778
|
-
const current = obj[key];
|
|
779
|
-
if (typeof current === "string" && current.trim().length > 0) {
|
|
780
|
-
bucket.add(current.trim());
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
for (const child of Object.values(obj)) {
|
|
784
|
-
collectPaths(child, bucket);
|
|
785
|
-
}
|
|
786
|
-
return bucket;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const globRegexCache = new Map();
|
|
790
|
-
|
|
791
|
-
function escapeRegex(value) {
|
|
792
|
-
return value.replace(/[.*+?^\\\${}()|[\\]\\\\]/gu, "\\\\$&");
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function globToRegExp(globPattern) {
|
|
796
|
-
const normalized = normalizePathForMatch(globPattern);
|
|
797
|
-
const cached = globRegexCache.get(normalized);
|
|
798
|
-
if (cached) return cached;
|
|
799
|
-
let pattern = normalized;
|
|
800
|
-
pattern = pattern.replace(/\\*\\*\\//gu, "__GLOBSTAR_DIR__");
|
|
801
|
-
pattern = pattern.replace(/\\/\\*\\*/gu, "__DIR_GLOBSTAR__");
|
|
802
|
-
pattern = pattern.replace(/\\*\\*/gu, "__GLOBSTAR__");
|
|
803
|
-
pattern = pattern.replace(/\\*/gu, "__STAR__");
|
|
804
|
-
pattern = escapeRegex(pattern);
|
|
805
|
-
pattern = pattern.replace(/__GLOBSTAR_DIR__/gu, "(?:.*\\\\/)?");
|
|
806
|
-
pattern = pattern.replace(/__DIR_GLOBSTAR__/gu, "\\\\/.*");
|
|
807
|
-
pattern = pattern.replace(/__GLOBSTAR__/gu, ".*");
|
|
808
|
-
pattern = pattern.replace(/__STAR__/gu, "[^\\\\/]*");
|
|
809
|
-
const built = new RegExp("^" + pattern + "$", "u");
|
|
810
|
-
globRegexCache.set(normalized, built);
|
|
811
|
-
return built;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function matchesPathPatterns(rawPath, patterns) {
|
|
815
|
-
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
816
|
-
const normalized = normalizePathForMatch(rawPath);
|
|
817
|
-
for (const pattern of patterns) {
|
|
818
|
-
if (globToRegExp(pattern).test(normalized)) return true;
|
|
819
|
-
}
|
|
820
|
-
return false;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function isCodeLikePath(rawPath) {
|
|
824
|
-
return /\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$/u.test(
|
|
825
|
-
normalizePathForMatch(rawPath)
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function isMutatingTool(toolLower) {
|
|
830
|
-
return /^(write|edit|multiedit|multi_edit|delete|applypatch|apply_patch|notebookedit|notebook_edit)$/u.test(toolLower);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function isExecutionOrMutatingTool(toolLower) {
|
|
834
|
-
if (isMutatingTool(toolLower)) return true;
|
|
835
|
-
return /^(shell|bash|runcommand|run_command|execcommand|exec_command|terminal)$/u.test(toolLower);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function isPlanModeSafeTool(toolLower) {
|
|
839
|
-
return /^(read|readfile|open|view|cat|head|tail|grep|glob|search|semanticsearch|ripgrep|rg|find|list_directory|ls|askquestion|askuserquestion|ask_question|ask_user_question|question|todowrite|todoread|todo_write|todo_read|webfetch|websearch|web_fetch|web_search|fetchmcpresource|switchmode|switch_mode|task|delegate)$/u.test(
|
|
840
|
-
toolLower
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function isCclawCliPayload(payloadLower) {
|
|
845
|
-
return /(cclaw |npx cclaw |\\/cc-|\\/cc[^a-z0-9_-])/u.test(payloadLower);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function stageIndex(stage) {
|
|
849
|
-
const ordered = [
|
|
850
|
-
"brainstorm",
|
|
851
|
-
"scope",
|
|
852
|
-
"design",
|
|
853
|
-
"spec",
|
|
854
|
-
"plan",
|
|
855
|
-
"tdd",
|
|
856
|
-
"review",
|
|
857
|
-
"ship"
|
|
858
|
-
];
|
|
859
|
-
const index = ordered.indexOf(stage);
|
|
860
|
-
return index < 0 ? 0 : index + 1;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function detectTargetStage(payloadLower) {
|
|
864
|
-
for (const stage of [
|
|
865
|
-
"brainstorm",
|
|
866
|
-
"scope",
|
|
867
|
-
"design",
|
|
868
|
-
"spec",
|
|
869
|
-
"plan",
|
|
870
|
-
"tdd",
|
|
871
|
-
"review",
|
|
872
|
-
"ship"
|
|
873
|
-
]) {
|
|
874
|
-
if (new RegExp("(/cc-" + stage + "|cc-" + stage + ")([^a-z0-9_-]|$)", "u").test(payloadLower)) {
|
|
875
|
-
return stage;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
return "";
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function isFlowProgressionCommand(payloadLower) {
|
|
882
|
-
return /\\/cc([^a-z0-9_-]|$)/u.test(payloadLower);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function isPreimplementationStage(stage) {
|
|
886
|
-
return ["brainstorm", "scope", "design", "spec", "plan"].includes(stage);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function extractCommandFromPayload(payload) {
|
|
890
|
-
const stack = [payload];
|
|
891
|
-
while (stack.length > 0) {
|
|
892
|
-
const current = stack.shift();
|
|
893
|
-
if (!current || typeof current !== "object") continue;
|
|
894
|
-
if (Array.isArray(current)) {
|
|
895
|
-
for (const item of current) stack.push(item);
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
for (const key of ["command", "cmd"]) {
|
|
899
|
-
const value = current[key];
|
|
900
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
901
|
-
return value.trim();
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
for (const value of Object.values(current)) {
|
|
905
|
-
stack.push(value);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return "";
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function extractExitCodeFromPayload(payload) {
|
|
912
|
-
const stack = [payload];
|
|
913
|
-
while (stack.length > 0) {
|
|
914
|
-
const current = stack.shift();
|
|
915
|
-
if (!current || typeof current !== "object") continue;
|
|
916
|
-
if (Array.isArray(current)) {
|
|
917
|
-
for (const item of current) stack.push(item);
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
for (const key of ["exitCode", "exit_code", "code", "status"]) {
|
|
921
|
-
const value = current[key];
|
|
922
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
923
|
-
return Math.trunc(value);
|
|
924
|
-
}
|
|
925
|
-
if (typeof value === "boolean") {
|
|
926
|
-
return value ? 0 : 1;
|
|
927
|
-
}
|
|
928
|
-
if (typeof value === "string" && /^-?[0-9]+$/u.test(value.trim())) {
|
|
929
|
-
return Number(value.trim());
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
for (const value of Object.values(current)) {
|
|
933
|
-
stack.push(value);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function extractRemainingPercent(payload) {
|
|
940
|
-
const readPath = (segments) => {
|
|
941
|
-
let current = payload;
|
|
942
|
-
for (const segment of segments) {
|
|
943
|
-
if (!current || typeof current !== "object" || Array.isArray(current)) return null;
|
|
944
|
-
current = current[segment];
|
|
945
|
-
}
|
|
946
|
-
if (typeof current !== "number" || !Number.isFinite(current)) return null;
|
|
947
|
-
return current;
|
|
948
|
-
};
|
|
949
|
-
const candidates = [
|
|
950
|
-
{ path: ["context", "remaining_percent"], invert: false },
|
|
951
|
-
{ path: ["context", "remainingPercent"], invert: false },
|
|
952
|
-
{ path: ["context_usage", "remaining_percent"], invert: false },
|
|
953
|
-
{ path: ["context_usage", "remainingPercent"], invert: false },
|
|
954
|
-
{ path: ["contextUsage", "remainingPercent"], invert: false },
|
|
955
|
-
{ path: ["context_window", "remaining_percent"], invert: false },
|
|
956
|
-
{ path: ["remaining_context_percent"], invert: false },
|
|
957
|
-
{ path: ["remainingContextPercent"], invert: false },
|
|
958
|
-
{ path: ["remaining_context_ratio"], invert: false },
|
|
959
|
-
{ path: ["remainingContextRatio"], invert: false },
|
|
960
|
-
{ path: ["context", "used_percent"], invert: true },
|
|
961
|
-
{ path: ["context", "usedPercent"], invert: true },
|
|
962
|
-
{ path: ["context_usage", "used_percent"], invert: true },
|
|
963
|
-
{ path: ["context_usage", "usedPercent"], invert: true },
|
|
964
|
-
{ path: ["contextUsage", "usedPercent"], invert: true },
|
|
965
|
-
{ path: ["context_window", "used_ratio"], invert: true },
|
|
966
|
-
{ path: ["context_window", "usedRatio"], invert: true }
|
|
967
|
-
];
|
|
968
|
-
for (const candidate of candidates) {
|
|
969
|
-
const value = readPath(candidate.path);
|
|
970
|
-
if (value === null) continue;
|
|
971
|
-
let percent = value <= 1 ? value * 100 : value;
|
|
972
|
-
if (candidate.invert) {
|
|
973
|
-
percent = 100 - percent;
|
|
974
|
-
}
|
|
975
|
-
if (!Number.isFinite(percent)) continue;
|
|
976
|
-
if (percent < 0) percent = 0;
|
|
977
|
-
if (percent > 100) percent = 100;
|
|
978
|
-
return Number(percent.toFixed(2));
|
|
979
|
-
}
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function extractTextBlobs(payload) {
|
|
984
|
-
const stack = [payload];
|
|
985
|
-
const lines = [];
|
|
986
|
-
while (stack.length > 0) {
|
|
987
|
-
const current = stack.shift();
|
|
988
|
-
if (typeof current === "string" && current.length > 0) {
|
|
989
|
-
lines.push(current);
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
if (!current || typeof current !== "object") continue;
|
|
993
|
-
if (Array.isArray(current)) {
|
|
994
|
-
for (const item of current) stack.push(item);
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
for (const value of Object.values(current)) {
|
|
998
|
-
stack.push(value);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return lines.join("\\n");
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function extractCodePathsFromText(value) {
|
|
1005
|
-
const pattern =
|
|
1006
|
-
/(?:[A-Za-z0-9_.-]+[\\\\/])+[A-Za-z0-9_.-]+\\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)/gu;
|
|
1007
|
-
const matches = value.match(pattern) || [];
|
|
1008
|
-
const out = [];
|
|
1009
|
-
const seen = new Set();
|
|
1010
|
-
for (const match of matches) {
|
|
1011
|
-
const normalized = match.trim().replace(/^[\\s"']+|[\\s"'.,:;()\\[\\]{}<>]+$/gu, "");
|
|
1012
|
-
if (normalized.length === 0) continue;
|
|
1013
|
-
const key = normalizePathForMatch(normalized);
|
|
1014
|
-
if (seen.has(key)) continue;
|
|
1015
|
-
seen.add(key);
|
|
1016
|
-
out.push(normalized);
|
|
1017
|
-
if (out.length >= 20) break;
|
|
1018
|
-
}
|
|
1019
|
-
return out;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
436
|
async function verifyFlowStateGuardInline(root, hookName) {
|
|
1023
437
|
const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
|
|
1024
438
|
const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
|
|
@@ -1070,206 +484,6 @@ async function readFlowState(root) {
|
|
|
1070
484
|
};
|
|
1071
485
|
}
|
|
1072
486
|
|
|
1073
|
-
async function readFileMtimeMs(filePath) {
|
|
1074
|
-
try {
|
|
1075
|
-
const stat = await fs.stat(filePath);
|
|
1076
|
-
if (!stat.isFile()) return 0;
|
|
1077
|
-
return Math.trunc(stat.mtimeMs);
|
|
1078
|
-
} catch {
|
|
1079
|
-
return 0;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function parseNumericMs(value) {
|
|
1084
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
1085
|
-
? Math.trunc(value)
|
|
1086
|
-
: -1;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
async function readSessionDigestLines(stateDir, state, flowStateMtimeMs) {
|
|
1090
|
-
const cachePath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1091
|
-
const cache = toObject(await readJsonFile(cachePath, {})) || {};
|
|
1092
|
-
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
1093
|
-
const sameStage = typeof cache.currentStage === "string" ? cache.currentStage === state.currentStage : true;
|
|
1094
|
-
const sameRun = typeof cache.activeRunId === "string" ? cache.activeRunId === state.activeRunId : true;
|
|
1095
|
-
const fresh = cachedMtimeMs === flowStateMtimeMs && sameStage && sameRun;
|
|
1096
|
-
if (!fresh) {
|
|
1097
|
-
return {
|
|
1098
|
-
ralphLoopLine: "",
|
|
1099
|
-
earlyLoopLine: "",
|
|
1100
|
-
compoundReadinessLine: "",
|
|
1101
|
-
fresh: false
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
return {
|
|
1105
|
-
ralphLoopLine: typeof cache.ralphLoopLine === "string" ? cache.ralphLoopLine : "",
|
|
1106
|
-
earlyLoopLine: typeof cache.earlyLoopLine === "string" ? cache.earlyLoopLine : "",
|
|
1107
|
-
compoundReadinessLine: typeof cache.compoundReadinessLine === "string" ? cache.compoundReadinessLine : "",
|
|
1108
|
-
fresh: true
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
async function refreshSessionDigestCache(root, state, flowStateMtimeMs) {
|
|
1113
|
-
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
1114
|
-
let ralphLoopLine = "";
|
|
1115
|
-
let earlyLoopLine = "";
|
|
1116
|
-
let compoundReadinessLine = "";
|
|
1117
|
-
|
|
1118
|
-
if (state.currentStage === "tdd") {
|
|
1119
|
-
try {
|
|
1120
|
-
const internalRalph = await runCclawInternal(
|
|
1121
|
-
root,
|
|
1122
|
-
["tdd-loop-status", "--json", "--write"],
|
|
1123
|
-
{ captureStdout: true }
|
|
1124
|
-
);
|
|
1125
|
-
if (internalRalph.code !== 0) {
|
|
1126
|
-
throw new Error(summarizeInternalFailure("tdd-loop-status", internalRalph));
|
|
1127
|
-
}
|
|
1128
|
-
const ralphStatus = parseJsonStdoutObject(internalRalph);
|
|
1129
|
-
if (!ralphStatus) {
|
|
1130
|
-
throw new Error("tdd-loop-status returned empty or malformed JSON");
|
|
1131
|
-
}
|
|
1132
|
-
ralphLoopLine = formatRalphLoopStatusLineFromJson(ralphStatus);
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
await recordHookError(
|
|
1135
|
-
root,
|
|
1136
|
-
"session-start:ralph-loop",
|
|
1137
|
-
err instanceof Error ? err.message : String(err)
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
if (
|
|
1142
|
-
EARLY_LOOP_ENABLED &&
|
|
1143
|
-
(state.currentStage === "brainstorm" || state.currentStage === "scope" || state.currentStage === "design")
|
|
1144
|
-
) {
|
|
1145
|
-
try {
|
|
1146
|
-
const internalEarly = await runCclawInternal(
|
|
1147
|
-
root,
|
|
1148
|
-
[
|
|
1149
|
-
"early-loop-status",
|
|
1150
|
-
"--json",
|
|
1151
|
-
"--write",
|
|
1152
|
-
"--stage",
|
|
1153
|
-
state.currentStage,
|
|
1154
|
-
"--run-id",
|
|
1155
|
-
state.activeRunId
|
|
1156
|
-
],
|
|
1157
|
-
{ captureStdout: true }
|
|
1158
|
-
);
|
|
1159
|
-
if (internalEarly.code !== 0) {
|
|
1160
|
-
throw new Error(summarizeInternalFailure("early-loop-status", internalEarly));
|
|
1161
|
-
}
|
|
1162
|
-
const earlyLoopStatus = parseJsonStdoutObject(internalEarly);
|
|
1163
|
-
if (!earlyLoopStatus) {
|
|
1164
|
-
throw new Error("early-loop-status returned empty or malformed JSON");
|
|
1165
|
-
}
|
|
1166
|
-
earlyLoopLine = formatEarlyLoopStatusLineFromJson(earlyLoopStatus);
|
|
1167
|
-
} catch (err) {
|
|
1168
|
-
await recordHookError(
|
|
1169
|
-
root,
|
|
1170
|
-
"session-start:early-loop",
|
|
1171
|
-
err instanceof Error ? err.message : String(err)
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
try {
|
|
1177
|
-
const shouldShowReadiness = state.currentStage === "review" || state.currentStage === "ship";
|
|
1178
|
-
const internalReadiness = await runCclawInternal(
|
|
1179
|
-
root,
|
|
1180
|
-
shouldShowReadiness ? ["compound-readiness"] : ["compound-readiness", "--quiet"],
|
|
1181
|
-
{ captureStdout: true }
|
|
1182
|
-
);
|
|
1183
|
-
if (internalReadiness.code !== 0) {
|
|
1184
|
-
throw new Error(summarizeInternalFailure("compound-readiness", internalReadiness));
|
|
1185
|
-
}
|
|
1186
|
-
if (shouldShowReadiness) {
|
|
1187
|
-
compoundReadinessLine = firstStdoutLine(internalReadiness.stdout);
|
|
1188
|
-
}
|
|
1189
|
-
} catch (err) {
|
|
1190
|
-
await recordHookError(
|
|
1191
|
-
root,
|
|
1192
|
-
"session-start:compound-readiness",
|
|
1193
|
-
err instanceof Error ? err.message : String(err)
|
|
1194
|
-
);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1198
|
-
await writeJsonFile(digestPath, {
|
|
1199
|
-
schemaVersion: SESSION_DIGEST_SCHEMA_VERSION,
|
|
1200
|
-
generatedAt: new Date().toISOString(),
|
|
1201
|
-
flowStateMtimeMs,
|
|
1202
|
-
currentStage: state.currentStage,
|
|
1203
|
-
activeRunId: state.activeRunId,
|
|
1204
|
-
ralphLoopLine,
|
|
1205
|
-
earlyLoopLine,
|
|
1206
|
-
compoundReadinessLine
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
async function scheduleSessionDigestRefresh(runtime, state, flowStateMtimeMs) {
|
|
1211
|
-
if (flowStateMtimeMs <= 0) return;
|
|
1212
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1213
|
-
const digestPath = path.join(stateDir, SESSION_DIGEST_CACHE_FILE);
|
|
1214
|
-
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
1215
|
-
|
|
1216
|
-
const cache = toObject(await readJsonFile(digestPath, {})) || {};
|
|
1217
|
-
const cachedMtimeMs = parseNumericMs(cache.flowStateMtimeMs);
|
|
1218
|
-
if (cachedMtimeMs === flowStateMtimeMs) return;
|
|
1219
|
-
|
|
1220
|
-
const marker = toObject(await readJsonFile(markerPath, {})) || {};
|
|
1221
|
-
const markerMtimeMs = parseNumericMs(marker.flowStateMtimeMs);
|
|
1222
|
-
const markerStartedAtMs = parseNumericMs(marker.startedAtMs);
|
|
1223
|
-
const markerFresh =
|
|
1224
|
-
markerMtimeMs === flowStateMtimeMs &&
|
|
1225
|
-
markerStartedAtMs > 0 &&
|
|
1226
|
-
Date.now() - markerStartedAtMs < SESSION_DIGEST_REFRESH_STALE_MS;
|
|
1227
|
-
if (markerFresh) return;
|
|
1228
|
-
|
|
1229
|
-
await writeJsonFile(markerPath, {
|
|
1230
|
-
flowStateMtimeMs,
|
|
1231
|
-
startedAtMs: Date.now(),
|
|
1232
|
-
currentStage: state.currentStage,
|
|
1233
|
-
activeRunId: state.activeRunId
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
try {
|
|
1237
|
-
const child = spawn(process.execPath, [process.argv[1], "session-start-refresh"], {
|
|
1238
|
-
cwd: runtime.root,
|
|
1239
|
-
stdio: "ignore",
|
|
1240
|
-
windowsHide: true,
|
|
1241
|
-
detached: true,
|
|
1242
|
-
env: {
|
|
1243
|
-
...process.env,
|
|
1244
|
-
CCLAW_PROJECT_ROOT: runtime.root,
|
|
1245
|
-
CCLAW_BG_WORKER: "1"
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
child.unref();
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
1251
|
-
await recordHookError(
|
|
1252
|
-
runtime.root,
|
|
1253
|
-
"session-start:spawn-refresh",
|
|
1254
|
-
err instanceof Error ? err.message : String(err)
|
|
1255
|
-
);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
async function handleSessionStartRefresh(runtime) {
|
|
1260
|
-
const state = await readFlowState(runtime.root);
|
|
1261
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1262
|
-
const markerPath = path.join(stateDir, SESSION_DIGEST_REFRESH_MARKER_FILE);
|
|
1263
|
-
try {
|
|
1264
|
-
const flowStateMtimeMs = await readFileMtimeMs(state.filePath);
|
|
1265
|
-
await refreshSessionDigestCache(runtime.root, state, flowStateMtimeMs);
|
|
1266
|
-
} finally {
|
|
1267
|
-
await fs.rm(markerPath, { force: true }).catch(() => undefined);
|
|
1268
|
-
}
|
|
1269
|
-
return 0;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
487
|
async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
1274
488
|
const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
1275
489
|
// Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
|
|
@@ -1347,6 +561,8 @@ async function handleSessionStart(runtime) {
|
|
|
1347
561
|
? stageInteractionHint.recordedAt
|
|
1348
562
|
: "";
|
|
1349
563
|
const metaContent = (await readTextFile(metaSkillFile, "")).trim();
|
|
564
|
+
const ironLawsSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "iron-laws", "SKILL.md");
|
|
565
|
+
const ironLawsContent = (await readTextFile(ironLawsSkillFile, "")).trim();
|
|
1350
566
|
const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
|
|
1351
567
|
|
|
1352
568
|
const parts = [
|
|
@@ -1398,6 +614,12 @@ async function handleSessionStart(runtime) {
|
|
|
1398
614
|
if (metaContent.length > 0) {
|
|
1399
615
|
parts.push(metaContent);
|
|
1400
616
|
}
|
|
617
|
+
// v6.9.0: load iron-laws content into the session-start digest so the
|
|
618
|
+
// non-negotiable workflow constraints are visible from the first turn,
|
|
619
|
+
// not lazily on tool dispatch.
|
|
620
|
+
if (ironLawsContent.length > 0) {
|
|
621
|
+
parts.push(ironLawsContent);
|
|
622
|
+
}
|
|
1401
623
|
|
|
1402
624
|
const context = parts.join("\\n");
|
|
1403
625
|
if (runtime.harness === "claude" || runtime.harness === "codex") {
|
|
@@ -1581,528 +803,6 @@ async function handleStopHandoff(runtime) {
|
|
|
1581
803
|
return 0;
|
|
1582
804
|
}
|
|
1583
805
|
|
|
1584
|
-
async function handlePromptGuard(runtime) {
|
|
1585
|
-
const mode = resolveStrictness();
|
|
1586
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1587
|
-
const guardLog = path.join(stateDir, "prompt-guard.jsonl");
|
|
1588
|
-
|
|
1589
|
-
const { tool, payloadText } = extractToolAndPayload(runtime.inputData, runtime.inputRaw);
|
|
1590
|
-
const toolLower = toLower(tool);
|
|
1591
|
-
const payloadLower = toLower(payloadText);
|
|
1592
|
-
const reasons = [];
|
|
1593
|
-
|
|
1594
|
-
if (/^(write|edit|multiedit|multi_edit|delete|applypatch|notebookedit|runcommand|shell|terminal|execcommand)$/u.test(toolLower)) {
|
|
1595
|
-
// Artifacts, runs, and knowledge writes are part of normal stage flow.
|
|
1596
|
-
// Guard only managed internals that should be mutated via installer/CLI.
|
|
1597
|
-
if (/\\.cclaw\\/(state|hooks|skills|commands|agents)/u.test(payloadLower)) {
|
|
1598
|
-
reasons.push("write_to_cclaw_runtime");
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
if (/(rm\\s+-rf\\s+\\.cclaw|curl\\s+.*https?:\\/\\/|wget\\s+.*https?:\\/\\/|base64\\s+-d|eval\\(|python\\s+-c)/u.test(payloadLower)) {
|
|
1602
|
-
reasons.push("suspicious_payload_pattern");
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
if (reasons.length > 0) {
|
|
1606
|
-
const note =
|
|
1607
|
-
"Cclaw advisory: potential risky write intent detected for " +
|
|
1608
|
-
RUNTIME_ROOT +
|
|
1609
|
-
" runtime (" +
|
|
1610
|
-
reasons.join(",") +
|
|
1611
|
-
"). Prefer installer commands before mutating managed runtime internals (.cclaw/state|hooks|skills|commands|agents).";
|
|
1612
|
-
await appendJsonLine(guardLog, {
|
|
1613
|
-
ts: new Date().toISOString(),
|
|
1614
|
-
harness: runtime.harness,
|
|
1615
|
-
tool,
|
|
1616
|
-
reasons,
|
|
1617
|
-
note
|
|
1618
|
-
});
|
|
1619
|
-
const advisoryNote = mode === "strict" ? note + " Blocked by strict mode." : note;
|
|
1620
|
-
emitAdvisoryContext(runtime, "prompt-guard", advisoryNote);
|
|
1621
|
-
if (mode === "strict") {
|
|
1622
|
-
process.stderr.write("[cclaw] " + note + " (blocked by strict mode)\\n");
|
|
1623
|
-
return 1;
|
|
1624
|
-
}
|
|
1625
|
-
process.stderr.write("[cclaw] " + note + "\\n");
|
|
1626
|
-
}
|
|
1627
|
-
return 0;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
|
|
1631
|
-
const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
|
|
1632
|
-
for (const line of cycleRaw.split(/\\r?\\n/gu)) {
|
|
1633
|
-
const trimmed = line.trim();
|
|
1634
|
-
if (trimmed.length === 0) continue;
|
|
1635
|
-
try {
|
|
1636
|
-
const row = JSON.parse(trimmed);
|
|
1637
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
1638
|
-
const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
|
|
1639
|
-
if (rowRun !== runId) continue;
|
|
1640
|
-
if (row.phase !== "red") continue;
|
|
1641
|
-
const exitCode =
|
|
1642
|
-
typeof row.exitCode === "number" && Number.isFinite(row.exitCode)
|
|
1643
|
-
? Math.trunc(row.exitCode)
|
|
1644
|
-
: null;
|
|
1645
|
-
if (exitCode === 0) continue;
|
|
1646
|
-
const files = Array.isArray(row.files) ? row.files : [];
|
|
1647
|
-
for (const filePath of files) {
|
|
1648
|
-
if (typeof filePath !== "string") continue;
|
|
1649
|
-
// endsWith-aware match (mirrors tdd-cycle.ts::pathMatchesTarget)
|
|
1650
|
-
// — previously the inline impl used strict === which disagreed
|
|
1651
|
-
// with the CLI/internal path and produced guard blind spots.
|
|
1652
|
-
if (pathMatchesTargetInline(filePath, rawPath)) return true;
|
|
1653
|
-
}
|
|
1654
|
-
} catch {
|
|
1655
|
-
// ignore malformed line
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
const autoRaw = await readTextFile(path.join(stateDir, "tdd-red-evidence.jsonl"), "");
|
|
1660
|
-
for (const line of autoRaw.split(/\\r?\\n/gu)) {
|
|
1661
|
-
const trimmed = line.trim();
|
|
1662
|
-
if (trimmed.length === 0) continue;
|
|
1663
|
-
try {
|
|
1664
|
-
const row = JSON.parse(trimmed);
|
|
1665
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
1666
|
-
const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
|
|
1667
|
-
if (rowRun !== runId) continue;
|
|
1668
|
-
const exitCode =
|
|
1669
|
-
typeof row.exitCode === "number" && Number.isFinite(row.exitCode)
|
|
1670
|
-
? Math.trunc(row.exitCode)
|
|
1671
|
-
: null;
|
|
1672
|
-
if (exitCode === 0) continue;
|
|
1673
|
-
const paths = Array.isArray(row.paths) ? row.paths : [];
|
|
1674
|
-
for (const filePath of paths) {
|
|
1675
|
-
if (typeof filePath !== "string") continue;
|
|
1676
|
-
if (pathMatchesTargetInline(filePath, rawPath)) return true;
|
|
1677
|
-
}
|
|
1678
|
-
} catch {
|
|
1679
|
-
// ignore malformed line
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return false;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function reviewCoverageComplete(reviewArmy) {
|
|
1686
|
-
const root = toObject(reviewArmy) || {};
|
|
1687
|
-
const reconciliation = toObject(root.reconciliation) || {};
|
|
1688
|
-
const coverage = toObject(reconciliation.layerCoverage) || {};
|
|
1689
|
-
for (const key of [
|
|
1690
|
-
"spec",
|
|
1691
|
-
"correctness",
|
|
1692
|
-
"security",
|
|
1693
|
-
"performance",
|
|
1694
|
-
"architecture",
|
|
1695
|
-
"external-safety"
|
|
1696
|
-
]) {
|
|
1697
|
-
if (coverage[key] !== true) return false;
|
|
1698
|
-
}
|
|
1699
|
-
return true;
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
function strictLawSet(ironLaws) {
|
|
1703
|
-
const root = toObject(ironLaws) || {};
|
|
1704
|
-
const set = new Set();
|
|
1705
|
-
if ((root.mode || "advisory") === "strict") {
|
|
1706
|
-
set.add("*");
|
|
1707
|
-
}
|
|
1708
|
-
const laws = Array.isArray(root.laws) ? root.laws : [];
|
|
1709
|
-
for (const row of laws) {
|
|
1710
|
-
if (!row || typeof row !== "object") continue;
|
|
1711
|
-
if (row.strict === true && typeof row.id === "string" && row.id.length > 0) {
|
|
1712
|
-
set.add(row.id);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
return set;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
function lawIsStrict(strictSet, lawId) {
|
|
1719
|
-
return strictSet.has("*") || strictSet.has(lawId);
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
function isTestPayload(payloadTextLower, payloadPaths, testPatterns) {
|
|
1723
|
-
for (const rawPath of payloadPaths) {
|
|
1724
|
-
if (matchesPathPatterns(rawPath, testPatterns)) return true;
|
|
1725
|
-
}
|
|
1726
|
-
return /(\\/tests?\\/|\\/__tests__\\/|\\.test\\.)/u.test(payloadTextLower);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
function isProductionPath(rawPath, testPatterns, productionPatterns) {
|
|
1730
|
-
const normalized = normalizePathForMatch(rawPath);
|
|
1731
|
-
if (normalized.includes("/.cclaw/") || normalized.startsWith(".cclaw/")) return false;
|
|
1732
|
-
if (matchesPathPatterns(normalized, testPatterns)) return false;
|
|
1733
|
-
if (productionPatterns.length > 0) {
|
|
1734
|
-
return matchesPathPatterns(normalized, productionPatterns);
|
|
1735
|
-
}
|
|
1736
|
-
return isCodeLikePath(normalized);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
async function handleWorkflowGuard(runtime) {
|
|
1740
|
-
const mode = resolveStrictness();
|
|
1741
|
-
const maxAgeRaw = process.env.CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC;
|
|
1742
|
-
const maxAgeSec =
|
|
1743
|
-
typeof maxAgeRaw === "string" && /^[0-9]+$/u.test(maxAgeRaw)
|
|
1744
|
-
? Number(maxAgeRaw)
|
|
1745
|
-
: 1800;
|
|
1746
|
-
// TDD enforcement now follows the same single strictness knob — keeping the
|
|
1747
|
-
// distinct local binding so the downstream block rules stay self-documenting.
|
|
1748
|
-
const tddEnforcement = mode;
|
|
1749
|
-
|
|
1750
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1751
|
-
const guardStateFile = path.join(stateDir, "workflow-guard.json");
|
|
1752
|
-
const guardLogFile = path.join(stateDir, "workflow-guard.jsonl");
|
|
1753
|
-
const flowState = await readFlowState(runtime.root);
|
|
1754
|
-
const currentStage = flowState.currentStage;
|
|
1755
|
-
const currentRun = flowState.activeRunId || "active";
|
|
1756
|
-
const reviewArmyFile = path.join(runtime.root, RUNTIME_ROOT, "artifacts", "07-review-army.json");
|
|
1757
|
-
const ironLaws = await readJsonFile(path.join(stateDir, "iron-laws.json"), {});
|
|
1758
|
-
const strictLaws = strictLawSet(ironLaws);
|
|
1759
|
-
|
|
1760
|
-
const { tool, payload, payloadText } = extractToolAndPayload(runtime.inputData, runtime.inputRaw);
|
|
1761
|
-
const toolLower = toLower(tool);
|
|
1762
|
-
const payloadLower = toLower(payloadText);
|
|
1763
|
-
const payloadPaths = [...collectPaths(runtime.inputData)].filter((value) => typeof value === "string");
|
|
1764
|
-
const reasons = [];
|
|
1765
|
-
let missingRedPaths = [];
|
|
1766
|
-
|
|
1767
|
-
const targetStage = detectTargetStage(payloadLower);
|
|
1768
|
-
const flowCommandInvoked = isFlowProgressionCommand(payloadLower);
|
|
1769
|
-
|
|
1770
|
-
if (targetStage.length > 0 && currentStage !== "none") {
|
|
1771
|
-
const currentIndex = stageIndex(currentStage);
|
|
1772
|
-
const targetIndex = stageIndex(targetStage);
|
|
1773
|
-
if (currentIndex > 0 && targetIndex > 0 && targetIndex > currentIndex + 1) {
|
|
1774
|
-
reasons.push("stage_jump_" + currentStage + "_to_" + targetStage);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
if (isMutatingTool(toolLower) && /\\.cclaw\\/state\\/flow-state\\.json/u.test(payloadLower)) {
|
|
1779
|
-
reasons.push("direct_flow_state_edit");
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
if (isPreimplementationStage(currentStage) && isMutatingTool(toolLower)) {
|
|
1783
|
-
if (!/\\.cclaw\\//u.test(payloadLower)) {
|
|
1784
|
-
reasons.push("implementation_write_before_" + currentStage + "_completion");
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
1789
|
-
const guardState = toObject(await readJsonFile(guardStateFile, {})) || {};
|
|
1790
|
-
const lastFlowReadAtEpoch =
|
|
1791
|
-
typeof guardState.lastFlowReadAtEpoch === "number" && Number.isFinite(guardState.lastFlowReadAtEpoch)
|
|
1792
|
-
? Math.trunc(guardState.lastFlowReadAtEpoch)
|
|
1793
|
-
: 0;
|
|
1794
|
-
const staleFlowRead =
|
|
1795
|
-
lastFlowReadAtEpoch <= 0 || nowEpoch - lastFlowReadAtEpoch > maxAgeSec;
|
|
1796
|
-
|
|
1797
|
-
if (isMutatingTool(toolLower) && staleFlowRead) {
|
|
1798
|
-
reasons.push("mutating_without_recent_flow_read");
|
|
1799
|
-
}
|
|
1800
|
-
if ((targetStage.length > 0 || flowCommandInvoked) && staleFlowRead) {
|
|
1801
|
-
reasons.push("stage_invocation_without_recent_flow_read");
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
const shouldRecordFlowRead =
|
|
1805
|
-
/^(read|readfile|open|view|cat|shell|runcommand|run_command|execcommand|exec_command|terminal)$/u.test(
|
|
1806
|
-
toolLower
|
|
1807
|
-
) &&
|
|
1808
|
-
/(\\.cclaw\\/state\\/flow-state\\.json|npx cclaw-cli sync|npx cclaw-cli sync|npx cclaw-cli sync|cclaw sync)/u.test(payloadLower);
|
|
1809
|
-
if (shouldRecordFlowRead) {
|
|
1810
|
-
await writeJsonFile(guardStateFile, {
|
|
1811
|
-
...guardState,
|
|
1812
|
-
lastFlowReadAt: new Date().toISOString(),
|
|
1813
|
-
lastFlowReadAtEpoch: nowEpoch
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
const testPatterns = DEFAULT_TDD_TEST_PATH_PATTERNS;
|
|
1818
|
-
const productionPatterns = DEFAULT_TDD_PRODUCTION_PATH_PATTERNS;
|
|
1819
|
-
|
|
1820
|
-
if (currentStage === "tdd" && isMutatingTool(toolLower)) {
|
|
1821
|
-
const productionPaths = payloadPaths.filter((rawPath) =>
|
|
1822
|
-
isProductionPath(rawPath, testPatterns, productionPatterns)
|
|
1823
|
-
);
|
|
1824
|
-
if (productionPaths.length > 0) {
|
|
1825
|
-
for (const productionPath of productionPaths) {
|
|
1826
|
-
const hasRed = await hasFailingRedEvidenceForPath(stateDir, currentRun, productionPath);
|
|
1827
|
-
if (!hasRed) {
|
|
1828
|
-
missingRedPaths.push(productionPath);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
if (missingRedPaths.length > 0) {
|
|
1832
|
-
reasons.push("tdd_write_without_red_for_path");
|
|
1833
|
-
}
|
|
1834
|
-
} else if (productionPatterns.length === 0 && !isTestPayload(payloadLower, payloadPaths, testPatterns)) {
|
|
1835
|
-
// Slice-aware fallback: the previous implementation used a flat
|
|
1836
|
-
// red/green count which said "ok" as long as the totals balanced
|
|
1837
|
-
// across ALL slices, so a closed S-1 could unlock production
|
|
1838
|
-
// writes that actually belonged to a new, not-yet-red S-2. Now
|
|
1839
|
-
// we reuse the canonical Ralph Loop status: if NO slice has an
|
|
1840
|
-
// open RED, we block.
|
|
1841
|
-
const internalRalph = await runCclawInternal(
|
|
1842
|
-
runtime.root,
|
|
1843
|
-
["tdd-loop-status", "--json", "--no-write"],
|
|
1844
|
-
{ captureStdout: true }
|
|
1845
|
-
);
|
|
1846
|
-
const ralphStatus = parseJsonStdoutObject(internalRalph);
|
|
1847
|
-
const redOpen = internalRalph.code === 0 && ralphStatus?.redOpen === true;
|
|
1848
|
-
if (!redOpen) {
|
|
1849
|
-
reasons.push("tdd_write_without_open_red");
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
if (isPreimplementationStage(currentStage) && !isPlanModeSafeTool(toolLower)) {
|
|
1855
|
-
if (!isMutatingTool(toolLower) && !/\\.cclaw\\//u.test(payloadLower) && !isCclawCliPayload(payloadLower)) {
|
|
1856
|
-
reasons.push("non_safe_tool_in_plan_stage_" + currentStage);
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
if (currentStage === "ship" && isExecutionOrMutatingTool(toolLower)) {
|
|
1861
|
-
if (/(npm publish|pnpm publish|yarn publish|gh release create|git push\\s+.*--tags|npm version)/u.test(payloadLower)) {
|
|
1862
|
-
const shipGate = toObject((toObject(flowState.raw.stageGateCatalog) || {}).ship) || {};
|
|
1863
|
-
const passed = Array.isArray(shipGate.passed) ? shipGate.passed : [];
|
|
1864
|
-
if (!passed.includes("ship_preflight_passed")) {
|
|
1865
|
-
reasons.push("ship_preflight_required");
|
|
1866
|
-
}
|
|
1867
|
-
const reviewArmy = await readJsonFile(reviewArmyFile, {});
|
|
1868
|
-
if (!reviewCoverageComplete(reviewArmy)) {
|
|
1869
|
-
reasons.push("ship_review_coverage_required");
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
if (isMutatingTool(toolLower) && /\\.cclaw\\/(state|hooks|skills)/u.test(payloadLower)) {
|
|
1875
|
-
if (!isCclawCliPayload(payloadLower)) {
|
|
1876
|
-
reasons.push("runtime_write_requires_managed_only");
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
if (reasons.length > 0) {
|
|
1881
|
-
let note =
|
|
1882
|
-
"Cclaw workflow guard: detected potential flow violation (" +
|
|
1883
|
-
reasons.join(",") +
|
|
1884
|
-
"). Re-read " +
|
|
1885
|
-
RUNTIME_ROOT +
|
|
1886
|
-
"/state/flow-state.json and align with stage constraints.";
|
|
1887
|
-
if (reasons.includes("tdd_write_without_red_for_path")) {
|
|
1888
|
-
note =
|
|
1889
|
-
"Cclaw workflow guard: missing failing RED evidence for production path(s): " +
|
|
1890
|
-
(missingRedPaths.length > 0 ? missingRedPaths.join(", ") : "unknown") +
|
|
1891
|
-
". Log failing tests before touching these files.";
|
|
1892
|
-
} else if (reasons.includes("tdd_write_without_open_red")) {
|
|
1893
|
-
note =
|
|
1894
|
-
"Cclaw workflow guard: Write a failing test first before editing production files during tdd stage.";
|
|
1895
|
-
} else if (reasons.includes("ship_preflight_required")) {
|
|
1896
|
-
note =
|
|
1897
|
-
"Cclaw workflow guard: ship finalization command detected before ship_preflight_passed gate.";
|
|
1898
|
-
} else if (reasons.includes("ship_review_coverage_required")) {
|
|
1899
|
-
note =
|
|
1900
|
-
"Cclaw workflow guard: ship finalization requires complete review layer coverage in 07-review-army.json.";
|
|
1901
|
-
} else if (reasons.includes("mutating_without_recent_flow_read")) {
|
|
1902
|
-
note =
|
|
1903
|
-
"Cclaw workflow guard: mutating action requires a fresh read of " +
|
|
1904
|
-
RUNTIME_ROOT +
|
|
1905
|
-
"/state/flow-state.json before edits.";
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
await appendJsonLine(guardLogFile, {
|
|
1909
|
-
ts: new Date().toISOString(),
|
|
1910
|
-
tool,
|
|
1911
|
-
currentStage,
|
|
1912
|
-
targetStage,
|
|
1913
|
-
reasons,
|
|
1914
|
-
note
|
|
1915
|
-
});
|
|
1916
|
-
|
|
1917
|
-
let shouldBlock = false;
|
|
1918
|
-
if (mode === "strict") shouldBlock = true;
|
|
1919
|
-
if (
|
|
1920
|
-
(reasons.includes("tdd_write_without_open_red") || reasons.includes("tdd_write_without_red_for_path")) &&
|
|
1921
|
-
tddEnforcement === "strict"
|
|
1922
|
-
) {
|
|
1923
|
-
shouldBlock = true;
|
|
1924
|
-
}
|
|
1925
|
-
if (
|
|
1926
|
-
(reasons.includes("tdd_write_without_open_red") || reasons.includes("tdd_write_without_red_for_path")) &&
|
|
1927
|
-
lawIsStrict(strictLaws, "tdd-red-before-write")
|
|
1928
|
-
) {
|
|
1929
|
-
shouldBlock = true;
|
|
1930
|
-
}
|
|
1931
|
-
if (reasons.includes("ship_preflight_required") && lawIsStrict(strictLaws, "ship-preflight-required")) {
|
|
1932
|
-
shouldBlock = true;
|
|
1933
|
-
}
|
|
1934
|
-
if (
|
|
1935
|
-
reasons.includes("ship_review_coverage_required") &&
|
|
1936
|
-
lawIsStrict(strictLaws, "review-coverage-complete-before-ship")
|
|
1937
|
-
) {
|
|
1938
|
-
shouldBlock = true;
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
if (shouldBlock) {
|
|
1942
|
-
emitAdvisoryContext(runtime, "workflow-guard", note + " Blocked by workflow guard.");
|
|
1943
|
-
process.stderr.write("[cclaw] " + note + " (blocked by workflow guard)\\n");
|
|
1944
|
-
return 1;
|
|
1945
|
-
}
|
|
1946
|
-
emitAdvisoryContext(runtime, "workflow-guard", note);
|
|
1947
|
-
process.stderr.write("[cclaw] " + note + "\\n");
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
return 0;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
async function handleContextMonitor(runtime) {
|
|
1954
|
-
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1955
|
-
const monitorStateFile = path.join(stateDir, "context-monitor.json");
|
|
1956
|
-
const autoEvidenceFile = path.join(stateDir, "tdd-red-evidence.jsonl");
|
|
1957
|
-
const flowState = await readFlowState(runtime.root);
|
|
1958
|
-
|
|
1959
|
-
const command = extractCommandFromPayload(runtime.inputData);
|
|
1960
|
-
const exitCode = extractExitCodeFromPayload(runtime.inputData);
|
|
1961
|
-
const commandLower = toLower(command);
|
|
1962
|
-
if (
|
|
1963
|
-
flowState.currentStage === "tdd" &&
|
|
1964
|
-
command.length > 0 &&
|
|
1965
|
-
exitCode !== null &&
|
|
1966
|
-
exitCode !== 0 &&
|
|
1967
|
-
/(npm test|npm run test|pnpm test|pnpm run test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)/u.test(
|
|
1968
|
-
commandLower
|
|
1969
|
-
)
|
|
1970
|
-
) {
|
|
1971
|
-
const textBlob = extractTextBlobs(runtime.inputData) + "\\n" + command;
|
|
1972
|
-
const paths = extractCodePathsFromText(textBlob);
|
|
1973
|
-
await appendJsonLine(autoEvidenceFile, {
|
|
1974
|
-
ts: new Date().toISOString(),
|
|
1975
|
-
runId: flowState.activeRunId || "active",
|
|
1976
|
-
stage: "tdd",
|
|
1977
|
-
source: "posttool-auto",
|
|
1978
|
-
command,
|
|
1979
|
-
tool: normalizeToolName(
|
|
1980
|
-
(toObject(runtime.inputData) || {}).tool_name ??
|
|
1981
|
-
(toObject(runtime.inputData) || {}).tool ??
|
|
1982
|
-
(toObject(toObject(runtime.inputData)?.input) || {}).tool ??
|
|
1983
|
-
""
|
|
1984
|
-
),
|
|
1985
|
-
exitCode,
|
|
1986
|
-
paths
|
|
1987
|
-
});
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
const remainingPercent = extractRemainingPercent(runtime.inputData);
|
|
1991
|
-
if (remainingPercent === null) return 0;
|
|
1992
|
-
|
|
1993
|
-
let band = "none";
|
|
1994
|
-
if (remainingPercent <= 20) {
|
|
1995
|
-
band = "critical";
|
|
1996
|
-
} else if (remainingPercent <= 35) {
|
|
1997
|
-
band = "warning";
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
const ttlRaw = process.env.CCLAW_CONTEXT_MONITOR_TTL_SEC;
|
|
2001
|
-
const ttlSeconds =
|
|
2002
|
-
typeof ttlRaw === "string" && /^[0-9]+$/u.test(ttlRaw) ? Number(ttlRaw) : 900;
|
|
2003
|
-
const now = new Date();
|
|
2004
|
-
const nowEpoch = Math.floor(now.getTime() / 1000);
|
|
2005
|
-
const monitorState = toObject(await readJsonFile(monitorStateFile, {})) || {};
|
|
2006
|
-
const lastBand = typeof monitorState.lastBand === "string" ? monitorState.lastBand : "none";
|
|
2007
|
-
const lastAdvisoryBand =
|
|
2008
|
-
typeof monitorState.lastAdvisoryBand === "string"
|
|
2009
|
-
? monitorState.lastAdvisoryBand
|
|
2010
|
-
: lastBand;
|
|
2011
|
-
const lastAdvisoryAt =
|
|
2012
|
-
typeof monitorState.lastAdvisoryAt === "string" ? monitorState.lastAdvisoryAt : "";
|
|
2013
|
-
const lastAdvisoryEpoch = lastAdvisoryAt.length > 0
|
|
2014
|
-
? Math.floor(Date.parse(lastAdvisoryAt) / 1000) || 0
|
|
2015
|
-
: 0;
|
|
2016
|
-
|
|
2017
|
-
let shouldEmit = false;
|
|
2018
|
-
if (band !== "none") {
|
|
2019
|
-
if (band !== lastAdvisoryBand) {
|
|
2020
|
-
shouldEmit = true;
|
|
2021
|
-
} else if (ttlSeconds === 0) {
|
|
2022
|
-
shouldEmit = true;
|
|
2023
|
-
} else if (nowEpoch - lastAdvisoryEpoch >= ttlSeconds) {
|
|
2024
|
-
shouldEmit = true;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
let nextAdvisoryBand = lastAdvisoryBand;
|
|
2029
|
-
let nextAdvisoryAt = lastAdvisoryAt;
|
|
2030
|
-
if (shouldEmit) {
|
|
2031
|
-
const note =
|
|
2032
|
-
"Cclaw advisory: context remaining is " +
|
|
2033
|
-
String(remainingPercent.toFixed(2)) +
|
|
2034
|
-
"% (" +
|
|
2035
|
-
band +
|
|
2036
|
-
"). Consider leaving a handoff note or compacting soon.";
|
|
2037
|
-
emitAdvisoryContext(runtime, "context-monitor", note);
|
|
2038
|
-
process.stderr.write("[cclaw] " + note + "\\n");
|
|
2039
|
-
nextAdvisoryBand = band;
|
|
2040
|
-
nextAdvisoryAt = now.toISOString();
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
await writeJsonFile(monitorStateFile, {
|
|
2044
|
-
lastUpdated: now.toISOString(),
|
|
2045
|
-
lastBand: band,
|
|
2046
|
-
lastRemainingPercent: remainingPercent,
|
|
2047
|
-
harness: runtime.harness,
|
|
2048
|
-
lastAdvisoryBand: nextAdvisoryBand,
|
|
2049
|
-
lastAdvisoryAt: nextAdvisoryAt
|
|
2050
|
-
});
|
|
2051
|
-
return 0;
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
async function handleVerifyCurrentState(runtime) {
|
|
2055
|
-
const mode = resolveStrictness();
|
|
2056
|
-
const result = await runCclawInternal(runtime.root, ["verify-current-state", "--quiet"]);
|
|
2057
|
-
if (result.missingBinary) {
|
|
2058
|
-
const message = result.stderr.trim().length > 0
|
|
2059
|
-
? result.stderr.trim()
|
|
2060
|
-
: "Cclaw verify-current-state requires a local Node runtime entrypoint.";
|
|
2061
|
-
emitAdvisoryContext(runtime, "verify-current-state", message);
|
|
2062
|
-
process.stderr.write(result.stderr.trim().length > 0
|
|
2063
|
-
? result.stderr
|
|
2064
|
-
: "[cclaw] hook: local Node runtime entrypoint is required for verify-current-state\\n");
|
|
2065
|
-
return mode === "strict" ? 1 : 0;
|
|
2066
|
-
}
|
|
2067
|
-
if (mode === "strict") {
|
|
2068
|
-
if (result.code !== 0) {
|
|
2069
|
-
emitAdvisoryContext(
|
|
2070
|
-
runtime,
|
|
2071
|
-
"verify-current-state",
|
|
2072
|
-
result.stderr.trim().length > 0
|
|
2073
|
-
? result.stderr.trim()
|
|
2074
|
-
: "Cclaw verify-current-state failed in strict mode."
|
|
2075
|
-
);
|
|
2076
|
-
}
|
|
2077
|
-
if (result.code !== 0 && result.stderr.trim().length > 0) {
|
|
2078
|
-
process.stderr.write(result.stderr);
|
|
2079
|
-
}
|
|
2080
|
-
return result.code === 0 ? 0 : 1;
|
|
2081
|
-
}
|
|
2082
|
-
return 0;
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
async function handlePreToolPipeline(runtime) {
|
|
2086
|
-
const promptExitCode = await handlePromptGuard(runtime);
|
|
2087
|
-
if (promptExitCode !== 0) {
|
|
2088
|
-
return promptExitCode;
|
|
2089
|
-
}
|
|
2090
|
-
return await handleWorkflowGuard(runtime);
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
async function handlePromptPipeline(runtime) {
|
|
2094
|
-
const promptExitCode = await handlePromptGuard(runtime);
|
|
2095
|
-
if (promptExitCode !== 0) {
|
|
2096
|
-
return promptExitCode;
|
|
2097
|
-
}
|
|
2098
|
-
const verifyExitCode = await handleVerifyCurrentState(runtime);
|
|
2099
|
-
if (verifyExitCode !== 0) {
|
|
2100
|
-
return verifyExitCode;
|
|
2101
|
-
}
|
|
2102
|
-
runtime.writeJson({ ok: true });
|
|
2103
|
-
return 0;
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
806
|
function normalizeHookName(rawName) {
|
|
2107
807
|
const value = normalizeText(rawName).toLowerCase();
|
|
2108
808
|
if (value === "session-start") return "session-start";
|
|
@@ -2146,6 +846,13 @@ async function main() {
|
|
|
2146
846
|
};
|
|
2147
847
|
|
|
2148
848
|
try {
|
|
849
|
+
const policy = await resolveHookPolicy(runtime.root);
|
|
850
|
+
if (isHookDisabled(policy, hookName)) {
|
|
851
|
+
// Honor CCLAW_HOOK_PROFILE / CCLAW_DISABLED_HOOKS / config disabledHooks.
|
|
852
|
+
// Disabled hooks must exit 0 quietly so harnesses keep running.
|
|
853
|
+
process.exitCode = 0;
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
2149
856
|
if (hookName === "session-start" || hookName === "stop-handoff") {
|
|
2150
857
|
const guardOk = await verifyFlowStateGuardInline(runtime.root, hookName);
|
|
2151
858
|
if (!guardOk) {
|