@kaydash9999/banker-plugins 0.20.5 → 0.21.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 (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +24 -0
  4. package/PRIVACY.md +21 -1
  5. package/README.md +22 -14
  6. package/bin/banker.js +144 -11
  7. package/bin/lib/codex-hooks.mjs +249 -0
  8. package/bin/lib/telemetry-config.mjs +2 -0
  9. package/codex/manifest.json +13 -10
  10. package/codex/skills/graceful-pause/SKILL.md +30 -0
  11. package/codex/skills/progress/SKILL.md +42 -0
  12. package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
  13. package/codex/skills/progress/scripts/progress.mjs +295 -0
  14. package/codex/transform-matrix.md +18 -13
  15. package/hooks/obsidize-hook.mjs +116 -4
  16. package/hooks/progress-plan.mjs +1 -1
  17. package/hooks/telemetry-count-codex.mjs +222 -0
  18. package/hooks/update-notify.mjs +31 -11
  19. package/package.json +1 -1
  20. package/skills/append_wiki/SKILL.md +34 -8
  21. package/skills/compact-wiki/SKILL.md +37 -6
  22. package/skills/deep-research/SKILL.md +6 -0
  23. package/skills/lfm/SKILL.md +148 -0
  24. package/skills/lineage/SKILL.md +14 -4
  25. package/skills/lineage/lineage.py +168 -4
  26. package/skills/obsidizer/SKILL.md +29 -10
  27. package/skills/obsidizer/obsidize.mjs +42 -11
  28. package/skills/omc-patch/SKILL.md +6 -0
  29. package/skills/payload-mon/SKILL.md +2 -0
  30. package/skills/rfp-author/SKILL.md +5 -3
  31. package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
  32. package/skills/setup-bypass-permissions/SKILL.md +27 -9
  33. package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
  34. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
  35. package/skills/setup-omc-hud/SKILL.md +33 -3
  36. package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
  37. package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
  38. package/skills/smart-compact/SKILL.md +58 -12
  39. package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
  40. package/skills/ultra-chain/SKILL.md +8 -4
  41. package/skills/ultra-interview/SKILL.md +13 -2
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * banker 텔레메트리 count 훅 (Codex Stop) - Codex 세션이 banker 스킬을 쓴 턴이 끝날 때 로컬 usage-log 에
4
+ * `banker:<name>\t<hour>` 한 줄을 남긴다. Claude Code 의 telemetry-count(.mjs, -skill.mjs)에 해당한다.
5
+ *
6
+ * Codex 0.144.5 실측으로 스킬이 쓰이는 길은 둘이다.
7
+ * - 모델이 고른 스킬: 도구 호출로 SKILL.md 를 읽는다(Bash `cat <CODEX_HOME>/skills/banker-<name>/SKILL.md`).
8
+ * - 사용자가 $banker-<name> 으로 부른 스킬: Codex 가 도구 호출 없이 `<skill>\n<name>..</name>\n<path>..</path>` 로
9
+ * 시작하는 사용자 역할 메시지로 본문을 넣는다. 이때는 머리의 <path> 줄만 보고 본문은 보지 않는다.
10
+ * 도구 호출마다 도는 PreToolUse 대신 턴마다 한 번 도는 Stop 에서, 훅 입력의 transcript_path(rollout) 중
11
+ * 이번 턴(task_started 의 turn_id 뒤)의 두 기록에 `skills/banker-<name>/SKILL.md` 가 있으면 센다. 나눠 읽어도 한 번이고, Stop 이 한 턴에 두 번 와도
12
+ * 최근 턴 키(configDir 의 codex-count-turns)로 한 번만 센다. 기록은 끝에서부터 32 MB 까지만 읽는다.
13
+ *
14
+ * fail-closed 규약은 Claude 쪽과 같다(telemetry-count-codex.test.mjs 가 검증):
15
+ * - countingActive() 가 아니면 로컬 쓰기조차 하지 않는다.
16
+ * - 이름은 SKILL_NAME_RE 전체 일치이고 이 Codex 에 설치된 banker 스킬(<CODEX_HOME>/skills/banker-<name>/SKILL.md)만.
17
+ * 모델이 `echo skills/banker-<아무 글>/SKILL.md` 를 돌려 체크인의 이름 칸으로 글을 내보내지 못하게 한다. 턴 키 파일은 일반 파일만 읽고, 링크 자리에 쓰지 않는다.
18
+ * - 어떤 오류도 삼키고 항상 exit 0. 도구 호출을 절대 막지 않는다. 표준 출력은 비운다.
19
+ */
20
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, renameSync, rmSync, writeFileSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import { basename, dirname, isAbsolute, join } from 'node:path';
23
+
24
+ import { appendUsageLog, CODEX_SHAPE_MISS_PATH, configDir, countingActive, readRegular, recordUsedSkill, SKILL_NAME_RE, writeByRename } from '../bin/lib/telemetry-config.mjs';
25
+
26
+ const SKILL_PATH_RE = /skills[\\/]+banker-([a-z0-9_-]{1,64})[\\/]+SKILL\.md/gi;
27
+ const KEEP_TURNS = 100;
28
+ export const TURNS_FILE = () => join(configDir(), 'codex-count-turns');
29
+
30
+ function readStdin() {
31
+ return new Promise((resolvePromise, rejectPromise) => {
32
+ let data = '';
33
+ process.stdin.setEncoding('utf8');
34
+ process.stdin.on('data', (chunk) => { data += chunk; });
35
+ process.stdin.on('end', () => resolvePromise(data));
36
+ process.stdin.on('error', rejectPromise);
37
+ });
38
+ }
39
+
40
+ const MAX_READ = 32 << 20;
41
+ const FIRST_READ = 1 << 20;
42
+
43
+ // 글에 나오는 banker 스킬 이름(banker:<name>), 중복 없이.
44
+ export function namesIn(text, names = new Set()) {
45
+ for (const m of text.matchAll(SKILL_PATH_RE)) {
46
+ const name = `banker:${m[1]}`;
47
+ if (SKILL_NAME_RE.test(name)) names.add(name);
48
+ }
49
+ return names;
50
+ }
51
+
52
+ // rollout 끝에서 max 바이트의 줄과, 파일 전체를 읽었는지. 일반 파일만: 기다리지 않게 O_NONBLOCK 으로 열고 연 것을 fstat 으로 본다. 앞에서 잘린 첫 줄은 버린다.
53
+ function tailLines(file, max) {
54
+ const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
55
+ try {
56
+ if (!fstatSync(fd).isFile()) return { lines: [], whole: true };
57
+ const size = fstatSync(fd).size;
58
+ const start = Math.max(0, size - max);
59
+ const buf = Buffer.alloc(size - start);
60
+ readSync(fd, buf, 0, buf.length, start);
61
+ const lines = buf.toString('utf8').split('\n');
62
+ if (start > 0) lines.shift();
63
+ return { lines, whole: start === 0 };
64
+ } finally {
65
+ closeSync(fd);
66
+ }
67
+ }
68
+
69
+ const isCall = (rec) => rec?.type === 'response_item' && /_call$/.test(String(rec?.payload?.type ?? ''));
70
+ const SKILL_HEAD_RE = /^<skill>\n<name>[^\n<]{1,80}<\/name>\n<path>([^\n<]{1,4096})<\/path>\n/;
71
+
72
+ // $banker-<name> 으로 부른 스킬을 Codex 가 넣은 메시지의 <path>. 그 밖의 기록은 ''.
73
+ function injectedPath(rec) {
74
+ const p = rec?.payload;
75
+ if (rec?.type !== 'response_item' || p?.type !== 'message' || p.role !== 'user' || !Array.isArray(p.content)) return '';
76
+ const text = p.content[0]?.text;
77
+ return typeof text === 'string' ? (SKILL_HEAD_RE.exec(text)?.[1] ?? '') : '';
78
+ }
79
+
80
+ // 이번 턴(turn_id 의 task_started 뒤)의 도구 호출과 넣은 스킬 머리에서 읽은 banker 스킬 이름. 사용자 글과 모델 답은 세지 않는다.
81
+ // 턴 시작은 대개 기록 끝 가까이에 있으므로 끝 1 MB 부터 읽고, 못 찾으면 두 배씩 max 까지 넓힌다.
82
+ // 기록 전체를 읽고도 task_started 가 하나도 없으면 null: Codex 는 턴마다 task_started 를 쓰므로 기록 형식이 바뀐 것이다.
83
+ // 이 턴의 것만 없을 때(시작이 기록되지 않는 턴)는 형식 변경으로 보지 않는다.
84
+ export function turnSkills(file, turnId, max = MAX_READ) {
85
+ for (let size = Math.min(FIRST_READ, max); ; size = Math.min(size * 2, max)) {
86
+ const { lines, whole } = tailLines(file, size);
87
+ const start = turnStart(lines, turnId);
88
+ if (start >= 0) return skillsAfter(lines, start);
89
+ if (whole) return lines.some((l) => l.trim()) && !lines.some((l) => l.includes('"task_started"')) ? null : [];
90
+ if (size >= max) return [];
91
+ }
92
+ }
93
+
94
+ // rollout 첫 기록(session_meta)의 Codex 판. Codex 기록이 아니면 ''.
95
+ export function codexVersionOf(file) {
96
+ try {
97
+ const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
98
+ try {
99
+ if (!fstatSync(fd).isFile()) return '';
100
+ const buf = Buffer.alloc(Math.min(fstatSync(fd).size, 8 << 20)); // session_meta 는 지침을 품어 클 수 있다
101
+ readSync(fd, buf, 0, buf.length, 0);
102
+ const rec = JSON.parse(buf.toString('utf8').split('\n')[0]);
103
+ return rec?.type === 'session_meta' ? String(rec.payload?.cli_version ?? '?').slice(0, 40) : '';
104
+ } finally {
105
+ closeSync(fd);
106
+ }
107
+ } catch {
108
+ return '';
109
+ }
110
+ }
111
+
112
+ // 이 Codex 판의 기록을 읽지 못했음을 남긴다(같은 판은 한 번만). 다음 세션 시작 때 update-notify --codex 가 알린다.
113
+ function noteShapeMiss(file) {
114
+ const version = codexVersionOf(file);
115
+ if (!version) return;
116
+ let prev = {};
117
+ try { prev = JSON.parse(readRegular(CODEX_SHAPE_MISS_PATH)); } catch { /* 없음 */ }
118
+ if (prev?.version !== version) writeByRename(CODEX_SHAPE_MISS_PATH, JSON.stringify({ version, notified: false }));
119
+ }
120
+
121
+ function turnStart(lines, turnId) {
122
+ let start = lines.length - 1;
123
+ while (start >= 0 && !(lines[start].includes('"task_started"') && lines[start].includes(turnId))) start--;
124
+ return start;
125
+ }
126
+
127
+ function skillsAfter(lines, start) {
128
+ const names = new Set();
129
+ for (const line of lines.slice(start + 1)) {
130
+ if (!line.includes('SKILL.md')) continue;
131
+ let rec;
132
+ try { rec = JSON.parse(line); } catch { continue; }
133
+ if (isCall(rec)) namesIn(JSON.stringify(rec.payload), names);
134
+ else namesIn(injectedPath(rec), names);
135
+ }
136
+ return [...names];
137
+ }
138
+
139
+ const ID_RE = /^[A-Za-z0-9._-]{1,80}$/;
140
+
141
+ // 이 턴의 키. session_id 와 turn_id 가 모양이 맞을 때만(파일 한 줄 형식을 깨지 않게).
142
+ export function turnKey(payload) {
143
+ const s = payload?.session_id;
144
+ const t = payload?.turn_id;
145
+ return typeof s === 'string' && typeof t === 'string' && ID_RE.test(s) && ID_RE.test(t) ? `${s}:${t}` : null;
146
+ }
147
+
148
+ function readTurns() {
149
+ try {
150
+ return readRegular(TURNS_FILE()).split('\n').filter(Boolean);
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ // 링크나 일반 파일이 아닌 자리에는 쓰지 않는다. 새 파일을 만들어 바꿔 넣는다.
157
+ function writeTurns(lines) {
158
+ const file = TURNS_FILE();
159
+ try {
160
+ if (!lstatSync(file).isFile()) return;
161
+ } catch (e) {
162
+ if (e?.code !== 'ENOENT') return;
163
+ }
164
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
165
+ try {
166
+ writeFileSync(tmp, `${lines.slice(-KEEP_TURNS).join('\n')}\n`, { flag: 'wx', mode: 0o600 });
167
+ renameSync(tmp, file);
168
+ } catch {
169
+ rmSync(tmp, { force: true });
170
+ }
171
+ }
172
+
173
+ // 이 세션의 Codex 홈: rollout 이 <홈>/sessions/YYYY/MM/DD/ 에 있으면 그 홈(상대 경로 CODEX_HOME 도 Codex 가 푼 그대로),
174
+ // 아니면 CODEX_HOME, 그것도 없으면 ~/.codex.
175
+ export function codexHomeOf(transcript, env = process.env) {
176
+ const sessions = dirname(dirname(dirname(dirname(String(transcript)))));
177
+ if (isAbsolute(String(transcript)) && basename(sessions) === 'sessions') return dirname(sessions);
178
+ return env.CODEX_HOME || join(homedir(), '.codex');
179
+ }
180
+
181
+ // 이 Codex 에 설치된 banker 스킬인지.
182
+ export const installed = (name, home) => existsSync(join(home, 'skills', `banker-${name.slice('banker:'.length)}`, 'SKILL.md'));
183
+
184
+ // 이 턴에 아직 세지 않은 이름만 센다.
185
+ export function countOnce(payload) {
186
+ const key = turnKey(payload);
187
+ const file = typeof payload?.transcript_path === 'string' ? payload.transcript_path : '';
188
+ const home = codexHomeOf(file);
189
+ const got = key && file ? turnSkills(file, payload.turn_id) : [];
190
+ if (got === null) {
191
+ noteShapeMiss(file);
192
+ return [];
193
+ }
194
+ const names = got.filter((n) => installed(n, home));
195
+ if (!names.length) return [];
196
+ const seen = readTurns();
197
+ const fresh = names.filter((n) => !seen.includes(`${key}\t${n}`));
198
+ if (!fresh.length) return [];
199
+ for (const n of fresh) {
200
+ appendUsageLog(n);
201
+ recordUsedSkill(n);
202
+ }
203
+ writeTurns([...seen, ...fresh.map((n) => `${key}\t${n}`)]);
204
+ return fresh;
205
+ }
206
+
207
+ async function main() {
208
+ let payload;
209
+ try {
210
+ payload = JSON.parse(await readStdin());
211
+ } catch {
212
+ return;
213
+ }
214
+ if (!countingActive()) return;
215
+ countOnce(payload);
216
+ }
217
+
218
+ if (process.argv[1] && /telemetry-count-codex\.mjs$/.test(process.argv[1])) {
219
+ main()
220
+ .catch(() => {})
221
+ .finally(() => process.exit(0));
222
+ }
@@ -22,6 +22,7 @@ import {
22
22
  UPDATE_CHECK_PATH, updateCheckDue,
23
23
  noUpdateCheck, countingActive, endpoint,
24
24
  installedVersion, compareVersions, writeUpdateCache, readUsedSkills, readRegular,
25
+ CODEX_SHAPE_MISS_PATH, writeByRename,
25
26
  } from '../bin/lib/telemetry-config.mjs';
26
27
 
27
28
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -65,7 +66,11 @@ function personalizedSkills(cached) {
65
66
  // (기존 동작). banker: 접두는 떼서 짧게. 이모지/em-dash 없음(사용자 가시 채널).
66
67
  function buildMessage(latest, installed, cached) {
67
68
  const head = `banker ${latest} 사용 가능 (현재 ${installed}).`;
68
- const tail = `업데이트: /banker:update-banker 또는 npm i -g ${NPM_PKG}`;
69
+ // Codex 의 훅은 --codex 를 받는다(banker setup --codex 가 설치). Codex 에는 /banker: 명령이 없어 스킬 이름과
70
+ // npm 뒤의 Codex 설치 명령을 안내한다.
71
+ const tail = process.argv.includes('--codex')
72
+ ? `업데이트: $banker-update-banker 또는 npm i -g ${NPM_PKG} 뒤 banker setup --codex`
73
+ : `업데이트: /banker:update-banker 또는 npm i -g ${NPM_PKG}`;
69
74
  const hits = personalizedSkills(cached);
70
75
  if (hits.length) {
71
76
  const names = hits.slice(0, 5).map((s) => s.replace(/^banker:/, '')).join(', ');
@@ -79,14 +84,26 @@ function buildMessage(latest, installed, cached) {
79
84
  // compareVersions null(미확정·malformed)·구버전·동일 → 무고지. 고지 후 notified 를 기록해 재고지를 막는다.
80
85
  // 고지 문구는 buildMessage 가 만든다(로컬 changedSkills∩써본스킬이 있으면 개인화, 없으면 일반).
81
86
  function maybeNotify(cached) {
82
- if (!cached) return;
87
+ if (!cached) return '';
83
88
  const installed = installedVersion();
84
89
  const cmp = compareVersions(cached.latest, installed);
85
- if (cmp === null || cmp <= 0) return; // 미확정/구/동일 → 무고지
86
- if (cached.notified === cached.latest) return; // 이미 고지한 버전 → dedupe
87
- const msg = buildMessage(cached.latest, installed, cached);
88
- process.stdout.write(JSON.stringify({ systemMessage: msg }));
90
+ if (cmp === null || cmp <= 0) return ''; // 미확정/구/동일 → 무고지
91
+ if (cached.notified === cached.latest) return ''; // 이미 고지한 버전 → dedupe
89
92
  writeUpdateCache({ notified: cached.latest }); // 형제 필드(latest/checkedAt/changedSkills) 보존하며 notified 만 병합.
93
+ return buildMessage(cached.latest, installed, cached);
94
+ }
95
+
96
+ // Codex: 사용량 카운팅 훅이 이 Codex 판의 기록 형식을 읽지 못했으면(codex-shape-miss.json) 한 번 알린다.
97
+ // 카운트가 조용히 0 으로 남지 않게 하려는 것이다. 업데이트 확인을 끈 사용자에게도 알린다.
98
+ export function shapeMissNotice() {
99
+ try {
100
+ const miss = JSON.parse(readRegular(CODEX_SHAPE_MISS_PATH));
101
+ if (!miss || typeof miss.version !== 'string' || miss.notified) return '';
102
+ writeByRename(CODEX_SHAPE_MISS_PATH, JSON.stringify({ version: miss.version, notified: true }));
103
+ return `banker 사용량 카운팅이 Codex ${miss.version.slice(0, 40)} 의 기록 형식을 읽지 못함. $banker-update-banker 로 banker 를 업데이트하고, 그래도 같으면 알려 주십시오`;
104
+ } catch {
105
+ return '';
106
+ }
90
107
  }
91
108
 
92
109
  // 갱신 fetcher 절대경로 결정: count-default-on 활성(countingActive) + 엔드포인트 설정 시
@@ -110,11 +127,14 @@ async function main() {
110
127
  // SessionStart payload 를 드레인(파이프 위생). 내용을 쓰지 않으므로 파싱 실패해도 무해.
111
128
  try { await readStdin(); } catch { /* stdin 없음/오류 무해 */ }
112
129
 
113
- if (noUpdateCheck()) return; // opt-out - 무동작.
114
-
115
- const cached = readCache(); // 포그라운드는 캐시 읽기만(네트워크 없음).
116
- maybeNotify(cached);
117
- if (updateCheckDue(cached)) spawnFetcher(); // 갱신은 백그라운드로 위임. 실패한 시도(triedAt)에서도 하루를 쉰다.
130
+ const notes = process.argv.includes('--codex') ? [shapeMissNotice()] : [];
131
+ if (!noUpdateCheck()) { // opt-out 이면 업데이트 확인만 무동작.
132
+ const cached = readCache(); // 포그라운드는 캐시 읽기만(네트워크 없음).
133
+ notes.unshift(maybeNotify(cached));
134
+ if (updateCheckDue(cached)) spawnFetcher(); // 갱신은 백그라운드로 위임. 실패한 시도(triedAt)에서도 하루를 쉰다.
135
+ }
136
+ const msg = notes.filter(Boolean).join('\n');
137
+ if (msg) process.stdout.write(JSON.stringify({ systemMessage: msg }));
118
138
  }
119
139
 
120
140
  // 직접 실행(훅)일 때만 main() 을 돌린다. import(테스트)시엔 chooseFetcher 만 노출하고 main 은 미실행.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.20.5",
3
+ "version": "0.21.0",
4
4
  "description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
5
5
  "bin": {
6
6
  "banker": "bin/banker.js"
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: append_wiki
3
- description: "새로 알게 된 비자명한 지식을 `.omc/wiki` 에 구조화해 넣고 기존 페이지와 병합. 'append_wiki'/'위키 기록'/'wiki에 정리'/'알게 된 내용 기록' 시 사용."
3
+ description: "새로 알게 된 비자명한 지식을 `.omc/wiki`(Codex 는 `omx_wiki/`) 에 구조화해 넣고 기존 페이지와 병합. 'append_wiki'/'위키 기록'/'wiki에 정리'/'알게 된 내용 기록' 시 사용."
4
4
  ---
5
5
 
6
6
  # append_wiki — 작업 지식을 wiki에 적재
7
7
 
8
- 이번 작업에서 **새로 배운 비자명한 지식**을 `.omc/wiki/`에 구조화해 남기는 워크플로우.
9
- OMC `wiki` 스킬의 MCP 도구를 사용하며, 답변·설명은 한글로 작성한다(도구명·경로·식별자 등
10
- 기술 토큰은 영문 유지).
8
+ 이번 작업에서 **새로 배운 비자명한 지식**을 위키에 구조화해 남기는 워크플로우.
9
+ Claude Code 는 OMC `wiki` 스킬의 MCP 도구로 `.omc/wiki/` 에, Codex 는 OMX 의 `omx wiki` CLI 로 `omx_wiki/` 에 쓴다.
10
+ 답변·설명은 한글로 작성한다(도구명·경로·식별자 등 기술 토큰은 영문 유지).
11
11
 
12
12
  ## 언제 쓰나
13
13
  - 기능 구현/디버깅/리팩터링 등 한 덩어리 작업이 끝났을 때.
@@ -21,12 +21,38 @@ OMC `wiki` 스킬의 MCP 도구를 사용하며, 답변·설명은 한글로 작
21
21
 
22
22
  ## 워크플로우
23
23
 
24
- ### 0) wiki 도구 로드 (deferred — 반드시 먼저)
25
- `wiki_*` 는 deferred MCP 도구다. 호출 전에 ToolSearch로 스키마를 로드한다:
24
+ ### 0) wiki 도구 준비 (런타임마다 다름 — 반드시 먼저)
25
+ **Claude Code:** `wiki_*` 는 deferred MCP 도구다. 호출 전에 ToolSearch로 스키마를 로드한다:
26
26
  ```
27
27
  ToolSearch(query="select:mcp__plugin_oh-my-claudecode_t__wiki_ingest,mcp__plugin_oh-my-claudecode_t__wiki_query,mcp__plugin_oh-my-claudecode_t__wiki_list,mcp__plugin_oh-my-claudecode_t__wiki_lint")
28
28
  ```
29
29
 
30
+ **Codex:** `wiki_*` MCP 도 ToolSearch 도 없다. OMX 의 `omx wiki` CLI 가 같은 도구 이름과 입력을 받는다. 위키 폴더는 `.omc/wiki/` 가 아니라 프로젝트 루트의 `omx_wiki/` 다.
31
+ ```bash
32
+ omx wiki wiki_list --json
33
+ omx wiki wiki_query --input '{"query":"<키워드>","tags":["<태그>"]}' --json
34
+ omx wiki wiki_ingest --input "$(cat <JSON 파일>)" --json
35
+ ```
36
+ - 아래 단계의 `wiki_xxx({...})` 는 Codex 에서 `omx wiki wiki_xxx --input '<같은 JSON>' --json` 이다.
37
+ - 본문에 작은따옴표나 줄바꿈이 많으면 JSON 을 파일로 쓰고 `--input "$(cat <파일>)"` 로 넘긴다. 셸 인용이 깨지지 않게 하기 위해서다(bash).
38
+ - Windows 에서 PowerShell 로 `omx` 를 부를 때: Windows PowerShell 5.1 은 `omx.cmd` 에 넘기는 인자에서 큰따옴표를 지우고,
39
+ 공백에서 인자를 나누고, 한글을 깨뜨린다(Windows 박스 실측). 그래서 JSON 을 아래 `Arg` 로 바꿔 넘긴다.
40
+ 본문 속 큰따옴표와 공백, ASCII 밖 글자, cmd 가 읽는 `% & | < > ^ !` 는 `\uXXXX` 로, JSON 구조의 큰따옴표는 `\"` 로 바꾼다.
41
+ 따옴표(홀수 개 포함), 역슬래시, `%TEMP%`, `&`, 한글, 줄바꿈이 든 본문도 그대로 왕복하고 명령으로 읽히지 않는다(박스 실측).
42
+ ```powershell
43
+ function Arg([string]$s) {
44
+ $a = [regex]::Replace($s, '(?<!\\)((?:\\\\)*)\\"', '$1\u0022')
45
+ $a = [regex]::Replace($a, '[^\x21-\x7E]|[%&|<>^!]', { param($m) '\u{0:x4}' -f [int][char]$m.Value })
46
+ $a -replace '(\\*)"', '$1$1\"'
47
+ }
48
+ $j = @{ title = "<영문 제목>"; content = "<본문>"; tags = @("<태그>"); category = "pattern" } | ConvertTo-Json -Compress
49
+ omx wiki wiki_ingest --input (Arg $j) --json
50
+ omx wiki wiki_query --input (Arg '{"query":"<키워드>"}') --json
51
+ ```
52
+ - 인자 하나는 Linux 에서 128KB 를 넘지 못한다. 본문이 그보다 크면 페이지를 나눈다.
53
+ - `omx` 가 없으면 멈추고 OMX 설치를 안내한다. 위키 파일을 손으로 쓰지 않는다(목차와 이력이 어긋난다).
54
+ - `omx wiki` 는 작업 폴더에만 쓰므로 샌드박스 승인이 필요 없다(Codex 0.144.5, OMX 0.20.2 에서 확인).
55
+
30
56
  ### 1) 무엇을 남길지 식별
31
57
  이번 작업의 산출에서 다음을 추린다:
32
58
  - **아키텍처/설계 결정**과 그 이유(대안 포함) → `architecture` / `decision`.
@@ -68,7 +94,7 @@ wiki_ingest({
68
94
  `wiki_lint()` 로 broken cross-reference / orphan / stale / oversized 를 점검하고,
69
95
  깨진 링크가 있으면 슬러그를 맞춰 다시 ingest 한다.
70
96
 
71
- **environment.md 위생 (이식 가능 셀프힐 — 모든 시스템 공통).**
97
+ **environment.md 위생 (이식 가능 셀프힐 — Claude Code 만. Codex 의 `omx_wiki/` 에는 이 파일이 생기지 않으므로 건너뛴다).**
72
98
  `.omc/wiki/environment.md` 는 append_wiki 가 아니라 OMC **SessionStart hook**
73
99
  (`hooks/wiki/session-hooks`)이 `project-memory.json` 에서 자동생성한다. 이 hook 에는 **알려진 버그**가
74
100
  있어 `techStack.frameworks`(객체 `{name,version}` 배열) 와 객체형 `build` 값(예: `scripts`)을 그대로
@@ -90,7 +116,7 @@ project-memory 가 안 바뀌면 hook 은 재생성을 건너뛰므로(`existing
90
116
  - 생성/갱신된 페이지와 슬러그 목록.
91
117
  - lint 결과.
92
118
  - environment.md 위생: `[object Object]` 정화 여부(및 hook 소스 근본패치 시 그 사실).
93
- - 저장 위치는 `.omc/wiki/*.md`(프로젝트 로컬, git-ignored), 목차 `index.md`, 이력 `log.md`.
119
+ - 저장 위치는 실제로 쓴 곳을 적는다. Claude Code 는 `.omc/wiki/*.md`(프로젝트 로컬, git-ignored), Codex 는 `omx_wiki/*.md` 다. 목차 `index.md`, 이력 `log.md`.
94
120
 
95
121
  ## 작성 원칙
96
122
  - **삭제보다 보존, 중복보다 병합.** 같은 주제는 새 페이지 대신 기존 페이지에 append.
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: compact-wiki
3
- description: "`.omc/wiki` 페이지를 무손실로 정리. 중복은 지우고, 낡은 내용은 새 내용으로 대체하고, 겹치는 페이지는 병합. 'compact-wiki'/'위키 정리'/'위키 압축'/'wiki 중복 제거'/'위키 병합' 시 사용."
3
+ description: "`.omc/wiki`(Codex 는 `omx_wiki/`) 페이지를 무손실로 정리. 중복은 지우고, 낡은 내용은 새 내용으로 대체하고, 겹치는 페이지는 병합. 'compact-wiki'/'위키 정리'/'위키 압축'/'wiki 중복 제거'/'위키 병합' 시 사용."
4
4
  ---
5
5
 
6
6
  # compact-wiki — wiki 정리·압축 (무손실)
7
7
 
8
8
  `append-wiki` 가 `.omc/wiki/` 에 누적한 지식을 **중복 제거·supersede·병합**으로 압축한다.
9
- append-wiki 가 "쌓기"라면 이 스킬은 "정리"다. 두 스킬 모두 OMC `wiki_*` MCP 도구를 쓰고,
10
- 답변·설명은 한글(도구명·슬러그·경로 등 기술 토큰은 영문).
9
+ append-wiki 가 "쌓기"라면 이 스킬은 "정리"다. 두 스킬 모두 Claude Code 는 OMC `wiki_*` MCP 도구로 `.omc/wiki/` 를,
10
+ Codex 는 OMX 의 `omx wiki` CLI 로 `omx_wiki/` 를 다룬다. 답변·설명은 한글(도구명·슬러그·경로 등 기술 토큰은 영문).
11
+ 아래의 `.omc/wiki` 는 Codex 에서 `omx_wiki` 로 읽는다.
11
12
 
12
13
  > ★**불변식 — 지식 무손실**: 이 스킬의 모든 destructive 동작(병합 후 삭제·supersede)은
13
14
  > **스냅샷 → 병합본 작성 → 병합본이 원본의 모든 고유 사실을 담았는지 검증 → 그 다음에만 삭제**
@@ -25,17 +26,47 @@ append-wiki 가 "쌓기"라면 이 스킬은 "정리"다. 두 스킬 모두 OMC
25
26
 
26
27
  ## 워크플로우
27
28
 
28
- ### 0) wiki 도구 로드 (deferred — 먼저)
29
- `wiki_*` 는 deferred MCP 도구다. 호출 전 ToolSearch로 스키마 로드:
29
+ ### 0) wiki 도구 준비 (런타임마다 다름 — 먼저)
30
+ **Claude Code:** `wiki_*` 는 deferred MCP 도구다. 호출 전 ToolSearch로 스키마 로드:
30
31
  ```
31
32
  ToolSearch(query="select:mcp__plugin_oh-my-claudecode_t__wiki_list,mcp__plugin_oh-my-claudecode_t__wiki_query,mcp__plugin_oh-my-claudecode_t__wiki_read,mcp__plugin_oh-my-claudecode_t__wiki_ingest,mcp__plugin_oh-my-claudecode_t__wiki_delete,mcp__plugin_oh-my-claudecode_t__wiki_lint")
32
33
  ```
33
34
 
35
+ **Codex:** `wiki_*` MCP 도 ToolSearch 도 없다. `omx wiki` CLI 가 같은 도구 이름과 입력을 받는다(`wiki_list`, `wiki_query`, `wiki_read`, `wiki_ingest`, `wiki_delete`, `wiki_lint`).
36
+ ```bash
37
+ omx wiki wiki_list --json
38
+ omx wiki wiki_read --input '{"page":"<slug>"}' --json
39
+ omx wiki wiki_ingest --input "$(cat <JSON 파일>)" --json
40
+ omx wiki wiki_delete --input '{"page":"<slug>"}' --json
41
+ ```
42
+ - 아래 단계의 `wiki_xxx({...})` 는 Codex 에서 `omx wiki wiki_xxx --input '<같은 JSON>' --json` 이다. `page` 는 슬러그나 파일 이름(`<slug>.md`)을 받는다.
43
+ - 병합본처럼 긴 본문은 JSON 을 파일로 쓰고 `--input "$(cat <파일>)"` 로 넘긴다. 셸 인용이 깨지지 않게 하기 위해서다(bash).
44
+ - Windows 에서 PowerShell 로 `omx` 를 부를 때: Windows PowerShell 5.1 은 `omx.cmd` 에 넘기는 인자에서 큰따옴표를 지우고,
45
+ 공백에서 인자를 나누고, 한글을 깨뜨린다(Windows 박스 실측). 그래서 JSON 을 아래 `Arg` 로 바꿔 넘긴다.
46
+ 본문 속 큰따옴표와 공백, ASCII 밖 글자, cmd 가 읽는 `% & | < > ^ !` 는 `\uXXXX` 로, JSON 구조의 큰따옴표는 `\"` 로 바꾼다.
47
+ 따옴표(홀수 개 포함), 역슬래시, `%TEMP%`, `&`, 한글, 줄바꿈이 든 본문도 그대로 왕복하고 명령으로 읽히지 않는다(박스 실측).
48
+ ```powershell
49
+ function Arg([string]$s) {
50
+ $a = [regex]::Replace($s, '(?<!\\)((?:\\\\)*)\\"', '$1\u0022')
51
+ $a = [regex]::Replace($a, '[^\x21-\x7E]|[%&|<>^!]', { param($m) '\u{0:x4}' -f [int][char]$m.Value })
52
+ $a -replace '(\\*)"', '$1$1\"'
53
+ }
54
+ $j = @{ title = "<영문 제목>"; content = "<본문>"; tags = @("<태그>"); category = "pattern" } | ConvertTo-Json -Compress
55
+ omx wiki wiki_ingest --input (Arg $j) --json
56
+ omx wiki wiki_query --input (Arg '{"query":"<키워드>"}') --json
57
+ ```
58
+ - 인자 하나는 Linux 에서 128KB 를 넘지 못한다. 본문이 그보다 크면 페이지를 나눈다.
59
+ - `omx` 가 없으면 멈추고 OMX 설치를 안내한다. 위키 파일을 손으로 지우거나 쓰지 않는다(목차와 이력이 어긋난다).
60
+ - `omx wiki` 와 스냅샷은 작업 폴더에만 쓰므로 샌드박스 승인이 필요 없다(Codex 0.144.5, OMX 0.20.2 에서 확인).
61
+
34
62
  ### 1) 스냅샷 (★안전망 — 가장 먼저)
35
63
  삭제·치환 전에 wiki 디렉터리를 통째로 백업해 복구 가능 상태를 만든다:
36
64
  ```bash
37
65
  ts=$(date +%Y%m%d-%H%M%S); cp -r .omc/wiki ".omc/wiki.bak-$ts" && echo "backup: .omc/wiki.bak-$ts"
38
66
  ```
67
+ Codex 는 `omx_wiki/` 를 `.omx/` 아래로 떠 둔다(`mkdir -p .omx && cp -r omx_wiki ".omx/wiki.bak-$ts"`).
68
+ 프로젝트 루트에 두지 않는 것은 OMX 가 `omx_wiki/` 를 커밋 대상으로 보아 옆의 백업 폴더가 커밋에 섞일 수 있기 때문이다.
69
+ `omx_wiki/` 가 없으면 정리할 위키가 없으니 그 사실을 알리고 멈춘다.
39
70
  작업 완료·사용자 확인 전까지 백업을 지우지 않는다(보고에 위치 명시).
40
71
 
41
72
  ### 2) 인벤토리 + 현재 상태 채증
@@ -71,7 +102,7 @@ ts=$(date +%Y%m%d-%H%M%S); cp -r .omc/wiki ".omc/wiki.bak-$ts" && echo "backup:
71
102
  ### 8) 보고 (한글)
72
103
  - 병합/supersede 매핑(어느 페이지 → 어느 정본), 삭제된 슬러그, 리포인트한 링크.
73
104
  - before→after: 페이지 수·총 크기·lint 결과.
74
- - **백업 위치**(`.omc/wiki.bak-*`)와 "이상 없으면 삭제해도 됨" 안내.
105
+ - **백업 위치**(Claude Code `.omc/wiki.bak-*`, Codex `.omx/wiki.bak-*`, 실제로 만든 경로)와 "이상 없으면 삭제해도 됨" 안내.
75
106
 
76
107
  ## 작성 원칙 (append-wiki 와 정합)
77
108
  - **보존 > 압축.** 압축은 중복·잡음 제거지 지식 삭제가 아니다. 고유 사실은 1건도 잃지 않는다.
@@ -32,6 +32,11 @@ description: "여러 출처를 병렬로 웹 리서치해 주장을 적대적으
32
32
  ### 1. Scope
33
33
  질문을 **~5개 검색 각도**로 분해(상호보완적 관점: 정의/현황·반대근거·정량데이터·사례·시간축 등).
34
34
 
35
+ 질문이 계획, 결정, 구현에 쓰이면 각도 하나를 **그 분야 최고 전문가 그룹의 작업 방식**으로 둔다.
36
+ - 방법은 `lfm` 의 2~5단계를 따른다: 근거가 있는 그룹 선정, 그 그룹이 공개한 1차 자료, 공통 관행과 그룹 고유 관행 구분, 계획 적용 판정(`M-1` …).
37
+ - 부른 쪽이 "전문가 작업 방식은 lfm 이 맡음" 이라고 넘기면 이 각도를 두지 않는다(같은 조사를 두 번 하지 않는다).
38
+ - 이 각도의 주장도 4단계의 적대적 검증을 거친다.
39
+
35
40
  ### 2. Search
36
41
  각도마다 **병렬** 웹검색(Claude=병렬 `Task`+`WebSearch`, Codex=병렬 조사). 각 에이전트는 서로의
37
42
  결과를 모른 채 독립 탐색.
@@ -51,6 +56,7 @@ URL 중복제거 후 상위 **≤15 소스**(`MAX_FETCH=15`) fetch → 각 소
51
56
  ## 출력 형식
52
57
  - **요약**: 핵심 발견 3~5개(확신도 표기).
53
58
  - **본문**: 주장별 근거 + 출처(URL/제목) + 확신도. 상충 증거는 병기.
59
+ - **전문가 작업 방식**(그 각도를 둔 경우): 고른 그룹과 선정 근거, 공통 관행, 계획 적용 항목 표(`lfm` 출력의 같은 이름 절과 같은 꼴).
54
60
  - **출처 목록**: fetch·인용한 소스.
55
61
  - **한계**: 검증 폐기된 주장, 미해결 불확실성, 조사 못 한 각도.
56
62
 
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: lfm
3
+ description: "learn from master. 어떤 분야의 최고 전문가 그룹이 실제로 일하는 방식을 그들이 공개한 1차 자료로 조사함. 작업 순서, 역할, 품질 기준, 검토 관행, 도구, 흔한 실패와 대응을 정리하고, 계획에 바로 쓸 적용 항목으로 바꿈. 'lfm'/'learn from master'/'전문가 작업 방식'/'대가에게 배우기' 시 사용."
4
+ argument-hint: "<분야나 작업> [--out <파일>] [--no-questions]"
5
+ ---
6
+
7
+ # lfm: learn from master
8
+
9
+ 어떤 분야의 최고 전문가 그룹이 그 일을 실제로 어떻게 하는지 조사한다. 답변은 한글.
10
+ 결과는 읽을거리가 아니라 계획의 입력이다. 모든 관행은 "우리 계획에 어떻게 쓰는가" 한 줄로 끝난다.
11
+
12
+ 다른 스킬(`ultra-interview`, `ultra-chain`, `deep-research`, `rfp-author`)이 리서치 단계에서 이 스킬을 부른다. 혼자 불러도 된다.
13
+
14
+ ## 언제 쓰나
15
+
16
+ - 처음 하는 종류의 일을 계획하기 전에, 그 분야의 잘하는 사람들이 어떻게 하는지 알고 싶을 때
17
+ - 계획이나 기준이 혼자 정한 것인지, 분야의 검증된 관행에 맞는지 확인하고 싶을 때
18
+ - 사용자가 `lfm`, `learn from master`, `전문가 작업 방식`, `대가에게 배우기` 라고 할 때
19
+
20
+ ## 쓰지 않을 때
21
+
22
+ - 사실 하나를 찾는 일. 그건 웹 검색 한 번이나 `deep-research` 다.
23
+ - 사내 비공개 절차처럼 공개 자료가 없는 일. 이 스킬은 공개된 것만 다룬다.
24
+
25
+ ## 인자
26
+
27
+ - 첫 인자는 분야나 작업이다(예: `결제 API 설계`, `모바일 앱 출시 QA`, `소프트웨어 외주 RFP`).
28
+ - `--out <파일>`: 결과를 그 파일에도 쓴다. 다른 스킬이 부를 때 쓴다.
29
+ - `--no-questions`: 사용자에게 묻지 않는다. 분야가 넓으면 스스로 좁히고 그 판단을 결과의 "분야" 줄에 적는다. 다른 스킬이 부를 때는 언제나 이 인자를 준다.
30
+
31
+ ## 절차
32
+
33
+ ### 1. 분야 좁히기
34
+
35
+ - 요청에서 분야를 1~3개로 정한다. 작업이 두 분야에 걸치면 둘 다 둔다(예: `결제 API 설계` 는 결제 시스템 엔지니어링과 API 설계).
36
+ - 분야가 너무 넓으면(예: `개발`) 질문 하나로 좁힌다. `--no-questions` 면 요청의 맥락으로 좁히고 그 이유를 적는다.
37
+ - 적용 대상의 전제를 함께 정한다: 팀 규모, 조직 형태(사내, 오픈소스, 외주), 규제, 일정. 5단계의 판정이 이 전제에 크게 좌우된다. `--no-questions` 면 맥락에서 정하고 가정으로 적는다.
38
+
39
+ ### 2. 최고 전문가 그룹 고르기 (분야마다 3~5곳)
40
+
41
+ 이름값으로 고르지 않는다. 아래 근거 중 하나 이상이 공개 자료로 확인되는 곳만 고른다.
42
+
43
+ | 근거 | 예 |
44
+ |---|---|
45
+ | 그 분야의 표준, 사양, 지침을 쓰거나 관리함 | 표준 기구의 작업반, 사양 편집 조직 |
46
+ | 그 분야에서 널리 쓰이는 도구나 오픈소스를 만들고 유지함 | 주요 프레임워크 유지 조직 |
47
+ | 작업 방식을 직접 공개함 | 엔지니어링 핸드북, 설계 문서 관행, 공개 포스트모템 |
48
+ | 같은 분야 사람들이 기준으로 인용함 | 다른 그룹의 문서가 그 방식을 기준으로 삼음 |
49
+
50
+ - 그룹마다 선정 근거와 그 출처 URL 을 단다.
51
+ - 기업, 표준 기구, 오픈소스 프로젝트, 연구 그룹, 공공 기관 모두 될 수 있다. 한 종류로 치우치지 않게 한다.
52
+
53
+ ### 3. 작업 방식 조사
54
+
55
+ 그룹이 직접 공개한 1차 자료만 근거로 쓴다: 핸드북, 엔지니어링 블로그, 설계 문서와 RFC, 표준 문서, 포스트모템, 그 그룹 사람이 한 발표, 그 그룹이 쓴 책.
56
+ "OO 의 일하는 방식 10가지" 같은 남의 요약은 1차 자료를 찾는 길잡이로만 쓰고 근거로 달지 않는다.
57
+
58
+ 그룹마다 아래 여섯 관점을 본다. 자료가 없는 관점은 비워 두고 "자료 없음" 으로 적는다.
59
+
60
+ | 관점 | 찾는 것 |
61
+ |---|---|
62
+ | 작업 순서 | 단계와 단계마다 나오는 산출물 |
63
+ | 역할과 결정 | 누가 정하고 누가 검토하는가, 결정 기록 방식 |
64
+ | 품질 기준 | 완료의 정의, 수치 기준(성능, 결함, 가용성 등) |
65
+ | 검토와 검증 | 리뷰 방식, 시험 범위, 단계적 출시, 되돌리기 |
66
+ | 도구와 형식 | 템플릿, 체크리스트, 문서 형식 |
67
+ | 흔한 실패와 대응 | 그 그룹이 공개한 실패와 그 뒤 바꾼 관행 |
68
+
69
+ - 1차 자료를 읽지 못하면(PDF 를 풀 도구가 없음, 접근 막힘) 한계에 적는다. 그 자료의 수치를 남의 요약으로만 알면 "모델 지식(미확인)" 과 같이 다룬다.
70
+
71
+ 조사 수단:
72
+ - Claude Code: `WebSearch`, `WebFetch`. 분야나 그룹이 여럿이면 그룹마다 독립 에이전트를 병렬로 띄운다. 에이전트를 띄울 수 없거나 이미 하위 에이전트로 돌고 있으면 그룹을 차례로 조사한다.
73
+ - Codex: 런타임의 웹 검색과 가져오기. OMX `best-practice-research` 가 있으면 함께 쓴다.
74
+
75
+ ### 4. 교차 확인
76
+
77
+ - 두 그룹 이상에서 같은 관행이 보이면 "공통 관행", 한 그룹에만 있으면 "그룹 고유 관행" 이다.
78
+ - 공통 관행마다 반대 근거를 한 번 찾는다(그 관행이 실패한 사례, 그 그룹이 나중에 버린 기록). 찾았으면 표의 "반대 근거" 칸에 적는다. 그룹 고유 관행은 적용 판정을 하기 전에만 찾는다.
79
+ - 같은 관행인데 그룹마다 숫자가 다르면(예: 변경 크기 상한) 차이를 "반대 근거" 칸에 적고, 판정은 "조정" 을 기본으로 한다.
80
+ - 그 그룹이 나중에 바꾼 관행은 지금 것을 적고 바뀐 시점을 남긴다.
81
+ - 출처 없이 알고 있는 내용은 "모델 지식(미확인)" 으로 따로 적고, 적용 항목의 근거로 쓰지 않는다.
82
+
83
+ ### 5. 계획 적용 항목
84
+
85
+ 관행마다 이 작업의 계획에 무엇을 할지 한 줄로 정한다. 번호는 `M-1`, `M-2` 처럼 단다.
86
+
87
+ | 판정 | 뜻 |
88
+ |---|---|
89
+ | 적용 | 그대로 계획에 넣는다 |
90
+ | 조정 | 규모나 제약에 맞게 줄이거나 바꿔 넣는다. 무엇을 바꿨는지 적는다 |
91
+ | 제외 | 이 작업에는 맞지 않는다. 이유를 적는다(규모, 비용, 규제, 팀 구성 등) |
92
+
93
+ - 공통 관행은 적용이나 조정이 기본이다. 제외하면 이유가 더 구체적이어야 한다.
94
+ - 품질 기준은 숫자 그대로 옮기지 않는다. 이 작업의 수용 기준 후보로 적고, 그대로 쓸 수 있는지 판정한다.
95
+
96
+ ## 출력
97
+
98
+ `--out` 이 있으면 그 파일에도 같은 내용을 쓴다.
99
+
100
+ ```markdown
101
+ # 전문가 작업 방식: <분야>
102
+
103
+ - 분야: <좁힌 분야와 그 이유>
104
+ - 적용 대상 전제: <팀 규모, 조직 형태, 규제, 일정. 가정이면 가정이라고 적음>
105
+ - 조사 시각: <YYYY-MM-DD>
106
+
107
+ ## 고른 그룹
108
+ | 그룹 | 선정 근거 | 출처 |
109
+
110
+ ## 관점별 자료
111
+ | 그룹 | 작업 순서 | 역할과 결정 | 품질 기준 | 검토와 검증 | 도구와 형식 | 실패와 대응 |
112
+ (칸마다 "있음" 이나 "자료 없음")
113
+
114
+ ## 공통 관행
115
+ | 관행 | 관점 | 그룹 | 출처 | 반대 근거 |
116
+
117
+ ## 그룹 고유 관행
118
+ | 관행 | 관점 | 그룹 | 출처 |
119
+
120
+ ## 품질 기준 후보
121
+ | 기준 | 그룹 | 출처 |
122
+
123
+ ## 흔한 실패와 대응
124
+ | 실패 | 대응 | 그룹 | 출처 |
125
+
126
+ ## 계획 적용 항목
127
+ | 번호 | 관행 | 판정 | 계획에 넣을 것, 또는 제외 이유 |
128
+
129
+ ## 한계
130
+ - 자료를 찾지 못한 관점과 그룹
131
+ - 읽지 못한 1차 자료
132
+ - 모델 지식(미확인)으로만 아는 것
133
+ ```
134
+
135
+ ## 웹 도구가 모두 실패하면
136
+
137
+ 검색과 가져오기가 모두 실패하면(네트워크 없음) 결과를 만들지 않고 "웹 조사 불가: <오류>" 로 끝낸다.
138
+ 모델이 아는 관행으로 표를 채우지 않는다. 부른 스킬은 이 결과를 "전문가 작업 방식 조사 못 함" 으로 기록하고 진행한다.
139
+
140
+ ## 함정
141
+
142
+ - 이름값으로 그룹을 고르는 것. 근거 없는 "업계 최고" 는 쓰지 않는다.
143
+ - 남의 요약글을 1차 자료처럼 다는 것.
144
+ - 관행을 나열만 하고 계획 적용 판정을 빼는 것. 판정 없는 관행은 이 스킬의 결과가 아니다.
145
+ - 큰 조직의 관행을 작은 작업에 그대로 옮기는 것. 규모 차이는 "조정" 이나 "제외" 로 드러낸다.
146
+ - 공개되지 않은 사내 절차를 짐작해 채우는 것.
147
+
148
+ ARGUMENTS: <분야나 작업> [--out <파일>] [--no-questions]