cclaw-cli 0.48.6 → 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.
@@ -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,34 +15,195 @@ 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"
@@ -567,7 +728,7 @@ export function stopCheckpointScript() {
567
728
  # Writes checkpoint state and reminds agent about flow/session consistency.
568
729
  set -euo pipefail
569
730
 
570
- ${DETECT_ROOT}
731
+ ${RUNTIME_SHELL_DETECT_ROOT}
571
732
 
572
733
  INPUT=$(cat 2>/dev/null || echo '{}')
573
734
 
@@ -844,7 +1005,7 @@ export function runHookDispatcherScript() {
844
1005
  # Single entrypoint used by harness hook JSON wiring.
845
1006
  set -euo pipefail
846
1007
 
847
- ${DETECT_ROOT}
1008
+ ${RUNTIME_SHELL_DETECT_ROOT}
848
1009
 
849
1010
  if [ "$#" -lt 1 ]; then
850
1011
  printf 'Usage: bash ${RUNTIME_ROOT}/hooks/run-hook.cmd <session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor>\\n' >&2
@@ -895,7 +1056,7 @@ export function stageCompleteScript() {
895
1056
  # mutation to \`cclaw internal advance-stage\`.
896
1057
  set -euo pipefail
897
1058
 
898
- ${DETECT_ROOT}
1059
+ ${RUNTIME_SHELL_DETECT_ROOT}
899
1060
 
900
1061
  if [ "$#" -lt 1 ]; then
901
1062
  printf 'Usage: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...]\\n' >&2
@@ -926,9 +1087,7 @@ export function preCompactScript() {
926
1087
  # having to re-derive it from scratch.
927
1088
  set -uo pipefail
928
1089
 
929
- ${DETECT_ROOT}
930
-
931
- INPUT=$(cat 2>/dev/null || echo '{}')
1090
+ ${RUNTIME_SHELL_DETECT_ROOT}
932
1091
 
933
1092
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
934
1093
  STATE_FILE="$STATE_DIR/flow-state.json"
@@ -1073,27 +1232,3 @@ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
1073
1232
  // OpenCode plugin — JS module
1074
1233
  // ---------------------------------------------------------------------------
1075
1234
  export { opencodePluginJs } from "./opencode-plugin.js";
1076
- // ---------------------------------------------------------------------------
1077
- // AGENTS.md block for hooks
1078
- // ---------------------------------------------------------------------------
1079
- export function hooksAgentsMdBlock() {
1080
- return `### Hooks (real lifecycle integration)
1081
-
1082
- Cclaw generates real hook integrations for every harness that exposes a
1083
- hook primitive:
1084
- - **Claude/Cursor:** lifecycle rehydration + PreToolUse/PostToolUse + Stop
1085
- - **OpenCode:** session lifecycle + system transform rehydration + bootstrap parity (digest/warnings/knowledge snapshot)
1086
- - **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.
1087
-
1088
- | Harness | Hook file | Events |
1089
- |---------|-----------|--------|
1090
- | Claude Code | \`.claude/hooks/hooks.json\` | SessionStart(startup/resume/clear/compact), PreToolUse, PostToolUse, Stop |
1091
- | Cursor | \`.cursor/hooks.json\` | sessionStart/sessionResume/sessionClear/sessionCompact, preToolUse, postToolUse, stop |
1092
- | OpenCode | \`${RUNTIME_ROOT}/hooks/opencode-plugin.mjs\` | session.created/updated/resumed/cleared/compacted/idle, tool.execute.before/after, system transform |
1093
- | Codex | \`.codex/hooks.json\` | SessionStart(startup/resume), UserPromptSubmit, PreToolUse(Bash), PostToolUse(Bash), Stop (feature-gated by \`codex_hooks = true\`) |
1094
-
1095
- Hook state files:
1096
- - \`${RUNTIME_ROOT}/state/stage-activity.jsonl\`
1097
- - \`${RUNTIME_ROOT}/state/checkpoint.json\`
1098
- `;
1099
- }
@@ -98,6 +98,14 @@ export declare const IRON_LAWS: readonly [{
98
98
  readonly enforcement: "PreToolUse";
99
99
  readonly severity: "hard-gate";
100
100
  readonly appliesTo: ["ship"];
101
+ }, {
102
+ readonly id: "review-coverage-complete-before-ship";
103
+ readonly title: "Review layer coverage before ship";
104
+ readonly rule: "Block ship finalization when review-army does not confirm full Layer 1/2 coverage map.";
105
+ readonly rationale: "Prevents finalization when multi-pass review evidence is incomplete or partially missing.";
106
+ readonly enforcement: "PreToolUse";
107
+ readonly severity: "hard-gate";
108
+ readonly appliesTo: ["ship"];
101
109
  }, {
102
110
  readonly id: "subagent-task-self-contained";
103
111
  readonly title: "Subagent tasks are self-contained";
@@ -71,6 +71,15 @@ export const IRON_LAWS = [
71
71
  severity: "hard-gate",
72
72
  appliesTo: ["ship"]
73
73
  },
74
+ {
75
+ id: "review-coverage-complete-before-ship",
76
+ title: "Review layer coverage before ship",
77
+ rule: "Block ship finalization when review-army does not confirm full Layer 1/2 coverage map.",
78
+ rationale: "Prevents finalization when multi-pass review evidence is incomplete or partially missing.",
79
+ enforcement: "PreToolUse",
80
+ severity: "hard-gate",
81
+ appliesTo: ["ship"]
82
+ },
74
83
  {
75
84
  id: "subagent-task-self-contained",
76
85
  title: "Subagent tasks are self-contained",
@@ -16,15 +16,6 @@ set -uo pipefail
16
16
  shopt -s globstar 2>/dev/null || true
17
17
  PROMPT_GUARD_MODE="${promptGuardMode}"
18
18
 
19
- HARNESS="codex"
20
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
21
- HARNESS="claude"
22
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
23
- HARNESS="cursor"
24
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
25
- HARNESS="opencode"
26
- fi
27
-
28
19
  ${RUNTIME_SHELL_DETECT_ROOT}
29
20
 
30
21
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
@@ -35,8 +26,12 @@ INPUT=$(cat 2>/dev/null || echo '{}')
35
26
  [ -n "$INPUT" ] || exit 0
36
27
 
37
28
  TOOL="unknown"
38
- PAYLOAD=""
39
- if command -v jq >/dev/null 2>&1; then
29
+ PAYLOAD="$INPUT"
30
+ if command -v cclaw_hook_extract_tool_and_payload >/dev/null 2>&1; then
31
+ cclaw_hook_extract_tool_and_payload "$INPUT"
32
+ TOOL="\${CCLAW_HOOK_TOOL:-unknown}"
33
+ PAYLOAD="\${CCLAW_HOOK_PAYLOAD:-$INPUT}"
34
+ elif command -v jq >/dev/null 2>&1; then
40
35
  TOOL=$(printf '%s' "$INPUT" | 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")
41
36
  PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
42
37
  elif command -v python3 >/dev/null 2>&1; then
@@ -93,8 +88,13 @@ if [ -z "$PAYLOAD" ]; then
93
88
  PAYLOAD=$(printf '%s' "$INPUT")
94
89
  fi
95
90
 
96
- PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
97
- TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
91
+ if command -v cclaw_hook_lower >/dev/null 2>&1; then
92
+ PAYLOAD_LOWER=$(cclaw_hook_lower "$PAYLOAD")
93
+ TOOL_LOWER=$(cclaw_hook_lower "$TOOL")
94
+ else
95
+ PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
96
+ TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
97
+ fi
98
98
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
99
99
  REASONS=""
100
100
 
@@ -180,6 +180,7 @@ STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
180
180
  FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
181
181
  TDD_LOG_FILE="$STATE_DIR/tdd-cycle-log.jsonl"
182
182
  IRON_LAWS_FILE="$STATE_DIR/iron-laws.json"
183
+ REVIEW_ARMY_FILE="$ROOT/${RUNTIME_ROOT}/artifacts/07-review-army.json"
183
184
  GUARD_STATE_FILE="$STATE_DIR/workflow-guard.json"
184
185
  GUARD_LOG="$STATE_DIR/workflow-guard.jsonl"
185
186
  mkdir -p "$STATE_DIR" 2>/dev/null || true
@@ -188,8 +189,12 @@ INPUT=$(cat 2>/dev/null || echo '{}')
188
189
  [ -n "$INPUT" ] || exit 0
189
190
 
190
191
  TOOL="unknown"
191
- PAYLOAD=""
192
- if command -v jq >/dev/null 2>&1; then
192
+ PAYLOAD="$INPUT"
193
+ if command -v cclaw_hook_extract_tool_and_payload >/dev/null 2>&1; then
194
+ cclaw_hook_extract_tool_and_payload "$INPUT"
195
+ TOOL="\${CCLAW_HOOK_TOOL:-unknown}"
196
+ PAYLOAD="\${CCLAW_HOOK_PAYLOAD:-$INPUT}"
197
+ elif command -v jq >/dev/null 2>&1; then
193
198
  TOOL=$(printf '%s' "$INPUT" | 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")
194
199
  PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
195
200
  elif command -v python3 >/dev/null 2>&1; then
@@ -242,8 +247,13 @@ else
242
247
  fi
243
248
 
244
249
  [ -n "$PAYLOAD" ] || PAYLOAD=$(printf '%s' "$INPUT")
245
- TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
246
- PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
250
+ if command -v cclaw_hook_lower >/dev/null 2>&1; then
251
+ TOOL_LOWER=$(cclaw_hook_lower "$TOOL")
252
+ PAYLOAD_LOWER=$(cclaw_hook_lower "$PAYLOAD")
253
+ else
254
+ TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
255
+ PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
256
+ fi
247
257
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
248
258
  NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
249
259
  REASONS=""
@@ -254,11 +264,19 @@ if [ -z "$ACTIVE_AGENT" ]; then
254
264
  ACTIVE_AGENT=$(printf '%s' "$INPUT" | jq -r '.agent_name // .agent // .input.agent_name // .input.agent // .tool_input.agent_name // .tool_input.agent // ""' 2>/dev/null || echo "")
255
265
  fi
256
266
  fi
257
- ACTIVE_AGENT_LOWER=$(printf '%s' "$ACTIVE_AGENT" | tr '[:upper:]' '[:lower:]')
267
+ if command -v cclaw_hook_lower >/dev/null 2>&1; then
268
+ ACTIVE_AGENT_LOWER=$(cclaw_hook_lower "$ACTIVE_AGENT")
269
+ else
270
+ ACTIVE_AGENT_LOWER=$(printf '%s' "$ACTIVE_AGENT" | tr '[:upper:]' '[:lower:]')
271
+ fi
258
272
 
259
273
  CURRENT_STAGE="none"
260
274
  CURRENT_RUN="active"
261
- if [ -f "$FLOW_STATE_FILE" ]; then
275
+ if command -v cclaw_hook_read_flow_state_minimal >/dev/null 2>&1; then
276
+ cclaw_hook_read_flow_state_minimal "$FLOW_STATE_FILE"
277
+ CURRENT_STAGE="\${CCLAW_HOOK_FLOW_STAGE:-none}"
278
+ CURRENT_RUN="\${CCLAW_HOOK_FLOW_RUN_ID:-active}"
279
+ elif [ -f "$FLOW_STATE_FILE" ]; then
262
280
  if command -v jq >/dev/null 2>&1; then
263
281
  CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
264
282
  CURRENT_RUN=$(jq -r '.activeRunId // "active"' "$FLOW_STATE_FILE" 2>/dev/null || echo "active")
@@ -674,6 +692,62 @@ is_tdd_production_write_payload() {
674
692
  return 0
675
693
  }
676
694
 
695
+ collect_tdd_production_paths() {
696
+ local payload_paths="$1"
697
+ local out=""
698
+ [ -n "$payload_paths" ] || {
699
+ printf ''
700
+ return 0
701
+ }
702
+ while IFS= read -r raw_path; do
703
+ [ -n "$raw_path" ] || continue
704
+ local normalized
705
+ normalized=$(normalize_payload_path "$raw_path")
706
+ if is_tdd_production_path "$normalized"; then
707
+ if [ -n "$out" ]; then
708
+ out="$out"$'\n'"$raw_path"
709
+ else
710
+ out="$raw_path"
711
+ fi
712
+ fi
713
+ done <<< "$payload_paths"
714
+ printf '%s' "$out"
715
+ }
716
+
717
+ review_layer_coverage_complete() {
718
+ if [ ! -f "$REVIEW_ARMY_FILE" ]; then
719
+ return 1
720
+ fi
721
+ if command -v jq >/dev/null 2>&1; then
722
+ jq -e '
723
+ ((.reconciliation.layerCoverage.spec // false) == true) and
724
+ ((.reconciliation.layerCoverage.correctness // false) == true) and
725
+ ((.reconciliation.layerCoverage.security // false) == true) and
726
+ ((.reconciliation.layerCoverage.performance // false) == true) and
727
+ ((.reconciliation.layerCoverage.architecture // false) == true) and
728
+ ((.reconciliation.layerCoverage["external-safety"] // false) == true)
729
+ ' "$REVIEW_ARMY_FILE" >/dev/null 2>&1
730
+ return $?
731
+ fi
732
+ if command -v python3 >/dev/null 2>&1; then
733
+ python3 - "$REVIEW_ARMY_FILE" <<'PY'
734
+ import json
735
+ import sys
736
+ keys = ["spec", "correctness", "security", "performance", "architecture", "external-safety"]
737
+ try:
738
+ with open(sys.argv[1], "r", encoding="utf-8") as handle:
739
+ parsed = json.load(handle)
740
+ coverage = ((parsed.get("reconciliation") or {}).get("layerCoverage") or {})
741
+ ok = all(coverage.get(key) is True for key in keys)
742
+ except Exception:
743
+ ok = False
744
+ raise SystemExit(0 if ok else 1)
745
+ PY
746
+ return $?
747
+ fi
748
+ return 1
749
+ }
750
+
677
751
  tdd_cycle_counts() {
678
752
  if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
679
753
  printf '0:0'
@@ -883,54 +957,100 @@ if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"
883
957
  fi
884
958
 
885
959
  if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
960
+ TDD_MISSING_RED_PATHS=""
886
961
  if is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
887
- if has_open_red_cycle; then
888
- TDD_CYCLE_STATE="red_open"
889
- else
890
- OPEN_RED_STATUS=$?
891
- if [ "$OPEN_RED_STATUS" -eq 2 ]; then
892
- TDD_CYCLE_STATE="counts_unavailable"
962
+ PRODUCTION_PATHS=$(collect_tdd_production_paths "$MUTATION_PATHS")
963
+ PER_PATH_RED_CHECKED="false"
964
+ if [ -n "$PRODUCTION_PATHS" ] && command -v cclaw >/dev/null 2>&1; then
965
+ PER_PATH_RED_CHECKED="true"
966
+ while IFS= read -r production_path; do
967
+ [ -n "$production_path" ] || continue
968
+ cclaw internal tdd-red-evidence --path="$production_path" --run-id="$CURRENT_RUN" --quiet >/dev/null 2>&1
969
+ EVIDENCE_STATUS=$?
970
+ if [ "$EVIDENCE_STATUS" -eq 0 ]; then
971
+ continue
972
+ fi
973
+ if [ "$EVIDENCE_STATUS" -eq 2 ]; then
974
+ if [ -n "$TDD_MISSING_RED_PATHS" ]; then
975
+ TDD_MISSING_RED_PATHS="$TDD_MISSING_RED_PATHS, $production_path"
976
+ else
977
+ TDD_MISSING_RED_PATHS="$production_path"
978
+ fi
979
+ continue
980
+ fi
893
981
  if [ -n "$REASONS" ]; then
894
- REASONS="$REASONS,tdd_cycle_counts_unavailable"
982
+ REASONS="$REASONS,tdd_red_evidence_check_failed"
895
983
  else
896
- REASONS="tdd_cycle_counts_unavailable"
984
+ REASONS="tdd_red_evidence_check_failed"
985
+ fi
986
+ done <<< "$PRODUCTION_PATHS"
987
+ if [ -n "$TDD_MISSING_RED_PATHS" ]; then
988
+ if [ -n "$REASONS" ]; then
989
+ REASONS="$REASONS,tdd_write_without_red_for_path"
990
+ else
991
+ REASONS="tdd_write_without_red_for_path"
897
992
  fi
898
- else
899
- TDD_CYCLE_STATE=$(tdd_cycle_state)
900
993
  fi
901
994
  fi
902
- if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
903
- if [ -n "$REASONS" ]; then
904
- REASONS="$REASONS,tdd_write_without_open_red"
995
+ if [ "$PER_PATH_RED_CHECKED" != "true" ]; then
996
+ if has_open_red_cycle; then
997
+ TDD_CYCLE_STATE="red_open"
905
998
  else
906
- REASONS="tdd_write_without_open_red"
999
+ OPEN_RED_STATUS=$?
1000
+ if [ "$OPEN_RED_STATUS" -eq 2 ]; then
1001
+ TDD_CYCLE_STATE="counts_unavailable"
1002
+ if [ -n "$REASONS" ]; then
1003
+ REASONS="$REASONS,tdd_cycle_counts_unavailable"
1004
+ else
1005
+ REASONS="tdd_cycle_counts_unavailable"
1006
+ fi
1007
+ else
1008
+ TDD_CYCLE_STATE=$(tdd_cycle_state)
1009
+ fi
907
1010
  fi
908
- elif [ "$TDD_CYCLE_STATE" = "__UNAVAILABLE__" ]; then
909
- if [ -n "$REASONS" ]; then
910
- REASONS="$REASONS,tdd_cycle_counts_unavailable"
911
- else
912
- REASONS="tdd_cycle_counts_unavailable"
1011
+ if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
1012
+ if [ -n "$REASONS" ]; then
1013
+ REASONS="$REASONS,tdd_write_without_open_red"
1014
+ else
1015
+ REASONS="tdd_write_without_open_red"
1016
+ fi
1017
+ elif [ "$TDD_CYCLE_STATE" = "__UNAVAILABLE__" ]; then
1018
+ if [ -n "$REASONS" ]; then
1019
+ REASONS="$REASONS,tdd_cycle_counts_unavailable"
1020
+ else
1021
+ REASONS="tdd_cycle_counts_unavailable"
1022
+ fi
913
1023
  fi
914
1024
  fi
915
1025
  fi
916
1026
  fi
917
1027
 
918
1028
  if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
919
- if [ "$ACTIVE_AGENT_LOWER" = "tdd-red" ] && is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
1029
+ ACTIVE_AGENT_EFFECTIVE="$ACTIVE_AGENT_LOWER"
1030
+ if [ -z "$ACTIVE_AGENT_EFFECTIVE" ]; then
1031
+ INFERRED_TDD_PHASE=$(tdd_cycle_state)
1032
+ case "$INFERRED_TDD_PHASE" in
1033
+ need_red) ACTIVE_AGENT_EFFECTIVE="tdd-red" ;;
1034
+ red_open) ACTIVE_AGENT_EFFECTIVE="tdd-green" ;;
1035
+ green_done) ACTIVE_AGENT_EFFECTIVE="tdd-refactor" ;;
1036
+ *) ACTIVE_AGENT_EFFECTIVE="" ;;
1037
+ esac
1038
+ fi
1039
+ if [ "$ACTIVE_AGENT_EFFECTIVE" = "tdd-red" ] && is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
920
1040
  if [ -n "$REASONS" ]; then
921
1041
  REASONS="$REASONS,tdd_red_agent_cannot_write_production"
922
1042
  else
923
1043
  REASONS="tdd_red_agent_cannot_write_production"
924
1044
  fi
925
1045
  fi
926
- if [ "$ACTIVE_AGENT_LOWER" = "tdd-green" ] && is_tdd_test_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
1046
+ if [ "$ACTIVE_AGENT_EFFECTIVE" = "tdd-green" ] && is_tdd_test_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
927
1047
  if [ -n "$REASONS" ]; then
928
1048
  REASONS="$REASONS,tdd_green_agent_cannot_write_tests"
929
1049
  else
930
1050
  REASONS="tdd_green_agent_cannot_write_tests"
931
1051
  fi
932
1052
  fi
933
- if [ "$ACTIVE_AGENT_LOWER" = "tdd-refactor" ]; then
1053
+ if [ "$ACTIVE_AGENT_EFFECTIVE" = "tdd-refactor" ]; then
934
1054
  TDD_AGENT_STATE=$(tdd_cycle_state)
935
1055
  if [ "$TDD_AGENT_STATE" != "green_done" ]; then
936
1056
  if [ -n "$REASONS" ]; then
@@ -971,6 +1091,13 @@ if [ "$CURRENT_STAGE" = "ship" ] && is_execution_or_mutating_tool "$TOOL_LOWER";
971
1091
  REASONS="ship_preflight_required"
972
1092
  fi
973
1093
  fi
1094
+ if ! review_layer_coverage_complete; then
1095
+ if [ -n "$REASONS" ]; then
1096
+ REASONS="$REASONS,ship_review_coverage_required"
1097
+ else
1098
+ REASONS="ship_review_coverage_required"
1099
+ fi
1100
+ fi
974
1101
  fi
975
1102
  fi
976
1103
 
@@ -1039,16 +1166,22 @@ PY
1039
1166
  fi
1040
1167
 
1041
1168
  if [ -n "$REASONS" ]; then
1042
- if printf '%s' "$REASONS" | grep -Eq 'tdd_red_agent_cannot_write_production'; then
1169
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_red_for_path'; then
1170
+ NOTE="Cclaw workflow guard: missing failing RED evidence for production path(s): \${TDD_MISSING_RED_PATHS:-unknown}. Log failing tests before touching these files."
1171
+ elif printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; then
1172
+ NOTE="Cclaw workflow guard: Write a failing test first before editing production files during tdd stage (state=\${TDD_CYCLE_STATE})."
1173
+ elif printf '%s' "$REASONS" | grep -Eq 'tdd_red_evidence_check_failed'; then
1174
+ NOTE="Cclaw workflow guard: failed to validate per-path RED evidence via \`cclaw internal tdd-red-evidence\`; refusing write until evidence check succeeds."
1175
+ elif printf '%s' "$REASONS" | grep -Eq 'tdd_red_agent_cannot_write_production'; then
1043
1176
  NOTE="Cclaw workflow guard: tdd-red agent is limited to test-side RED work and cannot edit production files."
1044
1177
  elif printf '%s' "$REASONS" | grep -Eq 'tdd_green_agent_cannot_write_tests'; then
1045
1178
  NOTE="Cclaw workflow guard: tdd-green agent can implement production fixes but should not author new RED tests."
1046
1179
  elif printf '%s' "$REASONS" | grep -Eq 'tdd_refactor_before_green'; then
1047
1180
  NOTE="Cclaw workflow guard: tdd-refactor requires a green_done cycle state before refactor edits."
1048
- elif printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; then
1049
- NOTE="Cclaw workflow guard: Write a failing test first before editing production files during tdd stage (state=\${TDD_CYCLE_STATE})."
1050
1181
  elif printf '%s' "$REASONS" | grep -Eq 'ship_preflight_required'; then
1051
1182
  NOTE="Cclaw workflow guard: ship finalization command detected before ship_preflight_passed gate. Run preflight and record evidence first."
1183
+ elif printf '%s' "$REASONS" | grep -Eq 'ship_review_coverage_required'; then
1184
+ NOTE="Cclaw workflow guard: ship finalization requires review layer coverage for spec/correctness/security/performance/architecture/external-safety in 07-review-army.json."
1052
1185
  elif printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
1053
1186
  NOTE="Cclaw workflow guard: unable to inspect run-scoped tdd-cycle counts (missing usable jq/python3/awk). Install one of these tools before writing production code in tdd."
1054
1187
  elif printf '%s' "$REASONS" | grep -Eq 'runtime_write_requires_managed_only|direct_flow_state_edit'; then
@@ -1077,10 +1210,10 @@ if [ -n "$REASONS" ]; then
1077
1210
  if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_'; then
1078
1211
  SHOULD_BLOCK="true"
1079
1212
  fi
1080
- if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
1213
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red|tdd_write_without_red_for_path' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
1081
1214
  SHOULD_BLOCK="true"
1082
1215
  fi
1083
- if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && iron_law_is_strict "tdd-red-before-write"; then
1216
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red|tdd_write_without_red_for_path' && iron_law_is_strict "tdd-red-before-write"; then
1084
1217
  SHOULD_BLOCK="true"
1085
1218
  fi
1086
1219
  if printf '%s' "$REASONS" | grep -Eq 'runtime_write_requires_managed_only|direct_flow_state_edit' && iron_law_is_strict "runtime-writes-managed-only"; then
@@ -1092,10 +1225,13 @@ if [ -n "$REASONS" ]; then
1092
1225
  if printf '%s' "$REASONS" | grep -Eq 'ship_preflight_required' && iron_law_is_strict "ship-preflight-required"; then
1093
1226
  SHOULD_BLOCK="true"
1094
1227
  fi
1228
+ if printf '%s' "$REASONS" | grep -Eq 'ship_review_coverage_required' && iron_law_is_strict "review-coverage-complete-before-ship"; then
1229
+ SHOULD_BLOCK="true"
1230
+ fi
1095
1231
  if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_plan_completion' && iron_law_is_strict "plan-requires-approval"; then
1096
1232
  SHOULD_BLOCK="true"
1097
1233
  fi
1098
- if printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
1234
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable|tdd_red_evidence_check_failed'; then
1099
1235
  SHOULD_BLOCK="true"
1100
1236
  fi
1101
1237
  if printf '%s' "$REASONS" | grep -Eq 'tdd_red_agent_cannot_write_production|tdd_green_agent_cannot_write_tests|tdd_refactor_before_green'; then
@@ -1117,25 +1253,214 @@ export function contextMonitorScript() {
1117
1253
  # Advisory-only context pressure warnings (best effort).
1118
1254
  set -uo pipefail
1119
1255
 
1120
- HARNESS="codex"
1121
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
1122
- HARNESS="claude"
1123
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
1124
- HARNESS="cursor"
1125
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
1126
- HARNESS="opencode"
1127
- fi
1128
-
1129
1256
  ${RUNTIME_SHELL_DETECT_ROOT}
1130
1257
 
1131
1258
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
1132
1259
  MONITOR_STATE="$STATE_DIR/context-monitor.json"
1133
1260
  WARNINGS_FILE="$STATE_DIR/context-warnings.jsonl"
1261
+ FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
1262
+ TDD_AUTO_EVIDENCE_FILE="$STATE_DIR/tdd-red-evidence.jsonl"
1134
1263
  mkdir -p "$STATE_DIR" 2>/dev/null || true
1135
1264
 
1136
1265
  INPUT=$(cat 2>/dev/null || echo '{}')
1137
1266
  [ -n "$INPUT" ] || exit 0
1138
1267
 
1268
+ CURRENT_STAGE="none"
1269
+ CURRENT_RUN="active"
1270
+ if command -v cclaw_hook_read_flow_state_minimal >/dev/null 2>&1; then
1271
+ cclaw_hook_read_flow_state_minimal "$FLOW_STATE_FILE"
1272
+ CURRENT_STAGE="\${CCLAW_HOOK_FLOW_STAGE:-none}"
1273
+ CURRENT_RUN="\${CCLAW_HOOK_FLOW_RUN_ID:-active}"
1274
+ elif [ -f "$FLOW_STATE_FILE" ]; then
1275
+ if command -v jq >/dev/null 2>&1; then
1276
+ CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
1277
+ CURRENT_RUN=$(jq -r '.activeRunId // "active"' "$FLOW_STATE_FILE" 2>/dev/null || echo "active")
1278
+ elif command -v python3 >/dev/null 2>&1; then
1279
+ FLOW_META=$(python3 - "$FLOW_STATE_FILE" <<'PY'
1280
+ import json
1281
+ import sys
1282
+ stage = "none"
1283
+ run_id = "active"
1284
+ try:
1285
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
1286
+ payload = json.load(fh)
1287
+ stage_value = payload.get("currentStage")
1288
+ run_value = payload.get("activeRunId")
1289
+ if isinstance(stage_value, str) and stage_value:
1290
+ stage = stage_value
1291
+ if isinstance(run_value, str) and run_value:
1292
+ run_id = run_value
1293
+ except Exception:
1294
+ pass
1295
+ print(stage)
1296
+ print(run_id)
1297
+ PY
1298
+ )
1299
+ {
1300
+ IFS= read -r CURRENT_STAGE
1301
+ IFS= read -r CURRENT_RUN
1302
+ } <<EOF
1303
+ $FLOW_META
1304
+ EOF
1305
+ fi
1306
+ fi
1307
+
1308
+ AUTO_TOOL=""
1309
+ AUTO_COMMAND=""
1310
+ AUTO_EXIT_CODE=""
1311
+ AUTO_PATHS_CSV=""
1312
+ if command -v python3 >/dev/null 2>&1; then
1313
+ AUTO_META=$(INPUT_JSON="$INPUT" python3 - <<'PY'
1314
+ import json
1315
+ import os
1316
+ import re
1317
+ from typing import Any, Iterator
1318
+
1319
+ raw = os.environ.get("INPUT_JSON", "{}")
1320
+ try:
1321
+ payload = json.loads(raw)
1322
+ except Exception:
1323
+ payload = {}
1324
+
1325
+ def walk(node: Any) -> Iterator[Any]:
1326
+ if isinstance(node, dict):
1327
+ yield node
1328
+ for value in node.values():
1329
+ yield from walk(value)
1330
+ elif isinstance(node, list):
1331
+ for value in node:
1332
+ yield from walk(value)
1333
+
1334
+ def first_string(keys: list[str]) -> str:
1335
+ for node in walk(payload):
1336
+ if not isinstance(node, dict):
1337
+ continue
1338
+ for key in keys:
1339
+ value = node.get(key)
1340
+ if isinstance(value, str) and value.strip():
1341
+ return value.strip()
1342
+ return ""
1343
+
1344
+ tool = first_string(["tool_name", "tool", "toolName", "name", "id"])
1345
+ command = ""
1346
+ for node in walk(payload):
1347
+ if not isinstance(node, dict):
1348
+ continue
1349
+ for key in ("command", "cmd"):
1350
+ value = node.get(key)
1351
+ if isinstance(value, str) and value.strip():
1352
+ command = value.strip()
1353
+ break
1354
+ if command:
1355
+ break
1356
+
1357
+ exit_code = ""
1358
+ for node in walk(payload):
1359
+ if not isinstance(node, dict):
1360
+ continue
1361
+ for key in ("exitCode", "exit_code", "code", "status"):
1362
+ value = node.get(key)
1363
+ if isinstance(value, bool):
1364
+ exit_code = "0" if value else "1"
1365
+ break
1366
+ if isinstance(value, (int, float)):
1367
+ exit_code = str(int(value))
1368
+ break
1369
+ if exit_code:
1370
+ break
1371
+
1372
+ blob_parts: list[str] = []
1373
+ for node in walk(payload):
1374
+ if not isinstance(node, dict):
1375
+ continue
1376
+ for key in ("stderr", "stdout", "output", "text", "message"):
1377
+ value = node.get(key)
1378
+ if isinstance(value, str) and value:
1379
+ blob_parts.append(value)
1380
+ blob_parts.append(command)
1381
+ blob = "\\n".join(blob_parts)
1382
+ path_pattern = re.compile(r"(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)")
1383
+ seen: set[str] = set()
1384
+ paths: list[str] = []
1385
+ for match in path_pattern.findall(blob):
1386
+ normalized = match.strip().strip("\\"'.,:;()[]{}<>")
1387
+ if not normalized or normalized in seen:
1388
+ continue
1389
+ seen.add(normalized)
1390
+ paths.append(normalized)
1391
+
1392
+ print(tool.replace("\\t", " ").replace("\\n", " "))
1393
+ print(command.replace("\\t", " ").replace("\\n", " "))
1394
+ print(exit_code)
1395
+ print(",".join(paths[:20]).replace("\\t", " ").replace("\\n", " "))
1396
+ PY
1397
+ )
1398
+ {
1399
+ IFS= read -r AUTO_TOOL
1400
+ IFS= read -r AUTO_COMMAND
1401
+ IFS= read -r AUTO_EXIT_CODE
1402
+ IFS= read -r AUTO_PATHS_CSV
1403
+ } <<EOF
1404
+ $AUTO_META
1405
+ EOF
1406
+ fi
1407
+
1408
+ if [ "$CURRENT_STAGE" = "tdd" ] && [ -n "$AUTO_COMMAND" ] && [ -n "$AUTO_EXIT_CODE" ]; then
1409
+ if command -v cclaw_hook_lower >/dev/null 2>&1; then
1410
+ AUTO_COMMAND_LOWER=$(cclaw_hook_lower "$AUTO_COMMAND")
1411
+ else
1412
+ AUTO_COMMAND_LOWER=$(printf '%s' "$AUTO_COMMAND" | tr '[:upper:]' '[:lower:]')
1413
+ fi
1414
+ if printf '%s' "$AUTO_COMMAND_LOWER" | grep -Eq '(npm test|npm run test|pnpm test|pnpm run test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)'; then
1415
+ if printf '%s' "$AUTO_EXIT_CODE" | grep -Eq '^-?[0-9]+$' && [ "$AUTO_EXIT_CODE" -ne 0 ]; then
1416
+ TS_AUTO=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
1417
+ if command -v jq >/dev/null 2>&1; then
1418
+ AUTO_ENTRY=$(jq -n -c \
1419
+ --arg ts "$TS_AUTO" \
1420
+ --arg run "$CURRENT_RUN" \
1421
+ --arg command "$AUTO_COMMAND" \
1422
+ --arg tool "$AUTO_TOOL" \
1423
+ --argjson exitCode "$AUTO_EXIT_CODE" \
1424
+ --arg paths "$AUTO_PATHS_CSV" \
1425
+ '{
1426
+ ts: $ts,
1427
+ runId: $run,
1428
+ stage: "tdd",
1429
+ source: "posttool-auto",
1430
+ command: $command,
1431
+ tool: $tool,
1432
+ exitCode: $exitCode,
1433
+ paths: ($paths | split(",") | map(select(length > 0)))
1434
+ }' 2>/dev/null || echo "")
1435
+ elif command -v python3 >/dev/null 2>&1; then
1436
+ AUTO_ENTRY=$(python3 - "$TS_AUTO" "$CURRENT_RUN" "$AUTO_COMMAND" "$AUTO_TOOL" "$AUTO_EXIT_CODE" "$AUTO_PATHS_CSV" <<'PY'
1437
+ import json
1438
+ import sys
1439
+ ts, run_id, command, tool, exit_code, paths_csv = sys.argv[1:7]
1440
+ paths = [value for value in paths_csv.split(",") if value]
1441
+ entry = {
1442
+ "ts": ts,
1443
+ "runId": run_id,
1444
+ "stage": "tdd",
1445
+ "source": "posttool-auto",
1446
+ "command": command,
1447
+ "tool": tool,
1448
+ "exitCode": int(exit_code),
1449
+ "paths": paths
1450
+ }
1451
+ print(json.dumps(entry, ensure_ascii=False))
1452
+ PY
1453
+ )
1454
+ else
1455
+ AUTO_ENTRY=""
1456
+ fi
1457
+ if [ -n "$AUTO_ENTRY" ]; then
1458
+ printf '%s\n' "$AUTO_ENTRY" >> "$TDD_AUTO_EVIDENCE_FILE" 2>/dev/null || true
1459
+ fi
1460
+ fi
1461
+ fi
1462
+ fi
1463
+
1139
1464
  REMAINING_PERCENT=""
1140
1465
  if command -v python3 >/dev/null 2>&1; then
1141
1466
  REMAINING_PERCENT=$(INPUT_JSON="$INPUT" python3 - <<'PY'
@@ -16,8 +16,7 @@ export const REVIEW = {
16
16
  ],
17
17
  whenNotToUse: [
18
18
  "There is no implementation diff to review",
19
- "TDD stage evidence is missing or stale",
20
- "The goal is direct release execution without layered quality checks"
19
+ "TDD stage evidence is missing or stale"
21
20
  ],
22
21
  checklist: [
23
22
  "Diff Scope — Run `git diff` against base branch. If no diff, exit early with APPROVED (no changes to review). Scope the review to changed files unless blast-radius analysis requires wider inspection.",
@@ -62,6 +61,7 @@ export const REVIEW = {
62
61
  requiredGates: [
63
62
  { id: "review_layer1_spec_compliance", description: "Spec compliance check completed with per-criterion verdict." },
64
63
  { id: "review_layer2_security", description: "Security review completed." },
64
+ { id: "review_layer_coverage_complete", description: "Layer coverage map in 07-review-army.json confirms spec/correctness/security/performance/architecture/external-safety passes." },
65
65
  { id: "review_criticals_resolved", description: "No unresolved critical blockers remain." },
66
66
  { id: "review_army_json_valid", description: "07-review-army.json passes schema validation (validateReviewArmy)." },
67
67
  { id: "review_trace_matrix_clean", description: "Trace matrix has no orphaned criteria/tasks/test slices for the active run." }
@@ -609,6 +609,14 @@ inputs_hash: sha256:pending
609
609
  "duplicatesCollapsed": 0,
610
610
  "conflicts": [],
611
611
  "multiSpecialistConfirmed": [],
612
+ "layerCoverage": {
613
+ "spec": false,
614
+ "correctness": false,
615
+ "security": false,
616
+ "performance": false,
617
+ "architecture": false,
618
+ "external-safety": false
619
+ },
612
620
  "shipBlockers": []
613
621
  }
614
622
  }
package/dist/doctor.js CHANGED
@@ -673,6 +673,7 @@ export async function doctorChecks(projectRoot, options = {}) {
673
673
  }
674
674
  // Hook scripts
675
675
  for (const script of [
676
+ "_lib.sh",
676
677
  "session-start.sh",
677
678
  "stop-checkpoint.sh",
678
679
  "run-hook.cmd",
@@ -877,6 +878,7 @@ export async function doctorChecks(projectRoot, options = {}) {
877
878
  const codexStopCmds = collectHookCommands(codexHooks.Stop);
878
879
  const codexWiringOk = codexSessionCmds.some((cmd) => cmd.includes("session-start.sh")) &&
879
880
  codexUserPromptCmds.some((cmd) => cmd.includes("prompt-guard.sh")) &&
881
+ codexUserPromptCmds.some((cmd) => cmd.includes("workflow-guard.sh")) &&
880
882
  codexUserPromptCmds.some((cmd) => cmd.includes("verify-current-state --quiet")) &&
881
883
  codexPreCmds.some((cmd) => cmd.includes("prompt-guard.sh")) &&
882
884
  codexPreCmds.some((cmd) => cmd.includes("workflow-guard.sh")) &&
@@ -885,7 +887,7 @@ export async function doctorChecks(projectRoot, options = {}) {
885
887
  checks.push({
886
888
  name: "hook:wiring:codex",
887
889
  ok: codexWiringOk,
888
- details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt-guard + verify-current-state), PreToolUse(prompt/workflow), PostToolUse(context-monitor), and Stop(stop-checkpoint). PreToolUse/PostToolUse run Bash-only in Codex v0.114+`
890
+ details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/workflow/verify-current-state), PreToolUse(prompt/workflow), PostToolUse(context-monitor), and Stop(stop-checkpoint). PreToolUse/PostToolUse run Bash-only in Codex v0.114+`
889
891
  });
890
892
  // Feature flag warning: Codex ignores `.codex/hooks.json` unless the
891
893
  // user has `[features] codex_hooks = true` in `~/.codex/config.toml`.
@@ -965,10 +967,12 @@ export async function doctorChecks(projectRoot, options = {}) {
965
967
  content.includes("workflow-guard.sh") &&
966
968
  content.includes("context-monitor.sh") &&
967
969
  content.includes("pre-compact.sh") &&
970
+ content.includes('"session.created"') &&
968
971
  content.includes('"session.idle"') &&
969
972
  content.includes('"session.resumed"') &&
970
973
  content.includes('"session.compacted"') &&
971
974
  content.includes('"session.cleared"') &&
975
+ content.includes('"session.updated"') &&
972
976
  content.includes('"experimental.chat.system.transform"');
973
977
  singleHandlerPathOk =
974
978
  !content.includes('eventType === "tool.execute.before"') &&
@@ -982,7 +986,7 @@ export async function doctorChecks(projectRoot, options = {}) {
982
986
  checks.push({
983
987
  name: "lifecycle:opencode:rehydration_events",
984
988
  ok,
985
- details: `${file} must include event lifecycle handler, tool.execute.before/after with prompt/workflow/context hooks, session.idle checkpoint, and transform rehydration`
989
+ details: `${file} must include event lifecycle handler, session.created/updated/resumed/cleared/compacted rehydration, tool.execute.before/after with prompt/workflow/context hooks, session.idle checkpoint, and transform rehydration`
986
990
  });
987
991
  checks.push({
988
992
  name: "hook:opencode:single_tool_handler_path",
package/dist/install.js CHANGED
@@ -24,7 +24,7 @@ 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
29
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
30
30
  import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
@@ -630,6 +630,7 @@ async function writeHooks(projectRoot, config) {
630
630
  mode: config.ironLaws?.mode,
631
631
  strictLaws: config.ironLaws?.strictLaws
632
632
  }), null, 2)}\n`);
633
+ await writeFileSafe(path.join(hooksDir, "_lib.sh"), hookLibScript());
633
634
  await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
634
635
  await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
635
636
  await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookDispatcherScript());
@@ -649,6 +650,7 @@ async function writeHooks(projectRoot, config) {
649
650
  await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
650
651
  try {
651
652
  for (const script of [
653
+ "_lib.sh",
652
654
  "session-start.sh",
653
655
  "stop-checkpoint.sh",
654
656
  "run-hook.cmd",
@@ -12,6 +12,7 @@ import { readFlowState, writeFlowState } from "../runs.js";
12
12
  import { FLOW_STAGES } from "../types.js";
13
13
  import { runEnvelopeValidateCommand } from "./envelope-validate.js";
14
14
  import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
15
+ import { runTddRedEvidenceCommand } from "./tdd-red-evidence.js";
15
16
  function unique(values) {
16
17
  return [...new Set(values)];
17
18
  }
@@ -623,7 +624,7 @@ async function runVerifyCurrentState(projectRoot, args, io) {
623
624
  export async function runInternalCommand(projectRoot, argv, io) {
624
625
  const [subcommand, ...tokens] = argv;
625
626
  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");
627
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence\n");
627
628
  return 1;
628
629
  }
629
630
  try {
@@ -642,7 +643,10 @@ export async function runInternalCommand(projectRoot, argv, io) {
642
643
  if (subcommand === "envelope-validate") {
643
644
  return await runEnvelopeValidateCommand(projectRoot, tokens, io);
644
645
  }
645
- io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate\n`);
646
+ if (subcommand === "tdd-red-evidence") {
647
+ return await runTddRedEvidenceCommand(projectRoot, tokens, io);
648
+ }
649
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence\n`);
646
650
  return 1;
647
651
  }
648
652
  catch (err) {
@@ -0,0 +1,7 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export declare function runTddRedEvidenceCommand(projectRoot: string, tokens: string[], io: InternalIo): Promise<number>;
7
+ export {};
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.6",
3
+ "version": "0.48.7",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {