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.
Files changed (48) hide show
  1. package/dist/artifact-linter.d.ts +20 -0
  2. package/dist/artifact-linter.js +368 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +8 -2
  5. package/dist/config.d.ts +4 -4
  6. package/dist/config.js +56 -5
  7. package/dist/constants.d.ts +4 -4
  8. package/dist/constants.js +6 -3
  9. package/dist/content/autoplan.js +51 -4
  10. package/dist/content/contexts.d.ts +9 -0
  11. package/dist/content/contexts.js +65 -0
  12. package/dist/content/hooks.d.ts +6 -2
  13. package/dist/content/hooks.js +448 -16
  14. package/dist/content/meta-skill.js +26 -0
  15. package/dist/content/next-command.d.ts +9 -0
  16. package/dist/content/next-command.js +138 -0
  17. package/dist/content/observe.d.ts +5 -1
  18. package/dist/content/observe.js +506 -24
  19. package/dist/content/skills.js +126 -0
  20. package/dist/content/stage-schema.d.ts +7 -0
  21. package/dist/content/stage-schema.js +70 -12
  22. package/dist/content/subagents.js +33 -0
  23. package/dist/content/templates.d.ts +1 -0
  24. package/dist/content/templates.js +182 -77
  25. package/dist/content/utility-skills.d.ts +5 -1
  26. package/dist/content/utility-skills.js +208 -2
  27. package/dist/delegation.d.ts +21 -0
  28. package/dist/delegation.js +94 -0
  29. package/dist/doctor.d.ts +5 -1
  30. package/dist/doctor.js +274 -23
  31. package/dist/fs-utils.d.ts +10 -0
  32. package/dist/fs-utils.js +47 -0
  33. package/dist/gate-evidence.d.ts +26 -0
  34. package/dist/gate-evidence.js +157 -0
  35. package/dist/harness-adapters.js +2 -0
  36. package/dist/hook-schema.d.ts +6 -0
  37. package/dist/hook-schema.js +45 -0
  38. package/dist/hook-schemas/claude-hooks.v1.json +12 -0
  39. package/dist/hook-schemas/codex-hooks.v1.json +12 -0
  40. package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
  41. package/dist/install.js +431 -16
  42. package/dist/policy.d.ts +5 -1
  43. package/dist/policy.js +52 -1
  44. package/dist/runs.js +8 -3
  45. package/dist/trace-matrix.d.ts +13 -0
  46. package/dist/trace-matrix.js +182 -0
  47. package/dist/types.d.ts +11 -1
  48. package/package.json +1 -1
@@ -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\\(|python[[:space:]]+-c)'; then
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
- [ "$BAND" != "none" ] || exit 0
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
- LAST_BAND=$(python3 - "$MONITOR_STATE" <<'PY'
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
- band = value.get("lastBand")
438
- if isinstance(band, str):
439
- print(band)
440
- else:
441
- print("none")
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
- if [ "$BAND" != "$LAST_BAND" ]; then
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: `${RUNTIME_ROOT}/hooks/session-start.sh`
1506
+ command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1032
1507
  }],
1033
1508
  sessionResume: [{
1034
- command: `${RUNTIME_ROOT}/hooks/session-start.sh`
1509
+ command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1035
1510
  }],
1036
1511
  sessionClear: [{
1037
- command: `${RUNTIME_ROOT}/hooks/session-start.sh`
1512
+ command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1038
1513
  }],
1039
1514
  sessionCompact: [{
1040
- command: `${RUNTIME_ROOT}/hooks/session-start.sh`
1515
+ command: `bash ${RUNTIME_ROOT}/hooks/session-start.sh`
1041
1516
  }],
1042
1517
  preToolUse: [{
1043
1518
  matcher: "*",
1044
- command: `${RUNTIME_ROOT}/hooks/prompt-guard.sh`
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: `${RUNTIME_ROOT}/hooks/observe.sh pre`
1525
+ command: `bash ${RUNTIME_ROOT}/hooks/observe.sh pre`
1048
1526
  }],
1049
1527
  postToolUse: [{
1050
1528
  matcher: "*",
1051
- command: `${RUNTIME_ROOT}/hooks/context-monitor.sh`
1529
+ command: `bash ${RUNTIME_ROOT}/hooks/context-monitor.sh`
1052
1530
  }, {
1053
1531
  matcher: "*",
1054
- command: `${RUNTIME_ROOT}/hooks/observe.sh post`
1532
+ command: `bash ${RUNTIME_ROOT}/hooks/observe.sh post`
1055
1533
  }],
1056
1534
  stop: [
1057
- { command: `${RUNTIME_ROOT}/hooks/summarize-observations.sh`, timeout: 15 },
1058
- { command: `${RUNTIME_ROOT}/hooks/stop-checkpoint.sh`, timeout: 10 }
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`