@kaydash9999/banker-plugins 0.23.3 → 0.24.1

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 (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +14 -6
  5. package/codex/manifest.json +6 -2
  6. package/codex/skills/teamwork/scripts/teamwork.mjs +3 -3
  7. package/codex/transform-matrix.md +6 -2
  8. package/package.json +1 -1
  9. package/skills/build-testbox/SKILL.md +213 -0
  10. package/skills/build-testbox/scripts/args.mjs +156 -0
  11. package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
  12. package/skills/build-testbox/scripts/browsers.mjs +164 -0
  13. package/skills/build-testbox/scripts/containers.mjs +237 -0
  14. package/skills/build-testbox/scripts/detect.mjs +166 -0
  15. package/skills/build-testbox/scripts/proc.mjs +91 -0
  16. package/skills/build-testbox/scripts/report.mjs +109 -0
  17. package/skills/build-testbox/scripts/setup.mjs +144 -0
  18. package/skills/build-testbox/scripts/testbox.mjs +148 -0
  19. package/skills/git-init/SKILL.md +152 -0
  20. package/skills/git-init/scripts/git-init.mjs +440 -0
  21. package/skills/git-init/scripts/marker-block.mjs +321 -0
  22. package/skills/git-init/scripts/payload-scan.mjs +186 -0
  23. package/skills/init-dir/SKILL.md +172 -0
  24. package/skills/init-dir/scripts/init-dir.mjs +308 -0
  25. package/skills/init-dir/scripts/marker-block.mjs +321 -0
  26. package/skills/init-dir/scripts/templates.mjs +100 -0
  27. package/skills/init-dir/templates/KB/README.md +15 -0
  28. package/skills/init-dir/templates/docs/README.md +14 -0
  29. package/skills/init-dir/templates/logs/README.md +14 -0
  30. package/skills/init-dir/templates/logs/gitignore +3 -0
  31. package/skills/init-dir/templates/mockup/README.md +7 -0
  32. package/skills/init-dir/templates/scripts/README.md +39 -0
  33. package/skills/init-dir/templates/scripts/lib/service.mjs +596 -0
  34. package/skills/init-dir/templates/scripts/restart.cmd +13 -0
  35. package/skills/init-dir/templates/scripts/restart.sh +10 -0
  36. package/skills/init-dir/templates/scripts/service.conf +5 -0
  37. package/skills/init-dir/templates/scripts/start.cmd +13 -0
  38. package/skills/init-dir/templates/scripts/start.sh +10 -0
  39. package/skills/init-dir/templates/scripts/status.cmd +13 -0
  40. package/skills/init-dir/templates/scripts/status.sh +10 -0
  41. package/skills/init-dir/templates/scripts/stop.cmd +13 -0
  42. package/skills/init-dir/templates/scripts/stop.sh +10 -0
  43. package/skills/init-dir/templates/tests/README.md +15 -0
  44. package/skills/init-dir/templates/tests/run.cmd +13 -0
  45. package/skills/init-dir/templates/tests/run.mjs +160 -0
  46. package/skills/init-dir/templates/tests/run.sh +10 -0
  47. package/skills/init-dir/templates/tests/tests.conf +7 -0
  48. package/skills/init-dir/templates/work/README.md +8 -0
  49. package/skills/progress-mon/README.md +11 -8
  50. package/skills/progress-mon/SKILL.md +53 -16
  51. package/skills/progress-mon/assets/fixture/README.md +1 -1
  52. package/skills/progress-mon/assets/fixture/adr.json +35 -0
  53. package/skills/progress-mon/assets/fixture/arch.json +1 -1
  54. package/skills/progress-mon/assets/fixture/flow.json +2 -2
  55. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
  56. package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
  57. package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
  58. package/skills/progress-mon/references/adr.md +184 -0
  59. package/skills/progress-mon/references/authoring.md +13 -5
  60. package/skills/progress-mon/scripts/pm-adr-view.mjs +101 -0
  61. package/skills/progress-mon/scripts/pm-adr.mjs +515 -0
  62. package/skills/progress-mon/scripts/pm-check.mjs +59 -22
  63. package/skills/progress-mon/scripts/pm-data.mjs +1 -1
  64. package/skills/progress-mon/scripts/pm-html.mjs +1 -0
  65. package/skills/progress-mon/scripts/pm-memory.mjs +113 -28
  66. package/skills/progress-mon/scripts/pm-render.mjs +6 -4
  67. package/skills/progress-mon/scripts/pm-server.mjs +2 -1
  68. package/skills/progress-mon/scripts/pm-sources.mjs +10 -0
  69. package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
  70. package/skills/tune-tmux/SKILL.md +235 -0
  71. package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
  72. package/skills/tune-tmux/scripts/marker-block.mjs +321 -0
  73. package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
  74. package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
  75. package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
  76. package/skills/ultra-chain/SKILL.md +52 -22
  77. package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
  78. package/skills/ultra-chain/references/chain-report.mjs +13 -2
@@ -71,6 +71,7 @@ export const TABS = [
71
71
  ['req', '구현요건'],
72
72
  ['workflow', 'workflow'],
73
73
  ['arch', '구성도'],
74
+ ['adr', 'ADR'],
74
75
  ];
75
76
 
76
77
  // 서버 화면의 탭 머리. cur 는 켜진 탭 id.
@@ -1,4 +1,5 @@
1
1
  // pm-memory.mjs: 메모리 규칙 등록(계획 6.2절). 표식으로 중복을 막고 사용자 파일은 끝에 덧붙이기만 한다.
2
+ // 예외: 표식이 붙은 우리 줄(앞머리로 가린 규칙 줄과 색인 줄)이 지금 판과 다르면 그 줄만 제자리에서 바꾼다(첫 원본은 .progress-mon.bak).
2
3
  // 대상 파일은 에이전트가 넘긴다(--memory, --memory-dir). 스크립트는 HOME 아래 쓰기 경로를 스스로 만들지 않는다.
3
4
  // 덧붙인 뒤 규칙 로드 한도를 재고 경고 줄을 낸다(종료 코드는 그대로). status, check 도 같은 측정을 다시 한다.
4
5
  import fs from 'node:fs';
@@ -14,22 +15,33 @@ const KINDS = ['claude-md', 'agents-md', 'auto-memory', 'none'];
14
15
  // 규칙 줄. 경로는 자료 폴더 기준 상대 표기뿐(스킬 폴더 절대 경로는 state.json 의 skillDir).
15
16
  export function ruleLine(runtime) {
16
17
  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
+ return `진척도 화면(progress-mon): 모든 명령에 --runtime codex 를 붙임. 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. 결정이 생기거나 스토리가 끝나면 .omx/progress-mon/adr.json(ADR 탭)을 고치고 check. .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
  }
19
- return `진척도 화면(progress-mon): 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. .omc/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 progress-mon 스킬로 호출. 세션 시작 때 status, 꺼져 있으면 start. ${MARKER}`;
20
+ return `진척도 화면(progress-mon): 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. 결정이 생기거나 스토리가 끝나면 .omc/progress-mon/adr.json(ADR 탭)을 고치고 check. .omc/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 progress-mon 스킬로 호출. 세션 시작 때 status, 꺼져 있으면 start. ${MARKER}`;
20
21
  }
21
22
 
22
23
  // 자동 메모리 규칙 파일(새로 만들 때)과 MEMORY.md 색인 줄.
23
24
  const ruleFile = (runtime) => `---
24
25
  name: progress-mon
25
- description: 진척도 화면(progress-mon) 갱신 규칙. 단계, 스토리, 그 밖 작업을 stage, story, item 으로 기록
26
+ description: 진척도 화면(progress-mon) 갱신 규칙. 단계, 스토리, 그 밖 작업을 stage, story, item 으로, 결정은 adr.json 에 기록
26
27
  metadata:
27
28
  type: feedback
28
29
  ---
29
30
 
30
31
  ${ruleLine(runtime)}
31
32
  `;
32
- const INDEX_LINE = `- [진척도 화면 갱신 규칙](progress-mon.md) - stage, story, item 으로 progress-mon 갱신 ${MARKER}`;
33
+ export const INDEX_LINE = `- [진척도 화면 갱신 규칙](progress-mon.md) - stage, story, item 과 adr.json 으로 progress-mon 갱신 ${MARKER}`;
34
+
35
+ // 표식 줄 가운데 우리가 쓴 줄을 가리는 앞머리. 규칙 줄의 런타임은 `--runtime codex` 가 있으면 codex.
36
+ const RULE_HEAD = '진척도 화면(progress-mon):';
37
+ const INDEX_HEAD = '- [진척도 화면 갱신 규칙](progress-mon.md)';
38
+ // 표식 줄이 지금 판에서 되어야 할 줄. 앞머리가 다른 표식 줄(사용자가 쓴 줄 등)은 null(손대지 않음).
39
+ export function currentLine(line) {
40
+ if (!line.includes(MARKER)) return null;
41
+ if (line.startsWith(INDEX_HEAD)) return INDEX_LINE;
42
+ if (line.startsWith(RULE_HEAD)) return ruleLine(line.includes('--runtime codex') ? 'codex' : 'claude');
43
+ return null;
44
+ }
33
45
 
34
46
  // ---------- 한도 ----------
35
47
 
@@ -250,6 +262,8 @@ function inspect(file, name, base, alt = '') {
250
262
  return { exists: true, text, eol: text.includes('\r\n') ? '\r\n' : '\n' };
251
263
  }
252
264
 
265
+ const changedError = (file) => new UsageError(`${path.basename(file)} 이 검사 뒤 하드링크나 특수 파일로 바뀌어 쓰지 않음`);
266
+
253
267
  // 원본 바이트를 그대로 두고 끝에 줄을 덧붙인다. 마지막 줄에 줄바꿈이 없으면 먼저 넣는다.
254
268
  // 연 fd 를 fstat 으로 다시 본다: 검사(inspect)와 쓰기 사이에 하드링크나 특수 파일로 바뀌었으면 쓰지 않는다.
255
269
  function append(file, info, lines) {
@@ -258,7 +272,7 @@ function append(file, info, lines) {
258
272
  // O_NONBLOCK: 검사 뒤 FIFO 로 바뀐 자리에서 open 이 읽는 쪽을 기다리며 멈추지 않게 한다(Windows 에는 없어 0)
259
273
  const { O_WRONLY, O_APPEND, O_CREAT, O_EXCL } = fs.constants;
260
274
  const flags = O_WRONLY | (fs.constants.O_NONBLOCK || 0) | O_CREAT | (info.exists ? O_APPEND : O_EXCL);
261
- const changed = () => new UsageError(`${path.basename(file)} 이 검사 뒤 하드링크나 특수 파일로 바뀌어 쓰지 않음`);
275
+ const changed = () => changedError(file);
262
276
  let fd;
263
277
  try {
264
278
  fd = fs.openSync(file, flags, 0o666);
@@ -312,32 +326,103 @@ export function planMemory({ kind, memoryDir, project, runtime }) {
312
326
  return { kind, runtime, steps: [{ file: t.file, name, info: inspect(t.file, name, root, AGENTS_ALT), what: 'block' }], memory: { kind, path: t.file } };
313
327
  }
314
328
 
329
+ // 표식 줄 가운데 우리 줄만 지금 판으로 바꾼 글. 줄 끝(CRLF, LF)과 나머지 줄은 그대로. 돌려줌: { text, changed }
330
+ function refreshText(text) {
331
+ let changed = 0;
332
+ const out = text.split('\n').map((seg) => {
333
+ const cr = seg.endsWith('\r') ? '\r' : '';
334
+ const line = cr ? seg.slice(0, -1) : seg;
335
+ const want = currentLine(line);
336
+ if (want === null || want === line) return seg;
337
+ changed++;
338
+ return want + cr;
339
+ });
340
+ return { text: out.join('\n'), changed };
341
+ }
342
+
343
+ const RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
344
+ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
345
+
346
+ // 같은 파일을 원자적으로 다시 쓴다: 대상(링크면 실제 파일) 옆 임시 파일에 쓰고 권한을 맞춘 뒤 rename.
347
+ // 쓰기 전에 대상을 다시 보고 하드링크나 특수 파일로 바뀌었으면 쓰지 않는다. 실패하면 임시 파일을 지우고 원본 그대로.
348
+ function rewrite(file, body) {
349
+ const real = fs.realpathSync(file);
350
+ const st = fs.statSync(real);
351
+ if (!st.isFile() || st.nlink > 1) throw changedError(file);
352
+ const tmp = `${real}.${process.pid}.${Date.now().toString(36)}.tmp`;
353
+ try {
354
+ fs.writeFileSync(tmp, body, { flag: 'wx', mode: st.mode & 0o777 });
355
+ fs.chmodSync(tmp, st.mode & 0o7777);
356
+ for (let i = 0; ; i++) {
357
+ try {
358
+ fs.renameSync(tmp, real);
359
+ return;
360
+ } catch (e) {
361
+ if (i >= 4 || !RENAME_CODES.has(e.code)) throw e;
362
+ pause(50); // Windows 에서 다른 프로세스가 잠깐 쥔 파일
363
+ }
364
+ }
365
+ } catch (e) {
366
+ fs.rmSync(tmp, { force: true });
367
+ throw e instanceof UsageError ? e : new UsageError(`${path.basename(file)} 쓰기 실패(${e.code || e.message}). 원본 그대로`);
368
+ }
369
+ }
370
+
371
+ // 첫 원본 백업. 이미 있으면 덮지 않는다. 만들지 못하면 바꾸지 않고 거부.
372
+ function backup(file, buf) {
373
+ const bak = `${file}.progress-mon.bak`;
374
+ try {
375
+ fs.writeFileSync(bak, buf, { flag: 'wx' });
376
+ } catch (e) {
377
+ if (e.code !== 'EEXIST') throw new UsageError(`${path.basename(bak)} 백업을 만들지 못해 규칙 줄을 바꾸지 않음(${e.code || e.message})`);
378
+ }
379
+ return bak;
380
+ }
381
+
382
+ // 표식이 이미 있는 파일. 우리 줄이 지금 판과 다를 때만 그 줄을 바꾸고 백업을 남긴다. BOM 과 나머지 바이트는 그대로.
383
+ // 검사 뒤 UTF-8 이 아니게 바뀐 파일은 바꾸지 않고 경고 줄(U+FFFD 로 바뀐 바이트를 쓰지 않게).
384
+ function refreshStep(s) {
385
+ const buf = fs.readFileSync(s.file);
386
+ const bom = buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf;
387
+ let text;
388
+ try {
389
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bom ? buf.subarray(3) : buf);
390
+ } catch {
391
+ return `경고: ${s.name} 이 UTF-8 이 아니라 규칙 줄을 바꾸지 않음`;
392
+ }
393
+ const r = refreshText(text);
394
+ if (!r.changed) return `규칙 이미 있음: ${s.name}`;
395
+ const bak = backup(s.file, buf);
396
+ rewrite(s.file, Buffer.concat([bom ? buf.subarray(0, 3) : Buffer.alloc(0), Buffer.from(r.text, 'utf8')]));
397
+ return `규칙 줄 교체(백업 ${bak}): ${s.name}`;
398
+ }
399
+
400
+ // 표식이 없는 파일에 규칙을 더한다. 돌려줌: 출력 줄.
401
+ function addStep(s, runtime) {
402
+ const { info } = s;
403
+ const fresh = !info.exists;
404
+ const made = fresh ? '새로 만듦, ' : '';
405
+ if (s.what === 'rule-file') {
406
+ if (fresh) fs.writeFileSync(s.file, ruleFile(runtime), { flag: 'wx' });
407
+ else append(s.file, info, [ruleLine(runtime)]);
408
+ return `규칙 추가: ${s.name} (${fresh ? '새로 만듦' : '1줄'})`;
409
+ }
410
+ if (s.what === 'index') {
411
+ append(s.file, info, [INDEX_LINE]);
412
+ return `규칙 추가: ${s.name} (${made}색인 1줄)`;
413
+ }
414
+ if (s.what === 'line') {
415
+ append(s.file, info, [ruleLine(runtime)]);
416
+ return `규칙 추가: ${s.name} (${made}1줄)`;
417
+ }
418
+ append(s.file, info, [OMX_OPEN, ruleLine(runtime), OMX_CLOSE]);
419
+ return `규칙 추가: ${s.name} (${made}3줄)`;
420
+ }
421
+
315
422
  // 계획대로 쓴다. 돌려줌: 출력 줄 목록(규칙 줄, 경고 줄).
316
423
  export function applyMemory(plan, project, env = process.env) {
317
424
  if (plan.kind === 'none') return ['규칙 등록 안 함(--memory none)'];
318
- const out = [];
319
425
  if (plan.dir) fs.mkdirSync(plan.dir, { recursive: true });
320
- for (const s of plan.steps) {
321
- const { info } = s;
322
- if (info.text.includes(MARKER)) {
323
- out.push(`규칙 이미 있음: ${s.name}`);
324
- continue;
325
- }
326
- const fresh = !info.exists;
327
- if (s.what === 'rule-file') {
328
- if (fresh) fs.writeFileSync(s.file, ruleFile(plan.runtime), { flag: 'wx' });
329
- else append(s.file, info, [ruleLine(plan.runtime)]);
330
- out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦' : '1줄'})`);
331
- } else if (s.what === 'index') {
332
- append(s.file, info, [INDEX_LINE]);
333
- out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}색인 1줄)`);
334
- } else if (s.what === 'line') {
335
- append(s.file, info, [ruleLine(plan.runtime)]);
336
- out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}1줄)`);
337
- } else {
338
- append(s.file, info, [OMX_OPEN, ruleLine(plan.runtime), OMX_CLOSE]);
339
- out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}3줄)`);
340
- }
341
- }
426
+ const out = plan.steps.map((s) => (s.info.text.includes(MARKER) ? refreshStep(s) : addStep(s, plan.runtime)));
342
427
  return [...out, ...memoryWarnings(plan.memory, project, env)];
343
428
  }
@@ -1,4 +1,4 @@
1
- // pm-render.mjs: 4탭(진척도, 구현요건, workflow, 구성도)과 단일 HTML 스냅숏, workflow 배치와 구성도 자동 배치.
1
+ // pm-render.mjs: 5탭(진척도, 구현요건, workflow, 구성도, ADR)과 단일 HTML 스냅숏, workflow 배치와 구성도 자동 배치.
2
2
  // 옛 진척도 서버의 클래스 이름과 DOM 구조를 그대로 둔다. 저장소 고유 값은 config.json 에서 읽는다.
3
3
  // 시각은 now 인자로만 받는다. CLI 하위 명령 export 도 여기서 맡는다.
4
4
  import fs from 'node:fs';
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { esc, color, finite, textW, wrap, FAVICON, CSS, TABS, tabsHtml } from './pm-html.mjs';
7
7
  import { configOrFail, loadConfig, UsageError, DEFAULT_REQ_RULE, STORYLESS_REQ_RULE } from './pm-config.mjs';
8
8
  import { collect, parseTime, agentJudge } from './pm-sources.mjs';
9
+ import { adrBody } from './pm-adr-view.mjs';
9
10
 
10
11
  const two = (n) => String(n).padStart(2, '0');
11
12
  const hm = (ms) => (ms == null ? '-' : Math.floor(ms / 3600e3) + ':' + two(Math.floor((ms % 3600e3) / 60000)));
@@ -1540,13 +1541,14 @@ ${legend ? `<h2>읽는 법</h2><ul class="meta">${legend}</ul>` : ''}
1540
1541
  export const tabKey = (tab) => (tab === 'flow' ? 'workflow' : TABS.some((t) => t[0] === tab) ? tab : 'history');
1541
1542
 
1542
1543
  function tabBody(m, config, k, now) {
1544
+ if (k === 'adr') return adrBody(m, emptyNotice);
1543
1545
  if (k === 'arch') return archBody(m, config);
1544
1546
  if (k === 'workflow') return flowBody(m);
1545
1547
  if (k === 'req') return reqBody(m, config);
1546
1548
  return historyBody(m, config, now);
1547
1549
  }
1548
1550
 
1549
- // 모은 자료(m)로 탭 하나의 HTML. workflow 와 구성도는 움직임이 있고 자주 바뀌지 않아 자동 갱신하지 않는다.
1551
+ // 모은 자료(m)로 탭 하나의 HTML. workflow, 구성도, ADR 은 자주 바뀌지 않아 자동 갱신하지 않는다.
1550
1552
  export function pageFrom(m, config, tab, now) {
1551
1553
  const k = tabKey(tab);
1552
1554
  const refresh = k === 'history' || k === 'req' ? `<meta http-equiv="refresh" content="${config.refreshSec};url=/?tab=${k}">` : '';
@@ -1560,8 +1562,8 @@ ${tabsHtml(k)}
1560
1562
  ${tabBody(m, config, k, now)}</body></html>`;
