cclaw-cli 0.48.5 → 0.48.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/artifact-linter.js +32 -0
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.js +44 -5
  4. package/dist/content/hooks.d.ts +2 -2
  5. package/dist/content/hooks.js +293 -89
  6. package/dist/content/ideate-command.js +11 -0
  7. package/dist/content/iron-laws.d.ts +142 -0
  8. package/dist/content/iron-laws.js +191 -0
  9. package/dist/content/meta-skill.js +1 -0
  10. package/dist/content/next-command.js +12 -0
  11. package/dist/content/observe.js +555 -45
  12. package/dist/content/ops-command.js +11 -0
  13. package/dist/content/session-hooks.js +3 -1
  14. package/dist/content/stage-schema.d.ts +16 -0
  15. package/dist/content/stage-schema.js +82 -5
  16. package/dist/content/stages/review.js +4 -4
  17. package/dist/content/stages/tdd.js +7 -7
  18. package/dist/content/start-command.js +12 -0
  19. package/dist/content/subagents.js +26 -0
  20. package/dist/content/templates.js +8 -0
  21. package/dist/content/view-command.js +11 -0
  22. package/dist/doctor.js +6 -2
  23. package/dist/harness-adapters.js +3 -0
  24. package/dist/install.js +11 -1
  25. package/dist/internal/advance-stage.js +14 -2
  26. package/dist/internal/envelope-validate.d.ts +7 -0
  27. package/dist/internal/envelope-validate.js +66 -0
  28. package/dist/internal/knowledge-digest.d.ts +7 -0
  29. package/dist/internal/knowledge-digest.js +93 -0
  30. package/dist/internal/tdd-red-evidence.d.ts +7 -0
  31. package/dist/internal/tdd-red-evidence.js +130 -0
  32. package/dist/knowledge-store.d.ts +8 -0
  33. package/dist/knowledge-store.js +95 -0
  34. package/dist/tdd-cycle.d.ts +7 -0
  35. package/dist/tdd-cycle.js +29 -0
  36. package/dist/types.d.ts +6 -0
  37. package/package.json +1 -1
@@ -16,15 +16,6 @@ set -uo pipefail
16
16
  shopt -s globstar 2>/dev/null || true
17
17
  PROMPT_GUARD_MODE="${promptGuardMode}"
18
18
 
19
- HARNESS="codex"
20
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
21
- HARNESS="claude"
22
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
23
- HARNESS="cursor"
24
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
25
- HARNESS="opencode"
26
- fi
27
-
28
19
  ${RUNTIME_SHELL_DETECT_ROOT}
29
20
 
30
21
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
@@ -35,8 +26,12 @@ INPUT=$(cat 2>/dev/null || echo '{}')
35
26
  [ -n "$INPUT" ] || exit 0
36
27
 
37
28
  TOOL="unknown"
38
- PAYLOAD=""
39
- if command -v jq >/dev/null 2>&1; then
29
+ PAYLOAD="$INPUT"
30
+ if command -v cclaw_hook_extract_tool_and_payload >/dev/null 2>&1; then
31
+ cclaw_hook_extract_tool_and_payload "$INPUT"
32
+ TOOL="\${CCLAW_HOOK_TOOL:-unknown}"
33
+ PAYLOAD="\${CCLAW_HOOK_PAYLOAD:-$INPUT}"
34
+ elif command -v jq >/dev/null 2>&1; then
40
35
  TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
41
36
  PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
42
37
  elif command -v python3 >/dev/null 2>&1; then
@@ -93,8 +88,13 @@ if [ -z "$PAYLOAD" ]; then
93
88
  PAYLOAD=$(printf '%s' "$INPUT")
94
89
  fi
95
90
 
96
- PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
97
- TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
91
+ if command -v cclaw_hook_lower >/dev/null 2>&1; then
92
+ PAYLOAD_LOWER=$(cclaw_hook_lower "$PAYLOAD")
93
+ TOOL_LOWER=$(cclaw_hook_lower "$TOOL")
94
+ else
95
+ PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
96
+ TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
97
+ fi
98
98
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
99
99
  REASONS=""
100
100
 
@@ -179,6 +179,8 @@ ${RUNTIME_SHELL_DETECT_ROOT}
179
179
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
180
180
  FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
181
181
  TDD_LOG_FILE="$STATE_DIR/tdd-cycle-log.jsonl"
182
+ IRON_LAWS_FILE="$STATE_DIR/iron-laws.json"
183
+ REVIEW_ARMY_FILE="$ROOT/${RUNTIME_ROOT}/artifacts/07-review-army.json"
182
184
  GUARD_STATE_FILE="$STATE_DIR/workflow-guard.json"
183
185
  GUARD_LOG="$STATE_DIR/workflow-guard.jsonl"
184
186
  mkdir -p "$STATE_DIR" 2>/dev/null || true
@@ -187,8 +189,12 @@ INPUT=$(cat 2>/dev/null || echo '{}')
187
189
  [ -n "$INPUT" ] || exit 0
188
190
 
189
191
  TOOL="unknown"
190
- PAYLOAD=""
191
- if command -v jq >/dev/null 2>&1; then
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
192
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")
193
199
  PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
194
200
  elif command -v python3 >/dev/null 2>&1; then
@@ -241,15 +247,36 @@ else
241
247
  fi
242
248
 
243
249
  [ -n "$PAYLOAD" ] || PAYLOAD=$(printf '%s' "$INPUT")
