@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,341 @@
1
+ // pm-config.mjs: 프로젝트 루트와 자료 폴더 결정, 런타임 판별, config.json 읽기와 기본값, 형식 검사, 원자적 쓰기.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import net from 'node:net';
5
+ import crypto from 'node:crypto';
6
+ import { execFileSync } from 'node:child_process';
7
+
8
+ const DATA_REL = { claude: '.omc/progress-mon', codex: '.omx/progress-mon' };
9
+ const RUNTIMES = Object.keys(DATA_REL);
10
+
11
+ // events 출처가 있을 때 쓰는 기본 단계(ultra-chain 10단계).
12
+ export const ULTRA_STAGES = [
13
+ { id: 'preflight', label: '0 사전 점검' },
14
+ { id: 'research', label: '1 자료 조사' },
15
+ { id: 'interview', label: '2 모호성 제거' },
16
+ { id: 'plan', label: '3 합의 계획' },
17
+ { id: 'mock', label: '4 목업 승인' },
18
+ { id: 'build', label: '5 구현' },
19
+ { id: 'qa', label: '6 합의 검증, 박스 시험' },
20
+ { id: 'visual', label: '7 시각 검토' },
21
+ { id: 'trace', label: '8 과정 시각화' },
22
+ { id: 'share', label: '9 결과 공유' },
23
+ ];
24
+
25
+ export const DEFAULT_REQ_RULE =
26
+ '구현 여부: 근거 AC 의 담당 스토리가 모두 완료되면 완료, 일부만 완료되면 일부, 남은 스토리에 에이전트가 돌면 진행 중, 완료가 없으면 미구현. 담당 스토리가 없는 명세는 미검증. 명세에 state 를 적어 따로 판정한 명세는 그 판정이 앞섬. 대상 제외: 범위에서 뺌. 완료율에 넣지 않음.';
27
+ // 담당 스토리 칸이 없을 때(스토리가 없고 AC 에 담당 스토리도 없음) text.reqRule 이 기본 문구 그대로면 대신 보이는 문구(판정은 명세의 state).
28
+ export const STORYLESS_REQ_RULE =
29
+ '구현 여부: 스토리가 없는 프로젝트라 명세에 적은 state 로 판정. state 가 없는 명세는 근거 AC 가 있으면 미검증, 없으면 명세 없음. 대상 제외: 범위에서 뺌. 완료율에 넣지 않음.';
30
+
31
+ export const isDir = (p) => {
32
+ try {
33
+ return fs.statSync(p).isDirectory();
34
+ } catch {
35
+ return false;
36
+ }
37
+ };
38
+
39
+ // JSON 글 앞의 BOM 을 뗀다.
40
+ export const noBom = (text) => (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
41
+
42
+ // 자료와 출처 파일을 읽는 크기 상한.
43
+ export const READ_MAX = 32 * 1024 * 1024;
44
+
45
+ // 일반 파일만 읽는다(링크는 따라감). FIFO, 장치 같은 특수 파일은 열지 않는다(열면 멈추거나 끝없이 읽어 메모리를 다 씀).
46
+ // 특수 파일은 EINVAL, max 를 넘는 파일은 EFBIG 예외. encoding 이 null 이면 Buffer.
47
+ export function readRegular(file, encoding = 'utf8', fsx = fs, max = READ_MAX) {
48
+ const st = fsx.statSync(file);
49
+ const code = !st.isFile() ? 'EINVAL' : st.size > max ? 'EFBIG' : null;
50
+ if (code) throw Object.assign(new Error(`${path.basename(file)}: ${code === 'EINVAL' ? '일반 파일 아님' : '크기 상한 초과'}`), { code });
51
+ return fsx.readFileSync(file, encoding);
52
+ }
53
+
54
+ // 프로젝트 안 자료 폴더의 경로 가운데 링크인 첫 폴더(.omc, progress-mon 등). 없으면 null.
55
+ // 프로젝트 밖(--data)이면 자료 폴더 자체만 본다. 아직 없는 폴더에서 멈춘다.
56
+ function linkedDir(root, dataDir) {
57
+ const rel = path.relative(root, dataDir);
58
+ const inside = rel && !rel.startsWith('..') && !path.isAbsolute(rel);
59
+ const parts = inside ? rel.split(path.sep) : [];
60
+ const dirs = inside ? parts.map((_, i) => path.join(root, ...parts.slice(0, i + 1))) : [dataDir];
61
+ for (const d of dirs) {
62
+ let st;
63
+ try {
64
+ st = fs.lstatSync(d);
65
+ } catch {
66
+ return null;
67
+ }
68
+ if (st.isSymbolicLink()) return d;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ // 자료 폴더나 그 위 폴더가 링크면 거부. 저장소가 심은 링크를 따라 프로젝트 밖의 파일을 쓰거나 지우지 않게.
74
+ function refuseLinkedData(ctx) {
75
+ const d = linkedDir(ctx.project, ctx.dataDir);
76
+ if (d) throw new UsageError(`자료 폴더 경로의 ${path.relative(ctx.project, d) || d} 가 링크라 거부. 링크를 지우고 다시 실행`);
77
+ return ctx;
78
+ }
79
+
80
+ // 사설 IPv4(10/8, 172.16/12, 192.168/16)만 참. 이름, IPv6, 0.0.0.0, 공인, 링크 로컬, 100.64/10 은 거짓.
81
+ export function privateIPv4(ip) {
82
+ if (typeof ip !== 'string' || !net.isIPv4(ip)) return false;
83
+ const [a, b] = ip.split('.').map(Number);
84
+ return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
85
+ }
86
+
87
+ // git 출력(앞뒤 공백 제거). git 이 없거나 실패하면 예외.
88
+ export const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
89
+
90
+ // 프로젝트 루트: --project, 아니면 cwd 의 git 최상위, 실패면 cwd.
91
+ export function projectRoot(cwd, project) {
92
+ if (project) return path.resolve(cwd, project);
93
+ try {
94
+ const top = git(cwd, ['rev-parse', '--show-toplevel']);
95
+ if (top) return path.resolve(top);
96
+ } catch {
97
+ // git 이 없거나 저장소가 아님. cwd 를 씀.
98
+ }
99
+ return path.resolve(cwd);
100
+ }
101
+
102
+ // 런타임과 자료 폴더 판별(계획 3절). 폴더를 만들지 않는다. 자료 폴더 경로에 링크가 있으면 UsageError.
103
+ // 돌려줌: { project, runtime, dataDir, exists, warn }
104
+ export const resolveContext = (opts = {}) => refuseLinkedData(pickContext(opts));
105
+
106
+ function pickContext({ cwd = process.cwd(), project, runtime, data } = {}) {
107
+ if (runtime !== undefined && !RUNTIMES.includes(runtime)) throw new UsageError(`--runtime 은 claude 또는 codex: ${runtime}`);
108
+ const root = projectRoot(cwd, project);
109
+ if (data) {
110
+ const dataDir = path.resolve(cwd, data);
111
+ return { project: root, runtime: runtime || 'claude', dataDir, exists: isDir(dataDir), warn: null };
112
+ }
113
+ const dirOf = (rt) => path.join(root, ...DATA_REL[rt].split('/'));
114
+ if (runtime) return { project: root, runtime, dataDir: dirOf(runtime), exists: isDir(dirOf(runtime)), warn: null };
115
+ const omc = isDir(dirOf('claude'));
116
+ const omx = isDir(dirOf('codex'));
117
+ if (omx && !omc) return { project: root, runtime: 'codex', dataDir: dirOf('codex'), exists: true, warn: null };
118
+ const warn = omc && omx ? '경고: .omc/progress-mon/ 과 .omx/progress-mon/ 이 모두 있음. .omc/progress-mon/ 사용(--runtime codex 로 바꿀 수 있음)' : null;
119
+ return { project: root, runtime: 'claude', dataDir: dirOf('claude'), exists: omc, warn };
120
+ }
121
+
122
+ // 사용법 오류(종료 1).
123
+ export class UsageError extends Error {}
124
+
125
+ const RENAME_RETRIES = 5;
126
+ const RENAME_STEP_MS = 50;
127
+ const RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
128
+
129
+ // 원자적 쓰기: 고유 이름 임시 파일을 wx 로 만들고 rename. 그 자리의 링크를 따라 쓰지 않는다.
130
+ // Windows 의 EPERM, EBUSY, EACCES 는 50 ms 간격으로 5회 다시. 실패면 임시 파일을 지우고 UsageError(원본 그대로).
131
+ export async function writeAtomic(file, text, fsx = fs) {
132
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
133
+ const fail = (e) => {
134
+ try {
135
+ fsx.rmSync(tmp, { force: true });
136
+ } catch {
137
+ // 임시 파일을 못 지워도 원본은 그대로
138
+ }
139
+ return new UsageError(`${path.basename(file)} 쓰기 실패(${e.code || e.message}). 원본 그대로`);
140
+ };
141
+ try {
142
+ fsx.writeFileSync(tmp, text, { flag: 'wx' });
143
+ } catch (e) {
144
+ throw e.code === 'EEXIST' ? new UsageError(`${path.basename(file)} 쓰기 실패(EEXIST). 원본 그대로`) : fail(e);
145
+ }
146
+ for (let i = 0; ; i++) {
147
+ try {
148
+ fsx.renameSync(tmp, file);
149
+ return;
150
+ } catch (e) {
151
+ if (i < RENAME_RETRIES && RENAME_RETRY_CODES.has(e.code)) {
152
+ await new Promise((r) => setTimeout(r, RENAME_STEP_MS));
153
+ continue;
154
+ }
155
+ throw fail(e);
156
+ }
157
+ }
158
+ }
159
+
160
+ // 화면 표기용 자료 폴더: 프로젝트 기준 상대 경로, / 구분, 끝에 /. 절대 경로를 내지 않는다.
161
+ export function dataLabel(ctx) {
162
+ const rel = path.relative(ctx.project, ctx.dataDir).split(path.sep).join('/');
163
+ if (!rel) return './';
164
+ if (path.isAbsolute(rel) || /^[A-Za-z]:/.test(rel)) return path.basename(ctx.dataDir) + '/';
165
+ return rel + '/';
166
+ }
167
+
168
+ export function defaults(project, runtime = 'claude') {
169
+ const name = path.basename(project);
170
+ return {
171
+ schema: 1,
172
+ title: `${name} 진척도`,
173
+ shortName: name,
174
+ port: 30000,
175
+ portStrict: false,
176
+ hosts: [],
177
+ refreshSec: 30,
178
+ stages: 'auto',
179
+ stories: [],
180
+ commitRegex: null,
181
+ agentLabelPattern: null,
182
+ sources: {
183
+ events: 'auto',
184
+ git: true,
185
+ gitMax: 5000,
186
+ workflowDirs: runtime === 'codex' ? [] : 'auto',
187
+ agentStaleMin: 15,
188
+ journalMaxAgeH: 24,
189
+ journalMaxSessions: 20,
190
+ },
191
+ text: { reqRule: DEFAULT_REQ_RULE, plannedPrefix: '예정' },
192
+ };
193
+ }
194
+
195
+ const isInt = (v) => Number.isInteger(v);
196
+ const isStr = (v) => typeof v === 'string' && v.length > 0;
197
+
198
+ // 사용자 정규식(commitRegex, agentLabelPattern)의 길이 상한. 실행 시간 상한은 pm-sources.mjs 의 regexBatch.
199
+ export const REGEX_MAX = 300;
200
+
201
+ // 정규식 문자열 컴파일. 실패면 이유 문자열.
202
+ function compileError(src, { needId, noGroups }) {
203
+ if (src.length > REGEX_MAX) return `정규식이 ${REGEX_MAX}자를 넘음(${src.length}자)`;
204
+ try {
205
+ new RegExp(src);
206
+ } catch (e) {
207
+ return `정규식 컴파일 실패: ${e.message}`;
208
+ }
209
+ // 빈 대안을 붙여 빈 문자열에 맞추면 결과 길이 - 1 이 캡처 그룹 수.
210
+ const groups = new RegExp(`${src}|`).exec('').length - 1;
211
+ if (needId && !/\(\?<id>/.test(src)) return '이름 그룹 id 가 없음';
212
+ if (noGroups && groups > 0) return '그룹을 쓰지 않음(비캡처 (?:) 만)';
213
+ return null;
214
+ }
215
+
216
+ // 설정 형식 검사. 돌려줌: [{ path, msg }]. 틀린 칸은 기본값으로 둔다.
217
+ export function validate(raw, project, runtime = 'claude') {
218
+ const d = defaults(project, runtime);
219
+ const errors = [];
220
+ const cfg = { ...d, sources: { ...d.sources }, text: { ...d.text } };
221
+ const bad = (p, msg) => errors.push({ path: p, msg });
222
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
223
+ bad('$', '객체가 아님');
224
+ return { config: cfg, errors };
225
+ }
226
+ // from[key] 가 있으면 ok 일 때 into 에 넣고, 아니면 base.key 오류.
227
+ const taker = (from, into, base) => (key, ok, msg) => {
228
+ if (from[key] === undefined) return;
229
+ if (ok(from[key])) into[key] = from[key];
230
+ else bad(`${base}.${key}`, msg);
231
+ };
232
+ const take = taker(raw, cfg, '$');
233
+ take('schema', (v) => v === 1, '1 이어야 함');
234
+ take('title', isStr, '빈 문자열이 아닌 문자열');
235
+ take('shortName', isStr, '빈 문자열이 아닌 문자열');
236
+ take('port', (v) => isInt(v) && v >= 1024 && v <= 65535, '1024~65535 정수');
237
+ take('portStrict', (v) => typeof v === 'boolean', '참 또는 거짓');
238
+ take('hosts', (v) => Array.isArray(v) && v.every(privateIPv4), '사설 IPv4(10/8, 172.16/12, 192.168/16) 목록');
239
+ take('refreshSec', (v) => isInt(v) && v >= 5 && v <= 3600, '5~3600 정수');
240
+ const idList = (v, nameKey) => Array.isArray(v) && v.every((x) => x && isStr(x.id) && (x[nameKey] === undefined || typeof x[nameKey] === 'string')) && new Set(v.map((x) => x.id)).size === v.length;
241
+ take('stages', (v) => v === 'auto' || idList(v, 'label'), '"auto" 또는 [{id, label}](id 겹침 없음)');
242
+ take('stories', (v) => idList(v, 'name'), '[{id, name}](id 겹침 없음)');
243
+ for (const [key, opt] of [['commitRegex', { needId: true }], ['agentLabelPattern', { noGroups: true }]]) {
244
+ if (raw[key] === undefined || raw[key] === null) continue;
245
+ if (typeof raw[key] !== 'string') {
246
+ bad(`$.${key}`, '정규식 문자열');
247
+ continue;
248
+ }
249
+ const why = compileError(raw[key], opt);
250
+ if (why) bad(`$.${key}`, why);
251
+ else cfg[key] = raw[key];
252
+ }
253
+ const src = raw.sources;
254
+ if (src !== undefined) {
255
+ if (!src || typeof src !== 'object' || Array.isArray(src)) bad('$.sources', '객체가 아님');
256
+ else {
257
+ const s = taker(src, cfg.sources, '$.sources');
258
+ s('events', (v) => v === null || isStr(v), '"auto", 경로, null');
259
+ s('git', (v) => typeof v === 'boolean', '참 또는 거짓');
260
+ s('gitMax', (v) => isInt(v) && v > 0, '양의 정수');
261
+ s('workflowDirs', (v) => v === 'auto' || (Array.isArray(v) && v.every(isStr)), '"auto" 또는 경로 목록');
262
+ s('agentStaleMin', (v) => isInt(v) && v > 0, '양의 정수');
263
+ s('journalMaxAgeH', (v) => isInt(v) && v > 0, '양의 정수');
264
+ s('journalMaxSessions', (v) => isInt(v) && v >= 0, '0 이상 정수(0 은 상한 없음)');
265
+ }
266
+ }
267
+ const text = raw.text;
268
+ if (text !== undefined) {
269
+ if (!text || typeof text !== 'object' || Array.isArray(text)) bad('$.text', '객체가 아님');
270
+ else
271
+ for (const key of ['reqRule', 'plannedPrefix']) {
272
+ if (text[key] === undefined) continue;
273
+ if (typeof text[key] === 'string') cfg.text[key] = text[key];
274
+ else bad(`$.text.${key}`, '문자열');
275
+ }
276
+ }
277
+ return { config: cfg, errors };
278
+ }
279
+
280
+ // 자료 폴더의 config.json 을 읽는다. 없으면 기본값. 돌려줌: { config, errors, found }
281
+ export function loadConfig(ctx) {
282
+ const file = path.join(ctx.dataDir, 'config.json');
283
+ let text;
284
+ try {
285
+ text = readRegular(file);
286
+ } catch (e) {
287
+ if (e.code === 'ENOENT') return { config: defaults(ctx.project, ctx.runtime), errors: [], found: false };
288
+ return { config: defaults(ctx.project, ctx.runtime), errors: [{ path: '$', msg: `읽기 실패(${e.code || 'error'})` }], found: true };
289
+ }
290
+ let raw;
291
+ try {
292
+ raw = JSON.parse(noBom(text));
293
+ } catch (e) {
294
+ return { config: defaults(ctx.project, ctx.runtime), errors: [{ path: '$', msg: `JSON 아님: ${e.message}` }], found: true };
295
+ }
296
+ return { ...validate(raw, ctx.project, ctx.runtime), found: true };
297
+ }
298
+
299
+ // config.json 을 읽고 형식 오류가 있으면 UsageError.
300
+ export function configOrFail(ctx) {
301
+ const { config, errors } = loadConfig(ctx);
302
+ if (errors.length) throw new UsageError(`config.json 형식 오류: ${errors.map((e) => `${e.path} ${e.msg}`).join('; ')}`);
303
+ return config;
304
+ }
305
+
306
+ // 단계 목록. "auto" 는 events 출처가 있으면 ultra-chain 10단계, 없으면 빈 목록.
307
+ export function stagesFor(config, hasEvents) {
308
+ if (config.stages === 'auto') return hasEvents ? ULTRA_STAGES.map((s) => ({ ...s })) : [];
309
+ return config.stages.map((s) => ({ id: s.id, label: s.label || s.id }));
310
+ }
311
+
312
+ // events.jsonl 경로. "auto" 는 <project>/.omc(Codex .omx)/ultra-chain/*/events.jsonl 가운데 수정 시각 최신.
313
+ // 상대 경로는 프로젝트 루트 기준. null 이나 찾지 못하면 null.
314
+ export function eventsPath(config, ctx) {
315
+ const v = config.sources.events;
316
+ if (v === null || v === undefined) return null;
317
+ if (v !== 'auto') return path.resolve(ctx.project, v);
318
+ const base = path.join(ctx.project, ctx.runtime === 'codex' ? '.omx' : '.omc', 'ultra-chain');
319
+ let best = null;
320
+ let bestT = -Infinity;
321
+ let runs = [];
322
+ try {
323
+ runs = fs.readdirSync(base, { withFileTypes: true });
324
+ } catch {
325
+ return null;
326
+ }
327
+ for (const e of runs) {
328
+ if (!e.isDirectory()) continue;
329
+ const f = path.join(base, e.name, 'events.jsonl');
330
+ try {
331
+ const t = fs.statSync(f).mtimeMs;
332
+ if (t > bestT) {
333
+ bestT = t;
334
+ best = f;
335
+ }
336
+ } catch {
337
+ // events 가 없는 실행 폴더
338
+ }
339
+ }
340
+ return best;
341
+ }