@techierathore/techieflow 1.0.2 → 1.0.4

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 (204) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -16
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
  7. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  8. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  9. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
  11. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  12. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  13. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  14. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  17. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  18. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  19. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  20. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  21. package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
  22. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
  23. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  24. package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
  25. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  26. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  27. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  28. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  29. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  30. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  31. package/.opencode/plugin/techieflow.js +40 -13
  32. package/.tfcore/agents/analyst.md +5 -13
  33. package/.tfcore/agents/architect.md +1 -16
  34. package/.tfcore/agents/flow-master.md +42 -215
  35. package/.tfcore/agents/verifier.md +24 -64
  36. package/.tfcore/core-config.yaml +10 -0
  37. package/.tfcore/hooks/block-git.sh +4 -4
  38. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  39. package/.tfcore/hooks/guard-build.sh +76 -0
  40. package/.tfcore/hooks/guard-db.sh +90 -0
  41. package/.tfcore/hooks/guard-metrics.sh +68 -0
  42. package/.tfcore/hooks/guard-status-html.sh +116 -71
  43. package/.tfcore/hooks/guard-status.sh +43 -16
  44. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  45. package/.tfcore/hooks/guard-verify.sh +70 -48
  46. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  47. package/.tfcore/install-manifest.yaml +0 -18
  48. package/.tfcore/routing.yaml +86 -14
  49. package/.tfcore/standards/coding-standards-core.md +61 -0
  50. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  51. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  52. package/.tfcore/tasks/_owner-language.md +26 -0
  53. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  54. package/.tfcore/tasks/_status-update-gate.md +10 -74
  55. package/.tfcore/tasks/_yolo-mode.md +20 -58
  56. package/.tfcore/tasks/amend-docs.md +21 -143
  57. package/.tfcore/tasks/build-phase.md +14 -224
  58. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  59. package/.tfcore/tasks/create-doc.md +4 -0
  60. package/.tfcore/tasks/create-project-brief.md +22 -0
  61. package/.tfcore/tasks/day1-brownfield.md +31 -505
  62. package/.tfcore/tasks/day1-greenfield.md +28 -213
  63. package/.tfcore/tasks/deploy-checklist.md +19 -0
  64. package/.tfcore/tasks/devguide.md +10 -219
  65. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  66. package/.tfcore/tasks/fix-issues.md +10 -129
  67. package/.tfcore/tasks/generate-html.md +5 -1
  68. package/.tfcore/tasks/handoff-phase.md +18 -16
  69. package/.tfcore/tasks/log-miss.md +19 -158
  70. package/.tfcore/tasks/metrics-report.md +3 -1
  71. package/.tfcore/tasks/mockups.md +12 -117
  72. package/.tfcore/tasks/refresh-status.md +9 -130
  73. package/.tfcore/tasks/split-brd.md +10 -126
  74. package/.tfcore/tasks/triage-and-fix.md +14 -0
  75. package/.tfcore/tasks/triage-issues.md +11 -152
  76. package/.tfcore/tasks/verify-phase.md +36 -572
  77. package/.tfcore/telemetry/SCHEMA.md +100 -8
  78. package/.tfcore/telemetry/tf-metrics.sh +564 -32
  79. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  80. package/.tfcore/templates/stack-questions.md +77 -0
  81. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  82. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  83. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  84. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  85. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  86. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  87. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  88. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  89. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  90. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  91. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  92. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  93. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  94. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  95. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  96. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  97. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  98. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  99. package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
  100. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  101. package/.tfcore/user-guide.md +0 -23
  102. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  107. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  108. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  109. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  110. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  111. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  112. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  113. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  114. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  115. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  116. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  117. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  118. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  119. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  120. package/.tfcore/utils/tf-brd-status.py +176 -0
  121. package/.tfcore/utils/tf-brd-status.sh +19 -0
  122. package/.tfcore/utils/tf-build-list.py +211 -0
  123. package/.tfcore/utils/tf-build-list.sh +21 -0
  124. package/.tfcore/utils/tf-build.sh +143 -0
  125. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  126. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  127. package/.tfcore/utils/tf-day1-files.py +208 -0
  128. package/.tfcore/utils/tf-day1-files.sh +20 -0
  129. package/.tfcore/utils/tf-devguide-list.py +249 -0
  130. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  131. package/.tfcore/utils/tf-doc-check.py +1369 -0
  132. package/.tfcore/utils/tf-doc-check.sh +29 -0
  133. package/.tfcore/utils/tf-emit.sh +545 -18
  134. package/.tfcore/utils/tf-fix-close.sh +115 -0
  135. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  136. package/.tfcore/utils/tf-goal.sh +284 -66
  137. package/.tfcore/utils/tf-harness.sh +4 -9
  138. package/.tfcore/utils/tf-log-miss.py +197 -0
  139. package/.tfcore/utils/tf-log-miss.sh +14 -0
  140. package/.tfcore/utils/tf-misses-md.py +208 -0
  141. package/.tfcore/utils/tf-misses-md.sh +14 -0
  142. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  143. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  144. package/.tfcore/utils/tf-model-pick.sh +475 -0
  145. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  146. package/.tfcore/utils/tf-perf-grade.py +97 -0
  147. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  148. package/.tfcore/utils/tf-phase.sh +92 -0
  149. package/.tfcore/utils/tf-render-html.py +20 -0
  150. package/.tfcore/utils/tf-routing-bind.sh +97 -6
  151. package/.tfcore/utils/tf-routing.sh +13 -7
  152. package/.tfcore/utils/tf-selfcheck.sh +323 -0
  153. package/.tfcore/utils/tf-split-brd.py +309 -0
  154. package/.tfcore/utils/tf-split-brd.sh +22 -0
  155. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  156. package/.tfcore/utils/tf-status-facts.py +358 -0
  157. package/.tfcore/utils/tf-status-facts.sh +20 -0
  158. package/.tfcore/utils/tf-triage.py +243 -0
  159. package/.tfcore/utils/tf-triage.sh +16 -0
  160. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  161. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  162. package/.tfcore/utils/tf-verify-env.sh +88 -0
  163. package/.tfcore/utils/tf-verify-list.py +270 -0
  164. package/.tfcore/utils/tf-verify-list.sh +13 -0
  165. package/.tfcore/utils/tf-verify-screens.mjs +355 -0
  166. package/.tfcore/utils/tf-verify-screens.sh +31 -0
  167. package/.tfcore/utils/tf-verify-tests.sh +166 -0
  168. package/.tfcore/utils/tf-verify-verdict.py +292 -0
  169. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  170. package/.tfcore/utils/tf-yolo.sh +59 -0
  171. package/README.md +120 -1145
  172. package/docs/TechieFlow-Installation.md +1 -5
  173. package/opencode.jsonc +37 -33
  174. package/package.json +1 -5
  175. package/scripts/install.mjs +56 -46
  176. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  177. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  178. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  179. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  180. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  181. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  182. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  183. package/.codex/config.toml +0 -46
  184. package/.codex/hooks.json +0 -63
  185. package/.codex/rules/techieflow.rules +0 -8
  186. package/.tfcore/hooks/codex-adapter.py +0 -248
  187. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  188. package/.tfcore/tasks/author-brd.md +0 -79
  189. package/.tfcore/tasks/document-project.md +0 -343
  190. package/.tfcore/tasks/execute-checklist.md +0 -86
  191. package/.tfcore/tasks/index-docs.md +0 -173
  192. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  193. package/.tfcore/tasks/shard-doc.md +0 -185
  194. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  195. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  196. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  197. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  198. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  199. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  200. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  201. package/.tfcore/utils/techieflow-doc-template.md +0 -325
  202. package/.tfcore/utils/tf-codex-bind.py +0 -196
  203. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  204. package/WORKFLOW.html +0 -1986
