cclaw-cli 0.48.5 → 0.48.7

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.
Files changed (37) hide show
  1. package/dist/artifact-linter.js +32 -0
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.js +44 -5
  4. package/dist/content/hooks.d.ts +2 -2
  5. package/dist/content/hooks.js +293 -89
  6. package/dist/content/ideate-command.js +11 -0
  7. package/dist/content/iron-laws.d.ts +142 -0
  8. package/dist/content/iron-laws.js +191 -0
  9. package/dist/content/meta-skill.js +1 -0
  10. package/dist/content/next-command.js +12 -0
  11. package/dist/content/observe.js +555 -45
  12. package/dist/content/ops-command.js +11 -0
  13. package/dist/content/session-hooks.js +3 -1
  14. package/dist/content/stage-schema.d.ts +16 -0
  15. package/dist/content/stage-schema.js +82 -5
  16. package/dist/content/stages/review.js +4 -4
  17. package/dist/content/stages/tdd.js +7 -7
  18. package/dist/content/start-command.js +12 -0
  19. package/dist/content/subagents.js +26 -0
  20. package/dist/content/templates.js +8 -0
  21. package/dist/content/view-command.js +11 -0
  22. package/dist/doctor.js +6 -2
  23. package/dist/harness-adapters.js +3 -0
  24. package/dist/install.js +11 -1
  25. package/dist/internal/advance-stage.js +14 -2
  26. package/dist/internal/envelope-validate.d.ts +7 -0
  27. package/dist/internal/envelope-validate.js +66 -0
  28. package/dist/internal/knowledge-digest.d.ts +7 -0
  29. package/dist/internal/knowledge-digest.js +93 -0
  30. package/dist/internal/tdd-red-evidence.d.ts +7 -0
  31. package/dist/internal/tdd-red-evidence.js +130 -0
  32. package/dist/knowledge-store.d.ts +8 -0
  33. package/dist/knowledge-store.js +95 -0
  34. package/dist/tdd-cycle.d.ts +7 -0
  35. package/dist/tdd-cycle.js +29 -0
  36. package/dist/types.d.ts +6 -0
  37. package/package.json +1 -1
@@ -1092,6 +1092,14 @@ export async function validateReviewArmy(projectRoot) {
1092
1092
  }
1093
1093
  const severitySet = new Set(["Critical", "Important", "Suggestion"]);
1094
1094
  const statusSet = new Set(["open", "accepted", "resolved"]);
1095
+ const sourceSet = new Set([
1096
+ "spec",
1097
+ "correctness",
1098
+ "security",
1099
+ "performance",
1100
+ "architecture",
1101
+ "external-safety"
1102
+ ]);
1095
1103
  const findingIds = new Set();
1096
1104
  const openCriticalIds = new Set();
1097
1105
  if (!Array.isArray(root.findings)) {
@@ -1128,6 +1136,17 @@ export async function validateReviewArmy(projectRoot) {
1128
1136
  if (!isStringArray(o.reportedBy) || o.reportedBy.length === 0) {
1129
1137
  errors.push(`findings[${i}].reportedBy must be a non-empty string array.`);
1130
1138
  }
1139
+ if (o.sources !== undefined) {
1140
+ if (!isStringArray(o.sources) || o.sources.length === 0) {
1141
+ errors.push(`findings[${i}].sources must be a non-empty string array when present.`);
1142
+ }
1143
+ else {
1144
+ const invalidSources = o.sources.filter((source) => !sourceSet.has(source));
1145
+ if (invalidSources.length > 0) {
1146
+ errors.push(`findings[${i}].sources contains unknown values: ${invalidSources.join(", ")}.`);
1147
+ }
1148
+ }
1149
+ }
1131
1150
  if (o.location === undefined || o.location === null) {
1132
1151
  errors.push(`findings[${i}].location is required and must be an object with file + line.`);
1133
1152
  }
@@ -1231,6 +1250,19 @@ export async function validateReviewArmy(projectRoot) {
1231
1250
  }
1232
1251
  }
1233
1252
  }
