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.
- package/dist/content/hooks.d.ts +2 -2
- package/dist/content/hooks.js +182 -47
- package/dist/content/iron-laws.d.ts +8 -0
- package/dist/content/iron-laws.js +9 -0
- package/dist/content/observe.js +381 -56
- package/dist/content/stages/review.js +2 -2
- package/dist/content/templates.js +8 -0
- package/dist/doctor.js +6 -2
- package/dist/install.js +3 -1
- package/dist/internal/advance-stage.js +6 -2
- package/dist/internal/tdd-red-evidence.d.ts +7 -0
- package/dist/internal/tdd-red-evidence.js +130 -0
- package/package.json +1 -1
package/dist/content/hooks.d.ts
CHANGED
|
@@ -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\"\
|
|
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;
|
package/dist/content/hooks.js
CHANGED
|
@@ -15,34 +15,195 @@ const ESCAPE_FN = `escape_json() {
|
|
|
15
15
|
str=\${str//$'\\n'/\\\\n}
|
|
16
16
|
printf '%s' "$str"
|
|
17
17
|
}`;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
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" ] && [ -
|
|
30
|
-
|
|
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 [ -
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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",
|
package/dist/content/observe.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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,
|
|
982
|
+
REASONS="$REASONS,tdd_red_evidence_check_failed"
|
|
895
983
|
else
|
|
896
|
-
REASONS="
|
|
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 [ "$
|
|
903
|
-
if
|
|
904
|
-
|
|
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
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
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 [ "$
|
|
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 [ "$
|
|
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 '
|
|
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
|
|
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
|
-
|
|
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,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
|
+
}
|