@@ -18,6 +18,16 @@ architecture:
18
18
  architectureSharded: false
19
19
  architectureShardedLocation: docs/architecture
20
20
  customTechnicalDocuments: null
21
+ # appSize / appKind: set by the day-1 tasks (reset Session 3, 2026-09-04). Size is Small (up to 10
22
+ # screens, one role, 50 requirements), Medium (up to 20 screens, 100 requirements) or Large (split
23
+ # into phases). Kind is app or library. tf-doc-check.sh reads them when a document header lacks them.
24
+ appSize: null
25
+ appKind: null
26
+ # appPhase: the phase being worked on a Large project (reset Sitting 4b, 2026-09-05). 1 means the
27
+ # plain file names (docs/{AppName}-BRD.md, -Checklist, -UIDesign, -DevGuide); 2 onward means
28
+ # docs/{AppName}-P2-BRD.md and so on. Small and Medium projects leave it at 1. Set with
29
+ # tf-day1-files.sh {AppName} --phase N; handoff moves it on when a phase is all Verified.
30
+ appPhase: 1
21
31
  # devLoadAlwaysFiles: the day-1 tasks rewrite this list with the project's concrete paths
22
32
  # (docs/{AppName}-Coding-Standards.md + docs/{AppName}-Architecture.md). Empty until then —
23
33
  # the stock paths (docs/architecture/coding-standards.md etc.) never exist in this flow.
@@ -7,7 +7,7 @@
7
7
  #
8
8
  # 1. GIT GUARD. Git is MANUAL in TechieFlow — agents never COMMIT, PUSH, STAGE,
9
9
  # RESET, CHECKOUT or otherwise WRITE to git, ever (.tfcore/tasks/_smoke-test-policy.md
10
- # §"Git is manual", _status-update-gate.md §"Never run git"). Outside YOLO,
10
+ # owner rule; this hook is the only place it is stated). Outside YOLO,
11
11
  # agents never READ git either (status/log/diff/blame) — status evidence is the
12
12
  # checklist table + working tree + a fresh build. In YOLO mode (below) READ-ONLY
13
13
  # git/gh is allowed; WRITES stay blocked in every mode. settings.json's deny
