@kaydash9999/banker-plugins 0.21.0 → 0.23.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 (129) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +69 -0
  4. package/PRIVACY.md +8 -3
  5. package/README.md +78 -15
  6. package/bin/banker.js +154 -32
  7. package/bin/lib/codex-hooks.mjs +123 -20
  8. package/bin/lib/codex-names.mjs +4 -0
  9. package/bin/lib/codex-routes.mjs +204 -0
  10. package/bin/lib/command-skill.cjs +50 -0
  11. package/bin/lib/telemetry-config.mjs +2 -1
  12. package/codex/command-skills/front-qa/SKILL.md +56 -0
  13. package/codex/command-skills/setup/SKILL.md +87 -0
  14. package/codex/hooks.json +29 -0
  15. package/codex/manifest.json +18 -15
  16. package/codex/skills/graceful-pause/SKILL.md +2 -2
  17. package/codex/skills/progress/scripts/progress.mjs +44 -14
  18. package/codex/skills/teamwork/SKILL.md +137 -0
  19. package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
  20. package/codex/transform-matrix.md +5 -3
  21. package/commands/setup.md +2 -2
  22. package/hooks/obsidize-hook.mjs +75 -32
  23. package/hooks/telemetry-count-codex.mjs +303 -35
  24. package/hooks/update-fetch.mjs +2 -1
  25. package/hooks/update-notify.mjs +146 -13
  26. package/package.json +3 -1
  27. package/skills/3d-intro-build/SKILL.md +101 -20
  28. package/skills/3d-intro-build/references/NOTICE.md +9 -2
  29. package/skills/3d-intro-build/references/assemble.mjs +113 -22
  30. package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
  31. package/skills/3d-intro-build/references/clip-post.mjs +443 -0
  32. package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
  33. package/skills/3d-intro-build/references/intro-fixes.css +11 -0
  34. package/skills/3d-intro-build/references/prompts.md +51 -4
  35. package/skills/3d-intro-build/references/scrub-engine.js +1 -0
  36. package/skills/3d-intro-build/references/step-nav.css +29 -0
  37. package/skills/3d-intro-build/references/step-nav.js +277 -106
  38. package/skills/3d-intro-build/references/verify-intro.mjs +387 -32
  39. package/skills/3d-intro-build/references/verify.md +6 -0
  40. package/skills/3d-intro-build/references/video-pool.mjs +142 -12
  41. package/skills/3d-intro-setup/SKILL.md +20 -7
  42. package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
  43. package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
  44. package/skills/all-in-one/SKILL.md +125 -86
  45. package/skills/anti-drm/SKILL.md +1 -1
  46. package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
  47. package/skills/arch-diagram/build_pptx_template.py +47 -28
  48. package/skills/audit-web-page/SKILL.md +1 -1
  49. package/skills/audit-web-page/harness.py +0 -1
  50. package/skills/lineage/lineage.py +1435 -533
  51. package/skills/motion-graphic-make/SKILL.md +1 -1
  52. package/skills/obsidizer/SKILL.md +20 -5
  53. package/skills/omc-patch/SKILL.md +1 -1
  54. package/skills/payload-mon/SKILL.md +2 -2
  55. package/skills/play-qa/SKILL.md +7 -5
  56. package/skills/progress-mon/README.md +162 -0
  57. package/skills/progress-mon/SKILL.md +165 -0
  58. package/skills/progress-mon/assets/fixture/README.md +1 -0
  59. package/skills/progress-mon/assets/fixture/arch.json +33 -0
  60. package/skills/progress-mon/assets/fixture/commits.json +7 -0
  61. package/skills/progress-mon/assets/fixture/config.json +30 -0
  62. package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
  63. package/skills/progress-mon/assets/fixture/flow.json +44 -0
  64. package/skills/progress-mon/assets/fixture/items.json +14 -0
  65. package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
  66. package/skills/progress-mon/assets/fixture/reqs.json +11 -0
  67. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
  68. package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
  69. package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
  70. package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
  71. package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
  72. package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
  73. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
  74. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
  75. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
  76. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
  77. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
  78. package/skills/progress-mon/references/arch.md +280 -0
  79. package/skills/progress-mon/references/authoring.md +199 -0
  80. package/skills/progress-mon/references/config.md +115 -0
  81. package/skills/progress-mon/references/flow.md +75 -0
  82. package/skills/progress-mon/references/inventory.md +58 -0
  83. package/skills/progress-mon/references/items.md +74 -0
  84. package/skills/progress-mon/references/req-spec.md +74 -0
  85. package/skills/progress-mon/references/reqs.md +30 -0
  86. package/skills/progress-mon/scripts/index.js +31 -0
  87. package/skills/progress-mon/scripts/pm-check.mjs +526 -0
  88. package/skills/progress-mon/scripts/pm-config.mjs +341 -0
  89. package/skills/progress-mon/scripts/pm-data.mjs +394 -0
  90. package/skills/progress-mon/scripts/pm-html.mjs +77 -0
  91. package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
  92. package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
  93. package/skills/progress-mon/scripts/pm-server.mjs +735 -0
  94. package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
  95. package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
  96. package/skills/ralph-qa/SKILL.md +105 -16
  97. package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
  98. package/skills/ralph-qa/references/payload-scan.mjs +19 -2
  99. package/skills/ralph-qa/references/run-folder.mjs +299 -0
  100. package/skills/ralph-qa/references/seat-payload.mjs +60 -13
  101. package/skills/ralph-qa/references/seat-run.mjs +159 -155
  102. package/skills/remains/SKILL.md +2 -1
  103. package/skills/remains/scripts/boxes.mjs +6 -2
  104. package/skills/rfp-author/scripts/md2docx.py +63 -34
  105. package/skills/setup-bypass-permissions/SKILL.md +2 -2
  106. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
  107. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
  108. package/skills/setup-mcp/SKILL.md +1 -1
  109. package/skills/setup-omc-hud/SKILL.md +2 -1
  110. package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
  111. package/skills/smart-compact/SKILL.md +1 -1
  112. package/skills/smart-compact/scripts/codex-gate.mjs +2 -1
  113. package/skills/theme-selection/SKILL.md +1 -1
  114. package/skills/tone-compact/SKILL.md +1 -1
  115. package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
  116. package/skills/ultra-chain/SKILL.md +34 -16
  117. package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
  118. package/skills/ultra-init/SKILL.md +30 -22
  119. package/skills/ultra-ui-qa/SKILL.md +1 -1
  120. package/skills/ultraqa/NOTICE.md +34 -0
  121. package/skills/ultraqa/SKILL.md +196 -0
  122. package/skills/ultraqa/references/gate-evidence.mjs +457 -0
  123. package/skills/ultraqa/references/run-folder.mjs +299 -0
  124. package/skills/ultraqa/references/tmux-drive.md +52 -0
  125. package/skills/ultraqa/references/uqa-run.mjs +339 -0
  126. package/skills/update-banker/SKILL.md +32 -7
  127. package/skills/vertical-pptx/SKILL.md +1 -1
  128. package/skills/vertical-pptx/build_a4p.js +20 -2
  129. package/skills/vertical-pptx/build_a4p.py +560 -359
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * uqa-run: the run folder and the machine checks of banker's ultraqa (Claude Code only). The model plans, fixes and
4
+ * writes; this program keeps the state as data and says when COMPLETE may be claimed.
5
+ *
6
+ * node uqa-run.mjs init --repo <repo> --gates <a,b,...> [--resume]
7
+ * a new run folder (run-folder.mjs) with state.md, or with --resume the unfinished run of the same repo (by real
8
+ * path) whose HEAD and work tree fingerprint are still the same
9
+ * node uqa-run.mjs row --dir <d> one scenario row (JSON on stdin) into matrix.md, by its id
10
+ * node uqa-run.mjs status --dir <d> same-failure, max-cycles, matrix-incomplete, stale-evidence, gates
11
+ * node uqa-run.mjs finish --dir <d> --end complete|max-cycles|same-failure|blocked|error [--why <text>]
12
+ * the end line; complete only when status allows it, max-cycles and same-failure only when status says so
13
+ * node uqa-run.mjs report --dir <d> the report skeleton (markdown): observed and reported apart
14
+ * node uqa-run.mjs cleanup --dir <d> that run folder alone; paths the matrix names outside it are reported only
15
+ *
16
+ * The gate commands run through gate-evidence.mjs (`run --iter <cycle>`), in the same folder. Out: JSON on stdout
17
+ * (report: markdown); exit 0 done, 1 a check that does not hold, 2 usage or internal error, 3 a stop.
18
+ */
19
+ import { readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
20
+ import { join, resolve, sep } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { CLASSES, gateConjunct, readEvidence, repeatedFailures } from "./gate-evidence.mjs";
23
+ import { Stop, bases, makeRunFolder, removeRunFolder, requireGitTree, runFolder, snapshot } from "./run-folder.mjs";
24
+
25
+ const KIND = ["ultraqa"];
26
+ export const MAX_CYCLES = 5;
27
+ const STATUSES = ["pass", "fail", "blocked", "excluded"];
28
+ const ROW_KEYS = ["id", "class", "intent", "model", "setup", "command", "expected", "status", "evidenceRef", "excludeReason", "cleanup"];
29
+ const END_LINES = {
30
+ complete: (n) => `ULTRAQA COMPLETE: Goal met after ${n} cycles`,
31
+ "max-cycles": () => "ULTRAQA STOPPED: Max cycles",
32
+ "same-failure": () => "ULTRAQA STOPPED: Same failure detected 3 times",
33
+ blocked: (_n, why) => `ULTRAQA BLOCKED: ${why}`,
34
+ error: (_n, why) => `ULTRAQA ERROR: ${why}`,
35
+ };
36
+
37
+ class Usage extends Error {}
38
+ const readJson = (p, fallback) => {
39
+ try {
40
+ return JSON.parse(readFileSync(p, "utf8"));
41
+ } catch {
42
+ return fallback;
43
+ }
44
+ };
45
+ const readState = (d) => {
46
+ const s = readJson(join(d, "state.md"), null);
47
+ if (!s) throw new Stop("no-state", `state.md 가 없거나 읽을 수 없다: ${d}`);
48
+ return s;
49
+ };
50
+ const writeState = (d, s) => writeFileSync(join(d, "state.md"), `${JSON.stringify(s, null, 2)}\n`, { mode: 0o600 });
51
+ export function readMatrix(d) {
52
+ let text = "";
53
+ try {
54
+ text = readFileSync(join(d, "matrix.md"), "utf8");
55
+ } catch {
56
+ return [];
57
+ }
58
+ return text.split("\n").filter(Boolean).map((l) => JSON.parse(l));
59
+ }
60
+ const realRepo = (repo) => {
61
+ try {
62
+ return realpathSync.native(resolve(repo));
63
+ } catch {
64
+ throw new Stop("no-repo", `저장소 폴더가 없다: ${repo}`);
65
+ }
66
+ };
67
+ const gatesOf = (v) => String(v || "").split(",").map((g) => g.trim()).filter(Boolean);
68
+
69
+ // ---- the checks (pure)
70
+
71
+ // A passed row rests on a scenario record of this cycle that passed and is that scenario's last record of the cycle:
72
+ // observed, its id scenario-*, verdict pass. A goal gate's record, a failed one, one of an earlier cycle or a pass a
73
+ // rerun of the same scenario has since failed is no ground for it (staleCheck adds the tree).
74
+ function restsOn(r, observed, cycle) {
75
+ const rec = Number.isInteger(r.evidenceRef) ? observed.find((x) => x.seq === r.evidenceRef) : null;
76
+ if (!rec || !String(rec.id).startsWith("scenario-") || rec.verdict !== "pass" || rec.iter !== cycle) return false;
77
+ return observed.filter((x) => x.id === rec.id && x.iter === cycle).pop() === rec;
78
+ }
79
+
80
+ // The record numbers two or more passed rows cite: one scenario run covers one row, never several classes at once.
81
+ const sharedRefs = (rows) => {
82
+ const seen = new Map();
83
+ for (const r of rows.filter((x) => x.status === "pass")) seen.set(r.evidenceRef, (seen.get(r.evidenceRef) || 0) + 1);
84
+ return new Set([...seen].filter(([, n]) => n > 1).map(([ref]) => ref));
85
+ };
86
+
87
+ // The matrix: every class has a row or is excluded with a reason; a row's status is one of four; a passed row names
88
+ // the passing scenario record of this cycle it rests on, a record no other passed row cites; a failed or blocked row
89
+ // is still open.
90
+ export function matrixCheck(rows, evidence = [], cycle = 0) {
91
+ const observed = evidence.filter((r) => r?.kind === "observed");
92
+ const missing = CLASSES.map(([id]) => id).filter((c) => !rows.some((r) => r.class === c && (r.status !== "excluded" || String(r.excludeReason || "").trim())));
93
+ const shared = sharedRefs(rows);
94
+ const bad = rows.filter((r) => !STATUSES.includes(r.status) || (r.status === "pass" && (shared.has(r.evidenceRef) || !restsOn(r, observed, cycle)))).map((r) => r.id);
95
+ const open = rows.filter((r) => r.status === "fail" || r.status === "blocked").map((r) => r.id);
96
+ return { ok: rows.length > 0 && !missing.length && !bad.length && !open.length, missing, bad, open };
97
+ }
98
+
99
+ // Each goal gate's last observed record, and each record a passed row rests on, was made on the tree as it is now.
100
+ export function staleCheck(evidence, gates, rows, fingerprint) {
101
+ const observed = evidence.filter((r) => r.kind === "observed");
102
+ const last = gates.map((g) => observed.filter((r) => r.id === g).pop()).filter(Boolean);
103
+ const cited = rows.filter((r) => r.status === "pass").map((r) => observed.find((x) => x.seq === r.evidenceRef) || { seq: r.evidenceRef, fingerprint: null });
104
+ const stale = [...last, ...cited].filter((r) => r.fingerprint !== fingerprint).map((r) => r.seq);
105
+ return { ok: stale.length === 0 && last.length === gates.length, stale, unrun: gates.filter((g) => !observed.some((r) => r.id === g)) };
106
+ }
107
+
108
+ // The goal gates as records: `interactive` is the drive scripts' scenario-* records (references/tmux-drive.md), as
109
+ // ralph-qa's `scenarios` is; with none it stays `interactive` (never run).
110
+ export function goalGates(gates, evidence) {
111
+ const scenarios = [...new Set(evidence.filter((r) => r?.kind === "observed" && String(r.id).startsWith("scenario-")).map((r) => r.id))];
112
+ return gates.flatMap((g) => (g !== "interactive" ? [g] : scenarios.length ? scenarios : [g]));
113
+ }
114
+
115
+ export function statusOf({ evidence, rows, gates, fingerprint }) {
116
+ const observed = evidence.filter((r) => r.kind === "observed");
117
+ const cycle = Math.max(0, ...observed.map((r) => r.iter));
118
+ const same = repeatedFailures(evidence);
119
+ const goal = goalGates(gates, evidence);
120
+ const checks = {
121
+ sameFailure: { ok: same.length === 0, signatures: same },
122
+ maxCycles: { ok: cycle <= MAX_CYCLES, cycle, max: MAX_CYCLES },
123
+ matrix: matrixCheck(rows, evidence, cycle),
124
+ staleEvidence: staleCheck(evidence, goal, rows, fingerprint),
125
+ gates: gateConjunct(evidence, goal, fingerprint, cycle),
126
+ };
127
+ const complete = Object.values(checks).every((c) => c.ok);
128
+ return { complete, cycle, checks, next: nextLine(complete, checks, cycle) };
129
+ }
130
+
131
+ function nextLine(complete, checks, cycle) {
132
+ if (complete) return END_LINES.complete(cycle);
133
+ if (!checks.sameFailure.ok) return END_LINES["same-failure"]();
134
+ return cycle >= MAX_CYCLES ? END_LINES["max-cycles"]() : null;
135
+ }
136
+
137
+ // Whether `end` may be claimed with this status: COMPLETE only when every check holds; the two STOPPED lines only
138
+ // when status says so; BLOCKED and ERROR need a reason.
139
+ const needsWhy = (_st, why) => (String(why || "").trim() ? null : "--why 가 필요하다");
140
+ const END_RULES = {
141
+ complete: (st) => (st.complete ? null : `COMPLETE 를 거부한다: ${Object.entries(st.checks).filter(([, c]) => !c.ok).map(([k]) => k).join(", ")}`),
142
+ "same-failure": (st) => (st.checks.sameFailure.ok ? "같은 실패 3회가 기록에 없다" : null),
143
+ "max-cycles": (st) => (st.cycle >= MAX_CYCLES ? null : `순환이 ${st.cycle} 번이라 아직 한도가 아니다`),
144
+ blocked: needsWhy,
145
+ error: needsWhy,
146
+ };
147
+ export function endAllowed(end, st, why) {
148
+ return Object.prototype.hasOwnProperty.call(END_RULES, end) ? END_RULES[end](st, why) : `모르는 종료다: ${end}`;
149
+ }
150
+
151
+ // ---- commands
152
+
153
+ async function current(d, ctx) {
154
+ const state = readState(d);
155
+ const evidence = readEvidence(d);
156
+ const snap = await snapshot(state.repo, ctx.env);
157
+ return { state, evidence, rows: readMatrix(d), fingerprint: snap.fingerprint, head: snap.head };
158
+ }
159
+
160
+ async function init(o, ctx) {
161
+ const repo = realRepo(o.repo);
162
+ const gates = gatesOf(o.gates);
163
+ if (!gates.length) throw new Usage("--gates 에 목표 게이트를 하나 이상 준다(tests, build, lint, typecheck, custom, interactive)");
164
+ requireGitTree(repo, ctx.env, "--repo");
165
+ if (o.resume) return resume(repo, ctx);
166
+ const snap = await snapshot(repo, ctx.env);
167
+ const { dir } = makeRunFolder(ctx, "ultraqa");
168
+ const state = { repo, gates, head: snap.head, fingerprint: snap.fingerprint, startedAt: new Date().toISOString(), finished: null };
169
+ writeState(dir, state);
170
+ writeFileSync(join(dir, "matrix.md"), "", { mode: 0o600 });
171
+ return [{ ok: true, dir, resumed: false, ...state }, 0];
172
+ }
173
+
174
+ // The unfinished runs of this repo under the known bases, newest first.
175
+ function unfinished(repo, ctx) {
176
+ const found = [];
177
+ for (const base of bases(ctx).known) {
178
+ let names = [];
179
+ try {
180
+ names = readdirSync(base).filter((n) => /^ultraqa\.[A-Za-z0-9]+$/.test(n));
181
+ } catch {
182
+ continue;
183
+ }
184
+ for (const n of names) {
185
+ try {
186
+ const { d } = runFolder(join(base, n), ctx, KIND);
187
+ const s = readState(d);
188
+ if (s.repo === repo && !s.finished) found.push({ d, s, at: statSync(join(d, "state.md")).mtimeMs });
189
+ } catch {
190
+ /* not a run folder of this account, or no state */
191
+ }
192
+ }
193
+ }
194
+ return found.sort((a, b) => b.at - a.at);
195
+ }
196
+
197
+ // Resumed only when the repo's HEAD and work tree are what the run last saw (its last observed record, else init).
198
+ async function resume(repo, ctx) {
199
+ const runs = unfinished(repo, ctx);
200
+ if (!runs.length) throw new Stop("no-resume", `이어 갈 끝나지 않은 실행이 없다: ${repo}`);
201
+ const { d, s } = runs[0];
202
+ const snap = await snapshot(repo, ctx.env);
203
+ const lastSeen = readEvidence(d).filter((r) => r.kind === "observed").pop()?.fingerprint || s.fingerprint;
204
+ if (snap.head !== s.head || snap.fingerprint !== lastSeen) {
205
+ throw new Stop("no-resume", `HEAD 나 작업 트리가 그 실행이 마지막에 본 것과 다르다. 새로 시작한다: ${d}`);
206
+ }
207
+ const st = statusOf({ evidence: readEvidence(d), rows: readMatrix(d), gates: s.gates, fingerprint: snap.fingerprint });
208
+ return [{ ok: true, dir: d, resumed: true, cycle: st.cycle, nextCycle: st.cycle + 1, ...s }, 0];
209
+ }
210
+
211
+ function row(o, ctx) {
212
+ const { d } = runFolder(o.dir, ctx, KIND);
213
+ const r = JSON.parse(String(ctx.stdin()).trim() || "null");
214
+ if (!r || typeof r !== "object" || !/^[A-Za-z0-9._-]{1,64}$/.test(String(r.id))) throw new Usage("행은 id(영숫자, . _ - 64자까지)가 있는 JSON 객체다");
215
+ if (!CLASSES.some(([c]) => c === r.class) && r.class !== "normal") throw new Usage(`class 는 9분류의 id 나 normal 이다: ${String(r.class).slice(0, 40)}`);
216
+ if (!STATUSES.includes(r.status)) throw new Usage(`status 는 ${STATUSES.join(", ")} 가운데 하나다`);
217
+ const clean = Object.fromEntries(ROW_KEYS.filter((k) => k in r).map((k) => [k, r[k]]));
218
+ const rows = readMatrix(d).filter((x) => x.id !== clean.id).concat(clean);
219
+ writeFileSync(join(d, "matrix.md"), rows.map((x) => JSON.stringify(x)).join("\n") + "\n", { mode: 0o600 });
220
+ return [{ ok: true, rows: rows.length, row: clean }, 0];
221
+ }
222
+
223
+ async function status(o, ctx) {
224
+ const { d } = runFolder(o.dir, ctx, KIND);
225
+ const c = await current(d, ctx);
226
+ const st = statusOf({ evidence: c.evidence, rows: c.rows, gates: c.state.gates, fingerprint: c.fingerprint });
227
+ return [{ ok: true, ...st, finished: c.state.finished }, st.complete ? 0 : 1];
228
+ }
229
+
230
+ async function finish(o, ctx) {
231
+ const { d } = runFolder(o.dir, ctx, KIND);
232
+ const c = await current(d, ctx);
233
+ const st = statusOf({ evidence: c.evidence, rows: c.rows, gates: c.state.gates, fingerprint: c.fingerprint });
234
+ const refused = endAllowed(o.end, st, o.why);
235
+ if (refused) return [{ ok: false, refused, next: st.next, checks: st.checks }, 1];
236
+ const line = END_LINES[o.end](st.cycle, String(o.why || "").trim());
237
+ writeState(d, { ...c.state, finished: { end: o.end, line, at: new Date().toISOString() } });
238
+ return [{ ok: true, line }, 0];
239
+ }
240
+
241
+ const cell = (v) => String(v ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ").slice(0, 200);
242
+
243
+ // The report skeleton: the parts the program knows are filled, the rest the model writes. Observed and reported apart.
244
+ export function reportOf({ state, evidence, rows, st }) {
245
+ const observed = evidence.filter((r) => r.kind === "observed");
246
+ const reported = evidence.filter((r) => r.kind === "reported");
247
+ return [
248
+ "# UltraQA 보고서", "", "## 목표와 성공 기준", `- 목표 게이트: ${state.gates.join(", ")}`, "- 성공 기준: (모델이 씀)", "- 종료 조건: COMPLETE, 순환 5회, 같은 실패 3회, 안전 경계, 환경 오류",
249
+ "- 적용한 안전 경계: (모델이 씀)", "", "## 시나리오 행렬",
250
+ "| ID | 분류 | 사용자/공격자 모델 | 시나리오 | 명령/하네스 | 기대 신호 | 상태 | 증거(기록 번호) | 정리 |", "|---|---|---|---|---|---|---|---|---|",
251
+ ...rows.map((r) => `| ${[r.id, r.class, r.model, r.intent, r.command, r.expected, r.status, r.evidenceRef ?? cell(r.excludeReason), r.cleanup].map(cell).join(" | ")} |`),
252
+ "", "## 실행한 명령(관측)", ...observed.map((r) => `- [${r.exit ?? r.signal ?? "-"}] \`${cell(r.cmd)}\` 순환 ${r.iter}, ${r.secs}s/${r.timeout}s, ${r.verdict}${r.why ? `(${r.why})` : ""}, 출력 sha256 ${String(r.outputSha256).slice(0, 12)}`),
253
+ "", "## 신고(판정에 쓰지 않음)", ...(reported.length ? reported.map((r) => `- ${r.id} 순환 ${r.iter}: ${cell(r.text)}`) : ["- 없음"]),
254
+ "", "## 찾은 실패", "- (모델이 씀: 시나리오 ID, 실패 신호, 근본 원인, 사용자 영향, 안전 영향)", "", "## 적용한 고침", "- (모델이 씀: 바꾼 파일, 까닭, 연결된 시나리오, 회귀 증거)",
255
+ "", "## 정리와 되돌림", "- (cleanup 결과와 작업 트리 전후)", "", "## 남은 위험", ...rows.filter((r) => r.status !== "pass").map((r) => `- ${r.id}: ${r.status} ${cell(r.excludeReason)}`),
256
+ "", "## 기계 판정", `- 순환 ${st.cycle}/${MAX_CYCLES}, COMPLETE ${st.complete ? "허용" : "거부"}${st.next ? `, 다음 줄: ${st.next}` : ""}`,
257
+ ...Object.entries(st.checks).map(([k, c]) => `- ${k}: ${c.ok ? "통과" : "실패"}`), "",
258
+ ].join("\n");
259
+ }
260
+
261
+ async function report(o, ctx) {
262
+ const { d } = runFolder(o.dir, ctx, KIND);
263
+ const c = await current(d, ctx);
264
+ const st = statusOf({ evidence: c.evidence, rows: c.rows, gates: c.state.gates, fingerprint: c.fingerprint });
265
+ return [{ markdown: reportOf({ ...c, st }) }, 0];
266
+ }
267
+
268
+ // The run folder alone. A path a row's cleanup names outside it is reported, never removed.
269
+ function cleanup(o, ctx) {
270
+ const { d } = runFolder(o.dir, ctx, KIND);
271
+ const outside = readMatrix(d).map((r) => r.cleanup).filter((p) => typeof p === "string" && p.trim() && !resolve(d, p).startsWith(d + sep));
272
+ const done = removeRunFolder(d, ctx, KIND);
273
+ return [{ ok: true, ...done, outside }, 0];
274
+ }
275
+
276
+ const FLAGS = { init: ["repo", "gates", "resume!"], row: ["dir"], status: ["dir"], finish: ["dir", "end", "why"], report: ["dir"], cleanup: ["dir"] };
277
+ const REQUIRED = { init: ["repo", "gates"], row: ["dir"], status: ["dir"], finish: ["dir", "end"], report: ["dir"], cleanup: ["dir"] };
278
+
279
+ function readFlag(spec, rest, i, o) {
280
+ const m = /^--([a-z]+)(?:=([\s\S]*))?$/.exec(rest[i]);
281
+ const flag = m && spec.find((f) => f.replace(/!$/, "") === m[1]);
282
+ if (!flag) throw new Usage(`모르는 인자다: ${String(rest[i]).slice(0, 80)}`);
283
+ if (m[1] in o) throw new Usage(`--${m[1]} 를 두 번 줬다`);
284
+ if (flag.endsWith("!")) return (o[m[1]] = true) && i + 1;
285
+ const value = m[2] !== undefined ? m[2] : rest[i + 1];
286
+ if (value === undefined) throw new Usage(`--${m[1]} 에 값이 없다`);
287
+ o[m[1]] = value;
288
+ return m[2] !== undefined ? i + 1 : i + 2;
289
+ }
290
+
291
+ export function parseArgs(argv) {
292
+ const [command, ...rest] = argv;
293
+ if (!Object.prototype.hasOwnProperty.call(FLAGS, command)) throw new Usage(`모르는 명령이다: ${command ?? "(없음)"}`);
294
+ const o = {};
295
+ for (let i = 0; i < rest.length; ) i = readFlag(FLAGS[command], rest, i, o);
296
+ for (const r of REQUIRED[command]) if (!(r in o)) throw new Usage(`--${r} 가 필요하다`);
297
+ return { command, o };
298
+ }
299
+
300
+ const COMMANDS = { init, row, status, finish, report, cleanup };
301
+
302
+ // A stop as JSON: what it is, its line, its paths, and for not-a-git-tree the folder it checked.
303
+ const stopJson = (e) => ({ ok: false, stop: e.code, message: `ultraqa: ${e.message}`, detail: e.detail, ...(e.checked ? { checked: e.checked } : {}) });
304
+
305
+ function failure(e, out, err) {
306
+ if (e instanceof Stop) {
307
+ out.write(`${JSON.stringify(stopJson(e), null, 2)}\n`);
308
+ return 3;
309
+ }
310
+ err.write(e instanceof Usage || e instanceof SyntaxError ? `ultraqa: ${e.message}\n` : `ultraqa: 내부 오류(${e?.code || e?.name || "error"}): ${String(e?.message || e).split("\n")[0]}\n`);
311
+ return 2;
312
+ }
313
+
314
+ export async function main(argv, io = {}) {
315
+ const ctx = { env: io.env || process.env, platform: io.platform || process.platform, stdin: io.stdin || (() => readFileSync(0, "utf8")) };
316
+ const out = io.stdout || process.stdout;
317
+ try {
318
+ const { command, o } = parseArgs(argv);
319
+ const [result, code] = await COMMANDS[command](o, ctx);
320
+ out.write(command === "report" ? result.markdown : `${JSON.stringify({ command, ...result }, null, 2)}\n`);
321
+ return code;
322
+ } catch (e) {
323
+ return failure(e, out, io.stderr || process.stderr);
324
+ }
325
+ }
326
+
327
+ const invokedDirectly = () => {
328
+ try {
329
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
330
+ } catch {
331
+ return false;
332
+ }
333
+ };
334
+
335
+ if (invokedDirectly()) {
336
+ main(process.argv.slice(2)).then((code) => {
337
+ process.exitCode = code;
338
+ });
339
+ }
@@ -13,8 +13,10 @@ banker 는 서로 다른 메커니즘의 채널 3개로 설치되고, 그 채널
13
13
  **런타임:** 축은 OS 가 아니라 **채널**이다. banker 는 dependencies 0 이고 설치가 파일 복사라 OS 분기가 사실상 없다.
14
14
 
15
15
  - **Claude Code**: 플러그인 채널이 있다. 갱신 동사는 `claude plugin update`, 관측 지점은 `~/.claude/plugins/installed_plugins.json`.
16
- - **Codex CLI**: `codex plugin` 에 update 동사가 **없다.** 유일한 경로는 npm 갱신 후 `banker setup --codex` 이고, 이건 파괴적이다.
17
- - **양쪽 공통**: npm 전역 CLI 채널. 이게 Codex 채널의 **상류**라 언제나 먼저다.
16
+ - **Codex CLI**: 설치 길이 둘이다. 갱신 방법이 다르다.
17
+ - npm 길(`banker setup --codex`, 스킬 `$banker-<이름>`): npm 갱신 후 `banker setup --codex` 를 다시 돌린다. 이건 파괴적이다.
18
+ - 플러그인 길(`codex plugin add banker@banker-plugins`, 스킬 `$banker:<이름>`): `codex plugin` 에 update 동사는 없지만 `codex plugin marketplace upgrade banker-plugins` 가 GitHub 의 새 판을 받아 설치된 플러그인도 바꾼다. Codex 는 세션을 시작할 때도 스스로 올린다(0.144.5 실측). 파괴적이지 않고 npm 과 무관하다.
19
+ - **양쪽 공통**: npm 전역 CLI 채널. 이게 Codex npm 길의 **상류**라 언제나 먼저다. Codex 플러그인 길의 상류는 GitHub 저장소라 npm 게이트를 받지 않는다.
18
20
  - 이 스킬은 `target: both` 다. Codex 런타임에 `claude` 가 없거나 Claude 런타임에 `codex` 가 없는 것은 **정상 경로**다. 채널 부재는 스킵이지 에러가 아니다.
19
21
  - **"채널 부재" 는 두 가지다.** (a) 런타임이 없다(`command -v claude` 실패), (b) 런타임은 있는데 **banker 플러그인이 설치돼 있지 않다.** `command -v` 는 (a) 만 잡는다. Codex 주력 + Claude Code 설치됨 + banker 플러그인 미설치인 머신이 실재하고, 거기서 (b) 를 스킵으로 처리하지 않으면 프로브가 예외로 죽는다. 둘 다 스킵이다.
20
22
 
@@ -26,7 +28,7 @@ banker 는 서로 다른 메커니즘의 채널 3개로 설치되고, 그 채널
26
28
 
27
29
  ## When NOT to use
28
30
  - **최초 설치**. 그건 `banker setup` 또는 `/banker:setup` 소관이다.
29
- - **OMC/OMX 갱신**. `setup-omc` 소관이다. banker 는 그 버전을 대신 고정하지 않는다.
31
+ - **OMC/OMX 갱신**. `setup-omc`(Codex 는 `setup-omx`) 소관이다. banker 는 그 버전을 대신 고정하지 않는다.
30
32
  - **깨진 설치 되살리기**. 갱신이 아니다. 아래 "복구" 절을 보고, 확인을 받고 진행한다.
31
33
  - 스킬 하나만 고치고 싶을 때. 갱신 단위는 채널 전체다.
32
34
  - 3채널이 아닌 경로(로컬 clone 을 직접 가리키는 개발 설치 등). 그건 이 스킬의 모델 밖이다.
@@ -37,7 +39,10 @@ banker 는 서로 다른 메커니즘의 채널 3개로 설치되고, 그 채널
37
39
  |---|---|---|---|
38
40
  | Claude 플러그인 | 마켓플레이스 등록 후 플러그인 설치 | `claude plugin update banker@banker-plugins` | `~/.claude/plugins/installed_plugins.json` |
39
41
  | npm 전역 CLI | npm 레지스트리에서 전역 prefix 로 설치 | `npm i -g @kaydash9999/banker-plugins@latest` | `banker --version` |
40
- | Codex | npm 패키지에서 `~/.codex/skills` **와 `~/.codex/prompts`** 로 복사 | `banker setup --codex` | `ls ~/.codex/skills` **와** `ls ~/.codex/prompts` |
42
+ | Codex (npm 길) | npm 패키지에서 `~/.codex/skills` **와 `~/.codex/prompts`** 로 복사 | `banker setup --codex` | `ls ~/.codex/skills` **와** `ls ~/.codex/prompts` |
43
+ | Codex (플러그인 길) | GitHub 마켓플레이스 스냅샷을 `~/.codex/plugins/cache/banker-plugins/banker/<판>/` 에 둠 | `codex plugin marketplace upgrade banker-plugins` | `ls ~/.codex/plugins/cache/banker-plugins/banker/` |
44
+
45
+ `CODEX_HOME` 이 있으면 `~/.codex` 대신 그 폴더다. Codex 의 두 길을 함께 두면 스킬이 `$banker-<이름>` 과 `$banker:<이름>` 으로 겹치고 훅도 두 번 돈다. 둘 다 보이면 보고에 적고 하나만 두라고 안내한다(npm 길 빼기 `banker uninstall --codex`, 플러그인 빼기 `codex plugin remove banker@banker-plugins`). 이 스킬이 대신 지우지는 않는다.
41
46
 
42
47
  세 채널은 다른 갱신 동사를 쓰고 각자의 값을 가진다. 한 채널을 갱신해도 나머지는 그대로다. 실제로 세 값이 모두 다른 상태가 관측된다.
43
48
 
@@ -75,6 +80,7 @@ in15 sh -c 'cd ~ && exec npm ping --fetch-retries=0 --fetch-timeout=10000 --no-u
75
80
  command -v claude >/dev/null && echo "claude: present" || echo "claude: absent (Claude 채널 스킵)"
76
81
  command -v codex >/dev/null && echo "codex: present" || echo "codex: absent (Codex 채널 스킵)"
77
82
  command -v banker >/dev/null && banker --version || echo "banker: absent"
83
+ command -v codex >/dev/null && in15 codex plugin list 2>/dev/null | grep -E '^banker@[^ ]+ +installed, enabled' || echo "codex plugin: absent (플러그인 길 스킵)" # 켜진 banker@<마켓플레이스>와 판
78
84
  [ "$P" = 0 ] && (cd ~ && npm view @kaydash9999/banker-plugins version --fetch-retries=0 --fetch-timeout=10000)
79
85
  ```
80
86
 
@@ -162,6 +168,18 @@ banker setup --codex --dry-run | grep -E '→ [0-9]+ skills'
162
168
  - `--scope project` 는 `./.codex` 를 대상으로 한다. 기본은 `user`(`~/.codex`).
163
169
  - 스킬은 `~/.codex/skills/banker-<name>/` 으로, 커맨드는 `~/.codex/prompts/banker-<name>.md` 로 설치된다. 스킬은 프론트매터 name 이 디렉터리명과 일치하도록 재작성된다. `~/.codex/AGENTS.md` 는 건드리지 않는다.
164
170
 
171
+ ### 3b. Codex 플러그인 길
172
+ `codex plugin: absent` 면 스킵이다. 0단계가 보인 `banker@<마켓플레이스>` 의 마켓플레이스 이름을 아래 `banker-plugins` 자리에 쓴다. 이 길은 npm 게이트를 받지 않는다(상류가 GitHub 다). 지우고 다시 복사하지 않으므로 dry-run 단계가 없다.
173
+
174
+ ```bash
175
+ codex plugin marketplace upgrade banker-plugins
176
+ ls "${CODEX_HOME:-$HOME/.codex}/plugins/cache/banker-plugins/banker/"
177
+ ```
178
+
179
+ - 설치된 플러그인 폴더가 새 판 이름으로 바뀌면 전진이다(옛 판 폴더는 Codex 가 지운다). 반영은 Codex 를 다시 연 뒤다.
180
+ - 마켓플레이스 이름이 다르면(`codex plugin marketplace list` 로 확인) 그 이름을 쓴다.
181
+ - 훅 신뢰는 명령 글에 묶여 있어 판이 바뀌어도 대개 그대로다. `/hooks` 에 banker 훅이 신뢰 필요로 보이면 그 사실만 보고한다.
182
+
165
183
  ### 4. 검증 (보고 의무)
166
184
  **exit code 로 판정하지 마라.** `claude plugin install` 이 아무것도 안 하고 exit 0 을 주는 것이 정확히 그 이유다.
167
185
  판정은 0단계와 **동일한 3-프로브를 재실행**해 before/after 를 비교하는 것으로만 한다.
@@ -195,9 +213,15 @@ console.log('manifest skills:',n('skill'));console.log('manifest commands:',n('c
195
213
 
196
214
  보조로 `banker doctor` 가 디스크 카운트와 매니페스트 카운트를 나란히 찍어준다. **다만 프롬프트 카운트가 없어 프로브 3 을 대체하지 못한다.** 스킬 축의 교차 확인용으로만 쓴다.
197
215
 
216
+ 프로브 4, Codex 플러그인 길(판 폴더 이름이 설치된 판이다):
217
+ ```bash
218
+ ls "${CODEX_HOME:-$HOME/.codex}/plugins/cache/banker-plugins/banker/" 2>/dev/null || echo "absent (플러그인 길 미설치 -> 스킵)"
219
+ ```
220
+
198
221
  보조 프로브(디스크 상태가 아니라 런타임이 실제로 발견한 표면을 본다. 인증 불필요):
199
222
  ```bash
200
- codex debug prompt-input </dev/null | grep -oE 'banker-[a-z0-9_-]+' | sort -u | wc -l
223
+ codex debug prompt-input </dev/null | grep -oE 'banker-[a-z0-9_-]+' | sort -u | wc -l # npm 길
224
+ codex debug prompt-input </dev/null | grep -oE 'banker:[a-z0-9_-]+' | sort -u | wc -l # 플러그인 길
201
225
  ```
202
226
 
203
227
  보고 형식(채널별 before 에서 after 로. 아래는 예시):
@@ -206,6 +230,7 @@ Claude 플러그인 : 0.6.0 -> 0.7.0 전진
206
230
  npm 전역 CLI : 0.2.0 -> 0.7.0 전진
207
231
  Codex skills : 41 -> 48 (매니페스트 48) 전진
208
232
  Codex prompts : 2 -> 2 (매니페스트 2) 유지
233
+ Codex 플러그인 : 0.6.0 -> 0.7.0 전진
209
234
  ```
210
235
 
211
236
  - **Codex 는 skills 와 prompts 를 반드시 두 줄로 적는다.** 한 줄로 뭉개면 프롬프트 소실이 보고에서 사라진다.
@@ -248,5 +273,5 @@ Codex prompts : 2 -> 2 (매니페스트 2) 유지
248
273
 
249
274
  ARGUMENTS: [--check] [--claude|--npm|--codex] (없으면 3채널 전부: 관측 → npm 게이트 → 나머지 채널 → 3-프로브 검증)
250
275
  - `--check`: 아무것도 변경하지 않는다. 0단계 관측 + 4단계 프로브만 돌려 채널별 드리프트를 보고한다.
251
- - `--claude` / `--npm` / `--codex`: 해당 채널만 갱신한다.
252
- - 단 `--codex` 단독이어도 1단계 게이트(`banker --version` == `npm view ...`)는 **반드시** 확인한다. 불일치면 npm 갱신을 먼저 제안하고 확인받는다. 게이트 없는 `--codex` 는 없다.
276
+ - `--claude` / `--npm` / `--codex`: 해당 채널만 갱신한다. `--codex` 는 Codex 의 두 길 중 설치된 것을 모두 갱신한다.
277
+ - 단 `--codex` 단독이어도 npm 길을 갱신하기 전에는 1단계 게이트(`banker --version` == `npm view ...`)를 **반드시** 확인한다. 불일치면 npm 갱신을 먼저 제안하고 확인받는다. 게이트 없는 npm 길 갱신은 없다. 플러그인 길만 설치돼 있으면 게이트 없이 3b 만 한다.
@@ -215,7 +215,7 @@ macOS 와 Linux 에서는 대체 폰트로 치환되고, 치환되면 글자 폭
215
215
 
216
216
  ```
217
217
  --font <이름> → BANKER_PPTX_FONT → meta.font →
218
- Noto Sans CJK KR → Malgun Gothic → Apple SD Gothic Neo → NanumGothic → 시스템 한국어 sans
218
+ Noto Sans CJK KR → Noto Sans KR → Malgun Gothic → Apple SD Gothic Neo → AppleGothic → NanumGothic → 시스템 한국어 sans
219
219
  ```
220
220
 
221
221
  명시한 이름(`--font`·환경변수·`meta.font`)이 해석되지 않으면 `FONT_NOT_FOUND` 경고를 남기고 다음으로 내려간다. 조용히 바꾸지 않는다.
@@ -542,14 +542,22 @@ function fontWeightRank(flat) {
542
542
  return 1;
543
543
  }
544
544
 
545
+ // 파일명 탐색에 쓰는 조각. 힌트 표에 없는 이름(constructor 처럼 Object 가 물려받은 이름 포함)은 정리한 이름 하나.
546
+ function fontHints(name) {
547
+ const key = String(name).toLowerCase();
548
+ return (Object.prototype.hasOwnProperty.call(FONT_FILE_HINTS, key) ? FONT_FILE_HINTS[key] : [normName(name)]).filter(Boolean);
549
+ }
550
+
545
551
  function globFontFile(name) {
546
- const hints = FONT_FILE_HINTS[String(name).toLowerCase()] || [normName(name)];
552
+ const hints = fontHints(name);
553
+ if (!hints.length) return null;
547
554
  let budget = 20000;
548
555
  const found = [];
549
556
  const walk = (dir, depth) => {
550
557
  if (depth > 6 || budget <= 0) return;
551
558
  let entries;
552
559
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }
560
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); // 어느 OS 에서나 이름순
553
561
  const subdirs = [];
554
562
  for (const ent of entries) {
555
563
  budget -= 1;
@@ -581,6 +589,13 @@ function globFontFile(name) {
581
589
  */
582
590
  function findFontFile(name) {
583
591
  const want = normName(name);
592
+ if (!want) {
593
+ // ASCII 글자가 없는 이름(예: 나눔고딕)은 정리한 형태로 대조할 수 없다. 빈 문자열은 어떤 family 에나 들어 있다.
594
+ // fc-match 가 돌려준 family 원문과 정확히 같을 때만 찾은 것으로 본다. 파일명 탐색은 하지 않는다.
595
+ const exact = String(name).trim().toLowerCase();
596
+ const hit = fcMatch(escFc(name));
597
+ return hit && hit.families.some((f) => f.toLowerCase() === exact) && fs.existsSync(hit.file) ? hit.file : null;
598
+ }
584
599
  // 한 방향(family 가 요청 이름을 품는가)만 본다. 반대 방향까지 허용하면
585
600
  // "Noto Sans CJK KR" 을 요청했는데 "Noto Sans" 가 통과해 한글 없는 폰트에 그 이름이 붙는다.
586
601
  const hit = fcMatch(escFc(name));
@@ -785,7 +800,8 @@ const USAGE = [
785
800
  ' 사람이 읽는 표와 상태 JSON 은 stderr 로 간다(리다이렉트 안전).',
786
801
  ' --out <경로> .pptx 출력 경로 (--emit-json 과 함께 쓰면 deck JSON 출력 경로)',
787
802
  ' --font <이름> 폰트 이름을 직접 지정. 체인: --font → BANKER_PPTX_FONT → meta.font',
788
- ' → Noto Sans CJK KR → Malgun Gothic → Apple SD Gothic Neo → NanumGothic → 시스템 한국어 sans',
803
+ ' → Noto Sans CJK KR → Noto Sans KR → Malgun Gothic → Apple SD Gothic Neo → AppleGothic',
804
+ ' → NanumGothic → 시스템 한국어 sans',
789
805
  ' -h, --help 이 도움말',
790
806
  '',
791
807
  '종료 코드: 0 통과 · 1 검증 실패 · 2 환경 미비(pptxgenjs 없음) · 3 사용법 오류',
@@ -920,4 +936,6 @@ module.exports = {
920
936
  BULLET_PREFIX,
921
937
  FONT_CHAIN,
922
938
  FONT_FILE_HINTS,
939
+ fontHints,
940
+ sanitizeFontName,
923
941
  };