cclaw-cli 0.48.10 → 0.48.12
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/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +22 -44
- package/dist/content/doctor-references.js +11 -7
- package/dist/content/node-hooks.d.ts +7 -3
- package/dist/content/node-hooks.js +26 -22
- package/dist/content/observe.js +4 -0
- package/dist/content/opencode-plugin.js +6 -3
- package/dist/doctor-registry.js +0 -9
- package/dist/doctor.js +12 -31
- package/dist/install.js +13 -5
- package/dist/types.d.ts +13 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ If cclaw detects a Node / Python / Go project at init time, a sixth
|
|
|
158
158
|
`pyproject.toml` / `requirements.txt`, `go.mod`). That is the full
|
|
159
159
|
default surface — a new user sees nothing they need to understand yet.
|
|
160
160
|
|
|
161
|
-
Advanced knobs (`
|
|
161
|
+
Advanced knobs (`ironLaws.strictLaws` per-law escapes,
|
|
162
162
|
`tdd.testPathPatterns` / `tdd.productionPathPatterns`,
|
|
163
163
|
`compound.recurrenceThreshold`, `defaultTrack`, `trackHeuristics`,
|
|
164
164
|
`sliceReview`) are **opt-in**: add them by hand when you need them.
|
package/dist/cli.js
CHANGED
|
@@ -828,7 +828,7 @@ async function runCommand(parsed, ctx) {
|
|
|
828
828
|
ctx.stdout.write(`${JSON.stringify({
|
|
829
829
|
track: previewConfig.defaultTrack ?? "standard",
|
|
830
830
|
harnesses: previewConfig.harnesses,
|
|
831
|
-
|
|
831
|
+
strictness: previewConfig.strictness ?? "advisory",
|
|
832
832
|
gitHookGuards: previewConfig.gitHookGuards,
|
|
833
833
|
languageRulePacks: previewConfig.languageRulePacks,
|
|
834
834
|
generatedSurfaces: previewSurfaces
|
package/dist/config.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
|
|
|
42
42
|
* the user set them explicitly. Keeps the default template small and honest:
|
|
43
43
|
* only knobs a new user would meaningfully flip show up.
|
|
44
44
|
*/
|
|
45
|
-
type AdvancedConfigKey = "
|
|
45
|
+
type AdvancedConfigKey = "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws";
|
|
46
46
|
/**
|
|
47
47
|
* Options controlling the serialisation shape of `config.yaml`.
|
|
48
48
|
*
|
package/dist/config.js
CHANGED
|
@@ -17,8 +17,6 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
17
17
|
"flowVersion",
|
|
18
18
|
"harnesses",
|
|
19
19
|
"strictness",
|
|
20
|
-
"promptGuardMode",
|
|
21
|
-
"tddEnforcement",
|
|
22
20
|
"tddTestGlobs",
|
|
23
21
|
"tdd",
|
|
24
22
|
"compound",
|
|
@@ -29,6 +27,16 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
29
27
|
"sliceReview",
|
|
30
28
|
"ironLaws"
|
|
31
29
|
]);
|
|
30
|
+
/**
|
|
31
|
+
* Config keys removed in the advisory-by-default consolidation. Kept here so
|
|
32
|
+
* the parser can emit a helpful migration error pointing users at the new
|
|
33
|
+
* single `strictness` knob instead of a generic "unknown key" message.
|
|
34
|
+
*/
|
|
35
|
+
const RETIRED_GUARD_CONFIG_KEYS = new Set([
|
|
36
|
+
"promptGuardMode",
|
|
37
|
+
"tddEnforcement",
|
|
38
|
+
"workflowGuardMode"
|
|
39
|
+
]);
|
|
32
40
|
/**
|
|
33
41
|
* Config keys always present in the minimal init template. Everything else
|
|
34
42
|
* is "advanced" — parsed when present, but not pre-populated by `cclaw init`.
|
|
@@ -131,8 +139,6 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
|
|
|
131
139
|
flowVersion: FLOW_VERSION,
|
|
132
140
|
harnesses,
|
|
133
141
|
strictness: "advisory",
|
|
134
|
-
promptGuardMode: "advisory",
|
|
135
|
-
tddEnforcement: "advisory",
|
|
136
142
|
tddTestGlobs: [...tddTestPathPatterns],
|
|
137
143
|
tdd: {
|
|
138
144
|
testPathPatterns: tddTestPathPatterns,
|
|
@@ -145,7 +151,6 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
|
|
|
145
151
|
defaultTrack,
|
|
146
152
|
languageRulePacks: [],
|
|
147
153
|
ironLaws: {
|
|
148
|
-
mode: "advisory",
|
|
149
154
|
strictLaws: []
|
|
150
155
|
}
|
|
151
156
|
};
|
|
@@ -208,6 +213,12 @@ export async function readConfig(projectRoot) {
|
|
|
208
213
|
const parsed = (parsedUnknown && typeof parsedUnknown === "object"
|
|
209
214
|
? parsedUnknown
|
|
210
215
|
: {});
|
|
216
|
+
const retiredGuardKeys = Object.keys(parsed).filter((key) => RETIRED_GUARD_CONFIG_KEYS.has(key));
|
|
217
|
+
if (retiredGuardKeys.length > 0) {
|
|
218
|
+
throw configValidationError(fullPath, `config key(s) ${retiredGuardKeys.join(", ")} were removed; ` +
|
|
219
|
+
`use the single \`strictness: advisory|strict\` knob instead ` +
|
|
220
|
+
`(advisory is the default). See docs/config.md#strictness for migration.`);
|
|
221
|
+
}
|
|
211
222
|
const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
|
|
212
223
|
if (unknownKeys.length > 0) {
|
|
213
224
|
throw configValidationError(fullPath, `unknown top-level key(s): ${unknownKeys.join(", ")}`);
|
|
@@ -235,29 +246,6 @@ export async function readConfig(projectRoot) {
|
|
|
235
246
|
throw configValidationError(fullPath, `"strictness" must be "advisory" or "strict"`);
|
|
236
247
|
}
|
|
237
248
|
const strictness = strictnessRaw === "strict" ? "strict" : "advisory";
|
|
238
|
-
// Legacy guard fields — keep honouring explicit values for power users who
|
|
239
|
-
// want asymmetric behaviour (e.g. strict prompt guard + advisory TDD).
|
|
240
|
-
// When the user only set `strictness`, both axes inherit from it.
|
|
241
|
-
const hasExplicitPromptGuard = Object.prototype.hasOwnProperty.call(parsed, "promptGuardMode");
|
|
242
|
-
const promptGuardModeRaw = parsed.promptGuardMode;
|
|
243
|
-
if (hasExplicitPromptGuard &&
|
|
244
|
-
promptGuardModeRaw !== "advisory" &&
|
|
245
|
-
promptGuardModeRaw !== "strict") {
|
|
246
|
-
throw configValidationError(fullPath, `"promptGuardMode" must be "advisory" or "strict"`);
|
|
247
|
-
}
|
|
248
|
-
const promptGuardMode = hasExplicitPromptGuard
|
|
249
|
-
? (promptGuardModeRaw === "strict" ? "strict" : "advisory")
|
|
250
|
-
: strictness;
|
|
251
|
-
const hasExplicitTddEnforcement = Object.prototype.hasOwnProperty.call(parsed, "tddEnforcement");
|
|
252
|
-
const tddEnforcementRaw = parsed.tddEnforcement;
|
|
253
|
-
if (hasExplicitTddEnforcement &&
|
|
254
|
-
tddEnforcementRaw !== "advisory" &&
|
|
255
|
-
tddEnforcementRaw !== "strict") {
|
|
256
|
-
throw configValidationError(fullPath, `"tddEnforcement" must be "advisory" or "strict"`);
|
|
257
|
-
}
|
|
258
|
-
const tddEnforcement = hasExplicitTddEnforcement
|
|
259
|
-
? (tddEnforcementRaw === "strict" ? "strict" : "advisory")
|
|
260
|
-
: strictness;
|
|
261
249
|
const tddTestGlobsRaw = parsed.tddTestGlobs;
|
|
262
250
|
const tddTestGlobs = validateStringArray(tddTestGlobsRaw, "tddTestGlobs", fullPath)
|
|
263
251
|
?? [...DEFAULT_TDD_TEST_GLOBS];
|
|
@@ -421,37 +409,31 @@ export async function readConfig(projectRoot) {
|
|
|
421
409
|
if (!isRecord(ironLawsRaw)) {
|
|
422
410
|
throw configValidationError(fullPath, `"ironLaws" must be an object`);
|
|
423
411
|
}
|
|
424
|
-
|
|
412
|
+
if (Object.prototype.hasOwnProperty.call(ironLawsRaw, "mode")) {
|
|
413
|
+
throw configValidationError(fullPath, `"ironLaws.mode" was removed; the project-wide \`strictness\` knob now ` +
|
|
414
|
+
`controls iron-law enforcement. Use \`ironLaws.strictLaws\` for per-law overrides.`);
|
|
415
|
+
}
|
|
416
|
+
const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "strictLaws");
|
|
425
417
|
if (unknownIronLawKeys.length > 0) {
|
|
426
418
|
throw configValidationError(fullPath, `"ironLaws" has unknown key(s): ${unknownIronLawKeys.join(", ")}`);
|
|
427
419
|
}
|
|
428
|
-
const modeRaw = ironLawsRaw.mode;
|
|
429
|
-
if (modeRaw !== undefined && modeRaw !== "advisory" && modeRaw !== "strict") {
|
|
430
|
-
throw configValidationError(fullPath, `"ironLaws.mode" must be "advisory" or "strict"`);
|
|
431
|
-
}
|
|
432
420
|
const strictLawIdsRaw = validateStringArray(ironLawsRaw.strictLaws, "ironLaws.strictLaws", fullPath) ?? [];
|
|
433
421
|
const unknownStrictLawIds = strictLawIdsRaw.filter((id) => !isIronLawId(id));
|
|
434
422
|
if (unknownStrictLawIds.length > 0) {
|
|
435
423
|
throw configValidationError(fullPath, `"ironLaws.strictLaws" contains unknown law id(s): ${unknownStrictLawIds.join(", ")}`);
|
|
436
424
|
}
|
|
437
425
|
ironLaws = {
|
|
438
|
-
mode: modeRaw === "strict" ? "strict" : "advisory",
|
|
439
426
|
strictLaws: normalizeStrictLawIds(strictLawIdsRaw)
|
|
440
427
|
};
|
|
441
428
|
}
|
|
442
429
|
else {
|
|
443
|
-
ironLaws = {
|
|
444
|
-
mode: strictness,
|
|
445
|
-
strictLaws: []
|
|
446
|
-
};
|
|
430
|
+
ironLaws = { strictLaws: [] };
|
|
447
431
|
}
|
|
448
432
|
return {
|
|
449
433
|
version: parsed.version ?? CCLAW_VERSION,
|
|
450
434
|
flowVersion: parsed.flowVersion ?? FLOW_VERSION,
|
|
451
435
|
harnesses,
|
|
452
436
|
strictness,
|
|
453
|
-
promptGuardMode,
|
|
454
|
-
tddEnforcement,
|
|
455
437
|
tddTestGlobs,
|
|
456
438
|
tdd: {
|
|
457
439
|
testPathPatterns: resolvedTddTestPathPatterns,
|
|
@@ -480,8 +462,6 @@ function buildSerializableConfig(config, options = {}) {
|
|
|
480
462
|
"flowVersion",
|
|
481
463
|
"harnesses",
|
|
482
464
|
"strictness",
|
|
483
|
-
"promptGuardMode",
|
|
484
|
-
"tddEnforcement",
|
|
485
465
|
"tddTestGlobs",
|
|
486
466
|
"tdd",
|
|
487
467
|
"compound",
|
|
@@ -536,8 +516,6 @@ export async function detectAdvancedKeys(projectRoot) {
|
|
|
536
516
|
if (!isRecord(parsedUnknown))
|
|
537
517
|
return new Set();
|
|
538
518
|
const advancedCandidates = [
|
|
539
|
-
"promptGuardMode",
|
|
540
|
-
"tddEnforcement",
|
|
541
519
|
"tddTestGlobs",
|
|
542
520
|
"tdd",
|
|
543
521
|
"compound",
|
|
@@ -13,7 +13,7 @@ Reference docs for \`cclaw doctor\` checks.
|
|
|
13
13
|
- \`state-and-gates.md\` - flow-state integrity and gate evidence contracts
|
|
14
14
|
- \`delegation-and-preamble.md\` - mandatory delegations and lightweight announce discipline
|
|
15
15
|
- \`traceability.md\` - spec/plan/tdd trace matrix expectations
|
|
16
|
-
- \`tooling-capabilities.md\` - local runtime prerequisites (
|
|
16
|
+
- \`tooling-capabilities.md\` - local runtime prerequisites (node only)
|
|
17
17
|
- \`config-and-policy.md\` - config schema, rules policy, and validation references
|
|
18
18
|
`,
|
|
19
19
|
"runtime-layout.md": `# Runtime Layout
|
|
@@ -116,17 +116,21 @@ Reference docs for \`cclaw doctor\` checks.
|
|
|
116
116
|
|
|
117
117
|
## Required
|
|
118
118
|
|
|
119
|
-
- \`
|
|
120
|
-
|
|
119
|
+
- \`node\` (>=20) — the only runtime dependency. All hooks, git-hook relays, and the
|
|
120
|
+
\`cclaw\` CLI itself run on Node.js. No \`bash\`, \`python3\`, or \`jq\` required.
|
|
121
|
+
- \`git\` — needed for worktree and pre-commit/pre-push relays.
|
|
121
122
|
|
|
122
|
-
##
|
|
123
|
+
## Not required (removed)
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
Earlier releases relied on \`bash\` to execute generated shell hooks and on
|
|
126
|
+
\`python3\`/\`jq\` as JSON fallback parsers. Node-only mode removes both: hooks
|
|
127
|
+
dispatch through \`node .cclaw/hooks/run-hook.mjs <hook-name>\`, so these tools
|
|
128
|
+
are no longer part of the supported runtime contract.
|
|
125
129
|
|
|
126
130
|
## Typical fixes
|
|
127
131
|
|
|
128
|
-
1. Install
|
|
129
|
-
2.
|
|
132
|
+
1. Install Node.js 20 or newer (matches \`package.json\` \`engines\`) and ensure \`node\` is on \`PATH\`.
|
|
133
|
+
2. Re-run \`cclaw sync\` to regenerate hook configs after upgrading Node.
|
|
130
134
|
`,
|
|
131
135
|
"config-and-policy.md": `# Config And Policy
|
|
132
136
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export interface NodeHookRuntimeOptions {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Single enforcement knob derived from `config.strictness`. Generated hooks
|
|
4
|
+
* embed this value as the default for every guard (prompt, workflow, TDD,
|
|
5
|
+
* iron-laws-coupled blocks). `CCLAW_STRICTNESS` env var overrides at run
|
|
6
|
+
* time; per-law strictness still flows through `iron-laws.json`.
|
|
7
|
+
*/
|
|
8
|
+
strictness?: "advisory" | "strict";
|
|
5
9
|
tddTestPathPatterns?: string[];
|
|
6
10
|
tddProductionPathPatterns?: string[];
|
|
7
11
|
}
|
|
@@ -11,9 +11,7 @@ function normalizePatterns(patterns, fallback) {
|
|
|
11
11
|
* bash/python/jq runtime dependencies.
|
|
12
12
|
*/
|
|
13
13
|
export function nodeHookRuntimeScript(options = {}) {
|
|
14
|
-
const
|
|
15
|
-
const workflowGuardMode = options.workflowGuardMode === "strict" ? "strict" : "advisory";
|
|
16
|
-
const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
|
|
14
|
+
const strictness = options.strictness === "strict" ? "strict" : "advisory";
|
|
17
15
|
const tddTestPathPatterns = normalizePatterns(options.tddTestPathPatterns, [
|
|
18
16
|
"**/*.test.*",
|
|
19
17
|
"**/tests/**",
|
|
@@ -27,12 +25,17 @@ import process from "node:process";
|
|
|
27
25
|
import { spawn } from "node:child_process";
|
|
28
26
|
|
|
29
27
|
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Single strictness default, derived from config.strictness at install time.
|
|
29
|
+
// \`CCLAW_STRICTNESS\` env var overrides for the current process. All guards
|
|
30
|
+
// (prompt, workflow, TDD, iron-laws) route through \`resolveStrictness()\`.
|
|
31
|
+
const DEFAULT_STRICTNESS = ${JSON.stringify(strictness)};
|
|
33
32
|
const DEFAULT_TDD_TEST_PATH_PATTERNS = ${JSON.stringify(tddTestPathPatterns)};
|
|
34
33
|
const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = ${JSON.stringify(tddProductionPathPatterns)};
|
|
35
34
|
|
|
35
|
+
function resolveStrictness() {
|
|
36
|
+
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
37
|
+
}
|
|
38
|
+
|
|
36
39
|
function toObject(value) {
|
|
37
40
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
38
41
|
return value;
|
|
@@ -186,12 +189,12 @@ async function detectRoot(env) {
|
|
|
186
189
|
try {
|
|
187
190
|
const runtimePath = path.join(candidate, RUNTIME_ROOT);
|
|
188
191
|
const stat = await fs.stat(runtimePath);
|
|
189
|
-
if (stat.isDirectory()) return candidate;
|
|
192
|
+
if (stat.isDirectory()) return { root: candidate, foundRuntime: true };
|
|
190
193
|
} catch {
|
|
191
194
|
// continue
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
|
-
return candidates[0] || process.cwd();
|
|
197
|
+
return { root: candidates[0] || process.cwd(), foundRuntime: false };
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
function toLower(value) {
|
|
@@ -1024,9 +1027,7 @@ async function handlePreCompact(runtime) {
|
|
|
1024
1027
|
}
|
|
1025
1028
|
|
|
1026
1029
|
async function handlePromptGuard(runtime) {
|
|
1027
|
-
const mode =
|
|
1028
|
-
? "strict"
|
|
1029
|
-
: DEFAULT_PROMPT_GUARD_MODE;
|
|
1030
|
+
const mode = resolveStrictness();
|
|
1030
1031
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1031
1032
|
const guardLog = path.join(stateDir, "prompt-guard.jsonl");
|
|
1032
1033
|
|
|
@@ -1202,17 +1203,15 @@ function isProductionPath(rawPath, testPatterns, productionPatterns) {
|
|
|
1202
1203
|
}
|
|
1203
1204
|
|
|
1204
1205
|
async function handleWorkflowGuard(runtime) {
|
|
1205
|
-
const mode =
|
|
1206
|
-
? "strict"
|
|
1207
|
-
: DEFAULT_WORKFLOW_GUARD_MODE;
|
|
1206
|
+
const mode = resolveStrictness();
|
|
1208
1207
|
const maxAgeRaw = process.env.CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC;
|
|
1209
1208
|
const maxAgeSec =
|
|
1210
1209
|
typeof maxAgeRaw === "string" && /^[0-9]+$/u.test(maxAgeRaw)
|
|
1211
1210
|
? Number(maxAgeRaw)
|
|
1212
1211
|
: 1800;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1212
|
+
// TDD enforcement now follows the same single strictness knob — keeping the
|
|
1213
|
+
// distinct local binding so the downstream block rules stay self-documenting.
|
|
1214
|
+
const tddEnforcement = mode;
|
|
1216
1215
|
|
|
1217
1216
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1218
1217
|
const guardStateFile = path.join(stateDir, "workflow-guard.json");
|
|
@@ -1513,12 +1512,10 @@ async function handleContextMonitor(runtime) {
|
|
|
1513
1512
|
}
|
|
1514
1513
|
|
|
1515
1514
|
async function handleVerifyCurrentState(runtime) {
|
|
1516
|
-
const mode =
|
|
1517
|
-
? "strict"
|
|
1518
|
-
: DEFAULT_WORKFLOW_GUARD_MODE;
|
|
1515
|
+
const mode = resolveStrictness();
|
|
1519
1516
|
const result = await runCclawInternal(runtime.root, ["verify-current-state", "--quiet"]);
|
|
1520
1517
|
if (result.missingBinary) {
|
|
1521
|
-
process.stderr.write("[cclaw]
|
|
1518
|
+
process.stderr.write("[cclaw] hook: cclaw binary is required for verify-current-state\\n");
|
|
1522
1519
|
return 1;
|
|
1523
1520
|
}
|
|
1524
1521
|
if (mode === "strict") {
|
|
@@ -1555,7 +1552,14 @@ async function main() {
|
|
|
1555
1552
|
}
|
|
1556
1553
|
|
|
1557
1554
|
const harness = detectHarness(process.env);
|
|
1558
|
-
const root = await detectRoot(process.env);
|
|
1555
|
+
const { root, foundRuntime } = await detectRoot(process.env);
|
|
1556
|
+
if (!foundRuntime) {
|
|
1557
|
+
// No .cclaw/ runtime in any candidate root — this directory is not
|
|
1558
|
+
// initialized for cclaw. Exit 0 silently so hooks never block harnesses
|
|
1559
|
+
// that run in unrelated repos; users initialize with \`cclaw init\`.
|
|
1560
|
+
process.exitCode = 0;
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1559
1563
|
const inputRaw = await readStdin();
|
|
1560
1564
|
const inputData = safeParseJson(inputRaw, {});
|
|
1561
1565
|
const runtime = {
|
package/dist/content/observe.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
2
|
function hookDispatcherCommand(hookName) {
|
|
3
|
+
// RUNTIME_ROOT is a relative path (".cclaw") that currently contains no
|
|
4
|
+
// whitespace, so quoting is unnecessary inside the JSON-encoded command
|
|
5
|
+
// string. If RUNTIME_ROOT ever becomes configurable, wrap the path with
|
|
6
|
+
// JSON.stringify to survive spaces.
|
|
3
7
|
return `node ${RUNTIME_ROOT}/hooks/run-hook.mjs ${hookName}`;
|
|
4
8
|
}
|
|
5
9
|
export function claudeHooksJsonWithObservation() {
|
|
@@ -409,6 +409,12 @@ export default function cclawPlugin(ctx) {
|
|
|
409
409
|
: typeof payload;
|
|
410
410
|
console.error("[cclaw] opencode unknown event payload keys: " + keys);
|
|
411
411
|
}
|
|
412
|
+
// session.compacted must run pre-compact BEFORE refreshing the bootstrap
|
|
413
|
+
// cache, otherwise the injected system prompt still shows the pre-compact
|
|
414
|
+
// digest/state until the next lifecycle event.
|
|
415
|
+
if (eventType === "session.compacted") {
|
|
416
|
+
await runHookScript("pre-compact", eventData ?? {});
|
|
417
|
+
}
|
|
412
418
|
if (
|
|
413
419
|
eventType === "session.created" ||
|
|
414
420
|
eventType === "session.resumed" ||
|
|
@@ -424,9 +430,6 @@ export default function cclawPlugin(ctx) {
|
|
|
424
430
|
// until the next compaction or restart.
|
|
425
431
|
await refreshBootstrapCache(true);
|
|
426
432
|
}
|
|
427
|
-
if (eventType === "session.compacted") {
|
|
428
|
-
await runHookScript("pre-compact", eventData ?? {});
|
|
429
|
-
}
|
|
430
433
|
if (eventType === "session.idle") {
|
|
431
434
|
await runHookScript("stop-checkpoint", { loop_count: 0 });
|
|
432
435
|
}
|
package/dist/doctor-registry.js
CHANGED
|
@@ -30,15 +30,6 @@ const RULES = [
|
|
|
30
30
|
docRef: ref("runtime-layout.md")
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
{
|
|
34
|
-
test: /^capability:runtime:json_parser$/,
|
|
35
|
-
metadata: {
|
|
36
|
-
severity: "warning",
|
|
37
|
-
summary: "Optional JSON fallback parser availability.",
|
|
38
|
-
fix: "Install at least one of `python3` or `jq` for resilient fallback parsing.",
|
|
39
|
-
docRef: ref("tooling-capabilities.md")
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
33
|
{
|
|
43
34
|
test: /^capability:required:/,
|
|
44
35
|
metadata: {
|
package/dist/doctor.js
CHANGED
|
@@ -483,17 +483,17 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
483
483
|
: `warning: ${RUNTIME_ROOT}/config.yaml uses deprecated "tddTestGlobs". Migrate to "tdd.testPathPatterns".`
|
|
484
484
|
: `no deprecated "tddTestGlobs" key detected in ${RUNTIME_ROOT}/config.yaml`
|
|
485
485
|
});
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
let
|
|
489
|
-
if (await exists(
|
|
490
|
-
const
|
|
491
|
-
|
|
486
|
+
const expectedStrictness = parsedConfig.strictness === "strict" ? "strict" : "advisory";
|
|
487
|
+
const hookRuntimePath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
488
|
+
let strictnessOk = false;
|
|
489
|
+
if (await exists(hookRuntimePath)) {
|
|
490
|
+
const runtimeContent = await fs.readFile(hookRuntimePath, "utf8");
|
|
491
|
+
strictnessOk = runtimeContent.includes(`const DEFAULT_STRICTNESS = "${expectedStrictness}"`);
|
|
492
492
|
}
|
|
493
493
|
checks.push({
|
|
494
|
-
name: "hook:
|
|
495
|
-
ok:
|
|
496
|
-
details: `${
|
|
494
|
+
name: "hook:runtime:strictness",
|
|
495
|
+
ok: strictnessOk,
|
|
496
|
+
details: `${hookRuntimePath} must embed DEFAULT_STRICTNESS = "${expectedStrictness}" matching config.strictness`
|
|
497
497
|
});
|
|
498
498
|
if (parsedConfig.gitHookGuards === true) {
|
|
499
499
|
const runtimePreCommit = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-commit.mjs");
|
|
@@ -743,12 +743,9 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
743
743
|
}
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
|
-
// OpenCode plugin
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "hooks", "opencode-plugin.mjs")),
|
|
750
|
-
details: `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs`
|
|
751
|
-
});
|
|
746
|
+
// OpenCode plugin deployed path. (Presence of the source under
|
|
747
|
+
// `${RUNTIME_ROOT}/hooks/opencode-plugin.mjs` is already asserted by the
|
|
748
|
+
// generic `hook:script:opencode-plugin.mjs` check above; avoid a duplicate.)
|
|
752
749
|
const opencodeEnabled = configuredHarnesses.includes("opencode");
|
|
753
750
|
const opencodeDeployed = await exists(path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs"));
|
|
754
751
|
checks.push({
|
|
@@ -1009,27 +1006,11 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1009
1006
|
});
|
|
1010
1007
|
}
|
|
1011
1008
|
const hasNode = await commandAvailable("node");
|
|
1012
|
-
const hasPython = await commandAvailable("python3");
|
|
1013
|
-
const hasJq = await commandAvailable("jq");
|
|
1014
1009
|
checks.push({
|
|
1015
1010
|
name: "capability:required:node",
|
|
1016
1011
|
ok: hasNode,
|
|
1017
1012
|
details: "node is required for cclaw runtime scripts and CLI wiring"
|
|
1018
1013
|
});
|
|
1019
|
-
checks.push({
|
|
1020
|
-
name: "warning:capability:jq",
|
|
1021
|
-
ok: true,
|
|
1022
|
-
details: hasJq
|
|
1023
|
-
? "jq available (optional)"
|
|
1024
|
-
: "warning: jq not found; Node hook runtime no longer depends on jq"
|
|
1025
|
-
});
|
|
1026
|
-
checks.push({
|
|
1027
|
-
name: "warning:capability:python3",
|
|
1028
|
-
ok: true,
|
|
1029
|
-
details: hasPython
|
|
1030
|
-
? "python3 available (optional)"
|
|
1031
|
-
: "warning: python3 not found; Node hook runtime no longer depends on python3"
|
|
1032
|
-
});
|
|
1033
1014
|
const windowsHookConfigCandidates = [
|
|
1034
1015
|
path.join(projectRoot, ".claude/hooks/hooks.json"),
|
|
1035
1016
|
path.join(projectRoot, ".cursor/hooks.json"),
|
package/dist/install.js
CHANGED
|
@@ -119,6 +119,11 @@ function resolveChangedFiles(root) {
|
|
|
119
119
|
const root = resolveRepoRoot();
|
|
120
120
|
const runtimeHook = path.join(root, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
121
121
|
if (!fs.existsSync(runtimeHook)) {
|
|
122
|
+
// cclaw git relay is installed but the runtime entrypoint is missing —
|
|
123
|
+
// warn visibly (without blocking the commit) so the drift is noticed.
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
"[cclaw] " + HOOK_NAME + ": " + runtimeHook + " not found; run \`cclaw sync\` to reinstall\\n"
|
|
126
|
+
);
|
|
122
127
|
process.exit(0);
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -715,15 +720,14 @@ async function writeHooks(projectRoot, config) {
|
|
|
715
720
|
const stateDir = runtimePath(projectRoot, "state");
|
|
716
721
|
await ensureDir(hooksDir);
|
|
717
722
|
await ensureDir(stateDir);
|
|
723
|
+
const effectiveStrictness = config.strictness ?? "advisory";
|
|
718
724
|
await writeFileSafe(runtimePath(projectRoot, "state", "iron-laws.json"), `${JSON.stringify(ironLawRuntimeDocument({
|
|
719
|
-
mode:
|
|
725
|
+
mode: effectiveStrictness,
|
|
720
726
|
strictLaws: config.ironLaws?.strictLaws
|
|
721
727
|
}), null, 2)}\n`);
|
|
722
728
|
await writeFileSafe(path.join(hooksDir, "stage-complete.mjs"), stageCompleteScript());
|
|
723
729
|
await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
|
|
724
|
-
|
|
725
|
-
workflowGuardMode: config.strictness ?? "advisory",
|
|
726
|
-
tddEnforcementMode: config.tddEnforcement ?? config.strictness ?? "advisory",
|
|
730
|
+
strictness: effectiveStrictness,
|
|
727
731
|
tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
|
|
728
732
|
tddProductionPathPatterns: config.tdd?.productionPathPatterns
|
|
729
733
|
}));
|
|
@@ -1428,7 +1432,11 @@ function stripManagedHookCommands(value) {
|
|
|
1428
1432
|
return { updated: root, changed: true };
|
|
1429
1433
|
}
|
|
1430
1434
|
function isManagedRuntimeHookCommand(command) {
|
|
1431
|
-
|
|
1435
|
+
// Normalize whitespace and collapse any Windows-style backslash path
|
|
1436
|
+
// separators to forward slashes so user-edited hook configs on Windows
|
|
1437
|
+
// (e.g. `node .cclaw\hooks\run-hook.mjs ...`) still round-trip through
|
|
1438
|
+
// sync without being duplicated alongside freshly generated entries.
|
|
1439
|
+
const normalized = command.trim().replace(/\s+/gu, " ").replace(/\\/gu, "/");
|
|
1432
1440
|
if (/(^|\s)(?:node\s+)?(?:"|')?(?:\.\/)?\.cclaw\/hooks\/run-hook\.mjs(?:"|')?\s+(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor|verify-current-state)(?:\s|$)/u.test(normalized)) {
|
|
1433
1441
|
return true;
|
|
1434
1442
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -110,7 +110,12 @@ export interface CompoundConfig {
|
|
|
110
110
|
recurrenceThreshold?: number;
|
|
111
111
|
}
|
|
112
112
|
export interface IronLawsConfig {
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Per-law escape hatch: list the iron-law ids that must always be strict,
|
|
115
|
+
* independent of the project-wide `strictness` knob. Kept as an advanced
|
|
116
|
+
* override for teams that want e.g. `tdd-red-before-write` strict while the
|
|
117
|
+
* rest of the pipeline stays advisory.
|
|
118
|
+
*/
|
|
114
119
|
strictLaws?: string[];
|
|
115
120
|
}
|
|
116
121
|
export interface CclawConfig {
|
|
@@ -118,30 +123,16 @@ export interface CclawConfig {
|
|
|
118
123
|
flowVersion: string;
|
|
119
124
|
harnesses: HarnessId[];
|
|
120
125
|
/**
|
|
121
|
-
* Single
|
|
122
|
-
*
|
|
123
|
-
*
|
|
126
|
+
* Single knob that controls enforcement behaviour of all hook-driven guards
|
|
127
|
+
* (prompt guard, workflow guard, TDD enforcement, iron laws). Default:
|
|
128
|
+
* `"advisory"` — hooks append a stderr nudge and exit 0. `"strict"` flips
|
|
129
|
+
* the same hooks to fail-closed (non-zero exit) so the harness refuses the
|
|
130
|
+
* offending action.
|
|
124
131
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* prompt guard, advisory TDD) can still set the legacy fields directly —
|
|
128
|
-
* explicit per-axis values override the derived strictness.
|
|
132
|
+
* Per-law escapes live on `ironLaws.strictLaws` for teams that need to keep
|
|
133
|
+
* specific iron laws strict while the project-wide knob stays advisory.
|
|
129
134
|
*/
|
|
130
135
|
strictness?: "advisory" | "strict";
|
|
131
|
-
/**
|
|
132
|
-
* Prompt guard behavior for runtime write-risk detection hooks.
|
|
133
|
-
*
|
|
134
|
-
* Since v0.43.0 this is an advanced override. Prefer `strictness` in new
|
|
135
|
-
* configs; set this explicitly only when you need strict prompt guarding
|
|
136
|
-
* while keeping TDD advisory, or vice versa.
|
|
137
|
-
*/
|
|
138
|
-
promptGuardMode?: "advisory" | "strict";
|
|
139
|
-
/**
|
|
140
|
-
* TDD RED -> GREEN -> REFACTOR enforcement mode used by workflow guard hooks.
|
|
141
|
-
*
|
|
142
|
-
* Since v0.43.0 this is an advanced override — see `strictness`.
|
|
143
|
-
*/
|
|
144
|
-
tddEnforcement?: "advisory" | "strict";
|
|
145
136
|
/**
|
|
146
137
|
* Legacy alias for test-side path detection in workflow-guard.
|
|
147
138
|
* Prefer `tdd.testPathPatterns` in new configs.
|