@@ -55,7 +55,7 @@ block_git_msg() {
55
55
  cat >&2 <<'MSG'
56
56
  BLOCKED by TechieFlow policy: agents NEVER WRITE to git or gh — no commit / push / add / reset / checkout / stash / tag / merge / rebase / branch edits / gh pr|issue create|merge|close. Git is manual, owner-only, in EVERY mode including YOLO.
57
57
  Do NOT retry with another git/gh form. Instead:
58
- - Status updates / "what changed": read the checklist Requirements Status table + the working-tree files (ls -lt, find -newer, Read/Glob/Grep) + a fresh dotnet build — never commit history (.tfcore/tasks/_status-update-gate.md).
58
+ - Status updates / "what changed": bash .tfcore/utils/tf-status-facts.sh <App> prints the facts from the checklist; the working-tree files (ls -lt, find -newer, Read) are the as-built code — never commit history.
59
59
  - Investigating code: read the files on disk at file:line — the working tree IS the as-built code.
60
60
  - Committing/tagging: never yours. Record REQ IDs in the checklist Remarks, not in commits. The owner commits manually.
61
61
  - If your command merely CONTAINS the word git (e.g. writing a doc, grepping for the string): use the Write/Edit/Grep tools instead of bash — that is the standing tool-preference rule anyway.
@@ -64,7 +64,7 @@ MSG
64
64
  block_read_msg() {
65
65
  if [[ "$STRICT_GIT" == "1" ]]; then
66
66
  cat >&2 <<'MSG'
67
- BLOCKED by TechieFlow Codex policy: agents do not run any git or gh command, including read-only status/log/diff/blame, in any mode.
67
+ BLOCKED by TechieFlow policy: agents do not run any git or gh command, including read-only status/log/diff/blame, in any mode.
68
68
  Use the checklist Requirements Status table, working-tree files, filesystem metadata, and fresh build/test evidence instead. The owner performs version-control operations manually.
69
69
  MSG
70
70
  return
@@ -72,7 +72,7 @@ MSG
72
72
  cat >&2 <<'MSG'
73
73
  BLOCKED by TechieFlow policy: agents do not READ git outside YOLO mode (no status/log/diff/show/blame/grep) — git is manual, owner-only.
74
74
  Do NOT retry with another git/gh form. Instead:
75
- - Status updates / "what changed": read the checklist Requirements Status table + the working-tree files (ls -lt, find -newer, Read/Glob/Grep) + a fresh dotnet build — never commit history (.tfcore/tasks/_status-update-gate.md).
75
+ - Status updates / "what changed": bash .tfcore/utils/tf-status-facts.sh <App> prints the facts from the checklist; the working-tree files (ls -lt, find -newer, Read) are the as-built code — never commit history.
76
76
  - Investigating code: read the files on disk at file:line — the working tree IS the as-built code.
77
77
  - Read-only git (status/log/diff/blame) becomes available only in YOLO / goal mode (`*yolo` → bash .tfcore/utils/tf-yolo.sh on). Git WRITES are never available.
78
78
  MSG
@@ -13,8 +13,8 @@
13
13
  # and overriding the config's pinned outputDir. This hook makes the rule
14
14
  # MECHANICAL, the same way block-git.sh made the git ban mechanical.
15
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.
16
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Bash");
17
+ # OpenCode via .opencode/plugin/techieflow.js.
18
18
  # Exit 2 + stderr = block the call and feed the message back to the agent.
19
19
  #
20
20
  # What it blocks:
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — in YOLO mode a build, test or app run stays in the FOREGROUND.
3
+ #
4
+ # Why a hook (owner, 2026-09-06; FR-46): twice in one build (MISS-TechieFlow-20260905-23 and
5
+ # MISS-TechieFlow-20260906-05) the agent started the build as a background job and ended its
6
+ # turn "waiting for the build". The turn ending kills the job; the supervisor only re-prompts;
7
+ # three cycles were lost that way. A rule ignored twice becomes a hook, not a third paragraph.
8
+ #
9
+ # What it refuses, only while YOLO is on (TF_YOLO=1, the yolo.json flag, or a harness in
10
+ # bypass / --auto mode):
11
+ # - a Bash call with run_in_background true whose command builds, tests, runs or publishes:
12
+ # tf-build.sh, dotnet build|test|run|publish|watch, msbuild, npm|pnpm|yarn build|test|start,
13
+ # also through winrun / cmd.exe / powershell.exe
14
+ # - the same commands backgrounded inside the command text: nohup, setsid, disown, or a
15
+ # trailing / inline `&` (not `&&`)
16
+ # Everything else passes. Outside YOLO nothing changes (the owner may background what they like).
17
+ #
18
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Bash"); OpenCode via
19
+ # .opencode/plugin/techieflow.js. Exit 2 + stderr = block the call and tell the agent why.
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
+ ROOT="${CLAUDE_PROJECT_DIR:-${TF_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
25
+
26
+ yolo=0
27
+ if [[ "${TF_YOLO:-0}" == "1" ]]; then yolo=1
28
+ elif [[ -f "$ROOT/.tfcore/utils/tf-yolo.sh" ]] && CLAUDE_PROJECT_DIR="$ROOT" bash "$ROOT/.tfcore/utils/tf-yolo.sh" is-on >/dev/null 2>&1; then yolo=1
29
+ fi
30
+
31
+ TF_HOOK_INPUT="$INPUT" TF_YOLO_ON="$yolo" python3 - <<'PY'
32
+ import json, os, re, sys
33
+
34
+ try:
35
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
36
+ except Exception:
37
+ sys.exit(0)
38
+ ti = data.get("tool_input") or {}
39
+ cmd = ti.get("command")
40
+ if not isinstance(cmd, str) or not cmd.strip():
41
+ sys.exit(0)
42
+
43
+ yolo = os.environ.get("TF_YOLO_ON") == "1" or str(data.get("permission_mode", "")) in ("bypassPermissions", "auto")
44
+ if not yolo:
45
+ sys.exit(0)
46
+
47
+ BUILD = (r"\btf-build\.sh\b"
48
+ r"|\bdotnet\s+(build|test|run|publish|watch)\b"
49
+ r"|\bmsbuild\b"
50
+ r"|\b(npm|pnpm|yarn)\s+(run\s+)?(build|test|start|dev)\b"
51
+ # the verify tooling (Sitting 4c, MISS-TechieFlow-20260906-22): a backgrounded browser
52
+ # test run ended a verify turn "waiting for the tests" on 2026-09-06
53
+ r"|\b(npx\s+)?playwright\s+test\b"
54
+ r"|\btf-verify-(tests|screens|boot|env)\.sh\b"
55
+ r"|\btf-(assets|mockup-parity|perf)\.sh\b"
56
+ r"|\bpytest\b")
57
+ low = cmd.lower()
58
+ if not re.search(BUILD, low):
59
+ sys.exit(0)
60
+
61
+ bg_flag = bool(ti.get("run_in_background"))
62
+ # `&` that is not `&&` and not inside `2>&1` / `>&2`
63
+ inline_bg = bool(re.search(r"(?<![&>\d])&(?![&\d])", low)) or bool(re.search(r"\b(nohup|setsid|disown)\b", low))
64
+ if not (bg_flag or inline_bg):
65
+ sys.exit(0)
66
+
67
+ how = "run_in_background" if bg_flag else "nohup / setsid / a trailing &"
68
+ print("BLOCKED by TechieFlow policy: in YOLO mode a build, test or app run is NEVER started in "
69
+ f"the background ({how} here). Ending the turn to wait for it kills the job, and nobody "
70
+ "wakes you; three build cycles were lost that way on 2026-09-06. Run it in the "
71
+ "foreground and wait: bash .tfcore/utils/tf-build.sh [build|test], npx playwright test, or the "
72
+ "tf-verify-*.sh scripts (a timeout of ten minutes is fine), then act on the result line. Owner rule 2026-09-06 (FR-46).",
73
+ file=sys.stderr)
74
+ sys.exit(2)
75
+ PY
76
+ exit $?
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — database writes happen only from build-phase or fix-issues,
3
+ # and only through the migration path the Stack decisions name (owner, 2026-09-05;
4
+ # MISS-TechieFlow-20260905-09: a day-1 run created a user and patched two stored
5
+ # procedures in the development database).
6
+ #
7
+ # What it refuses, in every command:
8
+ # - a direct SQL write through a client: psql, pgcli, sqlite3, mysql, mariadb, sqlcmd,
9
+ # mongosh with INSERT / UPDATE / DELETE / CREATE / ALTER / DROP / TRUNCATE / GRANT /
10
+ # REPLACE / MERGE in the command text, a -f / -i script, or stdin redirected from a file
11
+ # - dropping or resetting a database
12
+ # What it allows only while .tfcore/.session/phase.json says build-phase or fix-issues
13
+ # (written by bash .tfcore/utils/tf-phase.sh start <command>, step 0 of every task):
14
+ # - a migration runner: dotnet ef database update, dotnet run --project <…Db|…Migration(s)…>,
15
+ # dbup, flyway migrate, liquibase update, alembic upgrade, prisma migrate, knex migrate,
16
+ # rails db:migrate, sequelize db:migrate
17
+ # Reads (SELECT, \d, .tables, dumps to stdout) pass. YOLO does not relax this.
18
+ #
19
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Bash"); OpenCode via
20
+ # .opencode/plugin/techieflow.js. Exit 2 + stderr = block the call and tell the agent why.
21
+ # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
22
+
23
+ INPUT="$(cat)"
24
+ command -v python3 >/dev/null 2>&1 || exit 0
25
+ ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
26
+
27
+ TF_HOOK_INPUT="$INPUT" TF_ROOT="$ROOT" python3 - <<'PY'
28
+ import json, os, re, sys, time
29
+
30
+ try:
31
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
32
+ except Exception:
33
+ sys.exit(0)
34
+ cmd = (data.get("tool_input") or {}).get("command")
35
+ if not isinstance(cmd, str) or not cmd.strip():
36
+ sys.exit(0)
37
+
38
+ WRITE_VERBS = r"\b(insert|update|delete|create|alter|drop|truncate|grant|revoke|replace|merge|upsert)\b"
39
+ CLIENTS = r"\b(psql|pgcli|sqlite3|mysql|mariadb|sqlcmd|mongosh|mongo)\b"
40
+ MIGRATORS = (r"\bdotnet\s+ef\s+database\s+(update|drop)\b"
41
+ r"|\bdotnet\s+run\b[^|;&]*--project\s+\S*(db|migration|migrations)\b"
42
+ r"|\bdbup\b|\bflyway\s+(migrate|clean|undo)\b|\bliquibase\s+(update|rollback|drop-all)\b"
43
+ r"|\balembic\s+(upgrade|downgrade)\b|\bprisma\s+(migrate|db\s+push)\b|\bknex\s+migrate\b"
44
+ r"|\brails\s+db:(migrate|reset|drop|schema:load)\b|\bsequelize\s+db:migrate\b|\bgoose\s+(up|down|reset)\b")
45
+ DROP_DB = r"\b(dropdb|drop\s+database|mongo(sh)?\b[^|;&]*dropDatabase)\b"
46
+
47
+ def phase():
48
+ p = os.path.join(os.environ.get("TF_ROOT", "."), ".tfcore", ".session", "phase.json")
49
+ try:
50
+ if time.time() - os.path.getmtime(p) > 24 * 3600:
51
+ return None
52
+ with open(p, encoding="utf-8") as fh:
53
+ return (json.load(fh) or {}).get("cmd")
54
+ except Exception:
55
+ return None
56
+
57
+ # Words inside an echo/printf string or a comment line are not commands: an echo naming the
58
+ # migration tool refused a verify run's read on 2026-09-06 (MISS-TechieFlow-20260906-20).
59
+ # Strip them before matching, so only a command that runs a migrator or writes through a
60
+ # client is refused.
61
+ low = cmd.lower()
62
+ low = re.sub(r"(?m)^\s*#.*$", "", low)
63
+ low = re.sub(r"\b(echo|printf)\s+(\"[^\"]*\"|'[^']*'|[^\n;|&]*)", r"\1", low)
64
+ direct_write = (re.search(CLIENTS, low)
65
+ and (re.search(WRITE_VERBS, low)
66
+ or re.search(r"\s-(f|i)\s+\S+\.sql\b", low)
67
+ or re.search(r"<\s*\S+\.sql\b", low)))
68
+ if direct_write or re.search(DROP_DB, low):
69
+ print("BLOCKED by TechieFlow policy: a direct database write (SQL through a client, or dropping "
70
+ "a database) is not allowed from any command. Schema and data changes go through the "
71
+ "migration path named in the Architecture's Stack decisions (for the .NET answer set, the "
72
+ "<App>Db DbUp project), run from *build-phase or *fix-issues. A read (SELECT, \\d, .tables) "
73
+ "is fine. Owner rule 2026-09-05.", file=sys.stderr)
74
+ sys.exit(2)
75
+
76
+ if re.search(MIGRATORS, low):
77
+ ph = phase()
78
+ if ph in ("build-phase", "fix-issues"):
79
+ sys.exit(0)
80
+ who = f"the marker says {ph}" if ph else "no command marker is set"
81
+ print("BLOCKED by TechieFlow policy: a database migration runs only from *build-phase or "
82
+ f"*fix-issues, and {who}. Day-1, mockups, split-brd, amend-docs, devguide, verify, "
83
+ "triage and status commands write documents and records, never the database. If you are "
84
+ "inside build-phase or fix-issues, step 0 was skipped: run "
85
+ "bash .tfcore/utils/tf-phase.sh start <command> <App> and retry. Owner rule 2026-09-05.",
86
+ file=sys.stderr)
87
+ sys.exit(2)
88
+ sys.exit(0)
89
+ PY
90
+ exit $?
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow PreToolUse hook — the telemetry streams are written ONLY by tf-emit.sh.
3
+ #
4
+ # docs/metrics/*.jsonl are append-only histories (SCHEMA.md §10, _metrics-emit-gate.md).
5
+ # This hook refuses every other way of writing them:
6
+ # - Write / Edit / MultiEdit whose file_path is a docs/metrics/*.jsonl file
7
+ # - a shell command that writes one: redirection (> >>), tee, cp/mv/install onto
8
+ # it, sed -i / perl -i, a python/node one-liner naming it, truncate/dd
9
+ # Reading them is fine. `bash .tfcore/utils/tf-emit.sh <stream>` never names the
10
+ # file on the command line, so it passes; so do tf-metrics.sh --report/--rollup.
11
+ #
12
+ # Wired in .claude/settings.json -> hooks.PreToolUse (matcher "Write|Edit|MultiEdit"
13
+ # AND matcher "Bash"); OpenCode via .opencode/plugin/techieflow.js.
14
+ # Exit 2 + stderr = block the call and feed the message back to the agent.
15
+ # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
16
+
17
+ INPUT="$(cat)"
18
+ command -v python3 >/dev/null 2>&1 || exit 0
19
+
20
+ TF_HOOK_INPUT="$INPUT" python3 - <<'PY'
21
+ import json, os, re, sys
22
+
23
+ try:
24
+ data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
25
+ except Exception:
26
+ sys.exit(0)
27
+
28
+ ti = data.get("tool_input") or {}
29
+ STREAM = r"docs/metrics/[\w.-]+\.jsonl"
30
+ # What makes a python/node command a WRITE rather than a read: an explicit write mode,
31
+ # something that emits, or a move onto the path. A read names none of these.
32
+ WRITEY = (r"(?:['\"][wax]b?\+?['\"]|\.write\b|\.writelines\b|json\.dump\b|"
33
+ r"\btruncate\b|\bos\.rename\b|\bos\.replace\b|\bshutil\.(?:copy|move)\b|"
34
+ r"createWriteStream|writeFileSync|appendFileSync)")
35
+
36
+ MSG = ("BLOCKED by TechieFlow policy: docs/metrics/*.jsonl are append-only telemetry "
37
+ "streams written only through bash .tfcore/utils/tf-emit.sh <stream> (stdin JSON). "
38
+ "Never Write, Edit, redirect or copy onto them. A wrong record is corrected by a "
39
+ "new record (miss-fix, miss-amend, a later gate), never by an edit.")
40
+
41
+ fp = ti.get("file_path") or ti.get("filePath")
42
+ if isinstance(fp, str) and re.search(STREAM + r"$", fp.replace("\\", "/"), re.I):
43
+ print(MSG, file=sys.stderr)
44
+ sys.exit(2)
45
+
46
+ cmd = ti.get("command")
47
+ if isinstance(cmd, str) and re.search(STREAM, cmd, re.I):
48
+ if re.search(
49
+ r"(>>?\s*[\"']?[^\s|;&]*" + STREAM + r")"
50
+ r"|(\btee\b[^|;&]*" + STREAM + r")"
51
+ r"|(\b(cp|mv|install)\b[^|;&]*" + STREAM + r")"
52
+ r"|(\b(sed|perl)\b[^|;&]*\s-[a-zA-Z]*i[a-zA-Z]*\b[^|;&]*" + STREAM + r")"
53
+ # A python/node command NAMING a stream used to be blocked outright, which also
54
+ # refused every READ -- and this file's own header says reading is fine. Checking
55
+ # a stream with a one-liner is a normal, frequent thing to do, and a guard that
56
+ # blocks it teaches people to route around the guard. So the clause now needs a
57
+ # write in the command as well as the path. MISS-TechieFlow-20260909-03.
58
+ r"|(\b(python3?|node)\b[^|;&]*" + STREAM + r"[^|;&]*" + WRITEY + r")"
59
+ r"|(\b(python3?|node)\b[^|;&]*" + WRITEY + r"[^|;&]*" + STREAM + r")"
60
+ r"|(\b(truncate|dd)\b[^|;&]*" + STREAM + r")",
61
+ cmd, re.I,
62
+ ):
63
+ print(MSG, file=sys.stderr)
64
+ sys.exit(2)
65
+
66
+ sys.exit(0)
67
+ PY
68
+ exit $?
@@ -1,102 +1,147 @@
1
1
  #!/usr/bin/env bash
2
- # TechieFlow Stop hook — refuses to end a turn while PROJECT-STATUS.html is stale.
2
+ # TechieFlow Stop hook — refuses to end a turn while the status gate is incomplete.
3
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.
4
+ # The gate (_status-update-gate.md) is six steps an agent can forget. This hook
5
+ # checks the RESULT of each step, so a forgotten step cannot end the turn. It
6
+ # prints one line per outstanding item, each naming the one command to run.
8
7
  #
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.
8
+ # 1. PROJECT-STATUS.html missing or older than PROJECT-STATUS.md
9
+ # -> bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md
10
+ # Only when PROJECT-STATUS.md was written in this session (newer than the
11
+ # session pointer .tfcore/.session/<harness>.json):
12
+ # 2. tf-doc-check.sh FAILs on PROJECT-STATUS.md -> fix the file, re-run the check
13
+ # 3. docs/<App>-BRD.md older than docs/<App>-Checklist.md
14
+ # -> bash .tfcore/utils/tf-brd-status.sh <App>
15
+ # 4. no runs.jsonl record after the status write -> the run record (_metrics-emit-gate.md)
14
16
  #
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).
17
+ # Wired in .claude/settings.json -> hooks.Stop; OpenCode via
18
+ # .opencode/plugin/techieflow.js session.idle (a nudge: OpenCode has no blocking
19
+ # Stop hook). Exit 2 + stderr = block the stop and feed the message to the agent.
20
+ # Honours stop_hook_active so a wedged turn can still end (no loop).
25
21
  # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
26
22
 
27
23
  INPUT="$(cat)"
28
24
  command -v python3 >/dev/null 2>&1 || exit 0
29
25
 
30
26
  TF_HOOK_INPUT="$INPUT" TF_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" python3 - <<'PY'
31
- import json, os, sys
27
+ import datetime
28
+ import glob
29
+ import json
30
+ import os
31
+ import subprocess
32
+ import sys
32
33
 
33
34
  try:
34
35
  data = json.loads(os.environ.get("TF_HOOK_INPUT", ""))
35
36
  except Exception:
36
37
  sys.exit(0)
37
38
 
38
- # Already re-invoked once for this stop — let the turn end rather than loop.
39
39
  if data.get("stop_hook_active"):
40
40
  sys.exit(0)
41
41
 
42
42
  root = os.environ.get("TF_PROJECT_DIR") or os.getcwd()
43
43
  md = os.path.join(root, "PROJECT-STATUS.md")
44
44
  html = os.path.join(root, "PROJECT-STATUS.html")
45
-
46
45
  if not os.path.isfile(md):
47
46
  sys.exit(0)
48
47
 
48
+ problems = []
49
49
  md_mtime = os.path.getmtime(md)
50
50
 
51
+ # 1. HTML present and current
51
52
  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"
53
+ problems.append("PROJECT-STATUS.html does not exist. Run: bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md")
54
+ elif md_mtime > os.path.getmtime(html):
55
+ problems.append("PROJECT-STATUS.html is older than PROJECT-STATUS.md. Run: bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md")
56
+
57
+
58
+ def session_start():
59
+ """mtime of the harness's session pointer; None when there is none (checks 2-4 skipped)."""
60
+ d = os.path.join(root, ".tfcore", ".session")
61
+ if any(k.startswith("OPENCODE") for k in os.environ):
62
+ cands = [os.path.join(d, "opencode.json")]
63
+ elif os.environ.get("CLAUDECODE") or any(k.startswith("CLAUDE_CODE") for k in os.environ):
64
+ cands = [os.path.join(d, "claude-code.json")]
70
65
  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)