1561
1563
  }
1562
1564
 
1563
- // 단일 HTML 스냅숏: 네 탭을 한 파일에. 서버, 자동 갱신, 외부 자원 없이 file:// 로 열린다.
1564
- // 탭 전환은 주소의 #history, #req, #workflow, #arch 와 짧은 인라인 스크립트. 스크립트가 꺼져 있으면 네 탭을 모두 보인다.
1565
+ // 단일 HTML 스냅숏: 다섯 탭을 한 파일에. 서버, 자동 갱신, 외부 자원 없이 file:// 로 열린다.
1566
+ // 탭 전환은 주소의 #history, #req, #workflow, #arch, #adr 와 짧은 인라인 스크립트. 스크립트가 꺼져 있으면 다섯 탭을 모두 보인다.
1565
1567
  export function snapshotFrom(m, config, now) {
1566
1568
  const tabs = TABS.map(([k, name]) => `<a href="#${k}" data-tab="${k}">${esc(name)}</a>`).join('');
1567
1569
  const secs = TABS.map(([k, name]) => `<section id="tab-${k}" class="tab"><h2 class="tt">${esc(name)}</h2>\n\n${tabBody(m, config, k, now)}</section>`).join('\n');
@@ -780,7 +780,7 @@ function tabData(dataDir) {
780
780
  return e.code === 'ENOENT' ? '없음' : '깨짐';
781
781
  }
782
782
  };
