@techierathore/techieflow 1.0.4 → 1.1.1

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 (148) hide show
  1. package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
  2. package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
  3. package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
  4. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  6. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  9. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
  10. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  11. package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
  12. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  14. package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
  15. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  16. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  17. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  18. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  19. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  20. package/.opencode/plugin/techieflow.js +194 -14
  21. package/.tfcore/agents/flow-master.md +2 -0
  22. package/.tfcore/agents/verifier.md +2 -2
  23. package/.tfcore/core-config.yaml +2 -1
  24. package/.tfcore/hooks/block-git.sh +4 -2
  25. package/.tfcore/hooks/guard-build.sh +5 -1
  26. package/.tfcore/hooks/guard-db.sh +112 -13
  27. package/.tfcore/hooks/guard-status-html.sh +86 -2
  28. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  29. package/.tfcore/hooks/guard-verify.sh +8 -3
  30. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  31. package/.tfcore/hooks/yolo-allow.sh +27 -0
  32. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  33. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  34. package/.tfcore/standards/owner-words.txt +23 -0
  35. package/.tfcore/tasks/_develop-mode.md +34 -0
  36. package/.tfcore/tasks/_owner-language.md +1 -0
  37. package/.tfcore/tasks/_status-update-gate.md +4 -3
  38. package/.tfcore/tasks/_yolo-mode.md +2 -2
  39. package/.tfcore/tasks/amend-docs.md +2 -2
  40. package/.tfcore/tasks/day1-brownfield.md +3 -3
  41. package/.tfcore/tasks/day1-greenfield.md +5 -5
  42. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  43. package/.tfcore/tasks/fix-issues.md +3 -2
  44. package/.tfcore/tasks/generate-html.md +2 -2
  45. package/.tfcore/tasks/handoff-phase.md +2 -2
  46. package/.tfcore/tasks/mockups.md +2 -2
  47. package/.tfcore/tasks/productguide.md +2 -2
  48. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  49. package/.tfcore/tasks/triage-and-fix.md +2 -2
  50. package/.tfcore/tasks/triage-issues.md +2 -2
  51. package/.tfcore/tasks/verify-phase.md +12 -11
  52. package/.tfcore/telemetry/SCHEMA.md +10 -2
  53. package/.tfcore/telemetry/install-metrics.sh +3 -2
  54. package/.tfcore/telemetry/tf-metrics.sh +38 -2
  55. package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
  56. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  57. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  58. package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
  59. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  60. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  61. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  62. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  63. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  64. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  65. package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
  66. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  67. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  68. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  69. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  70. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  71. package/.tfcore/utils/tf-appium.mjs +179 -0
  72. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  73. package/.tfcore/utils/tf-assets.sh +97 -12
  74. package/.tfcore/utils/tf-brd-status.py +14 -5
  75. package/.tfcore/utils/tf-build-list.py +95 -12
  76. package/.tfcore/utils/tf-build-list.sh +2 -1
  77. package/.tfcore/utils/tf-build.sh +163 -15
  78. package/.tfcore/utils/tf-checklist-edit.py +21 -3
  79. package/.tfcore/utils/tf-ci-repro.py +685 -0
  80. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  81. package/.tfcore/utils/tf-day1-files.py +55 -28
  82. package/.tfcore/utils/tf-develop.sh +264 -0
  83. package/.tfcore/utils/tf-devguide-list.py +53 -24
  84. package/.tfcore/utils/tf-doc-check.py +218 -26
  85. package/.tfcore/utils/tf-doc-tests.sh +116 -0
  86. package/.tfcore/utils/tf-emit.sh +132 -45
  87. package/.tfcore/utils/tf-feedback.sh +13 -0
  88. package/.tfcore/utils/tf-fix-close.sh +87 -31
  89. package/.tfcore/utils/tf-goal.sh +12 -12
  90. package/.tfcore/utils/tf-harness.sh +9 -4
  91. package/.tfcore/utils/tf-lock.sh +29 -0
  92. package/.tfcore/utils/tf-log-miss.py +34 -8
  93. package/.tfcore/utils/tf-login.mjs +167 -0
  94. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  95. package/.tfcore/utils/tf-menus.mjs +401 -0
  96. package/.tfcore/utils/tf-misses-md.py +10 -4
  97. package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
  98. package/.tfcore/utils/tf-mockup-parity.sh +25 -2
  99. package/.tfcore/utils/tf-owner-text.py +233 -0
  100. package/.tfcore/utils/tf-owner-text.sh +14 -0
  101. package/.tfcore/utils/tf-phase.sh +38 -4
  102. package/.tfcore/utils/tf-portable.sh +230 -0
  103. package/.tfcore/utils/tf-render-html.py +79 -5
  104. package/.tfcore/utils/tf-req-ids.py +36 -0
  105. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  106. package/.tfcore/utils/tf-split-brd.py +11 -1
  107. package/.tfcore/utils/tf-stack-check.sh +3 -0
  108. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  109. package/.tfcore/utils/tf-status-facts.py +160 -45
  110. package/.tfcore/utils/tf-theme.mjs +189 -0
  111. package/.tfcore/utils/tf-triage.py +74 -17
  112. package/.tfcore/utils/tf-triage.sh +2 -0
  113. package/.tfcore/utils/tf-verify-boot.sh +529 -54
  114. package/.tfcore/utils/tf-verify-emit.sh +57 -17
  115. package/.tfcore/utils/tf-verify-env.sh +33 -4
  116. package/.tfcore/utils/tf-verify-list.py +290 -24
  117. package/.tfcore/utils/tf-verify-list.sh +3 -1
  118. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  119. package/.tfcore/utils/tf-verify-screens.mjs +341 -44
  120. package/.tfcore/utils/tf-verify-screens.sh +23 -5
  121. package/.tfcore/utils/tf-verify-tests.sh +310 -19
  122. package/.tfcore/utils/tf-verify-verdict.py +163 -21
  123. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  124. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  125. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  126. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  127. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  128. package/.tfcore/utils/tf-yolo.sh +31 -3
  129. package/.tfcore/utils/tf_defect.py +27 -0
  130. package/.tfcore/utils/tf_feedback.py +272 -0
  131. package/.tfcore/utils/tf_roadmap.py +35 -0
  132. package/.tfcore/utils/tf_stack_check.py +93 -0
  133. package/README.md +6 -2
  134. package/docs/TechieFlow-Installation.md +5 -3
  135. package/opencode.jsonc +6 -2
  136. package/package.json +1 -1
  137. package/scripts/install.mjs +13 -2
  138. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  139. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  140. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  141. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  142. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  143. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  144. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  145. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  146. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  147. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  148. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
