@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.
- package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
- 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-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +194 -14
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +112 -13
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +2 -2
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +5 -5
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +3 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +2 -2
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +10 -2
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +38 -2
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- 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-phases-tmpl.md +2 -1
- 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 +6 -2
- 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-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -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 +95 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +163 -15
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +53 -24
- package/.tfcore/utils/tf-doc-check.py +218 -26
- package/.tfcore/utils/tf-doc-tests.sh +116 -0
- package/.tfcore/utils/tf-emit.sh +132 -45
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +87 -31
- package/.tfcore/utils/tf-goal.sh +12 -12
- 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-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
- package/.tfcore/utils/tf-mockup-parity.sh +25 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +38 -4
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +79 -5
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +160 -45
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +74 -17
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +529 -54
- package/.tfcore/utils/tf-verify-emit.sh +57 -17
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +290 -24
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +341 -44
- package/.tfcore/utils/tf-verify-screens.sh +23 -5
- package/.tfcore/utils/tf-verify-tests.sh +310 -19
- package/.tfcore/utils/tf-verify-verdict.py +163 -21
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +272 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +6 -2
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.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-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- 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}
|
|
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")
|
|
@@ -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,
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// hook — they are `deny` in the map and exit-2 in
|
|
306
|
-
// if the harness never calls permission.ask
|
|
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 | |
|
package/.tfcore/core-config.yaml
CHANGED
|
@@ -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://
|
|
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
|
-
|
|
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,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
|
|
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
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# client
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
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.",
|