783
- return `진척도 ${has('items.json')}, 구현요건 ${has('req-spec.json')}, workflow ${has('flow.json')}, 구성도 ${has('arch.json')}`;
783
+ return `진척도 ${has('items.json')}, 구현요건 ${has('req-spec.json')}, workflow ${has('flow.json')}, 구성도 ${has('arch.json')}, ADR ${has('adr.json')}`;
784
784
  }
785
785
 
786
786
  // 서버를 띄운 뒤 config.json 의 port, portStrict, hosts 가 바뀌었으면 경고 한 줄(그 값은 다시 띄워야 적용).
@@ -815,6 +815,7 @@ export async function status(ctx, io) {
815
815
  const run = await runningServer(ctx.dataDir);
816
816
  if (!run) {
817
817
  io.out(`꺼짐(자료 폴더: ${label})`);
818
+ io.out(`탭 자료: ${tabData(ctx.dataDir)}`);
818
819
  statusWarnings(ctx, io);
819
820
  return 3;
820
821
  }
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import vm from 'node:vm';
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { eventsPath, stagesFor, isDir, noBom, readRegular } from './pm-config.mjs';
10
+ import { adrSha } from './pm-adr.mjs';
10
11
 
11
12
  // ---------- 시각 ----------
12
13
 
@@ -67,6 +68,13 @@ const parseLines = (t) =>
67
68
 
68
69
  // JSON 파일. 돌려줌: { value } | { missing: true } | { error }
69
70
  export const readJsonFile = (file) => cached(file, parseJson);
71
+
72
+ // adr.json: 값과 글의 sha256(check 대조 기록 adr-check.json 과 견줌). 돌려줌: { value, sha } | { missing: true } | { error }
73
+ const parseAdr = (t) => ({ json: JSON.parse(t), sha: adrSha(t) });
74
+ export function readAdrFile(file) {
75
+ const r = cached(file, parseAdr);
76
+ return r.value ? { value: r.value.json, sha: r.value.sha } : r;
77
+ }
70
78
  // JSONL 파일의 객체 줄 목록. 없거나 못 읽으면 빈 목록.
71
79
  export const readLines = (file) => cached(file, parseLines).value || [];
72
80
 
@@ -497,5 +505,7 @@ export function collect(ctx, config, now, inject = {}) {
497
505
  reqs: tab('reqs.json'),
498
506
  flow: tab('flow.json'),
499
507
  arch: tab('arch.json'),
508
+ adr: readAdrFile(path.join(ctx.dataDir, 'adr.json')),
509
+ adrCheck: tab('adr-check.json'),
500
510
  };
501
511
  }
