@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,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-render-html.sh — render framework Markdown docs to the shared HTML shell.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-render-html.sh docs/MyApp-BRD.md [more.md ...]
|
|
5
|
+
# bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md --quiet
|
|
6
|
+
#
|
|
7
|
+
# Writes a sibling <file>.html for each input, implementing
|
|
8
|
+
# .tfcore/templates/v4custom/html-render-shell.md — which stays the SPEC: the
|
|
9
|
+
# CSS (§2), theme script (§3) and JS (§7) are extracted from it at render time,
|
|
10
|
+
# so the shell can never drift from its own documentation.
|
|
11
|
+
#
|
|
12
|
+
# Thin wrapper over tf-render-html.py, matching the tf-emit.sh / tf-perf.sh
|
|
13
|
+
# invocation idiom. Python 3 standard library only — no pandoc, no node, no pip.
|
|
14
|
+
#
|
|
15
|
+
# Exit: 0 rendered · 1 a render failed · 2 refused (checklist / spec missing)
|
|
16
|
+
#
|
|
17
|
+
# NOTE: checklists are NEVER rendered (html-render-shell §0). Passing a
|
|
18
|
+
# *-Checklist.md is refused, not silently skipped.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
|
|
23
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
24
|
+
echo "tf-render-html: python3 is required (standard library only)." >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exec python3 "$SELF_DIR/tf-render-html.py" "$@"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-routing-bind.sh — emit (or remove) the harness bindings declared by
|
|
3
|
+
# .tfcore/routing.yaml (docs/Adapter-Design.md §5.3/§5.4; DECISIONS.md 2026-08-20).
|
|
4
|
+
#
|
|
5
|
+
# WHAT IT GENERATES when routing.yaml says `enabled: true`:
|
|
6
|
+
# Claude Code (§5.3):
|
|
7
|
+
# .claude/commands/tf/<phase>.md — routed wrapper commands (/tf:<phase>),
|
|
8
|
+
# frontmatter model:+effort: from the tier
|
|
9
|
+
# .claude/agents/{tf-builder,tf-test-writer,tf-explorer,trblazeui,techierag}.md
|
|
10
|
+
# — tier-bound subagents
|
|
11
|
+
# OpenCode (§5.4):
|
|
12
|
+
# .opencode/opencode.json — pure-JSON binding file loaded ALONGSIDE
|
|
13
|
+
# .opencode/opencode.jsonc; deep-merges
|
|
14
|
+
# model onto the existing agents/commands
|
|
15
|
+
# (verified against OpenCode 1.18.18:
|
|
16
|
+
# model-only agent entries merge, command
|
|
17
|
+
# entries must be complete → they carry
|
|
18
|
+
# template+description+model)
|
|
19
|
+
# Codex:
|
|
20
|
+
# .codex/agents/*.toml + .agents/skills/techieflow-*/SKILL.md, generated
|
|
21
|
+
# by tf-codex-bind.py. Subagent model/effort is pinned when routing is on;
|
|
22
|
+
# skills remain thin loaders and do not claim to switch the main thread.
|
|
23
|
+
#
|
|
24
|
+
# When `enabled: false` (the default) every generated artifact is REMOVED, using
|
|
25
|
+
# the manifest .tfcore/.session/routing-bind.manifest (gitignored) — the script
|
|
26
|
+
# never deletes anything it did not generate. Run it after editing routing.yaml.
|
|
27
|
+
# update-framework.sh runs it automatically in each app.
|
|
28
|
+
#
|
|
29
|
+
# Fail-soft: exits 0 unless the target is not a TechieFlow repo. Routing is
|
|
30
|
+
# declared and OBSERVED (runs.jsonl model/tier/routed) — never enforced.
|
|
31
|
+
|
|
32
|
+
set +e
|
|
33
|
+
|
|
34
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
ROOT="${1:-$(bash "$SELF_DIR/tf-harness.sh" root)}"
|
|
36
|
+
ROOT="$(cd "$ROOT" 2>/dev/null && pwd)"
|
|
37
|
+
[[ -n "$ROOT" && -d "$ROOT/.tfcore" ]] || { echo "tf-routing-bind: not a TechieFlow repo: ${ROOT:-<none>}" >&2; exit 1; }
|
|
38
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-routing-bind: python3 required" >&2; exit 0; }
|
|
39
|
+
|
|
40
|
+
python3 - "$ROOT" <<'PY'
|
|
41
|
+
import json, os, re, sys
|
|
42
|
+
|
|
43
|
+
root = sys.argv[1]
|
|
44
|
+
ryaml = os.path.join(root, ".tfcore", "routing.yaml")
|
|
45
|
+
manifest_path = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
|
|
46
|
+
|
|
47
|
+
# --- parse routing.yaml (flat format — see its header) --------------------
|
|
48
|
+
cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}}
|
|
49
|
+
sect = tier = None
|
|
50
|
+
try:
|
|
51
|
+
for line in open(ryaml, encoding="utf-8"):
|
|
52
|
+
line = line.rstrip("\n")
|
|
53
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
54
|
+
continue
|
|
55
|
+
if not line.startswith(" "):
|
|
56
|
+
key, _, val = line.partition(":")
|
|
57
|
+
sect, tier = key.strip(), None
|
|
58
|
+
if sect == "enabled":
|
|
59
|
+
cfg["enabled"] = val.strip() == "true"
|
|
60
|
+
elif sect == "tiers":
|
|
61
|
+
if re.match(r"^ [a-z-]+:\s*$", line):
|
|
62
|
+
tier = line.strip()[:-1]
|
|
63
|
+
cfg["tiers"][tier] = {}
|
|
64
|
+
elif tier and line.startswith(" "):
|
|
65
|
+
k, _, v = line.strip().partition(":")
|
|
66
|
+
cfg["tiers"][tier][k.strip()] = v.strip()
|
|
67
|
+
elif sect in ("phases", "subagents", "effort"):
|
|
68
|
+
k, _, v = line.strip().partition(":")
|
|
69
|
+
cfg[sect][k.strip()] = v.strip()
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
pass # no routing.yaml -> treat as disabled
|
|
72
|
+
|
|
73
|
+
def model_for(t, harness):
|
|
74
|
+
return cfg["tiers"].get(t, {}).get(harness)
|
|
75
|
+
|
|
76
|
+
# --- remove previously generated artifacts (manifest-driven) --------------
|
|
77
|
+
removed = 0
|
|
78
|
+
try:
|
|
79
|
+
for p in open(manifest_path, encoding="utf-8").read().splitlines():
|
|
80
|
+
p = p.strip()
|
|
81
|
+
if not p or ".." in p:
|
|
82
|
+
continue
|
|
83
|
+
fp = os.path.join(root, p)
|
|
84
|
+
if os.path.isfile(fp):
|
|
85
|
+
os.remove(fp)
|
|
86
|
+
removed += 1
|
|
87
|
+
os.remove(manifest_path)
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
pass
|
|
90
|
+
# prune empty generated dirs
|
|
91
|
+
for d in (os.path.join(root, ".claude", "commands", "tf"),):
|
|
92
|
+
try:
|
|
93
|
+
os.rmdir(d)
|
|
94
|
+
except OSError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
if not cfg["enabled"]:
|
|
98
|
+
print("tf-routing-bind: routing disabled — %d generated artifact(s) removed, nothing emitted" % removed)
|
|
99
|
+
raise SystemExit(0)
|
|
100
|
+
|
|
101
|
+
generated = []
|
|
102
|
+
|
|
103
|
+
def write(relpath, content):
|
|
104
|
+
fp = os.path.join(root, relpath)
|
|
105
|
+
os.makedirs(os.path.dirname(fp), exist_ok=True)
|
|
106
|
+
with open(fp, "w", encoding="utf-8", newline="\n") as fh:
|
|
107
|
+
fh.write(content)
|
|
108
|
+
generated.append(relpath)
|
|
109
|
+
|
|
110
|
+
MARK = "<!-- generated by tf-routing-bind.sh from .tfcore/routing.yaml — do not edit; edit routing.yaml and re-run -->"
|
|
111
|
+
|
|
112
|
+
# --- Claude Code: routed wrapper commands ---------------------------------
|
|
113
|
+
OWNER = {"day1-greenfield": "analyst", "day1-brownfield": "analyst",
|
|
114
|
+
"author-brd": "analyst", "amend-docs": "analyst", "mockups": "analyst",
|
|
115
|
+
"split-brd": "analyst", "verify-phase": "verifier"}
|
|
116
|
+
for phase, t in sorted(cfg["phases"].items()):
|
|
117
|
+
m = model_for(t, "claude")
|
|
118
|
+
if t == "inherit" or not m:
|
|
119
|
+
continue
|
|
120
|
+
owner = OWNER.get(phase, "flow-master")
|
|
121
|
+
eff = cfg["effort"].get(t)
|
|
122
|
+
fm = ["---",
|
|
123
|
+
"description: Routed TechieFlow phase — %s on the %s tier" % (phase, t),
|
|
124
|
+
"model: %s" % m]
|
|
125
|
+
if eff:
|
|
126
|
+
fm.append("effort: %s" % eff)
|
|
127
|
+
fm.append("---")
|
|
128
|
+
body = ("%s\n%s\n\nLoad `.tfcore/agents/%s.md` and adopt its core principles "
|
|
129
|
+
"(git ban, smoke policy, status gate) without printing the greeting. "
|
|
130
|
+
"Then execute `.tfcore/tasks/%s.md` with arguments: $ARGUMENTS\n"
|
|
131
|
+
% ("\n".join(fm), MARK, owner, phase))
|
|
132
|
+
write(os.path.join(".claude", "commands", "tf", phase + ".md"), body)
|
|
133
|
+
|
|
134
|
+
# --- Claude Code: tier-bound subagents ------------------------------------
|
|
135
|
+
SUB_BODY = {
|
|
136
|
+
"tf-builder": ("Builds ONE REQ cluster (FN/NFR) to the project's coding standards. "
|
|
137
|
+
"Read `.tfcore/tasks/_smoke-test-policy.md` first. Hard rules: NEVER run git/gh; "
|
|
138
|
+
"smoke your changed feature yourself before returning; tag work with its [REQ-*] id "
|
|
139
|
+
"in the checklist Remarks; return { reqsImplemented[], filesChanged[], libraryIssues[] }."),
|
|
140
|
+
"tf-test-writer": ("Writes verification tests for one REQ cluster (verify-phase §4). "
|
|
141
|
+
"Black-box: never touch application source. NEVER run git/gh. "
|
|
142
|
+
"Return { testsAdded[], testsRefreshed[], unobservable[] }."),
|
|
143
|
+
"tf-explorer": ("Read-only codebase scan (devguide OBSERVE, index-docs). Never edits, "
|
|
144
|
+
"never runs git/gh. Returns findings as structured text."),
|
|
145
|
+
"trblazeui": ("Read the NuGet-deployed TrBlazeUI persona and fully adopt it. Resolve it in this "
|
|
146
|
+
"order and use the FIRST that exists: `.claude/commands/trblazeui.md` (current deploy target), "
|
|
147
|
+
"`.claude/trblazeui.md` (legacy deploy target), `.trblazeui/TrBlazeUI-AI-Reference.md` "
|
|
148
|
+
"(packaged component reference). Only if NONE exists, report that TrBlazeUI is not deployed "
|
|
149
|
+
"(`dotnet build` the app to unpack the package) and stop."),
|
|
150
|
+
"techierag": ("Read the NuGet-deployed TechieRag persona and fully adopt it. Resolve it in this "
|
|
151
|
+
"order and use the FIRST that exists: `.claude/commands/techierag.md` (current deploy target), "
|
|
152
|
+
"`.claude/techierag.md` (legacy deploy target), `.techierag/TechieRag-AI-Reference.md` "
|
|
153
|
+
"(packaged service reference). Only if NONE exists, report that TechieRag is not deployed "
|
|
154
|
+
"(`dotnet build` the app to unpack the package) and stop."),
|
|
155
|
+
}
|
|
156
|
+
DESC = {
|
|
157
|
+
"tf-builder": "TechieFlow FN/NFR cluster builder (routed tier)",
|
|
158
|
+
"tf-test-writer": "TechieFlow verify-phase test writer (routed tier)",
|
|
159
|
+
"tf-explorer": "TechieFlow read-only explorer (routed tier)",
|
|
160
|
+
"trblazeui": "TrBlazeUI UI builder persona (routed tier wrapper)",
|
|
161
|
+
"techierag": "TechieRag RAG builder persona (routed tier wrapper)",
|
|
162
|
+
}
|
|
163
|
+
for name, t in sorted(cfg["subagents"].items()):
|
|
164
|
+
m = model_for(t, "claude")
|
|
165
|
+
if t == "inherit" or not m or name not in SUB_BODY:
|
|
166
|
+
continue
|
|
167
|
+
body = ("---\nname: %s\ndescription: %s\nmodel: %s\n---\n%s\n%s\n"
|
|
168
|
+
% (name, DESC[name], m, MARK, SUB_BODY[name]))
|
|
169
|
+
write(os.path.join(".claude", "agents", name + ".md"), body)
|
|
170
|
+
|
|
171
|
+
# --- OpenCode: pure-JSON binding file -------------------------------------
|
|
172
|
+
oc = {"$schema": "https://opencode.ai/config.json", "agent": {}, "command": {}}
|
|
173
|
+
PERSONA_TIER = {
|
|
174
|
+
"flow-master": cfg["phases"].get("build-phase"),
|
|
175
|
+
"flow-analyst": cfg["phases"].get("day1-greenfield"),
|
|
176
|
+
"flow-architect": cfg["phases"].get("day1-greenfield"),
|
|
177
|
+
"flow-verifier": cfg["phases"].get("verify-phase"),
|
|
178
|
+
}
|
|
179
|
+
for name, t in PERSONA_TIER.items():
|
|
180
|
+
m = model_for(t, "opencode") if t else None
|
|
181
|
+
if m and t != "inherit":
|
|
182
|
+
oc["agent"][name] = {"model": m}
|
|
183
|
+
for name, t in sorted(cfg["subagents"].items()):
|
|
184
|
+
m = model_for(t, "opencode")
|
|
185
|
+
if t == "inherit" or not m:
|
|
186
|
+
continue
|
|
187
|
+
if name in ("trblazeui", "techierag"):
|
|
188
|
+
oc["agent"][name] = {"model": m} # registered in opencode.jsonc; merge model on
|
|
189
|
+
else:
|
|
190
|
+
oc["agent"][name] = { # new subagents: entries must validate standalone
|
|
191
|
+
"description": DESC.get(name, name),
|
|
192
|
+
"mode": "subagent",
|
|
193
|
+
"model": m,
|
|
194
|
+
"prompt": SUB_BODY.get(name, name),
|
|
195
|
+
}
|
|
196
|
+
for phase, t in sorted(cfg["phases"].items()):
|
|
197
|
+
m = model_for(t, "opencode")
|
|
198
|
+
if t == "inherit" or not m:
|
|
199
|
+
continue
|
|
200
|
+
oc["command"]["techieflow:tasks:" + phase] = { # complete entry: files validate standalone
|
|
201
|
+
"template": "{file:../.tfcore/tasks/%s.md}" % phase,
|
|
202
|
+
"description": "Routed: %s (%s tier)" % (phase, t),
|
|
203
|
+
"model": m,
|
|
204
|
+
}
|
|
205
|
+
write(os.path.join(".opencode", "opencode.json"),
|
|
206
|
+
json.dumps(oc, indent=2, ensure_ascii=False) + "\n")
|
|
207
|
+
|
|
208
|
+
# --- manifest -------------------------------------------------------------
|
|
209
|
+
os.makedirs(os.path.dirname(manifest_path), exist_ok=True)
|
|
210
|
+
with open(manifest_path, "w", encoding="utf-8", newline="\n") as fh:
|
|
211
|
+
fh.write("\n".join(generated) + "\n")
|
|
212
|
+
print("tf-routing-bind: routing enabled — %d artifact(s) generated (%d stale removed)"
|
|
213
|
+
% (len(generated), removed))
|
|
214
|
+
PY
|
|
215
|
+
python3 "$SELF_DIR/tf-codex-bind.py" "$ROOT" || echo "tf-routing-bind: warning: Codex bindings were not refreshed" >&2
|
|
216
|
+
exit 0
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-routing.sh — the ONE command for model routing. Run it from an app repo.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-routing.sh status what is routing doing right now?
|
|
5
|
+
# bash .tfcore/utils/tf-routing.sh on turn routing ON (generates all bindings)
|
|
6
|
+
# bash .tfcore/utils/tf-routing.sh off turn routing OFF (removes all bindings)
|
|
7
|
+
# bash .tfcore/utils/tf-routing.sh set-tier <phase> <tier> e.g. set-tier verify-phase economy
|
|
8
|
+
# bash .tfcore/utils/tf-routing.sh set-model <tier> <harness> <model>
|
|
9
|
+
# e.g. set-model economy opencode opencode-go/deepseek-v4-flash
|
|
10
|
+
# e.g. set-model frontier claude opus
|
|
11
|
+
# e.g. set-model standard codex gpt-5.6-terra
|
|
12
|
+
# bash .tfcore/utils/tf-routing.sh bind re-generate bindings after editing routing.yaml by hand
|
|
13
|
+
# bash .tfcore/utils/tf-routing.sh set-escalation <phase> <attempts> <tier>
|
|
14
|
+
# e.g. set-escalation fix-issues 2 frontier
|
|
15
|
+
# ADVISORY: "after <attempts> runs of <phase> on the same
|
|
16
|
+
# REQs, launch the next one on <tier>" — you apply it at
|
|
17
|
+
# launch; nothing switches a running phase's model.
|
|
18
|
+
#
|
|
19
|
+
# Everything edits .tfcore/routing.yaml in place (comments preserved) and then
|
|
20
|
+
# regenerates the harness bindings automatically via tf-routing-bind.sh — you
|
|
21
|
+
# never have to touch the generated files. Design: docs/Adapter-Design.md §5;
|
|
22
|
+
# owner guide: README §17b / WORKFLOW.html §17b.
|
|
23
|
+
|
|
24
|
+
set +e
|
|
25
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
ROOT="$(bash "$SELF_DIR/tf-harness.sh" root)"
|
|
27
|
+
RY="$ROOT/.tfcore/routing.yaml"
|
|
28
|
+
CMD="${1:-status}"
|
|
29
|
+
|
|
30
|
+
[[ -f "$RY" ]] || { echo "No .tfcore/routing.yaml here ($ROOT) — run update-framework.sh first." >&2; exit 1; }
|
|
31
|
+
command -v python3 >/dev/null 2>&1 || { echo "python3 required" >&2; exit 1; }
|
|
32
|
+
|
|
33
|
+
_bind() { bash "$SELF_DIR/tf-routing-bind.sh" "$ROOT"; }
|
|
34
|
+
|
|
35
|
+
case "$CMD" in
|
|
36
|
+
on)
|
|
37
|
+
python3 - "$RY" <<'PY'
|
|
38
|
+
import sys, re
|
|
39
|
+
p = sys.argv[1]; s = open(p, encoding="utf-8").read()
|
|
40
|
+
s2 = re.sub(r"^enabled:.*$", "enabled: true", s, count=1, flags=re.M)
|
|
41
|
+
open(p, "w", encoding="utf-8", newline="\n").write(s2)
|
|
42
|
+
PY
|
|
43
|
+
echo "routing: ENABLED in $RY"
|
|
44
|
+
_bind
|
|
45
|
+
echo "Check with: bash .tfcore/utils/tf-routing.sh status"
|
|
46
|
+
;;
|
|
47
|
+
off)
|
|
48
|
+
python3 - "$RY" <<'PY'
|
|
49
|
+
import sys, re
|
|
50
|
+
p = sys.argv[1]; s = open(p, encoding="utf-8").read()
|
|
51
|
+
s2 = re.sub(r"^enabled:.*$", "enabled: false", s, count=1, flags=re.M)
|
|
52
|
+
open(p, "w", encoding="utf-8", newline="\n").write(s2)
|
|
53
|
+
PY
|
|
54
|
+
echo "routing: DISABLED in $RY"
|
|
55
|
+
_bind
|
|
56
|
+
;;
|
|
57
|
+
bind)
|
|
58
|
+
_bind
|
|
59
|
+
;;
|
|
60
|
+
set-tier)
|
|
61
|
+
NAME="${2:-}"; TIER="${3:-}"
|
|
62
|
+
case "$TIER" in frontier|standard|economy|inherit) ;; *)
|
|
63
|
+
echo "usage: tf-routing.sh set-tier <phase-or-subagent> <frontier|standard|economy|inherit>" >&2; exit 2 ;;
|
|
64
|
+
esac
|
|
65
|
+
[[ -n "$NAME" ]] || { echo "usage: tf-routing.sh set-tier <phase-or-subagent> <tier>" >&2; exit 2; }
|
|
66
|
+
python3 - "$RY" "$NAME" "$TIER" <<'PY'
|
|
67
|
+
import sys
|
|
68
|
+
p, name, tier = sys.argv[1:4]
|
|
69
|
+
lines = open(p, encoding="utf-8").read().splitlines(True)
|
|
70
|
+
sect = None; done = False
|
|
71
|
+
for i, line in enumerate(lines):
|
|
72
|
+
if line.strip() and not line.startswith(" ") and not line.lstrip().startswith("#"):
|
|
73
|
+
sect = line.split(":")[0].strip()
|
|
74
|
+
elif sect in ("phases", "subagents") and line.strip().startswith(name + ":"):
|
|
75
|
+
lines[i] = " %s: %s\n" % (name, tier)
|
|
76
|
+
done = True
|
|
77
|
+
break
|
|
78
|
+
if not done:
|
|
79
|
+
# unknown name: add it under phases (most common case)
|
|
80
|
+
for i, line in enumerate(lines):
|
|
81
|
+
if line.startswith("phases:"):
|
|
82
|
+
lines.insert(i + 1, " %s: %s\n" % (name, tier))
|
|
83
|
+
done = True
|
|
84
|
+
break
|
|
85
|
+
open(p, "w", encoding="utf-8", newline="\n").write("".join(lines))
|
|
86
|
+
print(("set %s -> %s" % (name, tier)) if done else "FAILED: no phases: section found")
|
|
87
|
+
PY
|
|
88
|
+
_bind
|
|
89
|
+
;;
|
|
90
|
+
set-model)
|
|
91
|
+
TIER="${2:-}"; HARNESS="${3:-}"; MODEL="${4:-}"
|
|
92
|
+
case "$TIER" in frontier|standard|economy) ;; *)
|
|
93
|
+
echo "usage: tf-routing.sh set-model <frontier|standard|economy> <claude|opencode|codex> <model-id>" >&2; exit 2 ;;
|
|
94
|
+
esac
|
|
95
|
+
case "$HARNESS" in claude|opencode|codex) ;; *)
|
|
96
|
+
echo "harness must be claude, opencode, or codex" >&2; exit 2 ;;
|
|
97
|
+
esac
|
|
98
|
+
[[ -n "$MODEL" ]] || { echo "usage: tf-routing.sh set-model <tier> <claude|opencode|codex> <model-id>" >&2; exit 2; }
|
|
99
|
+
python3 - "$RY" "$TIER" "$HARNESS" "$MODEL" <<'PY'
|
|
100
|
+
import sys
|
|
101
|
+
p, tier, harness, model = sys.argv[1:5]
|
|
102
|
+
lines = open(p, encoding="utf-8").read().splitlines(True)
|
|
103
|
+
in_tiers = in_tier = False; done = False
|
|
104
|
+
for i, line in enumerate(lines):
|
|
105
|
+
if line.strip() and not line.startswith(" ") and not line.lstrip().startswith("#"):
|
|
106
|
+
in_tiers = line.startswith("tiers:")
|
|
107
|
+
in_tier = False
|
|
108
|
+
elif in_tiers and line.startswith(" ") and not line.startswith(" ") and line.strip().rstrip(":"):
|
|
109
|
+
in_tier = line.strip().rstrip(":") == tier
|
|
110
|
+
elif in_tiers and in_tier and line.strip().startswith(harness + ":"):
|
|
111
|
+
lines[i] = " %s: %s\n" % (harness, model)
|
|
112
|
+
done = True
|
|
113
|
+
break
|
|
114
|
+
open(p, "w", encoding="utf-8", newline="\n").write("".join(lines))
|
|
115
|
+
print(("set tiers.%s.%s -> %s" % (tier, harness, model)) if done else "FAILED: tier '%s' / key '%s' not found in tiers: map" % (tier, harness))
|
|
116
|
+
PY
|
|
117
|
+
_bind
|
|
118
|
+
;;
|
|
119
|
+
set-escalation)
|
|
120
|
+
PHASE="${2:-}"; ATTEMPTS="${3:-}"; TIER="${4:-}"
|
|
121
|
+
[[ -n "$PHASE" ]] || { echo "usage: tf-routing.sh set-escalation <phase> <attempts> <frontier|standard|economy>" >&2; exit 2; }
|
|
122
|
+
[[ "$ATTEMPTS" =~ ^[1-9][0-9]*$ ]] || { echo "attempts must be a positive integer (e.g. 2)" >&2; exit 2; }
|
|
123
|
+
case "$TIER" in frontier|standard|economy) ;; *)
|
|
124
|
+
echo "usage: tf-routing.sh set-escalation <phase> <attempts> <frontier|standard|economy>" >&2; exit 2 ;;
|
|
125
|
+
esac
|
|
126
|
+
python3 - "$RY" "$PHASE" "$ATTEMPTS" "$TIER" <<'PY'
|
|
127
|
+
import sys
|
|
128
|
+
p, phase, attempts, tier = sys.argv[1:5]
|
|
129
|
+
lines = open(p, encoding="utf-8").read().splitlines(True)
|
|
130
|
+
block = [" %s:\n" % phase, " after_attempts: %s\n" % attempts, " tier: %s\n" % tier]
|
|
131
|
+
sect = None; start = end = None; esc_at = None
|
|
132
|
+
for i, line in enumerate(lines):
|
|
133
|
+
if line.strip() and not line.startswith(" ") and not line.lstrip().startswith("#"):
|
|
134
|
+
if sect == "escalation" and start is not None and end is None:
|
|
135
|
+
end = i
|
|
136
|
+
sect = line.split(":")[0].strip()
|
|
137
|
+
if sect == "escalation":
|
|
138
|
+
esc_at = i
|
|
139
|
+
elif sect == "escalation":
|
|
140
|
+
if line.startswith(" ") and not line.startswith(" ") and line.strip().rstrip(":") == phase:
|
|
141
|
+
start = i
|
|
142
|
+
elif start is not None and end is None and line.startswith(" ") and not line.startswith(" "):
|
|
143
|
+
end = i
|
|
144
|
+
if start is not None:
|
|
145
|
+
# replace the existing <phase> block (its 2-space header + 4-space children)
|
|
146
|
+
if end is None:
|
|
147
|
+
end = len(lines)
|
|
148
|
+
while end > start + 1 and not lines[end - 1].strip():
|
|
149
|
+
end -= 1
|
|
150
|
+
lines[start:end] = block
|
|
151
|
+
what = "updated"
|
|
152
|
+
elif esc_at is not None:
|
|
153
|
+
lines[esc_at + 1:esc_at + 1] = block
|
|
154
|
+
what = "added"
|
|
155
|
+
else:
|
|
156
|
+
if lines and not lines[-1].endswith("\n"):
|
|
157
|
+
lines[-1] += "\n"
|
|
158
|
+
lines += ["\n", "# Advisory escalation — see DECISIONS.md 2026-08-21.\n", "escalation:\n"] + block
|
|
159
|
+
what = "added (new escalation: section)"
|
|
160
|
+
open(p, "w", encoding="utf-8", newline="\n").write("".join(lines))
|
|
161
|
+
print("escalation.%s %s -> after_attempts: %s, tier: %s (advisory — applied at launch, never mid-run)" % (phase, what, attempts, tier))
|
|
162
|
+
PY
|
|
163
|
+
# No _bind: escalation is advisory and generates no harness binding.
|
|
164
|
+
;;
|
|
165
|
+
status)
|
|
166
|
+
python3 - "$RY" "$ROOT" <<'PY'
|
|
167
|
+
import sys, os, re
|
|
168
|
+
p, root = sys.argv[1], sys.argv[2]
|
|
169
|
+
cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}, "escalation": {}}
|
|
170
|
+
sect = tier = None
|
|
171
|
+
for line in open(p, encoding="utf-8"):
|
|
172
|
+
line = line.rstrip("\n")
|
|
173
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
174
|
+
continue
|
|
175
|
+
if not line.startswith(" "):
|
|
176
|
+
key, _, val = line.partition(":")
|
|
177
|
+
sect, tier = key.strip(), None
|
|
178
|
+
if sect == "enabled":
|
|
179
|
+
cfg["enabled"] = val.strip() == "true"
|
|
180
|
+
elif sect in ("tiers", "escalation"):
|
|
181
|
+
if re.match(r"^ [a-z0-9-]+:\s*$", line):
|
|
182
|
+
tier = line.strip()[:-1]; cfg[sect][tier] = {}
|
|
183
|
+
elif tier and line.startswith(" "):
|
|
184
|
+
k, _, v = line.strip().partition(":")
|
|
185
|
+
cfg[sect][tier][k.strip()] = v.strip()
|
|
186
|
+
elif sect in ("phases", "subagents", "effort"):
|
|
187
|
+
k, _, v = line.strip().partition(":")
|
|
188
|
+
cfg[sect][k.strip()] = v.strip()
|
|
189
|
+
|
|
190
|
+
manifest = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
|
|
191
|
+
bound = os.path.isfile(manifest)
|
|
192
|
+
n_art = sum(1 for l in open(manifest)) if bound else 0
|
|
193
|
+
|
|
194
|
+
print("Routing: %s (bindings on disk: %s)" % ("ON" if cfg["enabled"] else "OFF",
|
|
195
|
+
"%d generated artifacts" % n_art if bound else "none"))
|
|
196
|
+
if cfg["enabled"] != bound:
|
|
197
|
+
print(" !! flag and bindings disagree — run: bash .tfcore/utils/tf-routing.sh bind")
|
|
198
|
+
print()
|
|
199
|
+
print("Tier models:")
|
|
200
|
+
for t in ("frontier", "standard", "economy"):
|
|
201
|
+
m = cfg["tiers"].get(t, {})
|
|
202
|
+
print(" %-9s claude: %-10s opencode: %-36s codex: %s" % (t, m.get("claude", "-"), m.get("opencode", "-"), m.get("codex", "-")))
|
|
203
|
+
print()
|
|
204
|
+
print("Phases by tier:")
|
|
205
|
+
for t in ("frontier", "standard", "economy", "inherit"):
|
|
206
|
+
ph = sorted(k for k, v in cfg["phases"].items() if v == t)
|
|
207
|
+
if ph:
|
|
208
|
+
print(" %-9s %s" % (t, ", ".join(ph)))
|
|
209
|
+
sub = ["%s=%s" % (k, v) for k, v in sorted(cfg["subagents"].items())]
|
|
210
|
+
if sub:
|
|
211
|
+
print("Subagents: " + ", ".join(sub))
|
|
212
|
+
print()
|
|
213
|
+
print("Escalation (ADVISORY — you apply it when you launch; nothing switches a model mid-run):")
|
|
214
|
+
if cfg["escalation"]:
|
|
215
|
+
for ph in sorted(cfg["escalation"]):
|
|
216
|
+
e = cfg["escalation"][ph]
|
|
217
|
+
n, t = e.get("after_attempts", "?"), e.get("tier", "?")
|
|
218
|
+
base = cfg["phases"].get(ph, "inherit")
|
|
219
|
+
print(" %-18s after %s attempt(s) on the same REQs -> launch the next on %s (base tier: %s)" % (ph, n, t, base))
|
|
220
|
+
print(" next attempt number: bash .tfcore/utils/tf-emit.sh --next-run-attempt <phase> <REQ-ID>...")
|
|
221
|
+
print(" change: bash .tfcore/utils/tf-routing.sh set-escalation <phase> <attempts> <tier>")
|
|
222
|
+
else:
|
|
223
|
+
print(" none declared — add one: bash .tfcore/utils/tf-routing.sh set-escalation fix-issues 2 frontier")
|
|
224
|
+
print()
|
|
225
|
+
if cfg["enabled"]:
|
|
226
|
+
print("Invoke routed phases as:")
|
|
227
|
+
print(" OpenCode: /techieflow:tasks:<phase> (same commands as always — model now pinned)")
|
|
228
|
+
print(" Claude Code: /tf:<phase> (new wrappers; old commands still work, unrouted)")
|
|
229
|
+
print(" Codex: $techieflow-<phase> (main-thread model inherited; delegated roles are pinned)")
|
|
230
|
+
print()
|
|
231
|
+
print("WHERE YOU SEE IT — opening the TUI looks UNCHANGED on purpose: your normal chat")
|
|
232
|
+
print("(the default 'build' agent) stays on YOUR selected model. Routing becomes visible when:")
|
|
233
|
+
print(" 1. you run a /techieflow:tasks:* command -> the run executes on the tier model (footer)")
|
|
234
|
+
print(" 2. you Tab to a persona (flow-master, flow-verifier, ...) -> each carries its bound model")
|
|
235
|
+
print(" 3. a phase finishes -> docs/metrics/*.jsonl records tier, observed model, routed, cost")
|
|
236
|
+
print("NOTE (verified): in the TUI a routed command's model STAYS for the session afterwards —")
|
|
237
|
+
print("pick your model from the model list (or start a new session) if you keep chatting.")
|
|
238
|
+
else:
|
|
239
|
+
print("Turn on with: bash .tfcore/utils/tf-routing.sh on")
|
|
240
|
+
PY
|
|
241
|
+
;;
|
|
242
|
+
*)
|
|
243
|
+
sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'
|
|
244
|
+
exit 2
|
|
245
|
+
;;
|
|
246
|
+
esac
|
|
247
|
+
exit 0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-yolo.sh — YOLO / goal-mode state for TechieFlow (rule: .tfcore/tasks/_yolo-mode.md)
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-yolo.sh on [--source <who>] [--goal "<text>"] turn YOLO on
|
|
5
|
+
# bash .tfcore/utils/tf-yolo.sh off turn YOLO off
|
|
6
|
+
# bash .tfcore/utils/tf-yolo.sh status print ON/OFF + details (exit 0 always)
|
|
7
|
+
# bash .tfcore/utils/tf-yolo.sh is-on exit 0 if ON, 1 if OFF (for scripts/hooks)
|
|
8
|
+
# bash .tfcore/utils/tf-yolo.sh done [complete|blocked] ["<summary>"] goal finished → write the sentinel
|
|
9
|
+
# the tf-goal.sh supervisor stops on
|
|
10
|
+
# bash .tfcore/utils/tf-yolo.sh clear-done remove the sentinel (supervisor does this)
|
|
11
|
+
#
|
|
12
|
+
# YOLO is ON when ANY of these holds (checked in this order by block-git.sh too):
|
|
13
|
+
# 1. env TF_YOLO=1 (tf-goal.sh exports it for the whole run)
|
|
14
|
+
# 2. .tfcore/.session/yolo.json exists (written by `on`; the agent writes it on *yolo / a goal)
|
|
15
|
+
# 3. the harness itself is in bypass mode (Claude Code permission_mode=bypassPermissions —
|
|
16
|
+
# only the hook can see that; this script cannot)
|
|
17
|
+
#
|
|
18
|
+
# State lives under .tfcore/.session/ — gitignored in the framework repo and, in
|
|
19
|
+
# consumer apps, inside the ignored .tfcore/ block. Nothing here is ever committed.
|
|
20
|
+
#
|
|
21
|
+
# NO VETO: this script never fails a phase. Every path exits 0 except `is-on`,
|
|
22
|
+
# whose non-zero exit is its answer, not an error.
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
ROOT="${CLAUDE_PROJECT_DIR:-${TF_PROJECT_DIR:-}}"
|
|
27
|
+
if [[ -z "$ROOT" ]]; then
|
|
28
|
+
# walk up from cwd to the nearest .tfcore/
|
|
29
|
+
d="$PWD"
|
|
30
|
+
while [[ "$d" != "/" ]]; do
|
|
31
|
+
if [[ -d "$d/.tfcore" ]]; then ROOT="$d"; break; fi
|
|
32
|
+
d="$(dirname "$d")"
|
|
33
|
+
done
|
|
34
|
+
fi
|
|
35
|
+
ROOT="${ROOT:-$PWD}"
|
|
36
|
+
STATE_DIR="$ROOT/.tfcore/.session"
|
|
37
|
+
FLAG="$STATE_DIR/yolo.json"
|
|
38
|
+
DONE="$STATE_DIR/goal-done.json"
|
|
39
|
+
|
|
40
|
+
now_utc() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
41
|
+
|
|
42
|
+
# A flag nobody turned off EXPIRES. `off` and `done` clear it, but a session that
|
|
43
|
+
# is killed, crashes, or simply ends without the sentinel leaves it behind, and an
|
|
44
|
+
# unnoticed flag silently grants unprompted deletes for as long as it sits there
|
|
45
|
+
# (found on five repos, one of them 6 days old, 2026-08-28). Age is taken from the
|
|
46
|
+
# file's mtime via `find -mmin`, which behaves the same on GNU and BSD — parsing
|
|
47
|
+
# the ISO `since` field would need `date -d`, which does not exist on macOS.
|
|
48
|
+
# Override with TF_YOLO_TTL_HOURS; 0 disables expiry. tf-goal.sh is unaffected —
|
|
49
|
+
# it exports TF_YOLO=1, which is checked first and never expires mid-run.
|
|
50
|
+
TTL_HOURS="${TF_YOLO_TTL_HOURS:-24}"
|
|
51
|
+
flag_live() { # exit 0 = flag present AND not expired
|
|
52
|
+
[[ -f "$FLAG" ]] || return 1
|
|
53
|
+
[[ "$TTL_HOURS" == "0" ]] && return 0
|
|
54
|
+
[[ -n "$(find "$FLAG" -mmin "+$(( TTL_HOURS * 60 ))" 2>/dev/null)" ]] && return 1
|
|
55
|
+
return 0
|
|
56
|
+
}
|
|
57
|
+
json_escape() { printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || printf '"%s"' "${1//\"/\\\"}"; }
|
|
58
|
+
|
|
59
|
+
cmd="${1:-status}"; shift || true
|
|
60
|
+
|
|
61
|
+
case "$cmd" in
|
|
62
|
+
on)
|
|
63
|
+
SOURCE="agent"; GOAL=""
|
|
64
|
+
while [[ $# -gt 0 ]]; do
|
|
65
|
+
case "$1" in
|
|
66
|
+
--source) SOURCE="${2:-agent}"; shift 2 ;;
|
|
67
|
+
--goal) GOAL="${2:-}"; shift 2 ;;
|
|
68
|
+
*) shift ;;
|
|
69
|
+
esac
|
|
70
|
+
done
|
|
71
|
+
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
72
|
+
{
|
|
73
|
+
printf '{"on":true,"since":"%s","source":%s,"goal":%s,"harness":%s}\n' \
|
|
74
|
+
"$(now_utc)" "$(json_escape "$SOURCE")" "$(json_escape "$GOAL")" "$(json_escape "${TF_HARNESS:-claude-code}")"
|
|
75
|
+
} > "$FLAG" 2>/dev/null || true
|
|
76
|
+
rm -f "$DONE" 2>/dev/null || true
|
|
77
|
+
echo "YOLO: ON (source=$SOURCE) — no confirmations; deletes + read-only git allowed; git WRITES still blocked; run to completion."
|
|
78
|
+
;;
|
|
79
|
+
off)
|
|
80
|
+
rm -f "$FLAG" 2>/dev/null || true
|
|
81
|
+
echo "YOLO: OFF — back to ask-on-delete, no-git, confirm-at-phase-boundaries."
|
|
82
|
+
;;
|
|
83
|
+
is-on)
|
|
84
|
+
if [[ "${TF_YOLO:-0}" == "1" ]] || flag_live; then exit 0; else exit 1; fi
|
|
85
|
+
;;
|
|
86
|
+
status)
|
|
87
|
+
if [[ "${TF_YOLO:-0}" == "1" ]]; then
|
|
88
|
+
echo "YOLO: ON (env TF_YOLO=1)"
|
|
89
|
+
elif flag_live; then
|
|
90
|
+
echo "YOLO: ON ($(cat "$FLAG" 2>/dev/null))"
|
|
91
|
+
elif [[ -f "$FLAG" ]]; then
|
|
92
|
+
echo "YOLO: OFF (flag EXPIRED after ${TTL_HOURS}h and is ignored — $(cat "$FLAG" 2>/dev/null)). Run 'tf-yolo.sh off' to delete it, or 'on' to start a fresh window."
|
|
93
|
+
else
|
|
94
|
+
echo "YOLO: OFF"
|
|
95
|
+
fi
|
|
96
|
+
if [[ -f "$DONE" ]]; then echo "GOAL-DONE: $(cat "$DONE" 2>/dev/null)"; fi
|
|
97
|
+
;;
|
|
98
|
+
done)
|
|
99
|
+
OUTCOME="${1:-complete}"; SUMMARY="${2:-}"
|
|
100
|
+
case "$OUTCOME" in complete|blocked) ;; *) SUMMARY="$OUTCOME $SUMMARY"; OUTCOME="complete" ;; esac
|
|
101
|
+
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
102
|
+
printf '{"outcome":%s,"summary":%s,"ts":"%s"}\n' \
|
|
103
|
+
"$(json_escape "$OUTCOME")" "$(json_escape "$SUMMARY")" "$(now_utc)" > "$DONE" 2>/dev/null || true
|
|
104
|
+
# The grant ENDS WITH THE GOAL. Until 2026-08-28 `done` left the flag in place,
|
|
105
|
+
# so even the well-behaved path — the one _yolo-mode.md §Completion prescribes as
|
|
106
|
+
# the last action of every run — left the repo permanently in YOLO, with deletes
|
|
107
|
+
# unprompted and read-only git open and nothing on screen saying so. The
|
|
108
|
+
# supervisor reads $DONE, never $FLAG, so clearing it here stops nothing.
|
|
109
|
+
rm -f "$FLAG" 2>/dev/null || true
|
|
110
|
+
echo "GOAL-DONE ($OUTCOME) recorded at $DONE — the supervisor (tf-goal.sh) will stop. YOLO flag cleared."
|
|
111
|
+
;;
|
|
112
|
+
clear-done)
|
|
113
|
+
rm -f "$DONE" 2>/dev/null || true
|
|
114
|
+
;;
|
|
115
|
+
*)
|
|
116
|
+
echo "usage: tf-yolo.sh on|off|status|is-on|done [complete|blocked] [summary]|clear-done" >&2
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
exit 0
|