cclaw-cli 0.48.9 → 0.48.11
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/doctor-references.js +11 -7
- 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 +31 -15
- package/dist/content/observe.d.ts +0 -38
- package/dist/content/observe.js +31 -1718
- package/dist/content/opencode-plugin.js +10 -7
- 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-registry.js +0 -9
- package/dist/doctor.js +58 -66
- package/dist/install.js +134 -54
- package/dist/policy.js +13 -13
- package/package.json +2 -3
package/dist/content/observe.js
CHANGED
|
@@ -1,1666 +1,10 @@
|
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
// RUNTIME_ROOT is a relative path (".cclaw") that currently contains no
|
|
4
|
+
// whitespace, so quoting is unnecessary inside the JSON-encoded command
|
|
5
|
+
// string. If RUNTIME_ROOT ever becomes configurable, wrap the path with
|
|
6
|
+
// JSON.stringify to survive spaces.
|
|
7
|
+
return `node ${RUNTIME_ROOT}/hooks/run-hook.mjs ${hookName}`;
|
|
1664
8
|
}
|
|
1665
9
|
export function claudeHooksJsonWithObservation() {
|
|
1666
10
|
return JSON.stringify({
|
|
@@ -1670,39 +14,33 @@ export function claudeHooksJsonWithObservation() {
|
|
|
1670
14
|
matcher: "startup|resume|clear|compact",
|
|
1671
15
|
hooks: [{
|
|
1672
16
|
type: "command",
|
|
1673
|
-
command: hookDispatcherCommand("session-start
|
|
17
|
+
command: hookDispatcherCommand("session-start")
|
|
1674
18
|
}]
|
|
1675
19
|
}],
|
|
1676
20
|
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
21
|
matcher: "*",
|
|
1684
22
|
hooks: [{
|
|
1685
23
|
type: "command",
|
|
1686
|
-
command: hookDispatcherCommand("prompt-guard
|
|
24
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1687
25
|
}]
|
|
1688
26
|
}, {
|
|
1689
27
|
matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
1690
28
|
hooks: [{
|
|
1691
29
|
type: "command",
|
|
1692
|
-
command: hookDispatcherCommand("workflow-guard
|
|
30
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1693
31
|
}]
|
|
1694
32
|
}],
|
|
1695
33
|
PostToolUse: [{
|
|
1696
34
|
matcher: "*",
|
|
1697
35
|
hooks: [{
|
|
1698
36
|
type: "command",
|
|
1699
|
-
command: hookDispatcherCommand("context-monitor
|
|
37
|
+
command: hookDispatcherCommand("context-monitor")
|
|
1700
38
|
}]
|
|
1701
39
|
}],
|
|
1702
40
|
Stop: [{
|
|
1703
41
|
hooks: [{
|
|
1704
42
|
type: "command",
|
|
1705
|
-
command: hookDispatcherCommand("stop-checkpoint
|
|
43
|
+
command: hookDispatcherCommand("stop-checkpoint"),
|
|
1706
44
|
timeout: 10
|
|
1707
45
|
}]
|
|
1708
46
|
}],
|
|
@@ -1710,7 +48,7 @@ export function claudeHooksJsonWithObservation() {
|
|
|
1710
48
|
matcher: "manual|auto",
|
|
1711
49
|
hooks: [{
|
|
1712
50
|
type: "command",
|
|
1713
|
-
command: hookDispatcherCommand("pre-compact
|
|
51
|
+
command: hookDispatcherCommand("pre-compact"),
|
|
1714
52
|
timeout: 10
|
|
1715
53
|
}]
|
|
1716
54
|
}]
|
|
@@ -1723,53 +61,37 @@ export function cursorHooksJsonWithObservation() {
|
|
|
1723
61
|
version: 1,
|
|
1724
62
|
hooks: {
|
|
1725
63
|
sessionStart: [{
|
|
1726
|
-
command: hookDispatcherCommand("session-start
|
|
64
|
+
command: hookDispatcherCommand("session-start")
|
|
1727
65
|
}],
|
|
1728
66
|
sessionResume: [{
|
|
1729
|
-
command: hookDispatcherCommand("session-start
|
|
67
|
+
command: hookDispatcherCommand("session-start")
|
|
1730
68
|
}],
|
|
1731
69
|
sessionClear: [{
|
|
1732
|
-
command: hookDispatcherCommand("session-start
|
|
70
|
+
command: hookDispatcherCommand("session-start")
|
|
1733
71
|
}],
|
|
1734
72
|
sessionCompact: [{
|
|
1735
|
-
command: hookDispatcherCommand("pre-compact
|
|
73
|
+
command: hookDispatcherCommand("pre-compact")
|
|
1736
74
|
}, {
|
|
1737
|
-
command: hookDispatcherCommand("session-start
|
|
75
|
+
command: hookDispatcherCommand("session-start")
|
|
1738
76
|
}],
|
|
1739
77
|
preToolUse: [{
|
|
1740
78
|
matcher: "*",
|
|
1741
|
-
command: hookDispatcherCommand("prompt-guard
|
|
79
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1742
80
|
}, {
|
|
1743
81
|
matcher: "*",
|
|
1744
|
-
command: hookDispatcherCommand("workflow-guard
|
|
82
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1745
83
|
}],
|
|
1746
84
|
postToolUse: [{
|
|
1747
85
|
matcher: "*",
|
|
1748
|
-
command: hookDispatcherCommand("context-monitor
|
|
86
|
+
command: hookDispatcherCommand("context-monitor")
|
|
1749
87
|
}],
|
|
1750
|
-
stop: [{
|
|
88
|
+
stop: [{
|
|
89
|
+
command: hookDispatcherCommand("stop-checkpoint"),
|
|
90
|
+
timeout: 10
|
|
91
|
+
}]
|
|
1751
92
|
}
|
|
1752
93
|
}, null, 2);
|
|
1753
94
|
}
|
|
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
95
|
export function codexHooksJsonWithObservation() {
|
|
1774
96
|
return JSON.stringify({
|
|
1775
97
|
cclawHookSchemaVersion: 1,
|
|
@@ -1778,25 +100,16 @@ export function codexHooksJsonWithObservation() {
|
|
|
1778
100
|
matcher: "startup|resume",
|
|
1779
101
|
hooks: [{
|
|
1780
102
|
type: "command",
|
|
1781
|
-
command: hookDispatcherCommand("session-start
|
|
103
|
+
command: hookDispatcherCommand("session-start")
|
|
1782
104
|
}]
|
|
1783
105
|
}],
|
|
1784
106
|
UserPromptSubmit: [{
|
|
1785
107
|
hooks: [{
|
|
1786
108
|
type: "command",
|
|
1787
|
-
command: hookDispatcherCommand("prompt-guard
|
|
109
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1788
110
|
}, {
|
|
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
111
|
type: "command",
|
|
1799
|
-
command: hookDispatcherCommand("workflow-guard
|
|
112
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1800
113
|
}, {
|
|
1801
114
|
type: "command",
|
|
1802
115
|
command: hookDispatcherCommand("verify-current-state")
|
|
@@ -1806,23 +119,23 @@ export function codexHooksJsonWithObservation() {
|
|
|
1806
119
|
matcher: "Bash|bash",
|
|
1807
120
|
hooks: [{
|
|
1808
121
|
type: "command",
|
|
1809
|
-
command: hookDispatcherCommand("prompt-guard
|
|
122
|
+
command: hookDispatcherCommand("prompt-guard")
|
|
1810
123
|
}, {
|
|
1811
124
|
type: "command",
|
|
1812
|
-
command: hookDispatcherCommand("workflow-guard
|
|
125
|
+
command: hookDispatcherCommand("workflow-guard")
|
|
1813
126
|
}]
|
|
1814
127
|
}],
|
|
1815
128
|
PostToolUse: [{
|
|
1816
129
|
matcher: "Bash|bash",
|
|
1817
130
|
hooks: [{
|
|
1818
131
|
type: "command",
|
|
1819
|
-
command: hookDispatcherCommand("context-monitor
|
|
132
|
+
command: hookDispatcherCommand("context-monitor")
|
|
1820
133
|
}]
|
|
1821
134
|
}],
|
|
1822
135
|
Stop: [{
|
|
1823
136
|
hooks: [{
|
|
1824
137
|
type: "command",
|
|
1825
|
-
command: hookDispatcherCommand("stop-checkpoint
|
|
138
|
+
command: hookDispatcherCommand("stop-checkpoint"),
|
|
1826
139
|
timeout: 10
|
|
1827
140
|
}]
|
|
1828
141
|
}]
|