cclaw-cli 0.48.9 → 0.48.10
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/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/harness-playbooks.js +7 -7
- package/dist/content/hook-events.js +19 -19
- package/dist/content/hooks.d.ts +0 -16
- package/dist/content/hooks.js +112 -1220
- package/dist/content/learnings.js +2 -2
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +20 -11
- package/dist/content/observe.d.ts +0 -38
- package/dist/content/observe.js +27 -1718
- package/dist/content/opencode-plugin.js +5 -5
- package/dist/content/protocols.js +5 -9
- package/dist/content/skills.js +1 -1
- package/dist/content/stage-common-guidance.js +1 -1
- package/dist/content/stages/design.js +1 -1
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +1 -1
- package/dist/doctor.js +63 -52
- package/dist/install.js +124 -53
- package/dist/policy.js +13 -13
- package/package.json +2 -3
package/dist/content/observe.js
CHANGED
|
@@ -1,1666 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook helper scripts and harness hook JSON generators.
|
|
3
|
-
*
|
|
4
|
-
* This module still provides prompt/workflow/context guard scripts and
|
|
5
|
-
* cross-harness hook wiring. Observation pipeline scripts are retained only
|
|
6
|
-
* for backward compatibility and are not wired by default runtime generation.
|
|
7
|
-
*/
|
|
8
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const promptGuardMode = options.strictMode === true ? "strict" : "advisory";
|
|
12
|
-
return `#!/usr/bin/env bash
|
|
13
|
-
# cclaw prompt guard hook — generated by cclaw sync
|
|
14
|
-
# Advisory-only guard for risky writes into ${RUNTIME_ROOT} runtime files.
|
|
15
|
-
set -uo pipefail
|
|
16
|
-
shopt -s globstar 2>/dev/null || true
|
|
17
|
-
PROMPT_GUARD_MODE="${promptGuardMode}"
|
|
18
|
-
|
|
19
|
-
${RUNTIME_SHELL_DETECT_ROOT}
|
|
20
|
-
|
|
21
|
-
STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
|
|
22
|
-
GUARD_LOG="$STATE_DIR/prompt-guard.jsonl"
|
|
23
|
-
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
24
|
-
|
|
25
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
26
|
-
[ -n "$INPUT" ] || exit 0
|
|
27
|
-
|
|
28
|
-
TOOL="unknown"
|
|
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
|
|
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")
|
|
36
|
-
PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
|
|
37
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
38
|
-
TOOL=$(INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
39
|
-
import json
|
|
40
|
-
import os
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
value = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
44
|
-
except Exception:
|
|
45
|
-
value = {}
|
|
46
|
-
|
|
47
|
-
def pick_tool(payload):
|
|
48
|
-
if not isinstance(payload, dict):
|
|
49
|
-
return "unknown"
|
|
50
|
-
candidates = [
|
|
51
|
-
payload.get("tool_name"),
|
|
52
|
-
payload.get("tool"),
|
|
53
|
-
payload.get("toolName"),
|
|
54
|
-
payload.get("name"),
|
|
55
|
-
payload.get("id"),
|
|
56
|
-
payload.get("command")
|
|
57
|
-
]
|
|
58
|
-
top_tool = payload.get("tool")
|
|
59
|
-
if isinstance(top_tool, dict):
|
|
60
|
-
candidates.extend([top_tool.get("name"), top_tool.get("id")])
|
|
61
|
-
nested = payload.get("input")
|
|
62
|
-
if isinstance(nested, dict):
|
|
63
|
-
candidates.extend([
|
|
64
|
-
nested.get("tool_name"),
|
|
65
|
-
nested.get("tool"),
|
|
66
|
-
nested.get("toolName"),
|
|
67
|
-
nested.get("name"),
|
|
68
|
-
nested.get("id"),
|
|
69
|
-
nested.get("command")
|
|
70
|
-
])
|
|
71
|
-
nested_tool = nested.get("tool")
|
|
72
|
-
if isinstance(nested_tool, dict):
|
|
73
|
-
candidates.extend([nested_tool.get("name"), nested_tool.get("id")])
|
|
74
|
-
for candidate in candidates:
|
|
75
|
-
if isinstance(candidate, str) and candidate.strip():
|
|
76
|
-
return candidate.strip()
|
|
77
|
-
return "unknown"
|
|
78
|
-
|
|
79
|
-
print(pick_tool(value))
|
|
80
|
-
PY
|
|
81
|
-
)
|
|
82
|
-
PAYLOAD=$(printf '%s' "$INPUT")
|
|
83
|
-
else
|
|
84
|
-
PAYLOAD=$(printf '%s' "$INPUT")
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
if [ -z "$PAYLOAD" ]; then
|
|
88
|
-
PAYLOAD=$(printf '%s' "$INPUT")
|
|
89
|
-
fi
|
|
90
|
-
|
|
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
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
99
|
-
REASONS=""
|
|
100
|
-
|
|
101
|
-
case "$TOOL_LOWER" in
|
|
102
|
-
write|edit|multiedit|delete|applypatch|runcommand|shell|terminal|execcommand)
|
|
103
|
-
if printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/(state|artifacts|hooks|skills|commands|agents|runs|knowledge)'; then
|
|
104
|
-
REASONS="write_to_cclaw_runtime"
|
|
105
|
-
fi
|
|
106
|
-
;;
|
|
107
|
-
esac
|
|
108
|
-
|
|
109
|
-
if printf '%s' "$PAYLOAD_LOWER" | grep -Eq '(rm[[:space:]]+-rf[[:space:]]+\.cclaw|curl[[:space:]].*https?://|wget[[:space:]].*https?://|base64[[:space:]]+-d|eval[(]|python[[:space:]]+-c)'; then
|
|
110
|
-
if [ -n "$REASONS" ]; then
|
|
111
|
-
REASONS="$REASONS,suspicious_payload_pattern"
|
|
112
|
-
else
|
|
113
|
-
REASONS="suspicious_payload_pattern"
|
|
114
|
-
fi
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
if [ -n "$REASONS" ]; then
|
|
118
|
-
NOTE="Cclaw advisory: potential risky write intent detected for ${RUNTIME_ROOT} runtime (\${REASONS}). Prefer installer commands or explicit confirmation before mutating runtime internals."
|
|
119
|
-
if command -v jq >/dev/null 2>&1; then
|
|
120
|
-
ENTRY=$(jq -n -c \
|
|
121
|
-
--arg ts "$TS" \
|
|
122
|
-
--arg harness "$HARNESS" \
|
|
123
|
-
--arg tool "$TOOL" \
|
|
124
|
-
--arg reasons "$REASONS" \
|
|
125
|
-
--arg note "$NOTE" \
|
|
126
|
-
'{ts:$ts,harness:$harness,tool:$tool,reasons:($reasons|split(",")),note:$note}' 2>/dev/null || echo "")
|
|
127
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
128
|
-
ENTRY=$(TS="$TS" HARNESS="$HARNESS" TOOL="$TOOL" REASONS="$REASONS" NOTE="$NOTE" python3 - <<'PY'
|
|
129
|
-
import json, os
|
|
130
|
-
|
|
131
|
-
ts = os.environ.get("TS") or ""
|
|
132
|
-
harness = os.environ.get("HARNESS") or ""
|
|
133
|
-
tool = os.environ.get("TOOL") or "unknown"
|
|
134
|
-
reasons_raw = os.environ.get("REASONS") or ""
|
|
135
|
-
note = os.environ.get("NOTE") or ""
|
|
136
|
-
reasons = [r for r in reasons_raw.split(",") if r]
|
|
137
|
-
print(json.dumps({"ts": ts, "harness": harness, "tool": tool, "reasons": reasons, "note": note}, ensure_ascii=False))
|
|
138
|
-
PY
|
|
139
|
-
)
|
|
140
|
-
else
|
|
141
|
-
ENTRY=""
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
if [ -n "$ENTRY" ]; then
|
|
145
|
-
printf '%s\n' "$ENTRY" >> "$GUARD_LOG" 2>/dev/null || true
|
|
146
|
-
fi
|
|
147
|
-
if [ "$PROMPT_GUARD_MODE" = "strict" ]; then
|
|
148
|
-
printf '[cclaw] %s (blocked by strict mode)\n' "$NOTE" >&2
|
|
149
|
-
exit 1
|
|
150
|
-
fi
|
|
151
|
-
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
|
-
exit 0
|
|
155
|
-
`;
|
|
156
|
-
}
|
|
157
|
-
export function workflowGuardScript(options = {}) {
|
|
158
|
-
const workflowGuardMode = options.workflowGuardMode === "strict" ? "strict" : "advisory";
|
|
159
|
-
const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
|
|
160
|
-
const tddTestPathPatterns = options.tddTestPathPatterns && options.tddTestPathPatterns.length > 0
|
|
161
|
-
? options.tddTestPathPatterns.join(",")
|
|
162
|
-
: "**/*.test.*,**/tests/**,**/__tests__/**";
|
|
163
|
-
const tddProductionPathPatterns = options.tddProductionPathPatterns && options.tddProductionPathPatterns.length > 0
|
|
164
|
-
? options.tddProductionPathPatterns.join(",")
|
|
165
|
-
: "";
|
|
166
|
-
return `#!/usr/bin/env bash
|
|
167
|
-
# cclaw workflow guard hook — generated by cclaw sync
|
|
168
|
-
# Enforces stage-aware command discipline and recent flow-state read hygiene.
|
|
169
|
-
set -uo pipefail
|
|
170
|
-
shopt -s globstar 2>/dev/null || true
|
|
171
|
-
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-${workflowGuardMode}}"
|
|
172
|
-
MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
|
|
173
|
-
TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
|
|
174
|
-
TDD_TEST_PATH_PATTERNS="${tddTestPathPatterns}"
|
|
175
|
-
TDD_PRODUCTION_PATH_PATTERNS="${tddProductionPathPatterns}"
|
|
176
|
-
|
|
177
|
-
${RUNTIME_SHELL_DETECT_ROOT}
|
|
178
|
-
|
|
179
|
-
STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
|
|
180
|
-
FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
|
|
181
|
-
TDD_LOG_FILE="$STATE_DIR/tdd-cycle-log.jsonl"
|
|
182
|
-
IRON_LAWS_FILE="$STATE_DIR/iron-laws.json"
|
|
183
|
-
REVIEW_ARMY_FILE="$ROOT/${RUNTIME_ROOT}/artifacts/07-review-army.json"
|
|
184
|
-
GUARD_STATE_FILE="$STATE_DIR/workflow-guard.json"
|
|
185
|
-
GUARD_LOG="$STATE_DIR/workflow-guard.jsonl"
|
|
186
|
-
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
187
|
-
|
|
188
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
189
|
-
[ -n "$INPUT" ] || exit 0
|
|
190
|
-
|
|
191
|
-
TOOL="unknown"
|
|
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
|
|
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")
|
|
199
|
-
PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
|
|
200
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
201
|
-
TOOL=$(INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
202
|
-
import json
|
|
203
|
-
import os
|
|
204
|
-
try:
|
|
205
|
-
value = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
206
|
-
except Exception:
|
|
207
|
-
value = {}
|
|
208
|
-
|
|
209
|
-
def pick_tool(payload):
|
|
210
|
-
if not isinstance(payload, dict):
|
|
211
|
-
return "unknown"
|
|
212
|
-
candidates = [
|
|
213
|
-
payload.get("tool_name"),
|
|
214
|
-
payload.get("tool"),
|
|
215
|
-
payload.get("toolName"),
|
|
216
|
-
payload.get("name"),
|
|
217
|
-
payload.get("id"),
|
|
218
|
-
payload.get("command")
|
|
219
|
-
]
|
|
220
|
-
top_tool = payload.get("tool")
|
|
221
|
-
if isinstance(top_tool, dict):
|
|
222
|
-
candidates.extend([top_tool.get("name"), top_tool.get("id")])
|
|
223
|
-
nested = payload.get("input")
|
|
224
|
-
if isinstance(nested, dict):
|
|
225
|
-
candidates.extend([
|
|
226
|
-
nested.get("tool_name"),
|
|
227
|
-
nested.get("tool"),
|
|
228
|
-
nested.get("toolName"),
|
|
229
|
-
nested.get("name"),
|
|
230
|
-
nested.get("id"),
|
|
231
|
-
nested.get("command")
|
|
232
|
-
])
|
|
233
|
-
nested_tool = nested.get("tool")
|
|
234
|
-
if isinstance(nested_tool, dict):
|
|
235
|
-
candidates.extend([nested_tool.get("name"), nested_tool.get("id")])
|
|
236
|
-
for candidate in candidates:
|
|
237
|
-
if isinstance(candidate, str) and candidate.strip():
|
|
238
|
-
return candidate.strip()
|
|
239
|
-
return "unknown"
|
|
240
|
-
|
|
241
|
-
print(pick_tool(value))
|
|
242
|
-
PY
|
|
243
|
-
)
|
|
244
|
-
PAYLOAD=$(printf '%s' "$INPUT")
|
|
245
|
-
else
|
|
246
|
-
PAYLOAD=$(printf '%s' "$INPUT")
|
|
247
|
-
fi
|
|
248
|
-
|
|
249
|
-
[ -n "$PAYLOAD" ] || PAYLOAD=$(printf '%s' "$INPUT")
|
|
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
|
|
257
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
258
|
-
NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
|
|
259
|
-
REASONS=""
|
|
260
|
-
|
|
261
|
-
ACTIVE_AGENT="\${CCLAW_ACTIVE_AGENT:-}"
|
|
262
|
-
if [ -z "$ACTIVE_AGENT" ]; then
|
|
263
|
-
if command -v jq >/dev/null 2>&1; then
|
|
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 "")
|
|
265
|
-
fi
|
|
266
|
-
fi
|
|
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
|
|
272
|
-
|
|
273
|
-
CURRENT_STAGE="none"
|
|
274
|
-
CURRENT_RUN="active"
|
|
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
|
|
280
|
-
if command -v jq >/dev/null 2>&1; then
|
|
281
|
-
CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
|
|
282
|
-
CURRENT_RUN=$(jq -r '.activeRunId // "active"' "$FLOW_STATE_FILE" 2>/dev/null || echo "active")
|
|
283
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
284
|
-
CURRENT_STAGE=$(python3 - "$FLOW_STATE_FILE" <<'PY'
|
|
285
|
-
import json
|
|
286
|
-
import sys
|
|
287
|
-
stage = "none"
|
|
288
|
-
try:
|
|
289
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
290
|
-
parsed = json.load(fh)
|
|
291
|
-
value = parsed.get("currentStage")
|
|
292
|
-
if isinstance(value, str) and value:
|
|
293
|
-
stage = value
|
|
294
|
-
except Exception:
|
|
295
|
-
pass
|
|
296
|
-
print(stage)
|
|
297
|
-
PY
|
|
298
|
-
)
|
|
299
|
-
CURRENT_RUN=$(python3 - "$FLOW_STATE_FILE" <<'PY'
|
|
300
|
-
import json
|
|
301
|
-
import sys
|
|
302
|
-
run_id = "active"
|
|
303
|
-
try:
|
|
304
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
305
|
-
parsed = json.load(fh)
|
|
306
|
-
value = parsed.get("activeRunId")
|
|
307
|
-
if isinstance(value, str) and value:
|
|
308
|
-
run_id = value
|
|
309
|
-
except Exception:
|
|
310
|
-
pass
|
|
311
|
-
print(run_id)
|
|
312
|
-
PY
|
|
313
|
-
)
|
|
314
|
-
fi
|
|
315
|
-
fi
|
|
316
|
-
|
|
317
|
-
SHIP_PREFLIGHT_PASSED="false"
|
|
318
|
-
if [ -f "$FLOW_STATE_FILE" ]; then
|
|
319
|
-
if command -v jq >/dev/null 2>&1; then
|
|
320
|
-
SHIP_PREFLIGHT_PASSED=$(jq -r '
|
|
321
|
-
if ((.stageGateCatalog.ship.passed // []) | index("ship_preflight_passed")) == null
|
|
322
|
-
then "false"
|
|
323
|
-
else "true"
|
|
324
|
-
end
|
|
325
|
-
' "$FLOW_STATE_FILE" 2>/dev/null || echo "false")
|
|
326
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
327
|
-
SHIP_PREFLIGHT_PASSED=$(python3 - "$FLOW_STATE_FILE" <<'PY'
|
|
328
|
-
import json
|
|
329
|
-
import sys
|
|
330
|
-
value = "false"
|
|
331
|
-
try:
|
|
332
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
333
|
-
parsed = json.load(fh)
|
|
334
|
-
ship = ((parsed.get("stageGateCatalog") or {}).get("ship") or {}).get("passed") or []
|
|
335
|
-
if isinstance(ship, list) and "ship_preflight_passed" in ship:
|
|
336
|
-
value = "true"
|
|
337
|
-
except Exception:
|
|
338
|
-
value = "false"
|
|
339
|
-
print(value)
|
|
340
|
-
PY
|
|
341
|
-
)
|
|
342
|
-
fi
|
|
343
|
-
fi
|
|
344
|
-
|
|
345
|
-
IRON_LAW_STRICT_ALL="false"
|
|
346
|
-
IRON_LAW_STRICT_IDS=""
|
|
347
|
-
if [ -f "$IRON_LAWS_FILE" ]; then
|
|
348
|
-
if command -v jq >/dev/null 2>&1; then
|
|
349
|
-
IRON_LAW_STRICT_ALL=$(jq -r 'if (.mode // "advisory") == "strict" then "true" else "false" end' "$IRON_LAWS_FILE" 2>/dev/null || echo "false")
|
|
350
|
-
IRON_LAW_STRICT_IDS=$(jq -r '(.laws // []) | map(select(.strict == true) | (.id // "")) | map(select(length > 0)) | join(",")' "$IRON_LAWS_FILE" 2>/dev/null || echo "")
|
|
351
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
352
|
-
IRON_LAW_STRICT_ALL=$(python3 - "$IRON_LAWS_FILE" <<'PY'
|
|
353
|
-
import json
|
|
354
|
-
import sys
|
|
355
|
-
mode = "false"
|
|
356
|
-
try:
|
|
357
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
358
|
-
parsed = json.load(fh)
|
|
359
|
-
if str(parsed.get("mode", "advisory")) == "strict":
|
|
360
|
-
mode = "true"
|
|
361
|
-
except Exception:
|
|
362
|
-
mode = "false"
|
|
363
|
-
print(mode)
|
|
364
|
-
PY
|
|
365
|
-
)
|
|
366
|
-
IRON_LAW_STRICT_IDS=$(python3 - "$IRON_LAWS_FILE" <<'PY'
|
|
367
|
-
import json
|
|
368
|
-
import sys
|
|
369
|
-
out = []
|
|
370
|
-
try:
|
|
371
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
372
|
-
parsed = json.load(fh)
|
|
373
|
-
for row in parsed.get("laws", []):
|
|
374
|
-
if isinstance(row, dict) and row.get("strict") and isinstance(row.get("id"), str):
|
|
375
|
-
out.append(row["id"].strip())
|
|
376
|
-
except Exception:
|
|
377
|
-
out = []
|
|
378
|
-
print(",".join([v for v in out if v]))
|
|
379
|
-
PY
|
|
380
|
-
)
|
|
381
|
-
fi
|
|
382
|
-
fi
|
|
383
|
-
|
|
384
|
-
iron_law_is_strict() {
|
|
385
|
-
local law_id="$1"
|
|
386
|
-
if [ "$IRON_LAW_STRICT_ALL" = "true" ]; then
|
|
387
|
-
return 0
|
|
388
|
-
fi
|
|
389
|
-
if [ -z "$IRON_LAW_STRICT_IDS" ]; then
|
|
390
|
-
return 1
|
|
391
|
-
fi
|
|
392
|
-
case ",$IRON_LAW_STRICT_IDS," in
|
|
393
|
-
*",$law_id,"*) return 0 ;;
|
|
394
|
-
*) return 1 ;;
|
|
395
|
-
esac
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
LAST_FLOW_READ_AT=0
|
|
399
|
-
if [ -f "$GUARD_STATE_FILE" ]; then
|
|
400
|
-
if command -v jq >/dev/null 2>&1; then
|
|
401
|
-
LAST_FLOW_READ_AT=$(jq -r '.lastFlowReadAtEpoch // 0' "$GUARD_STATE_FILE" 2>/dev/null || echo "0")
|
|
402
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
403
|
-
LAST_FLOW_READ_AT=$(python3 - "$GUARD_STATE_FILE" <<'PY'
|
|
404
|
-
import json
|
|
405
|
-
import sys
|
|
406
|
-
value = 0
|
|
407
|
-
try:
|
|
408
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
409
|
-
parsed = json.load(fh)
|
|
410
|
-
raw = parsed.get("lastFlowReadAtEpoch", 0)
|
|
411
|
-
if isinstance(raw, (int, float)):
|
|
412
|
-
value = int(raw)
|
|
413
|
-
except Exception:
|
|
414
|
-
pass
|
|
415
|
-
print(value)
|
|
416
|
-
PY
|
|
417
|
-
)
|
|
418
|
-
fi
|
|
419
|
-
fi
|
|
420
|
-
[ -n "$LAST_FLOW_READ_AT" ] || LAST_FLOW_READ_AT=0
|
|
421
|
-
|
|
422
|
-
stage_index() {
|
|
423
|
-
case "$1" in
|
|
424
|
-
brainstorm) echo 1 ;;
|
|
425
|
-
scope) echo 2 ;;
|
|
426
|
-
design) echo 3 ;;
|
|
427
|
-
spec) echo 4 ;;
|
|
428
|
-
plan) echo 5 ;;
|
|
429
|
-
tdd) echo 6 ;;
|
|
430
|
-
review) echo 7 ;;
|
|
431
|
-
ship) echo 8 ;;
|
|
432
|
-
*) echo 0 ;;
|
|
433
|
-
esac
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
is_mutating_tool() {
|
|
437
|
-
case "$1" in
|
|
438
|
-
write|edit|multiedit|multi_edit|delete|applypatch|apply_patch) return 0 ;;
|
|
439
|
-
*) return 1 ;;
|
|
440
|
-
esac
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
is_execution_or_mutating_tool() {
|
|
444
|
-
case "$1" in
|
|
445
|
-
write|edit|multiedit|multi_edit|delete|applypatch|apply_patch) return 0 ;;
|
|
446
|
-
shell|bash|runcommand|run_command|execcommand|exec_command|terminal) return 0 ;;
|
|
447
|
-
*) return 1 ;;
|
|
448
|
-
esac
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
is_plan_mode_safe_tool() {
|
|
452
|
-
case "$1" in
|
|
453
|
-
read|readfile|open|view|cat|head|tail) return 0 ;;
|
|
454
|
-
grep|glob|search|semanticsearch|ripgrep|rg|find|list_directory|ls) return 0 ;;
|
|
455
|
-
askquestion|askuserquestion|ask_question|ask_user_question|question) return 0 ;;
|
|
456
|
-
todowrite|todoread|todo_write|todo_read) return 0 ;;
|
|
457
|
-
webfetch|websearch|web_fetch|web_search|fetchmcpresource) return 0 ;;
|
|
458
|
-
switchmode|switch_mode) return 0 ;;
|
|
459
|
-
task|delegate) return 0 ;;
|
|
460
|
-
*) return 1 ;;
|
|
461
|
-
esac
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
is_cclaw_cli_payload() {
|
|
465
|
-
printf '%s' "$1" | grep -Eq '(cclaw |npx cclaw |/cc-|/cc[^[:alnum:]_-])'
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
extract_flow_state_after_json() {
|
|
469
|
-
if command -v jq >/dev/null 2>&1; then
|
|
470
|
-
printf '%s' "$INPUT" | jq -r '
|
|
471
|
-
.tool_input?.content //
|
|
472
|
-
.input?.content //
|
|
473
|
-
.arguments?.content //
|
|
474
|
-
.params?.content //
|
|
475
|
-
.payload?.content //
|
|
476
|
-
.content //
|
|
477
|
-
.input?.new_string //
|
|
478
|
-
.tool_input?.new_string //
|
|
479
|
-
""
|
|
480
|
-
' 2>/dev/null || echo ""
|
|
481
|
-
return 0
|
|
482
|
-
fi
|
|
483
|
-
|
|
484
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
485
|
-
INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
486
|
-
import json
|
|
487
|
-
import os
|
|
488
|
-
|
|
489
|
-
try:
|
|
490
|
-
payload = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
491
|
-
except Exception:
|
|
492
|
-
payload = {}
|
|
493
|
-
|
|
494
|
-
def pick(value):
|
|
495
|
-
if not isinstance(value, dict):
|
|
496
|
-
return ""
|
|
497
|
-
for key in ("tool_input", "input", "arguments", "params", "payload"):
|
|
498
|
-
nested = value.get(key)
|
|
499
|
-
if isinstance(nested, dict):
|
|
500
|
-
content = nested.get("content")
|
|
501
|
-
if isinstance(content, str) and content.strip():
|
|
502
|
-
return content
|
|
503
|
-
new_string = nested.get("new_string")
|
|
504
|
-
if isinstance(new_string, str) and new_string.strip():
|
|
505
|
-
return new_string
|
|
506
|
-
content = value.get("content")
|
|
507
|
-
if isinstance(content, str) and content.strip():
|
|
508
|
-
return content
|
|
509
|
-
return ""
|
|
510
|
-
|
|
511
|
-
print(pick(payload))
|
|
512
|
-
PY
|
|
513
|
-
return 0
|
|
514
|
-
fi
|
|
515
|
-
|
|
516
|
-
printf ''
|
|
517
|
-
return 0
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
verify_flow_state_candidate() {
|
|
521
|
-
local candidate_json="$1"
|
|
522
|
-
[ -n "$candidate_json" ] || return 1
|
|
523
|
-
local tmp_file="$STATE_DIR/.flow-state-candidate.$$.$RANDOM.json"
|
|
524
|
-
printf '%s' "$candidate_json" > "$tmp_file" 2>/dev/null || {
|
|
525
|
-
rm -f "$tmp_file" 2>/dev/null || true
|
|
526
|
-
return 1
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if ! command -v cclaw >/dev/null 2>&1; then
|
|
530
|
-
rm -f "$tmp_file" 2>/dev/null || true
|
|
531
|
-
printf '[cclaw] workflow guard: cclaw binary is required to validate flow-state edits; install cclaw and re-run.\\n' >&2
|
|
532
|
-
return 1
|
|
533
|
-
fi
|
|
534
|
-
local verify_cmd=(cclaw internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
|
|
535
|
-
|
|
536
|
-
if "\${verify_cmd[@]}" >/dev/null 2>&1; then
|
|
537
|
-
rm -f "$tmp_file" 2>/dev/null || true
|
|
538
|
-
return 0
|
|
539
|
-
fi
|
|
540
|
-
|
|
541
|
-
rm -f "$tmp_file" 2>/dev/null || true
|
|
542
|
-
return 1
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
is_preimplementation_stage() {
|
|
546
|
-
case "$1" in
|
|
547
|
-
brainstorm|scope|design|spec|plan) return 0 ;;
|
|
548
|
-
*) return 1 ;;
|
|
549
|
-
esac
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
normalize_payload_path() {
|
|
553
|
-
local raw="$1"
|
|
554
|
-
local normalized="$raw"
|
|
555
|
-
normalized=$(printf '%s' "$normalized" | tr '\\\\' '/')
|
|
556
|
-
normalized=$(printf '%s' "$normalized" | tr '[:upper:]' '[:lower:]')
|
|
557
|
-
normalized="\${normalized#./}"
|
|
558
|
-
printf '%s' "$normalized"
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
extract_payload_paths() {
|
|
562
|
-
if command -v jq >/dev/null 2>&1; then
|
|
563
|
-
printf '%s' "$INPUT" | jq -r '
|
|
564
|
-
[.. | objects | (.path?, .file_path?, .filepath?) | select(type == "string" and length > 0)]
|
|
565
|
-
| unique
|
|
566
|
-
| .[]
|
|
567
|
-
' 2>/dev/null || printf ''
|
|
568
|
-
return 0
|
|
569
|
-
fi
|
|
570
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
571
|
-
INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
572
|
-
import json
|
|
573
|
-
import os
|
|
574
|
-
|
|
575
|
-
def visit(node, acc):
|
|
576
|
-
if isinstance(node, dict):
|
|
577
|
-
for key in ("path", "file_path", "filepath"):
|
|
578
|
-
value = node.get(key)
|
|
579
|
-
if isinstance(value, str) and value.strip():
|
|
580
|
-
acc.add(value.strip())
|
|
581
|
-
for value in node.values():
|
|
582
|
-
visit(value, acc)
|
|
583
|
-
elif isinstance(node, list):
|
|
584
|
-
for value in node:
|
|
585
|
-
visit(value, acc)
|
|
586
|
-
|
|
587
|
-
try:
|
|
588
|
-
payload = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
589
|
-
except Exception:
|
|
590
|
-
payload = {}
|
|
591
|
-
|
|
592
|
-
items = set()
|
|
593
|
-
visit(payload, items)
|
|
594
|
-
for value in sorted(items):
|
|
595
|
-
print(value)
|
|
596
|
-
PY
|
|
597
|
-
return 0
|
|
598
|
-
fi
|
|
599
|
-
printf ''
|
|
600
|
-
return 0
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
matches_path_patterns() {
|
|
604
|
-
local candidate="$1"
|
|
605
|
-
local patterns_csv="$2"
|
|
606
|
-
[ -n "$candidate" ] || return 1
|
|
607
|
-
[ -n "$patterns_csv" ] || return 1
|
|
608
|
-
local old_ifs="$IFS"
|
|
609
|
-
IFS=','
|
|
610
|
-
for pattern in $patterns_csv; do
|
|
611
|
-
local normalized_pattern
|
|
612
|
-
normalized_pattern=$(normalize_payload_path "$pattern")
|
|
613
|
-
[ -n "$normalized_pattern" ] || continue
|
|
614
|
-
case "$candidate" in
|
|
615
|
-
$normalized_pattern)
|
|
616
|
-
IFS="$old_ifs"
|
|
617
|
-
return 0
|
|
618
|
-
;;
|
|
619
|
-
esac
|
|
620
|
-
done
|
|
621
|
-
IFS="$old_ifs"
|
|
622
|
-
return 1
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
is_code_like_path() {
|
|
626
|
-
local candidate="$1"
|
|
627
|
-
printf '%s' "$candidate" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$'
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
is_tdd_test_payload() {
|
|
631
|
-
local text="$1"
|
|
632
|
-
local payload_paths="$2"
|
|
633
|
-
if [ -n "$payload_paths" ]; then
|
|
634
|
-
while IFS= read -r raw_path; do
|
|
635
|
-
[ -n "$raw_path" ] || continue
|
|
636
|
-
local normalized
|
|
637
|
-
normalized=$(normalize_payload_path "$raw_path")
|
|
638
|
-
if matches_path_patterns "$normalized" "$TDD_TEST_PATH_PATTERNS"; then
|
|
639
|
-
return 0
|
|
640
|
-
fi
|
|
641
|
-
done <<< "$payload_paths"
|
|
642
|
-
fi
|
|
643
|
-
if printf '%s' "$text" | grep -Eq '/tests?/|/__tests__/|\\.test\\.'; then
|
|
644
|
-
return 0
|
|
645
|
-
fi
|
|
646
|
-
return 1
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
is_tdd_production_path() {
|
|
650
|
-
local normalized="$1"
|
|
651
|
-
[ -n "$normalized" ] || return 1
|
|
652
|
-
if printf '%s' "$normalized" | grep -Eq '(^|/)\\.cclaw/'; then
|
|
653
|
-
return 1
|
|
654
|
-
fi
|
|
655
|
-
if matches_path_patterns "$normalized" "$TDD_TEST_PATH_PATTERNS"; then
|
|
656
|
-
return 1
|
|
657
|
-
fi
|
|
658
|
-
if [ -n "$TDD_PRODUCTION_PATH_PATTERNS" ]; then
|
|
659
|
-
matches_path_patterns "$normalized" "$TDD_PRODUCTION_PATH_PATTERNS"
|
|
660
|
-
return $?
|
|
661
|
-
fi
|
|
662
|
-
is_code_like_path "$normalized"
|
|
663
|
-
return $?
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
is_tdd_production_write_payload() {
|
|
667
|
-
local text="$1"
|
|
668
|
-
local payload_paths="$2"
|
|
669
|
-
if [ -n "$payload_paths" ]; then
|
|
670
|
-
while IFS= read -r raw_path; do
|
|
671
|
-
[ -n "$raw_path" ] || continue
|
|
672
|
-
local normalized
|
|
673
|
-
normalized=$(normalize_payload_path "$raw_path")
|
|
674
|
-
if is_tdd_production_path "$normalized"; then
|
|
675
|
-
return 0
|
|
676
|
-
fi
|
|
677
|
-
done <<< "$payload_paths"
|
|
678
|
-
return 1
|
|
679
|
-
fi
|
|
680
|
-
if [ -n "$TDD_PRODUCTION_PATH_PATTERNS" ]; then
|
|
681
|
-
return 1
|
|
682
|
-
fi
|
|
683
|
-
if printf '%s' "$text" | grep -Eq '\\.cclaw/'; then
|
|
684
|
-
return 1
|
|
685
|
-
fi
|
|
686
|
-
if ! printf '%s' "$text" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)'; then
|
|
687
|
-
return 1
|
|
688
|
-
fi
|
|
689
|
-
if is_tdd_test_payload "$text" ""; then
|
|
690
|
-
return 1
|
|
691
|
-
fi
|
|
692
|
-
return 0
|
|
693
|
-
}
|
|
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
|
-
|
|
751
|
-
tdd_cycle_counts() {
|
|
752
|
-
if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
|
|
753
|
-
printf '0:0'
|
|
754
|
-
return 0
|
|
755
|
-
fi
|
|
756
|
-
local red_count="0"
|
|
757
|
-
local green_count="0"
|
|
758
|
-
if command -v jq >/dev/null 2>&1 && jq -n '1' >/dev/null 2>&1; then
|
|
759
|
-
red_count=$(jq -r --arg run "$CURRENT_RUN" 'select((.runId // $run) == $run and .phase == "red") | .phase' "$TDD_LOG_FILE" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
760
|
-
green_count=$(jq -r --arg run "$CURRENT_RUN" 'select((.runId // $run) == $run and .phase == "green") | .phase' "$TDD_LOG_FILE" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
761
|
-
elif command -v python3 >/dev/null 2>&1 && python3 - <<'PY' >/dev/null 2>&1
|
|
762
|
-
print("ok")
|
|
763
|
-
PY
|
|
764
|
-
then
|
|
765
|
-
red_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
|
|
766
|
-
import json
|
|
767
|
-
import sys
|
|
768
|
-
count = 0
|
|
769
|
-
run_id = sys.argv[2]
|
|
770
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
771
|
-
for raw in fh:
|
|
772
|
-
raw = raw.strip()
|
|
773
|
-
if not raw:
|
|
774
|
-
continue
|
|
775
|
-
try:
|
|
776
|
-
parsed = json.loads(raw)
|
|
777
|
-
except Exception:
|
|
778
|
-
continue
|
|
779
|
-
if not isinstance(parsed, dict):
|
|
780
|
-
continue
|
|
781
|
-
if str(parsed.get("runId", run_id)) != run_id:
|
|
782
|
-
continue
|
|
783
|
-
if parsed.get("phase") == "red":
|
|
784
|
-
count += 1
|
|
785
|
-
print(count)
|
|
786
|
-
PY
|
|
787
|
-
)
|
|
788
|
-
green_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
|
|
789
|
-
import json
|
|
790
|
-
import sys
|
|
791
|
-
count = 0
|
|
792
|
-
run_id = sys.argv[2]
|
|
793
|
-
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
794
|
-
for raw in fh:
|
|
795
|
-
raw = raw.strip()
|
|
796
|
-
if not raw:
|
|
797
|
-
continue
|
|
798
|
-
try:
|
|
799
|
-
parsed = json.loads(raw)
|
|
800
|
-
except Exception:
|
|
801
|
-
continue
|
|
802
|
-
if not isinstance(parsed, dict):
|
|
803
|
-
continue
|
|
804
|
-
if str(parsed.get("runId", run_id)) != run_id:
|
|
805
|
-
continue
|
|
806
|
-
if parsed.get("phase") == "green":
|
|
807
|
-
count += 1
|
|
808
|
-
print(count)
|
|
809
|
-
PY
|
|
810
|
-
)
|
|
811
|
-
else
|
|
812
|
-
if command -v awk >/dev/null 2>&1; then
|
|
813
|
-
local fallback_counts
|
|
814
|
-
fallback_counts=$(awk -v run="$CURRENT_RUN" '
|
|
815
|
-
BEGIN { red=0; green=0; }
|
|
816
|
-
{
|
|
817
|
-
line=$0;
|
|
818
|
-
line_run=run;
|
|
819
|
-
if (match(line, /"runId"[[:space:]]*:[[:space:]]*"[^"]+"/)) {
|
|
820
|
-
line_run=substr(line, RSTART, RLENGTH);
|
|
821
|
-
sub(/.*"/, "", line_run);
|
|
822
|
-
sub(/"$/, "", line_run);
|
|
823
|
-
}
|
|
824
|
-
if (line_run != run) next;
|
|
825
|
-
if (match(line, /"phase"[[:space:]]*:[[:space:]]*"[^"]+"/)) {
|
|
826
|
-
phase=substr(line, RSTART, RLENGTH);
|
|
827
|
-
sub(/.*"/, "", phase);
|
|
828
|
-
sub(/"$/, "", phase);
|
|
829
|
-
if (phase == "red") red += 1;
|
|
830
|
-
else if (phase == "green") green += 1;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
END { printf "%d:%d", red, green; }
|
|
834
|
-
' "$TDD_LOG_FILE" 2>/dev/null || true)
|
|
835
|
-
if printf '%s' "$fallback_counts" | grep -Eq '^[0-9]+:[0-9]+$'; then
|
|
836
|
-
printf '%s' "$fallback_counts"
|
|
837
|
-
return 0
|
|
838
|
-
fi
|
|
839
|
-
fi
|
|
840
|
-
printf '__UNAVAILABLE__'
|
|
841
|
-
return 0
|
|
842
|
-
fi
|
|
843
|
-
[ -n "$red_count" ] || red_count="0"
|
|
844
|
-
[ -n "$green_count" ] || green_count="0"
|
|
845
|
-
printf '%s:%s' "$red_count" "$green_count"
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
has_open_red_cycle() {
|
|
849
|
-
local counts
|
|
850
|
-
counts=$(tdd_cycle_counts)
|
|
851
|
-
if [ "$counts" = "__UNAVAILABLE__" ]; then
|
|
852
|
-
return 2
|
|
853
|
-
fi
|
|
854
|
-
local red_count="\${counts%%:*}"
|
|
855
|
-
local green_count="\${counts##*:}"
|
|
856
|
-
if ! printf '%s' "$red_count:$green_count" | grep -Eq '^[0-9]+:[0-9]+$'; then
|
|
857
|
-
return 2
|
|
858
|
-
fi
|
|
859
|
-
if [ "$red_count" -gt "$green_count" ]; then
|
|
860
|
-
return 0
|
|
861
|
-
fi
|
|
862
|
-
return 1
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
tdd_cycle_state() {
|
|
866
|
-
local counts
|
|
867
|
-
counts=$(tdd_cycle_counts)
|
|
868
|
-
if [ "$counts" = "__UNAVAILABLE__" ]; then
|
|
869
|
-
printf '__UNAVAILABLE__'
|
|
870
|
-
return 0
|
|
871
|
-
fi
|
|
872
|
-
local red_count="\${counts%%:*}"
|
|
873
|
-
local green_count="\${counts##*:}"
|
|
874
|
-
if ! printf '%s' "$red_count:$green_count" | grep -Eq '^[0-9]+:[0-9]+$'; then
|
|
875
|
-
printf '__UNAVAILABLE__'
|
|
876
|
-
return 0
|
|
877
|
-
fi
|
|
878
|
-
if [ "$red_count" -le 0 ]; then
|
|
879
|
-
printf 'need_red'
|
|
880
|
-
return 0
|
|
881
|
-
fi
|
|
882
|
-
if [ "$red_count" -gt "$green_count" ]; then
|
|
883
|
-
printf 'red_open'
|
|
884
|
-
return 0
|
|
885
|
-
fi
|
|
886
|
-
printf 'green_done'
|
|
887
|
-
return 0
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
detect_target_stage() {
|
|
891
|
-
local text="$1"
|
|
892
|
-
for stage in brainstorm scope design spec plan tdd review ship; do
|
|
893
|
-
if printf '%s' "$text" | grep -Eq "(/cc-$stage|cc-$stage)([^[:alnum:]_-]|$)"; then
|
|
894
|
-
printf '%s' "$stage"
|
|
895
|
-
return 0
|
|
896
|
-
fi
|
|
897
|
-
done
|
|
898
|
-
printf ''
|
|
899
|
-
return 0
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
is_flow_progression_command() {
|
|
903
|
-
local text="$1"
|
|
904
|
-
if printf '%s' "$text" | grep -Eq '(/cc-next|cc-next)([^[:alnum:]_-]|$)'; then
|
|
905
|
-
return 0
|
|
906
|
-
fi
|
|
907
|
-
if printf '%s' "$text" | grep -Eq '/cc([^[:alnum:]_-]|$)'; then
|
|
908
|
-
return 0
|
|
909
|
-
fi
|
|
910
|
-
return 1
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
TARGET_STAGE=$(detect_target_stage "$PAYLOAD_LOWER")
|
|
914
|
-
FLOW_COMMAND_INVOKED=0
|
|
915
|
-
if is_flow_progression_command "$PAYLOAD_LOWER"; then
|
|
916
|
-
FLOW_COMMAND_INVOKED=1
|
|
917
|
-
fi
|
|
918
|
-
MUTATION_PATHS=""
|
|
919
|
-
if is_mutating_tool "$TOOL_LOWER"; then
|
|
920
|
-
MUTATION_PATHS=$(extract_payload_paths)
|
|
921
|
-
fi
|
|
922
|
-
TDD_CYCLE_STATE="unknown"
|
|
923
|
-
if [ -n "$TARGET_STAGE" ] && [ "$CURRENT_STAGE" != "none" ]; then
|
|
924
|
-
CURRENT_IDX=$(stage_index "$CURRENT_STAGE")
|
|
925
|
-
TARGET_IDX=$(stage_index "$TARGET_STAGE")
|
|
926
|
-
if [ "$CURRENT_IDX" -gt 0 ] && [ "$TARGET_IDX" -gt 0 ]; then
|
|
927
|
-
if [ "$TARGET_IDX" -gt $((CURRENT_IDX + 1)) ]; then
|
|
928
|
-
REASONS="stage_jump_\${CURRENT_STAGE}_to_\${TARGET_STAGE}"
|
|
929
|
-
fi
|
|
930
|
-
fi
|
|
931
|
-
fi
|
|
932
|
-
|
|
933
|
-
if is_mutating_tool "$TOOL_LOWER" && printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/state/flow-state\.json'; then
|
|
934
|
-
if [ -n "$REASONS" ]; then
|
|
935
|
-
REASONS="$REASONS,direct_flow_state_edit"
|
|
936
|
-
else
|
|
937
|
-
REASONS="direct_flow_state_edit"
|
|
938
|
-
fi
|
|
939
|
-
FLOW_STATE_AFTER_JSON=$(extract_flow_state_after_json)
|
|
940
|
-
if [ -n "$FLOW_STATE_AFTER_JSON" ]; then
|
|
941
|
-
if ! verify_flow_state_candidate "$FLOW_STATE_AFTER_JSON"; then
|
|
942
|
-
REASONS="$REASONS,flow_state_edit_failed_internal_validation"
|
|
943
|
-
fi
|
|
944
|
-
else
|
|
945
|
-
REASONS="$REASONS,flow_state_edit_without_serialized_content"
|
|
946
|
-
fi
|
|
947
|
-
fi
|
|
948
|
-
|
|
949
|
-
if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"; then
|
|
950
|
-
if ! printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/'; then
|
|
951
|
-
if [ -n "$REASONS" ]; then
|
|
952
|
-
REASONS="$REASONS,implementation_write_before_\${CURRENT_STAGE}_completion"
|
|
953
|
-
else
|
|
954
|
-
REASONS="implementation_write_before_\${CURRENT_STAGE}_completion"
|
|
955
|
-
fi
|
|
956
|
-
fi
|
|
957
|
-
fi
|
|
958
|
-
|
|
959
|
-
if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
|
|
960
|
-
TDD_MISSING_RED_PATHS=""
|
|
961
|
-
if is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
|
|
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
|
|
981
|
-
if [ -n "$REASONS" ]; then
|
|
982
|
-
REASONS="$REASONS,tdd_red_evidence_check_failed"
|
|
983
|
-
else
|
|
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"
|
|
992
|
-
fi
|
|
993
|
-
fi
|
|
994
|
-
fi
|
|
995
|
-
if [ "$PER_PATH_RED_CHECKED" != "true" ]; then
|
|
996
|
-
if has_open_red_cycle; then
|
|
997
|
-
TDD_CYCLE_STATE="red_open"
|
|
998
|
-
else
|
|
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
|
|
1010
|
-
fi
|
|
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
|
|
1023
|
-
fi
|
|
1024
|
-
fi
|
|
1025
|
-
fi
|
|
1026
|
-
fi
|
|
1027
|
-
|
|
1028
|
-
if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; 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
|
|
1040
|
-
if [ -n "$REASONS" ]; then
|
|
1041
|
-
REASONS="$REASONS,tdd_red_agent_cannot_write_production"
|
|
1042
|
-
else
|
|
1043
|
-
REASONS="tdd_red_agent_cannot_write_production"
|
|
1044
|
-
fi
|
|
1045
|
-
fi
|
|
1046
|
-
if [ "$ACTIVE_AGENT_EFFECTIVE" = "tdd-green" ] && is_tdd_test_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
|
|
1047
|
-
if [ -n "$REASONS" ]; then
|
|
1048
|
-
REASONS="$REASONS,tdd_green_agent_cannot_write_tests"
|
|
1049
|
-
else
|
|
1050
|
-
REASONS="tdd_green_agent_cannot_write_tests"
|
|
1051
|
-
fi
|
|
1052
|
-
fi
|
|
1053
|
-
if [ "$ACTIVE_AGENT_EFFECTIVE" = "tdd-refactor" ]; then
|
|
1054
|
-
TDD_AGENT_STATE=$(tdd_cycle_state)
|
|
1055
|
-
if [ "$TDD_AGENT_STATE" != "green_done" ]; then
|
|
1056
|
-
if [ -n "$REASONS" ]; then
|
|
1057
|
-
REASONS="$REASONS,tdd_refactor_before_green"
|
|
1058
|
-
else
|
|
1059
|
-
REASONS="tdd_refactor_before_green"
|
|
1060
|
-
fi
|
|
1061
|
-
fi
|
|
1062
|
-
fi
|
|
1063
|
-
fi
|
|
1064
|
-
|
|
1065
|
-
if is_mutating_tool "$TOOL_LOWER"; then
|
|
1066
|
-
if [ "$LAST_FLOW_READ_AT" -le 0 ] || [ "$NOW_EPOCH" -le 0 ] || [ $((NOW_EPOCH - LAST_FLOW_READ_AT)) -gt "$MAX_FLOW_READ_AGE_SEC" ]; then
|
|
1067
|
-
if [ -n "$REASONS" ]; then
|
|
1068
|
-
REASONS="$REASONS,mutating_without_recent_flow_read"
|
|
1069
|
-
else
|
|
1070
|
-
REASONS="mutating_without_recent_flow_read"
|
|
1071
|
-
fi
|
|
1072
|
-
fi
|
|
1073
|
-
fi
|
|
1074
|
-
|
|
1075
|
-
if is_mutating_tool "$TOOL_LOWER" && printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/(state|hooks|skills)'; then
|
|
1076
|
-
if ! is_cclaw_cli_payload "$PAYLOAD_LOWER"; then
|
|
1077
|
-
if [ -n "$REASONS" ]; then
|
|
1078
|
-
REASONS="$REASONS,runtime_write_requires_managed_only"
|
|
1079
|
-
else
|
|
1080
|
-
REASONS="runtime_write_requires_managed_only"
|
|
1081
|
-
fi
|
|
1082
|
-
fi
|
|
1083
|
-
fi
|
|
1084
|
-
|
|
1085
|
-
if [ "$CURRENT_STAGE" = "ship" ] && is_execution_or_mutating_tool "$TOOL_LOWER"; then
|
|
1086
|
-
if printf '%s' "$PAYLOAD_LOWER" | grep -Eq '(npm publish|pnpm publish|yarn publish|gh release create|git push[[:space:]].*--tags|npm version)'; then
|
|
1087
|
-
if [ "$SHIP_PREFLIGHT_PASSED" != "true" ]; then
|
|
1088
|
-
if [ -n "$REASONS" ]; then
|
|
1089
|
-
REASONS="$REASONS,ship_preflight_required"
|
|
1090
|
-
else
|
|
1091
|
-
REASONS="ship_preflight_required"
|
|
1092
|
-
fi
|
|
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
|
|
1101
|
-
fi
|
|
1102
|
-
fi
|
|
1103
|
-
|
|
1104
|
-
if is_preimplementation_stage "$CURRENT_STAGE" && ! is_plan_mode_safe_tool "$TOOL_LOWER"; then
|
|
1105
|
-
if ! is_mutating_tool "$TOOL_LOWER"; then
|
|
1106
|
-
if ! printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/' && ! is_cclaw_cli_payload "$PAYLOAD_LOWER"; then
|
|
1107
|
-
if [ -n "$REASONS" ]; then
|
|
1108
|
-
REASONS="$REASONS,non_safe_tool_in_plan_stage_\${CURRENT_STAGE}"
|
|
1109
|
-
else
|
|
1110
|
-
REASONS="non_safe_tool_in_plan_stage_\${CURRENT_STAGE}"
|
|
1111
|
-
fi
|
|
1112
|
-
fi
|
|
1113
|
-
fi
|
|
1114
|
-
fi
|
|
1115
|
-
|
|
1116
|
-
if [ -n "$TARGET_STAGE" ] || [ "$FLOW_COMMAND_INVOKED" -eq 1 ]; then
|
|
1117
|
-
if [ "$LAST_FLOW_READ_AT" -le 0 ] || [ "$NOW_EPOCH" -le 0 ] || [ $((NOW_EPOCH - LAST_FLOW_READ_AT)) -gt "$MAX_FLOW_READ_AGE_SEC" ]; then
|
|
1118
|
-
if [ -n "$REASONS" ]; then
|
|
1119
|
-
REASONS="$REASONS,stage_invocation_without_recent_flow_read"
|
|
1120
|
-
else
|
|
1121
|
-
REASONS="stage_invocation_without_recent_flow_read"
|
|
1122
|
-
fi
|
|
1123
|
-
fi
|
|
1124
|
-
fi
|
|
1125
|
-
|
|
1126
|
-
SHOULD_RECORD_FLOW_READ=0
|
|
1127
|
-
case "$TOOL_LOWER" in
|
|
1128
|
-
read|readfile|open|view|cat) SHOULD_RECORD_FLOW_READ=1 ;;
|
|
1129
|
-
shell|runcommand|run_command|execcommand|exec_command|terminal) SHOULD_RECORD_FLOW_READ=1 ;;
|
|
1130
|
-
esac
|
|
1131
|
-
|
|
1132
|
-
if [ "$SHOULD_RECORD_FLOW_READ" -eq 1 ] && printf '%s' "$PAYLOAD_LOWER" | grep -Eq '(\.cclaw/state/flow-state\.json|cclaw doctor|cclaw sync)'; then
|
|
1133
|
-
TMP_STATE_FILE="$GUARD_STATE_FILE.tmp.$$"
|
|
1134
|
-
if command -v jq >/dev/null 2>&1 && [ -f "$GUARD_STATE_FILE" ]; then
|
|
1135
|
-
jq --arg ts "$TS" --argjson epoch "$NOW_EPOCH" '
|
|
1136
|
-
.lastFlowReadAt = $ts
|
|
1137
|
-
| .lastFlowReadAtEpoch = $epoch
|
|
1138
|
-
' "$GUARD_STATE_FILE" > "$TMP_STATE_FILE" 2>/dev/null || true
|
|
1139
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
1140
|
-
python3 - "$GUARD_STATE_FILE" "$TMP_STATE_FILE" "$TS" "$NOW_EPOCH" <<'PY'
|
|
1141
|
-
import json
|
|
1142
|
-
import sys
|
|
1143
|
-
from pathlib import Path
|
|
1144
|
-
source = Path(sys.argv[1])
|
|
1145
|
-
target = Path(sys.argv[2])
|
|
1146
|
-
ts = sys.argv[3]
|
|
1147
|
-
epoch = int(float(sys.argv[4])) if sys.argv[4] else 0
|
|
1148
|
-
payload = {}
|
|
1149
|
-
if source.exists():
|
|
1150
|
-
try:
|
|
1151
|
-
raw = json.loads(source.read_text(encoding="utf-8"))
|
|
1152
|
-
if isinstance(raw, dict):
|
|
1153
|
-
payload.update(raw)
|
|
1154
|
-
except Exception:
|
|
1155
|
-
pass
|
|
1156
|
-
payload["lastFlowReadAt"] = ts
|
|
1157
|
-
payload["lastFlowReadAtEpoch"] = epoch
|
|
1158
|
-
target.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
|
|
1159
|
-
PY
|
|
1160
|
-
fi
|
|
1161
|
-
if [ -s "$TMP_STATE_FILE" ]; then
|
|
1162
|
-
mv "$TMP_STATE_FILE" "$GUARD_STATE_FILE" 2>/dev/null || rm -f "$TMP_STATE_FILE" 2>/dev/null || true
|
|
1163
|
-
else
|
|
1164
|
-
printf '{\\n "lastFlowReadAt": "%s",\\n "lastFlowReadAtEpoch": %s\\n}\\n' "$TS" "$NOW_EPOCH" > "$GUARD_STATE_FILE" 2>/dev/null || true
|
|
1165
|
-
fi
|
|
1166
|
-
fi
|
|
1167
|
-
|
|
1168
|
-
if [ -n "$REASONS" ]; 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
|
|
1176
|
-
NOTE="Cclaw workflow guard: tdd-red agent is limited to test-side RED work and cannot edit production files."
|
|
1177
|
-
elif printf '%s' "$REASONS" | grep -Eq 'tdd_green_agent_cannot_write_tests'; then
|
|
1178
|
-
NOTE="Cclaw workflow guard: tdd-green agent can implement production fixes but should not author new RED tests."
|
|
1179
|
-
elif printf '%s' "$REASONS" | grep -Eq 'tdd_refactor_before_green'; then
|
|
1180
|
-
NOTE="Cclaw workflow guard: tdd-refactor requires a green_done cycle state before refactor edits."
|
|
1181
|
-
elif printf '%s' "$REASONS" | grep -Eq 'ship_preflight_required'; then
|
|
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."
|
|
1185
|
-
elif printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
|
|
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."
|
|
1187
|
-
elif printf '%s' "$REASONS" | grep -Eq 'runtime_write_requires_managed_only|direct_flow_state_edit'; then
|
|
1188
|
-
NOTE="Cclaw workflow guard: runtime write to managed ${RUNTIME_ROOT} internals detected (\${REASONS}). Prefer cclaw-managed helpers (stage-complete, sync, command contracts) instead of ad-hoc edits."
|
|
1189
|
-
elif printf '%s' "$REASONS" | grep -Eq 'mutating_without_recent_flow_read'; then
|
|
1190
|
-
NOTE="Cclaw workflow guard: mutating action requires a fresh read of ${RUNTIME_ROOT}/state/flow-state.json before edits."
|
|
1191
|
-
else
|
|
1192
|
-
NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json, avoid source edits before tdd stage, and enforce RED -> GREEN -> REFACTOR discipline inside tdd."
|
|
1193
|
-
fi
|
|
1194
|
-
if command -v jq >/dev/null 2>&1; then
|
|
1195
|
-
ENTRY=$(jq -n -c \
|
|
1196
|
-
--arg ts "$TS" \
|
|
1197
|
-
--arg tool "$TOOL" \
|
|
1198
|
-
--arg stage "$CURRENT_STAGE" \
|
|
1199
|
-
--arg target "$TARGET_STAGE" \
|
|
1200
|
-
--arg reasons "$REASONS" \
|
|
1201
|
-
--arg note "$NOTE" \
|
|
1202
|
-
'{ts:$ts,tool:$tool,currentStage:$stage,targetStage:$target,reasons:($reasons|split(",")),note:$note}' 2>/dev/null || echo "")
|
|
1203
|
-
else
|
|
1204
|
-
ENTRY=""
|
|
1205
|
-
fi
|
|
1206
|
-
if [ -n "$ENTRY" ]; then
|
|
1207
|
-
printf '%s\n' "$ENTRY" >> "$GUARD_LOG" 2>/dev/null || true
|
|
1208
|
-
fi
|
|
1209
|
-
SHOULD_BLOCK="false"
|
|
1210
|
-
if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_'; then
|
|
1211
|
-
SHOULD_BLOCK="true"
|
|
1212
|
-
fi
|
|
1213
|
-
if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red|tdd_write_without_red_for_path' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
|
|
1214
|
-
SHOULD_BLOCK="true"
|
|
1215
|
-
fi
|
|
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
|
|
1217
|
-
SHOULD_BLOCK="true"
|
|
1218
|
-
fi
|
|
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
|
|
1220
|
-
SHOULD_BLOCK="true"
|
|
1221
|
-
fi
|
|
1222
|
-
if printf '%s' "$REASONS" | grep -Eq 'mutating_without_recent_flow_read|stage_invocation_without_recent_flow_read' && iron_law_is_strict "flow-state-read-fresh"; then
|
|
1223
|
-
SHOULD_BLOCK="true"
|
|
1224
|
-
fi
|
|
1225
|
-
if printf '%s' "$REASONS" | grep -Eq 'ship_preflight_required' && iron_law_is_strict "ship-preflight-required"; then
|
|
1226
|
-
SHOULD_BLOCK="true"
|
|
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
|
|
1231
|
-
if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_plan_completion' && iron_law_is_strict "plan-requires-approval"; then
|
|
1232
|
-
SHOULD_BLOCK="true"
|
|
1233
|
-
fi
|
|
1234
|
-
if printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable|tdd_red_evidence_check_failed'; then
|
|
1235
|
-
SHOULD_BLOCK="true"
|
|
1236
|
-
fi
|
|
1237
|
-
if printf '%s' "$REASONS" | grep -Eq 'tdd_red_agent_cannot_write_production|tdd_green_agent_cannot_write_tests|tdd_refactor_before_green'; then
|
|
1238
|
-
SHOULD_BLOCK="true"
|
|
1239
|
-
fi
|
|
1240
|
-
if [ "$WORKFLOW_GUARD_MODE" = "strict" ] || [ "$SHOULD_BLOCK" = "true" ]; then
|
|
1241
|
-
printf '[cclaw] %s (blocked by workflow guard)\n' "$NOTE" >&2
|
|
1242
|
-
exit 1
|
|
1243
|
-
fi
|
|
1244
|
-
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
1245
|
-
fi
|
|
1246
|
-
|
|
1247
|
-
exit 0
|
|
1248
|
-
`;
|
|
1249
|
-
}
|
|
1250
|
-
export function contextMonitorScript() {
|
|
1251
|
-
return `#!/usr/bin/env bash
|
|
1252
|
-
# cclaw context monitor hook — generated by cclaw sync
|
|
1253
|
-
# Advisory-only context pressure warnings (best effort).
|
|
1254
|
-
set -uo pipefail
|
|
1255
|
-
|
|
1256
|
-
${RUNTIME_SHELL_DETECT_ROOT}
|
|
1257
|
-
|
|
1258
|
-
STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
|
|
1259
|
-
MONITOR_STATE="$STATE_DIR/context-monitor.json"
|
|
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"
|
|
1263
|
-
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
1264
|
-
|
|
1265
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
1266
|
-
[ -n "$INPUT" ] || exit 0
|
|
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
|
-
|
|
1464
|
-
REMAINING_PERCENT=""
|
|
1465
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
1466
|
-
REMAINING_PERCENT=$(INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
1467
|
-
import json
|
|
1468
|
-
import os
|
|
1469
|
-
from typing import Any
|
|
1470
|
-
|
|
1471
|
-
raw = os.environ.get("INPUT_JSON", "{}")
|
|
1472
|
-
try:
|
|
1473
|
-
payload = json.loads(raw)
|
|
1474
|
-
except Exception:
|
|
1475
|
-
print("")
|
|
1476
|
-
raise SystemExit(0)
|
|
1477
|
-
|
|
1478
|
-
def pick(path: list[str]) -> Any:
|
|
1479
|
-
node: Any = payload
|
|
1480
|
-
for key in path:
|
|
1481
|
-
if not isinstance(node, dict):
|
|
1482
|
-
return None
|
|
1483
|
-
node = node.get(key)
|
|
1484
|
-
return node
|
|
1485
|
-
|
|
1486
|
-
def as_percent(value: Any, invert: bool = False):
|
|
1487
|
-
if not isinstance(value, (int, float)):
|
|
1488
|
-
return None
|
|
1489
|
-
number = float(value)
|
|
1490
|
-
if number <= 1.0:
|
|
1491
|
-
number *= 100.0
|
|
1492
|
-
if invert:
|
|
1493
|
-
number = 100.0 - number
|
|
1494
|
-
if number < 0:
|
|
1495
|
-
number = 0.0
|
|
1496
|
-
if number > 100:
|
|
1497
|
-
number = 100.0
|
|
1498
|
-
return number
|
|
1499
|
-
|
|
1500
|
-
candidates = [
|
|
1501
|
-
(["context", "remaining_percent"], False),
|
|
1502
|
-
(["context", "remainingPercent"], False),
|
|
1503
|
-
(["context_usage", "remaining_percent"], False),
|
|
1504
|
-
(["context_usage", "remainingPercent"], False),
|
|
1505
|
-
(["contextUsage", "remainingPercent"], False),
|
|
1506
|
-
(["context_window", "remaining_percent"], False),
|
|
1507
|
-
(["remaining_context_percent"], False),
|
|
1508
|
-
(["remainingContextPercent"], False),
|
|
1509
|
-
(["remaining_context_ratio"], False),
|
|
1510
|
-
(["remainingContextRatio"], False),
|
|
1511
|
-
(["context", "used_percent"], True),
|
|
1512
|
-
(["context", "usedPercent"], True),
|
|
1513
|
-
(["context_usage", "used_percent"], True),
|
|
1514
|
-
(["context_usage", "usedPercent"], True),
|
|
1515
|
-
(["contextUsage", "usedPercent"], True),
|
|
1516
|
-
(["context_window", "used_ratio"], True),
|
|
1517
|
-
(["context_window", "usedRatio"], True),
|
|
1518
|
-
]
|
|
1519
|
-
|
|
1520
|
-
for path, invert in candidates:
|
|
1521
|
-
value = pick(path)
|
|
1522
|
-
percent = as_percent(value, invert=invert)
|
|
1523
|
-
if percent is not None:
|
|
1524
|
-
print(f"{percent:.2f}")
|
|
1525
|
-
raise SystemExit(0)
|
|
1526
|
-
|
|
1527
|
-
print("")
|
|
1528
|
-
PY
|
|
1529
|
-
)
|
|
1530
|
-
fi
|
|
1531
|
-
|
|
1532
|
-
[ -n "$REMAINING_PERCENT" ] || exit 0
|
|
1533
|
-
|
|
1534
|
-
BAND="none"
|
|
1535
|
-
if awk "BEGIN { exit !($REMAINING_PERCENT <= 20) }"; then
|
|
1536
|
-
BAND="critical"
|
|
1537
|
-
elif awk "BEGIN { exit !($REMAINING_PERCENT <= 35) }"; then
|
|
1538
|
-
BAND="warning"
|
|
1539
|
-
fi
|
|
1540
|
-
|
|
1541
|
-
TTL_SECONDS_RAW="\${CCLAW_CONTEXT_MONITOR_TTL_SEC:-900}"
|
|
1542
|
-
if printf '%s' "$TTL_SECONDS_RAW" | grep -Eq '^[0-9]+$'; then
|
|
1543
|
-
TTL_SECONDS="$TTL_SECONDS_RAW"
|
|
1544
|
-
else
|
|
1545
|
-
TTL_SECONDS="900"
|
|
1546
|
-
fi
|
|
1547
|
-
|
|
1548
|
-
LAST_BAND="none"
|
|
1549
|
-
LAST_ADVISORY_BAND="none"
|
|
1550
|
-
LAST_ADVISORY_AT=""
|
|
1551
|
-
LAST_ADVISORY_EPOCH="0"
|
|
1552
|
-
if [ -f "$MONITOR_STATE" ]; then
|
|
1553
|
-
if command -v jq >/dev/null 2>&1; then
|
|
1554
|
-
LAST_BAND=$(jq -r '.lastBand // "none"' "$MONITOR_STATE" 2>/dev/null || echo "none")
|
|
1555
|
-
LAST_ADVISORY_BAND=$(jq -r '.lastAdvisoryBand // .lastBand // "none"' "$MONITOR_STATE" 2>/dev/null || echo "none")
|
|
1556
|
-
LAST_ADVISORY_AT=$(jq -r '.lastAdvisoryAt // ""' "$MONITOR_STATE" 2>/dev/null || echo "")
|
|
1557
|
-
LAST_ADVISORY_EPOCH=$(jq -r 'try ((.lastAdvisoryAt // "" | fromdateiso8601)) catch 0' "$MONITOR_STATE" 2>/dev/null || echo "0")
|
|
1558
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
1559
|
-
STATE_META=$(python3 - "$MONITOR_STATE" <<'PY'
|
|
1560
|
-
import json
|
|
1561
|
-
import sys
|
|
1562
|
-
try:
|
|
1563
|
-
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
|
1564
|
-
value = json.load(handle)
|
|
1565
|
-
last_band = value.get("lastBand")
|
|
1566
|
-
if not isinstance(last_band, str):
|
|
1567
|
-
last_band = "none"
|
|
1568
|
-
advisory_band = value.get("lastAdvisoryBand")
|
|
1569
|
-
if not isinstance(advisory_band, str):
|
|
1570
|
-
advisory_band = last_band
|
|
1571
|
-
advisory_at = value.get("lastAdvisoryAt")
|
|
1572
|
-
if not isinstance(advisory_at, str):
|
|
1573
|
-
advisory_at = ""
|
|
1574
|
-
advisory_epoch = 0
|
|
1575
|
-
if advisory_at:
|
|
1576
|
-
try:
|
|
1577
|
-
from datetime import datetime
|
|
1578
|
-
normalized = advisory_at.replace("Z", "+00:00")
|
|
1579
|
-
advisory_epoch = int(datetime.fromisoformat(normalized).timestamp())
|
|
1580
|
-
except Exception:
|
|
1581
|
-
advisory_epoch = 0
|
|
1582
|
-
print(f"{last_band}|{advisory_band}|{advisory_at}|{advisory_epoch}")
|
|
1583
|
-
except Exception:
|
|
1584
|
-
print("none|none||0")
|
|
1585
|
-
PY
|
|
1586
|
-
)
|
|
1587
|
-
LAST_BAND=$(printf '%s' "$STATE_META" | cut -d'|' -f1)
|
|
1588
|
-
LAST_ADVISORY_BAND=$(printf '%s' "$STATE_META" | cut -d'|' -f2)
|
|
1589
|
-
LAST_ADVISORY_AT=$(printf '%s' "$STATE_META" | cut -d'|' -f3)
|
|
1590
|
-
LAST_ADVISORY_EPOCH=$(printf '%s' "$STATE_META" | cut -d'|' -f4)
|
|
1591
|
-
fi
|
|
1592
|
-
fi
|
|
1593
|
-
|
|
1594
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
1595
|
-
NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
|
|
1596
|
-
if ! printf '%s' "$LAST_ADVISORY_EPOCH" | grep -Eq '^[0-9]+$'; then
|
|
1597
|
-
LAST_ADVISORY_EPOCH="0"
|
|
1598
|
-
fi
|
|
1599
|
-
|
|
1600
|
-
SHOULD_EMIT="false"
|
|
1601
|
-
if [ "$BAND" != "none" ]; then
|
|
1602
|
-
if [ "$BAND" != "$LAST_ADVISORY_BAND" ]; then
|
|
1603
|
-
SHOULD_EMIT="true"
|
|
1604
|
-
elif [ "$TTL_SECONDS" -eq 0 ]; then
|
|
1605
|
-
SHOULD_EMIT="true"
|
|
1606
|
-
else
|
|
1607
|
-
ELAPSED=$((NOW_EPOCH - LAST_ADVISORY_EPOCH))
|
|
1608
|
-
if [ "$ELAPSED" -ge "$TTL_SECONDS" ]; then
|
|
1609
|
-
SHOULD_EMIT="true"
|
|
1610
|
-
fi
|
|
1611
|
-
fi
|
|
1612
|
-
fi
|
|
1613
|
-
|
|
1614
|
-
NEXT_ADVISORY_BAND="$LAST_ADVISORY_BAND"
|
|
1615
|
-
NEXT_ADVISORY_AT="$LAST_ADVISORY_AT"
|
|
1616
|
-
if [ "$SHOULD_EMIT" = "true" ]; then
|
|
1617
|
-
NOTE="Cclaw advisory: context remaining is \${REMAINING_PERCENT}% (\${BAND}). Consider checkpointing or compacting soon."
|
|
1618
|
-
if command -v jq >/dev/null 2>&1; then
|
|
1619
|
-
ENTRY=$(jq -n -c \
|
|
1620
|
-
--arg ts "$TS" \
|
|
1621
|
-
--arg harness "$HARNESS" \
|
|
1622
|
-
--arg band "$BAND" \
|
|
1623
|
-
--arg remaining "$REMAINING_PERCENT" \
|
|
1624
|
-
--arg note "$NOTE" \
|
|
1625
|
-
'{ts:$ts,harness:$harness,band:$band,remainingPercent:($remaining|tonumber),note:$note}' 2>/dev/null || echo "")
|
|
1626
|
-
else
|
|
1627
|
-
ENTRY=$(printf '{"ts":"%s","harness":"%s","band":"%s","remainingPercent":"%s","note":"%s"}' "$TS" "$HARNESS" "$BAND" "$REMAINING_PERCENT" "$NOTE")
|
|
1628
|
-
fi
|
|
1629
|
-
|
|
1630
|
-
if [ -n "$ENTRY" ]; then
|
|
1631
|
-
printf '%s\n' "$ENTRY" >> "$WARNINGS_FILE" 2>/dev/null || true
|
|
1632
|
-
fi
|
|
1633
|
-
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
1634
|
-
NEXT_ADVISORY_BAND="$BAND"
|
|
1635
|
-
NEXT_ADVISORY_AT="$TS"
|
|
1636
|
-
fi
|
|
1637
|
-
|
|
1638
|
-
TMP_STATE="$MONITOR_STATE.tmp.$$"
|
|
1639
|
-
if command -v jq >/dev/null 2>&1; then
|
|
1640
|
-
jq -n \
|
|
1641
|
-
--arg ts "$TS" \
|
|
1642
|
-
--arg band "$BAND" \
|
|
1643
|
-
--arg advisoryBand "$NEXT_ADVISORY_BAND" \
|
|
1644
|
-
--arg advisoryAt "$NEXT_ADVISORY_AT" \
|
|
1645
|
-
--arg remaining "$REMAINING_PERCENT" \
|
|
1646
|
-
--arg harness "$HARNESS" \
|
|
1647
|
-
'{lastUpdated:$ts,lastBand:$band,lastRemainingPercent:($remaining|tonumber),harness:$harness,lastAdvisoryBand:$advisoryBand,lastAdvisoryAt:$advisoryAt}' > "$TMP_STATE" 2>/dev/null || true
|
|
1648
|
-
else
|
|
1649
|
-
printf '{\n "lastUpdated": "%s",\n "lastBand": "%s",\n "lastRemainingPercent": %s,\n "harness": "%s",\n "lastAdvisoryBand": "%s",\n "lastAdvisoryAt": "%s"\n}\n' \
|
|
1650
|
-
"$TS" "$BAND" "$REMAINING_PERCENT" "$HARNESS" "$NEXT_ADVISORY_BAND" "$NEXT_ADVISORY_AT" > "$TMP_STATE" 2>/dev/null || true
|
|
1651
|
-
fi
|
|
1652
|
-
if [ -s "$TMP_STATE" ]; then
|
|
1653
|
-
mv "$TMP_STATE" "$MONITOR_STATE" 2>/dev/null || rm -f "$TMP_STATE" 2>/dev/null || true
|
|
1654
|
-
fi
|
|
1655
|
-
|
|
1656
|
-
exit 0
|
|
1657
|
-
`;
|
|
1658
|
-
}
|
|
1659
|
-
/**
|
|
1660
|
-
* Updated hooks.json generators with PreToolUse/PostToolUse observation.
|
|
1661
|
-
*/
|
|
1662
|
-
function hookDispatcherCommand(scriptName) {
|
|
1663
|
-
return `node ${RUNTIME_ROOT}/hooks/run-hook.mjs ${scriptName}`;
|
|
2
|
+
function hookDispatcherCommand(hookName) {
|
|
3
|
+
return `node ${RUNTIME_ROOT}/hooks/run-hook.mjs ${hookName}`;
|
|
1664
4
|
}
|
|
1665
5
|
export function claudeHooksJsonWithObservation() {
|
|
1666
6
|
return JSON.stringify({
|
|
@@ -1670,39 +10,33 @@ export function claudeHooksJsonWithObservation() {
|
|
|
1670
10
|
matcher: "startup|resume|clear|compact",
|
|
1671
11
|
hooks: [{
|
|
1672
12
|
type: "command",
|
|
1673
|
-
command: hookDispatcherCommand("session-start
|
|
13
|
+
command: hookDispatcherCommand("session-start")
|
|
1674
14
|
}]
|
|
1675
15
|
}],
|
|
1676
16
|
PreToolUse: [{
|
|
1677
|
-
// `prompt-guard.sh` inspects tool inputs across all tool calls;
|
|
1678
|
-
// it has to stay on `*` so it sees MCP/Edit/Write/WebSearch
|
|
1679
|
-
// traffic too. `workflow-guard.sh`, however, only checks TDD
|
|
1680
|
-
// ordering on write-like operations — it is a no-op for reads.
|
|
1681
|
-
// Splitting the two matchers cuts Claude's per-read hook
|
|
1682
|
-
// overhead in half without reducing coverage on write paths.
|
|
1683
17
|
matcher: "*",
|
|
1684
18
|
hooks: [{
|
|
1685
19
|
type: "command",
|
|
1686
|
-
command: hookDispatcherCommand("prompt-guard
|
|
20
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1687
21
|
}]
|
|
1688
22
|
}, {
|
|
1689
23
|
matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
1690
24
|
hooks: [{
|
|
1691
25
|
type: "command",
|
|
1692
|
-
command: hookDispatcherCommand("workflow-guard
|
|
26
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1693
27
|
}]
|
|
1694
28
|
}],
|
|
1695
29
|
PostToolUse: [{
|
|
1696
30
|
matcher: "*",
|
|
1697
31
|
hooks: [{
|
|
1698
32
|
type: "command",
|
|
1699
|
-
command: hookDispatcherCommand("context-monitor
|
|
33
|
+
command: hookDispatcherCommand("context-monitor")
|
|
1700
34
|
}]
|
|
1701
35
|
}],
|
|
1702
36
|
Stop: [{
|
|
1703
37
|
hooks: [{
|
|
1704
38
|
type: "command",
|
|
1705
|
-
command: hookDispatcherCommand("stop-checkpoint
|
|
39
|
+
command: hookDispatcherCommand("stop-checkpoint"),
|
|
1706
40
|
timeout: 10
|
|
1707
41
|
}]
|
|
1708
42
|
}],
|
|
@@ -1710,7 +44,7 @@ export function claudeHooksJsonWithObservation() {
|
|
|
1710
44
|
matcher: "manual|auto",
|
|
1711
45
|
hooks: [{
|
|
1712
46
|
type: "command",
|
|
1713
|
-
command: hookDispatcherCommand("pre-compact
|
|
47
|
+
command: hookDispatcherCommand("pre-compact"),
|
|
1714
48
|
timeout: 10
|
|
1715
49
|
}]
|
|
1716
50
|
}]
|
|
@@ -1723,53 +57,37 @@ export function cursorHooksJsonWithObservation() {
|
|
|
1723
57
|
version: 1,
|
|
1724
58
|
hooks: {
|
|
1725
59
|
sessionStart: [{
|
|
1726
|
-
command: hookDispatcherCommand("session-start
|
|
60
|
+
command: hookDispatcherCommand("session-start")
|
|
1727
61
|
}],
|
|
1728
62
|
sessionResume: [{
|
|
1729
|
-
command: hookDispatcherCommand("session-start
|
|
63
|
+
command: hookDispatcherCommand("session-start")
|
|
1730
64
|
}],
|
|
1731
65
|
sessionClear: [{
|
|
1732
|
-
command: hookDispatcherCommand("session-start
|
|
66
|
+
command: hookDispatcherCommand("session-start")
|
|
1733
67
|
}],
|
|
1734
68
|
sessionCompact: [{
|
|
1735
|
-
command: hookDispatcherCommand("pre-compact
|
|
69
|
+
command: hookDispatcherCommand("pre-compact")
|
|
1736
70
|
}, {
|
|
1737
|
-
command: hookDispatcherCommand("session-start
|
|
71
|
+
command: hookDispatcherCommand("session-start")
|
|
1738
72
|
}],
|
|
1739
73
|
preToolUse: [{
|
|
1740
74
|
matcher: "*",
|
|
1741
|
-
command: hookDispatcherCommand("prompt-guard
|
|
75
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1742
76
|
}, {
|
|
1743
77
|
matcher: "*",
|
|
1744
|
-
command: hookDispatcherCommand("workflow-guard
|
|
78
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1745
79
|
}],
|
|
1746
80
|
postToolUse: [{
|
|
1747
81
|
matcher: "*",
|
|
1748
|
-
command: hookDispatcherCommand("context-monitor
|
|
82
|
+
command: hookDispatcherCommand("context-monitor")
|
|
1749
83
|
}],
|
|
1750
|
-
stop: [{
|
|
84
|
+
stop: [{
|
|
85
|
+
command: hookDispatcherCommand("stop-checkpoint"),
|
|
86
|
+
timeout: 10
|
|
87
|
+
}]
|
|
1751
88
|
}
|
|
1752
89
|
}, null, 2);
|
|
1753
90
|
}
|
|
1754
|
-
/**
|
|
1755
|
-
* Codex CLI ≥ v0.114 hooks. Differences vs. the Claude shape:
|
|
1756
|
-
*
|
|
1757
|
-
* - `SessionStart` matcher is limited to `startup|resume` — Codex does
|
|
1758
|
-
* not emit `clear` or `compact` lifecycle phases.
|
|
1759
|
-
* - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
|
|
1760
|
-
* (documented Codex limitation, v0.114/v0.115). We match both `Bash`
|
|
1761
|
-
* and `bash` variants to tolerate casing drift across Codex builds.
|
|
1762
|
-
* - `UserPromptSubmit` is supported and is the closest analogue to
|
|
1763
|
-
* Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
|
|
1764
|
-
* there so workflow/prompt checks still fire when the tool being
|
|
1765
|
-
* used is `Write` or `Edit` rather than `Bash`.
|
|
1766
|
-
* - There is no `PreCompact` event in Codex CLI — pre-compact
|
|
1767
|
-
* semantics are carried by the agent itself inside `/cc-ops retro`.
|
|
1768
|
-
*
|
|
1769
|
-
* The entire file is inert unless the user opts into
|
|
1770
|
-
* `[features] codex_hooks = true` in `~/.codex/config.toml`; cclaw
|
|
1771
|
-
* doctor and the init prompt handle that flag.
|
|
1772
|
-
*/
|
|
1773
91
|
export function codexHooksJsonWithObservation() {
|
|
1774
92
|
return JSON.stringify({
|
|
1775
93
|
cclawHookSchemaVersion: 1,
|
|
@@ -1778,25 +96,16 @@ export function codexHooksJsonWithObservation() {
|
|
|
1778
96
|
matcher: "startup|resume",
|
|
1779
97
|
hooks: [{
|
|
1780
98
|
type: "command",
|
|
1781
|
-
command: hookDispatcherCommand("session-start
|
|
99
|
+
command: hookDispatcherCommand("session-start")
|
|
1782
100
|
}]
|
|
1783
101
|
}],
|
|
1784
102
|
UserPromptSubmit: [{
|
|
1785
103
|
hooks: [{
|
|
1786
104
|
type: "command",
|
|
1787
|
-
command: hookDispatcherCommand("prompt-guard
|
|
105
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1788
106
|
}, {
|
|
1789
|
-
// `workflow-guard.sh` also runs here because Codex's PreToolUse
|
|
1790
|
-
// only sees Bash; Write/Edit/MCP writes never reach the hook
|
|
1791
|
-
// surface. Running workflow-guard on UserPromptSubmit catches
|
|
1792
|
-
// TDD-order violations that originate from the user's prompt
|
|
1793
|
-
// text (e.g. "edit X.ts to ..."). Payload is a prompt envelope,
|
|
1794
|
-
// not a tool call, so the script's TOOL extraction falls back
|
|
1795
|
-
// to "unknown" and advisory mode is a no-op by design — the
|
|
1796
|
-
// value is that prompt text is scanned for write-shaped intent
|
|
1797
|
-
// via the existing PAYLOAD_LOWER heuristics.
|
|
1798
107
|
type: "command",
|
|
1799
|
-
command: hookDispatcherCommand("workflow-guard
|
|
108
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1800
109
|
}, {
|
|
1801
110
|
type: "command",
|
|
1802
111
|
command: hookDispatcherCommand("verify-current-state")
|
|
@@ -1806,23 +115,23 @@ export function codexHooksJsonWithObservation() {
|
|
|
1806
115
|
matcher: "Bash|bash",
|
|
1807
116
|
hooks: [{
|
|
1808
117
|
type: "command",
|
|
1809
|
-
command: hookDispatcherCommand("prompt-guard
|
|
118
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1810
119
|
}, {
|
|
1811
120
|
type: "command",
|
|
1812
|
-
command: hookDispatcherCommand("workflow-guard
|
|
121
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1813
122
|
}]
|
|
1814
123
|
}],
|
|
1815
124
|
PostToolUse: [{
|
|
1816
125
|
matcher: "Bash|bash",
|
|
1817
126
|
hooks: [{
|
|
1818
127
|
type: "command",
|
|
1819
|
-
command: hookDispatcherCommand("context-monitor
|
|
128
|
+
command: hookDispatcherCommand("context-monitor")
|
|
1820
129
|
}]
|
|
1821
130
|
}],
|
|
1822
131
|
Stop: [{
|
|
1823
132
|
hooks: [{
|
|
1824
133
|
type: "command",
|
|
1825
|
-
command: hookDispatcherCommand("stop-checkpoint
|
|
134
|
+
command: hookDispatcherCommand("stop-checkpoint"),
|
|
1826
135
|
timeout: 10
|
|
1827
136
|
}]
|
|
1828
137
|
}]
|