@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,248 @@
1
+ #!/usr/bin/env python3
2
+ """Translate Codex hooks to TechieFlow's existing guard and telemetry contracts.
3
+
4
+ Modes (see .codex/hooks.json):
5
+ pre-tool Bash -> block-git.sh + guard-artifacts.sh
6
+ Edit/Write/apply_patch -> guard-status.sh + guard-verify.sh
7
+ stop guard-status-html.sh (PROJECT-STATUS.html must not be stale)
8
+ session-start session pointer + sweep-artifacts.sh (expired run material)
9
+ session-start write the .tfcore/.session/codex.json pointer
10
+ session-end pointer + session telemetry
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import pathlib
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ from datetime import datetime, timezone
22
+
23
+
24
+ def read_event() -> dict:
25
+ try:
26
+ value = json.load(sys.stdin)
27
+ return value if isinstance(value, dict) else {}
28
+ except Exception:
29
+ return {}
30
+
31
+
32
+ def project_root(event: dict) -> pathlib.Path:
33
+ start = pathlib.Path(str(event.get("cwd") or os.getcwd())).resolve()
34
+ for candidate in (start, *start.parents):
35
+ if (candidate / ".tfcore").is_dir():
36
+ return candidate
37
+ return start
38
+
39
+
40
+ def environment(root: pathlib.Path, event: dict) -> dict[str, str]:
41
+ env = os.environ.copy()
42
+ env.update(
43
+ TF_HARNESS="codex",
44
+ TF_PROJECT_DIR=str(root),
45
+ CLAUDE_PROJECT_DIR=str(root),
46
+ TF_SESSION_ID=str(event.get("session_id") or ""),
47
+ )
48
+ if (root / ".tfcore" / ".session" / "yolo.json").exists():
49
+ env["TF_YOLO"] = "1"
50
+ return env
51
+
52
+
53
+ def deny(reason: str) -> None:
54
+ print(json.dumps({
55
+ "hookSpecificOutput": {
56
+ "hookEventName": "PreToolUse",
57
+ "permissionDecision": "deny",
58
+ "permissionDecisionReason": reason,
59
+ }
60
+ }))
61
+
62
+
63
+ def run_guard(root: pathlib.Path, event: dict, script: str, payload: dict) -> str | None:
64
+ path = root / ".tfcore" / "hooks" / script
65
+ if not path.exists():
66
+ return None
67
+ try:
68
+ env = environment(root, event)
69
+ if script == "block-git.sh":
70
+ # Codex also has an all-git exec-policy rule. Keep the hook's
71
+ # compound-command fallback equally strict in YOLO mode.
72
+ env["TF_STRICT_GIT"] = "1"
73
+ result = subprocess.run(
74
+ ["bash", str(path)], input=json.dumps(payload), text=True,
75
+ capture_output=True, timeout=10, env=env, check=False,
76
+ )
77
+ except Exception as exc:
78
+ # Policy checks fail closed. Telemetry is handled separately and fails open.
79
+ return f"TechieFlow could not execute {script}: {exc}"
80
+ if result.returncode == 2:
81
+ return result.stderr.strip() or f"Blocked by TechieFlow guard {script}"
82
+ if result.returncode not in (0,):
83
+ return f"TechieFlow guard {script} failed unexpectedly (exit {result.returncode})."
84
+ return None
85
+
86
+
87
+ def pre_tool(event: dict) -> None:
88
+ root = project_root(event)
89
+ tool = str(event.get("tool_name") or "")
90
+ raw = event.get("tool_input")
91
+ args = raw if isinstance(raw, dict) else {}
92
+ scripts: list[str] = []
93
+ if tool == "Bash":
94
+ payload = {"tool_name": "Bash", "tool_input": {"command": str(args.get("command") or "")}}
95
+ scripts = ["block-git.sh", "guard-artifacts.sh"]
96
+ elif tool == "apply_patch":
97
+ patch = str(args.get("command") or args.get("patch") or "")
98
+ # Run the existing guards once per file using actual paths and separated
99
+ # added/removed lines. This avoids treating context or removed Verified
100
+ # cells as newly introduced content.
101
+ headers = list(re.finditer(r"(?m)^\*\*\* (Update|Add|Delete) File: (.+)$", patch))
102
+ for index, header in enumerate(headers):
103
+ end = headers[index + 1].start() if index + 1 < len(headers) else len(patch)
104
+ section = patch[header.end():end]
105
+ file_path = header.group(2).strip()
106
+ base_name = pathlib.PurePosixPath(file_path.replace("\\", "/")).name.lower()
107
+ if header.group(1) == "Delete" and (
108
+ base_name == "project-status.md" or base_name.endswith("-checklist.md")
109
+ ):
110
+ deny(
111
+ "TechieFlow protected documents cannot be deleted: "
112
+ f"{file_path}. Update their canonical fixed shape instead."
113
+ )
114
+ return
115
+ added = []
116
+ removed = []
117
+ for line in section.splitlines():
118
+ if line.startswith("+") and not line.startswith("+++"):
119
+ added.append(line[1:])
120
+ elif line.startswith("-") and not line.startswith("---"):
121
+ removed.append(line[1:])
122
+ payload = {
123
+ "hook_event_name": "PreToolUse", "cwd": str(root),
124
+ "session_id": event.get("session_id"), "tool_name": "Edit",
125
+ "tool_input": {"file_path": file_path,
126
+ "old_string": "\n".join(removed),
127
+ "new_string": "\n".join(added)},
128
+ }
129
+ for script in ("guard-status.sh", "guard-verify.sh"):
130
+ reason = run_guard(root, event, script, payload)
131
+ if reason:
132
+ deny(reason)
133
+ return
134
+ return
135
+ elif tool in ("Edit", "Write"):
136
+ payload = {"tool_name": tool, "tool_input": args}
137
+ scripts = ["guard-status.sh", "guard-verify.sh"]
138
+ else:
139
+ return
140
+ payload.update(hook_event_name="PreToolUse", cwd=str(root), session_id=event.get("session_id"))
141
+ for script in scripts:
142
+ reason = run_guard(root, event, script, payload)
143
+ if reason:
144
+ deny(reason)
145
+ return
146
+
147
+
148
+ def stop(event: dict) -> None:
149
+ """Stop hook: refuse to end the turn while PROJECT-STATUS.html is stale.
150
+
151
+ Same contract as the Claude Code Stop hook (_status-update-gate.md §8):
152
+ guard-status-html.sh exits 2 when the .html is older than the .md or
153
+ missing; `stop_hook_active` is passed through so a turn that genuinely
154
+ cannot render still terminates instead of looping.
155
+ """
156
+ root = project_root(event)
157
+ payload = {
158
+ "hook_event_name": "Stop", "cwd": str(root),
159
+ "session_id": event.get("session_id"),
160
+ "stop_hook_active": bool(event.get("stop_hook_active")),
161
+ }
162
+ reason = run_guard(root, event, "guard-status-html.sh", payload)
163
+ if reason:
164
+ print(json.dumps({"decision": "block", "reason": reason}))
165
+
166
+
167
+ def write_pointer(root: pathlib.Path, event: dict) -> None:
168
+ session_id = str(event.get("session_id") or "")
169
+ if not session_id:
170
+ return
171
+ target = root / ".tfcore" / ".session" / "codex.json"
172
+ try:
173
+ target.parent.mkdir(parents=True, exist_ok=True)
174
+ target.write_text(json.dumps({
175
+ "session_id": session_id,
176
+ "transcript_path": event.get("transcript_path"),
177
+ "model": event.get("model"),
178
+ "ts": datetime.now(timezone.utc).isoformat(),
179
+ }) + "\n", encoding="utf-8")
180
+ except Exception:
181
+ pass
182
+
183
+
184
+ def sweep_artifacts(root: pathlib.Path, event: dict) -> None:
185
+ """SessionStart analogue of the Claude Code sweep-artifacts.sh hook: delete
186
+ run material under tests/.artifacts/ and .verify/ older than the retention
187
+ window and any banned repo-root legacy dir. No veto — never raises."""
188
+ script = root / ".tfcore" / "hooks" / "sweep-artifacts.sh"
189
+ if not script.exists():
190
+ return
191
+ try:
192
+ res = subprocess.run(["bash", str(script)], input=json.dumps(event), text=True,
193
+ capture_output=True, timeout=30, env=environment(root, event), check=False)
194
+ summary = (res.stdout or "").strip()
195
+ if summary:
196
+ print(summary) # SessionStart stdout is surfaced into the session
197
+ except Exception:
198
+ pass
199
+
200
+
201
+ def session_end(root: pathlib.Path, event: dict) -> None:
202
+ # Transcript format is explicitly not a stable Codex hook interface. Record
203
+ # honest session metadata; codex exec --json telemetry is enriched separately.
204
+ emit = root / ".tfcore" / "utils" / "tf-emit.sh"
205
+ if not emit.exists() or not event.get("session_id"):
206
+ return
207
+ record = {
208
+ "kind": "session",
209
+ "session_id": event.get("session_id"),
210
+ "model": event.get("model"),
211
+ "duration_s": None,
212
+ "input_tokens": None,
213
+ "output_tokens": None,
214
+ "cache_read_tokens": None,
215
+ "cache_creation_tokens": None,
216
+ "cost_usd": None,
217
+ "children_sessions": None,
218
+ "harness": "codex",
219
+ }
220
+ try:
221
+ subprocess.run(["bash", str(emit), "sessions"], input=json.dumps(record), text=True,
222
+ capture_output=True, timeout=2, env=environment(root, event), check=False)
223
+ except Exception:
224
+ pass
225
+
226
+
227
+ def main() -> int:
228
+ event = read_event()
229
+ root = project_root(event)
230
+ mode = sys.argv[1] if len(sys.argv) > 1 else ""
231
+ if mode == "pre-tool":
232
+ pre_tool(event)
233
+ elif mode == "stop":
234
+ stop(event)
235
+ elif mode == "session-start":
236
+ write_pointer(root, event)
237
+ # .codex/hooks.json runs session-start for UserPromptSubmit too; sweep
238
+ # only on the real session start so every prompt does not walk the tree.
239
+ if str(event.get("hook_event_name") or "").lower() != "userpromptsubmit":
240
+ sweep_artifacts(root, event)
241
+ elif mode == "session-end":
242
+ write_pointer(root, event)
243
+ session_end(root, event)
244
+ return 0
245
+
246
+
247
+ if __name__ == "__main__":
248
+ raise SystemExit(main())
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — blocks repo-root run-artifact directories.
3
+ #
4
+ # verify-phase.md §1 ARTIFACT-LOCATION RULE: everything a run generates that is
5
+ # not a deliverable lands under tests/.artifacts/. Repo-root siblings —
6
+ # test-results-<slug>/, scripts-<slug>/ — are BANNED: a suffixed name escapes the
7
+ # ignore pattern written for the unsuffixed one, and nothing ever cleans them up.
8
+ #
9
+ # The prose rule has now failed THREE times: fourteen test-results-* dirs in one
10
+ # app, then four scripts-cluster-* dirs in the next fan-out after those were
11
+ # banned, then ten test-results-* dirs in TechieBlog (2026-08-23..25, swept
12
+ # 2026-08-25). Every one came from an agent passing `--output test-results-<slug>`
13
+ # and overriding the config's pinned outputDir. This hook makes the rule
14
+ # MECHANICAL, the same way block-git.sh made the git ban mechanical.
15
+ #
16
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Bash"); Codex via
17
+ # .tfcore/hooks/codex-adapter.py pre-tool; OpenCode via .opencode/plugin/techieflow.js.
18
+ # Exit 2 + stderr = block the call and feed the message back to the agent.
19
+ #
20
+ # What it blocks:
21
+ # - --output / --output-dir pointed at a root-level test-results* or scripts-*
22
+ # (bare -o is deliberately NOT matched — see the comment in the Python body)
23
+ # - mkdir of a root-level test-results* or scripts-* directory
24
+ # What it deliberately ALLOWS:
25
+ # - --output tests/.artifacts/<slug> (the sanctioned isolation form)
26
+ # - anything under tests/, and the project's own tracked scripts/ (no hyphen)
27
+ # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
28
+
29
+ INPUT="$(cat)"
30
+ command -v python3 >/dev/null 2>&1 || exit 0
31
+
32
+ TF_HOOK_INPUT="$INPUT" python3 - <<'PY'
33
+ import json, os, re, sys
34
+
35
+ try:
36
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
37
+ except Exception:
38
+ sys.exit(0)
39
+
40
+ ti = data.get("tool_input") or {}
41
+ cmd = ti.get("command")
42
+ if not isinstance(cmd, str) or not cmd.strip():
43
+ sys.exit(0)
44
+
45
+ errors = []
46
+
47
+ # A root-relative artifact path: optional ./ then test-results… or scripts-…
48
+ # `tests/…` never matches (the 's' in "tests" is followed by '/'), and the
49
+ # project's own `scripts/` never matches (requires the hyphen).
50
+ ROOT_ARTIFACT = r"(?:\./)?(test-results[\w.-]*|scripts-[\w.-]+)/?"
51
+
52
+ # Long form ONLY. A bare `-o` is deliberately NOT matched: Playwright has no -o
53
+ # shorthand for --output, while `grep -o 'test-results-…'` and `curl -o` are
54
+ # legitimate and would false-positive. Cheap specificity beats a clever pattern.
55
+ for m in re.finditer(
56
+ r"--output(?:-dir)?[=\s]+(['\"]?)" + ROOT_ARTIFACT,
57
+ cmd,
58
+ ):
59
+ errors.append(
60
+ f'--output points at repo-root "{m.group(2)}". The single artifact '
61
+ "destination is playwright.config.ts's pinned outputDir "
62
+ "(tests/.artifacts/test-results), which Playwright namespaces per test "
63
+ "AND wipes at the start of every run — that is what keeps the tree from "
64
+ "growing. If a parallel fan-out genuinely needs isolation, the ONLY "
65
+ "permitted override is a subfolder: --output tests/.artifacts/<slug>."
66
+ )
67
+
68
+ for m in re.finditer(
69
+ r"(?<!\w)mkdir(?:\s+-[\w-]+)*\s+(['\"]?)" + ROOT_ARTIFACT,
70
+ cmd,
71
+ ):
72
+ errors.append(
73
+ f'mkdir would create repo-root "{m.group(2)}". Run material — tool output '
74
+ "AND the throwaway scripts you write to drive it — lives under "
75
+ "tests/.artifacts/ (harness scripts: tests/.artifacts/harness/<name>.mjs). "
76
+ "Never a root-level <name>-<slug>/ per run or per cluster."
77
+ )
78
+
79
+ if errors:
80
+ print(
81
+ "BLOCKED by TechieFlow policy: no repo-root run-artifact directory "
82
+ "(verify-phase.md §1 ARTIFACT-LOCATION RULE).",
83
+ file=sys.stderr,
84
+ )
85
+ for e in dict.fromkeys(errors):
86
+ print(" - " + e, file=sys.stderr)
87
+ print(
88
+ "Do NOT reason by the letter of this list: any NEW root-level "
89
+ "<name>-<slug>/ you are about to create for a fan-out is the same defect, "
90
+ "whatever the noun. Re-run writing under tests/.artifacts/ instead. "
91
+ "Note: a script that imports the Playwright LIBRARY does not load "
92
+ "playwright.config.ts at all — it must place its own captures under "
93
+ "tests/.artifacts/ explicitly, resolved relative to the repo root, never "
94
+ "an absolute path.",
95
+ file=sys.stderr,
96
+ )
97
+ sys.exit(2)
98
+
99
+ sys.exit(0)
100
+ PY
101
+ exit $?
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow Stop hook — refuses to end a turn while PROJECT-STATUS.html is stale.
3
+ #
4
+ # _status-update-gate.md §8: "If you edited PROJECT-STATUS.md you re-render
5
+ # PROJECT-STATUS.html in the same turn, full stop." The owner reads the .html; a
6
+ # markdown-only update leaves the page the human actually looks at stale, which
7
+ # defeats the entire purpose of the gate.
8
+ #
9
+ # The prose rule kept failing silently: on 2026-08-25 the owner was reading a
10
+ # PROJECT-STATUS.html rendered 4.5 hours earlier that still listed REQ-FN-062 as
11
+ # blocked and repeated three "owner actions" he had already completed. guard-status.sh
12
+ # already enforces the SHAPE of the markdown; nothing checked that the sibling HTML
13
+ # was current. This closes that gap.
14
+ #
15
+ # Stop (not PostToolUse) is the right trigger because the rule is about the TURN,
16
+ # not the edit — an agent legitimately writes the .md and then renders the .html
17
+ # a few tool calls later. This only complains when the turn is actually ending
18
+ # with the two files out of sync.
19
+ #
20
+ # Wired in .claude/settings.json → hooks.Stop; Codex via .codex/hooks.json Stop →
21
+ # codex-adapter.py stop; OpenCode via .opencode/plugin/techieflow.js session.idle
22
+ # (one-shot nudge — OpenCode has no blocking Stop hook).
23
+ # Exit 2 + stderr = block the stop and feed the message back to the agent.
24
+ # Honours stop_hook_active so a wedged turn can still terminate (no loop).
25
+ # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
26
+
27
+ INPUT="$(cat)"
28
+ command -v python3 >/dev/null 2>&1 || exit 0
29
+
30
+ TF_HOOK_INPUT="$INPUT" TF_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" python3 - <<'PY'
31
+ import json, os, sys
32
+
33
+ try:
34
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
35
+ except Exception:
36
+ sys.exit(0)
37
+
38
+ # Already re-invoked once for this stop — let the turn end rather than loop.
39
+ if data.get("stop_hook_active"):
40
+ sys.exit(0)
41
+
42
+ root = os.environ.get("TF_PROJECT_DIR") or os.getcwd()
43
+ md = os.path.join(root, "PROJECT-STATUS.md")
44
+ html = os.path.join(root, "PROJECT-STATUS.html")
45
+
46
+ if not os.path.isfile(md):
47
+ sys.exit(0)
48
+
49
+ md_mtime = os.path.getmtime(md)
50
+
51
+ if not os.path.isfile(html):
52
+ print(
53
+ "BLOCKED by TechieFlow policy: PROJECT-STATUS.md exists but "
54
+ "PROJECT-STATUS.html does not. The owner reads the HTML "
55
+ "(_status-update-gate.md §8) — render it before ending the turn, using "
56
+ ".tfcore/templates/v4custom/html-render-shell.md. Never hand-roll a "
57
+ "different scaffold.",
58
+ file=sys.stderr,
59
+ )
60
+ sys.exit(2)
61
+
62
+ html_mtime = os.path.getmtime(html)
63
+
64
+ if md_mtime > html_mtime:
65
+ drift = int(md_mtime - html_mtime)
66
+ if drift >= 3600:
67
+ ago = f"{drift // 3600}h {(drift % 3600) // 60}m"
68
+ elif drift >= 60:
69
+ ago = f"{drift // 60}m"
70
+ else:
71
+ ago = f"{drift}s"
72
+ print(
73
+ "BLOCKED by TechieFlow policy: PROJECT-STATUS.html is STALE — "
74
+ f"PROJECT-STATUS.md is {ago} newer (_status-update-gate.md §8).",
75
+ file=sys.stderr,
76
+ )
77
+ print(
78
+ " - The owner reads the .html. A markdown-only update is an INCOMPLETE "
79
+ "update: it leaves the page the human actually looks at showing the "
80
+ "previous run's reality (closed REQs still open, retracted owner-actions "
81
+ "still listed, the wrong next command).",
82
+ file=sys.stderr,
83
+ )
84
+ print(
85
+ " - Re-render PROJECT-STATUS.html from the markdown NOW, in this turn, "
86
+ "using the shared scaffold in "
87
+ ".tfcore/templates/v4custom/html-render-shell.md (§1 slug rule, §2 CSS, "
88
+ "§3 skeleton, §4 anchors, §7 JS). Keep the existing shell — update the "
89
+ "body sections, the subtitle's current_phase/last-updated line, and add "
90
+ "the new Verification-log row. Sidebar TOC iff >6 H2s.",
91
+ file=sys.stderr,
92
+ )
93
+ print(
94
+ " - Then confirm parity: same H2 set and same slugs in both files, and no "
95
+ "string surviving in the HTML that the markdown no longer says.",
96
+ file=sys.stderr,
97
+ )
98
+ sys.exit(2)
99
+
100
+ sys.exit(0)
101
+ PY
102
+ exit $?
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — blocks convoluted PROJECT-STATUS.md writes.
3
+ #
4
+ # PROJECT-STATUS is a CRISP, FIXED-SHAPE snapshot agents OVERWRITE — never an
5
+ # append-log. See .tfcore/tasks/_status-update-gate.md §"CRISP, FIXED-SHAPE
6
+ # snapshot" and .tfcore/templates/v4custom/app-project-status-tmpl.md. The prose
7
+ # rule alone kept failing (AstroLyfe's 286-line mess, repeat offenses after);
8
+ # this hook makes the shape MECHANICAL, the same way block-git.sh made the git
9
+ # ban mechanical.
10
+ #
11
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Write|Edit|MultiEdit").
12
+ # Exit 2 + stderr = block the call and feed the message back to the agent.
13
+ #
14
+ # What it blocks (only for files named PROJECT-STATUS.md):
15
+ # - Any H2 heading outside the template's fixed section set (per-run dated
16
+ # H2s like "## *verify all — coverage matrix (DATE)" are the disease).
17
+ # - Headings that name a command run (*verify / *build-phase / *fix-issues).
18
+ # - A paragraph stuffed into `current_phase:` (must be ONE short line).
19
+ # - A full-file Write longer than 120 lines (template shape is ~60).
20
+ # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
21
+
22
+ INPUT="$(cat)"
23
+ command -v python3 >/dev/null 2>&1 || exit 0
24
+
25
+ # stdin feeds python the SCRIPT (heredoc), so the payload travels via env var.
26
+ TF_HOOK_INPUT="$INPUT" python3 - <<'PY'
27
+ import json, os, re, sys
28
+
29
+ try:
30
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
31
+ except Exception:
32
+ sys.exit(0)
33
+
34
+ ti = data.get("tool_input") or {}
35
+ path = (ti.get("file_path") or "").replace("\\", "/")
36
+ if path.rsplit("/", 1)[-1].lower() != "project-status.md":
37
+ sys.exit(0)
38
+
39
+ # Write carries the whole file in `content`; Edit/MultiEdit carry fragments.
40
+ pieces = []
41
+ is_full_write = False
42
+ if isinstance(ti.get("content"), str):
43
+ pieces.append(ti["content"])
44
+ is_full_write = True
45
+ if isinstance(ti.get("new_string"), str):
46
+ pieces.append(ti["new_string"])
47
+ for e in ti.get("edits") or []:
48
+ if isinstance(e, dict) and isinstance(e.get("new_string"), str):
49
+ pieces.append(e["new_string"])
50
+ new = "\n".join(pieces)
51
+ if not new:
52
+ sys.exit(0)
53
+
54
+ ALLOWED = {
55
+ "where i am",
56
+ "next command to run",
57
+ "open requirements",
58
+ "known blockers",
59
+ "verification log",
60
+ "library feedback summary",
61
+ "standards compliance",
62
+ "deferred / future",
63
+ "recovery note", # written by *refresh-status, may carry a date
64
+ }
65
+
66
+ errors = []
67
+
68
+ for m in re.finditer(r"(?m)^##\s+(.+)$", new):
69
+ heading = m.group(1).strip()
70
+ h = heading.rstrip(":").lower()
71
+ base = re.sub(r"\s*[—\-(].*$", "", h).strip() # drop trailing dates/qualifiers
72
+ if h in ALLOWED or base in ALLOWED:
73
+ continue
74
+ errors.append(
75
+ f'H2 "## {heading}" is not a template section. PROJECT-STATUS has EXACTLY '
76
+ "the fixed sections of app-project-status-tmpl.md — a run NEVER adds a new "
77
+ "H2; it overwrites the existing sections + adds ONE Verification-log row."
78
+ )
79
+
80
+ if re.search(r"(?m)^#{2,}\s.*\*(verify|build-phase|fix-issues|refresh-status)", new):
81
+ errors.append(
82
+ "A heading naming a command run (*verify / *build-phase / *fix-issues) is "
83
+ "per-run narrative. Record the run as ONE Verification-log table row; "
84
+ "per-REQ detail goes in the checklist Remarks cells."
85
+ )
86
+
87
+ m = re.search(r"(?m)^current_phase:\s*(.+)$", new)
88
+ if m and len(m.group(1).strip()) > 120:
89
+ errors.append(
90
+ "current_phase must be ONE short line (phase name + at most a half-line "
91
+ "qualifier), never a paragraph of run detail."
92
+ )
93
+
94
+ if is_full_write:
95
+ n_lines = new.count("\n") + 1
96
+ if n_lines > 120:
97
+ errors.append(
98
+ f"This write is {n_lines} lines. PROJECT-STATUS is a one-page snapshot "
99
+ "(~60 lines, hard cap 120). TRIM to the template shape — blow-by-blow "
100
+ "history lives in the checklist Remarks and .verify/ artifacts, never here."
101
+ )
102
+
103
+ if errors:
104
+ print(
105
+ "BLOCKED by TechieFlow policy: PROJECT-STATUS is a CRISP, FIXED-SHAPE "
106
+ "snapshot you OVERWRITE in place — never append to, never restructure "
107
+ "(.tfcore/tasks/_status-update-gate.md).",
108
+ file=sys.stderr,
109
+ )
110
+ for e in errors:
111
+ print(" - " + e, file=sys.stderr)
112
+ print(
113
+ "Fix and retry: keep ONLY the template sections (Where I am / Next command "
114
+ "to run / Open requirements / Known blockers / Verification log / Library "
115
+ "feedback summary / Standards compliance / Deferred / future), overwrite "
116
+ "their content in place, add ONE Verification-log row for this run, and put "
117
+ "the detail in docs/<APP>-Checklist.md Remarks. Do NOT pad, do NOT narrate.",
118
+ file=sys.stderr,
119
+ )
120
+ sys.exit(2)
121
+
122
+ sys.exit(0)
123
+ PY
124
+ exit $?
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — blocks self-attested `Verified` verdicts.
3
+ #
4
+ # `Verified` is the verifier's verdict, written ONLY by an EXECUTED verify-phase
5
+ # run (chained inline by build-phase §6b, or a standalone `*verify`). On
6
+ # 2026-07-09 a build orchestrator did its own smoke and wrote `Verified`
7
+ # verdicts itself — the prose rule ("chain the verifier") was skipped silently.
8
+ # This hook makes the gate MECHANICAL, the same way block-git.sh made the git
9
+ # ban mechanical and guard-status.sh made the status shape mechanical.
10
+ #
11
+ # How it works: verify-phase §6 writes a run ledger `docs/.last-verify.json`
12
+ # ({"date","app","scope","booted","gates","evidence"}) immediately before it
13
+ # records verdicts — after actually booting the app and running the gates.
14
+ # Any Write/Edit/MultiEdit to a `*-Checklist.md` that INTRODUCES a `Verified`
15
+ # status cell is blocked unless a same-day ledger sits next to the checklist.
16
+ #
17
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Write|Edit|MultiEdit").
18
+ # Exit 2 + stderr = block the call and feed the message back to the agent.
19
+ # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
20
+
21
+ INPUT="$(cat)"
22
+ command -v python3 >/dev/null 2>&1 || exit 0
23
+
24
+ # stdin feeds python the SCRIPT (heredoc), so the payload travels via env var.
25
+ TF_HOOK_INPUT="$INPUT" python3 - <<'PY'
26
+ import datetime, json, os, re, sys
27
+
28
+ try:
29
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
30
+ except Exception:
31
+ sys.exit(0)
32
+
33
+ ti = data.get("tool_input") or {}
34
+ path = (ti.get("file_path") or "").replace("\\", "/")
35
+ if not path.rsplit("/", 1)[-1].lower().endswith("-checklist.md"):
36
+ sys.exit(0)
37
+
38
+ # A `Verified` STATUS CELL: a table cell holding exactly Verified (optional
39
+ # bold / trailing date qualifier). Remarks prose like "VERIFIED 2026-07-07"
40
+ # or "Needs re-verify" does not match.
41
+ CELL = re.compile(r"\|\s*\**Verified\**\s*(?:\([^)|]*\))?\s*\|")
42
+
43
+ new_pieces, old_pieces = [], []
44
+ if isinstance(ti.get("content"), str):
45
+ new_pieces.append(ti["content"])
46
+ if isinstance(ti.get("new_string"), str):
47
+ new_pieces.append(ti["new_string"])
48
+ if isinstance(ti.get("old_string"), str):
49
+ old_pieces.append(ti["old_string"])
50
+ for e in ti.get("edits") or []:
51
+ if isinstance(e, dict):
52
+ if isinstance(e.get("new_string"), str):
53
+ new_pieces.append(e["new_string"])
54
+ if isinstance(e.get("old_string"), str):
55
+ old_pieces.append(e["old_string"])
56
+
57
+ n_new = sum(len(CELL.findall(p)) for p in new_pieces)
58
+ n_old = sum(len(CELL.findall(p)) for p in old_pieces)
59
+ if n_new <= n_old:
60
+ sys.exit(0) # no NEW Verified cell introduced (demotions, moves, other edits)
61
+
62
+ # Introducing `Verified` — require a same-day run ledger next to the checklist.
63
+ ledger_path = os.path.join(os.path.dirname(path), ".last-verify.json")
64
+ try:
65
+ with open(ledger_path, "r", encoding="utf-8") as f:
66
+ ledger = json.load(f)
67
+ if str(ledger.get("date", "")).strip() == datetime.date.today().isoformat():
68
+ sys.exit(0) # fresh ledger — an executed verify-phase (or documented reconcile) unlocked it
69
+ stale = f"exists but is dated {ledger.get('date')!r}, not today"
70
+ except FileNotFoundError:
71
+ stale = "does not exist"
72
+ except Exception:
73
+ stale = "exists but is not valid JSON"
74
+
75
+ print(
76
+ "BLOCKED by TechieFlow policy: this write INTRODUCES a `Verified` status, "
77
+ f"but the verify run ledger {ledger_path} {stale}.",
78
+ file=sys.stderr,
79
+ )
80
+ print(
81
+ " - `Verified` is the VERIFIER'S verdict. It is written only downstream of an "
82
+ "EXECUTED .tfcore/tasks/verify-phase.md run — boot the app, run the scoped "
83
+ "tests, apply the §4a data-render + §4b visual-truth gates, write the run "
84
+ "ledger (verify-phase §6), THEN record verdicts.",
85
+ file=sys.stderr,
86
+ )
87
+ print(
88
+ " - If you are the build orchestrator after your self-smoke: your ceiling is "
89
+ "`Implemented` (build-phase §6a / _smoke-test-policy). Chain the verifier by "
90
+ "EXECUTING verify-phase inline (build-phase §6b) — summarizing your smoke as "
91
+ "a verdict is self-attestation, the exact failure this hook exists to stop.",
92
+ file=sys.stderr,
93
+ )
94
+ print(
95
+ " - If you are *refresh-status* reconciling a status column to PRE-EXISTING "
96
+ "dated VERIFIED evidence already in that row's Remarks: write the ledger with "
97
+ '{"date":"<today>","mode":"reconcile","evidence":"<the pre-existing remark '
98
+ 'date>"} first (refresh-status §4).',
99
+ file=sys.stderr,
100
+ )
101
+ print(
102
+ " - Writing the ledger WITHOUT having run verify-phase's steps is falsifying "
103
+ "the audit record.",
104
+ file=sys.stderr,
105
+ )
106
+ sys.exit(2)
107
+ PY
108
+ exit $?