@techierathore/techieflow 1.0.3 → 1.1.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 +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +181 -9
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +27 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +3 -3
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +7 -3
- package/.tfcore/tasks/handoff-phase.md +20 -18
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +61 -1
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +435 -21
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +99 -11
- package/.tfcore/utils/tf-build-list.sh +5 -3
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +242 -10
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +413 -19
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +113 -31
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +105 -18
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +24 -21
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +33 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +80 -2
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +356 -0
- package/.tfcore/utils/tf-split-brd.py +17 -2
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +133 -44
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +254 -43
- package/.tfcore/utils/tf-verify-screens.sh +18 -4
- package/.tfcore/utils/tf-verify-tests.sh +272 -28
- package/.tfcore/utils/tf-verify-verdict.py +59 -11
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +19 -5
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
// auto-approves the permission map's rm/rmdir/sudo asks and TF_YOLO=1 is
|
|
19
19
|
// exported to tool shells so block-git.sh allows read-only git. Writes to
|
|
20
20
|
// git never reach this hook — they are `deny` in the map.
|
|
21
|
-
// bash {command} -> Bash {command} -> block-git.sh + guard-artifacts.sh
|
|
21
|
+
// bash {command} -> Bash {command} -> block-git.sh + guard-artifacts.sh + guard-verify-deps.sh
|
|
22
22
|
// edit {filePath,oldString,newString} -> Edit {file_path,old_string,new_string}
|
|
23
23
|
// write {filePath,content} -> Write {file_path,content} -> guard-status.sh + guard-verify.sh
|
|
24
|
-
// session.idle (root session) -> Stop {stop_hook_active}
|
|
24
|
+
// session.idle (root session) -> Stop {stop_hook_active, last_assistant_message, turn_started} -> guard-status-html.sh
|
|
25
25
|
// session.created (first root session) -> SessionStart -> sweep-artifacts.sh
|
|
26
26
|
// OpenCode has no blocking Stop hook, so the stale-PROJECT-STATUS.html guard
|
|
27
27
|
// (_status-update-gate.md step 4) is bridged as a ONE-SHOT nudge: when the root
|
|
@@ -42,14 +42,25 @@
|
|
|
42
42
|
// idles once). Records share session_id; consumers take the record with the
|
|
43
43
|
// highest output_tokens (or latest ts) per session_id. SCHEMA.md §4 notes this.
|
|
44
44
|
|
|
45
|
+
//
|
|
46
|
+
// Two OpenCode plugin APIs, one file (2026-09-27). OpenCode 1.x (the design's 1.18.18; 1.18.32 is
|
|
47
|
+
// the last 1.x) reads the default export's `server`; OpenCode 2 (checked on 2.0.18, 2026-09-28)
|
|
48
|
+
// accepts only a default export with `id` and `setup`, and refused this file with "Plugin must export
|
|
49
|
+
// a default definition with an id and an effect or setup function". The default export carries all
|
|
50
|
+
// three; `setup` translates 2.x's hook and event API into the 1.x hooks below, so the guards, the
|
|
51
|
+
// shell env and telemetry are the same code in both. What 2.x does not offer: `permission.ask` (YOLO
|
|
52
|
+
// auto-approval; OpenCode 2's own `--auto` flag is the switch there) and the session's messages, so
|
|
53
|
+
// the Stop nudge's owner-text check (guard-status-html.sh check 5) is skipped there; checks 1-4 run.
|
|
54
|
+
|
|
45
55
|
import fs from "node:fs"
|
|
46
56
|
import path from "node:path"
|
|
47
57
|
import os from "node:os"
|
|
48
58
|
import { spawnSync } from "node:child_process"
|
|
59
|
+
import { fileURLToPath } from "node:url"
|
|
49
60
|
|
|
50
61
|
const GUARD_TIMEOUT_MS = 10000
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
const TechieFlowPlugin = async ({ directory, client }) => {
|
|
53
64
|
const root = directory
|
|
54
65
|
const hooksDir = path.join(root, ".tfcore", "hooks")
|
|
55
66
|
const tfEmit = path.join(root, ".tfcore", "utils", "tf-emit.sh")
|
|
@@ -110,8 +121,29 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
|
|
|
110
121
|
const htmlNudged = Object.create(null) // rootID -> true once the stale-HTML nudge was sent
|
|
111
122
|
let pointerWritten = false
|
|
112
123
|
|
|
124
|
+
// What Claude Code hands its Stop hook and OpenCode does not: the closing message, and when
|
|
125
|
+
// the turn began (the last prompt the owner typed). The guard checks the message the owner
|
|
126
|
+
// reads at the end of a command (tf-owner-text.sh). Empty on any failure: the check then skips.
|
|
127
|
+
async function closingTurn(rootID) {
|
|
128
|
+
try {
|
|
129
|
+
if (typeof client.session.messages !== "function") return {}
|
|
130
|
+
const res = await client.session.messages({ path: { id: rootID } })
|
|
131
|
+
const list = Array.isArray(res) ? res : (res && res.data) || []
|
|
132
|
+
const text = (m) => (m.parts || []).filter((x) => x.type === "text" && !x.synthetic).map((x) => x.text).join("\n")
|
|
133
|
+
let message = "", turn = null
|
|
134
|
+
for (const m of list) {
|
|
135
|
+
const info = m.info || {}
|
|
136
|
+
if (info.role === "user" && text(m).trim()) turn = (info.time || {}).created || turn
|
|
137
|
+
if (info.role === "assistant" && text(m).trim()) message = text(m)
|
|
138
|
+
}
|
|
139
|
+
return turn ? { last_assistant_message: message, turn_started: turn } : {}
|
|
140
|
+
} catch {
|
|
141
|
+
return {}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
113
145
|
// Stop-hook analogue for guard-status-html.sh (see header). Never throws.
|
|
114
|
-
function nudgeStaleStatusHtml(rootID) {
|
|
146
|
+
async function nudgeStaleStatusHtml(rootID) {
|
|
115
147
|
try {
|
|
116
148
|
if (!client || !client.session || typeof client.session.prompt !== "function") return
|
|
117
149
|
const msg = guardBlocks("guard-status-html.sh", {
|
|
@@ -119,6 +151,7 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
|
|
|
119
151
|
cwd: root,
|
|
120
152
|
session_id: rootID,
|
|
121
153
|
stop_hook_active: htmlNudged[rootID] === true,
|
|
154
|
+
...(htmlNudged[rootID] === true ? {} : await closingTurn(rootID)),
|
|
122
155
|
})
|
|
123
156
|
if (!msg) return
|
|
124
157
|
htmlNudged[rootID] = true
|
|
@@ -232,14 +265,16 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
|
|
|
232
265
|
const args = output.args || {}
|
|
233
266
|
let payload = null
|
|
234
267
|
let scripts = []
|
|
235
|
-
if (input.tool === "bash") {
|
|
268
|
+
if (input.tool === "bash" || input.tool === "shell") {
|
|
269
|
+
// 1.x calls it bash; 2.x calls it shell (same `command` argument)
|
|
236
270
|
payload = { tool_name: "Bash", tool_input: { command: String(args.command || ""), run_in_background: !!(args.background || args.run_in_background) } }
|
|
237
|
-
scripts = ["block-git.sh", "guard-artifacts.sh", "guard-status.sh", "guard-metrics.sh", "guard-db.sh", "guard-build.sh"]
|
|
271
|
+
scripts = ["block-git.sh", "guard-artifacts.sh", "guard-status.sh", "guard-metrics.sh", "guard-db.sh", "guard-build.sh", "guard-verify-deps.sh"]
|
|
238
272
|
} else if (input.tool === "edit") {
|
|
273
|
+
// 1.x names the file filePath, 2.x names it path
|
|
239
274
|
payload = {
|
|
240
275
|
tool_name: "Edit",
|
|
241
276
|
tool_input: {
|
|
242
|
-
file_path: String(args.filePath || ""),
|
|
277
|
+
file_path: String(args.filePath || args.path || ""),
|
|
243
278
|
old_string: String(args.oldString || ""),
|
|
244
279
|
new_string: String(args.newString || ""),
|
|
245
280
|
},
|
|
@@ -248,7 +283,7 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
|
|
|
248
283
|
} else if (input.tool === "write") {
|
|
249
284
|
payload = {
|
|
250
285
|
tool_name: "Write",
|
|
251
|
-
tool_input: { file_path: String(args.filePath || ""), content: String(args.content || "") },
|
|
286
|
+
tool_input: { file_path: String(args.filePath || args.path || ""), content: String(args.content || "") },
|
|
252
287
|
}
|
|
253
288
|
scripts = ["guard-status.sh", "guard-verify.sh", "guard-metrics.sh"]
|
|
254
289
|
} else if (input.tool === "apply_patch") {
|
|
@@ -367,7 +402,7 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
|
|
|
367
402
|
if (!sid) return
|
|
368
403
|
if (rootOf(sid) === sid) {
|
|
369
404
|
emitSession(sid)
|
|
370
|
-
nudgeStaleStatusHtml(sid)
|
|
405
|
+
await nudgeStaleStatusHtml(sid)
|
|
371
406
|
}
|
|
372
407
|
return
|
|
373
408
|
}
|
|
@@ -375,3 +410,140 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
|
|
|
375
410
|
},
|
|
376
411
|
}
|
|
377
412
|
}
|
|
413
|
+
|
|
414
|
+
// ---- OpenCode 2.x: translate its API into the 1.x hooks above ----
|
|
415
|
+
// The 2.x context names no project directory, and its background service does not run in the
|
|
416
|
+
// project, so the root is where this file sits: <root>/.opencode/plugin/techieflow.js.
|
|
417
|
+
function projectRoot() {
|
|
418
|
+
try {
|
|
419
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..")
|
|
420
|
+
} catch {
|
|
421
|
+
return process.cwd()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function eventTime(value) {
|
|
426
|
+
const n = typeof value === "number" ? value : Date.parse(String(value || ""))
|
|
427
|
+
return Number.isFinite(n) ? n : Date.now()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function setupV2(context) {
|
|
431
|
+
const root = projectRoot()
|
|
432
|
+
// What the Stop nudge needs from a client: prompt a session. 2.x has no message listing, so
|
|
433
|
+
// closingTurn() finds no messages and the owner-text check is skipped (see the header).
|
|
434
|
+
const client = {
|
|
435
|
+
session: {
|
|
436
|
+
prompt: ({ path: p, body }) =>
|
|
437
|
+
context.session.prompt({
|
|
438
|
+
sessionID: p.id,
|
|
439
|
+
text: ((body && body.parts) || []).map((x) => x.text || "").join("\n"),
|
|
440
|
+
}),
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
const hooks = await TechieFlowPlugin({ directory: root, client })
|
|
444
|
+
const registrations = []
|
|
445
|
+
|
|
446
|
+
// Guards. A throw here stops the call, as in 1.x.
|
|
447
|
+
registrations.push(
|
|
448
|
+
await context.tool.hook("execute.before", async (event) => {
|
|
449
|
+
await hooks["tool.execute.before"]({ tool: event.tool, sessionID: event.sessionID, callID: event.id }, { args: event.input || {} })
|
|
450
|
+
}),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
// TF_HARNESS, TF_PROJECT_DIR, TF_YOLO for the agent's shell commands.
|
|
454
|
+
registrations.push(
|
|
455
|
+
await context.shell.hook("create.before", async (event) => {
|
|
456
|
+
if (!event || !event.env) return
|
|
457
|
+
await hooks["shell.env"]({}, { env: event.env })
|
|
458
|
+
}),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
// Telemetry and the Stop nudge: 2.x events rewritten as the 1.x events the handler reads.
|
|
462
|
+
// A step carries its own tokens and cost; each step is recorded once, under its own key.
|
|
463
|
+
// The session is created before this subscription starts, so no session.created arrives for
|
|
464
|
+
// it: a session is learned the first time an event of this project names it, and its parent
|
|
465
|
+
// is read with session.get (a root session has no parentID). A turn ends with
|
|
466
|
+
// session.execution.*, which carries no location, so only sessions learned here count.
|
|
467
|
+
const models = Object.create(null) // assistantMessageID -> {model, t0}
|
|
468
|
+
const known = Object.create(null) // sessionID -> true once learned
|
|
469
|
+
let stream = null
|
|
470
|
+
let stopped = false
|
|
471
|
+
const learn = async (sessionID) => {
|
|
472
|
+
if (!sessionID || known[sessionID]) return
|
|
473
|
+
known[sessionID] = true
|
|
474
|
+
let parentID = null
|
|
475
|
+
try {
|
|
476
|
+
const info = await context.session.get({ sessionID })
|
|
477
|
+
parentID = (info && info.parentID) || null
|
|
478
|
+
} catch {}
|
|
479
|
+
await hooks.event({ event: { type: "session.created", properties: { info: { id: sessionID, parentID } } } })
|
|
480
|
+
}
|
|
481
|
+
const translate = (ev) => {
|
|
482
|
+
const d = ev.data || {}
|
|
483
|
+
if (ev.type === "session.step.started") {
|
|
484
|
+
models[d.assistantMessageID] = { model: d.model || {}, t0: eventTime(ev.created) }
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
487
|
+
if (ev.type === "session.step.ended") {
|
|
488
|
+
const started = models[d.assistantMessageID] || { model: {}, t0: eventTime(ev.created) }
|
|
489
|
+
const t = d.tokens || {}
|
|
490
|
+
return {
|
|
491
|
+
type: "message.updated",
|
|
492
|
+
properties: {
|
|
493
|
+
info: {
|
|
494
|
+
role: "assistant",
|
|
495
|
+
id: String(d.assistantMessageID) + ":" + String(ev.id),
|
|
496
|
+
sessionID: d.sessionID,
|
|
497
|
+
providerID: started.model.providerID,
|
|
498
|
+
modelID: started.model.id,
|
|
499
|
+
cost: d.cost,
|
|
500
|
+
tokens: { input: t.input, output: t.output, cache: t.cache || {} },
|
|
501
|
+
time: { created: started.t0, completed: eventTime(ev.created) },
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const idle =
|
|
507
|
+
/^session\.execution\.(succeeded|failed|interrupted)$/.test(ev.type) ||
|
|
508
|
+
(ev.type === "session.status" && d.status && d.status.type === "idle") ||
|
|
509
|
+
ev.type === "session.idle"
|
|
510
|
+
if (idle && known[d.sessionID]) return { type: "session.idle", properties: { sessionID: d.sessionID } }
|
|
511
|
+
return null
|
|
512
|
+
}
|
|
513
|
+
;(async () => {
|
|
514
|
+
try {
|
|
515
|
+
stream = context.event.subscribe()
|
|
516
|
+
for await (const ev of stream) {
|
|
517
|
+
if (stopped) break
|
|
518
|
+
try {
|
|
519
|
+
if (!ev) continue
|
|
520
|
+
if (ev.location && ev.location.directory) {
|
|
521
|
+
if (path.resolve(ev.location.directory) !== root) continue
|
|
522
|
+
if (ev.data && ev.data.sessionID) await learn(ev.data.sessionID)
|
|
523
|
+
}
|
|
524
|
+
const event = translate(ev)
|
|
525
|
+
if (event) await hooks.event({ event })
|
|
526
|
+
} catch {} // telemetry never blocks anything
|
|
527
|
+
}
|
|
528
|
+
} catch {}
|
|
529
|
+
})()
|
|
530
|
+
|
|
531
|
+
return async () => {
|
|
532
|
+
stopped = true
|
|
533
|
+
try {
|
|
534
|
+
if (stream && typeof stream.return === "function") await stream.return()
|
|
535
|
+
} catch {}
|
|
536
|
+
for (const r of registrations) {
|
|
537
|
+
try {
|
|
538
|
+
await r.dispose()
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// OpenCode 1.x reads `server`; OpenCode 2.x reads `setup`. Each ignores the other.
|
|
545
|
+
export default {
|
|
546
|
+
id: "techieflow",
|
|
547
|
+
server: TechieFlowPlugin,
|
|
548
|
+
setup: setupV2,
|
|
549
|
+
}
|
|
@@ -41,6 +41,7 @@ persona:
|
|
|
41
41
|
identity: Strategic analyst specializing in brainstorming, market research, competitive analysis, and project briefing
|
|
42
42
|
focus: Research planning, ideation facilitation, strategic analysis, actionable insights
|
|
43
43
|
core_principles:
|
|
44
|
+
- OWNER LANGUAGE - Everything the owner reads is plain, simple English: no coined terms, no framework shorthand, no word they would have to look up. A choice only the owner can make goes in `docs/{App}-Decision-Request.md` with a recommendation and a block to paste back, and ONE line in the terminal — never argued out in the conversation. Reporting an upstream defect is never refused; it goes in the feedback file, answers "Blocks: yes|no" first, and a non-blocking entry never stops the run. `.tfcore/tasks/_owner-language.md`.
|
|
44
45
|
- Curiosity-Driven Inquiry - Ask probing "why" questions to uncover underlying truths
|
|
45
46
|
- Objective & Evidence-Based Analysis - Ground findings in verifiable data and credible sources
|
|
46
47
|
- Strategic Contextualization - Frame all work within broader strategic context
|
|
@@ -52,7 +53,7 @@ persona:
|
|
|
52
53
|
- Maintaining a Broad Perspective - Stay aware of market trends and dynamics
|
|
53
54
|
- Integrity of Information - Ensure accurate sourcing and representation
|
|
54
55
|
- Numbered Options Protocol - Always use numbered lists for selections
|
|
55
|
-
- RUN IT YOURSELF - When a task you run boots or smokes an app (devguide OBSERVE at brownfield day-1, render sweeps), the
|
|
56
|
+
- RUN IT YOURSELF - When a task you run boots or smokes an app (devguide OBSERVE at brownfield day-1, render sweeps), the project's own test harness is already set up - run it yourself; never hand the boot to the owner (_smoke-test-policy.md banned excuses).
|
|
56
57
|
- BRD Numbering Protocol - When authoring or extending a BRD, every business/functional requirement MUST receive a stable unique ID in the form 'BRD-{N}' (BRD-1, BRD-2, ...). IDs are APPEND-ONLY across revisions - never renumber existing IDs; new requirements take the next unused integer. Group requirements by phase under '## Phase {N}' headings so the verifier and UI agent can scope by phase.
|
|
57
58
|
- Interactive BRD Elicitation - BRD authoring runs with elicit=true. NEVER assume a requirement. For each section/area, surface candidate requirements as a numbered list and have the user confirm / reject / refine each one before assigning it a BRD-N. Do not batch-write multiple requirements without per-item confirmation.
|
|
58
59
|
# All commands require * prefix when used (e.g., *help)
|
|
@@ -60,14 +61,12 @@ commands:
|
|
|
60
61
|
- help: Show numbered list of the following commands to allow selection
|
|
61
62
|
- day1-brownfield {AppName}: Day-1 master task for an EXISTING project. Produces Architecture, BRD, Coding-Standards, .editorconfig, PROJECT-STATUS, CLAUDE.md + the screen-by-screen DevGuide (with per-screen screenshots) in one session; if an existing dev/phase plan is found, also migrates it into the one Checklist (inline split, statuses preserved). Runs task day1-brownfield.md.
|
|
62
63
|
- day1-greenfield {AppName}: Day-1 master task for a NEW project. Produces brief + BRD + target Architecture + UI mockups + Coding-Standards + .editorconfig + PROJECT-STATUS + CLAUDE.md + UsageGuide. Runs task day1-greenfield.md.
|
|
63
|
-
- mockups {AppName} [--update]: Produce the greenfield UI design — docs/{AppName}-UIDesign.md (per-screen component map) + rendered docs/mockups/*.html styled to look like
|
|
64
|
+
- mockups {AppName} [--update]: Produce the greenfield UI design — docs/{AppName}-UIDesign.md (per-screen component map) + rendered docs/mockups/*.html styled to look like the UI library the Architecture names (read that library's component catalog first when it ships one; replicable by construction). The visual contract the build matches + the verifier diffs against. Runs task mockups.md.
|
|
64
65
|
- split-brd {AppName}: Split docs/{AppName}-BRD.md into the one docs/{AppName}-Checklist.md (REQ-UI/FN/RAG/NFR-* in a single Requirements Status table); seeds phase tags + Done(pre-existing) statuses from any existing dev/phase plan. Runs task split-brd.md.
|
|
65
66
|
- brainstorm {topic}: Facilitate structured brainstorming session (run task facilitate-brainstorming-session.md with template brainstorming-output-tmpl.yaml)
|
|
66
67
|
- amend-docs {AppName} {change}: Fold an evolving concept / changed requirements into the EXISTING day-1 docs IN PLACE — surgically amends BRD + Architecture (append-only BRD IDs, unchanged sections preserved), ripples to PROJECT-STATUS / BRD §4 / the checklist (and points UI changes at *mockups --update), re-renders HTML. The incremental alternative to re-running *day1-* (which archives + regenerates). Runs task amend-docs.md.
|
|
67
|
-
- create-
|
|
68
|
-
- create-project-brief: use task create-doc with project-brief-tmpl.yaml
|
|
68
|
+
- create-project-brief {AppName}: Write docs/{AppName}-Brief.md, the one page day-1 reads — what it is, who for, must do, out of scope, open questions. Runs task create-project-brief.md.
|
|
69
69
|
- doc-out: Output full document in progress to current destination file
|
|
70
|
-
- perform-market-research: use task create-doc with market-research-tmpl.yaml
|
|
71
70
|
- research-prompt {topic}: execute task create-deep-research-prompt.md
|
|
72
71
|
- yolo: Toggle YOLO / goal mode — run `bash .tfcore/utils/tf-yolo.sh on|off`, then operate per .tfcore/tasks/_yolo-mode.md: no confirmations or elicitation pauses (take the sensible default, record it), deletes + read-only git allowed (git writes never), run the command to completion. Also implied by the word YOLO in any command, an active /goal, or a tf-goal.sh run.
|
|
73
72
|
- exit: Say goodbye as the Business Analyst, and then abandon inhabiting this persona
|
|
@@ -79,6 +78,7 @@ dependencies:
|
|
|
79
78
|
- amend-docs.md
|
|
80
79
|
- create-deep-research-prompt.md
|
|
81
80
|
- create-doc.md
|
|
81
|
+
- create-project-brief.md
|
|
82
82
|
- day1-brownfield.md
|
|
83
83
|
- day1-greenfield.md
|
|
84
84
|
- facilitate-brainstorming-session.md
|
|
@@ -86,7 +86,4 @@ dependencies:
|
|
|
86
86
|
- split-brd.md
|
|
87
87
|
templates:
|
|
88
88
|
- brainstorming-output-tmpl.yaml
|
|
89
|
-
- competitor-analysis-tmpl.yaml
|
|
90
|
-
- market-research-tmpl.yaml
|
|
91
|
-
- project-brief-tmpl.yaml
|
|
92
89
|
```
|
|
@@ -41,6 +41,7 @@ persona:
|
|
|
41
41
|
identity: Master of holistic application design who bridges frontend, backend, infrastructure, and everything in between
|
|
42
42
|
focus: Complete systems architecture, cross-stack optimization, pragmatic technology selection
|
|
43
43
|
core_principles:
|
|
44
|
+
- OWNER LANGUAGE - Everything the owner reads is plain, simple English: no coined terms, no framework shorthand, no word they would have to look up. A choice only the owner can make goes in `docs/{App}-Decision-Request.md` with a recommendation and a block to paste back, and ONE line in the terminal — never argued out in the conversation. Reporting an upstream defect is never refused; it goes in the feedback file, answers "Blocks: yes|no" first, and a non-blocking entry never stops the run. `.tfcore/tasks/_owner-language.md`.
|
|
44
45
|
- Holistic System Thinking - View every component as part of a larger system
|
|
45
46
|
- User Experience Drives Architecture - Start with user journeys and work backward
|
|
46
47
|
- Pragmatic Technology Selection - Choose boring technology where possible, exciting where necessary
|
|
@@ -54,10 +55,6 @@ persona:
|
|
|
54
55
|
# All commands require * prefix when used (e.g., *help)
|
|
55
56
|
commands:
|
|
56
57
|
- help: Show numbered list of the following commands to allow selection
|
|
57
|
-
- create-backend-architecture: use create-doc with architecture-tmpl.yaml
|
|
58
|
-
- create-brownfield-architecture: use create-doc with brownfield-architecture-tmpl.yaml
|
|
59
|
-
- create-front-end-architecture: use create-doc with front-end-architecture-tmpl.yaml
|
|
60
|
-
- create-full-stack-architecture: use create-doc with fullstack-architecture-tmpl.yaml
|
|
61
58
|
- doc-out: Output full document to current destination file
|
|
62
59
|
- research {topic}: execute task create-deep-research-prompt
|
|
63
60
|
- yolo: Toggle YOLO / goal mode — run `bash .tfcore/utils/tf-yolo.sh on|off`, then operate per .tfcore/tasks/_yolo-mode.md: no confirmations or elicitation pauses (take the sensible default, record it), deletes + read-only git allowed (git writes never), run the command to completion. Also implied by the word YOLO in any command, an active /goal, or a tf-goal.sh run.
|
|
@@ -69,10 +66,4 @@ dependencies:
|
|
|
69
66
|
- technical-preferences.md
|
|
70
67
|
tasks:
|
|
71
68
|
- create-deep-research-prompt.md
|
|
72
|
-
- create-doc.md
|
|
73
|
-
templates:
|
|
74
|
-
- architecture-tmpl.yaml
|
|
75
|
-
- brownfield-architecture-tmpl.yaml
|
|
76
|
-
- front-end-architecture-tmpl.yaml
|
|
77
|
-
- fullstack-architecture-tmpl.yaml
|
|
78
69
|
```
|
|
@@ -13,6 +13,7 @@ Madhav, the TechieFlow master (icon 🪈): the one persona for building, fixing,
|
|
|
13
13
|
|
|
14
14
|
Each rule lives in one place; these lines only point at it.
|
|
15
15
|
|
|
16
|
+
- Everything the owner reads is plain, simple English, a question for the owner goes in `docs/{App}-Decision-Request.md` rather than into the conversation, and an upstream defect is always filed — answering "Blocks: yes|no" first, and never stopping the run when the answer is no: `.tfcore/tasks/_owner-language.md`.
|
|
16
17
|
- Run to completion in YOLO or goal mode: `.tfcore/tasks/_yolo-mode.md`. `*build-phase` is in it by default.
|
|
17
18
|
- Run the application yourself and never ask the owner to boot or test anything: `.tfcore/tasks/_smoke-test-policy.md`, with `bash .tfcore/utils/tf-build.sh` for building on any host.
|
|
18
19
|
- A smoke is not a verify. `Verified` is written only by an executed `*verify`; the hook refuses it otherwise. Your ceiling as a builder is `Implemented`.
|
|
@@ -13,6 +13,7 @@ Vidur, the TechieFlow verifier (icon 🔍): the one persona that proves whether
|
|
|
13
13
|
|
|
14
14
|
Each rule lives in one place; these lines only point at it.
|
|
15
15
|
|
|
16
|
+
- Everything the owner reads is plain, simple English, a question for the owner goes in `docs/{App}-Decision-Request.md` rather than into the conversation, and an upstream defect is always filed — answering "Blocks: yes|no" first, and never stopping the run when the answer is no: `.tfcore/tasks/_owner-language.md`.
|
|
16
17
|
- Run to completion: `.tfcore/tasks/_yolo-mode.md`. `*verify` is in YOLO by default.
|
|
17
18
|
- Boot the application yourself, on this machine, and never ask the owner to start or test anything: `.tfcore/tasks/_smoke-test-policy.md` and `bash .tfcore/utils/tf-verify-boot.sh`.
|
|
18
19
|
- Evidence over assertion. A row is `Verified` only when every check that ran passed, and the verdict script writes it from the evidence files; a `Verified` written by hand is refused by the hook. A check that could not run is written as not measured, never as a pass.
|
|
@@ -85,7 +85,9 @@ if ! command -v python3 >/dev/null 2>&1; then
|
|
|
85
85
|
fi
|
|
86
86
|
|
|
87
87
|
# Verdicts: ALLOW | ASK_RM | BLOCK_WRITE | BLOCK_READ
|
|
88
|
-
|
|
88
|
+
# The program is read into a variable first: bash 3.2 (a stock Mac) cannot parse this program as a
|
|
89
|
+
# here-document inside $( ), because it scans the text for quotes and brackets.
|
|
90
|
+
IFS= read -r -d '' GIT_GUARD_PY <<'PY' || true
|
|
89
91
|
import json, os, re, shlex, sys
|
|
90
92
|
|
|
91
93
|
raw = os.environ.get("TF_HOOK_INPUT", "")
|
|
@@ -283,7 +285,7 @@ elif (found_rm[0] or found_sudo[0]) and not yolo:
|
|
|
283
285
|
else:
|
|
284
286
|
print("ALLOW")
|
|
285
287
|
PY
|
|
286
|
-
)"
|
|
288
|
+
VERDICT="$(TF_HOOK_INPUT="$INPUT" TF_YOLO_ON="$YOLO" python3 -c "$GIT_GUARD_PY" 2>/dev/null </dev/null)"
|
|
287
289
|
|
|
288
290
|
case "$VERDICT" in
|
|
289
291
|
BLOCK_WRITE) block_git_msg; exit 2 ;;
|
|
@@ -59,8 +59,12 @@ if not re.search(BUILD, low):
|
|
|
59
59
|
sys.exit(0)
|
|
60
60
|
|
|
61
61
|
bg_flag = bool(ti.get("run_in_background"))
|
|
62
|
+
# A quoted argument is data, not shell: `--screen "Scorecard & Portfolio=/x"` backgrounds nothing
|
|
63
|
+
# (AppManager TF-025). A quoted string that itself names a build stays, so
|
|
64
|
+
# `powershell.exe -Command "dotnet run &"` is still read.
|
|
65
|
+
bare = re.sub(r"\"[^\"]*\"|'[^']*'", lambda m: m.group(0) if re.search(BUILD, m.group(0)) else '""', low)
|
|
62
66
|
# `&` that is not `&&` and not inside `2>&1` / `>&2`
|
|
63
|
-
inline_bg = bool(re.search(r"(?<![&>\d])&(?![&\d])",
|
|
67
|
+
inline_bg = bool(re.search(r"(?<![&>\d])&(?![&\d])", bare)) or bool(re.search(r"\b(nohup|setsid|disown)\b", bare))
|
|
64
68
|
if not (bg_flag or inline_bg):
|
|
65
69
|
sys.exit(0)
|
|
66
70
|
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
# mongosh with INSERT / UPDATE / DELETE / CREATE / ALTER / DROP / TRUNCATE / GRANT /
|
|
10
10
|
# REPLACE / MERGE in the command text, a -f / -i script, or stdin redirected from a file
|
|
11
11
|
# - dropping or resetting a database
|
|
12
|
-
# What it allows only while .tfcore/.session/phase.json says build-phase
|
|
12
|
+
# What it allows only while .tfcore/.session/phase.json says build-phase, fix-issues or
|
|
13
|
+
# triage-and-fix (whose step 3 runs the fix-issues steps)
|
|
13
14
|
# (written by bash .tfcore/utils/tf-phase.sh start <command>, step 0 of every task):
|
|
14
15
|
# - a migration runner: dotnet ef database update, dotnet run --project <…Db|…Migration(s)…>,
|
|
15
16
|
# dbup, flyway migrate, liquibase update, alembic upgrade, prisma migrate, knex migrate,
|
|
@@ -75,11 +76,12 @@ if direct_write or re.search(DROP_DB, low):
|
|
|
75
76
|
|
|
76
77
|
if re.search(MIGRATORS, low):
|
|
77
78
|
ph = phase()
|
|
78
|
-
|
|
79
|
+
# triage-and-fix step 3 is fix-issues steps 2 to 5 under its own marker (AppManager TF-027)
|
|
80
|
+
if ph in ("build-phase", "fix-issues", "triage-and-fix"):
|
|
79
81
|
sys.exit(0)
|
|
80
82
|
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
|
|
82
|
-
f"*fix-issues, and {who}. Day-1, mockups, split-brd, amend-docs, devguide, verify, "
|
|
83
|
+
print("BLOCKED by TechieFlow policy: a database migration runs only from *build-phase, "
|
|
84
|
+
f"*fix-issues or *triage-and-fix, and {who}. Day-1, mockups, split-brd, amend-docs, devguide, verify, "
|
|
83
85
|
"triage and status commands write documents and records, never the database. If you are "
|
|
84
86
|
"inside build-phase or fix-issues, step 0 was skipped: run "
|
|
85
87
|
"bash .tfcore/utils/tf-phase.sh start <command> <App> and retry. Owner rule 2026-09-05.",
|
|
@@ -27,6 +27,12 @@ except Exception:
|
|
|
27
27
|
|
|
28
28
|
ti = data.get("tool_input") or {}
|
|
29
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
|
+
|
|
30
36
|
MSG = ("BLOCKED by TechieFlow policy: docs/metrics/*.jsonl are append-only telemetry "
|
|
31
37
|
"streams written only through bash .tfcore/utils/tf-emit.sh <stream> (stdin JSON). "
|
|
32
38
|
"Never Write, Edit, redirect or copy onto them. A wrong record is corrected by a "
|
|
@@ -44,7 +50,13 @@ if isinstance(cmd, str) and re.search(STREAM, cmd, re.I):
|
|
|
44
50
|
r"|(\btee\b[^|;&]*" + STREAM + r")"
|
|
45
51
|
r"|(\b(cp|mv|install)\b[^|;&]*" + STREAM + r")"
|
|
46
52
|
r"|(\b(sed|perl)\b[^|;&]*\s-[a-zA-Z]*i[a-zA-Z]*\b[^|;&]*" + STREAM + r")"
|
|
47
|
-
|
|
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")"
|
|
48
60
|
r"|(\b(truncate|dd)\b[^|;&]*" + STREAM + r")",
|
|
49
61
|
cmd, re.I,
|
|
50
62
|
):
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
# Only when PROJECT-STATUS.md was written in this session (newer than the
|
|
11
11
|
# session pointer .tfcore/.session/<harness>.json):
|
|
12
12
|
# 2. tf-doc-check.sh FAILs on PROJECT-STATUS.md -> fix the file, re-run the check
|
|
13
|
+
# 2c. a BRD or UI design written this session leaves a screen with no row, UI design
|
|
14
|
+
# entry or mockup -> add it, re-run the check (TF-030)
|
|
13
15
|
# 3. docs/<App>-BRD.md older than docs/<App>-Checklist.md
|
|
14
16
|
# -> bash .tfcore/utils/tf-brd-status.sh <App>
|
|
15
17
|
# 4. no runs.jsonl record after the status write -> the run record (_metrics-emit-gate.md)
|
|
18
|
+
# Only on the turn that wrote PROJECT-STATUS.md (a command's closing turn):
|
|
19
|
+
# 5. tf-owner-text.sh FAILs on the closing message or a free-form document it
|
|
20
|
+
# hands the owner -> rewrite it for the owner
|
|
16
21
|
#
|
|
17
22
|
# Wired in .claude/settings.json -> hooks.Stop; OpenCode via
|
|
18
23
|
# .opencode/plugin/techieflow.js session.idle (a nudge: OpenCode has no blocking
|
|
@@ -28,6 +33,7 @@ import datetime
|
|
|
28
33
|
import glob
|
|
29
34
|
import json
|
|
30
35
|
import os
|
|
36
|
+
import re
|
|
31
37
|
import subprocess
|
|
32
38
|
import sys
|
|
33
39
|
|
|
@@ -58,10 +64,18 @@ elif md_mtime > os.path.getmtime(html):
|
|
|
58
64
|
def session_start():
|
|
59
65
|
"""mtime of the harness's session pointer; None when there is none (checks 2-4 skipped)."""
|
|
60
66
|
d = os.path.join(root, ".tfcore", ".session")
|
|
61
|
-
|
|
67
|
+
# The harness in the order tf-harness.sh and tf-emit.sh take it: TF_HARNESS (the OpenCode
|
|
68
|
+
# plugin sets it on every guard it spawns), then Claude Code's own variables, then OPENCODE*.
|
|
69
|
+
# OpenCode first let any OPENCODE_* in the owner's shell (an API key, a config path) send a
|
|
70
|
+
# Claude Code turn to opencode.json; with no such file, checks 2-5 never ran (2026-09-27).
|
|
71
|
+
tf = os.environ.get("TF_HARNESS")
|
|
72
|
+
if tf == "opencode":
|
|
62
73
|
cands = [os.path.join(d, "opencode.json")]
|
|
63
|
-
elif
|
|
74
|
+
elif tf == "claude-code" or any(os.environ.get(k) for k in (
|
|
75
|
+
"CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_PROJECT_DIR")):
|
|
64
76
|
cands = [os.path.join(d, "claude-code.json")]
|
|
77
|
+
elif any(k.startswith("OPENCODE") for k in os.environ):
|
|
78
|
+
cands = [os.path.join(d, "opencode.json")]
|
|
65
79
|
else:
|
|
66
80
|
cands = [os.path.join(d, "claude-code.json"), os.path.join(d, "opencode.json")]
|
|
67
81
|
times = [os.path.getmtime(p) for p in cands if os.path.isfile(p)]
|
|
@@ -104,6 +118,31 @@ if written_this_session:
|
|
|
104
118
|
except Exception:
|
|
105
119
|
pass
|
|
106
120
|
|
|
121
|
+
# 2c. a screen is not finished until it has its row, its UI design entry and its mockup. An
|
|
122
|
+
# amendment added a Price providers screen inside a requirement with none of the three, the
|
|
123
|
+
# checker's one finding was on the checklist and passed as old, and the page was built with no
|
|
124
|
+
# design (TfLens TF-030, 2026-09-11). On a turn that wrote a BRD or a UI design, those findings
|
|
125
|
+
# block; a BRD's other findings stay with *amend-docs, as before.
|
|
126
|
+
try:
|
|
127
|
+
chk = os.path.join(root, ".tfcore", "utils", "tf-doc-check.sh")
|
|
128
|
+
chain = re.compile(r'names the screen "|has no "### Screen:" entry|is in the UIDesign but not in the BRD'
|
|
129
|
+
r'|has no mockup link \(docs/mockups')
|
|
130
|
+
apps = set()
|
|
131
|
+
for f in glob.glob(os.path.join(root, "docs", "*.md")):
|
|
132
|
+
m = re.match(r"^(.+?)-(?:P\d+-)?(?:BRD|UIDesign)\.md$", os.path.basename(f))
|
|
133
|
+
if m and os.path.getmtime(f) >= start:
|
|
134
|
+
apps.add(m.group(1))
|
|
135
|
+
for app in sorted(apps) if os.path.isfile(chk) else ():
|
|
136
|
+
out = subprocess.run(["bash", chk, "--quiet", "--app", app], cwd=root,
|
|
137
|
+
capture_output=True, text=True, timeout=180).stdout
|
|
138
|
+
fails = [l for l in out.splitlines() if l.startswith("FAIL") and chain.search(l)]
|
|
139
|
+
if fails:
|
|
140
|
+
problems.append(f"a screen in {app}'s documents has no row, UI design entry or mockup ({len(fails)} line(s)). "
|
|
141
|
+
f"Add what is missing, then run: bash .tfcore/utils/tf-doc-check.sh --app {app}")
|
|
142
|
+
problems.extend(" " + l for l in fails[:5])
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
107
146
|
# 3. the BRD's Development status table is not older than the checklist
|
|
108
147
|
try:
|
|
109
148
|
for cl in glob.glob(os.path.join(root, "docs", "*-Checklist.md")):
|
|
@@ -136,6 +175,51 @@ if written_this_session:
|
|
|
136
175
|
except Exception:
|
|
137
176
|
pass
|
|
138
177
|
|
|
178
|
+
# 5. what the owner reads when a command ends: the closing message, and every free-form
|
|
179
|
+
# document it hands over that this turn wrote, through tf-owner-text.sh. TfLens's hand-off of
|
|
180
|
+
# 2026-09-11 reached the owner in jargon, named upstream problems with no word on what they
|
|
181
|
+
# touch, called two fixed ones open, and gave no prompt (MISS-TechieFlow-20260911-03). Only on
|
|
182
|
+
# the turn that closed a command -- PROJECT-STATUS written after this turn's prompt -- so an
|
|
183
|
+
# ordinary conversation is never held to a hand-off's shape. Claude Code hands the hook the
|
|
184
|
+
# message; the OpenCode plugin passes the same two fields.
|
|
185
|
+
try:
|
|
186
|
+
message = data.get("last_assistant_message") or ""
|
|
187
|
+
turn = data.get("turn_started")
|
|
188
|
+
if turn is None and data.get("transcript_path") and os.path.isfile(data["transcript_path"]):
|
|
189
|
+
with open(data["transcript_path"], encoding="utf-8", errors="replace") as f:
|
|
190
|
+
for line in f:
|
|
191
|
+
try:
|
|
192
|
+
r = json.loads(line)
|
|
193
|
+
except Exception:
|
|
194
|
+
continue
|
|
195
|
+
c = (r.get("message") or {}).get("content")
|
|
196
|
+
prompt = r.get("type") == "user" and not r.get("isMeta") and (
|
|
197
|
+
isinstance(c, str) or (isinstance(c, list) and any(isinstance(x, dict) and x.get("type") == "text" for x in c)))
|
|
198
|
+
if prompt and r.get("timestamp"):
|
|
199
|
+
turn = r["timestamp"]
|
|
200
|
+
if isinstance(turn, str):
|
|
201
|
+
turn = datetime.datetime.strptime(turn[:19], "%Y-%m-%dT%H:%M:%S").replace(
|
|
202
|
+
tzinfo=datetime.timezone.utc).timestamp()
|
|
203
|
+
elif isinstance(turn, (int, float)) and turn > 1e12:
|
|
204
|
+
turn = turn / 1000.0
|
|
205
|
+
chk = os.path.join(root, ".tfcore", "utils", "tf-owner-text.sh")
|
|
206
|
+
if message.strip() and turn and md_mtime >= turn - 2 and os.path.isfile(chk):
|
|
207
|
+
handed = []
|
|
208
|
+
for m in set(re.findall(r"(?<![\w/.-])((?:docs/)?[\w.-]+\.md)\b", message)):
|
|
209
|
+
p = os.path.join(root, m if m.startswith("docs/") else os.path.join("docs", m))
|
|
210
|
+
if os.path.isfile(p) and os.path.getmtime(p) >= turn - 2:
|
|
211
|
+
handed.append(os.path.relpath(p, root))
|
|
212
|
+
res = subprocess.run(["bash", chk, "--root", root, "--final", "--stdin"] + sorted(handed), cwd=root,
|
|
213
|
+
input=message, capture_output=True, text=True, timeout=60)
|
|
214
|
+
fails = [l for l in res.stdout.splitlines() if l.startswith("FAIL")]
|
|
215
|
+
if fails:
|
|
216
|
+
problems.append(f"what you are handing the owner is not written for the owner ({len(fails)} line(s)). "
|
|
217
|
+
"Fix each, then write the closing message again — plain words, every upstream problem "
|
|
218
|
+
"with what it affects and its prompt, and the next prompt in a code block:")
|
|
219
|
+
problems.extend(" " + l for l in fails[:8])
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
139
223
|
if not problems:
|
|
140
224
|
sys.exit(0)
|
|
141
225
|
|