@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.
- package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +86 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +358 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
package/.tfcore/utils/tf-goal.sh
CHANGED
|
@@ -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
|
-
#
|
|
7
|
-
# SESSION), crashes, and "I stopped early to ask you something"
|
|
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
|
|
14
|
-
# --model <id> claude: --model; opencode: -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 ·
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
#
|
|
130
|
-
# one harness goes in that harness's block below
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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" &&
|
|
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
|
-
|
|
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
|
|
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: `
|
|
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))
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
|
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" == "
|
|
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" == "
|
|
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 ;;
|