@techierathore/techieflow 1.0.0
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 +97 -0
- package/.claude/commands/TechieFlow/agents/architect.md +84 -0
- package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
- package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
- package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
- package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
- package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
- package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
- package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
- package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
- package/.claude/commands/generate-html.md +124 -0
- package/.codex/config.toml +46 -0
- package/.codex/hooks.json +63 -0
- package/.codex/rules/techieflow.rules +8 -0
- package/.opencode/command/generate-html.md +129 -0
- package/.opencode/plugin/techieflow.js +350 -0
- package/.tfcore/TOKEN-GUIDE.md +59 -0
- package/.tfcore/agents/analyst.md +97 -0
- package/.tfcore/agents/architect.md +84 -0
- package/.tfcore/agents/flow-master.md +217 -0
- package/.tfcore/agents/verifier.md +72 -0
- package/.tfcore/checklists/architect-checklist.md +438 -0
- package/.tfcore/core-config.yaml +47 -0
- package/.tfcore/data/brainstorming-techniques.md +36 -0
- package/.tfcore/data/elicitation-methods.md +154 -0
- package/.tfcore/data/techieflow-kb.md +807 -0
- package/.tfcore/data/technical-preferences.md +3 -0
- package/.tfcore/enhanced-ide-development-workflow.md +248 -0
- package/.tfcore/hooks/block-git.sh +301 -0
- package/.tfcore/hooks/codex-adapter.py +248 -0
- package/.tfcore/hooks/guard-artifacts.sh +101 -0
- package/.tfcore/hooks/guard-status-html.sh +102 -0
- package/.tfcore/hooks/guard-status.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +108 -0
- package/.tfcore/hooks/metrics-session.sh +129 -0
- package/.tfcore/hooks/session-pointer.sh +52 -0
- package/.tfcore/hooks/sweep-artifacts.sh +198 -0
- package/.tfcore/install-manifest.yaml +231 -0
- package/.tfcore/routing.yaml +105 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
- package/.tfcore/tasks/_smoke-test-policy.md +98 -0
- package/.tfcore/tasks/_status-update-gate.md +78 -0
- package/.tfcore/tasks/_yolo-mode.md +77 -0
- package/.tfcore/tasks/advanced-elicitation.md +117 -0
- package/.tfcore/tasks/amend-docs.md +149 -0
- package/.tfcore/tasks/author-brd.md +79 -0
- package/.tfcore/tasks/build-phase.md +231 -0
- package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
- package/.tfcore/tasks/create-doc.md +101 -0
- package/.tfcore/tasks/day1-brownfield.md +507 -0
- package/.tfcore/tasks/day1-greenfield.md +224 -0
- package/.tfcore/tasks/devguide.md +224 -0
- package/.tfcore/tasks/document-project.md +343 -0
- package/.tfcore/tasks/execute-checklist.md +86 -0
- package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
- package/.tfcore/tasks/fix-issues.md +134 -0
- package/.tfcore/tasks/generate-html.md +115 -0
- package/.tfcore/tasks/handoff-phase.md +126 -0
- package/.tfcore/tasks/index-docs.md +173 -0
- package/.tfcore/tasks/kb-mode-interaction.md +75 -0
- package/.tfcore/tasks/log-miss.md +160 -0
- package/.tfcore/tasks/metrics-report.md +143 -0
- package/.tfcore/tasks/mockups.md +122 -0
- package/.tfcore/tasks/productguide.md +105 -0
- package/.tfcore/tasks/refresh-status.md +135 -0
- package/.tfcore/tasks/render-workflow-docs.md +117 -0
- package/.tfcore/tasks/shard-doc.md +185 -0
- package/.tfcore/tasks/split-brd.md +131 -0
- package/.tfcore/tasks/triage-issues.md +157 -0
- package/.tfcore/tasks/verify-phase.md +579 -0
- package/.tfcore/telemetry/SCHEMA.md +678 -0
- package/.tfcore/telemetry/install-metrics.sh +390 -0
- package/.tfcore/telemetry/pre-commit +94 -0
- package/.tfcore/telemetry/tf-metrics.sh +1415 -0
- package/.tfcore/templates/architecture-tmpl.yaml +650 -0
- package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
- package/.tfcore/templates/market-research-tmpl.yaml +252 -0
- package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
- package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
- package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
- package/.tfcore/user-guide.md +633 -0
- package/.tfcore/utils/techieflow-doc-template.md +325 -0
- package/.tfcore/utils/tf-assets.sh +310 -0
- package/.tfcore/utils/tf-codex-bind.py +196 -0
- package/.tfcore/utils/tf-codex-telemetry.py +60 -0
- package/.tfcore/utils/tf-emit.sh +1075 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
- package/.tfcore/utils/tf-goal.sh +421 -0
- package/.tfcore/utils/tf-harness.sh +160 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
- package/.tfcore/utils/tf-mockup-parity.sh +65 -0
- package/.tfcore/utils/tf-opencode-audit.py +126 -0
- package/.tfcore/utils/tf-perf.sh +241 -0
- package/.tfcore/utils/tf-render-html.py +715 -0
- package/.tfcore/utils/tf-render-html.sh +28 -0
- package/.tfcore/utils/tf-routing-bind.sh +216 -0
- package/.tfcore/utils/tf-routing.sh +247 -0
- package/.tfcore/utils/tf-yolo.sh +119 -0
- package/.tfcore/working-in-the-brownfield.md +606 -0
- package/LICENSE +201 -0
- package/README.md +1212 -0
- package/WORKFLOW.html +1986 -0
- package/docs/TechieFlow-Installation.md +299 -0
- package/opencode.jsonc +929 -0
- package/package.json +60 -0
- package/scripts/install.mjs +992 -0
- package/scripts/test-install.mjs +505 -0
- package/scripts/validate.mjs +155 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-goal.sh — unattended GOAL runner / supervisor for TechieFlow (rule: .tfcore/tasks/_yolo-mode.md)
|
|
3
|
+
#
|
|
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.
|
|
8
|
+
#
|
|
9
|
+
# bash .tfcore/utils/tf-goal.sh [options] <app-dir> "<goal text>"
|
|
10
|
+
# bash .tfcore/utils/tf-goal.sh [options] <app-dir> @goal.md
|
|
11
|
+
#
|
|
12
|
+
# Options
|
|
13
|
+
# --harness claude|opencode|codex default: claude
|
|
14
|
+
# --model <id> claude: --model; opencode: -m; codex: -m
|
|
15
|
+
# --buffer-min <n> minutes added after a stated limit-reset time (default 15)
|
|
16
|
+
# --probe-min <n> limit hit but NO reset time parseable → fire a one-turn probe every n
|
|
17
|
+
# minutes until the API answers again, then resume (default 15)
|
|
18
|
+
# --probe-max-hours <n> give up probing and resume anyway after n hours (default 8)
|
|
19
|
+
# --default-wait-min <n> (legacy) only used to stamp resume_at in goal.json when probing
|
|
20
|
+
# --max-cycles <n> give up after n launches (default 60)
|
|
21
|
+
# --idle-retry-sec <n> pause before re-prompting an agent that stopped without finishing (default 30)
|
|
22
|
+
# --resume continue the last goal run recorded in .tfcore/.session/goal.json
|
|
23
|
+
# --dry-run print the commands, run nothing
|
|
24
|
+
#
|
|
25
|
+
# Exit codes: 0 goal complete · 3 agent declared the goal BLOCKED (owner input
|
|
26
|
+
# needed) · 4 max cycles reached · 2 usage error.
|
|
27
|
+
#
|
|
28
|
+
# Files (all under <app-dir>/.tfcore/.session/, never committed):
|
|
29
|
+
# yolo.json YOLO flag (tf-yolo.sh on --source goal) — the hook reads it
|
|
30
|
+
# goal.json supervisor state: cycle, session_id, last_reason, resume_at
|
|
31
|
+
# goal.log everything the harness printed, all cycles, timestamped
|
|
32
|
+
# goal-done.json the agent's completion sentinel (tf-yolo.sh done [complete|blocked])
|
|
33
|
+
#
|
|
34
|
+
# How the agent is told to finish: the prompt preamble (below) instructs it to run
|
|
35
|
+
# `bash .tfcore/utils/tf-yolo.sh done complete "<summary>"` when the goal is met,
|
|
36
|
+
# or `... done blocked "<why>"` when only the owner can unblock it. A cycle that
|
|
37
|
+
# ends with neither sentinel is classified from its output: USAGE LIMIT → sleep
|
|
38
|
+
# until reset+buffer; CRASH/API error → exponential backoff (2m → 30m);
|
|
39
|
+
# otherwise the agent simply stopped (asked a question / summarised) → re-prompt.
|
|
40
|
+
#
|
|
41
|
+
# Harness invocation (verified flags — docs/Capability-Matrix.md):
|
|
42
|
+
# claude -p "<prompt>" --permission-mode bypassPermissions --output-format stream-json --verbose
|
|
43
|
+
# resume: claude -p --resume <session_id> "<continue>" (fallback: --continue)
|
|
44
|
+
# 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
|
+
# exit 2). The approval policy is set as a config override instead.
|
|
49
|
+
# resume: codex exec resume <thread-id> "<continue>" --json
|
|
50
|
+
# Override command lines with TF_GOAL_CLAUDE_FLAGS / TF_GOAL_OPENCODE_FLAGS /
|
|
51
|
+
# TF_GOAL_CODEX_FLAGS.
|
|
52
|
+
|
|
53
|
+
set -u
|
|
54
|
+
|
|
55
|
+
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
|
|
58
|
+
while [[ $# -gt 0 ]]; do
|
|
59
|
+
case "$1" in
|
|
60
|
+
--harness) HARNESS="$2"; shift 2 ;;
|
|
61
|
+
--model) MODEL="$2"; shift 2 ;;
|
|
62
|
+
--buffer-min) BUFFER_MIN="$2"; shift 2 ;;
|
|
63
|
+
--default-wait-min) DEFAULT_WAIT_MIN="$2"; shift 2 ;;
|
|
64
|
+
--probe-min) PROBE_MIN="$2"; shift 2 ;;
|
|
65
|
+
--probe-max-hours) PROBE_MAX_H="$2"; shift 2 ;;
|
|
66
|
+
--max-cycles) MAX_CYCLES="$2"; shift 2 ;;
|
|
67
|
+
--idle-retry-sec) IDLE_RETRY="$2"; shift 2 ;;
|
|
68
|
+
--resume) RESUME=1; shift ;;
|
|
69
|
+
--dry-run) DRY=1; shift ;;
|
|
70
|
+
-h|--help) sed -n '2,45p' "$0"; exit 0 ;;
|
|
71
|
+
--) shift; break ;;
|
|
72
|
+
-*) echo "unknown option $1" >&2; exit 2 ;;
|
|
73
|
+
*) break ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
APP_DIR="${1:-}"; GOAL_ARG="${2:-}"
|
|
77
|
+
if [[ -z "$APP_DIR" || ( -z "$GOAL_ARG" && $RESUME -eq 0 ) ]]; then
|
|
78
|
+
echo "usage: tf-goal.sh [options] <app-dir> \"<goal>\" | @goal.md (or --resume <app-dir>)" >&2; exit 2
|
|
79
|
+
fi
|
|
80
|
+
APP_DIR="$(cd "$APP_DIR" 2>/dev/null && pwd)" || { echo "no such dir: $1" >&2; exit 2; }
|
|
81
|
+
[[ -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
|
|
83
|
+
command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; exit 2; }
|
|
84
|
+
|
|
85
|
+
STATE_DIR="$APP_DIR/.tfcore/.session"; mkdir -p "$STATE_DIR"
|
|
86
|
+
STATE="$STATE_DIR/goal.json"; LOG="$STATE_DIR/goal.log"; DONE="$STATE_DIR/goal-done.json"
|
|
87
|
+
YOLO_SH="$APP_DIR/.tfcore/utils/tf-yolo.sh"
|
|
88
|
+
|
|
89
|
+
if [[ "$GOAL_ARG" == @* ]]; then
|
|
90
|
+
GOAL_FILE="${GOAL_ARG#@}"; [[ -f "$GOAL_FILE" ]] || { echo "goal file not found: $GOAL_FILE" >&2; exit 2; }
|
|
91
|
+
GOAL="$(cat "$GOAL_FILE")"
|
|
92
|
+
else
|
|
93
|
+
GOAL="$GOAL_ARG"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
97
|
+
log() { printf '[%s] tf-goal: %s\n' "$(ts)" "$*" | tee -a "$LOG" >&2; }
|
|
98
|
+
|
|
99
|
+
state_get() { python3 -c 'import json,sys
|
|
100
|
+
try: print(json.load(open(sys.argv[1])).get(sys.argv[2],""))
|
|
101
|
+
except Exception: print("")' "$STATE" "$1" 2>/dev/null; }
|
|
102
|
+
state_set() { # key value [key value ...]
|
|
103
|
+
python3 - "$STATE" "$@" <<'PY' 2>/dev/null
|
|
104
|
+
import json, sys
|
|
105
|
+
p = sys.argv[1]; kv = sys.argv[2:]
|
|
106
|
+
try: d = json.load(open(p))
|
|
107
|
+
except Exception: d = {}
|
|
108
|
+
for k, v in zip(kv[::2], kv[1::2]):
|
|
109
|
+
d[k] = int(v) if v.isdigit() else v
|
|
110
|
+
json.dump(d, open(p, "w"), indent=1)
|
|
111
|
+
PY
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if [[ $RESUME -eq 1 ]]; then
|
|
115
|
+
[[ -f "$STATE" ]] || { echo "--resume: no $STATE" >&2; exit 2; }
|
|
116
|
+
[[ -z "$GOAL" ]] && GOAL="$(state_get goal)"
|
|
117
|
+
HARNESS="$(state_get harness)"; HARNESS="${HARNESS:-claude}"
|
|
118
|
+
CYCLE="$(state_get cycle)"; CYCLE="${CYCLE:-0}"
|
|
119
|
+
SESSION_ID="$(state_get session_id)"
|
|
120
|
+
log "resuming goal (cycle $CYCLE, session ${SESSION_ID:-none})"
|
|
121
|
+
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"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------- prompts
|
|
128
|
+
# 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
|
|
132
|
+
# strict no-git policy into this shared text, which then told Claude and OpenCode
|
|
133
|
+
# goal runs to avoid read-only git that their own hook allows; nothing written down
|
|
134
|
+
# had said the preamble was shared, so this comment is that rule.
|
|
135
|
+
read -r -d '' PREAMBLE <<'TXT'
|
|
136
|
+
UNATTENDED GOAL RUN — YOLO MODE IS ON (TechieFlow rule .tfcore/tasks/_yolo-mode.md; read it first).
|
|
137
|
+
- 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
|
+
- 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
|
+
- Re-entry: start from PROJECT-STATUS.md + docs/*-Checklist.md (Requirements Status table) — continue from the weakest open REQ; do not redo terminal rows.
|
|
140
|
+
- 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
|
+
- When the goal is met (every in-scope REQ terminal, PROJECT-STATUS.md + .html updated, run record emitted): run
|
|
142
|
+
bash .tfcore/utils/tf-yolo.sh done complete "<one-line summary>"
|
|
143
|
+
If, and only if, something ONLY THE OWNER can resolve blocks every remaining REQ (credentials, a physical device, a paid account, a product decision you must not make): finish everything else, write the blocker into PROJECT-STATUS "Known blockers", then run
|
|
144
|
+
bash .tfcore/utils/tf-yolo.sh done blocked "<what the owner must do>"
|
|
145
|
+
Do not run either command before that point — the supervisor stops the moment you do.
|
|
146
|
+
TXT
|
|
147
|
+
|
|
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:
|
|
149
|
+
$GOAL"
|
|
150
|
+
|
|
151
|
+
FIRST_PROMPT="$PREAMBLE
|
|
152
|
+
|
|
153
|
+
THE GOAL:
|
|
154
|
+
$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
|
+
# ---------------------------------------------------------------- harness command
|
|
162
|
+
harness_cmd() { # $1 = first|resume ; prints the argv via NUL-separated echo
|
|
163
|
+
local kind="$1" prompt
|
|
164
|
+
if [[ "$kind" == first ]]; then prompt="$FIRST_PROMPT"; else prompt="$CONTINUE_PROMPT"; fi
|
|
165
|
+
if [[ "$HARNESS" == claude ]]; then
|
|
166
|
+
CMD=(claude -p --permission-mode bypassPermissions --output-format stream-json --verbose)
|
|
167
|
+
[[ -n "$MODEL" ]] && CMD+=(--model "$MODEL")
|
|
168
|
+
# shellcheck disable=SC2206
|
|
169
|
+
[[ -n "${TF_GOAL_CLAUDE_FLAGS:-}" ]] && CMD+=($TF_GOAL_CLAUDE_FLAGS)
|
|
170
|
+
if [[ "$kind" == resume ]]; then
|
|
171
|
+
if [[ -n "$SESSION_ID" ]]; then CMD+=(--resume "$SESSION_ID"); else CMD+=(--continue); fi
|
|
172
|
+
fi
|
|
173
|
+
CMD+=("$prompt")
|
|
174
|
+
elif [[ "$HARNESS" == opencode ]]; then
|
|
175
|
+
CMD=(opencode run --auto)
|
|
176
|
+
[[ -n "$MODEL" ]] && CMD+=(-m "$MODEL")
|
|
177
|
+
# shellcheck disable=SC2206
|
|
178
|
+
[[ -n "${TF_GOAL_OPENCODE_FLAGS:-}" ]] && CMD+=($TF_GOAL_OPENCODE_FLAGS)
|
|
179
|
+
if [[ "$kind" == resume ]]; then
|
|
180
|
+
if [[ -n "$SESSION_ID" ]]; then CMD+=(-s "$SESSION_ID"); else CMD+=(-c); fi
|
|
181
|
+
fi
|
|
182
|
+
CMD+=("$prompt")
|
|
183
|
+
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")
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------- classification
|
|
201
|
+
# Reads the cycle's output file; prints one line: KIND<TAB>detail
|
|
202
|
+
# LIMIT <epoch-to-resume> CRASH <text> IDLE <text>
|
|
203
|
+
classify_output() {
|
|
204
|
+
TF_OUT="$1" TF_BUFFER_MIN="$BUFFER_MIN" TF_DEFAULT_WAIT_MIN="$DEFAULT_WAIT_MIN" TF_RC="$2" python3 - <<'PY'
|
|
205
|
+
import os, re, sys, json, time, datetime, calendar
|
|
206
|
+
try:
|
|
207
|
+
from zoneinfo import ZoneInfo
|
|
208
|
+
except Exception:
|
|
209
|
+
ZoneInfo = None
|
|
210
|
+
|
|
211
|
+
path = os.environ["TF_OUT"]; buffer_min = int(os.environ["TF_BUFFER_MIN"]); default_wait = int(os.environ["TF_DEFAULT_WAIT_MIN"])
|
|
212
|
+
rc = int(os.environ.get("TF_RC") or 0)
|
|
213
|
+
try:
|
|
214
|
+
text = open(path, errors="replace").read()
|
|
215
|
+
except Exception:
|
|
216
|
+
text = ""
|
|
217
|
+
tail = text[-20000:]
|
|
218
|
+
now = time.time()
|
|
219
|
+
|
|
220
|
+
def out(kind, detail):
|
|
221
|
+
print(f"{kind}\t{detail}"); sys.exit(0)
|
|
222
|
+
|
|
223
|
+
# ---- 1. usage limit?
|
|
224
|
+
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)?|"
|
|
226
|
+
r"limit (has been )?(reached|exceeded)|too many requests|\b429\b|overloaded_error|quota exceeded|"
|
|
227
|
+
r"out of extra usage|resets? (at|in)\b|weekly limit|session limit)", re.I)
|
|
228
|
+
m = LIMIT_PAT.search(tail)
|
|
229
|
+
if m:
|
|
230
|
+
resume_at = None
|
|
231
|
+
# a) legacy "Claude AI usage limit reached|<epoch>"
|
|
232
|
+
e = re.search(r"limit reached\|(\d{10})", tail)
|
|
233
|
+
if e:
|
|
234
|
+
resume_at = int(e.group(1))
|
|
235
|
+
# b) ISO timestamp near the match
|
|
236
|
+
if resume_at is None:
|
|
237
|
+
iso = re.search(r"(20\d\d-\d\d-\d\dT\d\d:\d\d(?::\d\d)?(?:\.\d+)?(?:Z|[+-]\d\d:?\d\d)?)", tail[m.start()-200:m.end()+300])
|
|
238
|
+
if iso:
|
|
239
|
+
s = iso.group(1).replace("Z", "+00:00")
|
|
240
|
+
try:
|
|
241
|
+
resume_at = datetime.datetime.fromisoformat(s).timestamp()
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
# c) "resets in 2h 14m" / "resets in 45 minutes" / "retry after 120"
|
|
245
|
+
if resume_at is None:
|
|
246
|
+
r = re.search(r"(?:resets?|try again|retry)\s+(?:in|after)[:\s]+(?:(\d+)\s*h(?:ours?|rs?)?)?\s*(?:(\d+)\s*m(?:in(?:utes?)?)?)?\s*(?:(\d+)\s*s(?:ec(?:onds?)?)?)?", tail, re.I)
|
|
247
|
+
if r and any(r.groups()):
|
|
248
|
+
h, mi, s = (int(x) if x else 0 for x in r.groups())
|
|
249
|
+
resume_at = now + h*3600 + mi*60 + s
|
|
250
|
+
else:
|
|
251
|
+
r = re.search(r"retry[- ]after[:=\s]+(\d+)", tail, re.I)
|
|
252
|
+
if r:
|
|
253
|
+
resume_at = now + int(r.group(1))
|
|
254
|
+
# d) "resets 3pm (Asia/Kolkata)" / "resets at 14:30" / "resets Tue 3pm"
|
|
255
|
+
if resume_at is None:
|
|
256
|
+
r = re.search(r"resets?\s+(?:at\s+)?(?:(mon|tue|wed|thu|fri|sat|sun)[a-z]*\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?(?:\s*\(([^)]+)\))?", tail, re.I)
|
|
257
|
+
if r:
|
|
258
|
+
dow, hh, mm, ampm, tzname = r.groups()
|
|
259
|
+
hh = int(hh); mm = int(mm or 0)
|
|
260
|
+
if ampm:
|
|
261
|
+
if ampm.lower() == "pm" and hh != 12: hh += 12
|
|
262
|
+
if ampm.lower() == "am" and hh == 12: hh = 0
|
|
263
|
+
tz = None
|
|
264
|
+
if tzname and ZoneInfo:
|
|
265
|
+
try: tz = ZoneInfo(tzname.strip())
|
|
266
|
+
except Exception: tz = None
|
|
267
|
+
base = datetime.datetime.now(tz) if tz else datetime.datetime.now().astimezone()
|
|
268
|
+
cand = base.replace(hour=hh, minute=mm, second=0, microsecond=0)
|
|
269
|
+
if dow:
|
|
270
|
+
want = ["mon","tue","wed","thu","fri","sat","sun"].index(dow.lower()[:3])
|
|
271
|
+
delta = (want - cand.weekday()) % 7
|
|
272
|
+
cand = cand + datetime.timedelta(days=delta)
|
|
273
|
+
if cand.timestamp() <= now:
|
|
274
|
+
cand = cand + datetime.timedelta(days=1 if not dow else 7)
|
|
275
|
+
resume_at = cand.timestamp()
|
|
276
|
+
if resume_at is None:
|
|
277
|
+
# no parseable reset time → the supervisor PROBES instead of guessing
|
|
278
|
+
out("LIMIT", f"{int(now + default_wait*60)}\tprobe")
|
|
279
|
+
resume_at = max(resume_at, now + 60) + buffer_min*60
|
|
280
|
+
out("LIMIT", f"{int(resume_at)}\tparsed")
|
|
281
|
+
|
|
282
|
+
# ---- 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|"
|
|
284
|
+
r"authentication_error|invalid api key|not logged in|please run /login|error_during_execution|"
|
|
285
|
+
r"\"is_error\"\s*:\s*true|Error: .*(fetch|network|connect))", tail, re.I):
|
|
286
|
+
out("CRASH", f"rc={rc}")
|
|
287
|
+
|
|
288
|
+
# ---- 3. otherwise the agent just stopped
|
|
289
|
+
out("IDLE", "no sentinel")
|
|
290
|
+
PY
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
extract_session_id() { # from a cycle's output file (Claude/OpenCode/Codex JSONL)
|
|
294
|
+
python3 - "$1" <<'PY' 2>/dev/null
|
|
295
|
+
import sys, json, re
|
|
296
|
+
sid = ""
|
|
297
|
+
for line in open(sys.argv[1], errors="replace"):
|
|
298
|
+
line = line.strip()
|
|
299
|
+
if not line.startswith("{"): continue
|
|
300
|
+
try: d = json.loads(line)
|
|
301
|
+
except Exception: continue
|
|
302
|
+
if isinstance(d, dict) and d.get("session_id"): sid = d["session_id"]
|
|
303
|
+
if isinstance(d, dict) and d.get("type") == "thread.started" and d.get("thread_id"): sid = d["thread_id"]
|
|
304
|
+
if not sid:
|
|
305
|
+
m = re.findall(r'"(?:session_id|thread_id)"\s*:\s*"([0-9A-Za-z_-]{8,})"', open(sys.argv[1], errors="replace").read())
|
|
306
|
+
sid = m[-1] if m else ""
|
|
307
|
+
print(sid)
|
|
308
|
+
PY
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Fallback when a limit message carries no parseable reset time: fire a one-turn
|
|
312
|
+
# probe every PROBE_MIN minutes until it comes back clean (exit 0 AND no limit
|
|
313
|
+
# wording in its output). Gives up after PROBE_MAX_H hours and resumes anyway.
|
|
314
|
+
probe_until_clear() {
|
|
315
|
+
local deadline=$(( $(date +%s) + PROBE_MAX_H * 3600 )) n=0 pout prc
|
|
316
|
+
pout="$STATE_DIR/goal-probe.out"
|
|
317
|
+
while :; do
|
|
318
|
+
n=$(( n + 1 ))
|
|
319
|
+
if [[ "$HARNESS" == claude ]]; then
|
|
320
|
+
( 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
|
+
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=$?
|
|
325
|
+
fi
|
|
326
|
+
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
|
+
log "probe #$n OK"; return 0
|
|
328
|
+
fi
|
|
329
|
+
log "probe #$n still limited (rc=$prc: $(head -c 120 "$pout" | tr '\n' ' ')) — next in ${PROBE_MIN}m"
|
|
330
|
+
state_set resume_at "probe #$((n+1)) at $(date -d "+${PROBE_MIN} min" '+%H:%M' 2>/dev/null)"
|
|
331
|
+
if [[ $(date +%s) -ge $deadline ]]; then log "probe window (${PROBE_MAX_H}h) exhausted — resuming anyway"; return 1; fi
|
|
332
|
+
sleep $(( PROBE_MIN * 60 ))
|
|
333
|
+
done
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
sleep_until() { # epoch
|
|
337
|
+
local target="$1" now left
|
|
338
|
+
while :; do
|
|
339
|
+
now=$(date +%s); left=$(( target - now )); [[ $left -le 0 ]] && break
|
|
340
|
+
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
|
|
342
|
+
done
|
|
343
|
+
state_set resume_at ""
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Debug: `tf-goal.sh --classify <output-file> [rc] <app-dir> x` prints the classification and exits.
|
|
347
|
+
if [[ -n "${TF_GOAL_CLASSIFY:-}" ]]; then classify_output "$TF_GOAL_CLASSIFY" "${TF_GOAL_CLASSIFY_RC:-0}"; exit 0; fi
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------- main loop
|
|
350
|
+
export TF_YOLO=1
|
|
351
|
+
# 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
|
|
353
|
+
|
|
354
|
+
# The flag we just wrote is OURS — clear it on EVERY exit path (goal done, max
|
|
355
|
+
# cycles, Ctrl-C, kill). Without this it survives the run and silently puts every
|
|
356
|
+
# later interactive session in this repo into YOLO: rm/rmdir/sudo stop prompting
|
|
357
|
+
# and read-only git opens up, with nothing on screen saying so. Only a flag whose
|
|
358
|
+
# source is `goal` is removed — a `*yolo` the owner turned on by hand is left alone.
|
|
359
|
+
clear_goal_yolo() {
|
|
360
|
+
local f="$STATE_DIR/yolo.json"
|
|
361
|
+
[[ -f "$f" ]] || return 0
|
|
362
|
+
grep -q '"source":[[:space:]]*"goal"' "$f" 2>/dev/null || return 0
|
|
363
|
+
CLAUDE_PROJECT_DIR="$APP_DIR" TF_PROJECT_DIR="$APP_DIR" bash "$YOLO_SH" off >/dev/null 2>&1 || true
|
|
364
|
+
}
|
|
365
|
+
trap clear_goal_yolo EXIT INT TERM
|
|
366
|
+
BACKOFF=120
|
|
367
|
+
KIND=first; [[ $RESUME -eq 1 && $CYCLE -gt 0 ]] && KIND=resume
|
|
368
|
+
|
|
369
|
+
while :; do
|
|
370
|
+
if [[ -f "$DONE" ]]; then
|
|
371
|
+
OUTCOME="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("outcome","complete"))' "$DONE" 2>/dev/null || echo complete)"
|
|
372
|
+
log "goal-done sentinel present (outcome=$OUTCOME): $(cat "$DONE")"
|
|
373
|
+
state_set last_reason "done:$OUTCOME" ended "$(ts)"
|
|
374
|
+
[[ "$OUTCOME" == blocked ]] && exit 3 || exit 0
|
|
375
|
+
fi
|
|
376
|
+
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"
|
|
378
|
+
harness_cmd "$KIND"
|
|
379
|
+
OUT="$STATE_DIR/goal-cycle-$CYCLE.out"
|
|
380
|
+
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
|
+
|
|
383
|
+
( cd "$APP_DIR" && TF_YOLO=1 "${CMD[@]}" ) > >(tee -a "$LOG" > "$OUT") 2>&1
|
|
384
|
+
RC=$?
|
|
385
|
+
sleep 1 # let tee flush
|
|
386
|
+
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
|
+
KIND=resume
|
|
391
|
+
|
|
392
|
+
if [[ -f "$DONE" ]]; then continue; fi
|
|
393
|
+
|
|
394
|
+
IFS=$'\t' read -r CLASS DETAIL HOW < <(classify_output "$OUT" "$RC")
|
|
395
|
+
case "$CLASS" in
|
|
396
|
+
LIMIT)
|
|
397
|
+
if [[ "$HOW" == probe ]]; then
|
|
398
|
+
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
|
+
tail -c 400 "$OUT" | tr '\n' ' ' | sed 's/^/ /' | tee -a "$LOG" >&2; echo >&2
|
|
400
|
+
state_set last_reason "limit-probe" resume_at "probing every ${PROBE_MIN}m"
|
|
401
|
+
probe_until_clear
|
|
402
|
+
log "probe succeeded — resuming session ${SESSION_ID:-(--continue)} after a ${BUFFER_MIN}m buffer"
|
|
403
|
+
sleep $(( BUFFER_MIN * 60 ))
|
|
404
|
+
else
|
|
405
|
+
WHEN="$(date -d "@$DETAIL" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || echo "$DETAIL")"
|
|
406
|
+
log "USAGE LIMIT hit (cycle $CYCLE) — RETRY AT $WHEN (stated reset + ${BUFFER_MIN}m buffer). Sleeping."
|
|
407
|
+
state_set last_reason "limit" resume_at "$WHEN"
|
|
408
|
+
sleep_until "$DETAIL"
|
|
409
|
+
log "limit window over — resuming session ${SESSION_ID:-(--continue)}"
|
|
410
|
+
fi
|
|
411
|
+
BACKOFF=120 ;;
|
|
412
|
+
CRASH)
|
|
413
|
+
log "harness/API error (cycle $CYCLE, $DETAIL) — backing off ${BACKOFF}s then resuming"
|
|
414
|
+
state_set last_reason "crash:$DETAIL"
|
|
415
|
+
sleep "$BACKOFF"; BACKOFF=$(( BACKOFF * 2 )); [[ $BACKOFF -gt 1800 ]] && BACKOFF=1800 ;;
|
|
416
|
+
IDLE|*)
|
|
417
|
+
log "agent stopped without the goal-done sentinel (cycle $CYCLE) — re-prompting in ${IDLE_RETRY}s"
|
|
418
|
+
state_set last_reason "idle"
|
|
419
|
+
sleep "$IDLE_RETRY"; BACKOFF=120 ;;
|
|
420
|
+
esac
|
|
421
|
+
done
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# TechieFlow harness answer-machine (docs/Adapter-Design.md §2.4).
|
|
3
|
+
#
|
|
4
|
+
# One deterministic place for "which harness am I in / where is the root /
|
|
5
|
+
# what tier is this phase / what model is that tier here". Task templates and
|
|
6
|
+
# scripts call this instead of hard-coding either harness's dialect.
|
|
7
|
+
#
|
|
8
|
+
# USAGE
|
|
9
|
+
# tf-harness.sh detect -> claude-code | opencode | codex | unknown
|
|
10
|
+
# tf-harness.sh root -> project root path
|
|
11
|
+
# tf-harness.sh enabled -> true | false (routing.yaml flag)
|
|
12
|
+
# tf-harness.sh tier <phase|subagent> -> frontier | standard | economy | inherit
|
|
13
|
+
# tf-harness.sh model <tier> [harness] -> model id for the tier on the harness
|
|
14
|
+
# tf-harness.sh effort <tier> -> high | medium | low
|
|
15
|
+
# tf-harness.sh invoke task <name> "<args>" -> the harness's slash form
|
|
16
|
+
# tf-harness.sh invoke agent <name> "<args>" -> the harness's invocation form
|
|
17
|
+
# tf-harness.sh session -> the session pointer JSON for this harness
|
|
18
|
+
#
|
|
19
|
+
# Answers print on stdout; unknown answers print "unknown"/"inherit" rather than
|
|
20
|
+
# failing (callers embed this in prompts and next-command strings — a hard error
|
|
21
|
+
# there is worse than a graceful unknown). Usage errors exit 2.
|
|
22
|
+
#
|
|
23
|
+
# HARNESS DETECTION mirrors tf-emit.sh: TF_HARNESS (set by the harness bridge —
|
|
24
|
+
# the OpenCode plugin's shell.env — never by an agent) beats the marker-variable
|
|
25
|
+
# scan, which beats the /proc ancestry walk. See DECISIONS.md 2026-08-20 §3.
|
|
26
|
+
|
|
27
|
+
set +e
|
|
28
|
+
|
|
29
|
+
CMD="${1:-}"
|
|
30
|
+
|
|
31
|
+
# --- root ------------------------------------------------------------------
|
|
32
|
+
_root() {
|
|
33
|
+
if [[ -n "${TF_PROJECT_DIR:-}" && -d "${TF_PROJECT_DIR:-}" ]]; then printf '%s' "$TF_PROJECT_DIR"; return; fi
|
|
34
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" && -d "${CLAUDE_PROJECT_DIR:-}" ]]; then printf '%s' "$CLAUDE_PROJECT_DIR"; return; fi
|
|
35
|
+
local d="$PWD"
|
|
36
|
+
while [[ "$d" != "/" ]]; do
|
|
37
|
+
[[ -d "$d/.tfcore" ]] && { printf '%s' "$d"; return; }
|
|
38
|
+
d="$(dirname "$d")"
|
|
39
|
+
done
|
|
40
|
+
printf '%s' "$PWD"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# --- detect ----------------------------------------------------------------
|
|
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
|
|
47
|
+
if [[ -n "${CLAUDECODE:-}${CLAUDE_CODE_ENTRYPOINT:-}${CLAUDE_CODE_SESSION_ID:-}${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
48
|
+
printf 'claude-code'; return
|
|
49
|
+
fi
|
|
50
|
+
local k
|
|
51
|
+
for k in $(compgen -v | grep '^OPENCODE' 2>/dev/null); do printf 'opencode'; return; done
|
|
52
|
+
# /proc ancestry walk (Linux/WSL); bounded
|
|
53
|
+
local pid=$PPID name ppid seen=0 stat
|
|
54
|
+
while [[ -n "$pid" && "$pid" -gt 1 && $seen -lt 12 ]]; do
|
|
55
|
+
seen=$((seen + 1))
|
|
56
|
+
stat="$(cat /proc/$pid/stat 2>/dev/null)" || break
|
|
57
|
+
name="${stat#*(}"; name="${name%%)*}"
|
|
58
|
+
ppid="$(printf '%s' "${stat##*) }" | awk '{print $2}')"
|
|
59
|
+
case "$name" in
|
|
60
|
+
*opencode*) printf 'opencode'; return ;;
|
|
61
|
+
*codex*) printf 'codex'; return ;;
|
|
62
|
+
*claude*) printf 'claude-code'; return ;;
|
|
63
|
+
esac
|
|
64
|
+
pid="$ppid"
|
|
65
|
+
done
|
|
66
|
+
printf 'unknown'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# --- routing.yaml lookups (flat two-space format — see the file header) -----
|
|
70
|
+
_ryaml() { printf '%s/.tfcore/routing.yaml' "$(_root)"; }
|
|
71
|
+
|
|
72
|
+
_enabled() {
|
|
73
|
+
local f; f="$(_ryaml)"
|
|
74
|
+
[[ -f "$f" ]] || { printf 'false'; return; }
|
|
75
|
+
awk -F': *' '/^enabled:/ {print $2; found=1; exit} END {if (!found) print "false"}' "$f" | tr -d ' \r'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# tier for a phase or subagent name
|
|
79
|
+
_tier() {
|
|
80
|
+
local want="$1" f; f="$(_ryaml)"
|
|
81
|
+
[[ -f "$f" ]] || { printf 'inherit'; return; }
|
|
82
|
+
awk -v want="$want" '
|
|
83
|
+
/^[a-z]+:/ { sect=$1; sub(":", "", sect) }
|
|
84
|
+
(sect=="phases" || sect=="subagents") && $1==want":" { print $2; found=1; exit }
|
|
85
|
+
END { if (!found) print "inherit" }
|
|
86
|
+
' "$f" | tr -d ' \r'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# model id for a tier on a harness
|
|
90
|
+
_model() {
|
|
91
|
+
local tier="$1" harness="$2" f; f="$(_ryaml)"
|
|
92
|
+
[[ -f "$f" ]] || { printf 'inherit'; return; }
|
|
93
|
+
local key="claude"
|
|
94
|
+
[[ "$harness" == "opencode" ]] && key="opencode"
|
|
95
|
+
[[ "$harness" == "codex" ]] && key="codex"
|
|
96
|
+
awk -v tier="$tier" -v key="$key" '
|
|
97
|
+
/^tiers:/ { in_tiers=1; next }
|
|
98
|
+
/^[a-z]+:/ { in_tiers=0 }
|
|
99
|
+
in_tiers && $1==tier":" { in_tier=1; next }
|
|
100
|
+
in_tiers && in_tier && $1==key":" { print $2; found=1; exit }
|
|
101
|
+
in_tiers && in_tier && /^ [a-z-]+:$/ { in_tier=0 }
|
|
102
|
+
END { if (!found) print "inherit" }
|
|
103
|
+
' "$f" | tr -d ' \r'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_effort() {
|
|
107
|
+
local tier="$1" f; f="$(_ryaml)"
|
|
108
|
+
[[ -f "$f" ]] || return 0
|
|
109
|
+
awk -v tier="$tier" '
|
|
110
|
+
/^effort:/ { in_e=1; next }
|
|
111
|
+
/^[a-z]+:/ { if (NR>1 && $0 !~ /^effort:/) in_e=0 }
|
|
112
|
+
in_e && $1==tier":" { print $2; exit }
|
|
113
|
+
' "$f" | tr -d ' \r'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "$CMD" in
|
|
117
|
+
detect) _detect; echo ;;
|
|
118
|
+
root) _root; echo ;;
|
|
119
|
+
enabled) _enabled; echo ;;
|
|
120
|
+
tier)
|
|
121
|
+
[[ -n "${2:-}" ]] || { echo "usage: tf-harness.sh tier <phase|subagent>" >&2; exit 2; }
|
|
122
|
+
_tier "$2"
|
|
123
|
+
;;
|
|
124
|
+
model)
|
|
125
|
+
[[ -n "${2:-}" ]] || { echo "usage: tf-harness.sh model <tier> [harness]" >&2; exit 2; }
|
|
126
|
+
_model "$2" "${3:-$(_detect)}"
|
|
127
|
+
;;
|
|
128
|
+
effort)
|
|
129
|
+
[[ -n "${2:-}" ]] || { echo "usage: tf-harness.sh effort <tier>" >&2; exit 2; }
|
|
130
|
+
_effort "$2"
|
|
131
|
+
;;
|
|
132
|
+
invoke)
|
|
133
|
+
KIND="${2:-}"; NAME="${3:-}"; ARGS="${4:-}"
|
|
134
|
+
[[ -n "$KIND" && -n "$NAME" ]] || { echo "usage: tf-harness.sh invoke <task|agent> <name> [args]" >&2; exit 2; }
|
|
135
|
+
H="$(_detect)"
|
|
136
|
+
case "$KIND" in
|
|
137
|
+
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"
|
|
140
|
+
else printf '/TechieFlow:tasks:%s %s\n' "$NAME" "$ARGS"; fi
|
|
141
|
+
;;
|
|
142
|
+
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"
|
|
145
|
+
else printf '/TechieFlow:agents:%s %s\n' "$NAME" "$ARGS"; fi
|
|
146
|
+
;;
|
|
147
|
+
*) echo "usage: tf-harness.sh invoke <task|agent> <name> [args]" >&2; exit 2 ;;
|
|
148
|
+
esac
|
|
149
|
+
;;
|
|
150
|
+
session)
|
|
151
|
+
H="$(_detect)"
|
|
152
|
+
F="$(_root)/.tfcore/.session/$H.json"
|
|
153
|
+
if [[ -f "$F" ]]; then cat "$F"; else echo '{}'; fi
|
|
154
|
+
;;
|
|
155
|
+
*)
|
|
156
|
+
sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'
|
|
157
|
+
exit 2
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
160
|
+
exit 0
|