66
+ cands = [os.path.join(d, "claude-code.json"), os.path.join(d, "opencode.json")]
67
+ times = [os.path.getmtime(p) for p in cands if os.path.isfile(p)]
68
+ return max(times) if times else None
69
+
70
+
71
+ start = session_start()
72
+ written_this_session = start is not None and md_mtime >= start
73
+
74
+ if written_this_session:
75
+ # 2. the checker passes on the status file
76
+ try:
77
+ chk = os.path.join(root, ".tfcore", "utils", "tf-doc-check.sh")
78
+ if os.path.isfile(chk):
79
+ out = subprocess.run(["bash", chk, "--quiet", "PROJECT-STATUS.md"], cwd=root,
80
+ capture_output=True, text=True, timeout=60).stdout
81
+ fails = [l for l in out.splitlines() if l.startswith("FAIL")]
82
+ if fails:
83
+ problems.append(f"PROJECT-STATUS.md fails the document check ({len(fails)} line(s)). Fix it, then run: bash .tfcore/utils/tf-doc-check.sh PROJECT-STATUS.md")
84
+ problems.extend(" " + l for l in fails[:5])
85
+ except Exception:
86
+ pass
87
+
88
+ # 2b. the checker passes on every checklist written this session. Until 2026-09-06 only
89
+ # PROJECT-STATUS was checked here, so a build closed its gate with three Remarks cells over
90
+ # 60 words in the checklist and nothing refused the stop (MISS-TechieFlow-20260906-06).
91
+ try:
92
+ chk = os.path.join(root, ".tfcore", "utils", "tf-doc-check.sh")
93
+ if os.path.isfile(chk):
94
+ for cl in sorted(glob.glob(os.path.join(root, "docs", "*-Checklist.md"))):
95
+ if os.path.getmtime(cl) < start:
96
+ continue
97
+ rel = os.path.relpath(cl, root)
98
+ out = subprocess.run(["bash", chk, "--quiet", rel], cwd=root,
99
+ capture_output=True, text=True, timeout=120).stdout
100
+ fails = [l for l in out.splitlines() if l.startswith("FAIL")]
101
+ if fails:
102
+ problems.append(f"{rel} fails the document check ({len(fails)} line(s)). Fix the rows, then run: bash .tfcore/utils/tf-doc-check.sh {rel}")
103
+ problems.extend(" " + l for l in fails[:5])
104
+ except Exception:
105
+ pass
106
+
107
+ # 3. the BRD's Development status table is not older than the checklist
108
+ try:
109
+ for cl in glob.glob(os.path.join(root, "docs", "*-Checklist.md")):
110
+ app = os.path.basename(cl)[: -len("-Checklist.md")]
111
+ brd = os.path.join(root, "docs", f"{app}-BRD.md")
112
+ if os.path.isfile(brd) and os.path.getmtime(cl) > os.path.getmtime(brd):
113
+ problems.append(f"docs/{app}-BRD.md is older than docs/{app}-Checklist.md. Run: bash .tfcore/utils/tf-brd-status.sh {app}")
114
+ except Exception:
115
+ pass
116
+
117
+ # 4. a run record follows the status write (5 minutes of tolerance for emit-before-write)
118
+ try:
119
+ runs = os.path.join(root, "docs", "metrics", "runs.jsonl")
120
+ last = None
121
+ if os.path.isfile(runs):
122
+ with open(runs, encoding="utf-8", errors="replace") as f:
123
+ for line in f:
124
+ try:
125
+ ts = json.loads(line).get("ts")
126
+ except Exception:
127
+ continue
128
+ if ts:
129
+ last = ts
130
+ last_epoch = None
131
+ if last:
132
+ last_epoch = datetime.datetime.strptime(last[:19], "%Y-%m-%dT%H:%M:%S").replace(
133
+ tzinfo=datetime.timezone.utc).timestamp()
134
+ if last_epoch is None or last_epoch < md_mtime - 300:
135
+ problems.append("PROJECT-STATUS.md was written but no run record follows it in docs/metrics/runs.jsonl. Append the run record: .tfcore/tasks/_metrics-emit-gate.md")
136
+ except Exception:
137
+ pass
138
+
139
+ if not problems:
140
+ sys.exit(0)
141
+
142
+ print("BLOCKED by TechieFlow policy: the status gate is not complete (_status-update-gate.md).", file=sys.stderr)
143
+ for p in problems:
144
+ print(" - " + p, file=sys.stderr)
145
+ sys.exit(2)
101
146
  PY