@@ -21,7 +21,7 @@
21
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")
@@ -85,6 +96,15 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
85
96
  }
86
97
  }
87
98
 
99
+ // A flag that expired or went away without `tf-yolo.sh off` leaves the never-ask permissions
100
+ // behind (the rewritten OpenCode config): put it back
101
+ // when the project opens and YOLO is off (tf-yolo.sh sync; owner, 2026-10-14).
102
+ try {
103
+ if (!yoloOn() && fs.existsSync(path.join(root, ".tfcore", ".session", "yolo-perms.json"))) {
104
+ spawnSync("bash", [path.join(root, ".tfcore", "utils", "tf-yolo.sh"), "sync"], { cwd: root, env: { ...process.env, TF_PROJECT_DIR: root }, timeout: 15000 })
105
+ }
106
+ } catch {}
107
+
88
108
  // Returns the guard's stderr when the guard BLOCKS (exit 2), else null.
89
109
  function guardBlocks(script, payload) {
90
110
  try {
@@ -110,8 +130,29 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
110
130
  const htmlNudged = Object.create(null) // rootID -> true once the stale-HTML nudge was sent
111
131
  let pointerWritten = false
112
132
 
133
+ // What Claude Code hands its Stop hook and OpenCode does not: the closing message, and when
134
+ // the turn began (the last prompt the owner typed). The guard checks the message the owner
135
+ // reads at the end of a command (tf-owner-text.sh). Empty on any failure: the check then skips.
136
+ async function closingTurn(rootID) {
137
+ try {
138
+ if (typeof client.session.messages !== "function") return {}
139
+ const res = await client.session.messages({ path: { id: rootID } })
140
+ const list = Array.isArray(res) ? res : (res && res.data) || []
141
+ const text = (m) => (m.parts || []).filter((x) => x.type === "text" && !x.synthetic).map((x) => x.text).join("\n")
142
+ let message = "", turn = null
143
+ for (const m of list) {
144
+ const info = m.info || {}
145
+ if (info.role === "user" && text(m).trim()) turn = (info.time || {}).created || turn
146
+ if (info.role === "assistant" && text(m).trim()) message = text(m)
147
+ }
148
+ return turn ? { last_assistant_message: message, turn_started: turn } : {}
149
+ } catch {
150
+ return {}
151
+ }
152
+ }
153
+
113
154
  // Stop-hook analogue for guard-status-html.sh (see header). Never throws.
114
- function nudgeStaleStatusHtml(rootID) {
155
+ async function nudgeStaleStatusHtml(rootID) {
115
156
  try {
116
157
  if (!client || !client.session || typeof client.session.prompt !== "function") return
117
158
  const msg = guardBlocks("guard-status-html.sh", {
@@ -119,6 +160,7 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
119
160
  cwd: root,
120
161
  session_id: rootID,
121
162
  stop_hook_active: htmlNudged[rootID] === true,
163
+ ...(htmlNudged[rootID] === true ? {} : await closingTurn(rootID)),
122
164
  })
123
165
  if (!msg) return
124
166
  htmlNudged[rootID] = true
@@ -232,14 +274,16 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
232
274
  const args = output.args || {}
233
275
  let payload = null
234
276
  let scripts = []
235
- if (input.tool === "bash") {
277
+ if (input.tool === "bash" || input.tool === "shell") {
278
+ // 1.x calls it bash; 2.x calls it shell (same `command` argument)
236
279
  payload = { tool_name: "Bash", tool_input: { command: String(args.command || ""), run_in_background: !!(args.background || args.run_in_background) } }
237
280
  scripts = ["block-git.sh", "guard-artifacts.sh", "guard-status.sh", "guard-metrics.sh", "guard-db.sh", "guard-build.sh", "guard-verify-deps.sh"]
238
281
  } else if (input.tool === "edit") {
282
+ // 1.x names the file filePath, 2.x names it path
239
283
  payload = {
240
284
  tool_name: "Edit",
241
285
  tool_input: {
242
- file_path: String(args.filePath || ""),
286
+ file_path: String(args.filePath || args.path || ""),
243
287
  old_string: String(args.oldString || ""),
244
288
  new_string: String(args.newString || ""),
245
289
  },
@@ -248,7 +292,7 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
248
292
  } else if (input.tool === "write") {
249
293
  payload = {
250
294
  tool_name: "Write",
251
- tool_input: { file_path: String(args.filePath || ""), content: String(args.content || "") },
295
+ tool_input: { file_path: String(args.filePath || args.path || ""), content: String(args.content || "") },
252
296
  }
253
297
  scripts = ["guard-status.sh", "guard-verify.sh", "guard-metrics.sh"]
254
298
  } else if (input.tool === "apply_patch") {
@@ -299,17 +343,16 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
299
343
  }
300
344
  },
301
345
 
302
- // YOLO / goal mode (.tfcore/tasks/_yolo-mode.md): when the flag is on, the
303
- // `rm * / rmdir * / sudo *` asks from the permission map are auto-approved so
304
- // an unattended run never stalls on a delete. Git WRITES never reach this
305
- // hook — they are `deny` in the map and exit-2 in block-git.sh. Fail-open:
306
- // if the harness never calls permission.ask, nothing changes.
346
+ // YOLO / goal mode (.tfcore/tasks/_yolo-mode.md): when the flag is on, every
347
+ // ask is approved — a delete, an edit, a web fetch, a folder outside the project
348
+ // — so nothing asks (owner, 2026-10-14; it approved shell asks only until then).
349
+ // Git WRITES never reach this hook — they are `deny` in the map and exit-2 in
350
+ // block-git.sh. Fail-open: if the harness never calls permission.ask (OpenCode 2),
351
+ // the config tf-yolo.sh rewrites (every ask made allow) does the same job.
307
352
  "permission.ask": async (input, output) => {
308
353
  try {
309
354
  if (!active || !output) return
310
355
  if (!yoloOn()) return
311
- const type = String((input && input.type) || "")
312
- if (type && type !== "bash") return
313
356
  output.status = "allow"
314
357
  } catch {}
315
358
  },
@@ -367,7 +410,7 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
367
410
  if (!sid) return
368
411
  if (rootOf(sid) === sid) {
369
412
  emitSession(sid)
370
- nudgeStaleStatusHtml(sid)
413
+ await nudgeStaleStatusHtml(sid)
371
414
  }
372
415
  return
373
416
  }
@@ -375,3 +418,140 @@ export const TechieFlowPlugin = async ({ directory, client }) => {
375
418
  },
376
419
  }
377
420
  }
421
+
422
+ // ---- OpenCode 2.x: translate its API into the 1.x hooks above ----
423
+ // The 2.x context names no project directory, and its background service does not run in the
424
+ // project, so the root is where this file sits: <root>/.opencode/plugin/techieflow.js.
425
+ function projectRoot() {
426
+ try {
427
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..")
428
+ } catch {
429
+ return process.cwd()
430
+ }
431
+ }
432
+
433
+ function eventTime(value) {
434
+ const n = typeof value === "number" ? value : Date.parse(String(value || ""))
435
+ return Number.isFinite(n) ? n : Date.now()
436
+ }
437
+
438
+ async function setupV2(context) {
439
+ const root = projectRoot()
440
+ // What the Stop nudge needs from a client: prompt a session. 2.x has no message listing, so
441
+ // closingTurn() finds no messages and the owner-text check is skipped (see the header).
442
+ const client = {
443
+ session: {
444
+ prompt: ({ path: p, body }) =>
445
+ context.session.prompt({
446
+ sessionID: p.id,
447
+ text: ((body && body.parts) || []).map((x) => x.text || "").join("\n"),
448
+ }),
449
+ },
450
+ }
451
+ const hooks = await TechieFlowPlugin({ directory: root, client })
452
+ const registrations = []
453
+
454
+ // Guards. A throw here stops the call, as in 1.x.
455
+ registrations.push(
456
+ await context.tool.hook("execute.before", async (event) => {
457
+ await hooks["tool.execute.before"]({ tool: event.tool, sessionID: event.sessionID, callID: event.id }, { args: event.input || {} })
458
+ }),
459
+ )
460
+
461
+ // TF_HARNESS, TF_PROJECT_DIR, TF_YOLO for the agent's shell commands.
462
+ registrations.push(
463
+ await context.shell.hook("create.before", async (event) => {
464
+ if (!event || !event.env) return
465
+ await hooks["shell.env"]({}, { env: event.env })
466
+ }),
467
+ )
468
+
469
+ // Telemetry and the Stop nudge: 2.x events rewritten as the 1.x events the handler reads.
470
+ // A step carries its own tokens and cost; each step is recorded once, under its own key.
471
+ // The session is created before this subscription starts, so no session.created arrives for
472
+ // it: a session is learned the first time an event of this project names it, and its parent
473
+ // is read with session.get (a root session has no parentID). A turn ends with
474
+ // session.execution.*, which carries no location, so only sessions learned here count.
475
+ const models = Object.create(null) // assistantMessageID -> {model, t0}
476
+ const known = Object.create(null) // sessionID -> true once learned
477
+ let stream = null
478
+ let stopped = false
479
+ const learn = async (sessionID) => {
480
+ if (!sessionID || known[sessionID]) return
481
+ known[sessionID] = true
482
+ let parentID = null
483
+ try {
484
+ const info = await context.session.get({ sessionID })
485
+ parentID = (info && info.parentID) || null
486
+ } catch {}
487
+ await hooks.event({ event: { type: "session.created", properties: { info: { id: sessionID, parentID } } } })
488
+ }
489
+ const translate = (ev) => {
490
+ const d = ev.data || {}
491
+ if (ev.type === "session.step.started") {
492
+ models[d.assistantMessageID] = { model: d.model || {}, t0: eventTime(ev.created) }
493
+ return null
494
+ }
495
+ if (ev.type === "session.step.ended") {
496
+ const started = models[d.assistantMessageID] || { model: {}, t0: eventTime(ev.created) }
497
+ const t = d.tokens || {}
498
+ return {
499
+ type: "message.updated",
500
+ properties: {
501
+ info: {
502
+ role: "assistant",
503
+ id: String(d.assistantMessageID) + ":" + String(ev.id),
504
+ sessionID: d.sessionID,
505
+ providerID: started.model.providerID,
506
+ modelID: started.model.id,
507
+ cost: d.cost,
508
+ tokens: { input: t.input, output: t.output, cache: t.cache || {} },
509
+ time: { created: started.t0, completed: eventTime(ev.created) },
510
+ },
511
+ },
512
+ }
513
+ }
514
+ const idle =
515
+ /^session\.execution\.(succeeded|failed|interrupted)$/.test(ev.type) ||
516
+ (ev.type === "session.status" && d.status && d.status.type === "idle") ||
517
+ ev.type === "session.idle"
518
+ if (idle && known[d.sessionID]) return { type: "session.idle", properties: { sessionID: d.sessionID } }
519
+ return null
520
+ }
521
+ ;(async () => {
522
+ try {
523
+ stream = context.event.subscribe()
524
+ for await (const ev of stream) {
525
+ if (stopped) break
526
+ try {
527
+ if (!ev) continue
528
+ if (ev.location && ev.location.directory) {
529
+ if (path.resolve(ev.location.directory) !== root) continue
530
+ if (ev.data && ev.data.sessionID) await learn(ev.data.sessionID)
531
+ }
532
+ const event = translate(ev)
533
+ if (event) await hooks.event({ event })
534
+ } catch {} // telemetry never blocks anything
535
+ }
536
+ } catch {}
537
+ })()
538
+
539
+ return async () => {
540
+ stopped = true
541
+ try {
542
+ if (stream && typeof stream.return === "function") await stream.return()
543
+ } catch {}
544
+ for (const r of registrations) {
545
+ try {
546
+ await r.dispose()
547
+ } catch {}
548
+ }
549
+ }
550
+ }
551
+
552
+ // OpenCode 1.x reads `server`; OpenCode 2.x reads `setup`. Each ignores the other.
553
+ export default {
554
+ id: "techieflow",
555
+ server: TechieFlowPlugin,
556
+ setup: setupV2,
557
+ }
@@ -17,6 +17,7 @@ Each rule lives in one place; these lines only point at it.
17
17
  - Run to completion in YOLO or goal mode: `.tfcore/tasks/_yolo-mode.md`. `*build-phase` is in it by default.
18
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.
19
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`.
20
+ - A next command you name is the one `tf-status-facts.sh` printed, phase included. Rows waiting in another phase's checklist get `*verify {scope} {App} --phase N`, or `--phase all` when several phases wait; a bare `*verify` grades `appPhase` only.
20
21
  - Analyse is not fix. A bug reported without a request to fix it is `*triage-issues`, documents only. Code changes come only from `*build-phase` and `*fix-issues`.
21
22
  - A miss is a record. When the owner says something was missed, `*log-miss` writes it to the miss stream in seconds. Never argue, never guess who caused it; the emitter resolves that.
22
23
  - Git is manual and the hook refuses it. Evidence is the checklist and the files on disk.
@@ -25,6 +26,7 @@ Each rule lives in one place; these lines only point at it.
25
26
 
26
27
  | Command | What it does | Task file |
27
28
  |---|---|---|
29
+ | `*develop-end-to-end {App} {brief}` | a new app from a brief to UAT, unattended: Day 1, build and verify, handoff; commits and pushes each phase; reports time and tokens per phase (greenfield only) | `develop-end-to-end.md` |
28
30
  | `*build-phase {App}` | builds every open row of the phase's checklist, smokes, chains the verifier, fixes what fails | `build-phase.md` |
29
31
  | `*fix-issues {App} {folder}` | reproduces reported bugs from screenshots and notes, fixes, re-verifies | `fix-issues.md` |
30
32
  | `*triage-issues {App} {evidence} [verify]` | reproduces and logs reported bugs; no code | `triage-issues.md` |
@@ -7,7 +7,7 @@ Vidur, the TechieFlow verifier (icon 🔍): the one persona that proves whether
7
7
  - A command starts with `*`. `*verify` maps to `.tfcore/tasks/verify-phase.md`; read that file when the command is typed, not before. Read `.tfcore/core-config.yaml` first for the app name, size, kind and phase.
8
8
  - On activation, say who you are and print the command table below once. `*help` prints it again.
9
9
  - The task begins with `bash .tfcore/utils/tf-phase.sh start verify-phase <App>` and ends with the status gate (`.tfcore/tasks/_status-update-gate.md`) and one run record.
10
- - "verify the UI" is `*verify ui`, "check everything" is `*verify all`, "re-check these rows" is `*verify REQ-UI-004,REQ-FN-011`. When the scope is unclear, ask once.
10
+ - "verify the UI" is `*verify ui`, "check everything" is `*verify all`, "re-check these rows" is `*verify REQ-UI-004,REQ-FN-011`, "verify phase 1" is `*verify all {App} --phase 1`, "every phase" is `--phase all`. When the scope is unclear, ask once.
11
11
 
12
12
  ## Standing rules
13
13
 
@@ -26,7 +26,7 @@ Each rule lives in one place; these lines only point at it.
26
26
 
27
27
  | Command | What it does | Task file |
28
28
  |---|---|---|
29
- | `*verify {scope} {App}` | boots the app, runs the seven checks on every row in scope, writes the verdicts and the telemetry | `verify-phase.md` |
29
+ | `*verify {scope} {App} [--phase N\|all]` | boots the app, runs the seven checks on every row in scope of that phase's checklist (default `appPhase`; `all` is each phase in turn, phase 1 first), writes the verdicts and the telemetry | `verify-phase.md` |
30
30
  | `*report {scope} {App}` | prints the current Status and Remarks of the rows in scope from the checklist, without running anything | |
31
31
  | `*help` | prints this table | |
32
32
  | `*exit` | leaves the persona | |
@@ -54,4 +54,5 @@ slashPrefix: TechieFlow
54
54
  # url: http://192.168.1.50:4723 # the LAN Mac (stable IP via DHCP reservation); Appium bound to 0.0.0.0
55
55
  # simulator: "iPhone 15"
56
56
  # maccatalyst:
57
- # url: http://192.168.1.50:4723 # same Mac, mac2 driver
57
+ # url: http://localhost:4723 # this Mac (the default when unset); mac2 driver. tf-verify-boot.sh
58
+ # # starts a local Appium when none answers. A Blazor Hybrid Mac head is checked without control names.
@@ -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,11 +9,16 @@
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,
16
17
  # rails db:migrate, sequelize db:migrate
18
+ # - code a python / node / deno / bun / ruby / perl / pwsh run executes, inline or from a script
19
+ # file, that uses a database library and holds a SQL write, unless every database path it names
20
+ # is a throw-away (tests/.artifacts/, /tmp, :memory:) (Sevak TF-002)
21
+ # A client or a SQL verb that is only mentioned (a grep pattern, a note written to a file) passes.
17
22
  # Reads (SELECT, \d, .tables, dumps to stdout) pass. YOLO does not relax this.
18
23
  #
19
24
  # Wired in .claude/settings.json → hooks.PreToolUse (matcher "Bash"); OpenCode via
@@ -54,17 +59,110 @@ def phase():
54
59
  except Exception:
55
60
  return None
56
61
 
57
- # Words inside an echo/printf string or a comment line are not commands: an echo naming the
58
- # migration tool refused a verify run's read on 2026-09-06 (MISS-TechieFlow-20260906-20).
59
- # Strip them before matching, so only a command that runs a migrator or writes through a
60
- # client is refused.
62
+ # What counts is what RUNS, not what a command mentions (Sevak TF-002, 2026-10-06).
63
+ # - A client is a write only when it is the command word of a segment (after a start, ; & | ( or
64
+ # a backtick, past sudo / env / `exec <container>`), so a grep or a note naming a client and a
65
+ # SQL verb passes, while `<client> app.db "<write>"` does not.
66
+ # - A heredoc's body is SQL when it feeds a client, code when it feeds an interpreter, and data
67
+ # (dropped) when it feeds anything else (`cat > notes.md <<EOF`).
68
+ # - An interpreter (python, node, deno, bun, ruby, perl, pwsh) running inline code or a script
69
+ # file has that code read: a database library together with a SQL write is refused unless every
70
+ # database path it names is a throw-away one (tests/.artifacts/, /tmp, :memory:). A sub-agent's
71
+ # script deleted four owner settings from Sevak's app database because the guard saw only
72
+ # `python3 x.py`.
73
+ # Words inside an echo/printf string or a comment line are not commands (MISS-TechieFlow-20260906-20),
74
+ # unless the string is redirected into a file, which may then be run.
75
+ SEG = (r"(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?(?:env\s+(?:\S+=\S*\s+)*)?"
76
+ r"(?:[\w./\\-]*\bexec\s+(?:-\S+\s+)*\S+\s+)?")
77
+ CLIENT_AT = SEG + r"(?:\S*/)?" + CLIENTS
78
+ INTERP = r"(?:python3?|py|node|deno|bun|ruby|perl|pwsh|powershell(?:\.exe)?|tsx|ts-node)"
79
+ INTERP_AT = SEG + r"(?:\S*/)?" + INTERP + r"\b"
80
+ DB_LIBS = (r"\bsqlite3\b|better-sqlite3|\bsql\.js\b|node:sqlite|\bpsycopg|\bpymysql\b|mysql\.connector|\bmysql2\b"
81
+ r"|require\(\s*['\"]pg['\"]\s*\)|from\s+['\"]pg['\"]|\bsqlalchemy\b|microsoft\.data\.sqlite|system\.data\.sqlite"
82
+ r"|\bpymongo\b|\bmongodb\b|\baiosqlite\b|\bduckdb\b|invoke-sqlcmd")
83
+ SQL_WRITE = (r"\binsert\s+(?:or\s+\w+\s+)?into\b|\bupdate\s+[\w\"`\[\].]+\s+set\b|\bdelete\s+from\b"
84
+ r"|\bdrop\s+(?:table|index|view|database)\b|\bcreate\s+(?:table|index|view|trigger)\b|\balter\s+table\b"
85
+ r"|\btruncate\b|\breplace\s+into\b|\bexecutescript\b"
86
+ r"|\.(?:delete_many|delete_one|insert_one|insert_many|update_one|update_many|replace_one|drop)\s*\(")
87
+ DB_PATH = r"['\"]([^'\"\n]*\.(?:db|sqlite3?|mdf)|:memory:)['\"]"
88
+ SCRIPT_EXT = r"\.(?:py|js|mjs|cjs|ts|mts|rb|pl|ps1)\b"
89
+
90
+
91
+ def throwaway(path):
92
+ q = path.replace("\\", "/").lower()
93
+ return q == ":memory:" or "tests/.artifacts/" in q or q.startswith("/tmp/") or "/tmp/" in q or q.startswith("tmp/")
94
+
95
+
96
+ def code_writes(code):
97
+ """A database library and a SQL write in the same code, against a database that is not a throw-away."""
98
+ c = code.lower()
99
+ if not (re.search(DB_LIBS, c) and re.search(SQL_WRITE, c)):
100
+ return False
101
+ paths = re.findall(DB_PATH, c)
102
+ return not paths or not all(throwaway(x) for x in paths)
103
+
104
+
61
105
  low = cmd.lower()
62
106
  low = re.sub(r"(?m)^\s*#.*$", "", low)
63
- low = re.sub(r"\b(echo|printf)\s+(\"[^\"]*\"|'[^']*'|[^\n;|&]*)", r"\1", low)
64
- direct_write = (re.search(CLIENTS, low)
65
- and (re.search(WRITE_VERBS, low)
66
- or re.search(r"\s-(f|i)\s+\S+\.sql\b", low)
67
- or re.search(r"<\s*\S+\.sql\b", low)))
107
+
108
+ # heredocs first: keep the body only where it is SQL (fed to a client) or code (fed to an interpreter)
109
+ code_bits, body_sql = [], []
110
+
111
+
112
+ runs_code = bool(re.search(INTERP_AT, low))
113
+
114
+
115
+ def _heredoc(m):
116
+ head, body = m.group(1), m.group(4)
117
+ if re.search(CLIENT_AT, head):
118
+ body_sql.append(body)
119
+ elif re.search(INTERP_AT, head) or runs_code:
120
+ # fed to an interpreter, or written to a file in a command that also runs one
121
+ # (`cat > s.py <<EOF … EOF; python3 s.py`): the file does not exist when this hook runs
122
+ code_bits.append(body)
123
+ return head + "\n"
124
+
125
+
126
+ low = re.sub(r"(?ms)^([^\n]*?)<<-?\s*(['\"]?)(\w+)\2[^\n]*\n(.*?)^\s*\3\s*$", _heredoc, low)
127
+ if runs_code:
128
+ code_bits.append(low) # printf '…' > s.py && python3 s.py: the code is in the command itself
129
+ # an echo/printf string is dropped unless it is redirected into a file
130
+ low = re.sub(r"\b(echo|printf)\s+(?:\"[^\"]*\"|'[^']*'|[^\n;|&>]*)(?!\s*>)", r"\1", low)
131
+
132
+ direct_write = bool(re.search(CLIENT_AT, low)
133
+ and (re.search(WRITE_VERBS, low)
134
+ or re.search(r"\s-(f|i)\s+\S+\.sql\b", low)
135
+ or re.search(r"<\s*\S+\.sql\b", low))) or any(re.search(WRITE_VERBS, b) for b in body_sql)
136
+
137
+ # inline code (-c / -e / --eval / -Command) and script files run by an interpreter
138
+ script_write, script_name = False, ""
139
+ if re.search(INTERP_AT, low) or code_bits:
140
+ for m in re.finditer(INTERP_AT + r"[^\n;|&]*?\s(?:-c|-e|--eval|-p|-command)\s+(\"(?:[^\"\\]|\\.)*\"|'[^']*')", low):
141
+ code_bits.append(m.group(1))
142
+ roots = [data.get("cwd") or "", os.environ.get("TF_ROOT", ".")]
143
+ # `cd tools && python3 seed.py`: the script is found from the folder the command moved to
144
+ for cdir in re.findall(r"(?:^|[;&|(]\s*)cd\s+(['\"]?)([^\s'\";&|]+)\1", cmd):
145
+ roots += [os.path.join(r, cdir[1]) for r in list(roots) if r] + [cdir[1]]
146
+ for seg in re.finditer(INTERP_AT + r"([^\n;|&]*)", cmd, flags=re.I):
147
+ for tok in re.findall(r"[^\s'\"]+" + SCRIPT_EXT, seg.group(1), flags=re.I):
148
+ for cand in [tok] + [os.path.join(r, tok) for r in roots if r]:
149
+ try:
150
+ if os.path.isfile(cand) and os.path.getsize(cand) < 2_000_000:
151
+ with open(cand, encoding="utf-8", errors="replace") as fh:
152
+ if code_writes(fh.read()):
153
+ script_write, script_name = True, tok
154
+ break
155
+ except Exception:
156
+ pass
157
+ script_write = script_write or any(code_writes(b) for b in code_bits)
158
+
159
+ if script_write:
160
+ print("BLOCKED by TechieFlow policy: this runs code that writes to a database directly "
161
+ f"({script_name or 'inline code'}: a database library and an INSERT / UPDATE / DELETE / DROP / CREATE). "
162
+ "The app's database changes only through its migration path or through the app itself. For a "
163
+ "smoke or a seed, point the script at a throw-away copy whose path names tests/.artifacts/, /tmp "
164
+ "or :memory:, written literally in the script. Owner rule 2026-09-05; Sevak TF-002.", file=sys.stderr)
165
+ sys.exit(2)
68
166
  if direct_write or re.search(DROP_DB, low):
69
167
  print("BLOCKED by TechieFlow policy: a direct database write (SQL through a client, or dropping "
70
168
  "a database) is not allowed from any command. Schema and data changes go through the "
@@ -75,11 +173,12 @@ if direct_write or re.search(DROP_DB, low):
75
173
 
76
174
  if re.search(MIGRATORS, low):
77
175
  ph = phase()
78
- if ph in ("build-phase", "fix-issues"):
176
+ # triage-and-fix step 3 is fix-issues steps 2 to 5 under its own marker (AppManager TF-027)
177
+ if ph in ("build-phase", "fix-issues", "triage-and-fix"):
79
178
  sys.exit(0)
80
179
  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, "
180
+ print("BLOCKED by TechieFlow policy: a database migration runs only from *build-phase, "
181
+ f"*fix-issues or *triage-and-fix, and {who}. Day-1, mockups, split-brd, amend-docs, devguide, verify, "
83
182
  "triage and status commands write documents and records, never the database. If you are "
84
183
  "inside build-phase or fix-issues, step 0 was skipped: run "
85
184
  "bash .tfcore/utils/tf-phase.sh start <command> <App> and retry. Owner rule 2026-09-05.",