@@ -27,7 +27,8 @@ export const HELP = `progress-mon: 프로젝트 진척도 웹 화면
27
27
  note --key <k> [--note <t>] [--issue <t>]
28
28
  stage --id <id> --status started|done|skipped|failed|stopped [--label <t>]
29
29
  story --id <id> (--status todo|doing|done | --commit <hash>) [--name <t>]
30
- check 자료 파일 검사. FAIL, WARN 한 줄씩
30
+ check [--adr-cleanup]
31
+ 자료 파일 검사. FAIL, WARN 한 줄씩. --adr-cleanup 은 먼저 adr.json 정리(참조 FAIL 은 gap, 스키마 FAIL 이 남으면 adr.json.rejected-<시각> 으로 옮김)
31
32
 
32
33
  공통 인자
33
34
  --runtime claude|codex 자료 폴더 기본값(.omc/progress-mon/, .omx/progress-mon/)
@@ -39,7 +40,7 @@ export const HELP = `progress-mon: 프로젝트 진척도 웹 화면
39
40
 
40
41
  // 값을 받는 인자와 값 없는 인자.
41
42
  const VALUE_FLAGS = new Set(['runtime', 'project', 'data', 'port', 'host', 'memory', 'memory-dir', 'title', 'now', 'key', 'status', 'text', 'note', 'issue', 'id', 'label', 'commit', 'name', 'at']);
42
- const BOOL_FLAGS = new Set(['port-strict', 'help']);
43
+ const BOOL_FLAGS = new Set(['port-strict', 'help', 'adr-cleanup']);
43
44
 
44
45
  // 서버 밖 명령을 맡는 모듈. 쓰는 명령에서만 적재한다.
