@kaydash9999/banker-plugins 0.20.5 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +52 -0
  4. package/PRIVACY.md +25 -1
  5. package/README.md +63 -18
  6. package/bin/banker.js +173 -14
  7. package/bin/lib/codex-hooks.mjs +254 -0
  8. package/bin/lib/codex-routes.mjs +112 -0
  9. package/bin/lib/command-skill.cjs +19 -0
  10. package/bin/lib/telemetry-config.mjs +4 -1
  11. package/codex/command-skills/front-qa/SKILL.md +56 -0
  12. package/codex/command-skills/setup/SKILL.md +87 -0
  13. package/codex/hooks.json +29 -0
  14. package/codex/manifest.json +13 -10
  15. package/codex/skills/graceful-pause/SKILL.md +30 -0
  16. package/codex/skills/progress/SKILL.md +42 -0
  17. package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
  18. package/codex/skills/progress/scripts/progress.mjs +295 -0
  19. package/codex/transform-matrix.md +18 -13
  20. package/commands/setup.md +2 -2
  21. package/hooks/obsidize-hook.mjs +116 -4
  22. package/hooks/progress-plan.mjs +1 -1
  23. package/hooks/telemetry-count-codex.mjs +309 -0
  24. package/hooks/update-notify.mjs +105 -14
  25. package/package.json +1 -1
  26. package/skills/3d-intro-build/references/verify-intro.mjs +15 -1
  27. package/skills/anti-drm/SKILL.md +1 -1
  28. package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
  29. package/skills/append_wiki/SKILL.md +34 -8
  30. package/skills/arch-diagram/build_pptx_template.py +47 -28
  31. package/skills/audit-web-page/SKILL.md +1 -1
  32. package/skills/audit-web-page/harness.py +0 -1
  33. package/skills/compact-wiki/SKILL.md +37 -6
  34. package/skills/deep-research/SKILL.md +6 -0
  35. package/skills/lfm/SKILL.md +148 -0
  36. package/skills/lineage/SKILL.md +14 -4
  37. package/skills/lineage/lineage.py +1582 -516
  38. package/skills/motion-graphic-make/SKILL.md +1 -1
  39. package/skills/obsidizer/SKILL.md +44 -10
  40. package/skills/obsidizer/obsidize.mjs +42 -11
  41. package/skills/omc-patch/SKILL.md +6 -0
  42. package/skills/payload-mon/SKILL.md +3 -1
  43. package/skills/play-qa/SKILL.md +1 -1
  44. package/skills/ralph-qa/SKILL.md +1 -1
  45. package/skills/remains/SKILL.md +1 -1
  46. package/skills/rfp-author/SKILL.md +5 -3
  47. package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
  48. package/skills/rfp-author/scripts/md2docx.py +63 -34
  49. package/skills/setup-bypass-permissions/SKILL.md +28 -10
  50. package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
  51. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
  52. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
  53. package/skills/setup-omc-hud/SKILL.md +33 -3
  54. package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
  55. package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
  56. package/skills/smart-compact/SKILL.md +59 -13
  57. package/skills/smart-compact/scripts/codex-gate.mjs +385 -0
  58. package/skills/theme-selection/SKILL.md +1 -1
  59. package/skills/tone-compact/SKILL.md +1 -1
  60. package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
  61. package/skills/ultra-chain/SKILL.md +10 -6
  62. package/skills/ultra-interview/SKILL.md +13 -2
  63. package/skills/ultra-ui-qa/SKILL.md +1 -1
  64. package/skills/update-banker/SKILL.md +31 -6
  65. package/skills/vertical-pptx/SKILL.md +1 -1
  66. package/skills/vertical-pptx/build_a4p.js +20 -2
  67. package/skills/vertical-pptx/build_a4p.py +560 -359
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * smart-compact for Codex: a UserPromptSubmit hook that tells the session when its context passes a threshold.
4
+ *
5
+ * node codex-gate.mjs on [--threshold N] arm: save N (default 50), copy the hook, add it to hooks.json
6
+ * node codex-gate.mjs off disarm: take the hook out, with its copy, threshold and state
7
+ * node codex-gate.mjs status say whether it is armed and the hook is in place and current
8
+ * node codex-gate.mjs usage <rollout> print the context use the session record shows
9
+ * node codex-gate.mjs hook the hook itself (Codex runs it; reads the hook input on stdin)
10
+ *
11
+ * Codex gives a hook no context percentage, but every hook input carries transcript_path, the session record
12
+ * (rollout). Its token_count events hold last_token_usage and model_context_window, so the use of the last
13
+ * request over the window is the context use (measured on Codex 0.144.5).
14
+ * Files live in <CODEX_HOME> (default ~/.codex): smart-compact.json (threshold), smart-compact-state.json
15
+ * (sessions told), hooks.json.smart-compact.bak (hooks.json before each change) and the hook itself, copied to
16
+ * hooks-custom/banker-smart-compact-gate.mjs. Codex runs a trusted hook outside the sandbox, so the hook runs from
17
+ * that copy, where a sandboxed session cannot write, and not from a skill folder that may sit in a workspace.
18
+ * Codex trusts a hook by its handler and records the trust under the group's index: the threshold lives in a file
19
+ * so the command never changes with it, and the group is appended after the ones there, so OMX's hooks keep
20
+ * their indexes. On Windows Codex runs a hook command through PowerShell (measured on Codex 0.160.1), so the
21
+ * command there is `& '<node>' '<script>' hook`.
22
+ * The hook speaks once per session while the use stays at or over the threshold, and again after the use has
23
+ * dropped below it (after /compact). It never blocks a prompt: any error leaves it silent with exit 0.
24
+ * Exit status 1 means nothing was changed; 2 means an unexpected error stopped the command.
25
+ */
26
+ import {
27
+ chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync,
28
+ renameSync, rmSync, statSync, writeFileSync,
29
+ } from "node:fs";
30
+ import { homedir, tmpdir } from "node:os";
31
+ import { join, posix, win32 } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+
34
+ const SELF = fileURLToPath(import.meta.url);
35
+ const COPY_NAME = "banker-smart-compact-gate.mjs";
36
+ const KEEP_SESSIONS = 50;
37
+ const MAX_READ = 32 << 20;
38
+
39
+ export function paths(env = process.env) {
40
+ const home = env.CODEX_HOME || join(homedir(), ".codex");
41
+ return {
42
+ home,
43
+ hooks: join(home, "hooks.json"),
44
+ config: join(home, "smart-compact.json"),
45
+ state: join(home, "smart-compact-state.json"),
46
+ backup: join(home, "hooks.json.smart-compact.bak"),
47
+ copyDir: join(home, "hooks-custom"),
48
+ copy: join(home, "hooks-custom", COPY_NAME),
49
+ };
50
+ }
51
+
52
+ const isObject = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
53
+
54
+ // JSON from a regular file only: a FIFO or a device would stall the hook. null when missing, odd or unreadable.
55
+ function readJson(file) {
56
+ try {
57
+ return lstatSync(file).isFile() ? JSON.parse(readFileSync(file, "utf8")) : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ // A new file beside the target, given the target's mode when there is one, then renamed over it. The temp name is
64
+ // created exclusively and a link in the target's place is replaced, not followed. The temp file goes on failure.
65
+ function writeAtomic(file, data) {
66
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
67
+ try {
68
+ writeFileSync(tmp, data, { flag: "wx" });
69
+ const mode = modeOf(file);
70
+ if (mode !== null) chmodSync(tmp, mode);
71
+ renameSync(tmp, file);
72
+ } catch (e) {
73
+ rmSync(tmp, { force: true });
74
+ throw e;
75
+ }
76
+ }
77
+
78
+ function modeOf(file) {
79
+ try {
80
+ const st = lstatSync(file);
81
+ return st.isFile() ? st.mode & 0o7777 : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ const writeJson = (file, value) => writeAtomic(file, `${JSON.stringify(value, null, 2)}\n`);
88
+
89
+ // The last token_count with usage in a rollout, read from the end in growing spans, so a long session costs one
90
+ // tail read in the usual case; null past `max` bytes, for a file that is not regular (checked before opening, since
91
+ // opening a FIFO waits for a writer), or when none has usage yet (info is null on the first events of a session).
92
+ export function lastUsage(file, max = MAX_READ) {
93
+ if (!statSync(file).isFile()) return null;
94
+ const fd = openSync(file, "r");
95
+ try {
96
+ const size = fstatSync(fd).size;
97
+ for (let span = 1 << 18; ; span *= 4) {
98
+ const start = Math.max(0, size - Math.min(span, max));
99
+ const buf = Buffer.alloc(size - start);
100
+ readSync(fd, buf, 0, buf.length, start);
101
+ const found = scanUsage(buf.toString("utf8"), start > 0);
102
+ if (found || start === 0 || span >= max) return found;
103
+ }
104
+ } finally {
105
+ closeSync(fd);
106
+ }
107
+ }
108
+
109
+ function scanUsage(text, cutFirst) {
110
+ const lines = text.split("\n");
111
+ if (cutFirst) lines.shift();
112
+ for (let i = lines.length - 1; i >= 0; i--) {
113
+ const found = lines[i].includes('"token_count"') ? usageOf(lines[i]) : null;
114
+ if (found) return found;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ function usageOf(line) {
120
+ let rec;
121
+ try {
122
+ rec = JSON.parse(line);
123
+ } catch {
124
+ return null;
125
+ }
126
+ const info = rec && rec.payload && rec.payload.type === "token_count" ? rec.payload.info : null;
127
+ return info ? usageIn(info) : null;
128
+ }
129
+
130
+ function usageIn(info) {
131
+ const used = info.last_token_usage ? info.last_token_usage.total_tokens : NaN;
132
+ const win = info.model_context_window;
133
+ return Number.isFinite(used) && Number.isFinite(win) && win > 0 ? { used, window: win } : null;
134
+ }
135
+
136
+ export const percent = (u) => Math.round((u.used * 100) / u.window);
137
+
138
+ export function nudge(pct, threshold) {
139
+ return `smart-compact: 컨텍스트 사용률 ${pct}% (임계 ${threshold}%). 이번 응답에서 위생 체인을 실행한다: `
140
+ + "$banker-append_wiki, 그다음 한 줄 안내(다음 메시지가 resume 프롬프트이고 그 뒤 /copy, /compact, 붙여 넣기 순서), "
141
+ + "마지막으로 $banker-ready-compact. banker 를 Codex 플러그인으로 설치했으면 $banker:append_wiki, $banker:ready-compact 다. "
142
+ + "/copy, /compact, 붙여 넣기는 사용자가 한다.";
143
+ }
144
+
145
+ const text = (o, key) => (o && typeof o[key] === "string" ? o[key] : "");
146
+
147
+ // The hook's decision for one prompt: the text to inject, or "" for none. Mutates and returns the state.
148
+ export function decide(input, config, state) {
149
+ const sid = text(input, "session_id");
150
+ const file = text(input, "transcript_path");
151
+ const usage = config && sid && file ? lastUsage(file) : null;
152
+ if (!usage) return { text: "", state };
153
+ const pct = percent(usage);
154
+ if (pct < config.threshold) {
155
+ delete state[sid];
156
+ return { text: "", state };
157
+ }
158
+ if (state[sid]) return { text: "", state };
159
+ state[sid] = Date.now();
160
+ prune(state);
161
+ return { text: nudge(pct, config.threshold), state };
162
+ }
163
+
164
+ function prune(state) {
165
+ const ids = Object.keys(state).sort((a, b) => state[a] - state[b]);
166
+ for (const old of ids.slice(0, Math.max(0, ids.length - KEEP_SESSIONS))) delete state[old];
167
+ }
168
+
169
+ function validConfig(c) {
170
+ return isObject(c) && Number.isInteger(c.threshold) && c.threshold >= 1 && c.threshold <= 95 ? c : null;
171
+ }
172
+
173
+ async function runHook(p) {
174
+ try {
175
+ process.stdin.setEncoding("utf8");
176
+ let raw = "";
177
+ for await (const chunk of process.stdin) raw += chunk;
178
+ const read = readJson(p.state);
179
+ const before = isObject(read) ? read : {};
180
+ const was = JSON.stringify(before);
181
+ const { text: say, state } = decide(JSON.parse(raw), validConfig(readJson(p.config)), before);
182
+ if (JSON.stringify(state) !== was) writeJson(p.state, state);
183
+ if (say) {
184
+ process.stdout.write(JSON.stringify({
185
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: say },
186
+ }));
187
+ }
188
+ } catch {
189
+ /* a hook never blocks the prompt */
190
+ }
191
+ return 0;
192
+ }
193
+
194
+ // The command text, or null for a path the platform shell would still read into. POSIX: both paths in double
195
+ // quotes, refusing $, `, " and backslashes. Windows: PowerShell's call operator with single-quoted paths, every
196
+ // character PowerShell takes for a single quote (' and U+2018-U+201B) doubled.
197
+ export function formatCommand(node, script, win = process.platform === "win32") {
198
+ if (/[\r\n\0]/.test(node + script)) return null;
199
+ if (win) {
200
+ const q = (s) => s.replace(/['\u2018-\u201B]/g, "$&$&");
201
+ return `& '${q(node)}' '${q(script)}' hook`;
202
+ }
203
+ return /["$`\\]/.test(node + script) ? null : `"${node}" "${script}" hook`;
204
+ }
205
+
206
+ // The command `on` writes, or null when a location makes it unsafe. Codex runs a trusted hook outside the sandbox,
207
+ // so a node in the temp folder is refused, and when `on` itself runs inside Codex (CODEX_THREAD_ID), a node or a
208
+ // source script inside the workspace is refused too: the sandboxed session could change it first. Paths are judged
209
+ // by the target platform's rules.
210
+ export function hookCommand(node, script, opts = {}) {
211
+ const { win = process.platform === "win32", cwd = process.cwd(), codex = Boolean(process.env.CODEX_THREAD_ID) } = opts;
212
+ const P = win ? win32 : posix;
213
+ const under = (f, dir) => {
214
+ const r = P.relative(dir, f);
215
+ return r === "" || (!r.startsWith("..") && !P.isAbsolute(r));
216
+ };
217
+ if (under(node, tmpdir()) || (codex && [node, SELF].some((f) => under(f, cwd)))) return null;
218
+ return formatCommand(node, script, win);
219
+ }
220
+
221
+ const COPY_RE = new RegExp(`[/\\\\]${COPY_NAME.replace(/\./g, "\\.")}["']? hook$`);
222
+ const isOurs = (h) => isObject(h) && typeof h.command === "string" && COPY_RE.test(h.command);
223
+
224
+ // hooks.json as an object, or why it cannot be changed. A missing file reads as empty; a link, a file that is not
225
+ // JSON, or an error other than "not found" is refused, so OMX's hooks are never overwritten by mistake.
226
+ function loadHooks(file) {
227
+ const kind = fileKind(file);
228
+ if (kind === "missing") return { doc: { hooks: {} }, groups: [] };
229
+ if (kind) return { why: kind };
230
+ const doc = readJson(file);
231
+ if (!isObject(doc)) return { why: `${file} 이 JSON 객체가 아님` };
232
+ if (doc.hooks === undefined) doc.hooks = {};
233
+ if (!isObject(doc.hooks)) return { why: `${file} 의 hooks 가 객체가 아님` };
234
+ const ups = doc.hooks.UserPromptSubmit;
235
+ if (ups !== undefined && !Array.isArray(ups)) return { why: `${file} 의 UserPromptSubmit 이 배열이 아님` };
236
+ return { doc, groups: ups || [] };
237
+ }
238
+
239
+ // "" for a regular file, "missing", or why the file cannot be used.
240
+ function fileKind(file) {
241
+ try {
242
+ const st = lstatSync(file);
243
+ return st.isSymbolicLink() || !st.isFile() ? `${file} 이 일반 파일이 아님(링크나 폴더)` : "";
244
+ } catch (e) {
245
+ return e && e.code === "ENOENT" ? "missing" : `${file} 을 확인하지 못함(${e && e.code})`;
246
+ }
247
+ }
248
+
249
+ // Where our handler sits: [group index, handler index], or null.
250
+ function findOurs(groups) {
251
+ for (let g = 0; g < groups.length; g++) {
252
+ const hs = isObject(groups[g]) && Array.isArray(groups[g].hooks) ? groups[g].hooks : [];
253
+ const h = hs.findIndex(isOurs);
254
+ if (h >= 0) return [g, h];
255
+ }
256
+ return null;
257
+ }
258
+
259
+ function saveHooks(p, doc) {
260
+ if (existsSync(p.hooks)) writeAtomic(p.backup, readFileSync(p.hooks));
261
+ writeJson(p.hooks, doc);
262
+ }
263
+
264
+ // The hook copy in <CODEX_HOME>/hooks-custom, refreshed when this version differs. A folder or copy that is a
265
+ // link is refused.
266
+ function installCopy(p) {
267
+ mkdirSync(p.copyDir, { recursive: true });
268
+ if (lstatSync(p.copyDir).isSymbolicLink()) return `${p.copyDir} 이 링크임`;
269
+ if (existsSync(p.copy) && !lstatSync(p.copy).isFile()) return `${p.copy} 이 일반 파일이 아님`;
270
+ const mine = readFileSync(SELF);
271
+ if (!existsSync(p.copy) || !readFileSync(p.copy).equals(mine)) writeAtomic(p.copy, mine);
272
+ return "";
273
+ }
274
+
275
+ const TRUST = "Codex 를 다시 열면 뜨는 Hooks need review 창이나 /hooks 에서 이 훅을 신뢰하십시오. 신뢰 전에는 돌지 않습니다";
276
+
277
+ function placeHook(p, doc, groups, command) {
278
+ const at = findOurs(groups);
279
+ if (at && groups[at[0]].hooks[at[1]].command === command) return "훅은 이미 있음";
280
+ if (at) groups[at[0]].hooks[at[1]] = { ...groups[at[0]].hooks[at[1]], command, timeout: 5 };
281
+ else groups.push({ hooks: [{ type: "command", command, timeout: 5 }] });
282
+ doc.hooks.UserPromptSubmit = groups;
283
+ saveHooks(p, doc);
284
+ return at ? `훅 명령을 고쳤습니다. ${TRUST}` : `훅을 더했습니다. ${TRUST}`;
285
+ }
286
+
287
+ export function turnOn(p, threshold, node = process.execPath) {
288
+ if (!Number.isInteger(threshold) || threshold < 1 || threshold > 95) return { code: 1, msg: "임계값은 1~95 정수" };
289
+ const { doc, groups, why } = loadHooks(p.hooks);
290
+ if (why) return { code: 1, msg: `바꾸지 않음: ${why}` };
291
+ const command = hookCommand(node, p.copy);
292
+ if (!command) return { code: 1, msg: "바꾸지 않음: node 나 훅 경로가 안전하지 않음(셸이 읽는 글자, 임시 폴더의 node, Codex 작업 폴더 안의 node 나 스크립트). Codex 밖 터미널에서 실행하십시오" };
293
+ const bad = installCopy(p);
294
+ if (bad) return { code: 1, msg: `바꾸지 않음: ${bad}` };
295
+ const done = placeHook(p, doc, groups, command);
296
+ writeJson(p.config, { threshold });
297
+ return { code: 0, msg: `켰습니다(임계 ${threshold}%). ${done}` };
298
+ }
299
+
300
+ // Only our handler goes; a group it leaves empty goes too. Groups after a removed one move up an index, and
301
+ // Codex then asks to trust their hooks again, so the message says so.
302
+ export function turnOff(p) {
303
+ const { doc, groups, why } = loadHooks(p.hooks);
304
+ if (why) return { code: 1, msg: `바꾸지 않음: ${why}` };
305
+ const at = findOurs(groups);
306
+ let note = "켜져 있지 않았습니다(훅 없음). 설정만 지웠습니다";
307
+ if (at) {
308
+ const hs = groups[at[0]].hooks.filter((h) => !isOurs(h));
309
+ const shifts = hs.length === 0 && at[0] < groups.length - 1;
310
+ if (hs.length) groups[at[0]].hooks = hs;
311
+ else groups.splice(at[0], 1);
312
+ if (groups.length) doc.hooks.UserPromptSubmit = groups;
313
+ else delete doc.hooks.UserPromptSubmit;
314
+ saveHooks(p, doc);
315
+ note = shifts ? "껐습니다. 뒤에 있던 UserPromptSubmit 훅은 /hooks 에서 다시 신뢰해야 할 수 있습니다" : "껐습니다";
316
+ }
317
+ for (const f of [p.config, p.state, p.copy]) rmSync(f, { force: true });
318
+ return { code: 0, msg: note };
319
+ }
320
+
321
+ function staleness(p, groups, node) {
322
+ const at = findOurs(groups);
323
+ if (!at) return "no-hook";
324
+ if (groups[at[0]].hooks[at[1]].command !== formatCommand(node, p.copy)) return "stale";
325
+ try {
326
+ return readFileSync(p.copy).equals(readFileSync(SELF)) ? "" : "stale";
327
+ } catch {
328
+ return "stale";
329
+ }
330
+ }
331
+
332
+ export function status(p, node = process.execPath) {
333
+ const config = validConfig(readJson(p.config));
334
+ const { groups, why } = loadHooks(p.hooks);
335
+ if (why) return { code: 0, msg: `${config ? "켜짐 표시는 있으나 " : ""}hooks.json 을 읽지 못함: ${why}` };
336
+ const state = staleness(p, groups, node);
337
+ if (!config) return { code: 0, msg: state === "no-hook" ? "꺼짐" : "꺼짐(설정 없음, 훅만 남음). off 로 정리" };
338
+ if (state === "no-hook") return { code: 0, msg: "켜짐 표시는 있으나 훅이 없음(다른 도구나 손 편집이 지웠을 수 있음). on 을 다시 실행" };
339
+ if (state === "stale") return { code: 0, msg: "켜짐. 다만 훅 명령이나 사본이 이 판과 다름(node 경로가 바뀌었거나 banker 를 갱신함). on 을 다시 실행" };
340
+ return { code: 0, msg: `켜짐(임계 ${config.threshold}%). 알림이 없으면 /hooks 에서 이 훅을 신뢰했는지 확인` };
341
+ }
342
+
343
+ // on's arguments: --threshold N or --threshold=N, nothing else.
344
+ function parseOn(args) {
345
+ if (args.length === 0) return 50;
346
+ const one = args.length === 1 && /^--threshold=(\d+)$/.exec(args[0]);
347
+ if (one) return Number(one[1]);
348
+ return args.length === 2 && args[0] === "--threshold" && /^\d+$/.test(args[1]) ? Number(args[1]) : NaN;
349
+ }
350
+
351
+ function usageLine(file) {
352
+ const u = file ? lastUsage(file) : null;
353
+ return u ? { code: 0, msg: `${percent(u)}% (${u.used} / ${u.window} 토큰)` } : { code: 1, msg: "token_count 기록 없음" };
354
+ }
355
+
356
+ const COMMANDS = {
357
+ on: (p, rest) => turnOn(p, parseOn(rest)),
358
+ off: (p, rest) => (rest.length ? null : turnOff(p)),
359
+ status: (p, rest) => (rest.length ? null : status(p)),
360
+ usage: (p, rest) => (rest.length === 1 ? usageLine(rest[0]) : null),
361
+ };
362
+
363
+ async function main(argv) {
364
+ const [cmd = "status", ...rest] = argv;
365
+ const p = paths();
366
+ if (cmd === "hook") return runHook(p);
367
+ const run = Object.prototype.hasOwnProperty.call(COMMANDS, cmd) ? COMMANDS[cmd] : null;
368
+ const out = (run && run(p, rest)) || { code: 1, msg: "사용법: codex-gate.mjs on [--threshold N] | off | status | usage <rollout>" };
369
+ (out.code ? process.stderr : process.stdout).write(`smart-compact: ${out.msg}\n`);
370
+ return out.code;
371
+ }
372
+
373
+ const invoked = (() => {
374
+ try {
375
+ return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(SELF);
376
+ } catch {
377
+ return false;
378
+ }
379
+ })();
380
+ if (invoked) {
381
+ main(process.argv.slice(2)).then((c) => { process.exitCode = c; }, (e) => {
382
+ process.stderr.write(`smart-compact: 예기치 못한 오류: ${e && e.message}\n`);
383
+ process.exitCode = 2;
384
+ });
385
+ }
@@ -24,7 +24,7 @@ argument-hint: "list | css | json | widget | check [--design-md PATH] [--only SL
24
24
  | 런타임 | 스킬 디렉터리 |
25
25
  |---|---|
26
26
  | Claude Code | 이 파일 맨 위에 넣어 준 "Base directory for this skill:" 줄의 경로 |
27
- | Codex | `~/.codex/skills/banker-theme-selection/` 의 절대경로. 프로젝트 범위 설치면 `./.codex/skills/banker-theme-selection/` |
27
+ | Codex | Codex 스킬 목록이 보이는 이 SKILL.md 경로(file)의 폴더. npm 설치는 `~/.codex/skills/banker-theme-selection/`(프로젝트 범위 설치면 `./.codex/skills/banker-theme-selection/`), 플러그인 설치(`codex plugin add`)는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/theme-selection/`. 절대경로로 씀 |
28
28
 
29
29
  - `~` 는 따옴표 안에서 펼쳐지지 않으니 절대경로를 쓴다.
30
30
  2. 프로젝트에 `DESIGN.md` 가 있으면 `--design-md` 로 함께 먹인다. 없으면 번들 74종만 쓴다.
@@ -24,7 +24,7 @@ argument-hint: "on | off | status"
24
24
  | 런타임 | `--runtime` | 스킬 디렉터리 |
25
25
  |---|---|---|
26
26
  | Claude Code | `claude` | 스킬 맨 위 "Base directory for this skill:" 경로 |
27
- | Codex | `codex` | `~/.codex/skills/banker-tone-compact/` 의 절대경로. 프로젝트 범위 설치면 `./.codex/skills/banker-tone-compact/` |
27
+ | Codex | `codex` | Codex 스킬 목록이 보이는 이 SKILL.md 경로(file)의 폴더. npm 설치는 `~/.codex/skills/banker-tone-compact/`(프로젝트 범위 설치면 `./.codex/skills/banker-tone-compact/`), 플러그인 설치(`codex plugin add`)는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/tone-compact/`. 절대경로로 씀 |
28
28
 
29
29
  3. 결과 처리
30
30
 
@@ -186,7 +186,7 @@ export function writeFresh(file, data, mode = 0o644) {
186
186
  const tmp = `${file}.${process.pid}.tmp`;
187
187
  rmSync(tmp, { force: true });
188
188
  try {
189
- writeFileSync(tmp, data, { flag: "wx", mode });
189
+ writeNew(tmp, data, mode);
190
190
  renameOver(tmp, file);
191
191
  } catch (e) {
192
192
  rmSync(tmp, { force: true });
@@ -194,6 +194,48 @@ export function writeFresh(file, data, mode = 0o644) {
194
194
  }
195
195
  }
196
196
 
197
+ // Opens a file that is not there yet, as "wx" does, and makes sure the name is the file made. macOS and Linux refuse
198
+ // any name that is there, a link to nothing among them (EEXIST). Windows follows a link to nothing and makes the file
199
+ // it leads to: then that file is removed again when it is still the empty one made, and the open is refused with
200
+ // EEXIST as elsewhere. A look before the open catches such a link; this catches one planted between the look and the
201
+ // open. A look that fails after the open leaves the file to the caller, as a plain "wx" would.
202
+ export function openNew(path, mode) {
203
+ const fd = openSync(path, "wx", mode);
204
+ let made = null;
205
+ let st;
206
+ try {
207
+ made = fstatSync(fd, { bigint: true });
208
+ st = lstatSync(path, { bigint: true });
209
+ } catch {
210
+ return fd;
211
+ }
212
+ if (!st.isSymbolicLink() && (made.ino === 0n || sameInode(made, st))) return fd;
213
+ closeSync(fd);
214
+ if (st.isSymbolicLink()) dropFollowed(path, made);
215
+ throw Object.assign(new Error(`EEXIST: file already exists, open '${path}'`), { code: "EEXIST", syscall: "open", path });
216
+ }
217
+
218
+ // Removes the file a "wx" open made through the link at path, when it is still the empty file that open made.
219
+ function dropFollowed(path, made) {
220
+ try {
221
+ const target = realpathSync(path);
222
+ const st = statSync(target, { bigint: true });
223
+ if (sameInode(made, st) && st.size === 0n) rmSync(target, { force: true });
224
+ } catch {
225
+ // gone already, or the link changed again: nothing of ours to take back
226
+ }
227
+ }
228
+
229
+ // Writes data to a file that is not there yet (openNew) and closes it.
230
+ function writeNew(path, data, mode) {
231
+ const fd = openNew(path, mode);
232
+ try {
233
+ writeFileSync(fd, data);
234
+ } finally {
235
+ closeSync(fd);
236
+ }
237
+ }
238
+
197
239
  // Windows does not rename a file over a link to a folder: there the link goes first, and the folder it
198
240
  // leads to stays.
199
241
  function renameOver(tmp, file) {
@@ -510,13 +552,14 @@ function writeBlocked(changes) {
510
552
  // whatever the umask allows more. A file left empty is removed, unless it is a
511
553
  // link or has other names (a hard link), whose other names would keep the old
512
554
  // text: it is emptied instead. Codex skips an empty file anyway.
513
- // A file that was not there when it was read is created only if it still is not ("wx"): a name that appeared in
555
+ // A file that was not there when it was read is created only if it still is not (openNew): a name that appeared in
514
556
  // between, a link planted there among them, is not written through.
515
557
  function apply(t, changes) {
516
558
  for (const { file, after } of changes) {
517
559
  if (file.bytes !== null) writeFresh(`${file.path}.${t.slug}.bak`, file.bytes, 0o600);
518
560
  if (after === "" && !isLink(file.path) && !hasOtherNames(file.path)) rmSync(file.path, { force: true });
519
- else writeFileSync(file.path, after, { mode: 0o644, flag: file.bytes === null ? "wx" : "w" });
561
+ else if (file.bytes === null) writeNew(file.path, after, 0o644);
562
+ else writeFileSync(file.path, after, { mode: 0o644, flag: "w" });
520
563
  }
521
564
  }
522
565
 
@@ -568,9 +611,19 @@ const SLEEP = new Int32Array(new SharedArrayBuffer(4));
568
611
  // when it may still be there (no stats or no inode number to prove it is that file, or the share could not say).
569
612
  // Any other error (permissions, EIO) throws, after the same taking away.
570
613
  function tryLock(path, token) {
614
+ // A name that is there in any form is held. Windows opens "wx" through a link to nothing and makes the file it leads
615
+ // to; macOS and Linux answer EEXIST for it below, and openNew takes back a file Windows made through a link planted
616
+ // after this look. A look that fails leaves the answer to the open, as before.
617
+ let there = false;
618
+ try {
619
+ there = lstatSync(path, { throwIfNoEntry: false }) !== undefined;
620
+ } catch {
621
+ // ESTALE and the like: the open below answers
622
+ }
623
+ if (there) return "held";
571
624
  let fd;
572
625
  try {
573
- fd = openSync(path, "wx", 0o600);
626
+ fd = openNew(path, 0o600);
574
627
  } catch (e) {
575
628
  if (e.code === "EEXIST") return "held";
576
629
  if (e.code === "ESTALE") return "stale";
@@ -53,6 +53,7 @@ ultra-chain 이 직접 하는 일은 여섯 가지다.
53
53
  | 항목 | Claude Code (OMC) | Codex (OMX) |
54
54
  |---|---|---|
55
55
  | 1 자료 조사 | `Skill("banker:deep-research")` | `$banker-deep-research` |
56
+ | 1 전문가 작업 방식 | `Skill("banker:lfm")` | `$banker-lfm` |
56
57
  | 2 모호성 제거 | `Skill("banker:ultra-interview")` | `$banker-ultra-interview` |
57
58
  | 3 합의 계획 | `Skill("oh-my-claudecode:ralplan")` `--deliberate`, 이어서 `Skill("banker:ralph-qa")` | `$ralplan --deliberate`, 이어서 `$banker-ralph-qa` |
58
59
  | 5 구현 | `Skill("oh-my-claudecode:ralph")` `--critic=critic` | `$ralph` 뒤 이 스킬의 critic 루프(OMX `ralph` 에는 `--critic` 이 없다) |
@@ -64,7 +65,7 @@ ultra-chain 이 직접 하는 일은 여섯 가지다.
64
65
  | 이어 하기 메모 | OMC 작업 노트에 덧붙이기(`notepad_write_working`). 우선 노트는 통째로 바뀌므로 쓰지 않음 | `.omx/notepad.md` 에 한 줄 덧붙이기 |
65
66
  | 실행 기록 기본 폴더 | `.omc/ultra-chain/` | `.omx/ultra-chain/` |
66
67
 
67
- - Codex 에는 Skill 도구가 없다. 위 표의 `$이름` 은 그 스킬의 `SKILL.md` 를 읽고 그대로 따른다는 뜻이다. banker 스킬은 `<S>/../banker-<이름>/SKILL.md`, OMX 스킬은 `~/.codex/skills/<이름>/SKILL.md` 다. 그 스킬이 상태 기록을 요구하면 그 지시대로 직접 기록한다. 큰 `SKILL.md`(ralph-qa 는 약 71KB)는 셸 출력 한도에서 잘리니 나눠 읽는다.
68
+ - Codex 에는 Skill 도구가 없다. 위 표의 `$이름` 은 그 스킬의 `SKILL.md` 를 읽고 그대로 따른다는 뜻이다. banker 스킬은 npm 설치면 `<S>/../banker-<이름>/SKILL.md`, Codex 플러그인 설치(`<S>` 가 `plugins/cache/` 아래, 스킬 이름 `banker:<이름>`)면 `<S>/../<이름>/SKILL.md` 다. 표의 `$banker-<이름>` 은 플러그인 설치에서 `$banker:<이름>` 이다. OMX 스킬은 `~/.codex/skills/<이름>/SKILL.md` 다. 그 스킬이 상태 기록을 요구하면 그 지시대로 직접 기록한다. 큰 `SKILL.md`(ralph-qa 는 약 71KB)는 셸 출력 한도에서 잘리니 나눠 읽는다.
68
69
  - Codex 의 하위 에이전트(critic, 시각 판정)는 저자 대화를 넘기지 않는다. multi-agent v2 는 `fork_turns="none"` 을 주고, v1 은 `fork_context` 를 주지 않는다. 판정을 받으면 `close_agent` 로 닫는다.
69
70
  - Codex 환경 지시가 역할(`agent_type`)을 요구하면, 고정 모델이 세션 모델과 같은 역할을 쓰고 지시 앞머리에 파일 쓰기 금지를 적는다. 그런 역할이 없으면 역할 없이 띄운다. 쓴 역할과 모델, 강도를 판정 기록의 `note` 에 적는다.
70
71
  - Codex 의 오래 도는 명령은 기다리는 시간을 박스 `--timeout` 보다 길게 둔다. 기다리다 끊기면 박스 정리와 결과 JSON 이 남지 않는다.
@@ -190,16 +191,18 @@ node "<S>/references/chain-ledger.mjs" event "<run>" --stage research --status d
190
191
 
191
192
  ## 1. 자료 조사
192
193
 
193
- - deep-research 를 부른다. 넘기는 질문은 미리 좁히고, 인자에 "추가 질문 금지" 를 적는다. 범위 질문은 2단계가 맡는다. 질문은 공개 웹 검색어가 되므로 저장소 코드, 내부 이름, 비밀값을 넣지 않는다.
194
+ - deep-research 를 부른다. 넘기는 질문은 미리 좁히고, 인자에 "추가 질문 금지" 와 "전문가 작업 방식은 lfm 이 맡음" 을 적는다. 범위 질문은 2단계가 맡는다. 질문은 공개 웹 검색어가 되므로 저장소 코드, 내부 이름, 비밀값을 넣지 않는다.
194
195
  - 저장소 사실(구조, 시험 명령과 시험 도구, 빌드 방법, 관련 코드 위치, CI)은 읽기 전용 탐색으로 모은다.
195
196
  - 둘을 `<run>/01-research.md` 하나로 저장한다. 조사 보고, 그다음 `## 저장소 사실` 절이다. 조사 스킬은 보고를 대화에만 내므로 연쇄가 파일로 남긴다. 수동 경로였다면 충실도 한계를 그대로 옮긴다.
197
+ - 같은 단계에서 `lfm` 을 요청의 분야로 부른다. 인자는 `--no-questions --out <run>/01-masters.md` 다. 그 분야 최고 전문가 그룹이 이 일을 어떻게 하는지와 계획 적용 항목(`M-1` …)이 남는다. 검색어 규칙은 deep-research 와 같다(저장소 코드, 내부 이름, 비밀값을 넣지 않는다).
198
+ - `lfm` 이 "웹 조사 불가" 로 끝나면 `01-masters.md` 에 그 사실만 적고 진행한다. 3단계는 "전문가 작업 방식 없음" 으로 계획한다.
196
199
  - 게이트: 살아남은 주장마다 출처가 있고, 폐기된 주장과 한계가 적혀 있다. 주장이 0개면 게이트를 지나지 않은 것이다.
197
200
  - deep-research 가 "웹 조사 불가" 로 끝나면(네트워크 없음) 이 단계를 `skipped` 와 이유 "네트워크 없음" 으로 기록하고, 저장소 사실만 `01-research.md` 에 남긴 채 2단계로 간다. 사용자에게는 웹 조사를 하지 못했다고 알린다.
198
201
  - `--skip=research` 면 이 단계를 `skipped` 로 기록한다. 2단계의 ultra-interview 가 자기 조사를 한다.
199
202
 
200
203
  ## 2. 모호성 제거
201
204
 
202
- - ultra-interview 를 부른다. `01-research.md` 를 근거로 넘기고, 거기서 답하지 못한 항목만 더 조사하게 한다.
205
+ - ultra-interview 를 부른다. `01-research.md` 와 `01-masters.md` 를 근거로 넘기고, 거기서 답하지 못한 항목만 더 조사하게 한다. `01-masters.md` 가 있으면 `lfm` 을 다시 부르지 말라고 적는다.
203
206
  - `--auto` 면 인자에 "사람에게 묻지 않는다. P 항목은 보수적 가정으로 답하고 가정임을 표시한다" 를 넣는다. 이 지시는 ultra-interview 의 "가정을 사람이 훑기 전에는 판정하지 않는다" 규칙보다 앞선다. 사용자가 `--auto` 로 묻지 말라고 명시했기 때문이다. 그래서 이 결과는 통과가 아니라 "사람 확인 없음" 으로 기록한다. 보수적인 쪽이 없는 핵심 P(기능의 뜻 자체를 정하는 항목)가 남으면 멈추고 보고한다.
204
207
  - 결과를 `<run>/02-spec.md` 로 저장한다.
205
208
  - 번호 붙은 항목과 E, I, P 분류, 근거(URL, 파일 경로), 답, 가정
@@ -213,7 +216,8 @@ node "<S>/references/chain-ledger.mjs" event "<run>" --stage research --status d
213
216
  ralplan 은 계획 작성자 외에 Architect 와 Critic 두 검토자로 합의하고, 검토자를 늘리는 옵션이 없다. 그래서 ralplan 합의 뒤 ralph-qa 의 독립 검토자 3명 이상이 같은 계획을 다시 판정한다. Architect 와 Critic 은 이 3명에 세지 않는다.
214
217
 
215
218
  1. ralplan 을 `--deliberate` 로 부른다. `--interactive` 는 주지 않는다. 인자에 아래를 함께 넘긴다.
216
- - 요청 원문, `02-spec.md` 경로와 수용 기준, `01-research.md` 경로
219
+ - 요청 원문, `02-spec.md` 경로와 수용 기준, `01-research.md` 와 `01-masters.md` 경로
220
+ - 계획에 `## 전문가 작업 방식 적용` 절을 두라는 지시. `01-masters.md` 의 `M-n` 마다 판정(적용, 조정, 제외)과 계획의 어느 단계에 넣었는지, 조정과 제외는 그 이유를 적는다. 판정을 바꾸면(예: `lfm` 이 적용인데 계획은 제외) 이유를 적는다
217
221
  - 계획 저장 경로: Claude Code 는 `.omc/plans/ultra-chain-<run id>.md`. Codex 는 OMX 규칙대로 `.omx/plans/` 에 쓴다
218
222
  - Codex: 인터뷰가 끝났으니 `$deep-interview` 를 돌리지 말라는 말
219
223
  - 계획에 넣을 것
@@ -233,7 +237,7 @@ ralplan 은 계획 작성자 외에 Architect 와 Critic 두 검토자로 합의
233
237
  - Claude Code: 1 에서 지정한 경로, 또는 ralplan 이 보고한 계획 경로다. 둘 다 없으면 이 단계가 시작된 뒤 `.omc/plans/` 에 쓰였고 `pending approval` 표지가 있는 파일을 고른다. 검토 문서(이름이 `-critic.md`, `-architect.md`, `-review.md` 로 끝남)와 `open-questions.md` 는 고르지 않는다. 원본 경로는 `summary` 에 적는다.
234
238
  - Codex: 이번에 쓰인 `.omx/plans/prd-*.md` 와 `test-spec-*.md` 를 차례로 붙이고, 원본 경로를 맨 위에 적는다.
235
239
  4. ralph-qa 를 `--lens=plan --agents=<agents> --max=<max>` 로 부른다. 외부 좌석 플래그(`--codex`, `--gemini`, `--opencode`)는 사용자가 준 것만 넘긴다.
236
- - 검증 대상: `03-plan.md`(구현 전이라 `<base>` 대비 diff 가 없으니 원문으로 붙인다). 수용 기준: `02-spec.md` 의 AC 와 deliberate 필수 항목(사전 부검, 확장 시험 계획).
240
+ - 검증 대상: `03-plan.md`(구현 전이라 `<base>` 대비 diff 가 없으니 원문으로 붙인다). 수용 기준: `02-spec.md` 의 AC 와 deliberate 필수 항목(사전 부검, 확장 시험 계획), 그리고 `01-masters.md` 의 `M-n` 이 모두 `## 전문가 작업 방식 적용` 절에 판정과 함께 있는지(조정과 제외에 이유가 있는지).
237
241
  - 모든 좌석의 지시에 "저자의 주장이 틀렸다는 증거를 먼저 찾는 적대적 검토자로 판정한다" 를 넣는다. 렌즈 배정은 ralph-qa 그대로다. ralph-qa 의 기본 배정에서는 적대적 반증 렌즈가 한 좌석뿐이라, 이 문장이 있어야 검토자 모두가 적대적이다.
238
242
  - 출처-독립 검토자에게 줄 "사용자 원 요청" 은 `task.md` 와 `02-spec.md` 의 사용자 답변 원문이다.
239
243
  - ralph-qa 가 외부 모델을 물으면 여기서 한 번만 고르게 하고, 고른 모델을 기록한다. 6단계에는 `--gemini=<그 모델>` 처럼 값을 박아 넘긴다.
@@ -420,7 +424,7 @@ node "<S>/references/chain-report.mjs" "<run>"
420
424
 
421
425
  압축(compaction)이나 중단 뒤에는 다음 순서로 잇는다.
422
426
 
423
- 1. 이 스킬을 다시 부른다: `--run=<실행 폴더> --from=<단계>`(Claude Code `Skill("banker:ultra-chain")`, Codex `$banker-ultra-chain`). 지시와 `<S>` 를 되찾기 위해서다. 실행 폴더는 이어 하기 메모나 압축 요약에서 찾는다.
427
+ 1. 이 스킬을 다시 부른다: `--run=<실행 폴더> --from=<단계>`(Claude Code `Skill("banker:ultra-chain")`, Codex `$banker-ultra-chain`, 플러그인 설치면 `$banker:ultra-chain`). 지시와 `<S>` 를 되찾기 위해서다. 실행 폴더는 이어 하기 메모나 압축 요약에서 찾는다.
424
428
  2. `node "<S>/references/chain-ledger.mjs" show "<run>"` 로 `current`(다시 시작할 단계), 단계별 `lastVerdict`(마지막 판정과 blocker), `maxRound` 를 본다. 멈추거나 실패한 실행은 그 단계가 `current` 다. 원래 플래그는 `task.md` 첫 줄과 `preflight` 기록에 있다.
425
429
  3. 끊길 때 돌던 작업(박스 시험, 검토 좌석, 서버)이 끝났는지 확인한다. 끝나지 않았으면 알림을 기다리거나 멈춘다. 끊기기 전 실행의 늦은 판정은 쓰지 않는다.
426
430
  4. 돌던 ralph-qa 실행은 되살리지 않는다. 좌석과 끈끈한 반대는 한 실행 안에서만 유지되기 때문이다.
@@ -99,6 +99,16 @@ description: "리서치를 선행해 사람에게 묻는 질문을 최소화하
99
99
  - **공식 문서** (해당 기술·제품의 1차 출처)
100
100
  - **우수사례 / best practice**
101
101
  - **요청에 걸리는 공개 규제 정보** (규정·법령·시행세칙·규칙·기록)
102
+ - **그 분야 최고 전문가 그룹의 작업 방식** (`lfm`)
103
+
104
+ **전문가 작업 방식 (`lfm`):**
105
+ - 요청의 분야로 `lfm` 을 부른다. 인자에 `--no-questions` 를 준다. 이 스킬처럼 질문을 늘리지 않기 위해서다.
106
+ - 결과의 계획 적용 항목(`M-1` …)을 이 인터뷰에 쓴다.
107
+ - "적용" 이나 "조정" 판정의 관행이 답하는 항목은 E 로 답한다. 근거는 `lfm` 결과의 1차 출처 URL 이다. 이 작업에 맞는지는 사람이 가정을 훑으며 확인한다.
108
+ - 품질 기준 후보는 수용 기준 후보로 가정에 넣는다.
109
+ - 관행이 이 작업과 맞는지 사람의 판단이 필요하면(비용, 팀 구성, 일정) P 로 둔다.
110
+ - 결과에 `## 전문가 작업 방식` 절을 두고, `M-n` 마다 판정과 이 인터뷰의 어느 항목에 반영했는지 적는다. 다음 단계의 계획이 이 절을 그대로 받는다.
111
+ - `lfm` 이 "웹 조사 불가" 로 끝나면 그 사실을 이 절에 적고 진행한다.
102
112
 
103
113
  **규제 정보 접근 규칙:**
104
114
  - **엔드포인트·조회 파라미터·API 키를 하드코딩하지 마라.** 관할·기관·시점마다 다르고 이 스킬은 그것을 검증한 적이 없다. 하드코딩된 경로는 조용히 죽거나, 더 나쁘게는 엉뚱한 문서를 준다.
@@ -108,7 +118,7 @@ description: "리서치를 선행해 사람에게 묻는 질문을 최소화하
108
118
  - **규제 해석은 이 스킬의 산출물이 아니다.** 조문을 근거로 항목을 잠글 수는 있어도, 법적 판단이 필요한 자리는 P 로 두고 사람에게 넘긴다.
109
119
 
110
120
  **`deep-research` 위임:**
111
- - 넘길 때 **"추가 질문 금지" 를 인자에 명시한다.**
121
+ - 넘길 때 **"추가 질문 금지" 를 인자에 명시한다.** `lfm` 을 따로 부르므로 "전문가 작업 방식은 lfm 이 맡음" 도 적는다.
112
122
  - 이유: 명시하지 않으면 사용자는 리서치 단계에서 한 번, 인터뷰 단계에서 또 한 번, 총 두 번 질문받는다. **질문을 줄이려고 부른 리서치가 질문을 늘린다.** 그러면 이 스킬은 존재 이유를 훼손당한다.
113
123
  - 넘기는 질문은 **이쪽에서 이미 좁혀서** 넘긴다. 되물을 여지를 안 남기는 것이 인자에 적는 금지보다 확실하다.
114
124
 
@@ -144,6 +154,7 @@ E 후보에 리서치를 돌린다. **질문 전이다.**
144
154
  - 항목마다 근거 URL 을 붙인다. URL 없는 답은 채택하지 않는다.
145
155
  - 검색 수단이 런타임에 없으면 **없다고 보고**하고, E 후보를 전부 P 로 내린 뒤 그 사실을 명시한다.
146
156
  - 수단이 있어도 호출이 모두 실패하면(네트워크 없음) 같다. "조사하지 못함: <오류>" 로 보고하고 E 후보를 전부 P 로 내린다.
157
+ - 같은 단계에서 `lfm` 으로 그 분야 전문가 그룹의 작업 방식을 조사한다(위 "전문가 작업 방식" 참조).
147
158
 
148
159
  ### 2. E/I/P 판정
149
160
  전 항목을 분류한다. 애매하면 P.
@@ -230,4 +241,4 @@ E 후보에 리서치를 돌린다. **질문 전이다.**
230
241
 
231
242
  ARGUMENTS: [--threshold <퍼센트>] [--no-research] [요청/맥락] (없으면 현재 작업의 요청에 적용)
232
243
  - `--threshold`: 기본 3%. 올리면 게이트가 무르다. 내리는 것은 항목 33개 이하 구간에서 무의미하다(이미 미해결 0개가 조건이다). 바꿨으면 **바꾼 값을 보고에 적는다.**
233
- - `--no-research`: 리서치를 끈다. E 축이 죽어 전 항목이 P 로 떨어지고 **질문이 늘어난다.** 이 스킬의 델타 하나가 사라지므로, 켜서 부를 이유가 없으면 쓰지 마라.
244
+ - `--no-research`: 리서치를 끈다(`lfm` 도 부르지 않는다). E 축이 죽어 전 항목이 P 로 떨어지고 **질문이 늘어난다.** 이 스킬의 델타 하나가 사라지므로, 켜서 부를 이유가 없으면 쓰지 마라.
@@ -11,7 +11,7 @@ description: "웹 UI 를 Playwright 로 렌더해 디자인 원본(PDF 나 스
11
11
  ## 0. 전제조건: playwright (없으면 설치부터)
12
12
  이 스킬은 playwright 브라우저가 있어야 동작한다. **진행 전 설치 여부를 먼저 확인**하고, 없으면 **설치부터 안내**한다(추정·임의 재설치 금지):
13
13
  - 확인: `ls "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" 2>/dev/null | grep -qi chromium && echo ok`
14
- - 미설치면 먼저 `setup-playwright` 스킬로 설치(Claude Code: `/banker:setup` → setup-playwright / Codex: `banker-setup-playwright`). 설치·검증 후 이 스킬을 이어서 진행한다.
14
+ - 미설치면 먼저 `setup-playwright` 스킬로 설치(Claude Code: `/banker:setup` → setup-playwright / Codex: `banker-setup-playwright`, 플러그인 설치면 `banker:setup-playwright`). 설치·검증 후 이 스킬을 이어서 진행한다.
15
15
 
16
16
  ## When to use
17
17
  - "ultra ui qa", "UI 대조", "디자인 PDF와 대조", "내부개념/어색한 내용 점검", "표 title·차트 크기 비교"