@kaydash9999/banker-plugins 0.22.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 (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +41 -0
  4. package/PRIVACY.md +1 -0
  5. package/README.md +39 -13
  6. package/bin/banker.js +118 -22
  7. package/bin/lib/codex-hooks.mjs +116 -18
  8. package/bin/lib/codex-names.mjs +4 -0
  9. package/bin/lib/codex-routes.mjs +93 -1
  10. package/bin/lib/command-skill.cjs +37 -6
  11. package/codex/command-skills/front-qa/SKILL.md +1 -1
  12. package/codex/command-skills/setup/SKILL.md +4 -4
  13. package/codex/hooks.json +5 -5
  14. package/codex/manifest.json +18 -15
  15. package/codex/skills/progress/scripts/progress.mjs +44 -14
  16. package/codex/skills/teamwork/SKILL.md +137 -0
  17. package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
  18. package/codex/transform-matrix.md +5 -3
  19. package/hooks/obsidize-hook.mjs +75 -32
  20. package/hooks/telemetry-count-codex.mjs +238 -57
  21. package/hooks/update-fetch.mjs +2 -1
  22. package/hooks/update-notify.mjs +82 -20
  23. package/package.json +3 -1
  24. package/skills/3d-intro-build/SKILL.md +101 -20
  25. package/skills/3d-intro-build/references/NOTICE.md +9 -2
  26. package/skills/3d-intro-build/references/assemble.mjs +113 -22
  27. package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
  28. package/skills/3d-intro-build/references/clip-post.mjs +443 -0
  29. package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
  30. package/skills/3d-intro-build/references/intro-fixes.css +11 -0
  31. package/skills/3d-intro-build/references/prompts.md +51 -4
  32. package/skills/3d-intro-build/references/scrub-engine.js +1 -0
  33. package/skills/3d-intro-build/references/step-nav.css +29 -0
  34. package/skills/3d-intro-build/references/step-nav.js +277 -106
  35. package/skills/3d-intro-build/references/verify-intro.mjs +373 -32
  36. package/skills/3d-intro-build/references/verify.md +6 -0
  37. package/skills/3d-intro-build/references/video-pool.mjs +142 -12
  38. package/skills/3d-intro-setup/SKILL.md +20 -7
  39. package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
  40. package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
  41. package/skills/all-in-one/SKILL.md +125 -86
  42. package/skills/omc-patch/SKILL.md +1 -1
  43. package/skills/payload-mon/SKILL.md +1 -1
  44. package/skills/play-qa/SKILL.md +6 -4
  45. package/skills/progress-mon/README.md +162 -0
  46. package/skills/progress-mon/SKILL.md +165 -0
  47. package/skills/progress-mon/assets/fixture/README.md +1 -0
  48. package/skills/progress-mon/assets/fixture/arch.json +33 -0
  49. package/skills/progress-mon/assets/fixture/commits.json +7 -0
  50. package/skills/progress-mon/assets/fixture/config.json +30 -0
  51. package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
  52. package/skills/progress-mon/assets/fixture/flow.json +44 -0
  53. package/skills/progress-mon/assets/fixture/items.json +14 -0
  54. package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
  55. package/skills/progress-mon/assets/fixture/reqs.json +11 -0
  56. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
  57. package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
  58. package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
  59. package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
  60. package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
  61. package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
  62. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
  63. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
  64. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
  65. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
  66. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
  67. package/skills/progress-mon/references/arch.md +280 -0
  68. package/skills/progress-mon/references/authoring.md +199 -0
  69. package/skills/progress-mon/references/config.md +115 -0
  70. package/skills/progress-mon/references/flow.md +75 -0
  71. package/skills/progress-mon/references/inventory.md +58 -0
  72. package/skills/progress-mon/references/items.md +74 -0
  73. package/skills/progress-mon/references/req-spec.md +74 -0
  74. package/skills/progress-mon/references/reqs.md +30 -0
  75. package/skills/progress-mon/scripts/index.js +31 -0
  76. package/skills/progress-mon/scripts/pm-check.mjs +526 -0
  77. package/skills/progress-mon/scripts/pm-config.mjs +341 -0
  78. package/skills/progress-mon/scripts/pm-data.mjs +394 -0
  79. package/skills/progress-mon/scripts/pm-html.mjs +77 -0
  80. package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
  81. package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
  82. package/skills/progress-mon/scripts/pm-server.mjs +735 -0
  83. package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
  84. package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
  85. package/skills/ralph-qa/SKILL.md +104 -15
  86. package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
  87. package/skills/ralph-qa/references/payload-scan.mjs +19 -2
  88. package/skills/ralph-qa/references/run-folder.mjs +299 -0
  89. package/skills/ralph-qa/references/seat-payload.mjs +60 -13
  90. package/skills/ralph-qa/references/seat-run.mjs +159 -155
  91. package/skills/remains/SKILL.md +1 -0
  92. package/skills/remains/scripts/boxes.mjs +6 -2
  93. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
  94. package/skills/setup-mcp/SKILL.md +1 -1
  95. package/skills/setup-omc-hud/SKILL.md +2 -1
  96. package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
  97. package/skills/ultra-chain/SKILL.md +32 -14
  98. package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
  99. package/skills/ultra-init/SKILL.md +30 -22
  100. package/skills/ultraqa/NOTICE.md +34 -0
  101. package/skills/ultraqa/SKILL.md +196 -0
  102. package/skills/ultraqa/references/gate-evidence.mjs +457 -0
  103. package/skills/ultraqa/references/run-folder.mjs +299 -0
  104. package/skills/ultraqa/references/tmux-drive.md +52 -0
  105. package/skills/ultraqa/references/uqa-run.mjs +339 -0
  106. package/skills/update-banker/SKILL.md +1 -1
@@ -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
+ }
@@ -28,7 +28,7 @@ banker 는 서로 다른 메커니즘의 채널 3개로 설치되고, 그 채널
28
28
 
29
29
  ## When NOT to use
30
30
  - **최초 설치**. 그건 `banker setup` 또는 `/banker:setup` 소관이다.
31
- - **OMC/OMX 갱신**. `setup-omc` 소관이다. banker 는 그 버전을 대신 고정하지 않는다.
31
+ - **OMC/OMX 갱신**. `setup-omc`(Codex 는 `setup-omx`) 소관이다. banker 는 그 버전을 대신 고정하지 않는다.
32
32
  - **깨진 설치 되살리기**. 갱신이 아니다. 아래 "복구" 절을 보고, 확인을 받고 진행한다.
33
33
  - 스킬 하나만 고치고 싶을 때. 갱신 단위는 채널 전체다.
34
34
  - 3채널이 아닌 경로(로컬 clone 을 직접 가리키는 개발 설치 등). 그건 이 스킬의 모델 밖이다.