45
46
  const MODS = {
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: tune-tmux
3
+ description: "tmux, psmux 설정 파일에 표지 블록을 넣고 빼서, tmux 안에서 Claude Code 나 Codex 를 쓸 때 마우스 드래그 복사와 PageUp/PageDown 스크롤이 동작하게 맞춤. 전체 화면 앱의 키와 마우스는 빼앗지 않음. 첫 원본 백업, `off` 로 블록만 뺌, `--reload` 로 떠 있는 서버에 반영. 'tune-tmux'/'tmux 설정'/'psmux 설정'/'tmux 마우스 복사'/'tmux 드래그 복사'/'tmux PageUp 스크롤' 시 사용."
4
+ argument-hint: "[status|on|off] [--target auto|tmux|psmux] [--conf <경로>] [--follow-symlink] [--force-drag] [--reload] [--dry-run] [--json]"
5
+ ---
6
+
7
+ # tune-tmux: tmux, psmux 의 드래그 복사와 PageUp/PageDown 스크롤
8
+
9
+ tmux 설정 파일에 `# banker:tune-tmux:start` .. `# banker:tune-tmux:end` 블록 하나를 넣거나 뺀다.
10
+ 블록 밖 줄은 건드리지 않는다.
11
+ 답변은 한글.
12
+
13
+ | 하위 명령 | 동작 |
14
+ |---|---|
15
+ | `status`(기본) | 대상 도구와 판, 고른 설정 파일, 블록 상태, 넣을 줄과 빼는 줄의 이유, 상충 경고, 앱별 동작 표 |
16
+ | `on` | 블록이 없으면 끝에 덧붙이고, 있으면 블록 내용만 바꿈 |
17
+ | `off` | 블록만 뺌. 나머지 바이트는 그대로 |
18
+
19
+ ## 언제 쓰나
20
+
21
+ - tmux 안의 셸에서 PageUp 으로 기록을 보고 싶을 때
22
+ - 마우스로 끌어 고른 글을 OS 클립보드로 바로 복사하고 싶을 때
23
+ - tmux 안에서 Claude Code, Codex 를 쓰면서 위 두 가지가 앱 동작과 부딪히지 않게 하고 싶을 때
24
+
25
+ ## 쓰지 않을 때
26
+
27
+ | 할 일 | 쓸 스킬 |
28
+ |---|---|
29
+ | tmux, psmux 설치(Rocky8 소스 빌드, Windows psmux) | banker `setup-tmux` |
30
+ | Claude Code, Codex 설정 바꾸기(alternate screen 끄기 등) | 이 스킬은 하지 않음. 아래 앱별 표의 안내를 사용자에게 전함 |
31
+
32
+ - 역할 나눔: `setup-tmux` 는 도구를 설치하고, `tune-tmux` 는 설치된 도구의 설정 파일만 고친다
33
+ - tmux, psmux 가 없으면 `on`, `off` 는 종료 3 과 "setup-tmux 로 먼저 설치" 를 낸다
34
+
35
+ ## 자리 표시
36
+
37
+ | 자리 | 뜻 |
38
+ |---|---|
39
+ | `<S>` | 이 스킬 디렉터리 절대경로(아래 런타임 표의 "스킬 디렉터리") |
40
+
41
+ - 셸 변수는 도구 호출 사이에 남지 않는다. 명령마다 실제 경로를 적는다
42
+
43
+ ## 런타임 표
44
+
45
+ | 항목 | Claude Code | Codex |
46
+ |---|---|---|
47
+ | 스킬 디렉터리 | 스킬 맨 위 "Base directory for this skill:" 경로 | Codex 스킬 목록이 보이는 이 SKILL.md 경로(file)의 폴더. npm 설치는 `~/.codex/skills/banker-tune-tmux/`(프로젝트 범위 설치면 `./.codex/skills/banker-tune-tmux/`), 플러그인 설치(`codex plugin add`)는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/tune-tmux/`. 절대경로로 씀 |
48
+ | `status` | 바로 실행 | 바로 실행(읽기와 사설 tmux 서버 하나) |
49
+ | `on`, `off` | 바로 실행 | 설정 파일(`~/.tmux.conf` 등)이 작업 폴더 밖이라 명령에 `sandbox_permissions: "require_escalated"` 로 승인 요청. `--add-dir` 로 설정 폴더를 넓히지 않음 |
50
+ | `--reload` | 바로 실행 | tmux 서버 소켓(`/tmp/tmux-<uid>/`)이 작업 폴더 밖이라 같은 승인 요청 |
51
+
52
+ ## 실행
53
+
54
+ 1. 인자 확인
55
+ - 하위 명령은 `status`, `on`, `off`. 그 밖은 실행하지 않고 사용법 안내
56
+ - 사용자 입력을 셸 명령에 그대로 붙이지 않음
57
+ 2. 상태 먼저 보기
58
+
59
+ ```bash
60
+ node "<S>/scripts/tune-tmux.mjs" status
61
+ ```
62
+
63
+ 3. 넣기와 빼기
64
+
65
+ ```bash
66
+ node "<S>/scripts/tune-tmux.mjs" on [--reload] [--force-drag]
67
+ node "<S>/scripts/tune-tmux.mjs" off [--reload]
68
+ ```
69
+
70
+ - 계획만 보려면 `--dry-run`. 기계가 읽을 출력은 `--json`(JSON 한 줄)
71
+ - `--force-drag` 는 사용자가 맞바꿈(아래 절)을 듣고 고를 때만 붙임
72
+ 4. 결과 처리
73
+ - `결과:` 줄, `넣지 않은 줄:` 의 이유, `경고:` 줄을 그대로 전함
74
+ - psmux 의 `넣은 줄 0: psmux 기본값 유지. 미확인 n, 안 됨 n` 줄과 그 아래 세 목록(되는 것, 확인 못 한 것, 안 되는 것)을 그대로 전함. 종료 0 을 "원하는 동작이 다 됨" 으로 보고하지 않음
75
+ - `status` 의 앱별 동작 표를 사용자에게 보이고, 쓰는 앱(Claude Code, Codex)의 행을 짚어 줌
76
+ 5. 종료 코드
77
+
78
+ | 코드 | 뜻 | 처리 |
79
+ |---|---|---|
80
+ | 0 | 완료. 바뀐 것 없음 포함. psmux 에 넣을 줄이 없는 경우 포함(파일 처리 성공일 뿐, 미확인과 안 되는 동작이 남음) | 결과 보고. psmux 는 미확인, 안 됨 수와 목록도 |
81
+ | 1 | 거부(링크, 하드 링크, 소유자, 표지 손상, UTF-8 아님, 특수 파일), 쓰기 실패, `--reload` 반영 실패. 거부는 아무것도 바꾸지 않음 | 메시지 그대로 전하고 아래 "문제 해결" |
82
+ | 2 | 사용 오류 | 인자를 고쳐 다시 실행 |
83
+ | 3 | `on`, `off` 인데 tmux, psmux 둘 다 없음 | banker `setup-tmux` 안내. `status` 는 이때 종료 0 과 "미설치" |
84
+
85
+ - 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않음
86
+
87
+ ## 인자
88
+
89
+ | 인자 | 뜻 | 기본 |
90
+ |---|---|---|
91
+ | `--target auto\|tmux\|psmux` | `auto` 는 Windows 에서 psmux(없고 tmux 가 있으면 tmux), 그 밖은 tmux. `tmux -V` 둘째 줄이 psmux 면 psmux 로 처리 | `auto` |
92
+ | `--conf <경로>` | 블록을 넣을 파일. tmux 가 스스로 읽지 않는 파일이면 경고 | 아래 고르기 규칙 |
93
+ | `--follow-symlink` | 설정 파일이 링크(dotfiles)일 때 가리키는 파일에 씀. 링크는 링크로 남음 | 꺼짐(링크면 종료 1) |
94
+ | `--force-drag` | 앱이 마우스를 잡아도 드래그를 tmux copy-mode 선택으로 바꿈 | 꺼짐 |
95
+ | `--reload` | 떠 있는 서버에 반영 | 꺼짐 |
96
+ | `--socket <이름>` | 기본 서버 대신 `tmux -L <이름>` 서버에 반영(시험과 사설 서버용) | 없음 |
97
+ | `--dry-run`, `--json` | 계획만, JSON 한 줄 | 꺼짐 |
98
+
99
+ ## 블록 내용(tmux)
100
+
101
+ | 줄 | 효과 | 조건 |
102
+ |---|---|---|
103
+ | `set -g mouse on` | 마우스 사용 | 늘 |
104
+ | `set -s set-clipboard on` | OSC 52 로 터미널 클립보드에도 씀 | 늘 |
105
+ | `set -s copy-command '<클립보드 명령>'` | copy-mode 의 기본 `MouseDragEnd1Pane`(복사 후 끝냄)이 OS 클립보드로 보냄 | tmux 3.2 이상, 클립보드 도구 있음 |
106
+ | `bind -T copy-mode`(와 `copy-mode-vi`) `MouseDragEnd1Pane send -X copy-pipe-and-cancel '<클립보드 명령>'` | 같은 효과 | tmux 2.4 이상 3.2 미만, 클립보드 도구 있음 |
107
+ | `bind -n PPage if -F "#{\|\|:#{alternate_on},#{pane_in_mode}}" "send-keys PPage" "copy-mode -eu"` | 일반 화면에서 PageUp 은 copy-mode 진입과 한 쪽 위로. alternate screen 이나 모드 중이면 키를 그대로 넘김 | 사설 서버에서 `#{\|\|:0,1}#{\|\|:0,0}` 가 `10` 일 때 |
108
+ | `bind -n MouseDrag1Pane if -F "#{pane_in_mode}" "send-keys -M" "copy-mode -M"` | 앱이 마우스를 잡아도 tmux 선택 | `--force-drag` 일 때만 |
109
+
110
+ - 루트 `MouseDrag1Pane` 기본(앱이 마우스를 잡으면 앱으로, 아니면 copy-mode 선택)은 바꾸지 않는다
111
+ - PageDown 은 바인딩하지 않는다. copy-mode 안에서는 copy-mode 표가 받고(바닥에 닿으면 `-e` 로 빠짐), 밖에서는 앱이 받는다
112
+ - 클립보드 명령 고르기: macOS `pbcopy`, Wayland(`WAYLAND_DISPLAY`) `wl-copy`, X11(`DISPLAY`) `xclip -selection clipboard -in` 다음 `xsel -ib`, WSL `clip.exe`. 하나도 없으면 클립보드 줄을 넣지 않고 OSC 52 만 쓴다. 이때 터미널이 OSC 52 를 허용해야 한다
113
+ - 조건식 탐지는 `tmux -L banker-tune-<난수> -f /dev/null` 사설 서버를 띄워 묻고 바로 끈다. 사용자 서버는 건드리지 않는다
114
+
115
+ ## 설정 파일 고르기
116
+
117
+ | 대상 | 순서 | 실측 |
118
+ |---|---|---|
119
+ | tmux 3.1 이상 | `--conf` > `~/.tmux.conf`(있으면) > `$XDG_CONFIG_HOME/tmux/tmux.conf` > `~/.config/tmux/tmux.conf`. 셋 다 없으면 `~/.tmux.conf` 를 새로 만듦 | tmux 3.6a 는 있는 파일을 모두 읽음(사용자 옵션 `@banker_a`, `@banker_b`, `@banker_c` 를 각 파일에 두고 `show -gv` 로 확인) |
120
+ | tmux 3.1 미만 | `--conf` > `~/.tmux.conf` | tmux 2.7 은 `~/.tmux.conf` 만 읽음(같은 방법) |
121
+ | psmux | `--conf` > `PSMUX_CONFIG_FILE` > `~/.psmux.conf`, `~/.psmuxrc`, `~/.tmux.conf`, `~/.config/psmux/psmux.conf` 중 먼저 있는 것. 없으면 `~/.psmux.conf` | psmux 3.3.8 은 처음 찾은 파일 하나만 읽음(`-f` 가 가장 앞) |
122
+
123
+ - 상충 점검: tmux 가 함께 읽는 다른 파일에 `mouse off`, `set-clipboard off`, 루트 `PPage` 바인딩이 있으면 `status`, `on` 이 파일과 줄 번호로 경고한다. tmux 2.7 처럼 그 파일을 읽지 않는 판에서는 경고하지 않는다
124
+
125
+ ## 앱별 동작(tmux, 블록을 넣은 뒤)
126
+
127
+ | 앱 상태 | PageUp/PageDown | 드래그 | 출처 |
128
+ |---|---|---|---|
129
+ | 일반 화면(셸, Codex `--no-alt-screen` 이나 `/tui` 의 Scrollback, Claude Code 옛 렌더러) | PageUp 은 tmux copy-mode 진입과 한 쪽 위로. copy-mode 안 PageDown 은 아래로, 바닥에 닿으면 copy-mode 를 빠짐 | copy-mode 선택. 놓으면 OS 클립보드(도구가 없으면 OSC 52) | 이 스킬의 통합 시험(tmux 3.6a, 2.7) |
130
+ | Claude Code 전체 화면(alternate screen, 앱이 마우스를 잡음) | 둘 다 앱이 받음(반 화면 스크롤). "Keyboard scrolling with PgUp and PgDn works either way". 휠은 tmux `mouse on` 이 필요 | 앱 안 선택. 놓으면 클립보드와 tmux paste buffer. 앱 밖 선택은 Shift 드래그(Terminal.app 은 Fn, iTerm2 는 Option). 마우스를 끄려면 `CLAUDE_CODE_DISABLE_MOUSE=1` | https://code.claude.com/docs/en/fullscreen |
131
+ | Codex Fullscreen(기본, alternate screen) | 둘 다 앱이 받음(휠, PgUp/PgDn, Ctrl+Home/End 로 Codex 기록 스크롤) | Codex 가 선택하고 바로 복사(`[tui] copy_on_select` 기본 `auto`) | https://github.com/openai/codex/discussions/49129 |
132
+
133
+ - Codex 한계: Fullscreen 에서는 기록이 Codex 안에 있고 tmux 는 현재 화면만 본다. tmux copy-mode 로 Codex 기록을 스크롤하는 것은 tmux 설정으로 고칠 수 없다
134
+ - 공식 우회: `/tui` 에서 Scrollback 을 고른 뒤 재시작, `codex --no-alt-screen`, 설정 `tui.alternate_screen = "never"`, 복사용 `tui.raw_output_mode`(`/raw`, `alt-r`)
135
+ - `--no-alt-screen` 이어도 전체 다시 그리기 때문에 스크롤백이 덜 쌓인다는 보고가 있다(Zellij, tmux 미확인)
136
+ - 이 스킬은 Codex 설정을 바꾸지 않는다. 우회는 사용자에게 안내만 한다
137
+ - Claude Code 의 옛 렌더러(`CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1` 이나 `/tui default`)에서는 일반 화면 행대로 tmux copy-mode 를 쓴다
138
+ - tmux 의 검색과 copy-mode 는 Claude Code 전체 화면의 대화를 못 본다. Claude Code 의 `Ctrl+o` 뒤 `[` 가 대화를 네이티브 스크롤백에 쓴다
139
+
140
+ ## psmux(Windows)
141
+
142
+ psmux 3.3.8 실측(Windows 10 22H2 박스, 2026-11-01) 결과 블록에 넣을 줄이 없다.
143
+ psmux 기본값으로 모든 요구가 채워지는 것은 아니다. 되는 것, 확인 못 한 것, 안 되는 것이 나뉜다.
144
+ `on` 은 종료 0 과 `넣은 줄 0: psmux 기본값 유지. 미확인 2, 안 됨 3` 줄, 그 아래 세 목록을 낸다(`--json` 은 `note` 와 `psmux`).
145
+ 그 설정 파일에 옛 tune-tmux 블록이 있으면 `off` 처럼 블록만 뺀다(`넣을 줄이 0개라 옛 tune-tmux 블록을 뺌` 줄). 없으면 파일을 바꾸지 않는다.
146
+ 판단 근거는 `scripts/psmux-caps.json` 의 표다. `verified: true` 이고 psmux 기본값이 아닌 줄만 넣는다. 세 목록은 같은 파일의 `behavior` 다.
147
+
148
+ | 구분 | 내용 |
149
+ |---|---|
150
+ | 기본값으로 되는 것 | 마우스(`mouse on`) |
151
+ | 기본값으로 되는 것 | `set-clipboard on` |
152
+ | 기본값으로 되는 것 | 휠로 copy-mode 진입(`scroll-enter-copy-mode on`) |
153
+ | 확인 못 한 것 | 드래그한 글이 Windows 클립보드로 가는지 |
154
+ | 확인 못 한 것 | 전체 화면 앱 안의 PageUp/PageDown 을 앱이 받는지 |
155
+ | 안 되는 것 | 앱 밖 PageUp 은 prefix 뒤에만 copy-mode 진입 |
156
+ | 안 되는 것 | PageDown 이 바닥에서 copy-mode 를 빠지지 않음 |
157
+ | 안 되는 것 | `#{alternate_on}` 이 늘 0 이라 전체 화면 앱을 구분하지 못함 |
158
+
159
+ | 사용자 요구 | psmux 기본값으로 되는 것 | 안 되거나 미확인 |
160
+ |---|---|---|
161
+ | 드래그 복사 | `mouse on`, `mouse-selection on`, `set-clipboard on` 이 기본이라 내장 선택이 동작 | 고른 글이 OS 클립보드로 가는지는 실제 마우스와 데스크톱이 필요해 미확인 |
162
+ | PageUp | prefix 뒤 PageUp 이 copy-mode 진입과 한 쪽 위로(기본 바인딩 `copy-mode -u`) | 루트 PageUp 바인딩은 넣지 않음. Windows 10 에서 `#{alternate_on}` 이 늘 0 이라 넣으면 전체 화면 앱이 PageUp 을 못 받음(실측: 앱 로그가 빔) |
163
+ | PageDown | copy-mode 안에서 아래로 스크롤 | 바닥에서 copy-mode 를 빠지지 않음(`-e` 미지원). Escape 로 빠짐 |
164
+ | 전체 화면 앱(Claude Code, Codex) | tune-tmux 가 키를 가로채지 않음 | 블록 없이 앱이 PageUp 을 받는지는 미실측. 휠은 psmux 이슈 #597, #598 |
165
+
166
+ - psmux 함정(실측)
167
+ - 붙인 짧은 플래그를 풀지 못한다. `copy-mode -eu` 는 `-u` 가 사라지므로 `copy-mode -e -u` 처럼 따로 쓴다
168
+ - `copy-pipe-and-cancel` 바인딩과 루트 `MouseDrag1Pane` 바인딩은 경고 없이 버려진다. 그래서 `--force-drag` 줄도 넣지 않는다
169
+ - CLI 의 `bind` 는 따옴표 묶음을 잃는다. 바인딩은 설정 파일로만 넣는다
170
+ - 줄 끝 주석은 3.3.8 에서 값으로 읽히지 않는다
171
+
172
+ ## `--force-drag` 맞바꿈
173
+
174
+ - 얻는 것: Claude Code 전체 화면이나 Codex 처럼 마우스를 잡는 앱 위에서도 드래그가 늘 tmux 선택이 되어, 놓으면 tmux 의 복사 경로로 간다
175
+ - 잃는 것: 앱의 마우스 클릭, 드래그 처리를 tmux 가 빼앗는다. Claude Code 전체 화면의 앱 안 선택과 클릭이 안 된다
176
+ - 기본은 꺼짐이다. 사용자가 이 맞바꿈을 듣고 고를 때만 붙인다. 되돌리려면 `--force-drag` 없이 `on --reload`
177
+
178
+ ## `--reload`
179
+
180
+ - 기본 서버(`tmux ls` 성공)가 떠 있을 때만 반영한다. 없으면 "떠 있는 서버 없음. 다음 시작 때 적용" 으로 종료 0
181
+ - 예전 블록에만 있던 줄의 효과를 먼저 되돌린다
182
+ - 옵션은 `set -gu`, `set -su`
183
+ - 키는 설정 없이 띄운 사설 서버의 기본 바인딩을 그대로 다시 넣고, 기본이 없으면 `unbind`
184
+ - 그 뒤 설정 파일 전체를 `source-file` 해 블록 밖 사용자 줄을 되살린다. 사용자 파일의 다른 명령도 다시 실행된다
185
+ - `off --reload` 뒤 루트 `PPage` 는 비고, 사용자가 둔 `set -g mouse on` 같은 줄은 다시 살아 있다(통합 시험)
186
+
187
+ ## 오프라인
188
+
189
+ - 네트워크를 쓰지 않는다
190
+
191
+ ## 함정
192
+
193
+ - tmux 3.1 이상은 `~/.tmux.conf` 와 `~/.config/tmux/tmux.conf` 를 모두 읽는다. 뒤 파일의 `mouse off` 가 블록을 덮을 수 있어 경고한다
194
+ - 블록을 손으로 고치면 다음 `on` 이 다시 쓴다. 손으로 쓸 줄은 블록 밖에 둔다
195
+ - 표지 줄 하나만 지우거나 문장 안에 표지를 옮기면 거부(종료 1)한다. 짝을 임의로 맞추지 않는다
196
+ - `off` 로 블록만 있던 파일(이 스킬이 만든 파일)은 빈 파일로 남는다. 지우지 않는다
197
+ - `source-file` 은 지운 바인딩을 되돌리지 않는다. 그래서 `--reload` 가 되돌리기 명령을 먼저 보낸다
198
+ - tmux 3.6a 의 `send-keys -K` 는 바인딩 없는 PageDown 을 `k` 로 보냈다(실측). 키 시험은 바깥 서버 창에 키를 보내 실제 터미널 입력 경로로 한다
199
+
200
+ ## 문제 해결
201
+
202
+ - 아래 표에서 파일을 보인다는 것은 `ls -l` 로 그 이름의 종류와 링크 대상 경로를 보이는 것이다. 링크가 아닌 일반 파일임을 본 뒤에만 내용을 연다
203
+
204
+ | 메시지 | 조치 |
205
+ |---|---|
206
+ | 심볼릭 링크라 그대로 둠 | 설정 파일이 링크(dotfiles). 링크와 대상 경로를 보이고, 대상에 쓸지 물은 뒤 `--follow-symlink` 로 다시 실행 |
207
+ | 끊어진 링크라 그대로 둠 | 링크가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 어떻게 할지 물음 |
208
+ | 하드 링크가 n개라 그대로 둠 | 같은 파일에 이름이 여럿. 쓰면 링크가 갈라짐. 어느 이름을 쓸지 물음 |
209
+ | 다른 사용자 소유라 그대로 둠 | 파일 소유자가 실행 사용자와 다름. 소유자 확인을 안내 |
210
+ | 표지 손상이라 그대로 둠 | 표지 줄이 한쪽만 있거나 문장 안에 있거나 블록이 둘 이상. 남은 표지 줄을 보이고 지울지 물은 뒤 다시 실행 |
211
+ | UTF-8 이 아니라 그대로 둠 | 인코딩을 보이고 UTF-8 로 바꿀지 물음 |
212
+ | 이 tmux 는 #{\|\|:} 조건식을 읽지 못함 | PageUp 줄을 넣지 않은 이유. 새 tmux 는 banker `setup-tmux` |
213
+ | 외부 클립보드 도구 없음 | 터미널 설정에서 OSC 52 클립보드 쓰기를 허용하거나 `xclip`, `wl-copy` 등을 설치한 뒤 `on` 다시 실행 |
214
+ | 떠 있는 서버 반영 실패 | 파일은 바뀌었음. 메시지의 `source-file` 오류 줄을 보이고 사용자 설정의 해당 줄을 확인 |
215
+
216
+ ## 수동 확인 항목(자동 시험 아님)
217
+
218
+ - 실제 Claude Code 전체 화면을 tmux 안에서 띄워 드래그와 PageUp/PageDown 이 앱별 표대로인지
219
+ - 실제 Codex Fullscreen 을 tmux 안에서 띄워 같은 확인
220
+ - psmux 에서 드래그한 글이 Windows 클립보드로 가는지, 블록 없이 전체 화면 앱이 PageUp 을 받는지
221
+
222
+ ## 출처(조사한 것)
223
+
224
+ | 주제 | 확인한 사실 | 출처 |
225
+ |---|---|---|
226
+ | Claude Code 전체 화면 | PgUp/PgDn 과 휠을 앱이 처리, 앱 안 선택은 클립보드와 tmux paste buffer, 앱 밖은 Shift 드래그, 끄는 환경 변수 | https://code.claude.com/docs/en/fullscreen |
227
+ | Codex Fullscreen | 기본 Fullscreen, 기록은 Codex 안, 선택 즉시 복사, `/tui` 로 Scrollback | https://github.com/openai/codex/discussions/49129, https://github.com/openai/codex/issues/48057 |
228
+ | Codex tmux 스크롤 | tmux `Ctrl+B [` 가 스크롤 안 됨, 공식 답은 Scrollback 이나 `--no-alt-screen` | https://community.openai.com/t/umm-looks-like-recent-versions-may-prevent-scrolling-up-for-more-information-in-tmux-session-via-ctrl-b/1402100 |
229
+ | Codex 설정 | `tui.alternate_screen = auto \| always \| never`, `tui.raw_output_mode` | https://learn.chatgpt.com/docs/config-file/config-reference |
230
+ | Codex `--no-alt-screen` 한계 | Zellij 에서 스크롤백이 거의 안 쌓임 | https://github.com/openai/codex/issues/10331 |
231
+ | tmux 관용구와 판 | PageUp `if -F` 관용구, `copy-mode -e` 2.1, `copy-command` 3.2 | https://raw.githubusercontent.com/tmux/tmux/master/CHANGES |
232
+ | tmux 기본 바인딩 | 3.6a 루트 `MouseDrag1Pane` 은 `#{\|\|:#{pane_in_mode},#{mouse_any_flag}}` 면 앱으로, copy-mode `MouseDragEnd1Pane` 은 `copy-pipe-and-cancel`. 2.7 은 `copy-selection-and-cancel` | `tmux -f /dev/null` 사설 서버의 `list-keys`(실측) |
233
+ | tmux 2.7 조건식 | `#{\|\|:0,1}` 은 `1`, `#{\|\|:0,0}` 은 `0`. 2.7 도 조건식을 읽음 | Rocky 8 의 `/usr/bin/tmux` 2.7 사설 서버(실측) |
234
+ | psmux | 설정 파일 순서, 기본값, 플래그와 바인딩 동작, `alternate_on` | psmux 3.3.8 Windows 10 박스 실측, https://github.com/psmux/psmux |
235
+ | psmux 이슈 | SGR 마우스가 앱으로 안 감(#597), Codex 휠(#598) | https://github.com/psmux/psmux/issues/597, https://github.com/psmux/psmux/issues/598 |
@@ -0,0 +1,38 @@
1
+ /**
2
+ * app-notes: how PageUp/PageDown and mouse drag behave per app state with the tune-tmux block, for `status`.
3
+ * Same content as the "앱별 동작" tables in SKILL.md (sources there). Text only; nothing is read or written.
4
+ */
5
+
6
+ const TMUX = [
7
+ {
8
+ state: "일반 화면(셸, Codex `--no-alt-screen` 이나 /tui Scrollback, Claude Code 옛 렌더러)",
9
+ keys: "PageUp 은 tmux copy-mode 진입과 한 쪽 위로. copy-mode 안 PageDown 은 아래로, 바닥에 닿으면 copy-mode 를 빠짐",
10
+ drag: "tmux copy-mode 선택. 놓으면 OS 클립보드(도구가 없으면 OSC 52 로 터미널 클립보드)",
11
+ },
12
+ {
13
+ state: "Claude Code 전체 화면(alternate screen, 앱이 마우스를 잡음)",
14
+ keys: "둘 다 앱이 받음(반 화면 스크롤). tune-tmux 는 가로채지 않음",
15
+ drag: "앱 안 선택. 놓으면 클립보드와 tmux paste buffer. 앱 밖 선택은 Shift 드래그. 늘 tmux 선택을 원하면 `--force-drag`(앱 클릭을 빼앗음)",
16
+ },
17
+ {
18
+ state: "Codex Fullscreen(alternate screen)",
19
+ keys: "둘 다 앱이 받음(Codex 기록 스크롤). tmux copy-mode 로는 Codex 기록을 못 봄(tmux 설정으로 못 고침). tmux 스크롤이 필요하면 /tui 의 Scrollback, `codex --no-alt-screen`, `tui.alternate_screen = \"never\"`",
20
+ drag: "Codex 가 선택하고 바로 복사(`tui.copy_on_select`). 늘 tmux 선택을 원하면 `--force-drag`",
21
+ },
22
+ ];
23
+
24
+ const PSMUX = [
25
+ {
26
+ state: "일반 화면(셸)",
27
+ keys: "psmux 기본에는 루트 PageUp 바인딩이 없음. prefix 뒤 PageUp 이 copy-mode 진입과 한 쪽 위로. copy-mode 안 PageUp/PageDown 은 스크롤하지만 바닥에서 빠지지 않음(-e 미지원, Escape 로 빠짐)",
28
+ drag: "psmux 기본(mouse on, mouse-selection on, set-clipboard on)의 내장 선택. OS 클립보드로 가는지는 미확인",
29
+ },
30
+ {
31
+ state: "전체 화면 앱(Claude Code, Codex)",
32
+ keys: "tune-tmux 는 블록을 넣지 않으므로 키를 가로채지 않음. 앱이 받는지는 미실측. Windows 10 에서 #{alternate_on} 이 늘 0 이라 tmux 식 PageUp 바인딩을 넣으면 앱이 PageUp 을 못 받음",
33
+ drag: "앱의 선택 동작을 따름. psmux 3.3.8 은 SGR 마우스가 앱으로 안 가는 이슈(#597)가 열려 있음",
34
+ },
35
+ ];
36
+
37
+ /** The rows for target "tmux" or "psmux": { state, keys, drag }. */
38
+ export const appRows = (target) => (target === "psmux" ? PSMUX : TMUX);