@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/PRIVACY.md +21 -1
- package/README.md +22 -14
- package/bin/banker.js +144 -11
- package/bin/lib/codex-hooks.mjs +249 -0
- package/bin/lib/telemetry-config.mjs +2 -0
- package/codex/manifest.json +13 -10
- package/codex/skills/graceful-pause/SKILL.md +30 -0
- package/codex/skills/progress/SKILL.md +42 -0
- package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
- package/codex/skills/progress/scripts/progress.mjs +295 -0
- package/codex/transform-matrix.md +18 -13
- package/hooks/obsidize-hook.mjs +116 -4
- package/hooks/progress-plan.mjs +1 -1
- package/hooks/telemetry-count-codex.mjs +222 -0
- package/hooks/update-notify.mjs +31 -11
- package/package.json +1 -1
- package/skills/append_wiki/SKILL.md +34 -8
- package/skills/compact-wiki/SKILL.md +37 -6
- package/skills/deep-research/SKILL.md +6 -0
- package/skills/lfm/SKILL.md +148 -0
- package/skills/lineage/SKILL.md +14 -4
- package/skills/lineage/lineage.py +168 -4
- package/skills/obsidizer/SKILL.md +29 -10
- package/skills/obsidizer/obsidize.mjs +42 -11
- package/skills/omc-patch/SKILL.md +6 -0
- package/skills/payload-mon/SKILL.md +2 -0
- package/skills/rfp-author/SKILL.md +5 -3
- package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
- package/skills/setup-bypass-permissions/SKILL.md +27 -9
- package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/SKILL.md +33 -3
- package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
- package/skills/smart-compact/SKILL.md +58 -12
- package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
- package/skills/ultra-chain/SKILL.md +8 -4
- package/skills/ultra-interview/SKILL.md +13 -2
package/skills/lineage/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: lineage
|
|
3
3
|
description: "현재 세션 대화를 카카오톡 스타일의 단일 HTML 로 내보냄. Claude 답변은 한 줄 요약 아래에 원문을 접어 두고, 펼치면 마크다운으로 렌더해 보여 줌. 기본은 세션 모델이 요약을 쓰고 노이즈 턴을 걸러 내며, `--rulebase` 는 모델 없이 규칙만 씀. 여러 세션을 합칠 수 있음. 'lineage'/'대화 export'/'카톡 스타일 html' 시 사용."
|
|
4
4
|
invocation: /lineage
|
|
5
|
-
version: 3.
|
|
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 에서는
|
|
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 (
|
|
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
|
-
-
|
|
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
|
|
2760
|
-
|
|
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` 로 위키를 만들 때마다 자동 정규화할지 켜고 끔(
|
|
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
|
-
- **런타임 —
|
|
27
|
-
|
|
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 →
|
|
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` — 지속 정규화 (
|
|
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 경로).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" 같은 빈말 금지.
|