@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,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