1253
+ if (rec.layerCoverage !== undefined) {
1254
+ if (rec.layerCoverage === null || typeof rec.layerCoverage !== "object" || Array.isArray(rec.layerCoverage)) {
1255
+ errors.push("reconciliation.layerCoverage must be an object when present.");
1256
+ }
1257
+ else {
1258
+ const coverage = rec.layerCoverage;
1259
+ for (const source of sourceSet) {
1260
+ if (coverage[source] !== undefined && typeof coverage[source] !== "boolean") {
1261
+ errors.push(`reconciliation.layerCoverage.${source} must be boolean when present.`);
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1234
1266
  }
1235
1267
  return { valid: errors.length === 0, errors };
1236
1268
  }
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";
45
+ type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "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
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { CCLAW_VERSION, DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT } from "./constants.js";
5
+ import { isIronLawId, normalizeStrictLawIds } from "./content/iron-laws.js";
5
6
  import { exists, writeFileSafe } from "./fs-utils.js";
6
7
  import { FLOW_TRACKS, HARNESS_IDS, LANGUAGE_RULE_PACKS } from "./types.js";
7
8
  const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
@@ -25,7 +26,8 @@ const ALLOWED_CONFIG_KEYS = new Set([
25
26
  "defaultTrack",
26
27
  "languageRulePacks",
27
28
  "trackHeuristics",
28
- "sliceReview"
29
+ "sliceReview",
30
+ "ironLaws"
29
31
  ]);
30
32
  /**
31
33
  * Config keys always present in the minimal init template. Everything else
@@ -141,7 +143,11 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
141
143
  },
142
144
  gitHookGuards: false,
143
145
  defaultTrack,
144
- languageRulePacks: []
146
+ languageRulePacks: [],
147
+ ironLaws: {
148
+ mode: "advisory",
149
+ strictLaws: []
150
+ }
145
151
  };
146
152
  }
147
153
  /**
@@ -409,6 +415,36 @@ export async function readConfig(projectRoot) {
409
415
  enforceOnTracks: enforceOnTracks ?? DEFAULT_SLICE_REVIEW_TRACKS
410
416
  };
411
417
  }
418
+ const ironLawsRaw = parsed.ironLaws;
419
+ let ironLaws = undefined;
420
+ if (Object.prototype.hasOwnProperty.call(parsed, "ironLaws")) {
421
+ if (!isRecord(ironLawsRaw)) {
422
+ throw configValidationError(fullPath, `"ironLaws" must be an object`);
423
+ }
424
+ const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "mode" && key !== "strictLaws");
425
+ if (unknownIronLawKeys.length > 0) {
426
+ throw configValidationError(fullPath, `"ironLaws" has unknown key(s): ${unknownIronLawKeys.join(", ")}`);
427
+ }
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
+ const strictLawIdsRaw = validateStringArray(ironLawsRaw.strictLaws, "ironLaws.strictLaws", fullPath) ?? [];
433
+ const unknownStrictLawIds = strictLawIdsRaw.filter((id) => !isIronLawId(id));
434
+ if (unknownStrictLawIds.length > 0) {
435
+ throw configValidationError(fullPath, `"ironLaws.strictLaws" contains unknown law id(s): ${unknownStrictLawIds.join(", ")}`);
436
+ }
437
+ ironLaws = {
438
+ mode: modeRaw === "strict" ? "strict" : "advisory",
439
+ strictLaws: normalizeStrictLawIds(strictLawIdsRaw)
440
+ };
441
+ }
442
+ else {
443
+ ironLaws = {
444
+ mode: strictness,
445
+ strictLaws: []
446
+ };
447
+ }
412
448
  return {
413
449
  version: parsed.version ?? CCLAW_VERSION,
414
450
  flowVersion: parsed.flowVersion ?? FLOW_VERSION,
@@ -428,7 +464,8 @@ export async function readConfig(projectRoot) {
428
464
  defaultTrack,
429
465
  languageRulePacks,
430
466
  trackHeuristics,
431
- sliceReview
467
+ sliceReview,
468
+ ironLaws
432
469
  };
433
470
  }
434
471
  function isMinimalKey(key) {
@@ -452,7 +489,8 @@ function buildSerializableConfig(config, options = {}) {
452
489
  "defaultTrack",
453
490
  "languageRulePacks",
454
491
  "trackHeuristics",
455
- "sliceReview"
492
+ "sliceReview",
493
+ "ironLaws"
456
494
  ];
457
495
  for (const key of ordered) {
458
496
  const value = config[key];
@@ -506,7 +544,8 @@ export async function detectAdvancedKeys(projectRoot) {
506
544
  "defaultTrack",
507
545
  "languageRulePacks",
508
546
  "trackHeuristics",
509
- "sliceReview"
547
+ "sliceReview",
548
+ "ironLaws"
510
549
  ];
511
550
  const present = new Set();
512
551
  for (const key of advancedCandidates) {
@@ -8,7 +8,8 @@
8
8
  export interface HookRuntimeOptions {
9
9
  }
10
10
  /** Shared bash preamble for generated hook scripts. */
11
- export declare const RUNTIME_SHELL_DETECT_ROOT = "HARNESS=\"codex\"\nif [ -n \"${CLAUDE_PROJECT_DIR:-}\" ]; then\n HARNESS=\"claude\"\nelif [ -n \"${CURSOR_PROJECT_DIR:-}\" ] || [ -n \"${CURSOR_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"cursor\"\nelif [ -n \"${OPENCODE_PROJECT_DIR:-}\" ] || [ -n \"${OPENCODE_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"opencode\"\nfi\n\nROOT=\"\"\nfor candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -d \"$candidate/.cclaw\" ]; then\n ROOT=\"$candidate\"\n break\n fi\ndone\nif [ -z \"$ROOT\" ]; then\n ROOT=\"${CCLAW_PROJECT_ROOT:-${CLAUDE_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-${CURSOR_PROJECT_ROOT:-${OPENCODE_PROJECT_DIR:-${OPENCODE_PROJECT_ROOT:-${PWD}}}}}}}\"\nfi";
11
+ export declare const RUNTIME_SHELL_DETECT_ROOT = "CCLAW_HOOK_LIB_PATH=\"\"\nfor candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -f \"$candidate/.cclaw/hooks/_lib.sh\" ]; then\n CCLAW_HOOK_LIB_PATH=\"$candidate/.cclaw/hooks/_lib.sh\"\n break\n fi\ndone\nif [ -n \"$CCLAW_HOOK_LIB_PATH\" ] && [ -f \"$CCLAW_HOOK_LIB_PATH\" ]; then\n # shellcheck disable=SC1090\n . \"$CCLAW_HOOK_LIB_PATH\"\nfi\n\nif command -v cclaw_hook_detect_root >/dev/null 2>&1; then\n cclaw_hook_detect_root\nelse\n HARNESS=\"codex\"\n if [ -n \"${CLAUDE_PROJECT_DIR:-}\" ]; then\n HARNESS=\"claude\"\n elif [ -n \"${CURSOR_PROJECT_DIR:-}\" ] || [ -n \"${CURSOR_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"cursor\"\n elif [ -n \"${OPENCODE_PROJECT_DIR:-}\" ] || [ -n \"${OPENCODE_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"opencode\"\n fi\n\n ROOT=\"\"\n for candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -d \"$candidate/.cclaw\" ]; then\n ROOT=\"$candidate\"\n break\n fi\n done\n if [ -z \"$ROOT\" ]; then\n ROOT=\"${CCLAW_PROJECT_ROOT:-${CLAUDE_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-${CURSOR_PROJECT_ROOT:-${OPENCODE_PROJECT_DIR:-${OPENCODE_PROJECT_ROOT:-${PWD}}}}}}}\"\n fi\nfi";
12
+ export declare function hookLibScript(): string;
12
13
  export declare function sessionStartScript(_options?: HookRuntimeOptions): string;
13
14
  export declare function stopCheckpointScript(): string;
14
15
  export declare function runHookDispatcherScript(): string;
@@ -18,4 +19,3 @@ export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js"
18
19
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
19
20
  export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
20
21
  export { opencodePluginJs } from "./opencode-plugin.js";
21
- export declare function hooksAgentsMdBlock(): string;
@@ -15,39 +15,201 @@ const ESCAPE_FN = `escape_json() {
15
15
  str=\${str//$'\\n'/\\\\n}
16
16
  printf '%s' "$str"
17
17
  }`;
18
- const DETECT_ROOT = `HARNESS="codex"
19
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
20
- HARNESS="claude"
21
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
22
- HARNESS="cursor"
23
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
24
- HARNESS="opencode"
25
- fi
26
-
27
- ROOT=""
18
+ const HOOK_LIB_FILE = "_lib.sh";
19
+ /** Shared bash preamble for generated hook scripts. */
20
+ export const RUNTIME_SHELL_DETECT_ROOT = `CCLAW_HOOK_LIB_PATH=""
28
21
  for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
29
- if [ -n "$candidate" ] && [ -d "$candidate/${RUNTIME_ROOT}" ]; then
30
- ROOT="$candidate"
22
+ if [ -n "$candidate" ] && [ -f "$candidate/${RUNTIME_ROOT}/hooks/${HOOK_LIB_FILE}" ]; then
23
+ CCLAW_HOOK_LIB_PATH="$candidate/${RUNTIME_ROOT}/hooks/${HOOK_LIB_FILE}"
31
24
  break
32
25
  fi
33
26
  done
34
- if [ -z "$ROOT" ]; then
35
- ROOT="\${CCLAW_PROJECT_ROOT:-\${CLAUDE_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-\${CURSOR_PROJECT_ROOT:-\${OPENCODE_PROJECT_DIR:-\${OPENCODE_PROJECT_ROOT:-\${PWD}}}}}}}"
27
+ if [ -n "$CCLAW_HOOK_LIB_PATH" ] && [ -f "$CCLAW_HOOK_LIB_PATH" ]; then
28
+ # shellcheck disable=SC1090
29
+ . "$CCLAW_HOOK_LIB_PATH"
30
+ fi
31
+
32
+ if command -v cclaw_hook_detect_root >/dev/null 2>&1; then
33
+ cclaw_hook_detect_root
34
+ else
35
+ HARNESS="codex"
36
+ if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
37
+ HARNESS="claude"
38
+ elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
39
+ HARNESS="cursor"
40
+ elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
41
+ HARNESS="opencode"
42
+ fi
43
+
44
+ ROOT=""
45
+ for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
46
+ if [ -n "$candidate" ] && [ -d "$candidate/${RUNTIME_ROOT}" ]; then
47
+ ROOT="$candidate"
48
+ break
49
+ fi
50
+ done
51
+ if [ -z "$ROOT" ]; then
52
+ ROOT="\${CCLAW_PROJECT_ROOT:-\${CLAUDE_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-\${CURSOR_PROJECT_ROOT:-\${OPENCODE_PROJECT_DIR:-\${OPENCODE_PROJECT_ROOT:-\${PWD}}}}}}}"
53
+ fi
36
54
  fi`;
37
- /** Shared bash preamble for generated hook scripts. */
38
- export const RUNTIME_SHELL_DETECT_ROOT = DETECT_ROOT;
55
+ export function hookLibScript() {
56
+ return `#!/usr/bin/env bash
57
+ # cclaw shared hook library — generated by cclaw sync
58
+ # Shared helper functions for root detection and lightweight JSON parsing.
59
+
60
+ cclaw_hook_detect_root() {
61
+ HARNESS="codex"
62
+ if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
63
+ HARNESS="claude"
64
+ elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
65
+ HARNESS="cursor"
66
+ elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
67
+ HARNESS="opencode"
68
+ fi
69
+
70
+ ROOT=""
71
+ for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
72
+ if [ -n "$candidate" ] && [ -d "$candidate/${RUNTIME_ROOT}" ]; then
73
+ ROOT="$candidate"
74
+ break
75
+ fi
76
+ done
77
+ if [ -z "$ROOT" ]; then
78
+ ROOT="\${CCLAW_PROJECT_ROOT:-\${CLAUDE_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-\${CURSOR_PROJECT_ROOT:-\${OPENCODE_PROJECT_DIR:-\${OPENCODE_PROJECT_ROOT:-\${PWD}}}}}}}"
79
+ fi
80
+ }
81
+
82
+ cclaw_hook_lower() {
83
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
84
+ }
85
+
86
+ cclaw_hook_extract_tool_and_payload() {
87
+ local input_json="$1"
88
+ CCLAW_HOOK_TOOL="unknown"
89
+ CCLAW_HOOK_PAYLOAD=""
90
+ if command -v jq >/dev/null 2>&1; then
91
+ CCLAW_HOOK_TOOL=$(printf '%s' "$input_json" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
92
+ CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
93
+ elif command -v python3 >/dev/null 2>&1; then
94
+ CCLAW_HOOK_TOOL=$(INPUT_JSON="$input_json" python3 - <<'PY'
95
+ import json
96
+ import os
97
+ try:
98
+ value = json.loads(os.environ.get("INPUT_JSON", "{}"))
99
+ except Exception:
100
+ value = {}
101
+
102
+ def pick_tool(payload):
103
+ if not isinstance(payload, dict):
104
+ return "unknown"
105
+ candidates = [
106
+ payload.get("tool_name"),
107
+ payload.get("tool"),
108
+ payload.get("toolName"),
109
+ payload.get("name"),
110
+ payload.get("id"),
111
+ payload.get("command")
112
+ ]
113
+ top_tool = payload.get("tool")
114
+ if isinstance(top_tool, dict):
115
+ candidates.extend([top_tool.get("name"), top_tool.get("id")])
116
+ nested = payload.get("input")
117
+ if isinstance(nested, dict):
118
+ candidates.extend([
119
+ nested.get("tool_name"),
120
+ nested.get("tool"),
121
+ nested.get("toolName"),
122
+ nested.get("name"),
123
+ nested.get("id"),
124
+ nested.get("command")
125
+ ])
126
+ nested_tool = nested.get("tool")
127
+ if isinstance(nested_tool, dict):
128
+ candidates.extend([nested_tool.get("name"), nested_tool.get("id")])
129
+ for candidate in candidates:
130
+ if isinstance(candidate, str) and candidate.strip():
131
+ return candidate.strip()
132
+ return "unknown"
133
+
134
+ print(pick_tool(value))
135
+ PY
136
+ )
137
+ CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json")
138
+ else
139
+ CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json")
140
+ fi
141
+ [ -n "$CCLAW_HOOK_PAYLOAD" ] || CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json")
142
+ [ -n "$CCLAW_HOOK_TOOL" ] || CCLAW_HOOK_TOOL="unknown"
143
+ }
144
+
145
+ cclaw_hook_read_flow_state_minimal() {
146
+ local flow_state_file="$1"
147
+ CCLAW_HOOK_FLOW_STAGE="none"
148
+ CCLAW_HOOK_FLOW_RUN_ID="active"
149
+ CCLAW_HOOK_FLOW_COMPLETED="0"
150
+ [ -f "$flow_state_file" ] || return 0
151
+
152
+ if command -v jq >/dev/null 2>&1; then
153
+ CCLAW_HOOK_FLOW_STAGE=$(jq -r '.currentStage // "none"' "$flow_state_file" 2>/dev/null || echo "none")
154
+ CCLAW_HOOK_FLOW_RUN_ID=$(jq -r '.activeRunId // "active"' "$flow_state_file" 2>/dev/null || echo "active")
155
+ CCLAW_HOOK_FLOW_COMPLETED=$(jq -r '(.completedStages // []) | length' "$flow_state_file" 2>/dev/null || echo "0")
156
+ return 0
157
+ fi
158
+
159
+ if command -v python3 >/dev/null 2>&1; then
160
+ local flow_meta
161
+ flow_meta=$(python3 - "$flow_state_file" <<'PY'
162
+ import json
163
+ import sys
164
+ stage = "none"
165
+ run_id = "active"
166
+ completed = 0
167
+ try:
168
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
169
+ payload = json.load(fh)
170
+ stage_value = payload.get("currentStage")
171
+ run_value = payload.get("activeRunId")
172
+ completed_value = payload.get("completedStages")
173
+ if isinstance(stage_value, str) and stage_value:
174
+ stage = stage_value
175
+ if isinstance(run_value, str) and run_value:
176
+ run_id = run_value
177
+ if isinstance(completed_value, list):
178
+ completed = len(completed_value)
179
+ except Exception:
180
+ pass
181
+ print(stage)
182
+ print(run_id)
183
+ print(completed)
184
+ PY
185
+ )
186
+ {
187
+ IFS= read -r CCLAW_HOOK_FLOW_STAGE
188
+ IFS= read -r CCLAW_HOOK_FLOW_RUN_ID
189
+ IFS= read -r CCLAW_HOOK_FLOW_COMPLETED
190
+ } <<EOF
191
+ $flow_meta
192
+ EOF
193
+ [ -n "$CCLAW_HOOK_FLOW_STAGE" ] || CCLAW_HOOK_FLOW_STAGE="none"
194
+ [ -n "$CCLAW_HOOK_FLOW_RUN_ID" ] || CCLAW_HOOK_FLOW_RUN_ID="active"
195
+ [ -n "$CCLAW_HOOK_FLOW_COMPLETED" ] || CCLAW_HOOK_FLOW_COMPLETED="0"
196
+ fi
197
+ }
198
+ `;
199
+ }
39
200
  export function sessionStartScript(_options = {}) {
40
201
  return `#!/usr/bin/env bash
41
202
  # cclaw session-start hook — generated by cclaw sync
42
203
  # Injects using-cclaw + flow status + active artifacts + compact knowledge digest + checkpoint/activity summary.
43
204
  set -euo pipefail
44
205
 
45
- ${DETECT_ROOT}
206
+ ${RUNTIME_SHELL_DETECT_ROOT}
46
207
 
47
208
  STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
48
209
  ACTIVE_FEATURE_FILE="$ROOT/${RUNTIME_ROOT}/state/active-feature.json"
49
210
  CHECKPOINT_FILE="$ROOT/${RUNTIME_ROOT}/state/checkpoint.json"
50
211
  ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
212
+ IRON_LAWS_FILE="$ROOT/${RUNTIME_ROOT}/state/iron-laws.json"
51
213
  SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
52
214
  CONTEXT_WARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/state/context-warnings.jsonl"
53
215
  CONTEXT_MODE_FILE="$ROOT/${RUNTIME_ROOT}/state/context-mode.json"
@@ -352,74 +514,99 @@ if [ -f "$META_SKILL" ]; then
352
514
  META_CONTENT=$(cat "$META_SKILL" 2>/dev/null || echo "")
353
515
  fi
354
516
 
355
- # --- Build compact knowledge digest (stage-biased, top entries only) ---
517
+ # --- Build compact knowledge digest (stage + branch + diff aware) ---
356
518
  KNOWLEDGE_DIGEST=""
357
519
  LEARNINGS_COUNT=0
358
520
  if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
359
521
  LEARNINGS_COUNT=$(grep -c '^{' "$KNOWLEDGE_FILE" 2>/dev/null || echo "0")
522
+ fi
523
+
524
+ if command -v cclaw >/dev/null 2>&1 && [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
525
+ BRANCH_NAME=""
526
+ if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
527
+ BRANCH_NAME=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
528
+ fi
529
+ DIFF_FILES_CSV=""
530
+ if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
531
+ DIFF_FILES_CSV=$(git -C "$ROOT" diff --name-only HEAD~5..HEAD 2>/dev/null | head -n 20 | tr '\n' ',' | sed 's/,$//' || echo "")
532
+ fi
533
+ OPEN_GATES_CSV=""
534
+ if [ -f "$STATE_FILE" ] && command -v jq >/dev/null 2>&1; then
535
+ OPEN_GATES_CSV=$(jq -r --arg stage "$STAGE" '
536
+ (.stageGateCatalog[$stage].required // [])
537
+ - (.stageGateCatalog[$stage].passed // [])
538
+ | join(",")
539
+ ' "$STATE_FILE" 2>/dev/null || echo "")
540
+ fi
541
+ DIGEST_CMD=(cclaw internal knowledge-digest --stage="$STAGE" --limit=8)
542
+ if [ -n "$BRANCH_NAME" ]; then
543
+ DIGEST_CMD+=("--branch=$BRANCH_NAME")
544
+ fi
545
+ if [ -n "$DIFF_FILES_CSV" ]; then
546
+ DIGEST_CMD+=("--diff-files=$DIFF_FILES_CSV")
547
+ fi
548
+ if [ -n "$OPEN_GATES_CSV" ]; then
549
+ DIGEST_CMD+=("--open-gates=$OPEN_GATES_CSV")
550
+ fi
551
+ KNOWLEDGE_DIGEST=$("\${DIGEST_CMD[@]}" 2>/dev/null || echo "")
552
+ fi
553
+
554
+ if [ -z "$KNOWLEDGE_DIGEST" ] && [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
360
555
  if command -v jq >/dev/null 2>&1; then
361
- KNOWLEDGE_DIGEST=$(tail -n 200 "$KNOWLEDGE_FILE" 2>/dev/null | jq -Rsc --arg stage "$STAGE" '
556
+ KNOWLEDGE_DIGEST=$(tail -n 120 "$KNOWLEDGE_FILE" 2>/dev/null | jq -Rsc --arg stage "$STAGE" '
362
557
  split("\\n")
363
558
  | map(select(length > 0))
364
559
  | map(try fromjson catch null)
365
560
  | map(select(type == "object"))
366
561
  | map(select((.stage // null) == $stage or (.stage // null) == null))
367
562
  | reverse
368
- | .[0:8]
563
+ | .[0:6]
369
564
  | map("- [" + ((.confidence // "unknown")|tostring) + " • " + ((.stage // "global")|tostring) + " • " + ((.domain // "general")|tostring) + "] " + ((.trigger // "trigger")|tostring) + " -> " + ((.action // "action")|tostring))
370
565
  | join("\\n")
371
566
  ' 2>/dev/null || echo "")
567
+ else
568
+ KNOWLEDGE_DIGEST=$(tail -n 6 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
569
+ fi
570
+ fi
571
+
572
+ if [ -n "$KNOWLEDGE_DIGEST" ]; then
573
+ printf '# Knowledge digest (auto-generated)\\n\\n%s\\n' "$KNOWLEDGE_DIGEST" > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
574
+ elif [ -f "$KNOWLEDGE_DIGEST_FILE" ]; then
575
+ printf '# Knowledge digest (auto-generated)\\n\\n(no matching entries for current stage)\\n' > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
576
+ fi
577
+
578
+ IRON_LAWS_SUMMARY=""
579
+ if [ -f "$IRON_LAWS_FILE" ]; then
580
+ if command -v jq >/dev/null 2>&1; then
581
+ IRON_LAWS_SUMMARY=$(jq -r '
582
+ (.laws // [])
583
+ | map("- [" + (if (.strict // false) then "strict" else "advisory" end) + "] " + ((.id // "law")|tostring) + " -> " + ((.rule // "")|tostring))
584
+ | .[0:6]
585
+ | join("\\n")
586
+ ' "$IRON_LAWS_FILE" 2>/dev/null || echo "")
372
587
  elif command -v python3 >/dev/null 2>&1; then
373
- KNOWLEDGE_DIGEST=$(python3 - "$KNOWLEDGE_FILE" "$STAGE" <<'PY'
588
+ IRON_LAWS_SUMMARY=$(python3 - "$IRON_LAWS_FILE" <<'PY'
374
589
  import json
375
590
  import sys
376
-
377
- path = sys.argv[1]
378
- stage = sys.argv[2]
379
- entries = []
591
+ out = []
380
592
  try:
381
- with open(path, "r", encoding="utf-8") as fh:
382
- lines = fh.readlines()[-200:]
383
- for raw in lines:
384
- raw = raw.strip()
385
- if not raw:
386
- continue
387
- try:
388
- obj = json.loads(raw)
389
- except Exception:
390
- continue
391
- if not isinstance(obj, dict):
392
- continue
393
- row_stage = obj.get("stage")
394
- if row_stage not in (stage, None):
593
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
594
+ parsed = json.load(fh)
595
+ for row in (parsed.get("laws") or [])[:6]:
596
+ if not isinstance(row, dict):
395
597
  continue
396
- entries.append(obj)
598
+ strict = "strict" if row.get("strict") else "advisory"
599
+ law_id = str(row.get("id") or "law")
600
+ rule = str(row.get("rule") or "")
601
+ out.append(f"- [{strict}] {law_id} -> {rule}")
397
602
  except Exception:
398
- entries = []
399
-
400
- entries = list(reversed(entries))[:8]
401
- out = []
402
- for obj in entries:
403
- conf = str(obj.get("confidence", "unknown"))
404
- row_stage = str(obj.get("stage", "global"))
405
- domain = str(obj.get("domain", "general"))
406
- trigger = str(obj.get("trigger", "trigger"))
407
- action = str(obj.get("action", "action"))
408
- out.append(f"- [{conf} • {row_stage} • {domain}] {trigger} -> {action}")
603
+ out = []
409
604
  print("\\n".join(out))
410
605
  PY
411
606
  )
412
- else
413
- KNOWLEDGE_DIGEST=$(tail -n 8 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
414
607
  fi
415
608
  fi
416
609
 
417
- if [ -n "$KNOWLEDGE_DIGEST" ]; then
418
- printf '# Knowledge digest (auto-generated)\\n\\n%s\\n' "$KNOWLEDGE_DIGEST" > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
419
- elif [ -f "$KNOWLEDGE_DIGEST_FILE" ]; then
420
- printf '# Knowledge digest (auto-generated)\\n\\n(no matching entries for current stage)\\n' > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
421
- fi
422
-
423
610
  # --- Installed cclaw-cli version vs. project's recorded version (one-block
424
611
  # upgrade-check, gstack-style). Purely informational — we never block. ---
425
612
  VERSION_NOTE=""
@@ -503,6 +690,11 @@ if [ -n "$KNOWLEDGE_DIGEST" ]; then
503
690
  Knowledge digest (top relevant entries):
504
691
  $KNOWLEDGE_DIGEST"
505
692
  fi
693
+ if [ -n "$IRON_LAWS_SUMMARY" ]; then
694
+ CTX="$CTX
695
+ Iron laws (enforced policy highlights):
696
+ $IRON_LAWS_SUMMARY"
697
+ fi
506
698
  if [ -n "$META_CONTENT" ]; then
507
699
  CTX="$CTX
508
700
 
@@ -536,7 +728,7 @@ export function stopCheckpointScript() {
536
728
  # Writes checkpoint state and reminds agent about flow/session consistency.
537
729
  set -euo pipefail
538
730
 
539
- ${DETECT_ROOT}
731
+ ${RUNTIME_SHELL_DETECT_ROOT}
540
732
 
541
733
  INPUT=$(cat 2>/dev/null || echo '{}')
542
734
 
@@ -545,6 +737,7 @@ STATE_FILE="$STATE_DIR/flow-state.json"
545
737
  CHECKPOINT_FILE="$STATE_DIR/checkpoint.json"
546
738
  CHECKPOINT_TMP="$STATE_DIR/checkpoint.json.tmp.$$"
547
739
  CHECKPOINT_LOCK_DIR="$STATE_DIR/.checkpoint.lock"
740
+ IRON_LAWS_FILE="$STATE_DIR/iron-laws.json"
548
741
  STAGE="none"
549
742
  ACTIVE_RUN="none"
550
743
  LOOP_COUNT=""
@@ -617,6 +810,38 @@ if command -v git >/dev/null 2>&1; then
617
810
  fi
618
811
  fi
619
812
 
813
+ STRICT_STOP_DIRTY="false"
814
+ if [ -f "$IRON_LAWS_FILE" ]; then
815
+ if command -v jq >/dev/null 2>&1; then
816
+ STRICT_STOP_DIRTY=$(jq -r '
817
+ if (.mode // "advisory") == "strict" then "true"
818
+ elif ((.laws // []) | any(.id == "stop-clean-or-checkpointed" and .strict == true)) then "true"
819
+ else "false"
820
+ end
821
+ ' "$IRON_LAWS_FILE" 2>/dev/null || echo "false")
822
+ elif command -v python3 >/dev/null 2>&1; then
823
+ STRICT_STOP_DIRTY=$(python3 - "$IRON_LAWS_FILE" <<'PY'
824
+ import json
825
+ import sys
826
+ value = "false"
827
+ try:
828
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
829
+ parsed = json.load(fh)
830
+ if str(parsed.get("mode", "advisory")) == "strict":
831
+ value = "true"
832
+ else:
833
+ for row in parsed.get("laws", []):
834
+ if isinstance(row, dict) and row.get("id") == "stop-clean-or-checkpointed" and row.get("strict") is True:
835
+ value = "true"
836
+ break
837
+ except Exception:
838
+ value = "false"
839
+ print(value)
840
+ PY
841
+ )
842
+ fi
843
+ fi
844
+
620
845
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
621
846
  mkdir -p "$STATE_DIR" 2>/dev/null || true
622
847
  CHECKPOINT_WRITTEN=0
@@ -742,6 +967,11 @@ if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
742
967
  CHECKPOINT_NOTE="Checkpoint update failed. Review ${RUNTIME_ROOT}/state/checkpoint.json manually."
743
968
  fi
744
969
 
970
+ if [ "$DIRTY_STATE" = "dirty" ] && [ "$STRICT_STOP_DIRTY" = "true" ]; then
971
+ printf '[cclaw] Stop blocked by iron law "stop-clean-or-checkpointed": working tree is dirty. Commit/revert changes or update checkpoint blockers before ending the session.\\n' >&2
972
+ exit 1
973
+ fi
974
+
745
975
  RUN_SYNC_NOTE="Run metadata sync removed; active artifacts stay in ${RUNTIME_ROOT}/artifacts until /cc-ops archive (or cclaw archive runtime)."
746
976
 
747
977
  # --- Escape for JSON ---
@@ -775,7 +1005,7 @@ export function runHookDispatcherScript() {
775
1005
  # Single entrypoint used by harness hook JSON wiring.
776
1006
  set -euo pipefail
777
1007
 
778
- ${DETECT_ROOT}
1008
+ ${RUNTIME_SHELL_DETECT_ROOT}
779
1009
 
780
1010
  if [ "$#" -lt 1 ]; then
781
1011
  printf 'Usage: bash ${RUNTIME_ROOT}/hooks/run-hook.cmd <session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor>\\n' >&2
@@ -826,7 +1056,7 @@ export function stageCompleteScript() {
826
1056
  # mutation to \`cclaw internal advance-stage\`.
827
1057
  set -euo pipefail
828
1058
 
829
- ${DETECT_ROOT}
1059
+ ${RUNTIME_SHELL_DETECT_ROOT}
830
1060
 
831
1061
  if [ "$#" -lt 1 ]; then
832
1062
  printf 'Usage: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...]\\n' >&2
@@ -857,9 +1087,7 @@ export function preCompactScript() {
857
1087
  # having to re-derive it from scratch.
858
1088
  set -uo pipefail
859
1089
 
860
- ${DETECT_ROOT}
861
-
862
- INPUT=$(cat 2>/dev/null || echo '{}')
1090
+ ${RUNTIME_SHELL_DETECT_ROOT}
863
1091
 
864
1092
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
865
1093
  STATE_FILE="$STATE_DIR/flow-state.json"
@@ -1004,27 +1232,3 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
1004
1232
  // OpenCode plugin — JS module
1005
1233
  // ---------------------------------------------------------------------------
1006
1234
  export { opencodePluginJs } from "./opencode-plugin.js";
1007
- // ---------------------------------------------------------------------------
1008
- // AGENTS.md block for hooks
1009
- // ---------------------------------------------------------------------------
1010
- export function hooksAgentsMdBlock() {
1011
- return `### Hooks (real lifecycle integration)
1012
-
1013
- Cclaw generates real hook integrations for every harness that exposes a
1014
- hook primitive:
1015
- - **Claude/Cursor:** lifecycle rehydration + PreToolUse/PostToolUse + Stop
1016
- - **OpenCode:** session lifecycle + system transform rehydration + bootstrap parity (digest/warnings/knowledge snapshot)
1017
- - **Codex:** Codex CLI ≥ v0.114 exposes lifecycle hooks at \`.codex/hooks.json\`, gated behind \`[features] codex_hooks = true\` in \`~/.codex/config.toml\`. \`PreToolUse\`/\`PostToolUse\` intercept **only the \`Bash\` tool** in Codex; \`Write\`/\`Edit\`/\`WebSearch\`/MCP calls are substituted via the \`/cc\` skill bodies under \`.agents/skills/cc*/SKILL.md\` and explicit in-turn agent steps. See \`.cclaw/references/harnesses/codex-playbook.md\` for the coverage matrix.
1018
-
1019
- | Harness | Hook file | Events |
1020
- |---------|-----------|--------|
1021
- | Claude Code | \`.claude/hooks/hooks.json\` | SessionStart(startup/resume/clear/compact), PreToolUse, PostToolUse, Stop |
1022
- | Cursor | \`.cursor/hooks.json\` | sessionStart/sessionResume/sessionClear/sessionCompact, preToolUse, postToolUse, stop |
1023
- | OpenCode | \`${RUNTIME_ROOT}/hooks/opencode-plugin.mjs\` | session.created/updated/resumed/cleared/compacted/idle, tool.execute.before/after, system transform |
1024
- | Codex | \`.codex/hooks.json\` | SessionStart(startup/resume), UserPromptSubmit, PreToolUse(Bash), PostToolUse(Bash), Stop (feature-gated by \`codex_hooks = true\`) |
1025
-
1026
- Hook state files:
1027
- - \`${RUNTIME_ROOT}/state/stage-activity.jsonl\`
1028
- - \`${RUNTIME_ROOT}/state/checkpoint.json\`
1029
- `;
1030
- }