cclaw-cli 0.1.1 → 0.2.1
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/artifact-linter.d.ts +20 -0
- package/dist/artifact-linter.js +368 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +8 -2
- package/dist/config.d.ts +4 -4
- package/dist/config.js +56 -5
- package/dist/constants.d.ts +4 -4
- package/dist/constants.js +6 -3
- package/dist/content/autoplan.js +51 -4
- package/dist/content/contexts.d.ts +9 -0
- package/dist/content/contexts.js +65 -0
- package/dist/content/hooks.d.ts +6 -2
- package/dist/content/hooks.js +448 -16
- package/dist/content/meta-skill.js +26 -0
- package/dist/content/next-command.d.ts +9 -0
- package/dist/content/next-command.js +138 -0
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +506 -24
- package/dist/content/skills.js +126 -0
- package/dist/content/stage-schema.d.ts +7 -0
- package/dist/content/stage-schema.js +70 -12
- package/dist/content/subagents.js +33 -0
- package/dist/content/templates.d.ts +1 -0
- package/dist/content/templates.js +182 -77
- package/dist/content/utility-skills.d.ts +5 -1
- package/dist/content/utility-skills.js +208 -2
- package/dist/delegation.d.ts +21 -0
- package/dist/delegation.js +94 -0
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +274 -23
- package/dist/fs-utils.d.ts +10 -0
- package/dist/fs-utils.js +47 -0
- package/dist/gate-evidence.d.ts +26 -0
- package/dist/gate-evidence.js +157 -0
- package/dist/harness-adapters.js +2 -0
- package/dist/hook-schema.d.ts +6 -0
- package/dist/hook-schema.js +45 -0
- package/dist/hook-schemas/claude-hooks.v1.json +12 -0
- package/dist/hook-schemas/codex-hooks.v1.json +12 -0
- package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
- package/dist/install.js +431 -16
- package/dist/policy.d.ts +5 -1
- package/dist/policy.js +52 -1
- package/dist/runs.js +8 -3
- package/dist/trace-matrix.d.ts +13 -0
- package/dist/trace-matrix.js +182 -0
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
package/dist/content/observe.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
12
12
|
import { RUNTIME_SHELL_DETECT_ROOT } from "./hooks.js";
|
|
13
|
-
export function promptGuardScript() {
|
|
13
|
+
export function promptGuardScript(options = {}) {
|
|
14
|
+
const promptGuardMode = options.strictMode === true ? "strict" : "advisory";
|
|
14
15
|
return `#!/usr/bin/env bash
|
|
15
16
|
# cclaw prompt guard hook — generated by cclaw sync
|
|
16
17
|
# Advisory-only guard for risky writes into ${RUNTIME_ROOT} runtime files.
|
|
17
18
|
set -uo pipefail
|
|
19
|
+
PROMPT_GUARD_MODE="${promptGuardMode}"
|
|
18
20
|
|
|
19
21
|
HARNESS="codex"
|
|
20
22
|
if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
@@ -74,7 +76,7 @@ case "$TOOL_LOWER" in
|
|
|
74
76
|
;;
|
|
75
77
|
esac
|
|
76
78
|
|
|
77
|
-
if printf '%s' "$PAYLOAD_LOWER" | grep -Eq '(rm[[:space:]]+-rf[[:space:]]+\.cclaw|curl[[:space:]].*https?://|wget[[:space:]].*https?://|base64[[:space:]]+-d|eval
|
|
79
|
+
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
|
|
78
80
|
if [ -n "$REASONS" ]; then
|
|
79
81
|
REASONS="$REASONS,suspicious_payload_pattern"
|
|
80
82
|
else
|
|
@@ -112,6 +114,221 @@ PY
|
|
|
112
114
|
if [ -n "$ENTRY" ]; then
|
|
113
115
|
printf '%s\n' "$ENTRY" >> "$GUARD_LOG" 2>/dev/null || true
|
|
114
116
|
fi
|
|
117
|
+
if [ "$PROMPT_GUARD_MODE" = "strict" ]; then
|
|
118
|
+
printf '[cclaw] %s (blocked by strict mode)\n' "$NOTE" >&2
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
exit 0
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
export function workflowGuardScript() {
|
|
128
|
+
return `#!/usr/bin/env bash
|
|
129
|
+
# cclaw workflow guard hook — generated by cclaw sync
|
|
130
|
+
# Enforces stage-aware command discipline and recent flow-state read hygiene.
|
|
131
|
+
set -uo pipefail
|
|
132
|
+
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-advisory}"
|
|
133
|
+
MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
|
|
134
|
+
|
|
135
|
+
${RUNTIME_SHELL_DETECT_ROOT}
|
|
136
|
+
|
|
137
|
+
STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
|
|
138
|
+
FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
|
|
139
|
+
GUARD_STATE_FILE="$STATE_DIR/workflow-guard.json"
|
|
140
|
+
GUARD_LOG="$STATE_DIR/workflow-guard.jsonl"
|
|
141
|
+
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
142
|
+
|
|
143
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
144
|
+
[ -n "$INPUT" ] || exit 0
|
|
145
|
+
|
|
146
|
+
TOOL="unknown"
|
|
147
|
+
PAYLOAD=""
|
|
148
|
+
if command -v jq >/dev/null 2>&1; then
|
|
149
|
+
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // .tool // "unknown"' 2>/dev/null || echo "unknown")
|
|
150
|
+
PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
|
|
151
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
152
|
+
TOOL=$(INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
153
|
+
import json
|
|
154
|
+
import os
|
|
155
|
+
try:
|
|
156
|
+
value = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
157
|
+
except Exception:
|
|
158
|
+
value = {}
|
|
159
|
+
tool = value.get("tool_name") or value.get("tool") or "unknown"
|
|
160
|
+
print(tool if isinstance(tool, str) else "unknown")
|
|
161
|
+
PY
|
|
162
|
+
)
|
|
163
|
+
PAYLOAD=$(printf '%s' "$INPUT")
|
|
164
|
+
else
|
|
165
|
+
PAYLOAD=$(printf '%s' "$INPUT")
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
[ -n "$PAYLOAD" ] || PAYLOAD=$(printf '%s' "$INPUT")
|
|
169
|
+
TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
|
|
170
|
+
PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
|
|
171
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
172
|
+
NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
|
|
173
|
+
REASONS=""
|
|
174
|
+
|
|
175
|
+
CURRENT_STAGE="none"
|
|
176
|
+
if [ -f "$FLOW_STATE_FILE" ]; then
|
|
177
|
+
if command -v jq >/dev/null 2>&1; then
|
|
178
|
+
CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
|
|
179
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
180
|
+
CURRENT_STAGE=$(python3 - "$FLOW_STATE_FILE" <<'PY'
|
|
181
|
+
import json
|
|
182
|
+
import sys
|
|
183
|
+
stage = "none"
|
|
184
|
+
try:
|
|
185
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
186
|
+
parsed = json.load(fh)
|
|
187
|
+
value = parsed.get("currentStage")
|
|
188
|
+
if isinstance(value, str) and value:
|
|
189
|
+
stage = value
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
print(stage)
|
|
193
|
+
PY
|
|
194
|
+
)
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
LAST_FLOW_READ_AT=0
|
|
199
|
+
if [ -f "$GUARD_STATE_FILE" ]; then
|
|
200
|
+
if command -v jq >/dev/null 2>&1; then
|
|
201
|
+
LAST_FLOW_READ_AT=$(jq -r '.lastFlowReadAtEpoch // 0' "$GUARD_STATE_FILE" 2>/dev/null || echo "0")
|
|
202
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
203
|
+
LAST_FLOW_READ_AT=$(python3 - "$GUARD_STATE_FILE" <<'PY'
|
|
204
|
+
import json
|
|
205
|
+
import sys
|
|
206
|
+
value = 0
|
|
207
|
+
try:
|
|
208
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
209
|
+
parsed = json.load(fh)
|
|
210
|
+
raw = parsed.get("lastFlowReadAtEpoch", 0)
|
|
211
|
+
if isinstance(raw, (int, float)):
|
|
212
|
+
value = int(raw)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
print(value)
|
|
216
|
+
PY
|
|
217
|
+
)
|
|
218
|
+
fi
|
|
219
|
+
fi
|
|
220
|
+
[ -n "$LAST_FLOW_READ_AT" ] || LAST_FLOW_READ_AT=0
|
|
221
|
+
|
|
222
|
+
stage_index() {
|
|
223
|
+
case "$1" in
|
|
224
|
+
brainstorm) echo 1 ;;
|
|
225
|
+
scope) echo 2 ;;
|
|
226
|
+
design) echo 3 ;;
|
|
227
|
+
spec) echo 4 ;;
|
|
228
|
+
plan) echo 5 ;;
|
|
229
|
+
test) echo 6 ;;
|
|
230
|
+
build) echo 7 ;;
|
|
231
|
+
review) echo 8 ;;
|
|
232
|
+
ship) echo 9 ;;
|
|
233
|
+
*) echo 0 ;;
|
|
234
|
+
esac
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
detect_target_stage() {
|
|
238
|
+
local text="$1"
|
|
239
|
+
for stage in brainstorm scope design spec plan test build review ship; do
|
|
240
|
+
if printf '%s' "$text" | grep -Eq "(/cc-$stage|cc-$stage)\\b"; then
|
|
241
|
+
printf '%s' "$stage"
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
done
|
|
245
|
+
printf ''
|
|
246
|
+
return 0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
TARGET_STAGE=$(detect_target_stage "$PAYLOAD_LOWER")
|
|
250
|
+
if [ -n "$TARGET_STAGE" ] && [ "$CURRENT_STAGE" != "none" ]; then
|
|
251
|
+
CURRENT_IDX=$(stage_index "$CURRENT_STAGE")
|
|
252
|
+
TARGET_IDX=$(stage_index "$TARGET_STAGE")
|
|
253
|
+
if [ "$CURRENT_IDX" -gt 0 ] && [ "$TARGET_IDX" -gt 0 ]; then
|
|
254
|
+
if [ "$TARGET_IDX" -gt $((CURRENT_IDX + 1)) ]; then
|
|
255
|
+
REASONS="stage_jump_\${CURRENT_STAGE}_to_\${TARGET_STAGE}"
|
|
256
|
+
fi
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
if [ -n "$TARGET_STAGE" ]; then
|
|
261
|
+
if [ "$LAST_FLOW_READ_AT" -le 0 ] || [ "$NOW_EPOCH" -le 0 ] || [ $((NOW_EPOCH - LAST_FLOW_READ_AT)) -gt "$MAX_FLOW_READ_AGE_SEC" ]; then
|
|
262
|
+
if [ -n "$REASONS" ]; then
|
|
263
|
+
REASONS="$REASONS,stage_invocation_without_recent_flow_read"
|
|
264
|
+
else
|
|
265
|
+
REASONS="stage_invocation_without_recent_flow_read"
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
SHOULD_RECORD_FLOW_READ=0
|
|
271
|
+
case "$TOOL_LOWER" in
|
|
272
|
+
read|readfile|open|view|cat) SHOULD_RECORD_FLOW_READ=1 ;;
|
|
273
|
+
esac
|
|
274
|
+
|
|
275
|
+
if [ "$SHOULD_RECORD_FLOW_READ" -eq 1 ] && printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/state/flow-state\.json'; then
|
|
276
|
+
TMP_STATE_FILE="$GUARD_STATE_FILE.tmp.$$"
|
|
277
|
+
if command -v jq >/dev/null 2>&1 && [ -f "$GUARD_STATE_FILE" ]; then
|
|
278
|
+
jq --arg ts "$TS" --argjson epoch "$NOW_EPOCH" '
|
|
279
|
+
.lastFlowReadAt = $ts
|
|
280
|
+
| .lastFlowReadAtEpoch = $epoch
|
|
281
|
+
' "$GUARD_STATE_FILE" > "$TMP_STATE_FILE" 2>/dev/null || true
|
|
282
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
283
|
+
python3 - "$GUARD_STATE_FILE" "$TMP_STATE_FILE" "$TS" "$NOW_EPOCH" <<'PY'
|
|
284
|
+
import json
|
|
285
|
+
import sys
|
|
286
|
+
from pathlib import Path
|
|
287
|
+
source = Path(sys.argv[1])
|
|
288
|
+
target = Path(sys.argv[2])
|
|
289
|
+
ts = sys.argv[3]
|
|
290
|
+
epoch = int(float(sys.argv[4])) if sys.argv[4] else 0
|
|
291
|
+
payload = {}
|
|
292
|
+
if source.exists():
|
|
293
|
+
try:
|
|
294
|
+
raw = json.loads(source.read_text(encoding="utf-8"))
|
|
295
|
+
if isinstance(raw, dict):
|
|
296
|
+
payload.update(raw)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
payload["lastFlowReadAt"] = ts
|
|
300
|
+
payload["lastFlowReadAtEpoch"] = epoch
|
|
301
|
+
target.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
|
|
302
|
+
PY
|
|
303
|
+
fi
|
|
304
|
+
if [ -s "$TMP_STATE_FILE" ]; then
|
|
305
|
+
mv "$TMP_STATE_FILE" "$GUARD_STATE_FILE" 2>/dev/null || rm -f "$TMP_STATE_FILE" 2>/dev/null || true
|
|
306
|
+
else
|
|
307
|
+
printf '{\\n "lastFlowReadAt": "%s",\\n "lastFlowReadAtEpoch": %s\\n}\\n' "$TS" "$NOW_EPOCH" > "$GUARD_STATE_FILE" 2>/dev/null || true
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
if [ -n "$REASONS" ]; then
|
|
312
|
+
NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json and continue from current stage ordering."
|
|
313
|
+
if command -v jq >/dev/null 2>&1; then
|
|
314
|
+
ENTRY=$(jq -n -c \
|
|
315
|
+
--arg ts "$TS" \
|
|
316
|
+
--arg tool "$TOOL" \
|
|
317
|
+
--arg stage "$CURRENT_STAGE" \
|
|
318
|
+
--arg target "$TARGET_STAGE" \
|
|
319
|
+
--arg reasons "$REASONS" \
|
|
320
|
+
--arg note "$NOTE" \
|
|
321
|
+
'{ts:$ts,tool:$tool,currentStage:$stage,targetStage:$target,reasons:($reasons|split(",")),note:$note}' 2>/dev/null || echo "")
|
|
322
|
+
else
|
|
323
|
+
ENTRY=""
|
|
324
|
+
fi
|
|
325
|
+
if [ -n "$ENTRY" ]; then
|
|
326
|
+
printf '%s\n' "$ENTRY" >> "$GUARD_LOG" 2>/dev/null || true
|
|
327
|
+
fi
|
|
328
|
+
if [ "$WORKFLOW_GUARD_MODE" = "strict" ]; then
|
|
329
|
+
printf '[cclaw] %s (blocked by strict mode)\n' "$NOTE" >&2
|
|
330
|
+
exit 1
|
|
331
|
+
fi
|
|
115
332
|
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
116
333
|
fi
|
|
117
334
|
|
|
@@ -186,6 +403,56 @@ rotate_file() {
|
|
|
186
403
|
fi
|
|
187
404
|
}
|
|
188
405
|
|
|
406
|
+
sync_run_artifacts() {
|
|
407
|
+
if [ "$PHASE" != "post" ]; then
|
|
408
|
+
return
|
|
409
|
+
fi
|
|
410
|
+
[ -n "$ACTIVE_RUN" ] || return
|
|
411
|
+
if [ "$ACTIVE_RUN" = "none" ]; then
|
|
412
|
+
return
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
local tool_lower
|
|
416
|
+
tool_lower=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
|
|
417
|
+
case "$tool_lower" in
|
|
418
|
+
write|edit|multiedit|multi_edit|delete|applypatch|runcommand|shell|terminal|execcommand) ;;
|
|
419
|
+
*) return ;;
|
|
420
|
+
esac
|
|
421
|
+
|
|
422
|
+
local active_dir="$ROOT/${RUNTIME_ROOT}/artifacts"
|
|
423
|
+
local run_dir="$ROOT/${RUNTIME_ROOT}/runs/$ACTIVE_RUN/artifacts"
|
|
424
|
+
[ -d "$active_dir" ] || return
|
|
425
|
+
mkdir -p "$run_dir" 2>/dev/null || return
|
|
426
|
+
|
|
427
|
+
for run_file in "$run_dir"/*; do
|
|
428
|
+
[ -e "$run_file" ] || continue
|
|
429
|
+
[ -f "$run_file" ] || continue
|
|
430
|
+
local base_name
|
|
431
|
+
base_name=$(basename "$run_file")
|
|
432
|
+
local active_file="$active_dir/$base_name"
|
|
433
|
+
if [ ! -f "$active_file" ]; then
|
|
434
|
+
rm -f "$run_file" 2>/dev/null || true
|
|
435
|
+
continue
|
|
436
|
+
fi
|
|
437
|
+
if command -v cmp >/dev/null 2>&1 && cmp -s "$active_file" "$run_file" 2>/dev/null; then
|
|
438
|
+
continue
|
|
439
|
+
fi
|
|
440
|
+
cp "$active_file" "$run_file" 2>/dev/null || true
|
|
441
|
+
done
|
|
442
|
+
|
|
443
|
+
for active_file in "$active_dir"/*; do
|
|
444
|
+
[ -e "$active_file" ] || continue
|
|
445
|
+
[ -f "$active_file" ] || continue
|
|
446
|
+
local base_name
|
|
447
|
+
base_name=$(basename "$active_file")
|
|
448
|
+
local run_file="$run_dir/$base_name"
|
|
449
|
+
if [ -f "$run_file" ] && command -v cmp >/dev/null 2>&1 && cmp -s "$active_file" "$run_file" 2>/dev/null; then
|
|
450
|
+
continue
|
|
451
|
+
fi
|
|
452
|
+
cp "$active_file" "$run_file" 2>/dev/null || true
|
|
453
|
+
done
|
|
454
|
+
}
|
|
455
|
+
|
|
189
456
|
# Read stdin (hook JSON)
|
|
190
457
|
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
191
458
|
[ -z "$INPUT" ] && exit 0
|
|
@@ -316,6 +583,8 @@ if acquire_lock; then
|
|
|
316
583
|
trap - EXIT INT TERM
|
|
317
584
|
fi
|
|
318
585
|
|
|
586
|
+
sync_run_artifacts
|
|
587
|
+
|
|
319
588
|
exit 0
|
|
320
589
|
`;
|
|
321
590
|
}
|
|
@@ -421,33 +690,82 @@ elif awk "BEGIN { exit !($REMAINING_PERCENT <= 35) }"; then
|
|
|
421
690
|
BAND="warning"
|
|
422
691
|
fi
|
|
423
692
|
|
|
424
|
-
|
|
693
|
+
TTL_SECONDS_RAW="\${CCLAW_CONTEXT_MONITOR_TTL_SEC:-900}"
|
|
694
|
+
if printf '%s' "$TTL_SECONDS_RAW" | grep -Eq '^[0-9]+$'; then
|
|
695
|
+
TTL_SECONDS="$TTL_SECONDS_RAW"
|
|
696
|
+
else
|
|
697
|
+
TTL_SECONDS="900"
|
|
698
|
+
fi
|
|
425
699
|
|
|
426
700
|
LAST_BAND="none"
|
|
701
|
+
LAST_ADVISORY_BAND="none"
|
|
702
|
+
LAST_ADVISORY_AT=""
|
|
703
|
+
LAST_ADVISORY_EPOCH="0"
|
|
427
704
|
if [ -f "$MONITOR_STATE" ]; then
|
|
428
705
|
if command -v jq >/dev/null 2>&1; then
|
|
429
706
|
LAST_BAND=$(jq -r '.lastBand // "none"' "$MONITOR_STATE" 2>/dev/null || echo "none")
|
|
707
|
+
LAST_ADVISORY_BAND=$(jq -r '.lastAdvisoryBand // .lastBand // "none"' "$MONITOR_STATE" 2>/dev/null || echo "none")
|
|
708
|
+
LAST_ADVISORY_AT=$(jq -r '.lastAdvisoryAt // ""' "$MONITOR_STATE" 2>/dev/null || echo "")
|
|
709
|
+
LAST_ADVISORY_EPOCH=$(jq -r 'try ((.lastAdvisoryAt // "" | fromdateiso8601)) catch 0' "$MONITOR_STATE" 2>/dev/null || echo "0")
|
|
430
710
|
elif command -v python3 >/dev/null 2>&1; then
|
|
431
|
-
|
|
711
|
+
STATE_META=$(python3 - "$MONITOR_STATE" <<'PY'
|
|
432
712
|
import json
|
|
433
713
|
import sys
|
|
434
714
|
try:
|
|
435
715
|
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
|
436
716
|
value = json.load(handle)
|
|
437
|
-
|
|
438
|
-
if isinstance(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
717
|
+
last_band = value.get("lastBand")
|
|
718
|
+
if not isinstance(last_band, str):
|
|
719
|
+
last_band = "none"
|
|
720
|
+
advisory_band = value.get("lastAdvisoryBand")
|
|
721
|
+
if not isinstance(advisory_band, str):
|
|
722
|
+
advisory_band = last_band
|
|
723
|
+
advisory_at = value.get("lastAdvisoryAt")
|
|
724
|
+
if not isinstance(advisory_at, str):
|
|
725
|
+
advisory_at = ""
|
|
726
|
+
advisory_epoch = 0
|
|
727
|
+
if advisory_at:
|
|
728
|
+
try:
|
|
729
|
+
from datetime import datetime
|
|
730
|
+
normalized = advisory_at.replace("Z", "+00:00")
|
|
731
|
+
advisory_epoch = int(datetime.fromisoformat(normalized).timestamp())
|
|
732
|
+
except Exception:
|
|
733
|
+
advisory_epoch = 0
|
|
734
|
+
print(f"{last_band}|{advisory_band}|{advisory_at}|{advisory_epoch}")
|
|
442
735
|
except Exception:
|
|
443
|
-
print("none")
|
|
736
|
+
print("none|none||0")
|
|
444
737
|
PY
|
|
445
738
|
)
|
|
739
|
+
LAST_BAND=$(printf '%s' "$STATE_META" | cut -d'|' -f1)
|
|
740
|
+
LAST_ADVISORY_BAND=$(printf '%s' "$STATE_META" | cut -d'|' -f2)
|
|
741
|
+
LAST_ADVISORY_AT=$(printf '%s' "$STATE_META" | cut -d'|' -f3)
|
|
742
|
+
LAST_ADVISORY_EPOCH=$(printf '%s' "$STATE_META" | cut -d'|' -f4)
|
|
446
743
|
fi
|
|
447
744
|
fi
|
|
448
745
|
|
|
449
746
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
450
|
-
|
|
747
|
+
NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
|
|
748
|
+
if ! printf '%s' "$LAST_ADVISORY_EPOCH" | grep -Eq '^[0-9]+$'; then
|
|
749
|
+
LAST_ADVISORY_EPOCH="0"
|
|
750
|
+
fi
|
|
751
|
+
|
|
752
|
+
SHOULD_EMIT="false"
|
|
753
|
+
if [ "$BAND" != "none" ]; then
|
|
754
|
+
if [ "$BAND" != "$LAST_ADVISORY_BAND" ]; then
|
|
755
|
+
SHOULD_EMIT="true"
|
|
756
|
+
elif [ "$TTL_SECONDS" -eq 0 ]; then
|
|
757
|
+
SHOULD_EMIT="true"
|
|
758
|
+
else
|
|
759
|
+
ELAPSED=$((NOW_EPOCH - LAST_ADVISORY_EPOCH))
|
|
760
|
+
if [ "$ELAPSED" -ge "$TTL_SECONDS" ]; then
|
|
761
|
+
SHOULD_EMIT="true"
|
|
762
|
+
fi
|
|
763
|
+
fi
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
NEXT_ADVISORY_BAND="$LAST_ADVISORY_BAND"
|
|
767
|
+
NEXT_ADVISORY_AT="$LAST_ADVISORY_AT"
|
|
768
|
+
if [ "$SHOULD_EMIT" = "true" ]; then
|
|
451
769
|
NOTE="Cclaw advisory: context remaining is \${REMAINING_PERCENT}% (\${BAND}). Consider checkpointing or compacting soon."
|
|
452
770
|
if command -v jq >/dev/null 2>&1; then
|
|
453
771
|
ENTRY=$(jq -n -c \
|
|
@@ -465,6 +783,8 @@ if [ "$BAND" != "$LAST_BAND" ]; then
|
|
|
465
783
|
printf '%s\n' "$ENTRY" >> "$WARNINGS_FILE" 2>/dev/null || true
|
|
466
784
|
fi
|
|
467
785
|
printf '[cclaw] %s\n' "$NOTE" >&2
|
|
786
|
+
NEXT_ADVISORY_BAND="$BAND"
|
|
787
|
+
NEXT_ADVISORY_AT="$TS"
|
|
468
788
|
fi
|
|
469
789
|
|
|
470
790
|
TMP_STATE="$MONITOR_STATE.tmp.$$"
|
|
@@ -472,12 +792,14 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
472
792
|
jq -n \
|
|
473
793
|
--arg ts "$TS" \
|
|
474
794
|
--arg band "$BAND" \
|
|
795
|
+
--arg advisoryBand "$NEXT_ADVISORY_BAND" \
|
|
796
|
+
--arg advisoryAt "$NEXT_ADVISORY_AT" \
|
|
475
797
|
--arg remaining "$REMAINING_PERCENT" \
|
|
476
798
|
--arg harness "$HARNESS" \
|
|
477
|
-
'{lastUpdated:$ts,lastBand:$band,lastRemainingPercent:($remaining|tonumber),harness:$harness}' > "$TMP_STATE" 2>/dev/null || true
|
|
799
|
+
'{lastUpdated:$ts,lastBand:$band,lastRemainingPercent:($remaining|tonumber),harness:$harness,lastAdvisoryBand:$advisoryBand,lastAdvisoryAt:$advisoryAt}' > "$TMP_STATE" 2>/dev/null || true
|
|
478
800
|
else
|
|
479
|
-
printf '{\n "lastUpdated": "%s",\n "lastBand": "%s",\n "lastRemainingPercent": %s,\n "harness": "%s"\n}\n' \
|
|
480
|
-
"$TS" "$BAND" "$REMAINING_PERCENT" "$HARNESS" > "$TMP_STATE" 2>/dev/null || true
|
|
801
|
+
printf '{\n "lastUpdated": "%s",\n "lastBand": "%s",\n "lastRemainingPercent": %s,\n "harness": "%s",\n "lastAdvisoryBand": "%s",\n "lastAdvisoryAt": "%s"\n}\n' \
|
|
802
|
+
"$TS" "$BAND" "$REMAINING_PERCENT" "$HARNESS" "$NEXT_ADVISORY_BAND" "$NEXT_ADVISORY_AT" > "$TMP_STATE" 2>/dev/null || true
|
|
481
803
|
fi
|
|
482
804
|
if [ -s "$TMP_STATE" ]; then
|
|
483
805
|
mv "$TMP_STATE" "$MONITOR_STATE" 2>/dev/null || rm -f "$TMP_STATE" 2>/dev/null || true
|
|
@@ -644,6 +966,135 @@ for (const [tool, count] of longPayload.entries()) {
|
|
|
644
966
|
});
|
|
645
967
|
}
|
|
646
968
|
|
|
969
|
+
const toolFilePathCounts = new Map();
|
|
970
|
+
function extractFilePathsFromPayload(dataVal) {
|
|
971
|
+
const text = toText(dataVal);
|
|
972
|
+
if (!text) return [];
|
|
973
|
+
const found = [];
|
|
974
|
+
const seen = new Set();
|
|
975
|
+
try {
|
|
976
|
+
const obj = JSON.parse(text);
|
|
977
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
978
|
+
const keys = [
|
|
979
|
+
"path",
|
|
980
|
+
"file_path",
|
|
981
|
+
"filePath",
|
|
982
|
+
"target_file",
|
|
983
|
+
"file",
|
|
984
|
+
"filepath",
|
|
985
|
+
"old_path",
|
|
986
|
+
"new_path",
|
|
987
|
+
"targetPath"
|
|
988
|
+
];
|
|
989
|
+
for (const k of keys) {
|
|
990
|
+
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
|
|
991
|
+
const v = obj[k];
|
|
992
|
+
if (typeof v === "string" && v.length > 1 && (v.includes("/") || v.includes("\\\\"))) {
|
|
993
|
+
const norm = v.replace(/\\\\/g, "/").replace(/\\/+/g, "/");
|
|
994
|
+
if (!seen.has(norm)) {
|
|
995
|
+
seen.add(norm);
|
|
996
|
+
found.push(norm);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
// ignore JSON parse errors
|
|
1003
|
+
}
|
|
1004
|
+
return found;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
for (const obs of observations) {
|
|
1008
|
+
const toolRaw = typeof obs.tool === "string" ? obs.tool : "unknown";
|
|
1009
|
+
const tool = toolRaw.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "unknown";
|
|
1010
|
+
for (const filePath of extractFilePathsFromPayload(obs.data)) {
|
|
1011
|
+
const pairKey = JSON.stringify([tool, filePath]);
|
|
1012
|
+
toolFilePathCounts.set(pairKey, (toolFilePathCounts.get(pairKey) || 0) + 1);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
for (const [pairKey, uses] of toolFilePathCounts.entries()) {
|
|
1017
|
+
if (uses < 5) continue;
|
|
1018
|
+
let tool = "unknown";
|
|
1019
|
+
let filePath = "";
|
|
1020
|
+
try {
|
|
1021
|
+
const parsed = JSON.parse(pairKey);
|
|
1022
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) continue;
|
|
1023
|
+
if (typeof parsed[0] === "string") tool = parsed[0];
|
|
1024
|
+
if (typeof parsed[1] === "string") filePath = parsed[1];
|
|
1025
|
+
} catch {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
if (!filePath) continue;
|
|
1029
|
+
const parts = filePath.split(/[/\\\\]/);
|
|
1030
|
+
let basename = parts[parts.length - 1] || filePath;
|
|
1031
|
+
basename = basename.trim().replace(/[^A-Za-z0-9._-]+/g, "-") || "file";
|
|
1032
|
+
const key = "file-affinity-" + tool + "-" + basename;
|
|
1033
|
+
if (!keyPattern.test(key)) continue;
|
|
1034
|
+
candidates.push({
|
|
1035
|
+
ts: timestamp,
|
|
1036
|
+
skill: "observation",
|
|
1037
|
+
type: "pattern",
|
|
1038
|
+
key: key,
|
|
1039
|
+
insight:
|
|
1040
|
+
"Tool " +
|
|
1041
|
+
tool +
|
|
1042
|
+
" frequently targets " +
|
|
1043
|
+
filePath +
|
|
1044
|
+
"; consider pre-loading it for this stage.",
|
|
1045
|
+
confidence: 4,
|
|
1046
|
+
source: "observed"
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (observations.length >= 10) {
|
|
1051
|
+
const stages = new Set();
|
|
1052
|
+
for (const obs of observations) {
|
|
1053
|
+
const s = typeof obs.stage === "string" ? obs.stage.trim() : "none";
|
|
1054
|
+
stages.add(s || "none");
|
|
1055
|
+
}
|
|
1056
|
+
if (stages.size === 1) {
|
|
1057
|
+
const onlyStage = [...stages][0];
|
|
1058
|
+
if (onlyStage && onlyStage !== "none") {
|
|
1059
|
+
const stageSan = onlyStage.replace(/[^A-Za-z0-9._-]+/g, "-") || "none";
|
|
1060
|
+
const times = [];
|
|
1061
|
+
for (const obs of observations) {
|
|
1062
|
+
if (typeof obs.ts === "string") {
|
|
1063
|
+
const ms = Date.parse(obs.ts);
|
|
1064
|
+
if (!Number.isNaN(ms)) times.push(ms);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
times.sort((a, b) => a - b);
|
|
1068
|
+
if (times.length >= 2) {
|
|
1069
|
+
const spanMin = Math.max(
|
|
1070
|
+
1,
|
|
1071
|
+
Math.round((times[times.length - 1] - times[0]) / 60000)
|
|
1072
|
+
);
|
|
1073
|
+
const M = observations.length;
|
|
1074
|
+
const velKey = "stage-velocity-" + stageSan;
|
|
1075
|
+
if (keyPattern.test(velKey)) {
|
|
1076
|
+
candidates.push({
|
|
1077
|
+
ts: timestamp,
|
|
1078
|
+
skill: "observation",
|
|
1079
|
+
type: "operational",
|
|
1080
|
+
key: velKey,
|
|
1081
|
+
insight:
|
|
1082
|
+
"Stage " +
|
|
1083
|
+
onlyStage +
|
|
1084
|
+
" took approximately " +
|
|
1085
|
+
spanMin +
|
|
1086
|
+
" minutes with " +
|
|
1087
|
+
M +
|
|
1088
|
+
" tool calls.",
|
|
1089
|
+
confidence: 3,
|
|
1090
|
+
source: "observed"
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
647
1098
|
const valid = [];
|
|
648
1099
|
const bestCandidate = new Map();
|
|
649
1100
|
for (const candidate of candidates) {
|
|
@@ -964,6 +1415,25 @@ for file in $ARCHIVE_LIST; do
|
|
|
964
1415
|
fi
|
|
965
1416
|
done
|
|
966
1417
|
|
|
1418
|
+
# Write session digest for cross-session context
|
|
1419
|
+
STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
|
|
1420
|
+
DIGEST_FILE="$ROOT/${RUNTIME_ROOT}/state/session-digest.md"
|
|
1421
|
+
STAGE_NOW="none"
|
|
1422
|
+
RUN_DIGEST_ID="none"
|
|
1423
|
+
if [ -f "$STATE_FILE" ]; then
|
|
1424
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1425
|
+
STAGE_NOW=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
1426
|
+
RUN_DIGEST_ID=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
1427
|
+
fi
|
|
1428
|
+
fi
|
|
1429
|
+
{
|
|
1430
|
+
printf '%s\\n' "# Session Digest"
|
|
1431
|
+
printf '%s\\n' "- Stage: $STAGE_NOW"
|
|
1432
|
+
printf '%s\\n' "- Observations: $OBS_COUNT"
|
|
1433
|
+
printf '%s\\n' "- Timestamp: $TS"
|
|
1434
|
+
printf '%s\\n' "- Run: $RUN_DIGEST_ID"
|
|
1435
|
+
} > "$DIGEST_FILE" 2>/dev/null || true
|
|
1436
|
+
|
|
967
1437
|
# Keep stage activity bounded by line count.
|
|
968
1438
|
rotate_file "$ACTIVITY_FILE" 2000
|
|
969
1439
|
|
|
@@ -978,6 +1448,7 @@ exit 0
|
|
|
978
1448
|
*/
|
|
979
1449
|
export function claudeHooksJsonWithObservation() {
|
|
980
1450
|
return JSON.stringify({
|
|
1451
|
+
cclawHookSchemaVersion: 1,
|
|
981
1452
|
hooks: {
|
|
982
1453
|
SessionStart: [{
|
|
983
1454
|
matcher: "startup|resume|clear|compact",
|
|
@@ -991,6 +1462,9 @@ export function claudeHooksJsonWithObservation() {
|
|
|
991
1462
|
hooks: [{
|
|
992
1463
|
type: "command",
|
|
993
1464
|
command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
|
|
1465
|
+
}, {
|
|
1466
|
+
type: "command",
|
|
1467
|
+
command: `bash ${RUNTIME_ROOT}/hooks/workflow-guard.sh`
|
|
994
1468
|
}, {
|
|
995
1469
|
type: "command",
|
|
996
1470
|
command: `bash ${RUNTIME_ROOT}/hooks/observe.sh pre`
|
|
@@ -1025,43 +1499,48 @@ export function claudeHooksJsonWithObservation() {
|
|
|
1025
1499
|
}
|
|
1026
1500
|
export function cursorHooksJsonWithObservation() {
|
|
1027
1501
|
return JSON.stringify({
|
|
1502
|
+
cclawHookSchemaVersion: 1,
|
|
1028
1503
|
version: 1,
|
|
1029
1504
|
hooks: {
|
|
1030
1505
|
sessionStart: [{
|
|
1031
|
-
command:
|
|
1506
|
+
command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
|
|
1032
1507
|
}],
|
|
1033
1508
|
sessionResume: [{
|
|
1034
|
-
command:
|
|
1509
|
+
command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
|
|
1035
1510
|
}],
|
|
1036
1511
|
sessionClear: [{
|
|
1037
|
-
command:
|
|
1512
|
+
command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
|
|
1038
1513
|
}],
|
|
1039
1514
|
sessionCompact: [{
|
|
1040
|
-
command:
|
|
1515
|
+
command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
|
|
1041
1516
|
}],
|
|
1042
1517
|
preToolUse: [{
|
|
1043
1518
|
matcher: "*",
|
|
1044
|
-
command:
|
|
1519
|
+
command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
|
|
1520
|
+
}, {
|
|
1521
|
+
matcher: "*",
|
|
1522
|
+
command: `bash ${RUNTIME_ROOT}/hooks/workflow-guard.sh`
|
|
1045
1523
|
}, {
|
|
1046
1524
|
matcher: "*",
|
|
1047
|
-
command:
|
|
1525
|
+
command: `bash ${RUNTIME_ROOT}/hooks/observe.sh pre`
|
|
1048
1526
|
}],
|
|
1049
1527
|
postToolUse: [{
|
|
1050
1528
|
matcher: "*",
|
|
1051
|
-
command:
|
|
1529
|
+
command: `bash ${RUNTIME_ROOT}/hooks/context-monitor.sh`
|
|
1052
1530
|
}, {
|
|
1053
1531
|
matcher: "*",
|
|
1054
|
-
command:
|
|
1532
|
+
command: `bash ${RUNTIME_ROOT}/hooks/observe.sh post`
|
|
1055
1533
|
}],
|
|
1056
1534
|
stop: [
|
|
1057
|
-
{ command:
|
|
1058
|
-
{ command:
|
|
1535
|
+
{ command: `bash ${RUNTIME_ROOT}/hooks/summarize-observations.sh`, timeout: 15 },
|
|
1536
|
+
{ command: `bash ${RUNTIME_ROOT}/hooks/stop-checkpoint.sh`, timeout: 10 }
|
|
1059
1537
|
]
|
|
1060
1538
|
}
|
|
1061
1539
|
}, null, 2);
|
|
1062
1540
|
}
|
|
1063
1541
|
export function codexHooksJsonWithObservation() {
|
|
1064
1542
|
return JSON.stringify({
|
|
1543
|
+
cclawHookSchemaVersion: 1,
|
|
1065
1544
|
hooks: {
|
|
1066
1545
|
SessionStart: [{
|
|
1067
1546
|
matcher: "startup|resume|clear|compact",
|
|
@@ -1076,6 +1555,9 @@ export function codexHooksJsonWithObservation() {
|
|
|
1076
1555
|
hooks: [{
|
|
1077
1556
|
type: "command",
|
|
1078
1557
|
command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
|
|
1558
|
+
}, {
|
|
1559
|
+
type: "command",
|
|
1560
|
+
command: `bash ${RUNTIME_ROOT}/hooks/workflow-guard.sh`
|
|
1079
1561
|
}, {
|
|
1080
1562
|
type: "command",
|
|
1081
1563
|
command: `bash ${RUNTIME_ROOT}/hooks/observe.sh pre`
|