@techierathore/techieflow 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/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/_smoke-test-policy.md +7 -5
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +15 -7
- 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 +25 -2
- package/.tfcore/hooks/guard-db.sh +106 -9
- package/.tfcore/hooks/guard-status-html.sh +5 -1
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- 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/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +4 -4
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +1 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +6 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -0
- package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
- package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-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-brd-status.py +10 -1
- package/.tfcore/utils/tf-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +28 -3
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +6 -2
- package/.tfcore/utils/tf-doc-check.py +124 -21
- package/.tfcore/utils/tf-doc-tests.sh +40 -3
- package/.tfcore/utils/tf-emit.sh +40 -10
- package/.tfcore/utils/tf-fix-close.sh +13 -7
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- package/.tfcore/utils/tf-owner-text.py +27 -5
- package/.tfcore/utils/tf-phase.sh +24 -4
- package/.tfcore/utils/tf-render-html.py +19 -3
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-facts.py +90 -15
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +30 -4
- package/.tfcore/utils/tf-verify-boot.sh +296 -21
- package/.tfcore/utils/tf-verify-emit.sh +37 -17
- package/.tfcore/utils/tf-verify-list.py +219 -20
- 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 +184 -20
- package/.tfcore/utils/tf-verify-screens.sh +17 -4
- package/.tfcore/utils/tf-verify-tests.sh +107 -17
- package/.tfcore/utils/tf-verify-verdict.py +118 -15
- 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_feedback.py +86 -18
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +2 -2
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +14 -3
- 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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// tf-verify-zoom.mjs — presses the OS zoom keys on a Blazor Hybrid head and measures what they did
|
|
2
|
+
// (coding-standards-dotnet.md §9b; Lekhak, 2026-10-11). Called by tf-verify-zoom.sh; see there.
|
|
3
|
+
//
|
|
4
|
+
// Mac (--appium): mac2 presses Cmd+= / Cmd+- / Cmd+0 as real keys, so they go through the menu bar the
|
|
5
|
+
// way a person's do; macOS takes the window alone after each. The accessibility tree does not follow a
|
|
6
|
+
// WKWebView's page zoom (its text heights stayed 13 px at 130% on BlogAdmin), so the level is measured
|
|
7
|
+
// from the pictures: the scale that best maps the actual-size picture onto the zoomed one, from the
|
|
8
|
+
// first row where the two differ (the web view's top edge; the title bar above it does not zoom).
|
|
9
|
+
// Windows and any Chromium web view (--cdp): Playwright presses Ctrl+= / Ctrl+- / Ctrl+0 in the page
|
|
10
|
+
// and reads the zoom it paints at (CSS zoom on <html> times the device pixel ratio).
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { inflateSync } from 'node:zlib';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const opt = (k, d = '') => { const i = args.indexOf(k); return i >= 0 && i + 1 < args.length ? args[i + 1] : d; };
|
|
19
|
+
const flag = (k) => args.includes(k);
|
|
20
|
+
const OUT = opt('--out', 'tests/.artifacts/verify/zoom');
|
|
21
|
+
const LEAVE = parseInt(opt('--leave', '0'), 10) || 0;
|
|
22
|
+
const STEPS = [
|
|
23
|
+
// what is pressed, how often, the level expected afterwards (1.0 = actual size)
|
|
24
|
+
{ key: 'reset', n: 1, want: 1.0, say: 'actual size' },
|
|
25
|
+
{ key: 'in', n: 3, want: 1.3, say: 'zoom in 3 times' },
|
|
26
|
+
{ key: 'out', n: 1, want: 1.2, say: 'zoom out once' },
|
|
27
|
+
{ key: 'reset', n: 1, want: 1.0, say: 'actual size again' },
|
|
28
|
+
];
|
|
29
|
+
const TOL = 0.05;
|
|
30
|
+
|
|
31
|
+
// ---- PNG, 8-bit RGB or RGBA, not interlaced: what screencapture and sips write -------------------
|
|
32
|
+
export function decodePng(buf) {
|
|
33
|
+
let p = 8, w = 0, h = 0, ct = 0; const idat = [];
|
|
34
|
+
while (p < buf.length) {
|
|
35
|
+
const len = buf.readUInt32BE(p); const type = buf.toString('latin1', p + 4, p + 8);
|
|
36
|
+
const data = buf.subarray(p + 8, p + 8 + len);
|
|
37
|
+
if (type === 'IHDR') { w = data.readUInt32BE(0); h = data.readUInt32BE(4); ct = data[9]; if (data[8] !== 8 || data[12]) throw new Error('only 8-bit, non-interlaced PNG'); }
|
|
38
|
+
if (type === 'IDAT') idat.push(data);
|
|
39
|
+
if (type === 'IEND') break;
|
|
40
|
+
p += 12 + len;
|
|
41
|
+
}
|
|
42
|
+
const bpp = ct === 6 ? 4 : ct === 2 ? 3 : ct === 0 ? 1 : ct === 4 ? 2 : 0;
|
|
43
|
+
if (!bpp) throw new Error(`PNG colour type ${ct} not read`);
|
|
44
|
+
const raw = inflateSync(Buffer.concat(idat)); const stride = w * bpp;
|
|
45
|
+
const px = Buffer.alloc(h * stride); const gray = new Float32Array(w * h);
|
|
46
|
+
for (let y = 0; y < h; y++) {
|
|
47
|
+
const f = raw[y * (stride + 1)]; const src = y * (stride + 1) + 1; const row = y * stride;
|
|
48
|
+
for (let x = 0; x < stride; x++) {
|
|
49
|
+
const a = x >= bpp ? px[row + x - bpp] : 0; const b = y ? px[row - stride + x] : 0; const c = x >= bpp && y ? px[row - stride + x - bpp] : 0;
|
|
50
|
+
let v = raw[src + x];
|
|
51
|
+
if (f === 1) v += a; else if (f === 2) v += b; else if (f === 3) v += (a + b) >> 1;
|
|
52
|
+
else if (f === 4) { const pp = a + b - c; const pa = Math.abs(pp - a), pb = Math.abs(pp - b), pc = Math.abs(pp - c); v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c; }
|
|
53
|
+
px[row + x] = v & 255;
|
|
54
|
+
}
|
|
55
|
+
for (let x = 0; x < w; x++) { const o = row + x * bpp; gray[y * w + x] = bpp >= 3 ? 0.299 * px[o] + 0.587 * px[o + 1] + 0.114 * px[o + 2] : px[o]; }
|
|
56
|
+
}
|
|
57
|
+
return { w, h, gray };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// a picture shrunk to 640 px wide by sips (macOS), so the search stays quick
|
|
61
|
+
function load(file) {
|
|
62
|
+
const small = file.replace(/\.png$/, '.small.png');
|
|
63
|
+
try { execFileSync('sips', ['-Z', '1000', file, '--out', small], { stdio: 'ignore' }); } catch { /* not macOS: read it whole */ }
|
|
64
|
+
return decodePng(readFileSync(existsSync(small) ? small : file));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The page zoom that turns picture `base` (actual size) into picture `zoomed`. Page zoom scales every
|
|
68
|
+
// horizontal position from the web view's left edge, but not always vertical ones: a header or sidebar
|
|
69
|
+
// sized to the window height keeps its height (BlogAdmin's did), so a 2-D match read 121% for 130%.
|
|
70
|
+
// So the two pictures are compared across the window only: for each column, how much vertical edge
|
|
71
|
+
// (a border, a box side, the strokes of text) it holds below the title bar, and the scale that best
|
|
72
|
+
// lines the actual-size profile up with the zoomed one. The title bar is the rows the two share.
|
|
73
|
+
export function scaleBetween(zoomed, base) {
|
|
74
|
+
const { w, h } = base; if (zoomed.w !== w || zoomed.h !== h) return { scale: 0, why: 'the two pictures differ in size' };
|
|
75
|
+
let top = 0;
|
|
76
|
+
for (; top < h; top++) { let d = 0; for (let x = 0; x < w; x++) d += Math.abs(zoomed.gray[top * w + x] - base.gray[top * w + x]); if (d / w > 2) break; }
|
|
77
|
+
if (top >= h - 10) return { scale: 1, top, why: 'the pictures are the same' };
|
|
78
|
+
const profile = (img) => {
|
|
79
|
+
const p = new Float64Array(w);
|
|
80
|
+
for (let y = top; y < h; y++) for (let x = 0; x + 1 < w; x++) p[x] += Math.abs(img.gray[y * w + x + 1] - img.gray[y * w + x]);
|
|
81
|
+
return p;
|
|
82
|
+
};
|
|
83
|
+
const pz = profile(zoomed), pb = profile(base);
|
|
84
|
+
let best = { scale: 0, r: -2 };
|
|
85
|
+
for (let s = 0.5; s <= 3.0001; s += 0.01) {
|
|
86
|
+
let n = 0, sa = 0, sb = 0, saa = 0, sbb = 0, sab = 0;
|
|
87
|
+
for (let x = 0; x < w; x++) {
|
|
88
|
+
const bx = x / s; const i = Math.floor(bx); if (i + 1 >= w) break;
|
|
89
|
+
const a = pz[x], b = pb[i] + (pb[i + 1] - pb[i]) * (bx - i);
|
|
90
|
+
n++; sa += a; sb += b; saa += a * a; sbb += b * b; sab += a * b;
|
|
91
|
+
}
|
|
92
|
+
if (n < w / 4) continue;
|
|
93
|
+
const r = (sab - (sa * sb) / n) / Math.sqrt(Math.max(1e-9, (saa - (sa * sa) / n) * (sbb - (sb * sb) / n)));
|
|
94
|
+
if (r > best.r) best = { scale: Math.round(s * 100) / 100, r };
|
|
95
|
+
}
|
|
96
|
+
return { scale: best.scale, top };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const RESULT = { checks: [], kept_checked: false, head: '', evidence: '' };
|
|
100
|
+
const line = (ok, text) => { console.log(`${ok ? 'OK ' : 'FAIL'} ${text}`); RESULT.checks.push({ ok, text }); return ok; };
|
|
101
|
+
|
|
102
|
+
async function mac() {
|
|
103
|
+
const { openSession, parseSource, appPid, windowShot } = await import('./tf-appium.mjs');
|
|
104
|
+
let boot = {}; try { boot = JSON.parse(readFileSync('tests/.artifacts/verify/boot.json', 'utf8')); } catch { /* flags only */ }
|
|
105
|
+
const appPath = opt('--app', boot.app_path || ''); const bundleId = opt('--bundle', boot.bundle || '');
|
|
106
|
+
let s;
|
|
107
|
+
try { s = await openSession(opt('--appium'), { bundleId, appPath }); }
|
|
108
|
+
catch (e) { line(false, `no session on the app at ${opt('--appium')}: ${e.message} (boot it with bash .tfcore/utils/tf-verify-boot.sh start --head maccatalyst)`); return 2; }
|
|
109
|
+
const pid = appPid(appPath);
|
|
110
|
+
let ok = true; let k = 0;
|
|
111
|
+
const shot = (name, same = false) => { const f = join(OUT, `${String(same ? k : k++).padStart(2, '0')}-${name}-mac.png`); return windowShot(pid, f) ? f : ''; };
|
|
112
|
+
const press = async (key) => {
|
|
113
|
+
await s.call('POST', '/execute/sync', { script: 'macos: keys', args: [{ keys: [{ key: { in: '=', out: '-', reset: '0' }[key], modifierFlags: 1 << 4 }] }] });
|
|
114
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
115
|
+
};
|
|
116
|
+
// the View menu names the three commands
|
|
117
|
+
const nodes = parseSource(await s.source());
|
|
118
|
+
const view = nodes.find((n) => n.type === 'MenuBarItem' && /^view$/i.test(n.title || n.label));
|
|
119
|
+
const items = []; if (view) { const todo = [...view.kids]; while (todo.length) { const n = nodes[todo.shift()]; if (n.type === 'MenuItem') items.push(n.title || n.label); todo.push(...n.kids); } }
|
|
120
|
+
const missing = ['Zoom In', 'Zoom Out', 'Actual Size'].filter((t) => !items.includes(t));
|
|
121
|
+
ok = line(!missing.length, missing.length ? `the View menu has no ${missing.join(', ')} (it has: ${items.filter(Boolean).join(', ') || 'no View menu'})` : 'View › Zoom In, Zoom Out, Actual Size are in the menu bar') && ok;
|
|
122
|
+
// A mac2 session starts the app again: wait for a drawn page that holds still for two pictures
|
|
123
|
+
// (the first ones on BlogAdmin were the bare window while Blazor started)
|
|
124
|
+
const ink = (f) => { const g = load(f); let e = 0; for (let i = 1; i < g.gray.length; i++) e += Math.abs(g.gray[i] - g.gray[i - 1]); return e / g.gray.length; };
|
|
125
|
+
let opened = ''; let last = -1;
|
|
126
|
+
for (let t = Date.now(); Date.now() - t < 60000;) {
|
|
127
|
+
const f = shot('as-opened', true); const e = f ? ink(f) : 0; // a bare window reads 0.04, a drawn page 0.96
|
|
128
|
+
if (e > 0.2 && last > 0 && Math.abs(e - last) / last < 0.02) { opened = f; break; }
|
|
129
|
+
last = e; await new Promise((r) => setTimeout(r, 1500));
|
|
130
|
+
}
|
|
131
|
+
k++;
|
|
132
|
+
if (!opened) { await s.close(); line(false, 'the app showed no drawn page within 60 s'); return 2; }
|
|
133
|
+
await press('reset'); const actual = shot('actual-size');
|
|
134
|
+
if (!opened || !actual) { await s.close(); return line(false, 'could not take the window (the terminal needs Screen Recording)') ? 0 : 5; }
|
|
135
|
+
RESULT.head = 'maccatalyst'; RESULT.evidence = actual;
|
|
136
|
+
const base = load(actual);
|
|
137
|
+
const restored = scaleBetween(load(opened), base).scale;
|
|
138
|
+
console.log(`INFO opened at ${Math.round(restored * 100)}% (the level the last run left; run again after a restart with --leave 3 before it to check the level is kept)`);
|
|
139
|
+
if (flag('--expect-restored')) RESULT.kept_checked = true;
|
|
140
|
+
if (flag('--expect-restored')) ok = line(Math.abs(restored - parseFloat(opt('--expect-restored'))) <= TOL, `opened at ${Math.round(restored * 100)}%, expected ${Math.round(parseFloat(opt('--expect-restored')) * 100)}% from the last run`) && ok;
|
|
141
|
+
for (const st of STEPS.slice(1)) {
|
|
142
|
+
for (let i = 0; i < st.n; i++) await press(st.key);
|
|
143
|
+
const f = shot(st.key); const got = f ? scaleBetween(load(f), base).scale : 0;
|
|
144
|
+
ok = line(Math.abs(got - st.want) <= TOL, `${st.say} (Cmd+${{ in: '=', out: '-', reset: '0' }[st.key]}): ${Math.round(got * 100)}%, expected ${Math.round(st.want * 100)}%`) && ok;
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < LEAVE; i++) await press('in');
|
|
147
|
+
if (LEAVE) console.log(`INFO left at ${100 + LEAVE * 10}% for the restart check`);
|
|
148
|
+
await s.close();
|
|
149
|
+
return ok ? 0 : 5;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function cdp() {
|
|
153
|
+
const { chromium } = await import('playwright');
|
|
154
|
+
let browser;
|
|
155
|
+
try { browser = await chromium.connectOverCDP(opt('--cdp')); }
|
|
156
|
+
catch (e) { line(false, `nothing answers at ${opt('--cdp')}: ${e.message.split('\n')[0]}`); return 2; }
|
|
157
|
+
const page = browser.contexts().flatMap((c) => c.pages()).find((p) => !p.url().startsWith('devtools:')) || null;
|
|
158
|
+
if (!page) { line(false, 'no page to drive at ' + opt('--cdp')); return 2; }
|
|
159
|
+
const level = () => page.evaluate(() => (parseFloat(getComputedStyle(document.documentElement).zoom) || 1) * window.devicePixelRatio);
|
|
160
|
+
let ok = true; let k = 0;
|
|
161
|
+
const press = async (key) => { await page.keyboard.press({ in: 'Control+Equal', out: 'Control+Minus', reset: 'Control+Digit0' }[key]); await page.waitForTimeout(400); };
|
|
162
|
+
RESULT.head = 'cdp';
|
|
163
|
+
const opened = await level(); await press('reset'); const base = await level();
|
|
164
|
+
console.log(`INFO opened at ${Math.round((opened / base) * 100)}% (the level the last run left)`);
|
|
165
|
+
if (flag('--expect-restored')) RESULT.kept_checked = true;
|
|
166
|
+
if (flag('--expect-restored')) ok = line(Math.abs(opened / base - parseFloat(opt('--expect-restored'))) <= TOL, `opened at ${Math.round((opened / base) * 100)}%, expected ${Math.round(parseFloat(opt('--expect-restored')) * 100)}%`) && ok;
|
|
167
|
+
for (const st of STEPS.slice(1)) {
|
|
168
|
+
for (let i = 0; i < st.n; i++) await press(st.key);
|
|
169
|
+
await page.screenshot({ path: join(OUT, `${String(k++).padStart(2, '0')}-${st.key}-cdp.png`) }).catch(() => {});
|
|
170
|
+
const got = (await level()) / base;
|
|
171
|
+
ok = line(Math.abs(got - st.want) <= TOL, `${st.say} (Ctrl+${{ in: '=', out: '-', reset: '0' }[st.key]}): ${Math.round(got * 100)}%, expected ${Math.round(st.want * 100)}%`) && ok;
|
|
172
|
+
}
|
|
173
|
+
// Ctrl+wheel up is one step in
|
|
174
|
+
await page.mouse.move(200, 200); await page.keyboard.down('Control'); await page.mouse.wheel(0, -100); await page.keyboard.up('Control'); await page.waitForTimeout(400);
|
|
175
|
+
const wheel = (await level()) / base;
|
|
176
|
+
ok = line(Math.abs(wheel - 1.1) <= TOL, `Ctrl+wheel up: ${Math.round(wheel * 100)}%, expected 110%`) && ok;
|
|
177
|
+
await press('reset');
|
|
178
|
+
for (let i = 0; i < LEAVE; i++) await press('in');
|
|
179
|
+
if (LEAVE) console.log(`INFO left at ${100 + LEAVE * 10}% for the restart check`);
|
|
180
|
+
await browser.close().catch(() => {});
|
|
181
|
+
return ok ? 0 : 5;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (process.argv[1] && process.argv[1].endsWith('tf-verify-zoom.mjs')) {
|
|
185
|
+
if (flag('--scale')) { // tf-verify-zoom.mjs --scale <zoomed.png> <actual.png>: the measure alone
|
|
186
|
+
console.log(scaleBetween(load(args[args.indexOf('--scale') + 1]), load(args[args.indexOf('--scale') + 2])).scale);
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
mkdirSync(OUT, { recursive: true });
|
|
190
|
+
const rc = flag('--appium') ? await mac() : flag('--cdp') ? await cdp() : (console.error('tf-verify-zoom: --appium <url> or --cdp <url>'), 3);
|
|
191
|
+
if (opt('--json-out')) writeFileSync(opt('--json-out'), JSON.stringify({ ...RESULT, verdict: rc === 0 ? 'PASS' : 'FAIL' }, null, 1));
|
|
192
|
+
process.exit(rc);
|
|
193
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-verify-zoom.sh — presses the OS zoom keys on a booted Blazor Hybrid head and says what they did
|
|
3
|
+
# (coding-standards-dotnet.md §9b; Lekhak, 2026-10-11).
|
|
4
|
+
#
|
|
5
|
+
# bash .tfcore/utils/tf-verify-zoom.sh --appium http://localhost:4723 [--leave 3] [--expect-restored 1.3]
|
|
6
|
+
# a Mac Catalyst head booted by tf-verify-boot.sh (its .app and bundle come from boot.json)
|
|
7
|
+
# bash .tfcore/utils/tf-verify-zoom.sh --cdp <the boot url> [--leave 3] [--expect-restored 1.3]
|
|
8
|
+
# a Windows (WebView2) head, or any Chromium web view, over its debugging address
|
|
9
|
+
#
|
|
10
|
+
# Checks, in order: (Mac) View › Zoom In, Zoom Out and Actual Size are in the menu bar; the level the
|
|
11
|
+
# app opened at is printed, then Cmd/Ctrl+0 is actual size; zoom in 3 times reads 130%, zoom out once
|
|
12
|
+
# 120%, actual size 100%; (Windows) Ctrl+wheel up reads 110%. The kept level across a restart is two
|
|
13
|
+
# runs: the first with --leave 3 (it ends at 130%), then stop and start the head, then the second with
|
|
14
|
+
# --expect-restored 1.3. Pictures go to tests/.artifacts/verify/zoom/. The Mac level is measured from
|
|
15
|
+
# the window's pictures, because a WKWebView's accessibility tree does not follow its page zoom.
|
|
16
|
+
# Exit 0 every check passed · 5 one failed · 2 nothing to drive · 3 usage · 4 Playwright missing (--cdp).
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
case "${1:-}" in
|
|
20
|
+
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
21
|
+
"") sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 3 ;;
|
|
22
|
+
esac
|
|
23
|
+
command -v node >/dev/null 2>&1 || { echo "tf-verify-zoom: node is required (bash .tfcore/utils/tf-verify-env.sh)" >&2; exit 4; }
|
|
24
|
+
case " $* " in *" --cdp "*)
|
|
25
|
+
node -e "import('playwright').then(()=>process.exit(0)).catch(()=>process.exit(1))" 2>/dev/null || {
|
|
26
|
+
echo "tf-verify-zoom: the 'playwright' package is not resolvable from this folder; run bash .tfcore/utils/tf-verify-env.sh" >&2; exit 4; } ;;
|
|
27
|
+
esac
|
|
28
|
+
source "$HERE/tf-lock.sh"; tf_take_lock verify-screens # one check drives the app at a time
|
|
29
|
+
node "$HERE/tf-verify-zoom.mjs" "$@"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-yolo-alias.sh — put the `tfyolo` command in this machine's shell profile, once (owner, 2026-10-14).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-yolo-alias.sh install add it to ~/.bashrc and/or ~/.zshrc (idempotent)
|
|
5
|
+
# bash .tfcore/utils/tf-yolo-alias.sh remove take it out again
|
|
6
|
+
# bash .tfcore/utils/tf-yolo-alias.sh status say where it is
|
|
7
|
+
#
|
|
8
|
+
# `tfyolo` is a small shell function, the same text in bash and zsh. Typed anywhere inside a
|
|
9
|
+
# TechieFlow project, it finds the project's .tfcore/utils/tf-yolo-start.sh above the current folder
|
|
10
|
+
# and runs it: YOLO on, Claude Code started in bypass mode in this terminal, YOLO off when the session
|
|
11
|
+
# ends. `tfyolo -c` continues the last conversation. It never opens or closes a window: exit the
|
|
12
|
+
# session you are in (/exit), then type it in the same terminal, or type it in another tab.
|
|
13
|
+
# The function lives between two marker lines and is replaced whole on a second install, so an update
|
|
14
|
+
# to it reaches the profile by running install again. Profiles: ~/.zshrc when the login shell is zsh
|
|
15
|
+
# or the file exists (macOS), ~/.bashrc when the login shell is bash or the file exists (WSL, Linux).
|
|
16
|
+
# A new terminal picks it up; an open one needs `source ~/.bashrc` (or ~/.zshrc). Exit 0; 2 bad use.
|
|
17
|
+
|
|
18
|
+
set -u
|
|
19
|
+
BEGIN="# >>> TechieFlow tfyolo >>>"
|
|
20
|
+
END="# <<< TechieFlow tfyolo <<<"
|
|
21
|
+
HOME_DIR="${TF_HOME:-$HOME}"
|
|
22
|
+
|
|
23
|
+
block() {
|
|
24
|
+
cat <<'FN'
|
|
25
|
+
# >>> TechieFlow tfyolo >>>
|
|
26
|
+
# Start Claude Code in bypass mode with TechieFlow YOLO on, in the project around this folder.
|
|
27
|
+
# `tfyolo -c` continues the last conversation. Added by .tfcore/utils/tf-yolo-alias.sh; remove with
|
|
28
|
+
# `bash .tfcore/utils/tf-yolo-alias.sh remove`.
|
|
29
|
+
tfyolo() {
|
|
30
|
+
_tf_d="$PWD"
|
|
31
|
+
while [ "$_tf_d" != "/" ] && [ ! -f "$_tf_d/.tfcore/utils/tf-yolo-start.sh" ]; do _tf_d="$(dirname "$_tf_d")"; done
|
|
32
|
+
if [ ! -f "$_tf_d/.tfcore/utils/tf-yolo-start.sh" ]; then
|
|
33
|
+
echo "tfyolo: no TechieFlow project around $PWD (no .tfcore/utils/tf-yolo-start.sh above it)" >&2
|
|
34
|
+
unset _tf_d; return 2
|
|
35
|
+
fi
|
|
36
|
+
bash "$_tf_d/.tfcore/utils/tf-yolo-start.sh" "$@"
|
|
37
|
+
_tf_rc=$?; unset _tf_d; return $_tf_rc
|
|
38
|
+
}
|
|
39
|
+
# <<< TechieFlow tfyolo <<<
|
|
40
|
+
FN
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
profiles() {
|
|
44
|
+
local sh; sh="$(basename "${SHELL:-}")"
|
|
45
|
+
[[ "$sh" == zsh || -f "$HOME_DIR/.zshrc" ]] && echo "$HOME_DIR/.zshrc"
|
|
46
|
+
[[ "$sh" == bash || -f "$HOME_DIR/.bashrc" ]] && echo "$HOME_DIR/.bashrc"
|
|
47
|
+
[[ "$sh" != zsh && "$sh" != bash && ! -f "$HOME_DIR/.zshrc" && ! -f "$HOME_DIR/.bashrc" ]] && echo "$HOME_DIR/.bashrc"
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
strip() { # the file without our block
|
|
52
|
+
awk -v b="$BEGIN" -v e="$END" '$0==b{skip=1;next} $0==e{skip=0;next} !skip' "$1"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "${1:-status}" in
|
|
56
|
+
install)
|
|
57
|
+
while IFS= read -r p; do
|
|
58
|
+
[[ -n "$p" ]] || continue
|
|
59
|
+
tmp="$p.tftmp.$$"
|
|
60
|
+
if [[ -f "$p" ]]; then strip "$p" > "$tmp"; else : > "$tmp"; fi
|
|
61
|
+
# one blank line before the block, never a growing run of them
|
|
62
|
+
[[ -s "$tmp" && -n "$(tail -c 1 "$tmp")" ]] && echo "" >> "$tmp"
|
|
63
|
+
block >> "$tmp"
|
|
64
|
+
cat "$tmp" > "$p"; rm -f "$tmp"
|
|
65
|
+
echo "tfyolo: installed in $p (open a new terminal, or: source $p)"
|
|
66
|
+
done < <(profiles)
|
|
67
|
+
;;
|
|
68
|
+
remove)
|
|
69
|
+
for p in "$HOME_DIR/.bashrc" "$HOME_DIR/.zshrc"; do
|
|
70
|
+
[[ -f "$p" ]] && grep -qxF "$BEGIN" "$p" || continue
|
|
71
|
+
tmp="$p.tftmp.$$"; strip "$p" > "$tmp"; cat "$tmp" > "$p"; rm -f "$tmp"
|
|
72
|
+
echo "tfyolo: removed from $p"
|
|
73
|
+
done
|
|
74
|
+
;;
|
|
75
|
+
status)
|
|
76
|
+
found=0
|
|
77
|
+
for p in "$HOME_DIR/.bashrc" "$HOME_DIR/.zshrc"; do
|
|
78
|
+
[[ -f "$p" ]] && grep -qxF "$BEGIN" "$p" && { echo "tfyolo: in $p"; found=1; }
|
|
79
|
+
done
|
|
80
|
+
[[ $found -eq 1 ]] || echo "tfyolo: not installed (bash .tfcore/utils/tf-yolo-alias.sh install)"
|
|
81
|
+
;;
|
|
82
|
+
*) echo "usage: tf-yolo-alias.sh install|remove|status" >&2; exit 2 ;;
|
|
83
|
+
esac
|
|
84
|
+
exit 0
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-yolo-perms.py — switch OpenCode to never ask, and back (owner, 2026-10-14). Run by tf-yolo.sh.
|
|
3
|
+
|
|
4
|
+
python3 .tfcore/utils/tf-yolo-perms.py on <project-root>
|
|
5
|
+
python3 .tfcore/utils/tf-yolo-perms.py off <project-root>
|
|
6
|
+
|
|
7
|
+
on OpenCode: the config file it reads LAST (it reads opencode.json, opencode.jsonc,
|
|
8
|
+
.opencode/opencode.json, .opencode/opencode.jsonc, in that order, the last matching rule
|
|
9
|
+
winning; checked on OpenCode 2.0.27) is rewritten with every "ask" made "allow", every "deny"
|
|
10
|
+
kept in its place, and edit, webfetch, external_directory and doom_loop allowed, in the root map
|
|
11
|
+
and in every agent's. In a project that file is the framework's own .opencode/opencode.jsonc.
|
|
12
|
+
Its exact text is saved once, in .tfcore/.session/yolo-perms.json; a second `on` keeps the
|
|
13
|
+
first saving.
|
|
14
|
+
Claude Code is NOT switched here: it ignores permissions.defaultMode "bypassPermissions" in
|
|
15
|
+
project and local settings ("only policy/user/flag settings may grant bypass mode", Claude Code
|
|
16
|
+
2.1.299, 2026-10-14). Its side is the hook .tfcore/hooks/yolo-allow.sh, and for the prompts a
|
|
17
|
+
hook cannot clear (ask rules, safety checks), a session started with
|
|
18
|
+
`claude --permission-mode bypassPermissions` (tf-goal.sh always starts it so).
|
|
19
|
+
off Puts the OpenCode file back as it was (one the updater replaced meanwhile is left as the updater
|
|
20
|
+
wrote it) and removes the saving. Without a saving it does nothing.
|
|
21
|
+
Exit 0 always: YOLO never fails a phase.
|
|
22
|
+
"""
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
MARK = "// tf-yolo.sh on rewrote this file so nothing asks while YOLO is on; tf-yolo.sh off puts the original back"
|
|
29
|
+
OPEN_ALLOW = ("edit", "webfetch", "external_directory", "doom_loop")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def jsonc(text):
|
|
33
|
+
"""JSON with // and /* */ comments and trailing commas, as opencode.jsonc is written"""
|
|
34
|
+
out, i, n, s = [], 0, len(text), None
|
|
35
|
+
while i < n:
|
|
36
|
+
c = text[i]
|
|
37
|
+
if s:
|
|
38
|
+
out.append(c)
|
|
39
|
+
if c == "\\" and i + 1 < n:
|
|
40
|
+
out.append(text[i + 1]); i += 2; continue
|
|
41
|
+
if c == s:
|
|
42
|
+
s = None
|
|
43
|
+
elif c == '"':
|
|
44
|
+
s = c; out.append(c)
|
|
45
|
+
elif text.startswith("//", i):
|
|
46
|
+
while i < n and text[i] != "\n":
|
|
47
|
+
i += 1
|
|
48
|
+
continue
|
|
49
|
+
elif text.startswith("/*", i):
|
|
50
|
+
j = text.find("*/", i + 2)
|
|
51
|
+
i = n if j < 0 else j + 2
|
|
52
|
+
continue
|
|
53
|
+
else:
|
|
54
|
+
out.append(c)
|
|
55
|
+
i += 1
|
|
56
|
+
return json.loads(re.sub(r",(\s*[}\]])", r"\1", "".join(out)))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def never_ask(perm):
|
|
60
|
+
"""a permission map with every "ask" made "allow"; order (and so every deny's place) kept"""
|
|
61
|
+
if perm == "ask":
|
|
62
|
+
return "allow"
|
|
63
|
+
if isinstance(perm, dict):
|
|
64
|
+
out = {k: never_ask(v) for k, v in perm.items()}
|
|
65
|
+
for k in OPEN_ALLOW:
|
|
66
|
+
if k not in out or out[k] == "ask":
|
|
67
|
+
out[k] = "allow"
|
|
68
|
+
return out
|
|
69
|
+
return perm
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read(p):
|
|
73
|
+
try:
|
|
74
|
+
with open(p, encoding="utf-8") as f:
|
|
75
|
+
return f.read()
|
|
76
|
+
except OSError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write(p, text):
|
|
81
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
82
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
83
|
+
f.write(text)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(argv):
|
|
87
|
+
if len(argv) < 3 or argv[1] not in ("on", "off"):
|
|
88
|
+
print(__doc__)
|
|
89
|
+
return 0
|
|
90
|
+
verb, root = argv[1], argv[2]
|
|
91
|
+
saving = os.path.join(root, ".tfcore", ".session", "yolo-perms.json")
|
|
92
|
+
oc = next((os.path.join(root, *p) for p in ((".opencode", "opencode.jsonc"), (".opencode", "opencode.json"),
|
|
93
|
+
("opencode.jsonc",), ("opencode.json",))
|
|
94
|
+
if os.path.isfile(os.path.join(root, *p))), None)
|
|
95
|
+
|
|
96
|
+
if verb == "on":
|
|
97
|
+
if not os.path.isfile(saving):
|
|
98
|
+
write(saving, json.dumps({"opencode_file": oc, "opencode_text": read(oc) if oc else None}, indent=1))
|
|
99
|
+
# OpenCode
|
|
100
|
+
text = read(oc) if oc else None
|
|
101
|
+
if text is not None and MARK not in text:
|
|
102
|
+
try:
|
|
103
|
+
cfg = jsonc(text)
|
|
104
|
+
except ValueError:
|
|
105
|
+
cfg = None
|
|
106
|
+
if isinstance(cfg, dict):
|
|
107
|
+
cfg["permission"] = never_ask(cfg.get("permission") or {})
|
|
108
|
+
for a in (cfg.get("agent") or {}).values():
|
|
109
|
+
if isinstance(a, dict) and a.get("permission"):
|
|
110
|
+
a["permission"] = never_ask(a["permission"])
|
|
111
|
+
write(oc, MARK + "\n" + json.dumps(cfg, indent=2) + "\n")
|
|
112
|
+
print("permissions: OpenCode set to never ask; Claude Code: the yolo-allow hook")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
# off
|
|
116
|
+
if not os.path.isfile(saving):
|
|
117
|
+
return 0
|
|
118
|
+
try:
|
|
119
|
+
before = json.loads(read(saving) or "{}")
|
|
120
|
+
except ValueError:
|
|
121
|
+
before = {}
|
|
122
|
+
oc_file, oc_text = before.get("opencode_file"), before.get("opencode_text")
|
|
123
|
+
if oc_file and oc_text is not None and MARK in (read(oc_file) or ""):
|
|
124
|
+
write(oc_file, oc_text)
|
|
125
|
+
os.remove(saving)
|
|
126
|
+
print("permissions: OpenCode config put back as it was")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
try:
|
|
132
|
+
sys.exit(main(sys.argv))
|
|
133
|
+
except Exception as e: # never a veto
|
|
134
|
+
print(f"tf-yolo-perms: {e}", file=sys.stderr)
|
|
135
|
+
sys.exit(0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-yolo-start.sh — what `tfyolo` runs: Claude Code in bypass mode with YOLO on (owner, 2026-10-14).
|
|
3
|
+
#
|
|
4
|
+
# tfyolo start a new Claude Code session here, nothing asks
|
|
5
|
+
# tfyolo -c the same, continuing the last conversation in this folder (claude --continue)
|
|
6
|
+
# tfyolo <claude args> any other claude arguments are passed through unchanged
|
|
7
|
+
#
|
|
8
|
+
# Why it exists: Claude Code drops its permission prompts only for a session STARTED in bypass mode
|
|
9
|
+
# (`claude --permission-mode bypassPermissions`). Project settings cannot grant that mode (Claude Code
|
|
10
|
+
# 2.1.299 ignores it there), and `*yolo` typed into an open session cannot switch it. So `tfyolo`
|
|
11
|
+
# turns YOLO on (tf-yolo.sh on: the git-write ban stays, OpenCode stops asking too), starts Claude in
|
|
12
|
+
# this terminal, and turns YOLO off again when that session ends, however it ends.
|
|
13
|
+
#
|
|
14
|
+
# It refuses outside a TechieFlow project: the hooks that keep git writes refused live in the
|
|
15
|
+
# project, and bypass mode without them would let a commit through.
|
|
16
|
+
# The `tfyolo` command itself is a shell function put in ~/.bashrc / ~/.zshrc by tf-yolo-alias.sh.
|
|
17
|
+
# Exit: claude's own exit code; 2 when it could not start.
|
|
18
|
+
|
|
19
|
+
set -u
|
|
20
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
ROOT="$(cd "$HERE/../.." && pwd)"
|
|
22
|
+
if [[ ! -f "$ROOT/.tfcore/hooks/block-git.sh" || ! -f "$ROOT/.claude/settings.json" ]]; then
|
|
23
|
+
echo "tfyolo: $ROOT is not a TechieFlow project with its Claude Code hooks (no .tfcore/hooks/block-git.sh or .claude/settings.json); not starting in bypass mode." >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
grep -q 'block-git.sh' "$ROOT/.claude/settings.json" 2>/dev/null || {
|
|
27
|
+
echo "tfyolo: $ROOT/.claude/settings.json does not register block-git.sh, so git writes would not be refused; run update-framework.sh on this project first." >&2
|
|
28
|
+
exit 2
|
|
29
|
+
}
|
|
30
|
+
CLAUDE_BIN="${TF_CLAUDE_BIN:-$(command -v claude 2>/dev/null)}"
|
|
31
|
+
[[ -n "$CLAUDE_BIN" ]] || { echo "tfyolo: the claude command is not on PATH" >&2; exit 2; }
|
|
32
|
+
|
|
33
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
34
|
+
bash "$HERE/tf-yolo.sh" on --source tfyolo >/dev/null
|
|
35
|
+
off() { CLAUDE_PROJECT_DIR="$ROOT" bash "$HERE/tf-yolo.sh" off >/dev/null 2>&1; }
|
|
36
|
+
trap 'off' EXIT
|
|
37
|
+
trap 'off; exit 130' INT TERM
|
|
38
|
+
|
|
39
|
+
echo "tfyolo: YOLO on, Claude Code starting in bypass mode in $ROOT (git writes stay refused; YOLO goes off when this session ends)"
|
|
40
|
+
cd "$ROOT" || exit 2
|
|
41
|
+
"$CLAUDE_BIN" --permission-mode bypassPermissions "$@"
|
|
42
|
+
rc=$?
|
|
43
|
+
echo "tfyolo: session ended; YOLO off"
|
|
44
|
+
exit $rc
|
package/.tfcore/utils/tf-yolo.sh
CHANGED
|
@@ -8,6 +8,26 @@
|
|
|
8
8
|
# bash .tfcore/utils/tf-yolo.sh done [complete|blocked] ["<summary>"] goal finished → write the sentinel
|
|
9
9
|
# the tf-goal.sh supervisor stops on
|
|
10
10
|
# bash .tfcore/utils/tf-yolo.sh clear-done remove the sentinel (supervisor does this)
|
|
11
|
+
# bash .tfcore/utils/tf-yolo.sh sync put the harness permissions back when
|
|
12
|
+
# the flag has gone or expired (hooks call it)
|
|
13
|
+
#
|
|
14
|
+
# YOLO NEVER ASKS (owner, 2026-10-14: "if it's still asking for permission, what's the purpose of
|
|
15
|
+
# YOLO mode?"). While the flag is live both harnesses stop asking, and `off`, `done` and an expired
|
|
16
|
+
# flag put them back:
|
|
17
|
+
# - Claude Code: the hook .tfcore/hooks/yolo-allow.sh approves every tool call, in the session
|
|
18
|
+
# already running. Claude Code still asks where a hook's allow is not enough — an ask rule or one
|
|
19
|
+
# of its own safety checks ("requires the full permission pipeline") — and it refuses bypass mode
|
|
20
|
+
# from project or local settings ("only policy/user/flag settings may grant bypass mode"; both
|
|
21
|
+
# seen in its debug log, 2.1.299, 2026-10-14). Those prompts go only in a session started with
|
|
22
|
+
# `claude --permission-mode bypassPermissions`, which tf-goal.sh always uses.
|
|
23
|
+
# - OpenCode: the config file it reads last (.opencode/opencode.jsonc in a project) is rewritten
|
|
24
|
+
# with each "ask" turned to "allow" and every deny kept, and edits, web fetches, folders outside
|
|
25
|
+
# the project and repeated calls allowed; `off` puts its exact text back. OpenCode reads it when
|
|
26
|
+
# a session starts; the plugin's permission.ask approves everything while the flag is live
|
|
27
|
+
# (OpenCode 1.x).
|
|
28
|
+
# Version-control WRITES stay refused in both: the deny rules are kept, and block-git.sh refuses
|
|
29
|
+
# them before any permission check. What each file held before is kept under .tfcore/.session/ and
|
|
30
|
+
# restored exactly.
|
|
11
31
|
#
|
|
12
32
|
# YOLO is ON when ANY of these holds (checked in this order by block-git.sh too):
|
|
13
33
|
# 1. env TF_YOLO=1 (tf-goal.sh exports it for the whole run)
|
|
@@ -54,6 +74,8 @@ flag_live() { # exit 0 = flag present AND not expired
|
|
|
54
74
|
[[ -n "$(find "$FLAG" -mmin "+$(( TTL_HOURS * 60 ))" 2>/dev/null)" ]] && return 1
|
|
55
75
|
return 0
|
|
56
76
|
}
|
|
77
|
+
PERM_PY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/tf-yolo-perms.py"
|
|
78
|
+
perms() { python3 "$PERM_PY" "$1" "$ROOT" 2>/dev/null || true; } # on | off; never fails the caller
|
|
57
79
|
json_escape() { printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || printf '"%s"' "${1//\"/\\\"}"; }
|
|
58
80
|
|
|
59
81
|
cmd="${1:-status}"; shift || true
|
|
@@ -74,11 +96,16 @@ case "$cmd" in
|
|
|
74
96
|
"$(now_utc)" "$(json_escape "$SOURCE")" "$(json_escape "$GOAL")" "$(json_escape "${TF_HARNESS:-claude-code}")"
|
|
75
97
|
} > "$FLAG" 2>/dev/null || true
|
|
76
98
|
rm -f "$DONE" 2>/dev/null || true
|
|
77
|
-
|
|
99
|
+
perms on
|
|
100
|
+
echo "YOLO: ON (source=$SOURCE) — no confirmations; OpenCode never asks (from its next session); Claude Code approves every call it can (its own safety checks still ask unless the session was started with: claude --permission-mode bypassPermissions); deletes + read-only git allowed; git WRITES still blocked; run to completion."
|
|
78
101
|
;;
|
|
79
102
|
off)
|
|
80
103
|
rm -f "$FLAG" 2>/dev/null || true
|
|
81
|
-
|
|
104
|
+
perms off
|
|
105
|
+
echo "YOLO: OFF — back to ask-on-delete, no-git, confirm-at-phase-boundaries; OpenCode's asks restored."
|
|
106
|
+
;;
|
|
107
|
+
sync)
|
|
108
|
+
if [[ "${TF_YOLO:-0}" == "1" ]] || flag_live; then :; else perms off; fi
|
|
82
109
|
;;
|
|
83
110
|
is-on)
|
|
84
111
|
if [[ "${TF_YOLO:-0}" == "1" ]] || flag_live; then exit 0; else exit 1; fi
|
|
@@ -166,13 +193,14 @@ PY2
|
|
|
166
193
|
# unprompted and read-only git open and nothing on screen saying so. The
|
|
167
194
|
# supervisor reads $DONE, never $FLAG, so clearing it here stops nothing.
|
|
168
195
|
rm -f "$FLAG" 2>/dev/null || true
|
|
196
|
+
perms off
|
|
169
197
|
echo "GOAL-DONE ($OUTCOME) recorded at $DONE — the supervisor (tf-goal.sh) will stop. YOLO flag cleared."
|
|
170
198
|
;;
|
|
171
199
|
clear-done)
|
|
172
200
|
rm -f "$DONE" 2>/dev/null || true
|
|
173
201
|
;;
|
|
174
202
|
*)
|
|
175
|
-
echo "usage: tf-yolo.sh on|off|status|is-on|done [complete|blocked] [summary]|clear-done" >&2
|
|
203
|
+
echo "usage: tf-yolo.sh on|off|status|is-on|done [complete|blocked] [summary]|clear-done|sync" >&2
|
|
176
204
|
;;
|
|
177
205
|
esac
|
|
178
206
|
exit 0
|