@kaydash9999/banker-plugins 0.22.0 → 0.23.2

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 +68 -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 +143 -0
  17. package/codex/skills/teamwork/scripts/teamwork.mjs +1007 -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 +284 -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 +164 -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 +549 -0
  77. package/skills/progress-mon/scripts/pm-config.mjs +343 -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 +333 -0
  81. package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
  82. package/skills/progress-mon/scripts/pm-server.mjs +781 -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 +107 -17
  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 +200 -159
  91. package/skills/remains/SKILL.md +2 -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 +197 -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,333 @@
1
+ // pm-memory.mjs: 메모리 규칙 등록(계획 6.2절). 표식으로 중복을 막고 사용자 파일은 끝에 덧붙이기만 한다.
2
+ // 대상 파일은 에이전트가 넘긴다(--memory, --memory-dir). 스크립트는 HOME 아래 쓰기 경로를 스스로 만들지 않는다.
3
+ // 덧붙인 뒤 규칙 로드 한도를 재고 경고 줄을 낸다(종료 코드는 그대로). status, check 도 같은 측정을 다시 한다.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { git, readRegular, UsageError } from './pm-config.mjs';
8
+
9
+ export const MARKER = '<!-- progress-mon:rule -->';
10
+ export const OMX_OPEN = '<!-- USER:OMX:POLICY:START -->';
11
+ export const OMX_CLOSE = '<!-- USER:OMX:POLICY:END -->';
12
+ const KINDS = ['claude-md', 'agents-md', 'auto-memory', 'none'];
13
+
14
+ // 규칙 줄. 경로는 자료 폴더 기준 상대 표기뿐(스킬 폴더 절대 경로는 state.json 의 skillDir).
15
+ export function ruleLine(runtime) {
16
+ if (runtime === 'codex') {
17
+ return `진척도 화면(progress-mon): 모든 명령에 --runtime codex 를 붙임. 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. .omx/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 banker-progress-mon 스킬(플러그인 설치는 banker:progress-mon)로 호출. 세션 시작 때 status, 꺼져 있으면 start. start, stop, status 는 sandbox_permissions "require_escalated" 로 승인 요청. 세션을 닫아도 화면을 두려면 사용자 터미널에서 serve 실행을 안내. ${MARKER}`;
18
+ }
19
+ return `진척도 화면(progress-mon): 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. .omc/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 progress-mon 스킬로 호출. 세션 시작 때 status, 꺼져 있으면 start. ${MARKER}`;
20
+ }
21
+
22
+ // 자동 메모리 규칙 파일(새로 만들 때)과 MEMORY.md 색인 줄.
23
+ const ruleFile = (runtime) => `---
24
+ name: progress-mon
25
+ description: 진척도 화면(progress-mon) 갱신 규칙. 단계, 스토리, 그 밖 작업을 stage, story, item 으로 기록
26
+ metadata:
27
+ type: feedback
28
+ ---
29
+
30
+ ${ruleLine(runtime)}
31
+ `;
32
+ const INDEX_LINE = `- [진척도 화면 갱신 규칙](progress-mon.md) - stage, story, item 으로 progress-mon 갱신 ${MARKER}`;
33
+
34
+ // ---------- 한도 ----------
35
+
36
+ const MEMORY_LIMIT = { lines: 200, bytes: 25000 };
37
+ const CODEX_DEFAULT_BYTES = 32768;
38
+ const nine = (n) => Math.ceil(n * 0.9);
39
+ const comma = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
40
+
41
+ // Codex 가 파일을 빈 것으로 보는 기준(Rust trim 과 같이 BOM 은 글로 친다).
42
+ const hasContent = (text) => /\P{White_Space}/u.test(text);
43
+
44
+ // 줄 수: 마지막 줄 끝의 줄바꿈은 줄을 늘리지 않는다.
45
+ export function lineCount(text) {
46
+ if (!text) return 0;
47
+ const n = text.split('\n').length;
48
+ return text.endsWith('\n') ? n - 1 : n;
49
+ }
50
+
51
+ // MEMORY.md 한 파일의 측정. 돌려줌: { lines, bytes, level: 'over' | 'near' | null }
52
+ export function measureMemory(file) {
53
+ let buf;
54
+ try {
55
+ buf = readRegular(file, null);
56
+ } catch {
57
+ return { lines: 0, bytes: 0, level: null };
58
+ }
59
+ const lines = lineCount(buf.toString('utf8'));
60
+ const bytes = buf.length;
61
+ const level = lines >= MEMORY_LIMIT.lines || bytes >= MEMORY_LIMIT.bytes ? 'over' : lines >= nine(MEMORY_LIMIT.lines) || bytes >= nine(MEMORY_LIMIT.bytes) ? 'near' : null;
62
+ return { lines, bytes, level };
63
+ }
64
+
65
+ // $CODEX_HOME/config.toml 의 최상위 project_doc_max_bytes(읽기만). 첫 [표] 머리 전의 줄만 보고 주석은 뺀다.
66
+ // 돌려줌: { bytes, assumed } (파일이 없거나 값이 정수가 아니면 기본값 가정)
67
+ export function codexLimit(env = process.env) {
68
+ const home = env.CODEX_HOME || path.join(os.homedir(), '.codex');
69
+ let text;
70
+ try {
71
+ text = readRegular(path.join(home, 'config.toml'));
72
+ } catch {
73
+ return { bytes: CODEX_DEFAULT_BYTES, assumed: true };
74
+ }
75
+ for (const raw of text.split(/\r?\n/)) {
76
+ const line = raw.replace(/#.*$/, '').trim();
77
+ if (line.startsWith('[')) break;
78
+ const m = /^project_doc_max_bytes\s*=\s*(.*)$/.exec(line);
79
+ if (!m) continue;
80
+ const v = m[1].trim();
81
+ if (!/^\+?\d+(?:_\d+)*$/.test(v)) return { bytes: CODEX_DEFAULT_BYTES, assumed: true };
82
+ return { bytes: Number(v.replace(/[+_]/g, '')), assumed: false };
83
+ }
84
+ return { bytes: CODEX_DEFAULT_BYTES, assumed: true };
85
+ }
86
+
87
+ // ---------- Codex 쓰기 대상(계획 6.2절 표) ----------
88
+
89
+ const readText = (file) => {
90
+ try {
91
+ return readRegular(file);
92
+ } catch {
93
+ return null;
94
+ }
95
+ };
96
+
97
+ // banker 블록과 우리 블록을 뺀 글. 안이 빈 USER:OMX:POLICY 묶음도 뺀다.
98
+ export function ownText(text) {
99
+ const kept = [];
100
+ let banker = null;
101
+ for (const line of text.split(/\r?\n/)) {
102
+ const t = line.trim();
103
+ if (banker) {
104
+ if (t === `<!-- banker:${banker}:end -->`) banker = null;
105
+ continue;
106
+ }
107
+ const m = /^<!-- banker:([a-z0-9-]+):start -->$/.exec(t);
108
+ if (m) {
109
+ banker = m[1];
110
+ continue;
111
+ }
112
+ if (line.includes(MARKER)) continue;
113
+ kept.push(line);
114
+ }
115
+ const out = [];
116
+ for (let i = 0; i < kept.length; i++) {
117
+ if (kept[i].trim() === OMX_OPEN) {
118
+ let j = i + 1;
119
+ while (j < kept.length && kept[j].trim() !== OMX_CLOSE && !hasContent(kept[j])) j++;
120
+ if (j < kept.length && kept[j].trim() === OMX_CLOSE) {
121
+ i = j;
122
+ continue;
123
+ }
124
+ }
125
+ out.push(kept[i]);
126
+ }
127
+ return out.join('\n');
128
+ }
129
+
130
+ // 폴더 하나에서 Codex 가 읽는 파일과 우리가 쓸 파일. shadow: override 에 블록만 있어 AGENTS.md 가 가려짐.
131
+ export function codexTarget(dir) {
132
+ const override = path.join(dir, 'AGENTS.override.md');
133
+ const main = path.join(dir, 'AGENTS.md');
134
+ const ot = readText(override);
135
+ if (ot === null || !hasContent(ot)) return { file: main, shadow: false };
136
+ return { file: override, shadow: !hasContent(ownText(ot)) };
137
+ }
138
+
139
+ const gitTop = (dir) => {
140
+ try {
141
+ return path.resolve(git(dir, ['rev-parse', '--show-toplevel']));
142
+ } catch {
143
+ return null;
144
+ }
145
+ };
146
+
147
+ // Codex 프로젝트 지시 합계: 저장소 루트에서 프로젝트 루트까지 각 폴더에서 Codex 가 읽는 파일 크기의 합.
148
+ export function measureCodex(project, env = process.env) {
149
+ const limit = codexLimit(env);
150
+ const proj = path.resolve(project);
151
+ const top = gitTop(proj);
152
+ const rel = top ? path.relative(top, proj) : '';
153
+ const dirs = [top && !rel.startsWith('..') && !path.isAbsolute(rel) ? top : proj];
154
+ if (dirs[0] !== proj) for (const part of rel.split(path.sep).filter(Boolean)) dirs.push(path.join(dirs[dirs.length - 1], part));
155
+ let total = 0;
156
+ for (const d of dirs) {
157
+ try {
158
+ total += fs.statSync(codexTarget(d).file).size;
159
+ } catch {
160
+ // 그 폴더에 지시 파일 없음
161
+ }
162
+ }
163
+ const level = limit.bytes === 0 ? 'off' : total >= limit.bytes ? 'over' : total >= nine(limit.bytes) ? 'near' : null;
164
+ return { total, limit: limit.bytes, assumed: limit.assumed, level };
165
+ }
166
+
167
+ // ---------- 경고 줄 ----------
168
+
169
+ const MEM_ALT = '대안: --memory claude-md 로 다시 등록(CLAUDE.md 는 한도 없음)';
170
+ // init 의 메모리 대상이 거부될 때 안내하는 종류별 대안.
171
+ const CLAUDE_ALT = '대안: --memory agents-md, 또는 --memory auto-memory --memory-dir <dir>';
172
+ const AGENTS_ALT = '대안: --memory claude-md, 또는 --memory auto-memory --memory-dir <dir>';
173
+ const AUTO_ALT = '대안: 다른 --memory-dir, 또는 --memory claude-md 나 agents-md';
174
+ const CODEX_ALT = '대안: 프로젝트 지시 파일 정리(banker cleansing-memory)';
175
+ export const SHADOW_WARN = '경고: AGENTS.override.md 에 블록만 있어 AGENTS.md 가 가려짐';
176
+
177
+ function memoryLines(m) {
178
+ const lim = `로드 한도(${MEMORY_LIMIT.lines}줄 또는 ${comma(MEMORY_LIMIT.bytes)} B)`;
179
+ if (m.level === 'over') return [`경고: MEMORY.md 가 ${lim}를 넘어 규칙이 로드되지 않음`, MEM_ALT];
180
+ if (m.level === 'near') return [`경고: MEMORY.md 가 ${lim}의 90% 이상이라 규칙이 곧 로드 밖으로 밀릴 수 있음`, MEM_ALT];
181
+ return [];
182
+ }
183
+
184
+ function codexLines(c) {
185
+ if (c.level === 'off') return ['경고: Codex 프로젝트 문서 로딩이 꺼져 있어 규칙이 로드되지 않음', '대안: config.toml 의 project_doc_max_bytes 설정 확인'];
186
+ const lim = `로드 한도(${comma(c.limit)} B${c.assumed ? ', 기본값 가정' : ''})`;
187
+ if (c.level === 'over') return [`경고: Codex 프로젝트 지시 합계가 ${lim}를 넘어 규칙이 로드되지 않음`, CODEX_ALT];
188
+ if (c.level === 'near') return [`경고: Codex 프로젝트 지시 합계가 ${lim}의 90% 이상이라 규칙이 곧 로드 밖으로 밀릴 수 있음`, CODEX_ALT];
189
+ return [];
190
+ }
191
+
192
+ // state.json 의 memory 로 다시 잰 경고 줄(status, check, init 이 같이 씀). 경고가 없으면 빈 목록.
193
+ export function memoryWarnings(memory, project, env = process.env) {
194
+ if (!memory || typeof memory !== 'object') return [];
195
+ if (memory.kind === 'auto-memory' && typeof memory.path === 'string') return memoryLines(measureMemory(memory.path));
196
+ if (memory.kind === 'agents-md') return [...(codexTarget(project).shadow ? [SHADOW_WARN] : []), ...codexLines(measureCodex(project, env))];
197
+ return [];
198
+ }
199
+
200
+ // ---------- 쓰기 ----------
201
+
202
+ // 경로 b 가 폴더 a 안(같음 포함)인지.
203
+ const within = (a, b) => {
204
+ const rel = path.relative(a, b);
205
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
206
+ };
207
+
208
+ // 링크의 대상 stat. 대상이 없거나 실제 경로가 base 밖이면 UsageError.
209
+ function linkTarget(file, name, base, tail) {
210
+ let st;
211
+ let real;
212
+ try {
213
+ st = fs.statSync(file);
214
+ real = fs.realpathSync(file);
215
+ } catch {
216
+ throw new UsageError(`${name} 은 대상이 없는 링크라 거부${tail}`);
217
+ }
218
+ if (!within(fs.realpathSync(base), real)) throw new UsageError(`${name} 은 ${path.basename(base)} 밖을 가리키는 링크라 거부${tail}`);
219
+ return st;
220
+ }
221
+
222
+ // 파일을 UTF-8 로 읽는다(BOM 은 뗌). UTF-8 이 아니면 UsageError.
223
+ function readUtf8(file, name, tail) {
224
+ const buf = fs.readFileSync(file);
225
+ try {
226
+ return new TextDecoder('utf-8', { fatal: true }).decode(buf);
227
+ } catch {
228
+ throw new UsageError(`${name} 이 UTF-8 이 아니라 거부${tail}`);
229
+ }
230
+ }
231
+
232
+ // 덧붙일 수 있는 파일인지. 일반 파일이나, 실제 경로가 base(프로젝트 루트나 --memory-dir) 안인 일반 파일을 가리키는 링크만.
233
+ // 저장소가 심은 링크로 밖의 파일에 덧붙이지 않게 한다. CLAUDE.md -> AGENTS.md 같은 안쪽 링크는 받는다. 없으면 새로 만든다.
234
+ // 하드링크(nlink > 1)는 거부한다. 다른 이름이 어디 있는지 알 수 없어, 밖의 파일과 같은 내용일 수 있다.
235
+ // 돌려줌: { exists, text, eol }. 거부는 UsageError.
236
+ function inspect(file, name, base, alt = '') {
237
+ const tail = alt ? `. ${alt}` : '';
238
+ let lst;
239
+ try {
240
+ lst = fs.lstatSync(file);
241
+ } catch (e) {
242
+ if (e.code === 'ENOENT') return { exists: false, text: '', eol: '\n' };
243
+ throw new UsageError(`${name} 을 읽지 못함(${e.code || e.message})${tail}`);
244
+ }
245
+ const st = lst.isSymbolicLink() ? linkTarget(file, name, base, tail) : lst;
246
+ if (!st.isFile()) throw new UsageError(`${name} 은 일반 파일이 아니라 거부${tail}`);
247
+ if (st.nlink > 1) throw new UsageError(`${name} 은 하드링크(이름 ${st.nlink}개)라 거부${tail}`);
248
+ const text = readUtf8(file, name, tail);
249
+ // TextDecoder 는 BOM 을 떼므로 줄 끝 판정만 쓴다. 쓰기는 원본 바이트 뒤에 덧붙인다.
250
+ return { exists: true, text, eol: text.includes('\r\n') ? '\r\n' : '\n' };
251
+ }
252
+
253
+ // 원본 바이트를 그대로 두고 끝에 줄을 덧붙인다. 마지막 줄에 줄바꿈이 없으면 먼저 넣는다.
254
+ // 연 fd 를 fstat 으로 다시 본다: 검사(inspect)와 쓰기 사이에 하드링크나 특수 파일로 바뀌었으면 쓰지 않는다.
255
+ function append(file, info, lines) {
256
+ const glue = info.text && !info.text.endsWith('\n') ? info.eol : '';
257
+ const body = glue + lines.join(info.eol) + info.eol;
258
+ // O_NONBLOCK: 검사 뒤 FIFO 로 바뀐 자리에서 open 이 읽는 쪽을 기다리며 멈추지 않게 한다(Windows 에는 없어 0)
259
+ const { O_WRONLY, O_APPEND, O_CREAT, O_EXCL } = fs.constants;
260
+ const flags = O_WRONLY | (fs.constants.O_NONBLOCK || 0) | O_CREAT | (info.exists ? O_APPEND : O_EXCL);
261
+ const fd = fs.openSync(file, flags, 0o666);
262
+ try {
263
+ const st = fs.fstatSync(fd);
264
+ if (!st.isFile() || st.nlink > 1) throw new UsageError(`${path.basename(file)} 이 검사 뒤 하드링크나 특수 파일로 바뀌어 쓰지 않음`);
265
+ fs.writeFileSync(fd, body); // 다 쓸 때까지 반복
266
+ } finally {
267
+ fs.closeSync(fd);
268
+ }
269
+ }
270
+
271
+ // 등록 계획. 쓰기 전에 대상을 모두 검사한다(거부면 아무것도 바꾸지 않음).
272
+ // o: { kind, memoryDir, project, runtime }. 돌려줌: { kind, runtime, steps, memory: {kind, path} }
273
+ export function planMemory({ kind, memoryDir, project, runtime }) {
274
+ if (kind === undefined) throw new UsageError('init 에는 --memory claude-md|agents-md|auto-memory|none 가 필요함');
275
+ if (!KINDS.includes(kind)) throw new UsageError(`--memory 는 ${KINDS.join(', ')} 가운데 하나(받은 값 ${kind})`);
276
+ if (kind === 'none') return { kind, runtime, steps: [], memory: { kind, path: null } };
277
+ if (kind === 'auto-memory') {
278
+ if (!memoryDir) throw new UsageError('--memory auto-memory 에는 --memory-dir <dir> 가 필요함');
279
+ const dir = path.resolve(memoryDir);
280
+ if (fs.existsSync(dir) && !fs.statSync(dir).isDirectory()) throw new UsageError('--memory-dir 가 폴더가 아님');
281
+ const rule = path.join(dir, 'progress-mon.md');
282
+ const index = path.join(dir, 'MEMORY.md');
283
+ return {
284
+ kind,
285
+ runtime,
286
+ dir,
287
+ steps: [
288
+ { file: rule, name: '자동 메모리 progress-mon.md', info: inspect(rule, 'progress-mon.md', dir, AUTO_ALT), what: 'rule-file' },
289
+ { file: index, name: '자동 메모리 MEMORY.md', info: inspect(index, 'MEMORY.md', dir, AUTO_ALT), what: 'index' },
290
+ ],
291
+ memory: { kind, path: index },
292
+ };
293
+ }
294
+ if (memoryDir) throw new UsageError('--memory-dir 는 --memory auto-memory 에만 씀');
295
+ const root = path.resolve(project);
296
+ if (kind === 'claude-md') {
297
+ const file = path.join(root, 'CLAUDE.md');
298
+ return { kind, runtime, steps: [{ file, name: 'CLAUDE.md', info: inspect(file, 'CLAUDE.md', root, CLAUDE_ALT), what: 'line' }], memory: { kind, path: file } };
299
+ }
300
+ const t = codexTarget(root);
301
+ const name = path.basename(t.file);
302
+ return { kind, runtime, steps: [{ file: t.file, name, info: inspect(t.file, name, root, AGENTS_ALT), what: 'block' }], memory: { kind, path: t.file } };
303
+ }
304
+
305
+ // 계획대로 쓴다. 돌려줌: 출력 줄 목록(규칙 줄, 경고 줄).
306
+ export function applyMemory(plan, project, env = process.env) {
307
+ if (plan.kind === 'none') return ['규칙 등록 안 함(--memory none)'];
308
+ const out = [];
309
+ if (plan.dir) fs.mkdirSync(plan.dir, { recursive: true });
310
+ for (const s of plan.steps) {
311
+ const { info } = s;
312
+ if (info.text.includes(MARKER)) {
313
+ out.push(`규칙 이미 있음: ${s.name}`);
314
+ continue;
315
+ }
316
+ const fresh = !info.exists;
317
+ if (s.what === 'rule-file') {
318
+ if (fresh) fs.writeFileSync(s.file, ruleFile(plan.runtime), { flag: 'wx' });
319
+ else append(s.file, info, [ruleLine(plan.runtime)]);
320
+ out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦' : '1줄'})`);
321
+ } else if (s.what === 'index') {
322
+ append(s.file, info, [INDEX_LINE]);
323
+ out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}색인 1줄)`);
324
+ } else if (s.what === 'line') {
325
+ append(s.file, info, [ruleLine(plan.runtime)]);
326
+ out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}1줄)`);
327
+ } else {
328
+ append(s.file, info, [OMX_OPEN, ruleLine(plan.runtime), OMX_CLOSE]);
329
+ out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}3줄)`);
330
+ }
331
+ }
332
+ return [...out, ...memoryWarnings(plan.memory, project, env)];
333
+ }