@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
@@ -2,7 +2,7 @@
2
2
  name: lineage
3
3
  description: "현재 세션 대화를 카카오톡 스타일의 단일 HTML 로 내보냄. Claude 답변은 한 줄 요약 아래에 원문을 접어 두고, 펼치면 마크다운으로 렌더해 보여 줌. 기본은 세션 모델이 요약을 쓰고 노이즈 턴을 걸러 내며, `--rulebase` 는 모델 없이 규칙만 씀. 여러 세션을 합칠 수 있음. 'lineage'/'대화 export'/'카톡 스타일 html' 시 사용."
4
4
  invocation: /lineage
5
- version: 3.0.10
5
+ version: 3.1.1
6
6
  schema_version: 1
7
7
  ---
8
8
 
@@ -84,7 +84,7 @@ Python 은 3.7 이상을 쓴다(EL8 기본 `python3` 는 3.6 이라 `python3.11`
84
84
  python3 "<스킬 폴더>/lineage.py" <사용자 인자> --emit-review work/.lineage-review.json
85
85
  ```
86
86
  - stderr 의 `part K/N: <파트 파일> (to review: X) -> <결정 파일>` 한 줄이 검토 1건이다.
87
- - 자동 탐색이 고른 기록(stderr 의 `auto-discovered session:`)을 사용자에게 알린다. Codex 에서는 같은 폴더의 Claude Code 기록이 골라진다.
87
+ - 자동 탐색이 고른 기록(stderr 의 `auto-discovered session:`)을 사용자에게 알린다. Codex 에서는 지금 Codex 세션의 기록(rollout)이 골라진다.
88
88
  - 검토할 파트가 6개를 넘으면 진행 전에 묻는다. Claude Code 는 `AskUserQuestion` 으로 진행, `--last N`, `--rulebase` 중에서 고르게 한다. 물을 수 없는 실행이면 파트 수를 알리고 진행한다. 물을 때는 비용도 알린다: 파트 하나에 검토 에이전트 1개가 돌고, 이 저장소 실측은 40턴 파트 하나에 약 11만 토큰과 7분이었다.
89
89
  - 묶음의 본문은 이미 redact 되어 있다. 검토자가 읽는 파트 파일은 `--redact-mode` 와 관계없이 전부 가린다(Cache & Secret Hygiene).
90
90
  - 규칙이 확실한 노이즈(래퍼 블록, 훅 피드백, 중단 표시, 주입된 스킬과 워크플로 본문, 하네스 오류, 조작 명령)는 이미 빠져 있다. `--keep-trivia` 를 주면 규칙이 주입 본문, 하네스 오류, 조작 명령, 에코 교환을 남기고(래퍼 블록, 훅 피드백, 중단 표시는 그래도 빠진다), 파트 파일의 `keep_trivia: true` 가 검토자에게 턴을 빼지 말라고 알린다.
@@ -176,8 +176,10 @@ Python 은 3.7 이상을 쓴다(EL8 기본 `python3` 는 3.6 이라 `python3.11`
176
176
 
177
177
  ```
178
178
  --all-sessions → 프로젝트 폴더의 모든 *.jsonl (레코드단위 시간순 병합)
179
+ Codex 안이면 첫 기록(session_meta)의 cwd 가 지금 폴더인 rollout 전부
179
180
  또는 단일 세션:
180
- auto-discover (~/.claude/projects/<encoded-cwd>/*.jsonl most-recent)
181
+ auto-discover: Codex 안이면(CODEX_THREAD_ID) <CODEX_HOME>/sessions/*/*/*/rollout-*-<그 id>.jsonl,
182
+ 아니면 (~/.claude/projects/<encoded-cwd>/*.jsonl most-recent)
181
183
  → --session FILE 명시
182
184
  → --from-transcript - stdin paste
183
185
  → 모두 실패 시 exit 2 + 옵션 안내
@@ -187,6 +189,12 @@ auto-discover (~/.claude/projects/<encoded-cwd>/*.jsonl most-recent)
187
189
  (Claude Code 의 실제 project-dir 명명과 일치 — `_` 도 `-` 로). 비-ASCII cwd(한글 등)는
188
190
  자동탐색이 빗나갈 수 있으니 `--session` 을 쓴다.
189
191
 
192
+ 기록 형식은 첫 레코드로 가린다. 첫 레코드가 `session_meta` 이면 Codex rollout 으로 읽는다.
193
+ - 사용자 턴: `event_msg` 의 `user_message`(사용자가 입력한 글). 모델 입력에 들어간 환경, 지침, 주입된 스킬 본문(`response_item` 의 사용자 메시지)은 턴이 아니다.
194
+ - 모델 턴: `event_msg` 의 `agent_message`.
195
+ - 도구 호출: `response_item` 의 `*_call`(이름은 `name`, 없으면 종류). 본문 없는 모델 레코드라 Claude Code 의 `tool_use` 처럼 앞뒤 모델 턴에 합쳐진다. 도구 결과(`*_call_output`)는 읽지 않는다.
196
+ - `compacted` 는 compaction 구분선이다.
197
+
190
198
  Schema-tolerant 파서: 미지 record type → stderr WARN + 다음 line. `v1`/`v2` 같은 schema marker
191
199
  발견 시 `--unsafe-schema` 명시 없으면 exit 2. UTF-8 디코드 실패 파일은 그 파일만 건너뛴다(전체 실패 X).
192
200
 
@@ -322,12 +330,14 @@ pip install 'detect-secrets>=1.5' # (선택) 강한 redaction
322
330
  ## 한계
323
331
 
324
332
  - 자동탐색은 Claude Code 의 `~/.claude/projects/` 레이아웃을 가정 — 비-ASCII cwd 는 `--session` / `--from-transcript` 로 쓴다.
325
- - 읽을 수 있는 기록은 Claude Code 형식뿐이다. Codex 의 rollout 은 0턴으로 읽히므로, Codex 에서는 Claude Code 기록을 내보낼 때만 쓴다.
333
+ - 읽는 기록은 Claude Code 형식과 Codex rollout 이다. Codex rollout 에서는 사용자가 입력한 글, 모델의 답, 도구 호출 이름만 읽는다. 모델의 추론 요약과 도구 결과는 페이지에 없다.
334
+ - Codex 는 rollout 형식을 공개 약속으로 내놓지 않는다. 사용자 턴을 하나도 읽지 못한 rollout 은 빈 페이지 대신 exit 2(`no user turn was read`, Codex 버전 표시)로 멈춘다. 이 오류가 나면 사용자에게 banker 업데이트를 권하고, Codex 를 업데이트했다면 `banker doctor` 의 기록 형식 점검 결과를 함께 보인다. `--all-sessions` 는 그런 rollout 을 WARN 과 함께 뺀다.
326
335
  - 기본 흐름은 redact 된 세션 본문을 세션 모델의 프로바이더로 보낸다. 모델에 보내지 않으려면 `--rulebase` 를 쓴다.
327
336
  - `--rulebase` 요약은 추출식(head+tail) — 말미 정리 문장을 결론 대신 고르는 경우가 있다. 기본 흐름은 검토자가 이를 고쳐 쓴다.
328
337
  - 기본 흐름은 파트(40턴 이하, 고르게 나눔)마다 검토 에이전트 1개가 돈다. 이 저장소 실측은 40턴 파트 하나에 약 11만 토큰과 7분이었다. 긴 세션은 `--last N` 으로 범위를 줄이거나 `--rulebase` 를 쓴다.
329
338
  - Claude Code 검토자(`Plan`)는 Write·Edit 가 도구로 막혀 있지만 Bash 는 있다. 셸로 쓰지 않는 것은 지시로 선다. banker 는 에이전트 정의를 배포하지 않아(모든 표면이 두 런타임 대상) 도구를 더 막을 수 없다.
330
339
  - Codex 의 `workspace-write` 샌드박스에서는 `~/.cache` 에 쓸 수 없어 이번 실행의 검토 결정을 캐시에 쓰지 못한다(WARN 한 번). 앞선 실행이 남긴 결정은 읽으므로, 그 결정이 없는 턴만 다시 검토한다.
340
+ - 캐시를 쓰려면 4단계(`--apply-review`) 명령을 샌드박스 밖 실행으로 승인을 요청하거나, Codex 를 `--add-dir ~/.cache/lineage` 로 시작한다(설정으로는 `sandbox_workspace_write.writable_roots`). 폴더가 없으면 샌드박스 밖에서 먼저 만든다. 쓰기 허용 폴더로 캐시가 저장되는 것은 Codex 0.144.5 에서 확인했다.
331
341
  - 규칙 요약은 가린 본문에서 자른다(요약기 버전 4). lineage 3.0.8 까지 만든 요약 캐시는 쓰지 않는다. 그 캐시에는 평문 조각이 남아 있을 수 있어 `--purge-cache` 로 지운다.
332
342
  - 규칙 요약 캐시는 본문만으로 키를 잡는다. `--redact-mode` 나 `--redact-extra` 를 바꿔 다시 돌린 `--rulebase` 는 앞선 실행의 요약을 쓸 수 있다(2.x 동작 그대로). `--redact-mode mask` 로 만든 요약은 뒤의 기본 실행에서도 mask(페이지 패턴이 찾은 값의 앞뒤 4글자)를 그대로 보인다. 키워드를 나중에 준 실행은 요약 끝에서 잘린 키워드 조각을 보일 수 있다. 설정을 바꿔 다시 돌릴 때는 `--rebuild-summaries` 를 함께 준다. 기본 흐름의 묶음은 요약을 매번 새로 만든다. 묶음 형식은 `lineage-review/2` 라 0.15.1 이 만든 묶음은 `--emit-review` 부터 다시 한다.
333
343
  - 검토자는 긴 턴의 앞뒤만 본다(`clipped: true`). 가운데에만 있는 결론은 놓칠 수 있다.
@@ -723,7 +723,68 @@ def encode_cwd(cwd) -> str:
723
723
  return re.sub(r"[^A-Za-z0-9]", "-", str(cwd))
724
724
 
725
725
 
726
+ def codex_sessions_dir():
727
+ """Where Codex keeps its session records (rollouts): <CODEX_HOME>/sessions/YYYY/MM/DD/rollout-*.jsonl."""
728
+ home = os.environ.get("CODEX_HOME") or str(pathlib.Path.home() / ".codex")
729
+ return pathlib.Path(home) / "sessions"
730
+
731
+
732
+ def _codex_thread():
733
+ """This Codex session's id when lineage runs inside Codex (Codex gives its shell CODEX_THREAD_ID, the id at the
734
+ end of the rollout file name), else None."""
735
+ tid = os.environ.get("CODEX_THREAD_ID", "")
736
+ return tid if re.fullmatch(r"[0-9A-Za-z-]{8,64}", tid) else None
737
+
738
+
739
+ def _mtime(path):
740
+ """A file's change time, or 0 for one that went away between the listing and the look."""
741
+ try:
742
+ return path.stat().st_mtime
743
+ except OSError:
744
+ return 0
745
+
746
+
747
+ def _codex_rollouts(pattern):
748
+ try:
749
+ found = list(codex_sessions_dir().glob(f"*/*/*/{pattern}"))
750
+ except OSError:
751
+ return []
752
+ return sorted(found, key=_mtime)
753
+
754
+
755
+ def _codex_meta(path):
756
+ """The session_meta payload a rollout opens with, else None (not a Codex rollout, or unreadable). Only a regular
757
+ file is opened, so a named pipe is not waited on."""
758
+ try:
759
+ if not stat.S_ISREG(os.stat(str(path)).st_mode):
760
+ return None
761
+ with open(path, encoding="utf-8") as f:
762
+ obj = json.loads(f.readline())
763
+ return obj["payload"] if obj.get("type") == "session_meta" and isinstance(obj.get("payload"), dict) else None
764
+ except (OSError, UnicodeDecodeError, ValueError, RecursionError, KeyError, TypeError, AttributeError):
765
+ return None
766
+
767
+
768
+ def _codex_cwd(path):
769
+ """The cwd a rollout's first record (session_meta) names, else None. A subagent's rollout (its source names
770
+ the parent thread) gives None: what the parent handed it reads as a user turn, so it is no session of the user."""
771
+ try:
772
+ with open(path, encoding="utf-8") as f:
773
+ obj = json.loads(f.readline())
774
+ meta = obj["payload"] if obj.get("type") == "session_meta" else {}
775
+ source = meta.get("source")
776
+ if isinstance(source, dict) and "subagent" in source:
777
+ return None
778
+ return meta.get("cwd")
779
+ except (OSError, UnicodeDecodeError, ValueError, RecursionError, KeyError, TypeError, AttributeError):
780
+ return None
781
+
782
+
726
783
  def auto_discover_jsonl():
784
+ tid = _codex_thread()
785
+ if tid: # inside Codex: this session or nothing, never a Claude Code one
786
+ found = _codex_rollouts(f"rollout-*-{tid}.jsonl")
787
+ return found[-1] if found else None
727
788
  projdir = pathlib.Path.home() / ".claude" / "projects" / encode_cwd(
728
789
  pathlib.Path.cwd())
729
790
  if not projdir.exists():
@@ -737,7 +798,10 @@ def auto_discover_jsonl():
737
798
 
738
799
 
739
800
  def project_jsonl_files():
740
- """All *.jsonl in the encoded project dir, for --all-sessions."""
801
+ """All *.jsonl in the encoded project dir, for --all-sessions. Inside Codex, the Codex sessions of this folder."""
802
+ if _codex_thread():
803
+ here = str(pathlib.Path.cwd())
804
+ return [p for p in _codex_rollouts("rollout-*.jsonl") if _codex_cwd(p) == here]
741
805
  projdir = pathlib.Path.home() / ".claude" / "projects" / encode_cwd(
742
806
  pathlib.Path.cwd())
743
807
  if not projdir.exists():
@@ -909,6 +973,74 @@ def _content_parts(content, line_no):
909
973
  return text_parts, tools
910
974
 
911
975
 
976
+ def _is_codex_meta(obj):
977
+ """Whether a record opens a Codex session record (rollout): a session_meta with a payload."""
978
+ return obj.get("type") == "session_meta" and isinstance(obj.get("payload"), dict)
979
+
980
+
981
+ _CODEX_ITEM_ROLE = {"UserMessage": "user", "AgentMessage": "assistant"}
982
+ _CODEX_INNER_TOOL_RE = re.compile(r"\btools\.([A-Za-z_][A-Za-z0-9_]{0,63})\(")
983
+
984
+
985
+ def _codex_item_text(item):
986
+ """The text parts of a completed item (Codex 0.160: content [{type: text|Text, text}])."""
987
+ content = item.get("content")
988
+ if not isinstance(content, list):
989
+ return ""
990
+ return "\n".join(c["text"] for c in content if isinstance(c, dict) and isinstance(c.get("text"), str))
991
+
992
+
993
+ def _codex_tools(p, pt):
994
+ """Tool counts of one call record. Codex 0.160 runs tools from code (`exec` with `tools.exec_command(...)` in its
995
+ input), so the inner tool names count, not the wrapper."""
996
+ name = p.get("name")
997
+ name = name if isinstance(name, str) and name else pt[:-5]
998
+ inner = _CODEX_INNER_TOOL_RE.findall(p.get("input")) if isinstance(p.get("input"), str) else []
999
+ counts = {}
1000
+ for n in inner or [name]:
1001
+ counts[n] = counts.get(n, 0) + 1
1002
+ return counts
1003
+
1004
+
1005
+ def _codex_record(obj):
1006
+ """(role, text, tools) a Codex rollout record shows as a turn, else None. What the user typed and what the
1007
+ model said come from the event records (0.144: user_message, agent_message; 0.160: item_completed with a
1008
+ UserMessage or AgentMessage item), so what Codex puts into the model input (environment, instructions, an
1009
+ injected skill body) is not a turn; a tool call is an assistant record with no text, merged into the turn around
1010
+ it like a Claude Code tool_use; a compaction is a divider."""
1011
+ p = obj.get("payload")
1012
+ kind = obj.get("type")
1013
+ if kind == "compacted":
1014
+ return "mark", "compaction", {}
1015
+ if not isinstance(p, dict):
1016
+ return None
1017
+ pt = p.get("type")
1018
+ if kind == "event_msg" and pt in ("user_message", "agent_message"):
1019
+ msg = p.get("message")
1020
+ return ("user" if pt == "user_message" else "assistant"), (msg if isinstance(msg, str) else ""), {}
1021
+ item = p.get("item")
1022
+ if kind == "event_msg" and pt == "item_completed" and isinstance(item, dict) and item.get("type") in _CODEX_ITEM_ROLE:
1023
+ return _CODEX_ITEM_ROLE[item["type"]], _codex_item_text(item), {}
1024
+ if kind == "response_item" and isinstance(pt, str) and pt.endswith("_call"):
1025
+ return "assistant", "", _codex_tools(p, pt)
1026
+ return None
1027
+
1028
+
1029
+ def _codex_turn(obj, line_no, session_id, session_name):
1030
+ """The turn dict of one Codex rollout record, else None."""
1031
+ got = _codex_record(obj)
1032
+ if got is None:
1033
+ return None
1034
+ role, text, tools = got
1035
+ text = _CTRL_RE.sub("", text.strip())
1036
+ if not text and not tools:
1037
+ return None
1038
+ role, text, tools, ts, uuid_ = _clean_strings(line_no, role, text, tools, _stamp(obj.get("timestamp")), None)
1039
+ uuid_ = _turn_id(uuid_, line_no, ts)
1040
+ return {"role": role, "text": text, "ts": ts, "uuid": uuid_, "tools": tools, "line_no": line_no,
1041
+ "parts": [text] if text else [], "session": session_id, "session_name": session_name, "meta": False}
1042
+
1043
+
912
1044
  def parse_turns(stream, unsafe_schema: bool = False, session_id=None,
913
1045
  session_name=None):
914
1046
  """Yield dicts: {role, text, ts, uuid, tools, line_no, parts, session,
@@ -919,11 +1051,23 @@ def parse_turns(stream, unsafe_schema: bool = False, session_id=None,
919
1051
  needs it) and `session` (session guards for merge/echo).
920
1052
  """
921
1053
  seen_unknown_type = set()
1054
+ codex = None # set by the first record: a Codex rollout or a Claude transcript
1055
+ last_said = None # Codex: (role, text) of the last message, so one said twice counts once
922
1056
  for line_no, raw in enumerate(stream, 1):
923
1057
  raw = raw.strip()
924
1058
  if not raw:
925
1059
  continue
926
1060
  obj = _json_object(raw, line_no)
1061
+ if obj is not None and codex is None:
1062
+ codex = _is_codex_meta(obj)
1063
+ if obj is not None and codex:
1064
+ turn = _codex_turn(obj, line_no, session_id, session_name)
1065
+ said = (turn["role"], turn["text"]) if turn and turn["text"] else None
1066
+ if turn and not (said and said == last_said):
1067
+ yield turn
1068
+ if said:
1069
+ last_said = said
1070
+ continue
927
1071
  if obj is None or not _is_turn_record(obj, seen_unknown_type, unsafe_schema, line_no):
928
1072
  continue
929
1073
  msg = obj.get("message") or {}
@@ -2713,8 +2857,13 @@ def _read_all_sessions(args):
2713
2857
  starts = {p: session_start(p) for p in files}
2714
2858
  turns = []
2715
2859
  for p in files:
2716
- name = discover_session_name(p) or p.stem[:8]
2860
+ name = discover_session_name(p) or (p.stem[-36:] if p.stem.startswith("rollout-") else p.stem)[:8]
2717
2861
  part = _load_turns_from(p, args.unsafe_schema, p.stem, name)
2862
+ meta = _codex_meta(p)
2863
+ if meta is not None and not any(t["role"] == "user" for t in part):
2864
+ print(f"[lineage] WARN: {p.name}: no user turn read from this Codex {meta.get('cli_version', '?')} "
2865
+ "record (a new record shape?); left out", file=sys.stderr)
2866
+ continue
2718
2867
  fill_missing_ts(part)
2719
2868
  for t in part:
2720
2869
  t["session_ts"] = starts[p] or "9999"
@@ -2756,8 +2905,23 @@ def _read_discovered(args):
2756
2905
 
2757
2906
 
2758
2907
  def _read_input(args):
2759
- """(exit code or None, raw turns, session id, jsonl path) from the source the flags name. The code is 2 when
2760
- there is no source, or the code a schema marker stopped the read with."""
2908
+ """(exit code or None, raw turns, session id, jsonl path) from the source the flags name: a Claude Code
2909
+ transcript or a Codex session record (rollout), told apart by its first record. The code is 2 when there is no
2910
+ source, or the code a schema marker stopped the read with. A Codex rollout that gives no user turn stops with 2
2911
+ too: every Codex session starts with what the user typed, so none means this Codex version writes its record in a
2912
+ shape lineage does not read, and an empty page would hide that."""
2913
+ rc, turns, session_id, jsonl_path = _read_source(args)
2914
+ meta = _codex_meta(jsonl_path) if rc is None and jsonl_path is not None else None
2915
+ if meta is not None and not any(t["role"] == "user" for t in turns):
2916
+ print(f"[lineage] ERROR: {jsonl_path.name} is a Codex {meta.get('cli_version', '?')} session record, but no "
2917
+ "user turn was read from it; this Codex version may write its record in a new shape "
2918
+ "(nothing written; update banker or report it)", file=sys.stderr)
2919
+ return 2, [], session_id, None
2920
+ return rc, turns, session_id, jsonl_path
2921
+
2922
+
2923
+ def _read_source(args):
2924
+ """(exit code or None, raw turns, session id, jsonl path) as read, before the Codex shape check."""
2761
2925
  try:
2762
2926
  if args.all_sessions:
2763
2927
  return _read_all_sessions(args)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: obsidizer
3
- description: "AI 가 만든 마크다운 위키를, 의미를 보존한 채 Obsidian 지식 그래프로 정규화하며, 링크와 백링크를 연결. `--enable`, `--disable` 로 위키를 만들 때마다 자동 정규화할지 켜고 끔(Claude Code 만). 'obsidizer'/'옵시디언화'/'obsidian 최적화'/'위키 그래프 정리' 시 사용."
3
+ description: "AI 가 만든 마크다운 위키를, 의미를 보존한 채 Obsidian 지식 그래프로 정규화하며, 링크와 백링크를 연결. `--enable`, `--disable` 로 위키를 만들 때마다 자동 정규화할지 켜고 끔(Codex 는 훅 설치에 승인 필요). 'obsidizer'/'옵시디언화'/'obsidian 최적화'/'위키 그래프 정리' 시 사용."
4
4
  ---
5
5
 
6
6
  # obsidizer — AI 위키를 Obsidian 지식그래프로 정규화
@@ -23,9 +23,11 @@ AI가 생성한 마크다운 위키를 **의미 보존**한 채 그래프 관점
23
23
  경로를 **품질 근거로 거부한다**(불가능이 아니라 선택): (1) soft·assistant-honored일 뿐 강제가
24
24
  아님, (2) 미리보기 없는 자동 변형 → dry-run-기본 원칙과 정면 충돌, (3) 자동 경로에 LLM 판단이
25
25
  들어가 결정성 축이 깨짐, (4) 세션을 가로챔(저장만 요청했는데 그래프 큐레이션이 끼어듦).
26
- - **런타임 — Claude 전용.** Codex CLI엔 MCP 도구 hook 자체가 없다(PostToolUse=Bash·apply_patch
27
- 만). `--enable`/`--disable`은 Codex에서 **정직한 no-op**: 명확한 메시지만 내고, 아무도 읽지
28
- 않을 플래그를 쓰지 않으며, 성공을 보고하지도 않는다.
26
+ - **런타임 — Codex 는 `omx wiki` 셸 호출에 건다.** Codex 에는 위키 MCP 가 없고 위키 쓰기는 `omx wiki`
27
+ CLI 셸 호출이다. 그래서 Codex 의 hook 은 `PostToolUse`(matcher `Bash`)로 걸려, 명령이 `omx wiki`
28
+ 의 ingest 나 add 일 때만 그 출력 JSON 이 가리키는 `omx_wiki/` 페이지를 정규화한다. 다른 셸 호출은
29
+ 입력만 읽고 끝난다. 이 hook 은 Codex 사용자 범위(`~/.codex/hooks.json`)에 설치되어 모든 프로젝트의
30
+ 셸 호출 뒤에 node 하나를 띄우므로, `--enable` 한 때만 설치한다.
29
31
  - **🔴 커버리지 — session-log 페이지는 절대 걸리지 않는다.** `session-hooks.ts:133`이 MCP 도구
30
32
  없이 `writePageUnsafe`를 직접 호출 → `PostToolUse` 이벤트 자체가 없다 → hook이 **영원히 안
31
33
  켜진다**. session-log는 이 vault에서 **가장 흔한 페이지 유형**(오늘 12개 중 3개, 세션마다 +1).
@@ -42,20 +44,21 @@ AI가 생성한 마크다운 위키를 **의미 보존**한 채 그래프 관점
42
44
  - **엔진은 동일**: Claude·Codex 모두 LLM이 파일을 직접 편집(Read/Glob/Edit/Write)하고, 마지막에
43
45
  `obsidize.mjs`(zero-dep 순수 Node 스크립트)가 기계적 패스로 마무리한다. `wiki_*` 쓰기 의존이
44
46
  없어 두 런타임에 동형으로 동작한다 — `target:both`가 정직한 이유.
45
- - **기본 타깃이 다르다**: Claude → 감지된 `.omc/wiki/`. Codex/OMX → 사용자가 지정한 vault 경로
46
- (자동 감지 대상이 없음).
47
+ - **기본 타깃이 다르다**: Claude → 감지된 `.omc/wiki/`. Codex/OMX → 작업 폴더의 `omx_wiki/`
48
+ (OMX 위키 CLI 가 쓰는 곳. OMC 와 같은 9-필드 프론트매터와 reserved files). 없으면 사용자가 지정한
49
+ vault 경로.
47
50
  - **제약 프로파일이 조직 축이다.** 타깃을 감지 즉시 **OMC-managed 위키**(`index.md`+`log.md`+
48
51
  9-필드 프론트매터 시그니처)와 **일반 Obsidian vault**로 분류하고, 이 분류가 이후 전부를
49
52
  게이팅한다: 프론트매터 envelope(9-key 한정 vs 자유) · 위키링크 형식(bare-only vs piped 허용) ·
50
53
  reserved files · no-rename · `aliases`(OMC=거부, 일반=작성).
51
54
  - **검증은 읽기 전용**: OMC-managed일 때만 `wiki_lint`를 ToolSearch로 로드해 before/after 베이스라인
52
- 대조에 쓴다. **쓰기 의존은 없다** — `wiki_ingest`/`wiki_add`를 호출하지 않고 파일을 직접
55
+ 대조에 쓴다. Codex 는 `omx wiki wiki_lint --input '{}' --json`. **쓰기 의존은 없다** — `wiki_ingest`/`wiki_add`를 호출하지 않고 파일을 직접
53
56
  정규화한다.
54
57
 
55
58
  ## 모드
56
59
 
57
60
  **1) bare `obsidizer [path]` — 리뷰 & 수정 (기본 = dry-run)**
58
- 1. **Phase 0 — 타깃 감지**: 경로 인자 우선, 없으면 `.omc/wiki/` 감지, 둘 다 없으면 중단. 프로파일
61
+ 1. **Phase 0 — 타깃 감지**: 경로 인자 우선, 없으면 `.omc/wiki/`(Codex 는 `omx_wiki/`) 감지, 둘 다 없으면 중단. 프로파일
59
62
  분류(위 참조). OMC-managed면 `wiki_lint` 베이스라인 기록 + `<wikiDir>/.obsidizer-hook.log` 존재
60
63
  시 읽음.
61
64
  2. **Phase 1 — 전체 vault 스캔(읽기 전용)**: 모든 `.md` glob(reserved 파일은 개념맵용으로만 읽고
@@ -72,12 +75,28 @@ AI가 생성한 마크다운 위키를 **의미 보존**한 채 그래프 관점
72
75
  불변 확인, OMC 프로파일이면 bare-form 전용 grep + PM-1 프론트매터 grep, 캐노니컬라이저 멱등
73
76
  자가검사.
74
77
 
75
- **2) `obsidizer --enable` / `--disable` — 지속 정규화 (Claude 전용, structural)**
78
+ **2) `obsidizer --enable` / `--disable` — 지속 정규화 (structural)**
76
79
  banker의 **첫 플러그인 선언 hook**(`hooks/hooks.json`)이 OMC 위키 쓰기 도구(`wiki_ingest`/
77
80
  `wiki_add`)에 `PostToolUse`로 걸려 항상 등록되어 있다. **`<wikiDir>/.obsidizer` 플래그 파일이
78
81
  스위치**다 — `--enable`은 이 파일을 쓰고 `--disable`은 지운다, 그게 전부다(`settings.json` 변경
79
82
  없음). 플래그가 없으면 hook은 즉시 exit 0. 방금 banker를 설치/갱신한 세션이면 완료 보고에
80
- **`/reload-plugins`** 실행을 안내한다(hot-reload 경로). Codex에서는 위 한계 섹션대로 정직한 no-op.
83
+ **`/reload-plugins`** 실행을 안내한다(hot-reload 경로).
84
+
85
+ Codex 에서는 플래그 자리가 `omx_wiki/.obsidizer` 이고, hook 은 banker 가 Codex 사용자 범위에 따로 설치한다.
86
+
87
+ - `--enable`
88
+ 1. 작업 폴더에 `omx_wiki/` 가 없으면 만들고 `omx_wiki/.obsidizer` 를 쓴다. `omx_wiki/` 는 저장소에
89
+ 넣는 폴더라 이 플래그도 커밋 대상이 될 수 있음을 보고에 적는다.
90
+ 2. `banker hooks status obsidize` 로 hook 상태를 본다. `on` 이면 3을 건너뛴다.
91
+ 3. `banker hooks enable obsidize` 를 실행한다. 이 명령은 `~/.codex/hooks.json` 과
92
+ `~/.codex/hooks-custom/banker/` 에 쓰므로 `workspace-write` 샌드박스 밖에서 돌아야 한다. 그 한 명령만
93
+ 샌드박스 밖 실행 승인을 요청한다. `--add-dir ~/.codex` 는 권하지 않는다(세션이 훅과 신뢰 기록을 고칠 수
94
+ 있게 됨). 승인을 요청할 수 없으면(`codex exec`, 승인 정책 `never`) 실행하지 않고, 사용자가 입력창에
95
+ `! banker hooks enable obsidize` 로 직접 실행하도록 안내한 뒤 멈춘다.
96
+ 4. Codex 를 다시 열 때 뜨는 Hooks need review 창이나 `/hooks` 에서 banker obsidize hook 을 신뢰해야
97
+ 돈다고 보고한다. 신뢰 전에는 플래그가 있어도 정규화되지 않는다.
98
+ - `--disable`: `omx_wiki/.obsidizer` 를 지운다. hook 은 다른 프로젝트의 플래그도 보므로 남겨 두고, 어느
99
+ 프로젝트에서도 쓰지 않으면 `banker hooks disable obsidize` 로 뺄 수 있다고 안내한다(같은 승인 규칙).
81
100
 
82
101
  ## 변환 규칙
83
102
 
@@ -15,7 +15,12 @@
15
15
  * the torn read outright; the CAS only narrows the lost-update window to that
16
16
  * one syscall — a residual race remains, and is accepted (see writeIfUnchanged).
17
17
  *
18
- * Usage: node obsidize.mjs <path> [--profile=auto|omc|generic] [--dry-run] [--json]
18
+ * Usage: node obsidize.mjs <path> [--profile=auto|omc|generic] [--dry-run] [--json] [--page]
19
+ * --page: <path> must be one plain page (a regular file with one name, judged without following a link), and the
20
+ * vault model reads only plain pages. The Codex hook passes it, since Codex runs that hook outside its sandbox.
21
+ * With --page, <path> is a bare page name, the process runs in the vault folder, and --vault=<dev>:<ino> must name
22
+ * that folder: every read and write then goes by a name relative to a cwd fixed on the checked folder, so a folder
23
+ * swapped for a link after the check cannot lead outside it.
19
24
  */
20
25
  import crypto from 'node:crypto';
21
26
  import fs from 'node:fs';
@@ -298,15 +303,32 @@ export function detectProfile(target) {
298
303
  const read = (p) => fs.readFileSync(p, 'utf8');
299
304
 
300
305
  /** Sorted explicitly — readdir order is not deterministic across filesystems. */
301
- function listMarkdown(dir) {
306
+ // Why a --page run may not go on: the page must be a bare name and the cwd the vault folder the caller checked.
307
+ export function pageRunRefusal(name, vault) {
308
+ if (!name || /[\\/]/.test(name) || name === '.' || name === '..') return `--page takes a bare page name: ${name}`;
309
+ const here = fs.statSync('.', { bigint: true });
310
+ return vault === `${here.dev}:${here.ino}` ? '' : '--page runs in the vault folder named by --vault=<dev>:<ino>';
311
+ }
312
+
313
+ // A regular file with one name: not a link, a hard link or a pipe.
314
+ function plain(file) {
315
+ try {
316
+ const st = fs.lstatSync(file);
317
+ return st.isFile() && st.nlink === 1;
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ function listMarkdown(dir, plainOnly = false) {
302
324
  return fs.readdirSync(dir)
303
- .filter((f) => f.endsWith('.md') && !f.endsWith('.excalidraw.md'))
325
+ .filter((f) => f.endsWith('.md') && !f.endsWith('.excalidraw.md') && (!plainOnly || plain(path.join(dir, f))))
304
326
  .sort(byCodeUnit);
305
327
  }
306
328
 
307
329
  /** Slug index + H1 index for the whole vault: alias uniqueness and footer resolvability need both. */
308
- export function buildVaultModel(dir) {
309
- const files = listMarkdown(dir);
330
+ export function buildVaultModel(dir, plainOnly = false) {
331
+ const files = listMarkdown(dir, plainOnly);
310
332
  const basenames = new Set(files);
311
333
  const pages = new Map();
312
334
  const h1Counts = new Map();
@@ -473,12 +495,14 @@ function writeIfUnchanged(file, baseline, text) {
473
495
  }
474
496
  }
475
497
 
476
- export function run(target, { profile = 'auto', dryRun = false, now = null } = {}) {
477
- if (!fs.existsSync(target)) throw new Error(`path not found: ${target}`);
478
- const isFile = fs.statSync(target).isFile();
498
+ export function run(target, { profile = 'auto', dryRun = false, now = null, page = false } = {}) {
499
+ if (!fs.existsSync(target) && !page) throw new Error(`path not found: ${target}`);
500
+ // --page judges with one lstat, so a page swapped for a link after the caller looked is refused, not followed.
501
+ if (page && !plain(target)) throw new Error(`not a plain page: ${target}`);
502
+ const isFile = page || fs.statSync(target).isFile();
479
503
  const dir = isFile ? path.dirname(target) : target;
480
504
  const resolved = profile === 'auto' ? detectProfile(target) : profile;
481
- const model = buildVaultModel(dir);
505
+ const model = buildVaultModel(dir, page);
482
506
  const aliases = computeAliases(model, resolved);
483
507
  const stamp = now || new Date().toISOString();
484
508
 
@@ -528,7 +552,7 @@ function main(argv) {
528
552
  return hit ? hit.slice(name.length + 3) : def;
529
553
  };
530
554
  if (args.length !== 1) {
531
- console.error('usage: obsidize.mjs <path> [--profile=auto|omc|generic] [--dry-run] [--json]');
555
+ console.error('usage: obsidize.mjs <path> [--profile=auto|omc|generic] [--dry-run] [--json] [--page]');
532
556
  return 2;
533
557
  }
534
558
  const profile = opt('profile', 'auto');
@@ -536,7 +560,14 @@ function main(argv) {
536
560
  console.error(`unknown profile: ${profile}`);
537
561
  return 2;
538
562
  }
539
- const report = run(path.resolve(args[0]), { profile, dryRun: flag('dry-run') });
563
+ if (flag('page')) {
564
+ const why = pageRunRefusal(args[0], opt('vault', ''));
565
+ if (why) {
566
+ console.error(why);
567
+ return 2;
568
+ }
569
+ }
570
+ const report = run(flag('page') ? args[0] : path.resolve(args[0]), { profile, dryRun: flag('dry-run'), page: flag('page') });
540
571
  if (flag('json')) {
541
572
  console.log(JSON.stringify(report));
542
573
  } else {
@@ -51,6 +51,12 @@ OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅
51
51
  - 판정은 출력 문구로 한다. 종료 코드 0 만으로 완료라고 보고하지 않는다.
52
52
  - 캐시에는 정션이나 심볼릭 링크로 이어진 버전 폴더가 있을 수 있다(예: 옛 버전 폴더가 새 버전을 가리킴). 이때 한 번의 변경이 두 버전 폴더에 함께 보인다. 정상이다.
53
53
  - 도구는 `os.homedir()/.claude` 를 쓴다. `CLAUDE_CONFIG_DIR` 로 설정 폴더를 옮겼다면 이 도구는 그 폴더를 보지 않는다.
54
+ - Codex 에서 실행하면 `workspace-write` 샌드박스가 `~/.claude` 쓰기를 막는다.
55
+ - 진단(`--check --no-update`)과 `probe-pm.mjs` 는 승인 없이 된다(probe 는 임시 폴더에만 쓴다). 진단의 `쓸 수 없음 (쓸 권한이 없음)` 은 샌드박스 때문이며, 패치가 필요한지의 판정은 그대로 맞다.
56
+ - 적용과 되돌리기는 그 명령을 샌드박스 밖 실행으로 승인을 요청해 돌린다. 승인 없이 돌리면 `실패 N건` 으로 끝나고 아무것도 바뀌지 않는다.
57
+ - 승인한 실행은 Codex 0.144.5 에서 적용, 고정, `PROBE GREEN` 까지 확인했다.
58
+ - 승인을 요청할 수 없거나(승인 정책 `never`) 거부되면, 같은 명령을 Codex 입력창에서 `!` 를 붙여 사용자가 직접 실행하게 안내한다(예: `!node "<경로>" --no-update`). 사용자가 친 `!` 명령은 샌드박스 밖에서 돈다(Codex 0.144.5, `-a never` 에서 실측). `codex exec` 처럼 사람이 없는 실행이면 그 명령을 보고하고 멈춘다.
59
+ - `--add-dir ~/.claude` 처럼 설정 폴더를 통째로 여는 방법은 쓰지 않는다. 그 세션이 훅과 설정을 고칠 수 있게 되어 샌드박스가 무력해진다.
54
60
  - 명령의 자리 표시(`<...>`)에 도구 출력이나 `find` 가 낸 값(경로, 판, 이름)을 넣을 때는 bash, PowerShell 모두 작은따옴표로 감싼다. 값에 작은따옴표(PowerShell 은 `‘` `’` `‚` `‛` 도 작은따옴표로 읽는다)나 줄바꿈이 있으면 명령을 만들지 않고 그 값을 사용자에게 보인다.
55
61
  - 출력의 origin 에 `https://사용자:토큰@...` 처럼 자격증명이 들어 있으면, 보고할 때 `//` 뒤부터 마지막 `@` 까지를 `***` 로 가린다(도구와 같은 범위). 출력을 그대로 보고하라는 단계에서도 이 가림은 지킨다.
56
62
  - 옛 판의 도구가 만든 `PINNED` 에는 그 URL 이 남아 있을 수 있다.
@@ -35,6 +35,8 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
35
35
  - **Codex**: Codex 자신의 요청 크기는 재지 않는다. 32MB 한도와 세션 파일 형식은 Claude Code 의 것이기 때문이다.
36
36
  Codex 에서 실행해도 같은 머신의 Claude Code HUD 래퍼를 켜고 끄는 것만 하고, `status` 의 "현재 세션" 줄은 Claude Code 세션에서만 나온다.
37
37
  사용자가 Codex 쪽 표시를 기대하면 이 점을 먼저 알린다.
38
+ Codex 의 `workspace-write` 샌드박스는 `~/.claude` 쓰기를 막는다. `status` 는 읽기만 해 그대로 된다. `on`, `off` 는 샌드박스 밖 실행으로 승인을 요청해 돌린다.
39
+ 승인 없이 돌리면 종료 1 과 `EROFS` 로 끝나고 아무것도 바뀌지 않는다(승인한 실행은 Codex 0.144.5 에서 확인). 승인을 요청할 수 없거나(승인 정책 `never`) 거부되면, 같은 명령을 Codex 입력창에서 `!` 를 붙여 사용자가 직접 실행하게 안내한다(예: `!node "<경로>" on`). 사용자가 친 `!` 명령은 샌드박스 밖에서 돈다(Codex 0.144.5, `-a never` 에서 실측). `codex exec` 처럼 사람이 없는 실행이면 그 명령을 보고하고 멈춘다. `--add-dir ~/.claude` 처럼 설정 폴더를 통째로 여는 방법은 쓰지 않는다. 그 세션이 훅과 설정을 고칠 수 있게 되어 샌드박스가 무력해진다.
38
40
 
39
41
  ## 동작
40
42
 
@@ -23,7 +23,7 @@ description: "소프트웨어 외주 제안요청서(RFP)를 작성. 한국 공
23
23
  5. **OPEN 정직 표기**: 미검증 항목(예: 망분리/PIPA 조문 현행 호수·LLM 계약문구·LLM eval binary 임계·SoT 전달방식)은 RFP에 **〈OPEN〉**으로 명시하고 추가 리서치/법무 검토 표시. "정합 OK"/"아마도" 금지.
24
24
  6. **verify-before-claim**: 모든 수치·조문·배점은 `references/`로 추적(인용 동반). 완료 전 4-field(① 변경 ② Evidence ③ 검증 ④ Unknown), ⏳=0일 때만 "완료". 커밋/푸시는 사용자 명시 시만.
25
25
  7. **쪽번호·인용 의무**: 디자인/문서 SoT(예: 디자인 데크 PDF의 N쪽, 인용키 `<DesignRef(page-section)>`) 인용 시 **page-NNN/쪽번호 직접 기록**. file:LINE/page 없는 "정합 OK" 금지.
26
- 8. **벤더 노출 게이트**: 발송본엔 현황(As-Is)·내부 SoT file:LINE·실제 호스트/포트·내부식별자(보안필터 제품명/내부 테이블 prefix/모델ID)·"deep-research"/추적박스·em-dash(`—`→`-`) 비포함. 내부 추적은 `references/`에만.
26
+ 8. **벤더 노출 게이트**: 발송본엔 현황(As-Is)·내부 SoT file:LINE·실제 호스트/포트·내부식별자(보안필터 제품명/내부 테이블 prefix/모델ID)·"deep-research"·"lfm"/추적박스·em-dash(`—`→`-`) 비포함. 내부 추적은 `references/`에만.
27
27
 
28
28
  ## 번들 구성
29
29
  | 파일 | 내용 | 신뢰도 |
@@ -39,8 +39,9 @@ description: "소프트웨어 외주 제안요청서(RFP)를 작성. 한국 공
39
39
  ## 워크플로우 (도구 오케스트레이션)
40
40
  RFP 저작은 **저작 lane과 검수 lane을 분리**(자기승인 금지). 권장 순서:
41
41
 
42
+ 0. **전문가 작업 방식 조사** — `lfm` 을 두 분야로 부른다(`--no-questions`). ① 과업의 분야(그 종류의 시스템을 잘 만드는 그룹이 어떤 단계, 산출물, 품질 기준, 검수로 일하는지) ② 소프트웨어 외주 발주와 제안 평가(공공 SW 사업 관리 지침을 내는 기관, 조달 기관 등). 결과는 `references/expert-practices.md` 에 내부 추적용으로 둔다. ①의 계획 적용 항목은 기능, 기술, 보안, 비기능 요건과 산출물, 검수 기준 후보가 되고, ②는 평가 기준, 마일스톤, 검수 절차 후보가 된다. 2단계 합의에서 `M-n` 마다 적용, 조정, 제외를 정한다. 벤더 발송본에는 그룹 이름, 출처, `lfm` 이라는 말을 넣지 않는다(노출 게이트). "웹 조사 불가" 면 그 사실을 `references/expert-practices.md` 에 적고 진행한다.
42
43
  1. **과업내용 추출** — 현 코드/spec → 요구사항. `research`(OMC 5 에서 삭제된 `deep-dive` 의 대체) 또는 agent `feature-dev:code-explorer` 로 화면·API·데이터·외부연계를 SFR/INR/DAR로 도출 → `references/this-project-scope.md` 채움(file:LINE 동반). 디자인은 PDF/이미지 추출 도구로 `<DesignRef(page-section)>` 형태 인용키 추출.
43
- 2. **구조·범위·배점 합의** — `ralplan --deliberate`로 RFP 구조·15유형 범위·평가 정성/정량 배점을 Planner→Architect→Critic 합의(Planner는 SoT word-for-word 인용 의무). broad/risky 결정만.
44
+ 2. **구조·범위·배점 합의** — `ralplan --deliberate`로 RFP 구조·15유형 범위·평가 정성/정량 배점을 Planner→Architect→Critic 합의(Planner는 SoT word-for-word 인용 의무). broad/risky 결정만. `references/expert-practices.md` 의 `M-n` 마다 판정(적용, 조정, 제외)과 이유를 합의에 포함한다.
44
45
  3. **섹션 병렬 작성** — 5+ 섹션이면 `team`(워커별 절 담당), 소규모는 agent `oh-my-claudecode:writer`. 각 절은 `templates/rfp-skeleton.md`를 채우고 `references/`를 인용.
45
46
  4. **미검증 보강** — OPEN 항목(특히 망분리/PIPA/보안성심의/secure-coding)은 `autoresearch`/`deep-research` 또는 agent `document-specialist`로 **추가 리서치 후** SER에 반영. 미보강 시 〈OPEN〉 유지.
46
47
  5. **검수(별도 lane)** — agent `oh-my-claudecode:critic` 또는 `verify`로 ① 반증 항목 미혼입 ② currency(2020 진흥법) ③ 인용 추적성 ④ 기밀 누출 0 ⑤ 배점 합(80+20 또는 채택 배점) 검증.
@@ -48,6 +49,7 @@ RFP 저작은 **저작 lane과 검수 lane을 분리**(자기승인 금지). 권
48
49
 
49
50
  | 작업 | 권장 도구 |
50
51
  |---|---|
52
+ | 전문가 작업 방식(과업 분야, 외주 발주) | `lfm` |
51
53
  | 과업내용 추출(코드→요구) | `research`(구 `deep-dive`) / `feature-dev:code-explorer` / PDF·이미지 추출 도구 |
52
54
  | 구조·배점 합의 | `ralplan --deliberate` |
53
55
  | 섹션 병렬 저작 | `team` / `oh-my-claudecode:writer` |
@@ -67,7 +69,7 @@ RFP 저작은 **저작 lane과 검수 lane을 분리**(자기승인 금지). 권
67
69
  - **source-strength 혼동**: 한국 규제·NIST=primary, 국제 계약절=template+법무 secondary(한국법 검토 전 mandate처럼 단정 금지). IP-assignment default는 반대.
68
70
  - **OPEN을 채운 척**: 미검증 항목을 합성으로 메우지 말 것. 〈OPEN〉 + 추가 리서치 경로 표기.
69
71
  - **배포 환경 혼동**: 운영 타깃 구성(웹서버/WAS/HTTP 버전 등)을 ECR로, dev simple-run divergence는 참고로만(this-project §운영/dev 구분).
70
- - **벤더 문서 내부누출**: 요건 발의형 벤더 RFP에 내부 SoT `file:LINE`·실제 호스트/포트·내부식별자(보안필터 제품명·내부 테이블 prefix·모델ID)·"deep-research"/추적박스 **절대 비포함**. 내부 추적은 `references/`에만(검수 lane grep).
72
+ - **벤더 문서 내부누출**: 요건 발의형 벤더 RFP에 내부 SoT `file:LINE`·실제 호스트/포트·내부식별자(보안필터 제품명·내부 테이블 prefix·모델ID)·"deep-research"·"lfm"/추적박스 **절대 비포함**. 내부 추적은 `references/`에만(검수 lane grep).
71
73
  - **md→docx 표 균등폭**: §산출물 gridCol 함정 — `cell.width`만 set 시 표가 균등폭으로 렌더. `w:gridCol@w:w` 직접 set.
72
74
 
73
75
  References: `references/*` (5종 — vendor-rfp-structure 포함) · `templates/rfp-skeleton.md` · `scripts/md2docx.py`.
@@ -32,7 +32,7 @@
32
32
  - 전수 상세(인벤토리·카탈로그·시나리오)는 **착수 후 제공 자료**(§10) — 기밀은 격리망/마스킹.
33
33
 
34
34
  ## 4. ★노출 게이트 (벤더 발송본 = 깨끗)
35
- **절대 비포함**(검수 lane grep): 내부 SoT `file:LINE`(`<spec/wiki 경로:NN>`) · 실제 호스트/포트(`<internal-host>`·`<:port>`) · 내부식별자(보안필터 제품명·내부 테이블 prefix·내부 데이터셋명·모델ID) · `.env.*` 값 · "deep-research"/provenance/"추적박스" · em-dash(`—`→`-`). 내부 추적은 본 `references/`에만.
35
+ **절대 비포함**(검수 lane grep): 내부 SoT `file:LINE`(`<spec/wiki 경로:NN>`) · 실제 호스트/포트(`<internal-host>`·`<:port>`) · 내부식별자(보안필터 제품명·내부 테이블 prefix·내부 데이터셋명·모델ID) · `.env.*` 값 · "deep-research"/"lfm"/provenance/"추적박스"/전문가 그룹 이름과 출처 · em-dash(`—`→`-`). 내부 추적은 본 `references/`에만.
36
36
 
37
37
  ## 5. 쪽번호·인용 의무
38
38
  디자인/문서 인용 시 **page-NNN/쪽번호 직접 기록**. 디자인 SoT(예: 디자인 PDF/이미지, 인용키 `<DesignRef(page-section)>`)는 페이지 번호를 직접 박는다. "정합 OK" 같은 빈말 금지.