244
- TOOL_LOWER=$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')
245
- PAYLOAD_LOWER=$(printf '%s' "$PAYLOAD" | tr '[:upper:]' '[:lower:]')
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
246
257
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
247
258
  NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
248
259
  REASONS=""
249
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
+
250
273
  CURRENT_STAGE="none"
251
274
  CURRENT_RUN="active"
252
- if [ -f "$FLOW_STATE_FILE" ]; then
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
253
280
  if command -v jq >/dev/null 2>&1; then
254
281
  CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
255
282
  CURRENT_RUN=$(jq -r '.activeRunId // "active"' "$FLOW_STATE_FILE" 2>/dev/null || echo "active")
@@ -287,6 +314,87 @@ PY
287
314
  fi
288
315
  fi
289
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
+
290
398
  LAST_FLOW_READ_AT=0
291
399
  if [ -f "$GUARD_STATE_FILE" ]; then
292
400
  if command -v jq >/dev/null 2>&1; then
@@ -332,6 +440,14 @@ is_mutating_tool() {
332
440
  esac
333
441
  }
334
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
+
335
451
  is_plan_mode_safe_tool() {
336
452
  case "$1" in
337
453
  read|readfile|open|view|cat|head|tail) return 0 ;;
@@ -576,6 +692,62 @@ is_tdd_production_write_payload() {
576
692
  return 0
577
693
  }
578
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
+
579
751
  tdd_cycle_counts() {
580
752
  if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
581
753
  printf '0:0'
@@ -785,33 +957,145 @@ if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"
785
957
  fi
786
958
 
787
959
  if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
960
+ TDD_MISSING_RED_PATHS=""
788
961
  if is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
789
- if has_open_red_cycle; then
790
- TDD_CYCLE_STATE="red_open"
791
- else
792
- OPEN_RED_STATUS=$?
793
- if [ "$OPEN_RED_STATUS" -eq 2 ]; then
794
- TDD_CYCLE_STATE="counts_unavailable"
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
795
1018
  if [ -n "$REASONS" ]; then
796
1019
  REASONS="$REASONS,tdd_cycle_counts_unavailable"
797
1020
  else
798
1021
  REASONS="tdd_cycle_counts_unavailable"
799
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"
800
1058
  else
801
- TDD_CYCLE_STATE=$(tdd_cycle_state)
1059
+ REASONS="tdd_refactor_before_green"
802
1060
  fi
803
1061
  fi
804
- if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
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
805
1088
  if [ -n "$REASONS" ]; then
806
- REASONS="$REASONS,tdd_write_without_open_red"
1089
+ REASONS="$REASONS,ship_preflight_required"
807
1090
  else
808
- REASONS="tdd_write_without_open_red"
1091
+ REASONS="ship_preflight_required"
809
1092
  fi
810
- elif [ "$TDD_CYCLE_STATE" = "__UNAVAILABLE__" ]; then
1093
+ fi
1094
+ if ! review_layer_coverage_complete; then
811
1095
  if [ -n "$REASONS" ]; then
812
- REASONS="$REASONS,tdd_cycle_counts_unavailable"
1096
+ REASONS="$REASONS,ship_review_coverage_required"
813
1097
  else
814
- REASONS="tdd_cycle_counts_unavailable"
1098
+ REASONS="ship_review_coverage_required"
815
1099
  fi
816
1100
  fi
817
1101
  fi
@@ -882,12 +1166,28 @@ PY
882
1166
  fi
883
1167
 
884
1168
  if [ -n "$REASONS" ]; then
885
- if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; 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
886
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."
887
1185
  elif printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
888
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."
889
- elif printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
890
- NOTE="Cclaw workflow guard: direct flow-state edit bypasses the canonical stage-complete helper (\${REASONS}). Prefer: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage>. In strict mode this is blocked."
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."
891
1191
  else
892
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."
893
1193
  fi
@@ -910,10 +1210,31 @@ if [ -n "$REASONS" ]; then
910
1210
  if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_'; then
911
1211
  SHOULD_BLOCK="true"
912
1212
  fi
913
- if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
1213
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red|tdd_write_without_red_for_path' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
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
914
1229
  SHOULD_BLOCK="true"
915
1230
  fi
916
- if printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
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
917
1238
  SHOULD_BLOCK="true"
918
1239
  fi
919
1240
  if [ "$WORKFLOW_GUARD_MODE" = "strict" ] || [ "$SHOULD_BLOCK" = "true" ]; then
@@ -932,25 +1253,214 @@ export function contextMonitorScript() {
932
1253
  # Advisory-only context pressure warnings (best effort).
933
1254
  set -uo pipefail
934
1255
 
935
- HARNESS="codex"
936
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
937
- HARNESS="claude"
938
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
939
- HARNESS="cursor"
940
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
941
- HARNESS="opencode"
942
- fi
943
-
944
1256
  ${RUNTIME_SHELL_DETECT_ROOT}
945
1257
 
946
1258
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
947
1259
  MONITOR_STATE="$STATE_DIR/context-monitor.json"
948
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"
949
1263
  mkdir -p "$STATE_DIR" 2>/dev/null || true
950
1264
 
951
1265
  INPUT=$(cat 2>/dev/null || echo '{}')
952
1266
  [ -n "$INPUT" ] || exit 0
953
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
+
954
1464
  REMAINING_PERCENT=""
955
1465
  if command -v python3 >/dev/null 2>&1; then
956
1466
  REMAINING_PERCENT=$(INPUT_JSON="$INPUT" python3 - <<'PY'