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 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 (`promptGuardMode` / `tddEnforcement` per-axis overrides,
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
- promptGuardMode: previewConfig.promptGuardMode,
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 = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws";
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
- const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "mode" && key !== "strictLaws");
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 (bash/node/python/jq)
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
- - \`bash\` for runtime hook scripts
120
- - \`node\` for generated runtime scripts/plugins
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
- ## Optional fallback
123
+ ## Not required (removed)
123
124
 
124
- - at least one of \`python3\` or \`jq\` for JSON parsing fallback paths
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 missing runtime tools.
129
- 2. Keep at least one JSON fallback parser available (\`python3\` or \`jq\`).
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
- promptGuardMode?: "advisory" | "strict";
3
- workflowGuardMode?: "advisory" | "strict";
4
- tddEnforcementMode?: "advisory" | "strict";
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 promptGuardMode = options.promptGuardMode === "strict" ? "strict" : "advisory";
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
- const DEFAULT_PROMPT_GUARD_MODE = ${JSON.stringify(promptGuardMode)};
31
- const DEFAULT_WORKFLOW_GUARD_MODE = ${JSON.stringify(workflowGuardMode)};
32
- const DEFAULT_TDD_ENFORCEMENT_MODE = ${JSON.stringify(tddEnforcementMode)};
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 = process.env.PROMPT_GUARD_MODE === "strict"
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 = process.env.CCLAW_WORKFLOW_GUARD_MODE === "strict"
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
- const tddEnforcement = process.env.TDD_ENFORCEMENT_MODE === "strict"
1214
- ? "strict"
1215
- : DEFAULT_TDD_ENFORCEMENT_MODE;
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 = process.env.CCLAW_WORKFLOW_GUARD_MODE === "strict"
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] codex hook: cclaw binary is required for verify-current-state\\n");
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 = {
@@ -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
  }
@@ -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 expectedMode = parsedConfig.promptGuardMode === "strict" ? "strict" : "advisory";
487
- const promptGuardPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
488
- let promptGuardModeOk = false;
489
- if (await exists(promptGuardPath)) {
490
- const promptGuardContent = await fs.readFile(promptGuardPath, "utf8");
491
- promptGuardModeOk = promptGuardContent.includes(`const DEFAULT_PROMPT_GUARD_MODE = "${expectedMode}"`);
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:prompt_guard:mode",
495
- ok: promptGuardModeOk,
496
- details: `${promptGuardPath} must match promptGuardMode=${expectedMode}`
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 source + deployed path
747
- checks.push({
748
- name: "hook:opencode_plugin_source",
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: config.ironLaws?.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
- promptGuardMode: config.promptGuardMode ?? config.strictness ?? "advisory",
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
- const normalized = command.trim().replace(/\s+/gu, " ");
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
- mode?: "advisory" | "strict";
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-knob strictness for both guard families. When set, cclaw derives
122
- * `promptGuardMode` and `tddEnforcement` from this value unless the legacy
123
- * fields are explicitly provided. Default: "advisory".
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
- * Added in v0.43.0 to collapse two fields that always moved together for
126
- * ~99% of users. Power users who want asymmetric strictness (e.g. strict
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.10",
3
+ "version": "0.48.12",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {