@techierathore/techieflow 1.0.2 → 1.0.4

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 (204) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -16
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
  7. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  8. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  9. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
  11. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  12. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  13. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  14. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  17. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  18. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  19. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  20. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  21. package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
  22. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
  23. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  24. package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
  25. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  26. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  27. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  28. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  29. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  30. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  31. package/.opencode/plugin/techieflow.js +40 -13
  32. package/.tfcore/agents/analyst.md +5 -13
  33. package/.tfcore/agents/architect.md +1 -16
  34. package/.tfcore/agents/flow-master.md +42 -215
  35. package/.tfcore/agents/verifier.md +24 -64
  36. package/.tfcore/core-config.yaml +10 -0
  37. package/.tfcore/hooks/block-git.sh +4 -4
  38. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  39. package/.tfcore/hooks/guard-build.sh +76 -0
  40. package/.tfcore/hooks/guard-db.sh +90 -0
  41. package/.tfcore/hooks/guard-metrics.sh +68 -0
  42. package/.tfcore/hooks/guard-status-html.sh +116 -71
  43. package/.tfcore/hooks/guard-status.sh +43 -16
  44. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  45. package/.tfcore/hooks/guard-verify.sh +70 -48
  46. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  47. package/.tfcore/install-manifest.yaml +0 -18
  48. package/.tfcore/routing.yaml +86 -14
  49. package/.tfcore/standards/coding-standards-core.md +61 -0
  50. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  51. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  52. package/.tfcore/tasks/_owner-language.md +26 -0
  53. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  54. package/.tfcore/tasks/_status-update-gate.md +10 -74
  55. package/.tfcore/tasks/_yolo-mode.md +20 -58
  56. package/.tfcore/tasks/amend-docs.md +21 -143
  57. package/.tfcore/tasks/build-phase.md +14 -224
  58. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  59. package/.tfcore/tasks/create-doc.md +4 -0
  60. package/.tfcore/tasks/create-project-brief.md +22 -0
  61. package/.tfcore/tasks/day1-brownfield.md +31 -505
  62. package/.tfcore/tasks/day1-greenfield.md +28 -213
  63. package/.tfcore/tasks/deploy-checklist.md +19 -0
  64. package/.tfcore/tasks/devguide.md +10 -219
  65. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  66. package/.tfcore/tasks/fix-issues.md +10 -129
  67. package/.tfcore/tasks/generate-html.md +5 -1
  68. package/.tfcore/tasks/handoff-phase.md +18 -16
  69. package/.tfcore/tasks/log-miss.md +19 -158
  70. package/.tfcore/tasks/metrics-report.md +3 -1
  71. package/.tfcore/tasks/mockups.md +12 -117
  72. package/.tfcore/tasks/refresh-status.md +9 -130
  73. package/.tfcore/tasks/split-brd.md +10 -126
  74. package/.tfcore/tasks/triage-and-fix.md +14 -0
  75. package/.tfcore/tasks/triage-issues.md +11 -152
  76. package/.tfcore/tasks/verify-phase.md +36 -572
  77. package/.tfcore/telemetry/SCHEMA.md +100 -8
  78. package/.tfcore/telemetry/tf-metrics.sh +564 -32
  79. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  80. package/.tfcore/templates/stack-questions.md +77 -0
  81. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  82. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  83. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  84. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  85. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  86. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  87. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  88. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  89. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  90. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  91. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  92. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  93. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  94. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  95. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  96. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  97. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  98. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  99. package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
  100. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  101. package/.tfcore/user-guide.md +0 -23
  102. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  107. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  108. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  109. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  110. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  111. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  112. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  113. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  114. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  115. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  116. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  117. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  118. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  119. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  120. package/.tfcore/utils/tf-brd-status.py +176 -0
  121. package/.tfcore/utils/tf-brd-status.sh +19 -0
  122. package/.tfcore/utils/tf-build-list.py +211 -0
  123. package/.tfcore/utils/tf-build-list.sh +21 -0
  124. package/.tfcore/utils/tf-build.sh +143 -0
  125. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  126. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  127. package/.tfcore/utils/tf-day1-files.py +208 -0
  128. package/.tfcore/utils/tf-day1-files.sh +20 -0
  129. package/.tfcore/utils/tf-devguide-list.py +249 -0
  130. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  131. package/.tfcore/utils/tf-doc-check.py +1369 -0
  132. package/.tfcore/utils/tf-doc-check.sh +29 -0
  133. package/.tfcore/utils/tf-emit.sh +545 -18
  134. package/.tfcore/utils/tf-fix-close.sh +115 -0
  135. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  136. package/.tfcore/utils/tf-goal.sh +284 -66
  137. package/.tfcore/utils/tf-harness.sh +4 -9
  138. package/.tfcore/utils/tf-log-miss.py +197 -0
  139. package/.tfcore/utils/tf-log-miss.sh +14 -0
  140. package/.tfcore/utils/tf-misses-md.py +208 -0
  141. package/.tfcore/utils/tf-misses-md.sh +14 -0
  142. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  143. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  144. package/.tfcore/utils/tf-model-pick.sh +475 -0
  145. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  146. package/.tfcore/utils/tf-perf-grade.py +97 -0
  147. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  148. package/.tfcore/utils/tf-phase.sh +92 -0
  149. package/.tfcore/utils/tf-render-html.py +20 -0
  150. package/.tfcore/utils/tf-routing-bind.sh +97 -6
  151. package/.tfcore/utils/tf-routing.sh +13 -7
  152. package/.tfcore/utils/tf-selfcheck.sh +323 -0
  153. package/.tfcore/utils/tf-split-brd.py +309 -0
  154. package/.tfcore/utils/tf-split-brd.sh +22 -0
  155. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  156. package/.tfcore/utils/tf-status-facts.py +358 -0
  157. package/.tfcore/utils/tf-status-facts.sh +20 -0
  158. package/.tfcore/utils/tf-triage.py +243 -0
  159. package/.tfcore/utils/tf-triage.sh +16 -0
  160. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  161. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  162. package/.tfcore/utils/tf-verify-env.sh +88 -0
  163. package/.tfcore/utils/tf-verify-list.py +270 -0
  164. package/.tfcore/utils/tf-verify-list.sh +13 -0
  165. package/.tfcore/utils/tf-verify-screens.mjs +355 -0
  166. package/.tfcore/utils/tf-verify-screens.sh +31 -0
  167. package/.tfcore/utils/tf-verify-tests.sh +166 -0
  168. package/.tfcore/utils/tf-verify-verdict.py +292 -0
  169. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  170. package/.tfcore/utils/tf-yolo.sh +59 -0
  171. package/README.md +120 -1145
  172. package/docs/TechieFlow-Installation.md +1 -5
  173. package/opencode.jsonc +37 -33
  174. package/package.json +1 -5
  175. package/scripts/install.mjs +56 -46
  176. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  177. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  178. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  179. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  180. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  181. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  182. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  183. package/.codex/config.toml +0 -46
  184. package/.codex/hooks.json +0 -63
  185. package/.codex/rules/techieflow.rules +0 -8
  186. package/.tfcore/hooks/codex-adapter.py +0 -248
  187. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  188. package/.tfcore/tasks/author-brd.md +0 -79
  189. package/.tfcore/tasks/document-project.md +0 -343
  190. package/.tfcore/tasks/execute-checklist.md +0 -86
  191. package/.tfcore/tasks/index-docs.md +0 -173
  192. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  193. package/.tfcore/tasks/shard-doc.md +0 -185
  194. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  195. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  196. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  197. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  198. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  199. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  200. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  201. package/.tfcore/utils/techieflow-doc-template.md +0 -325
  202. package/.tfcore/utils/tf-codex-bind.py +0 -196
  203. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  204. package/WORKFLOW.html +0 -1986
@@ -2,16 +2,20 @@
2
2
  # tf-goal.sh — unattended GOAL runner / supervisor for TechieFlow (rule: .tfcore/tasks/_yolo-mode.md)
3
3
  #
4
4
  # Runs a goal headless in YOLO mode and keeps it running until the agent itself
5
- # declares the goal done — surviving the subscription 5-hour / weekly usage
6
- # limits (sleeps until the stated reset + a buffer, then RESUMES THE SAME
7
- # SESSION), crashes, and "I stopped early to ask you something" turns.
5
+ # declares the goal done — surviving the subscription 5-hour / weekly usage limits (moves to the
6
+ # next model in the tier's fallback chain, or sleeps until the stated reset + a buffer when there
7
+ # is none, then RESUMES THE SAME SESSION), crashes, and "I stopped early to ask you something".
8
8
  #
9
9
  # bash .tfcore/utils/tf-goal.sh [options] <app-dir> "<goal text>"
10
10
  # bash .tfcore/utils/tf-goal.sh [options] <app-dir> @goal.md
11
11
  #
12
12
  # Options
13
- # --harness claude|opencode|codex default: claude
14
- # --model <id> claude: --model; opencode: -m; codex: -m
13
+ # --harness claude|opencode default: claude
14
+ # --model <id> claude: --model; opencode: -m
15
+ # --tier <tier> which routing tier this run belongs to, for the fallback chain
16
+ # (default standard) — .tfcore/routing.yaml `fallbacks:`
17
+ # --no-fallback never switch model on a usage limit; sleep until the reset instead
18
+ # (the behaviour before 2026-09-10)
15
19
  # --buffer-min <n> minutes added after a stated limit-reset time (default 15)
16
20
  # --probe-min <n> limit hit but NO reset time parseable → fire a one-turn probe every n
17
21
  # minutes until the API answers again, then resume (default 15)
@@ -19,11 +23,21 @@
19
23
  # --default-wait-min <n> (legacy) only used to stamp resume_at in goal.json when probing
20
24
  # --max-cycles <n> give up after n launches (default 60)
21
25
  # --idle-retry-sec <n> pause before re-prompting an agent that stopped without finishing (default 30)
26
+ # --stall-min <n> a cycle whose output has not grown for n minutes is killed and
27
+ # re-prompted (default 15; 0 disables) — MISS-TechieFlow-20260905-22
22
28
  # --resume continue the last goal run recorded in .tfcore/.session/goal.json
29
+ # --fresh with --resume: keep the goal and the cycle count but start a NEW
30
+ # harness session (the old one hung on continue). The supervisor does
31
+ # this by itself after two stalled resumes in a row.
23
32
  # --dry-run print the commands, run nothing
24
33
  #
25
34
  # Exit codes: 0 goal complete · 3 agent declared the goal BLOCKED (owner input
26
- # needed) · 4 max cycles reached · 2 usage error.
35
+ # needed) · 4 max cycles reached · 5 the provider refuses the model (a monthly or
36
+ # balance limit OpenCode reports only in its own log) · 2 usage error · 130 stopped
37
+ # by Ctrl-C / kill (the harness child is stopped too; `--resume` continues the same session).
38
+ #
39
+ # Test-only knobs (tests/goal/run.sh): TF_GOAL_FAKE_CMD="<shell>" replaces the harness
40
+ # command; TF_GOAL_STALL_SEC / TF_GOAL_STALL_TICK set the stall clock in seconds.
27
41
  #
28
42
  # Files (all under <app-dir>/.tfcore/.session/, never committed):
29
43
  # yolo.json YOLO flag (tf-yolo.sh on --source goal) — the hook reads it
@@ -31,6 +45,14 @@
31
45
  # goal.log everything the harness printed, all cycles, timestamped
32
46
  # goal-done.json the agent's completion sentinel (tf-yolo.sh done [complete|blocked])
33
47
  #
48
+ # WHEN A MODEL RUNS OUT (added 2026-09-10, MISS-TechieFlow-20260910-01). A usage limit used to mean
49
+ # one thing: sleep until the reset. It now means: park that model in the per-machine cooldown file
50
+ # (tf-model-pick.sh), ask the tier for the next model in its `fallbacks:` chain, re-bind so the
51
+ # sub-agents move too, and start the next cycle straight away on that model. Only when the whole
52
+ # chain is parked does the supervisor sleep, exactly as before. If the fallback is limited as well
53
+ # the next cycle parks that one too, so an account-wide limit — every Claude model at once — costs
54
+ # one wasted cycle and then behaves as it always did. `--no-fallback` turns all of it off.
55
+ #
34
56
  # How the agent is told to finish: the prompt preamble (below) instructs it to run
35
57
  # `bash .tfcore/utils/tf-yolo.sh done complete "<summary>"` when the goal is met,
36
58
  # or `... done blocked "<why>"` when only the owner can unblock it. A cycle that
@@ -42,49 +64,54 @@
42
64
  # claude -p "<prompt>" --permission-mode bypassPermissions --output-format stream-json --verbose
43
65
  # resume: claude -p --resume <session_id> "<continue>" (fallback: --continue)
44
66
  # opencode run --auto "<prompt>" resume: opencode run --auto -c "<continue>"
45
- # codex exec --json --sandbox workspace-write -c approval_policy="never" "<prompt>"
46
- # NB: `--ask-for-approval` is NOT a `codex exec` flag (verified against
47
- # codex-cli 0.149.1: "error: unexpected argument '--ask-for-approval'",
48
67
  # exit 2). The approval policy is set as a config override instead.
49
- # resume: codex exec resume <thread-id> "<continue>" --json
50
68
  # Override command lines with TF_GOAL_CLAUDE_FLAGS / TF_GOAL_OPENCODE_FLAGS /
51
- # TF_GOAL_CODEX_FLAGS.
52
69
 
53
70
  set -u
54
71
 
55
72
  HARNESS="claude"; MODEL=""; BUFFER_MIN=15; DEFAULT_WAIT_MIN=60; MAX_CYCLES=60; IDLE_RETRY=30
56
- PROBE_MIN=15; PROBE_MAX_H=8
57
- RESUME=0; DRY=0
73
+ PROBE_MIN=15; PROBE_MAX_H=8; STALL_MIN=15
74
+ TIER="standard"; FALLBACK=1
75
+ RESUME=0; FRESH=0; DRY=0
58
76
  while [[ $# -gt 0 ]]; do
59
77
  case "$1" in
60
78
  --harness) HARNESS="$2"; shift 2 ;;
61
79
  --model) MODEL="$2"; shift 2 ;;
80
+ --tier) TIER="$2"; shift 2 ;;
81
+ --no-fallback) FALLBACK=0; shift ;;
62
82
  --buffer-min) BUFFER_MIN="$2"; shift 2 ;;
63
83
  --default-wait-min) DEFAULT_WAIT_MIN="$2"; shift 2 ;;
64
84
  --probe-min) PROBE_MIN="$2"; shift 2 ;;
65
85
  --probe-max-hours) PROBE_MAX_H="$2"; shift 2 ;;
66
86
  --max-cycles) MAX_CYCLES="$2"; shift 2 ;;
67
87
  --idle-retry-sec) IDLE_RETRY="$2"; shift 2 ;;
88
+ --stall-min) STALL_MIN="$2"; shift 2 ;;
68
89
  --resume) RESUME=1; shift ;;
90
+ --fresh) FRESH=1; shift ;;
69
91
  --dry-run) DRY=1; shift ;;
70
- -h|--help) sed -n '2,45p' "$0"; exit 0 ;;
92
+ -h|--help) sed -n '2,48p' "$0"; exit 0 ;;
71
93
  --) shift; break ;;
72
94
  -*) echo "unknown option $1" >&2; exit 2 ;;
73
95
  *) break ;;
74
96
  esac
75
97
  done
76
98
  APP_DIR="${1:-}"; GOAL_ARG="${2:-}"
99
+ [[ -n "${TF_GOAL_CLASSIFY:-}" ]] && DRY=1 # the classify debug path touches no state
77
100
  if [[ -z "$APP_DIR" || ( -z "$GOAL_ARG" && $RESUME -eq 0 ) ]]; then
78
101
  echo "usage: tf-goal.sh [options] <app-dir> \"<goal>\" | @goal.md (or --resume <app-dir>)" >&2; exit 2
79
102
  fi
80
103
  APP_DIR="$(cd "$APP_DIR" 2>/dev/null && pwd)" || { echo "no such dir: $1" >&2; exit 2; }
81
104
  [[ -d "$APP_DIR/.tfcore" ]] || { echo "$APP_DIR has no .tfcore/ — scaffold it first" >&2; exit 2; }
82
- case "$HARNESS" in claude|opencode|codex) ;; *) echo "--harness must be claude|opencode|codex" >&2; exit 2 ;; esac
105
+ case "$HARNESS" in claude|opencode) ;; *) echo "--harness must be claude|opencode" >&2; exit 2 ;; esac
106
+ case "$TIER" in frontier|standard|economy) ;; *) echo "--tier must be frontier|standard|economy" >&2; exit 2 ;; esac
83
107
  command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; exit 2; }
84
108
 
85
109
  STATE_DIR="$APP_DIR/.tfcore/.session"; mkdir -p "$STATE_DIR"
86
110
  STATE="$STATE_DIR/goal.json"; LOG="$STATE_DIR/goal.log"; DONE="$STATE_DIR/goal-done.json"
87
111
  YOLO_SH="$APP_DIR/.tfcore/utils/tf-yolo.sh"
112
+ PICK_SH="$APP_DIR/.tfcore/utils/tf-model-pick.sh"
113
+ BIND_SH="$APP_DIR/.tfcore/utils/tf-routing-bind.sh"
114
+ [[ -f "$PICK_SH" ]] || FALLBACK=0 # a project that predates tf-model-pick.sh keeps the old behaviour
88
115
 
89
116
  if [[ "$GOAL_ARG" == @* ]]; then
90
117
  GOAL_FILE="${GOAL_ARG#@}"; [[ -f "$GOAL_FILE" ]] || { echo "goal file not found: $GOAL_FILE" >&2; exit 2; }
@@ -94,6 +121,9 @@ else
94
121
  fi
95
122
 
96
123
  ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
124
+ # nap: a sleep the INT/TERM trap can interrupt at once (bash defers a trap while a
125
+ # foreground command runs, so a plain `sleep 300` held a kill for up to five minutes).
126
+ nap() { sleep "$1" & wait $!; }
97
127
  log() { printf '[%s] tf-goal: %s\n' "$(ts)" "$*" | tee -a "$LOG" >&2; }
98
128
 
99
129
  state_get() { python3 -c 'import json,sys
@@ -115,20 +145,37 @@ if [[ $RESUME -eq 1 ]]; then
115
145
  [[ -f "$STATE" ]] || { echo "--resume: no $STATE" >&2; exit 2; }
116
146
  [[ -z "$GOAL" ]] && GOAL="$(state_get goal)"
117
147
  HARNESS="$(state_get harness)"; HARNESS="${HARNESS:-claude}"
148
+ # A resume keeps the model a fallback moved to, and the tier its chain came from; an explicit
149
+ # --model on the resume command still wins.
150
+ _t="$(state_get tier)"; [[ -n "$_t" ]] && TIER="$_t"
151
+ _m="$(state_get model)"; [[ -n "$_m" && -z "$MODEL" ]] && MODEL="$_m"
118
152
  CYCLE="$(state_get cycle)"; CYCLE="${CYCLE:-0}"
119
153
  SESSION_ID="$(state_get session_id)"
120
- log "resuming goal (cycle $CYCLE, session ${SESSION_ID:-none})"
154
+ STALLS="$(state_get stalls)"; STALLS="${STALLS:-0}"
155
+ if [[ $FRESH -eq 1 ]]; then log "resuming goal (cycle $CYCLE) in a FRESH session — the old one (${SESSION_ID:-none}) is left behind"; SESSION_ID=""; STALLS=0; state_set session_id "" stalls 0
156
+ else log "resuming goal (cycle $CYCLE, session ${SESSION_ID:-none})"; fi
121
157
  else
122
- CYCLE=0; SESSION_ID=""
123
- state_set goal "$GOAL" harness "$HARNESS" cycle 0 session_id "" started "$(ts)" last_reason "start"
124
- rm -f "$DONE"
158
+ # Refuse to start over a run that looks active in this folder: a goal.json written or
159
+ # touched in the last three hours whose last_reason is not a finished state. A second
160
+ # supervisor here would clobber the first one's state and flag (MISS-TechieFlow-20260905-21).
161
+ if [[ $DRY -eq 0 && -f "$STATE" ]] && [[ -n "$(find "$STATE" -mmin -180 2>/dev/null)" ]]; then
162
+ _lr="$(state_get last_reason)"
163
+ case "$_lr" in
164
+ done:*|max-cycles|stopped|"") ;;
165
+ *) echo "tf-goal: a run looks active in $APP_DIR (goal.json last_reason=$_lr, touched $(date -u -r "$STATE" +%H:%MZ)). Wait for it, or continue it with --resume." >&2; exit 2 ;;
166
+ esac
167
+ fi
168
+ CYCLE=0; SESSION_ID=""; STALLS=0
169
+ if [[ $DRY -eq 0 ]]; then # a dry run touches nothing: no state, no flag, no log line
170
+ state_set goal "$GOAL" harness "$HARNESS" tier "$TIER" model "$MODEL" cycle 0 session_id "" stalls 0 started "$(ts)" last_reason "start"
171
+ rm -f "$DONE"
172
+ fi
125
173
  fi