102
147
  exit $?
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  # TechieFlow PreToolUse hook — blocks convoluted PROJECT-STATUS.md writes.
3
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.
4
+ # PROJECT-STATUS is a one-page snapshot agents overwrite in place, never an
5
+ # append-log. The shape is the template .tfcore/templates/v4custom/app-project-status-tmpl.md;
6
+ # the steps are .tfcore/tasks/_status-update-gate.md. The prose rule alone kept
7
+ # failing (AstroLyfe's 286-line mess, repeat offenses after); this hook makes the
8
+ # shape mechanical, the same way block-git.sh made the git ban mechanical.
10
9
  #
11
- # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Write|Edit|MultiEdit").
10
+ # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Write|Edit|MultiEdit"
11
+ # AND matcher "Bash"); OpenCode via .opencode/plugin/techieflow.js.
12
12
  # Exit 2 + stderr = block the call and feed the message back to the agent.
13
13
  #
14
14
  # What it blocks (only for files named PROJECT-STATUS.md):
@@ -17,6 +17,11 @@
17
17
  # - Headings that name a command run (*verify / *build-phase / *fix-issues).
18
18
  # - A paragraph stuffed into `current_phase:` (must be ONE short line).
19
19
  # - A full-file Write longer than 120 lines (template shape is ~60).
