@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.
Files changed (150) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -10
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  8. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  9. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  12. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  13. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  14. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
  15. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  16. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  17. package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
  18. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
  19. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  20. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  21. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  22. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  23. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  24. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  25. package/.opencode/plugin/techieflow.js +181 -9
  26. package/.tfcore/agents/analyst.md +5 -8
  27. package/.tfcore/agents/architect.md +1 -10
  28. package/.tfcore/agents/flow-master.md +1 -0
  29. package/.tfcore/agents/verifier.md +1 -0
  30. package/.tfcore/hooks/block-git.sh +4 -2
  31. package/.tfcore/hooks/guard-build.sh +5 -1
  32. package/.tfcore/hooks/guard-db.sh +6 -4
  33. package/.tfcore/hooks/guard-metrics.sh +13 -1
  34. package/.tfcore/hooks/guard-status-html.sh +86 -2
  35. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  36. package/.tfcore/hooks/guard-verify.sh +8 -3
  37. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  38. package/.tfcore/routing.yaml +61 -0
  39. package/.tfcore/standards/owner-words.txt +23 -0
  40. package/.tfcore/tasks/_owner-language.md +27 -0
  41. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  42. package/.tfcore/tasks/_status-update-gate.md +4 -3
  43. package/.tfcore/tasks/amend-docs.md +6 -5
  44. package/.tfcore/tasks/build-phase.md +1 -1
  45. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  46. package/.tfcore/tasks/create-doc.md +4 -0
  47. package/.tfcore/tasks/create-project-brief.md +22 -0
  48. package/.tfcore/tasks/day1-brownfield.md +3 -3
  49. package/.tfcore/tasks/day1-greenfield.md +3 -3
  50. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  51. package/.tfcore/tasks/fix-issues.md +2 -2
  52. package/.tfcore/tasks/generate-html.md +7 -3
  53. package/.tfcore/tasks/handoff-phase.md +20 -18
  54. package/.tfcore/tasks/metrics-report.md +2 -0
  55. package/.tfcore/tasks/mockups.md +1 -1
  56. package/.tfcore/tasks/productguide.md +2 -2
  57. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  58. package/.tfcore/tasks/triage-issues.md +2 -2
  59. package/.tfcore/tasks/verify-phase.md +1 -1
  60. package/.tfcore/telemetry/SCHEMA.md +61 -1
  61. package/.tfcore/telemetry/install-metrics.sh +3 -2
  62. package/.tfcore/telemetry/tf-metrics.sh +435 -21
  63. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  64. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  65. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  66. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  67. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  68. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  69. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  70. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  71. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  72. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  73. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  74. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  75. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  76. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  77. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  78. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  79. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  80. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  81. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  82. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  83. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  84. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  90. package/.tfcore/utils/tf-assets.sh +97 -12
  91. package/.tfcore/utils/tf-brd-status.py +14 -5
  92. package/.tfcore/utils/tf-build-list.py +99 -11
  93. package/.tfcore/utils/tf-build-list.sh +5 -3
  94. package/.tfcore/utils/tf-build.sh +88 -15
  95. package/.tfcore/utils/tf-ci-repro.py +685 -0
  96. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  97. package/.tfcore/utils/tf-devguide-list.py +47 -22
  98. package/.tfcore/utils/tf-doc-check.py +242 -10
  99. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  100. package/.tfcore/utils/tf-emit.sh +413 -19
  101. package/.tfcore/utils/tf-feedback.sh +13 -0
  102. package/.tfcore/utils/tf-fix-close.sh +113 -31
  103. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  104. package/.tfcore/utils/tf-goal.sh +105 -18
  105. package/.tfcore/utils/tf-harness.sh +9 -4
  106. package/.tfcore/utils/tf-lock.sh +29 -0
  107. package/.tfcore/utils/tf-log-miss.py +34 -8
  108. package/.tfcore/utils/tf-login.mjs +167 -0
  109. package/.tfcore/utils/tf-misses-md.py +24 -21
  110. package/.tfcore/utils/tf-misses-md.sh +5 -4
  111. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  112. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  113. package/.tfcore/utils/tf-model-pick.sh +475 -0
  114. package/.tfcore/utils/tf-owner-text.py +233 -0
  115. package/.tfcore/utils/tf-owner-text.sh +14 -0
  116. package/.tfcore/utils/tf-phase.sh +33 -1
  117. package/.tfcore/utils/tf-portable.sh +230 -0
  118. package/.tfcore/utils/tf-render-html.py +80 -2
  119. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  120. package/.tfcore/utils/tf-routing.sh +7 -0
  121. package/.tfcore/utils/tf-selfcheck.sh +356 -0
  122. package/.tfcore/utils/tf-split-brd.py +17 -2
  123. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  124. package/.tfcore/utils/tf-status-facts.py +133 -44
  125. package/.tfcore/utils/tf-triage.py +62 -16
  126. package/.tfcore/utils/tf-triage.sh +2 -0
  127. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  128. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  129. package/.tfcore/utils/tf-verify-env.sh +33 -4
  130. package/.tfcore/utils/tf-verify-list.py +75 -8
  131. package/.tfcore/utils/tf-verify-screens.mjs +254 -43
  132. package/.tfcore/utils/tf-verify-screens.sh +18 -4
  133. package/.tfcore/utils/tf-verify-tests.sh +272 -28
  134. package/.tfcore/utils/tf-verify-verdict.py +59 -11
  135. package/.tfcore/utils/tf_defect.py +27 -0
  136. package/.tfcore/utils/tf_feedback.py +209 -0
  137. package/.tfcore/utils/tf_roadmap.py +35 -0
  138. package/README.md +19 -5
  139. package/docs/TechieFlow-Installation.md +5 -3
  140. package/opencode.jsonc +24 -0
  141. package/package.json +1 -1
  142. package/scripts/install.mjs +4 -0
  143. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  144. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  145. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  146. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  147. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  148. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  149. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  150. 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} -> guard-status-html.sh
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
- export const TechieFlowPlugin = async ({ directory, client }) => {
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 Playwright/MAUI/Appium harness is already set up - run it yourself; never hand the boot to the owner (_smoke-test-policy.md banned excuses).
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 TrBlazeUI (reads the TrBlazeUI catalog first; replicable by construction). The visual contract the build matches + the verifier diffs against. Runs task mockups.md.
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-competitor-analysis: use task create-doc with competitor-analysis-tmpl.yaml
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
- VERDICT="$(TF_HOOK_INPUT="$INPUT" TF_YOLO_ON="$YOLO" python3 - <<'PY' 2>/dev/null
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])", low)) or bool(re.search(r"\b(nohup|setsid|disown)\b", low))
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 or fix-issues
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
- if ph in ("build-phase", "fix-issues"):
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 or "
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
- r"|(\b(python3?|node)\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")"
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
- if any(k.startswith("OPENCODE") for k in os.environ):
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 os.environ.get("CLAUDECODE") or any(k.startswith("CLAUDE_CODE") for k in os.environ):
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