cclaw-cli 0.48.6 → 0.48.8
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/content/hooks.d.ts +3 -2
- package/dist/content/hooks.js +183 -47
- package/dist/content/iron-laws.d.ts +8 -0
- package/dist/content/iron-laws.js +9 -0
- package/dist/content/node-hooks.d.ts +14 -0
- package/dist/content/node-hooks.js +1527 -0
- package/dist/content/observe.js +381 -56
- package/dist/content/stages/review.js +2 -2
- package/dist/content/templates.js +8 -0
- package/dist/doctor.js +6 -2
- package/dist/install.js +12 -1
- package/dist/internal/advance-stage.js +57 -2
- package/dist/internal/tdd-red-evidence.d.ts +7 -0
- package/dist/internal/tdd-red-evidence.js +130 -0
- package/package.json +1 -1
package/dist/install.js
CHANGED
|
@@ -24,8 +24,9 @@ import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rew
|
|
|
24
24
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
25
25
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
26
26
|
import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
|
|
27
|
-
import { sessionStartScript, stopCheckpointScript, runHookDispatcherScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
27
|
+
import { hookLibScript, sessionStartScript, stopCheckpointScript, runHookDispatcherScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
28
28
|
import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
|
|
29
|
+
import { nodeHookRuntimeScript } from "./content/node-hooks.js";
|
|
29
30
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
30
31
|
import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
|
|
31
32
|
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
@@ -630,6 +631,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
630
631
|
mode: config.ironLaws?.mode,
|
|
631
632
|
strictLaws: config.ironLaws?.strictLaws
|
|
632
633
|
}), null, 2)}\n`);
|
|
634
|
+
await writeFileSafe(path.join(hooksDir, "_lib.sh"), hookLibScript());
|
|
633
635
|
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
|
|
634
636
|
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
635
637
|
await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookDispatcherScript());
|
|
@@ -645,10 +647,18 @@ async function writeHooks(projectRoot, config) {
|
|
|
645
647
|
tddProductionPathPatterns: config.tdd?.productionPathPatterns
|
|
646
648
|
}));
|
|
647
649
|
await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
|
|
650
|
+
await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
|
|
651
|
+
promptGuardMode: config.promptGuardMode ?? config.strictness ?? "advisory",
|
|
652
|
+
workflowGuardMode: config.strictness ?? "advisory",
|
|
653
|
+
tddEnforcementMode: config.tddEnforcement ?? config.strictness ?? "advisory",
|
|
654
|
+
tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
|
|
655
|
+
tddProductionPathPatterns: config.tdd?.productionPathPatterns
|
|
656
|
+
}));
|
|
648
657
|
const opencodePluginSource = opencodePluginJs();
|
|
649
658
|
await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
|
|
650
659
|
try {
|
|
651
660
|
for (const script of [
|
|
661
|
+
"_lib.sh",
|
|
652
662
|
"session-start.sh",
|
|
653
663
|
"stop-checkpoint.sh",
|
|
654
664
|
"run-hook.cmd",
|
|
@@ -657,6 +667,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
657
667
|
"prompt-guard.sh",
|
|
658
668
|
"workflow-guard.sh",
|
|
659
669
|
"context-monitor.sh",
|
|
670
|
+
"run-hook.mjs",
|
|
660
671
|
"opencode-plugin.mjs"
|
|
661
672
|
]) {
|
|
662
673
|
await fs.chmod(path.join(hooksDir, script), 0o755);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import process from "node:process";
|
|
3
5
|
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
4
6
|
import { stageSchema } from "../content/stage-schema.js";
|
|
5
7
|
import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
|
|
@@ -12,6 +14,7 @@ import { readFlowState, writeFlowState } from "../runs.js";
|
|
|
12
14
|
import { FLOW_STAGES } from "../types.js";
|
|
13
15
|
import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
14
16
|
import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
|
|
17
|
+
import { runTddRedEvidenceCommand } from "./tdd-red-evidence.js";
|
|
15
18
|
function unique(values) {
|
|
16
19
|
return [...new Set(values)];
|
|
17
20
|
}
|
|
@@ -305,6 +308,17 @@ function parseVerifyCurrentStateArgs(tokens) {
|
|
|
305
308
|
}
|
|
306
309
|
return { quiet };
|
|
307
310
|
}
|
|
311
|
+
function parseHookArgs(tokens) {
|
|
312
|
+
const [hookName, ...rest] = tokens;
|
|
313
|
+
const normalizedHook = typeof hookName === "string" ? hookName.trim() : "";
|
|
314
|
+
if (normalizedHook.length === 0) {
|
|
315
|
+
throw new Error("internal hook requires a hook name: cclaw internal hook <name>.");
|
|
316
|
+
}
|
|
317
|
+
if (rest.length > 0) {
|
|
318
|
+
throw new Error(`Unknown arguments for internal hook: ${rest.join(" ")}`);
|
|
319
|
+
}
|
|
320
|
+
return { hookName: normalizedHook };
|
|
321
|
+
}
|
|
308
322
|
async function buildValidationReport(projectRoot, flowState) {
|
|
309
323
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
|
|
310
324
|
const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState);
|
|
@@ -620,10 +634,45 @@ async function runVerifyCurrentState(projectRoot, args, io) {
|
|
|
620
634
|
}
|
|
621
635
|
return validation.ok ? 0 : 1;
|
|
622
636
|
}
|
|
637
|
+
async function runHookCommand(projectRoot, args, io) {
|
|
638
|
+
const runHookPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
639
|
+
try {
|
|
640
|
+
await fs.access(runHookPath);
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
io.stderr.write(`cclaw internal hook: missing hook runtime at ${runHookPath}. Run \`cclaw sync\` first.\n`);
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
return await new Promise((resolve) => {
|
|
647
|
+
const child = spawn(process.execPath, [runHookPath, args.hookName], {
|
|
648
|
+
cwd: projectRoot,
|
|
649
|
+
env: process.env,
|
|
650
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
651
|
+
});
|
|
652
|
+
child.stdout.on("data", (chunk) => {
|
|
653
|
+
io.stdout.write(chunk);
|
|
654
|
+
});
|
|
655
|
+
child.stderr.on("data", (chunk) => {
|
|
656
|
+
io.stderr.write(chunk);
|
|
657
|
+
});
|
|
658
|
+
child.on("error", (err) => {
|
|
659
|
+
io.stderr.write(`cclaw internal hook: failed to launch runtime (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
660
|
+
resolve(1);
|
|
661
|
+
});
|
|
662
|
+
child.on("close", (code, signal) => {
|
|
663
|
+
if (signal) {
|
|
664
|
+
io.stderr.write(`cclaw internal hook: runtime terminated by signal ${signal}.\n`);
|
|
665
|
+
resolve(1);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
resolve(typeof code === "number" ? code : 1);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
}
|
|
623
672
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
624
673
|
const [subcommand, ...tokens] = argv;
|
|
625
674
|
if (!subcommand) {
|
|
626
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate\n");
|
|
675
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | hook\n");
|
|
627
676
|
return 1;
|
|
628
677
|
}
|
|
629
678
|
try {
|
|
@@ -642,7 +691,13 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
642
691
|
if (subcommand === "envelope-validate") {
|
|
643
692
|
return await runEnvelopeValidateCommand(projectRoot, tokens, io);
|
|
644
693
|
}
|
|
645
|
-
|
|
694
|
+
if (subcommand === "tdd-red-evidence") {
|
|
695
|
+
return await runTddRedEvidenceCommand(projectRoot, tokens, io);
|
|
696
|
+
}
|
|
697
|
+
if (subcommand === "hook") {
|
|
698
|
+
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
699
|
+
}
|
|
700
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | hook\n`);
|
|
646
701
|
return 1;
|
|
647
702
|
}
|
|
648
703
|
catch (err) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RUNTIME_ROOT } from "../constants.js";
|
|
4
|
+
import { readFlowState } from "../runs.js";
|
|
5
|
+
import { hasFailingTestForPath, parseTddCycleLog } from "../tdd-cycle.js";
|
|
6
|
+
function normalizePath(value) {
|
|
7
|
+
return value.replace(/\\/gu, "/").toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function parseArgs(tokens) {
|
|
10
|
+
const args = { quiet: false };
|
|
11
|
+
for (const token of tokens) {
|
|
12
|
+
if (token === "--quiet") {
|
|
13
|
+
args.quiet = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (token.startsWith("--path=")) {
|
|
17
|
+
const value = token.slice("--path=".length).trim();
|
|
18
|
+
if (!value) {
|
|
19
|
+
throw new Error("--path must not be empty.");
|
|
20
|
+
}
|
|
21
|
+
args.targetPath = value;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (token.startsWith("--run-id=")) {
|
|
25
|
+
const value = token.slice("--run-id=".length).trim();
|
|
26
|
+
if (value) {
|
|
27
|
+
args.runId = value;
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Unknown flag for tdd-red-evidence: ${token}`);
|
|
32
|
+
}
|
|
33
|
+
if (!args.targetPath) {
|
|
34
|
+
throw new Error("Missing required flag: --path=<production-file-path>");
|
|
35
|
+
}
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
function parseAutoEvidence(text) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const rawLine of text.split(/\r?\n/gu)) {
|
|
41
|
+
const line = rawLine.trim();
|
|
42
|
+
if (!line)
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(line);
|
|
46
|
+
const exitCode = parsed.exitCode;
|
|
47
|
+
if (typeof exitCode !== "number" || exitCode === 0)
|
|
48
|
+
continue;
|
|
49
|
+
const runId = typeof parsed.runId === "string" && parsed.runId.length > 0
|
|
50
|
+
? parsed.runId
|
|
51
|
+
: "active";
|
|
52
|
+
const rawPaths = Array.isArray(parsed.paths)
|
|
53
|
+
? parsed.paths
|
|
54
|
+
: typeof parsed.path === "string"
|
|
55
|
+
? [parsed.path]
|
|
56
|
+
: [];
|
|
57
|
+
const paths = rawPaths
|
|
58
|
+
.filter((value) => typeof value === "string")
|
|
59
|
+
.map((value) => value.trim())
|
|
60
|
+
.filter((value) => value.length > 0);
|
|
61
|
+
if (paths.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
out.push({
|
|
64
|
+
runId,
|
|
65
|
+
exitCode,
|
|
66
|
+
paths
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore malformed lines
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
|
|
76
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (options.runId && entry.runId !== options.runId)
|
|
79
|
+
continue;
|
|
80
|
+
for (const filePath of entry.paths) {
|
|
81
|
+
const normalized = normalizePath(filePath);
|
|
82
|
+
if (normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
|
|
90
|
+
const args = parseArgs(tokens);
|
|
91
|
+
const flowState = await readFlowState(projectRoot).catch(() => null);
|
|
92
|
+
const effectiveRunId = args.runId ?? flowState?.activeRunId;
|
|
93
|
+
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
94
|
+
const autoEvidencePath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-red-evidence.jsonl");
|
|
95
|
+
let cycleLogHasRed = false;
|
|
96
|
+
let autoEvidenceHasRed = false;
|
|
97
|
+
try {
|
|
98
|
+
const raw = await fs.readFile(tddLogPath, "utf8");
|
|
99
|
+
const entries = parseTddCycleLog(raw);
|
|
100
|
+
cycleLogHasRed = hasFailingTestForPath(entries, args.targetPath, {
|
|
101
|
+
runId: effectiveRunId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
cycleLogHasRed = false;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const raw = await fs.readFile(autoEvidencePath, "utf8");
|
|
109
|
+
const entries = parseAutoEvidence(raw);
|
|
110
|
+
autoEvidenceHasRed = hasFailingAutoEvidenceForPath(entries, args.targetPath, {
|
|
111
|
+
runId: effectiveRunId
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
autoEvidenceHasRed = false;
|
|
116
|
+
}
|
|
117
|
+
const hasRed = cycleLogHasRed || autoEvidenceHasRed;
|
|
118
|
+
if (!args.quiet) {
|
|
119
|
+
io.stdout.write(`${JSON.stringify({
|
|
120
|
+
ok: hasRed,
|
|
121
|
+
path: args.targetPath,
|
|
122
|
+
runId: effectiveRunId ?? null,
|
|
123
|
+
sources: {
|
|
124
|
+
tddCycleLog: cycleLogHasRed,
|
|
125
|
+
autoEvidence: autoEvidenceHasRed
|
|
126
|
+
}
|
|
127
|
+
}, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
return hasRed ? 0 : 2;
|
|
130
|
+
}
|