20
+ # - (Bash) any shell command that WRITES the file — redirection, tee, cp, mv,
21
+ # sed -i, a Python/Node one-liner — because a shell write is invisible to the
22
+ # Write/Edit checks above (reset Session 3, 2026-09-04). Reading it is fine.
23
+ # Content rules (section word limits, the two command blocks, the five-row log)
24
+ # are checked by .tfcore/utils/tf-doc-check.sh at the status gate.
20
25
  # Fails OPEN (exit 0) if python3 or parseable JSON is unavailable.
21
26
 
22
27
  INPUT="$(cat)"
@@ -32,6 +37,31 @@ except Exception:
32
37
  sys.exit(0)
33
38
 
34
39
  ti = data.get("tool_input") or {}
40
+
41
+ # --- Bash branch: refuse shell writes to PROJECT-STATUS.md -------------------
42
+ cmd = ti.get("command")
43
+ if isinstance(cmd, str):
44
+ if re.search(r"PROJECT-STATUS\.md", cmd, re.I) and re.search(
45
+ r"(>>?\s*[\"']?[^\s|;&]*PROJECT-STATUS\.md)" # > / >> redirection
46
+ r"|(\btee\b[^|;&]*PROJECT-STATUS\.md)" # tee
47
+ r"|(\b(cp|mv|install)\b[^|;&]*PROJECT-STATUS\.md)" # copy / move onto it
48
+ r"|(\b(sed|perl)\b[^|;&]*\s-[a-zA-Z]*i[a-zA-Z]*\b[^|;&]*PROJECT-STATUS\.md)" # in-place edit
49
+ r"|(\b(python3?|node)\b[^|;&]*PROJECT-STATUS\.md)" # a one-liner that opens it
50
+ r"|(\b(truncate|dd)\b[^|;&]*PROJECT-STATUS\.md)",
51
+ cmd, re.I,
52
+ ):
53
+ print(
54
+ "BLOCKED by TechieFlow policy: PROJECT-STATUS.md is written ONLY through "
55
+ "the harness Write/Edit tool, never through the shell (redirection, tee, "
56
+ "cp, mv, sed -i, a script). The shell path bypasses the shape guard, which "
57
+ "is how status files got spoiled. Write the file with the Write tool in the "
58
+ "template shape (.tfcore/templates/v4custom/app-project-status-tmpl.md), "
59
+ "then run: bash .tfcore/utils/tf-doc-check.sh PROJECT-STATUS.md",
60
+ file=sys.stderr,
61
+ )
62
+ sys.exit(2)
63
+ sys.exit(0)
64
+
35
65
  path = (ti.get("file_path") or "").replace("\\", "/")
36
66
  if path.rsplit("/", 1)[-1].lower() != "project-status.md":
37
67
  sys.exit(0)
@@ -97,24 +127,21 @@ if is_full_write:
97
127
  errors.append(
98
128
  f"This write is {n_lines} lines. PROJECT-STATUS is a one-page snapshot "
99
129
  "(~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."
130
+ "history lives in the checklist Remarks and the telemetry streams, never here."
101
131
  )
102
132
 
103
133
  if errors:
104
134
  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).",
135
+ "BLOCKED by TechieFlow policy: PROJECT-STATUS is a one-page snapshot you "
136
+ "overwrite in place, never append to (.tfcore/tasks/_status-update-gate.md).",
108
137
  file=sys.stderr,
109
138
  )
110
139
  for e in errors:
111
140
  print(" - " + e, file=sys.stderr)
112
141
  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.",
142
+ "Fix and retry: run bash .tfcore/utils/tf-status-facts.sh <App> and write only "
143
+ "the eight template sections (.tfcore/templates/v4custom/app-project-status-tmpl.md), "
144
+ "each overwritten in place. The detail goes in the checklist Remarks, not here.",
118
145
  file=sys.stderr,
119
146
  )
120
147
  sys.exit(2)