126
174
 
127
175
  # ---------------------------------------------------------------- prompts
128
176
  # PREAMBLE IS HARNESS-NEUTRAL. Every claim in it must hold for claude AND opencode
129
- # AND codex, because all three are sent this text verbatim. Anything true of only
130
- # one harness goes in that harness's block below (see the `codex` note after
131
- # FIRST_PROMPT) — never in here. The 2026-08-28 Codex adapter review put Codex's
177
+ # because both are sent this text verbatim. Anything true of only
178
+ # one harness goes in that harness's block below — never in here. The 2026-08-28 review put its
132
179
  # strict no-git policy into this shared text, which then told Claude and OpenCode
133
180
  # goal runs to avoid read-only git that their own hook allows; nothing written down
134
181
  # had said the preamble was shared, so this comment is that rule.
@@ -137,6 +184,7 @@ UNATTENDED GOAL RUN — YOLO MODE IS ON (TechieFlow rule .tfcore/tasks/_yolo-mod
137
184
  - Nobody is watching. NEVER ask a question, NEVER pause for confirmation, NEVER end your turn with a plan, options, or "shall I…". Decide the sensible default yourself and record the decision in the checklist Remarks.
138
185
  - Permissions: deletes are allowed when they are necessary and precisely scoped. Git WRITES (commit/push/add/reset/checkout/stash/tag) are blocked in every mode — never attempt them; the owner commits. Read-only git (status/log/diff/blame) is decided by the harness policy stated below, if any; where nothing further is stated it is available in this mode as supplementary evidence only — the checklist Requirements Status table and the working tree stay primary.
139
186
  - Re-entry: start from PROJECT-STATUS.md + docs/*-Checklist.md (Requirements Status table) — continue from the weakest open REQ; do not redo terminal rows.
187
+ - A build, a test run or any long command runs in the FOREGROUND and you wait for it (a ten-minute timeout is fine). Never start it as a background job and end your turn to wait for it: the turn ending kills the job, and nobody will wake you.
140
188
  - A build pass means the WHOLE checklist: every open REQ reaches at least `Implemented` in this pass, then the verifier is chained inline, then FIX mode loops on FAIL rows until they pass. Never stop with "run build-phase again for the remaining REQs".
141
189
  - When the goal is met (every in-scope REQ terminal, PROJECT-STATUS.md + .html updated, run record emitted): run
142
190
  bash .tfcore/utils/tf-yolo.sh done complete "<one-line summary>"
@@ -145,19 +193,13 @@ UNATTENDED GOAL RUN — YOLO MODE IS ON (TechieFlow rule .tfcore/tasks/_yolo-mod
145
193
  Do not run either command before that point — the supervisor stops the moment you do.
146
194
  TXT
147
195
 
148
- CONTINUE_PROMPT="Continue the UNATTENDED GOAL RUN (YOLO ON). The previous turn ended without the goal-done sentinel — pick up from PROJECT-STATUS.md + the checklist Requirements Status table and keep going. Do not summarise, do not ask; work until the goal is met, then run: bash .tfcore/utils/tf-yolo.sh done complete \"<summary>\". The goal, again:
196
+ CONTINUE_PROMPT="Continue the UNATTENDED GOAL RUN (YOLO ON). The previous turn ended without the goal-done sentinel — pick up from PROJECT-STATUS.md + the checklist Requirements Status table and keep going. Do not summarise, do not ask; work until the goal is met, then run: bash .tfcore/utils/tf-yolo.sh done complete \"<summary>\". Builds and tests run in the FOREGROUND and you wait for them; never end the turn to wait for a background job (the turn ending kills it). The goal, again:
149
197
  $GOAL"
150
198
 
151
199
  FIRST_PROMPT="$PREAMBLE
152
200
 
153
201
  THE GOAL:
154
202
  $GOAL"
155
- if [[ "$HARNESS" == codex ]]; then
156
- FIRST_PROMPT="$FIRST_PROMPT
157
-
158
- CODEX POLICY NOTE: \`.codex/rules/techieflow.rules\` forbids every git/gh command even in YOLO mode, read-only diagnostics included. Use working-tree files and framework artifacts; do not attempt read-only git."
159
- fi
160
-
161
203
  # ---------------------------------------------------------------- harness command
162
204
  harness_cmd() { # $1 = first|resume ; prints the argv via NUL-separated echo
163
205
  local kind="$1" prompt
@@ -181,19 +223,12 @@ harness_cmd() { # $1 = first|resume ; prints the argv via NUL-separated echo
181
223
  fi
182
224
  CMD+=("$prompt")
183
225
  else
184
- if [[ "$kind" == resume && -n "$SESSION_ID" ]]; then
185
- CMD=(codex exec resume "$SESSION_ID" "$prompt" --json)
186
- else
187
- CMD=(codex exec --json --sandbox workspace-write -c approval_policy="never")
188
- [[ -n "$MODEL" ]] && CMD+=(-m "$MODEL")
189
- local tier effort
190
- tier="$(bash "$APP_DIR/.tfcore/utils/tf-harness.sh" tier build-phase)"
191
- effort="$(bash "$APP_DIR/.tfcore/utils/tf-harness.sh" effort "$tier")"
192
- [[ -n "$effort" ]] && CMD+=(-c "model_reasoning_effort=\"$effort\"")
193
- # shellcheck disable=SC2206
194
- [[ -n "${TF_GOAL_CODEX_FLAGS:-}" ]] && CMD+=($TF_GOAL_CODEX_FLAGS)
195
- CMD+=("$prompt")
226
+ CMD=(opencode run --auto)
227
+ [[ -n "$MODEL" ]] && CMD+=(-m "$MODEL")
228
+ if [[ "$kind" == resume ]]; then
229
+ if [[ -n "$SESSION_ID" ]]; then CMD+=(-s "$SESSION_ID"); else CMD+=(-c); fi
196
230
  fi
231
+ CMD+=("$prompt")
197
232
  fi
198
233
  }
199
234
 
@@ -220,9 +255,35 @@ now = time.time()
220
255
  def out(kind, detail):
221
256
  print(f"{kind}\t{detail}"); sys.exit(0)
222
257
 
258
+ # ---- 0. Claude Code stream-json: the last `result` line says how the turn ended, and a
259
+ # clean one (is_error false) is a STOP, never a crash. Until 2026-09-06 the crash regex
260
+ # below matched the harmless field "api_error_status":null that every clean result carries,
261
+ # so every early stop was called a harness error and backed off 2m, 4m, 8m … instead of
262
+ # being re-prompted after 30s (MISS-TechieFlow-20260905-23, MISS-TechieFlow-20260906-01).
263
+ last_result = None
264
+ for line in text.splitlines():
265
+ line = line.strip()
266
+ if not line.startswith("{") or '"result"' not in line:
267
+ continue
268
+ try:
269
+ d = json.loads(line)
270
+ except Exception:
271
+ continue
272
+ if isinstance(d, dict) and d.get("type") == "result":
273
+ last_result = d
274
+ if last_result is not None and rc == 0 and not last_result.get("is_error"):
275
+ out("IDLE", "clean stop: result is_error=false, %s turn(s)" % last_result.get("num_turns", "?"))
276
+ # a rejected rate_limit_event carries the exact reset epoch — better than parsing "9am"
277
+ if last_result is not None and last_result.get("is_error"):
278
+ ev = re.findall(r'"rate_limit_info"\s*:\s*\{[^{}]*"status"\s*:\s*"rejected"[^{}]*"resetsAt"\s*:\s*(\d{10})', text)
279
+ if not ev:
280
+ ev = re.findall(r'"status"\s*:\s*"rejected"[^{}]*?"resetsAt"\s*:\s*(\d{10})', text)
281
+ if ev:
282
+ out("LIMIT", f"{int(ev[-1]) + buffer_min*60}\tparsed")
283
+
223
284
  # ---- 1. usage limit?
224
285
  LIMIT_PAT = re.compile(
225
- r"(usage limit|hit your limit|exceeded your .*limit|you've hit your (5-hour|weekly|usage) limit|rate[ _-]?limit(ed)?|"
286
+ r"(usage limit|hit your limit|exceeded your .*limit|you've hit your (5-hour|weekly|usage) limit|rate[ _-]?limit(ed)?(?!_event)|"
226
287
  r"limit (has been )?(reached|exceeded)|too many requests|\b429\b|overloaded_error|quota exceeded|"
227
288
  r"out of extra usage|resets? (at|in)\b|weekly limit|session limit)", re.I)
228
289
  m = LIMIT_PAT.search(tail)
@@ -280,7 +341,7 @@ if m:
280
341
  out("LIMIT", f"{int(resume_at)}\tparsed")
281
342
 
282
343
  # ---- 2. a crash / API error / auth problem?
283
- if rc != 0 or re.search(r"(api_error|internal server error|\b5\d\d\b .*error|ECONNRESET|ETIMEDOUT|ENOTFOUND|socket hang up|"
344
+ if rc != 0 or re.search(r"(api_error(?!_status)|internal server error|\b5\d\d\b .*error|ECONNRESET|ETIMEDOUT|ENOTFOUND|socket hang up|"
284
345
  r"authentication_error|invalid api key|not logged in|please run /login|error_during_execution|"
285
346
  r"\"is_error\"\s*:\s*true|Error: .*(fetch|network|connect))", tail, re.I):
286
347
  out("CRASH", f"rc={rc}")
@@ -290,7 +351,7 @@ out("IDLE", "no sentinel")
290
351
  PY
291
352
  }
292
353
 
293
- extract_session_id() { # from a cycle's output file (Claude/OpenCode/Codex JSONL)
354
+ extract_session_id() { # from a cycle's output file (Claude/OpenCode JSONL)
294
355
  python3 - "$1" <<'PY' 2>/dev/null
295
356
  import sys, json, re
296
357
  sid = ""
@@ -308,6 +369,60 @@ print(sid)
308
369
  PY
309
370
  }
310
371
 
372
+ extract_model() { # the model the harness reported for this cycle, from its own output
373
+ python3 - "$1" <<'PY' 2>/dev/null
374
+ import sys, json, re
375
+ m = ""
376
+ try:
377
+ for line in open(sys.argv[1], errors="replace"):
378
+ line = line.strip()
379
+ if not line.startswith("{") or '"model"' not in line:
380
+ continue
381
+ try: d = json.loads(line)
382
+ except Exception: continue
383
+ if not isinstance(d, dict): continue
384
+ if isinstance(d.get("model"), str) and d["model"]:
385
+ m = d["model"]
386
+ msg = d.get("message")
387
+ if isinstance(msg, dict) and isinstance(msg.get("model"), str) and msg["model"]:
388
+ m = msg["model"]
389
+ except Exception:
390
+ pass
391
+ print(m)
392
+ PY
393
+ }
394
+
395
+ # Park the model this cycle ran on and move to the next one in its tier. Prints nothing and
396
+ # returns 1 when there is nowhere to go — the caller then sleeps, which is what it always did.
397
+ # $1 = epoch the limit is stated to reset at (or empty when it could not be parsed)
398
+ # $2 = why, for the cooldown record
399
+ try_fallback() {
400
+ [[ $FALLBACK -eq 1 ]] || return 1
401
+ local until="${1:-}" why="${2:-usage limit}" cur next
402
+ cur="$MODEL"
403
+ [[ -z "$cur" ]] && cur="$(extract_model "$OUT")"
404
+ if [[ -z "$cur" ]]; then
405
+ cur="$(TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" pick "$TIER" "$HARNESS" 2>/dev/null | tail -1)"
406
+ [[ "$cur" == inherit ]] && cur=""
407
+ fi
408
+ if [[ -z "$cur" ]]; then
409
+ log "no fallback: nothing here says which model this cycle ran on (no --model, no model id in the output, no routing answer for tier $TIER)"
410
+ return 1
411
+ fi
412
+ [[ -z "$until" ]] && until="probe"
413
+ TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" cooldown "$HARNESS" "$cur" "$until" "$why" >>"$LOG" 2>&1
414
+ next="$(TF_PROJECT_DIR="$APP_DIR" bash "$PICK_SH" pick "$TIER" "$HARNESS" 2>/dev/null | tail -1)"
415
+ if [[ -z "$next" || "$next" == "inherit" ]]; then
416
+ log "no fallback left: every model in the $TIER chain is on cooldown — waiting for the reset instead"
417
+ return 1
418
+ fi
419
+ MODEL="$next"
420
+ state_set model "$MODEL" fallback_from "$cur" last_reason "fallback:$cur->$MODEL"
421
+ log "FALLBACK: $cur is limited ($why) — the next cycle runs on $MODEL (tier $TIER). Re-binding so the sub-agents move too."
422
+ [[ -f "$BIND_SH" ]] && bash "$BIND_SH" "$APP_DIR" 2>&1 | sed 's/^/ /' | tee -a "$LOG" >&2
423
+ return 0
424
+ }
425
+
311
426
  # Fallback when a limit message carries no parseable reset time: fire a one-turn
312
427
  # probe every PROBE_MIN minutes until it comes back clean (exit 0 AND no limit
313
428
  # wording in its output). Gives up after PROBE_MAX_H hours and resumes anyway.
@@ -318,10 +433,8 @@ probe_until_clear() {
318
433
  n=$(( n + 1 ))
319
434
  if [[ "$HARNESS" == claude ]]; then
320
435
  ( cd "$APP_DIR" && claude -p --max-turns 1 --output-format text "Reply with the single word OK." ) > "$pout" 2>&1; prc=$?
321
- elif [[ "$HARNESS" == opencode ]]; then
322
- ( cd "$APP_DIR" && opencode run --auto "Reply with the single word OK." ) > "$pout" 2>&1; prc=$?
323
436
  else
324
- ( cd "$APP_DIR" && codex exec --json --sandbox read-only -c approval_policy="never" "Reply with the single word OK." ) > "$pout" 2>&1; prc=$?
437
+ ( cd "$APP_DIR" && opencode run --auto "Reply with the single word OK." ) > "$pout" 2>&1; prc=$?
325
438
  fi
326
439
  if [[ $prc -eq 0 ]] && ! grep -qiE 'usage limit|hit your limit|rate[ _-]?limit|limit (has been )?(reached|exceeded)|too many requests|\b429\b|overloaded|weekly limit|resets? (at|in)\b' "$pout"; then
327
440
  log "probe #$n OK"; return 0
@@ -329,7 +442,7 @@ probe_until_clear() {
329
442
  log "probe #$n still limited (rc=$prc: $(head -c 120 "$pout" | tr '\n' ' ')) — next in ${PROBE_MIN}m"
330
443
  state_set resume_at "probe #$((n+1)) at $(date -d "+${PROBE_MIN} min" '+%H:%M' 2>/dev/null)"
331
444
  if [[ $(date +%s) -ge $deadline ]]; then log "probe window (${PROBE_MAX_H}h) exhausted — resuming anyway"; return 1; fi
332
- sleep $(( PROBE_MIN * 60 ))
445
+ nap $(( PROBE_MIN * 60 ))
333
446
  done
334
447
  }
335
448
 
@@ -338,18 +451,95 @@ sleep_until() { # epoch
338
451
  while :; do
339
452
  now=$(date +%s); left=$(( target - now )); [[ $left -le 0 ]] && break
340
453
  state_set resume_at "$(date -u -d "@$target" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "$target")"
341
- if [[ $left -gt 300 ]]; then sleep 300; else sleep "$left"; fi
454
+ if [[ $left -gt 300 ]]; then nap 300; else nap "$left"; fi
342
455
  done
343
456
  state_set resume_at ""
344
457
  }
345
458
 
346
- # Debug: `tf-goal.sh --classify <output-file> [rc] <app-dir> x` prints the classification and exits.
459
+ # Debug: `TF_GOAL_CLASSIFY=<output-file> [TF_GOAL_CLASSIFY_RC=n] tf-goal.sh <app-dir> x` prints the classification and exits.
347
460
  if [[ -n "${TF_GOAL_CLASSIFY:-}" ]]; then classify_output "$TF_GOAL_CLASSIFY" "${TF_GOAL_CLASSIFY_RC:-0}"; exit 0; fi
348
461
 
462
+ # ---------------------------------------------------------------- the harness child
463
+ # The harness runs as a background child in its own process group, so the supervisor can
464
+ # (a) watch its output file grow and kill it after STALL_MIN silent minutes — the OpenCode
465
+ # run that printed nothing for 43 minutes while the supervisor waited forever
466
+ # (MISS-TechieFlow-20260905-22) — and (b) take the child down with it on Ctrl-C / kill,
467
+ # instead of leaving an orphaned agent working in the folder (the previous trap only
468
+ # cleared the YOLO flag and did not even exit, so a killed supervisor carried on to the
469
+ # next cycle: MISS-TechieFlow-20260906-02).
470
+ CHILD=""; STALLED=0
471
+ STALL_SEC="${TF_GOAL_STALL_SEC:-$(( STALL_MIN * 60 ))}"; STALL_TICK="${TF_GOAL_STALL_TICK:-30}"
472
+ kill_child() {
473
+ [[ -n "$CHILD" ]] || return 0
474
+ kill -0 "$CHILD" 2>/dev/null || { CHILD=""; return 0; }
475
+ kill -TERM -- "-$CHILD" 2>/dev/null || kill -TERM "$CHILD" 2>/dev/null || true
476
+ local i=0
477
+ while kill -0 "$CHILD" 2>/dev/null && [[ $i -lt 10 ]]; do sleep 1; i=$((i+1)); done
478
+ kill -KILL -- "-$CHILD" 2>/dev/null || kill -KILL "$CHILD" 2>/dev/null || true
479
+ CHILD=""
480
+ }
481
+ run_cycle() { # runs "${CMD[@]}" in $APP_DIR, output to $OUT and $LOG; sets RC, STALLED
482
+ STALLED=0
483
+ local quiet=0 prev=-1 size
484
+ if [[ -n "${TF_GOAL_FAKE_CMD:-}" ]]; then CMD=(bash -c "$TF_GOAL_FAKE_CMD"); fi
485
+ if command -v setsid >/dev/null 2>&1; then
486
+ ( cd "$APP_DIR" && TF_YOLO=1 exec setsid "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
487
+ else
488
+ ( cd "$APP_DIR" && TF_YOLO=1 exec "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1 &
489
+ fi
490
+ CHILD=$!
491
+ while kill -0 "$CHILD" 2>/dev/null; do
492
+ nap "$STALL_TICK"
493
+ kill -0 "$CHILD" 2>/dev/null || break
494
+ size="$(stat -c %s "$OUT" 2>/dev/null || echo 0)"
495
+ if [[ "$size" == "$prev" ]]; then quiet=$(( quiet + STALL_TICK )); else quiet=0; prev="$size"; fi
496
+ if [[ "$STALL_SEC" -gt 0 && $quiet -ge "$STALL_SEC" ]]; then
497
+ log "STALL: no output for $(( quiet / 60 ))m$(( quiet % 60 ))s (cycle $CYCLE, ${size} bytes) — stopping the harness, re-prompting"
498
+ kill_child; STALLED=1; break
499
+ fi
500
+ done
501
+ wait "$CHILD" 2>/dev/null; RC=$?; CHILD=""
502
+ sleep 1 # let tee flush
503
+ }
504
+ # OpenCode prints NOTHING on a provider refusal — `opencode run` shows its header and waits
505
+ # while ~/.local/share/opencode/log/opencode.log says "Monthly usage limit reached. Resets in
506
+ # 6 days" (2026-09-06, three silent 15-minute stalls on MyDiary-oc; MISS-TechieFlow-20260906-11).
507
+ # After a stall the supervisor reads that log for stream errors newer than the cycle start.
508
+ OPENCODE_LOG="${TF_GOAL_OPENCODE_LOG:-$HOME/.local/share/opencode/log/opencode.log}"
509
+ opencode_log_error() { # $1 = cycle start epoch; prints the newest provider error text since then, or nothing
510
+ [[ "$HARNESS" == opencode && -f "$OPENCODE_LOG" ]] || return 0
511
+ python3 - "$OPENCODE_LOG" "$1" <<'PY2' 2>/dev/null
512
+ import re, sys, datetime
513
+ path, since = sys.argv[1], float(sys.argv[2])
514
+ last = ""
515
+ try:
516
+ for line in open(path, errors="replace"):
517
+ if "stream error" not in line: continue
518
+ m = re.search(r"timestamp=(\S+)", line)
519
+ try:
520
+ ts = datetime.datetime.fromisoformat(m.group(1).replace("Z", "+00:00")).timestamp()
521
+ except Exception:
522
+ continue
523
+ if ts < since: continue
524
+ e = re.search(r'error\.error="([^"]*)"', line)
525
+ last = (e.group(1) if e else line.strip())[:300]
526
+ except Exception:
527
+ pass
528
+ print(last)
529
+ PY2
530
+ }
531
+ on_signal() {
532
+ trap - INT TERM
533
+ log "supervisor stopped by signal (cycle $CYCLE) — stopping the harness child; continue with --resume"
534
+ kill_child
535
+ state_set last_reason "stopped" ended "$(ts)"
536
+ exit 130
537
+ }
538
+
349
539
  # ---------------------------------------------------------------- main loop
350
540
  export TF_YOLO=1
351
541
  # Pin the state dir to the APP (not the caller's cwd / a parent repo's CLAUDE_PROJECT_DIR).
352
- CLAUDE_PROJECT_DIR="$APP_DIR" TF_PROJECT_DIR="$APP_DIR" bash "$YOLO_SH" on --source goal --goal "$GOAL" >/dev/null 2>&1 || true
542
+ [[ $DRY -eq 0 ]] && { CLAUDE_PROJECT_DIR="$APP_DIR" TF_PROJECT_DIR="$APP_DIR" bash "$YOLO_SH" on --source goal --goal "$GOAL" >/dev/null 2>&1 || true; }
353
543
 
354
544
  # The flag we just wrote is OURS — clear it on EVERY exit path (goal done, max
355
545
  # cycles, Ctrl-C, kill). Without this it survives the run and silently puts every
@@ -362,9 +552,9 @@ clear_goal_yolo() {
362
552
  grep -q '"source":[[:space:]]*"goal"' "$f" 2>/dev/null || return 0
363
553
  CLAUDE_PROJECT_DIR="$APP_DIR" TF_PROJECT_DIR="$APP_DIR" bash "$YOLO_SH" off >/dev/null 2>&1 || true
364
554
  }
365
- trap clear_goal_yolo EXIT INT TERM
555
+ [[ $DRY -eq 0 ]] && { trap clear_goal_yolo EXIT; trap on_signal INT TERM; }
366
556
  BACKOFF=120
367
- KIND=first; [[ $RESUME -eq 1 && $CYCLE -gt 0 ]] && KIND=resume
557
+ KIND=first; [[ $RESUME -eq 1 && $CYCLE -gt 0 && $FRESH -eq 0 ]] && KIND=resume
368
558
 
369
559
  while :; do
370
560
  if [[ -f "$DONE" ]]; then
@@ -374,33 +564,61 @@ while :; do
374
564
  [[ "$OUTCOME" == blocked ]] && exit 3 || exit 0
375
565
  fi
376
566
  if [[ $CYCLE -ge $MAX_CYCLES ]]; then log "max cycles ($MAX_CYCLES) reached — stopping"; state_set last_reason "max-cycles"; exit 4; fi
377
- CYCLE=$((CYCLE + 1)); state_set cycle "$CYCLE"
567
+ CYCLE=$((CYCLE + 1))
378
568
  harness_cmd "$KIND"
379
569
  OUT="$STATE_DIR/goal-cycle-$CYCLE.out"
570
+ if [[ $DRY -eq 1 ]]; then echo "dry run — would launch cycle $CYCLE ($KIND):" >&2; printf ' %q' "${CMD[@]}"; echo; exit 0; fi
571
+ state_set cycle "$CYCLE"; CYCLE_START="$(date +%s)"
380
572
  log "cycle $CYCLE ($KIND) → ${CMD[*]:0:6} … (prompt ${#CMD[-1]} chars)"
381
- if [[ $DRY -eq 1 ]]; then printf ' %q' "${CMD[@]}"; echo; exit 0; fi
382
573
 
383
- ( cd "$APP_DIR" && TF_YOLO=1 "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1
384
- RC=$?
385
- sleep 1 # let tee flush
574
+ # The run's start is now, not when the agent reaches step 0: write an unclaimed marker the
575
+ # first command's `tf-phase.sh start` claims (MISS-TechieFlow-20260905-20).
576
+ [[ $CYCLE -eq 1 && -f "$APP_DIR/.tfcore/utils/tf-phase.sh" ]] && bash "$APP_DIR/.tfcore/utils/tf-phase.sh" goal "$(basename "$APP_DIR")" >/dev/null 2>&1 || true
577
+ run_cycle
386
578
  SID="$(extract_session_id "$OUT")"; [[ -n "$SID" ]] && { SESSION_ID="$SID"; state_set session_id "$SID"; }
387
- if [[ "$HARNESS" == codex ]]; then
388
- python3 "$APP_DIR/.tfcore/utils/tf-codex-telemetry.py" "$APP_DIR" "$OUT" || true
389
- fi
390
579
  KIND=resume
391
580
 
392
581
  if [[ -f "$DONE" ]]; then continue; fi
393
582
 
394
- IFS=$'\t' read -r CLASS DETAIL HOW < <(classify_output "$OUT" "$RC")
583
+ if [[ $STALLED -eq 1 ]]; then
584
+ CLASS=IDLE; DETAIL="stalled ${STALL_MIN}m"; HOW=""
585
+ PERR="$(opencode_log_error "$CYCLE_START")"
586
+ if [[ -n "$PERR" ]] && grep -qiE "usage limit|monthly|balance|quota|rate limit|429|insufficient|billing|unauthorized|api key" <<<"$PERR"; then
587
+ log "the provider refused the model while the harness printed nothing (cycle $CYCLE): $PERR"
588
+ # A monthly limit or an empty balance states no reset time, so the model is parked for the
589
+ # probe window and the run continues on the next model in the tier. Only when the chain is
590
+ # exhausted does this stay what it was before 2026-09-10: the owner's problem, exit 5.
591
+ if try_fallback "" "provider refused the model: ${PERR:0:120}"; then
592
+ STALLS=0; state_set stalls 0; BACKOFF=120; continue
593
+ fi
594
+ log "no other model in the $TIER chain is available — the owner must act (enable balance, wait for the reset, or pick another model); stopping"
595
+ state_set last_reason "provider-limit" ended "$(ts)"
596
+ exit 5
597
+ fi
598
+ STALLS=$(( STALLS + 1 )); state_set stalls "$STALLS"
599
+ # A resumed session that hangs twice running is not coming back (OpenCode `-c` printed its
600
+ # header and nothing else for 15 minutes, twice, on 2026-09-06): the next cycle starts a
601
+ # fresh session with the full goal; the checklist and PROJECT-STATUS carry the state.
602
+ if [[ "$KIND" == resume && $STALLS -ge 2 ]]; then
603
+ log "two stalled resumes in a row — the next cycle starts a FRESH session (the old one, ${SESSION_ID:-none}, is left behind)"
604
+ KIND=first; SESSION_ID=""; STALLS=0; state_set session_id "" stalls 0
605
+ fi
606
+ else
607
+ IFS=$'\t' read -r CLASS DETAIL HOW < <(classify_output "$OUT" "$RC")
608
+ STALLS=0; state_set stalls 0
609
+ fi
395
610
  case "$CLASS" in
396
611
  LIMIT)
612
+ # Another model in the same tier beats waiting, when there is one.
613
+ if [[ "$HOW" == probe ]]; then _FB_UNTIL=""; else _FB_UNTIL="$DETAIL"; fi
614
+ if try_fallback "$_FB_UNTIL" "usage limit"; then BACKOFF=120; continue; fi
397
615
  if [[ "$HOW" == probe ]]; then
398
616
  log "USAGE LIMIT hit (cycle $CYCLE) but no reset time could be parsed from the message — probing every ${PROBE_MIN}m until the API answers again (max ${PROBE_MAX_H}h). Tail of the message:"
399
617
  tail -c 400 "$OUT" | tr '\n' ' ' | sed 's/^/ /' | tee -a "$LOG" >&2; echo >&2
400
618
  state_set last_reason "limit-probe" resume_at "probing every ${PROBE_MIN}m"
401
619
  probe_until_clear
402
620
  log "probe succeeded — resuming session ${SESSION_ID:-(--continue)} after a ${BUFFER_MIN}m buffer"
403
- sleep $(( BUFFER_MIN * 60 ))
621
+ nap $(( BUFFER_MIN * 60 ))
404
622
  else
405
623
  WHEN="$(date -d "@$DETAIL" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || echo "$DETAIL")"
406
624
  log "USAGE LIMIT hit (cycle $CYCLE) — RETRY AT $WHEN (stated reset + ${BUFFER_MIN}m buffer). Sleeping."
@@ -412,10 +630,10 @@ while :; do
412
630
  CRASH)
413
631
  log "harness/API error (cycle $CYCLE, $DETAIL) — backing off ${BACKOFF}s then resuming"
414
632
  state_set last_reason "crash:$DETAIL"
415
- sleep "$BACKOFF"; BACKOFF=$(( BACKOFF * 2 )); [[ $BACKOFF -gt 1800 ]] && BACKOFF=1800 ;;
633
+ nap "$BACKOFF"; BACKOFF=$(( BACKOFF * 2 )); [[ $BACKOFF -gt 1800 ]] && BACKOFF=1800 ;;
416
634
  IDLE|*)
417
- log "agent stopped without the goal-done sentinel (cycle $CYCLE) — re-prompting in ${IDLE_RETRY}s"
635
+ log "agent stopped without the goal-done sentinel (cycle $CYCLE, ${DETAIL:-no sentinel}) — re-prompting in ${IDLE_RETRY}s"
418
636
  state_set last_reason "idle"
419
- sleep "$IDLE_RETRY"; BACKOFF=120 ;;
637
+ nap "$IDLE_RETRY"; BACKOFF=120 ;;
420
638
  esac
421
639
  done
@@ -6,7 +6,7 @@
6
6
  # scripts call this instead of hard-coding either harness's dialect.
7
7
  #
8
8
  # USAGE
9
- # tf-harness.sh detect -> claude-code | opencode | codex | unknown
9
+ # tf-harness.sh detect -> claude-code | opencode | unknown
10
10
  # tf-harness.sh root -> project root path
11
11
  # tf-harness.sh enabled -> true | false (routing.yaml flag)
12
12
  # tf-harness.sh tier <phase|subagent> -> frontier | standard | economy | inherit
@@ -42,8 +42,7 @@ _root() {
42
42
 
43
43
  # --- detect ----------------------------------------------------------------
44
44
  _detect() {
45
- case "${TF_HARNESS:-}" in claude-code|opencode|codex) printf '%s' "$TF_HARNESS"; return ;; esac
46
- if [[ -n "${CODEX_THREAD_ID:-}${CODEX_SESSION_ID:-}" ]]; then printf 'codex'; return; fi
45
+ case "${TF_HARNESS:-}" in claude-code|opencode) printf '%s' "$TF_HARNESS"; return ;; esac
47
46
  if [[ -n "${CLAUDECODE:-}${CLAUDE_CODE_ENTRYPOINT:-}${CLAUDE_CODE_SESSION_ID:-}${CLAUDE_PROJECT_DIR:-}" ]]; then
48
47
  printf 'claude-code'; return
49
48
  fi
@@ -58,7 +57,6 @@ _detect() {
58
57
  ppid="$(printf '%s' "${stat##*) }" | awk '{print $2}')"
59
58
  case "$name" in
60
59
  *opencode*) printf 'opencode'; return ;;
61
- *codex*) printf 'codex'; return ;;
62
60
  *claude*) printf 'claude-code'; return ;;
63
61
  esac
64
62
  pid="$ppid"
@@ -92,7 +90,6 @@ _model() {
92
90
  [[ -f "$f" ]] || { printf 'inherit'; return; }
93
91
  local key="claude"
94
92
  [[ "$harness" == "opencode" ]] && key="opencode"
95
- [[ "$harness" == "codex" ]] && key="codex"
96
93
  awk -v tier="$tier" -v key="$key" '
97
94
  /^tiers:/ { in_tiers=1; next }
98
95
  /^[a-z]+:/ { in_tiers=0 }
@@ -135,13 +132,11 @@ case "$CMD" in
135
132
  H="$(_detect)"
136
133
  case "$KIND" in
137
134
  task)
138
- if [[ "$H" == "codex" ]]; then printf 'Use the $techieflow-%s skill with arguments: %s\n' "${NAME%-phase}" "$ARGS"
139
- elif [[ "$H" == "opencode" ]]; then printf '/techieflow:tasks:%s %s\n' "$NAME" "$ARGS"
135
+ if [[ "$H" == "opencode" ]]; then printf '/techieflow:tasks:%s %s\n' "$NAME" "$ARGS"
140
136
  else printf '/TechieFlow:tasks:%s %s\n' "$NAME" "$ARGS"; fi
141
137
  ;;
142
138
  agent)
143
- if [[ "$H" == "codex" ]]; then printf 'Delegate to the `%s` Codex subagent with: %s\n' "$NAME" "$ARGS"
144
- elif [[ "$H" == "opencode" ]]; then printf 'opencode run --agent %s "%s" (TUI: Tab to %s, then type: %s)\n' "$NAME" "$ARGS" "$NAME" "$ARGS"
139
+ if [[ "$H" == "opencode" ]]; then printf 'opencode run --agent %s "%s" (TUI: Tab to %s, then type: %s)\n' "$NAME" "$ARGS" "$NAME" "$ARGS"
145
140
  else printf '/TechieFlow:agents:%s %s\n' "$NAME" "$ARGS"; fi
146
141
  ;;
147
142
  *) echo "usage: tf-harness.sh invoke <task|agent> <name> [args]" >&2; exit 2 ;;