@techierathore/techieflow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
- package/.claude/commands/TechieFlow/agents/architect.md +84 -0
- package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
- package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
- package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
- package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
- package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
- package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
- package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
- package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
- package/.claude/commands/generate-html.md +124 -0
- package/.codex/config.toml +46 -0
- package/.codex/hooks.json +63 -0
- package/.codex/rules/techieflow.rules +8 -0
- package/.opencode/command/generate-html.md +129 -0
- package/.opencode/plugin/techieflow.js +350 -0
- package/.tfcore/TOKEN-GUIDE.md +59 -0
- package/.tfcore/agents/analyst.md +97 -0
- package/.tfcore/agents/architect.md +84 -0
- package/.tfcore/agents/flow-master.md +217 -0
- package/.tfcore/agents/verifier.md +72 -0
- package/.tfcore/checklists/architect-checklist.md +438 -0
- package/.tfcore/core-config.yaml +47 -0
- package/.tfcore/data/brainstorming-techniques.md +36 -0
- package/.tfcore/data/elicitation-methods.md +154 -0
- package/.tfcore/data/techieflow-kb.md +807 -0
- package/.tfcore/data/technical-preferences.md +3 -0
- package/.tfcore/enhanced-ide-development-workflow.md +248 -0
- package/.tfcore/hooks/block-git.sh +301 -0
- package/.tfcore/hooks/codex-adapter.py +248 -0
- package/.tfcore/hooks/guard-artifacts.sh +101 -0
- package/.tfcore/hooks/guard-status-html.sh +102 -0
- package/.tfcore/hooks/guard-status.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +108 -0
- package/.tfcore/hooks/metrics-session.sh +129 -0
- package/.tfcore/hooks/session-pointer.sh +52 -0
- package/.tfcore/hooks/sweep-artifacts.sh +198 -0
- package/.tfcore/install-manifest.yaml +231 -0
- package/.tfcore/routing.yaml +105 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
- package/.tfcore/tasks/_smoke-test-policy.md +98 -0
- package/.tfcore/tasks/_status-update-gate.md +78 -0
- package/.tfcore/tasks/_yolo-mode.md +77 -0
- package/.tfcore/tasks/advanced-elicitation.md +117 -0
- package/.tfcore/tasks/amend-docs.md +149 -0
- package/.tfcore/tasks/author-brd.md +79 -0
- package/.tfcore/tasks/build-phase.md +231 -0
- package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
- package/.tfcore/tasks/create-doc.md +101 -0
- package/.tfcore/tasks/day1-brownfield.md +507 -0
- package/.tfcore/tasks/day1-greenfield.md +224 -0
- package/.tfcore/tasks/devguide.md +224 -0
- package/.tfcore/tasks/document-project.md +343 -0
- package/.tfcore/tasks/execute-checklist.md +86 -0
- package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
- package/.tfcore/tasks/fix-issues.md +134 -0
- package/.tfcore/tasks/generate-html.md +115 -0
- package/.tfcore/tasks/handoff-phase.md +126 -0
- package/.tfcore/tasks/index-docs.md +173 -0
- package/.tfcore/tasks/kb-mode-interaction.md +75 -0
- package/.tfcore/tasks/log-miss.md +160 -0
- package/.tfcore/tasks/metrics-report.md +143 -0
- package/.tfcore/tasks/mockups.md +122 -0
- package/.tfcore/tasks/productguide.md +105 -0
- package/.tfcore/tasks/refresh-status.md +135 -0
- package/.tfcore/tasks/render-workflow-docs.md +117 -0
- package/.tfcore/tasks/shard-doc.md +185 -0
- package/.tfcore/tasks/split-brd.md +131 -0
- package/.tfcore/tasks/triage-issues.md +157 -0
- package/.tfcore/tasks/verify-phase.md +579 -0
- package/.tfcore/telemetry/SCHEMA.md +678 -0
- package/.tfcore/telemetry/install-metrics.sh +390 -0
- package/.tfcore/telemetry/pre-commit +94 -0
- package/.tfcore/telemetry/tf-metrics.sh +1415 -0
- package/.tfcore/templates/architecture-tmpl.yaml +650 -0
- package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
- package/.tfcore/templates/market-research-tmpl.yaml +252 -0
- package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
- package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
- package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
- package/.tfcore/user-guide.md +633 -0
- package/.tfcore/utils/techieflow-doc-template.md +325 -0
- package/.tfcore/utils/tf-assets.sh +310 -0
- package/.tfcore/utils/tf-codex-bind.py +196 -0
- package/.tfcore/utils/tf-codex-telemetry.py +60 -0
- package/.tfcore/utils/tf-emit.sh +1075 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
- package/.tfcore/utils/tf-goal.sh +421 -0
- package/.tfcore/utils/tf-harness.sh +160 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
- package/.tfcore/utils/tf-mockup-parity.sh +65 -0
- package/.tfcore/utils/tf-opencode-audit.py +126 -0
- package/.tfcore/utils/tf-perf.sh +241 -0
- package/.tfcore/utils/tf-render-html.py +715 -0
- package/.tfcore/utils/tf-render-html.sh +28 -0
- package/.tfcore/utils/tf-routing-bind.sh +216 -0
- package/.tfcore/utils/tf-routing.sh +247 -0
- package/.tfcore/utils/tf-yolo.sh +119 -0
- package/.tfcore/working-in-the-brownfield.md +606 -0
- package/LICENSE +201 -0
- package/README.md +1212 -0
- package/WORKFLOW.html +1986 -0
- package/docs/TechieFlow-Installation.md +299 -0
- package/opencode.jsonc +929 -0
- package/package.json +60 -0
- package/scripts/install.mjs +992 -0
- package/scripts/test-install.mjs +505 -0
- package/scripts/validate.mjs +155 -0
|
@@ -0,0 +1,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 $?
|