@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.
Files changed (151) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
  2. package/.claude/commands/TechieFlow/agents/architect.md +84 -0
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
  9. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
  11. package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
  12. package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
  13. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
  14. package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
  17. package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
  18. package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
  19. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
  20. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
  21. package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
  22. package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
  23. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
  24. package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
  25. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
  26. package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
  27. package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
  28. package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
  29. package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
  30. package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
  31. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
  32. package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
  33. package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
  34. package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
  35. package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
  36. package/.claude/commands/generate-html.md +124 -0
  37. package/.codex/config.toml +46 -0
  38. package/.codex/hooks.json +63 -0
  39. package/.codex/rules/techieflow.rules +8 -0
  40. package/.opencode/command/generate-html.md +129 -0
  41. package/.opencode/plugin/techieflow.js +350 -0
  42. package/.tfcore/TOKEN-GUIDE.md +59 -0
  43. package/.tfcore/agents/analyst.md +97 -0
  44. package/.tfcore/agents/architect.md +84 -0
  45. package/.tfcore/agents/flow-master.md +217 -0
  46. package/.tfcore/agents/verifier.md +72 -0
  47. package/.tfcore/checklists/architect-checklist.md +438 -0
  48. package/.tfcore/core-config.yaml +47 -0
  49. package/.tfcore/data/brainstorming-techniques.md +36 -0
  50. package/.tfcore/data/elicitation-methods.md +154 -0
  51. package/.tfcore/data/techieflow-kb.md +807 -0
  52. package/.tfcore/data/technical-preferences.md +3 -0
  53. package/.tfcore/enhanced-ide-development-workflow.md +248 -0
  54. package/.tfcore/hooks/block-git.sh +301 -0
  55. package/.tfcore/hooks/codex-adapter.py +248 -0
  56. package/.tfcore/hooks/guard-artifacts.sh +101 -0
  57. package/.tfcore/hooks/guard-status-html.sh +102 -0
  58. package/.tfcore/hooks/guard-status.sh +124 -0
  59. package/.tfcore/hooks/guard-verify.sh +108 -0
  60. package/.tfcore/hooks/metrics-session.sh +129 -0
  61. package/.tfcore/hooks/session-pointer.sh +52 -0
  62. package/.tfcore/hooks/sweep-artifacts.sh +198 -0
  63. package/.tfcore/install-manifest.yaml +231 -0
  64. package/.tfcore/routing.yaml +105 -0
  65. package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
  66. package/.tfcore/tasks/_smoke-test-policy.md +98 -0
  67. package/.tfcore/tasks/_status-update-gate.md +78 -0
  68. package/.tfcore/tasks/_yolo-mode.md +77 -0
  69. package/.tfcore/tasks/advanced-elicitation.md +117 -0
  70. package/.tfcore/tasks/amend-docs.md +149 -0
  71. package/.tfcore/tasks/author-brd.md +79 -0
  72. package/.tfcore/tasks/build-phase.md +231 -0
  73. package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
  74. package/.tfcore/tasks/create-doc.md +101 -0
  75. package/.tfcore/tasks/day1-brownfield.md +507 -0
  76. package/.tfcore/tasks/day1-greenfield.md +224 -0
  77. package/.tfcore/tasks/devguide.md +224 -0
  78. package/.tfcore/tasks/document-project.md +343 -0
  79. package/.tfcore/tasks/execute-checklist.md +86 -0
  80. package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
  81. package/.tfcore/tasks/fix-issues.md +134 -0
  82. package/.tfcore/tasks/generate-html.md +115 -0
  83. package/.tfcore/tasks/handoff-phase.md +126 -0
  84. package/.tfcore/tasks/index-docs.md +173 -0
  85. package/.tfcore/tasks/kb-mode-interaction.md +75 -0
  86. package/.tfcore/tasks/log-miss.md +160 -0
  87. package/.tfcore/tasks/metrics-report.md +143 -0
  88. package/.tfcore/tasks/mockups.md +122 -0
  89. package/.tfcore/tasks/productguide.md +105 -0
  90. package/.tfcore/tasks/refresh-status.md +135 -0
  91. package/.tfcore/tasks/render-workflow-docs.md +117 -0
  92. package/.tfcore/tasks/shard-doc.md +185 -0
  93. package/.tfcore/tasks/split-brd.md +131 -0
  94. package/.tfcore/tasks/triage-issues.md +157 -0
  95. package/.tfcore/tasks/verify-phase.md +579 -0
  96. package/.tfcore/telemetry/SCHEMA.md +678 -0
  97. package/.tfcore/telemetry/install-metrics.sh +390 -0
  98. package/.tfcore/telemetry/pre-commit +94 -0
  99. package/.tfcore/telemetry/tf-metrics.sh +1415 -0
  100. package/.tfcore/templates/architecture-tmpl.yaml +650 -0
  101. package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
  102. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
  103. package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
  104. package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
  105. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
  106. package/.tfcore/templates/market-research-tmpl.yaml +252 -0
  107. package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
  108. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
  109. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
  110. package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
  111. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
  112. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
  113. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
  114. package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
  115. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
  116. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
  117. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
  118. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
  119. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
  120. package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
  121. package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
  122. package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
  123. package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
  124. package/.tfcore/user-guide.md +633 -0
  125. package/.tfcore/utils/techieflow-doc-template.md +325 -0
  126. package/.tfcore/utils/tf-assets.sh +310 -0
  127. package/.tfcore/utils/tf-codex-bind.py +196 -0
  128. package/.tfcore/utils/tf-codex-telemetry.py +60 -0
  129. package/.tfcore/utils/tf-emit.sh +1075 -0
  130. package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
  131. package/.tfcore/utils/tf-goal.sh +421 -0
  132. package/.tfcore/utils/tf-harness.sh +160 -0
  133. package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
  134. package/.tfcore/utils/tf-mockup-parity.sh +65 -0
  135. package/.tfcore/utils/tf-opencode-audit.py +126 -0
  136. package/.tfcore/utils/tf-perf.sh +241 -0
  137. package/.tfcore/utils/tf-render-html.py +715 -0
  138. package/.tfcore/utils/tf-render-html.sh +28 -0
  139. package/.tfcore/utils/tf-routing-bind.sh +216 -0
  140. package/.tfcore/utils/tf-routing.sh +247 -0
  141. package/.tfcore/utils/tf-yolo.sh +119 -0
  142. package/.tfcore/working-in-the-brownfield.md +606 -0
  143. package/LICENSE +201 -0
  144. package/README.md +1212 -0
  145. package/WORKFLOW.html +1986 -0
  146. package/docs/TechieFlow-Installation.md +299 -0
  147. package/opencode.jsonc +929 -0
  148. package/package.json +60 -0
  149. package/scripts/install.mjs +992 -0
  150. package/scripts/test-install.mjs +505 -0
  151. 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