@wooojin/forgen 0.5.5 → 0.5.6

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://claude.ai/schemas/claude-plugin.json",
3
3
  "name": "forgen",
4
- "version": "0.5.5",
4
+ "version": "0.5.6",
5
5
  "description": "Claude Code harness — the more you use Claude, the better it gets",
6
6
  "author": {
7
7
  "name": "jang-ujin",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.6] — 2026-10-02 — Codex notify 폴백 · 훅 번들(재승인 2건) · Claude verify 스킬 (ADR-016)
11
+
12
+ > **Codex 사용자 — 재승인 필요**: 업그레이드 후 `forgen install codex` 를 다시 실행하면 훅 2개
13
+ > (`session_start` 변경, `session_end` 신규)가 Codex `/hooks` 승인 전까지 skip 된다. 그동안 Codex 세션에
14
+ > `<forgen-rules>` 블록이 주입되지 않는다. install 출력과 `forgen doctor` 가 대상 훅을 표시한다.
15
+
16
+ ### Added
17
+ - **Codex `notify` 폴백** (`dist/host/codex-notify.js`). `forgen install codex` 가 config.toml 최상단에 마커 블록으로
18
+ `notify` 를 등록한다. Codex 의 notify 는 훅 신뢰와 무관하게 턴 완료마다 detached 로 실행되므로, forgen 훅이
19
+ 미승인/변경 상태로 조용히 skip 되는 동안에도 (a) `state/codex-hooks-silent.json` 에 관측을 남겨 `forgen doctor`
20
+ [Codex Hooks] 가 보여주고 (b) 프롬프트 ≥10 인 세션은 Stop 훅과 같은 디바운스 경로로 auto-compound 를 띄운다.
21
+ 훅이 정상 실행 중이면(codex-adapter 의 alive 마커) 아무것도 하지 않는다. 사용자가 이미 `notify` 를 정의했으면
22
+ **건드리지 않는다** (수동 체인: argv 뒤에 `"--", "<program>", …`). 끄기: `--no-notify` (기존 블록도 제거).
23
+ `forgen uninstall` 도 블록을 제거한다. Codex 가 블록 사이에 써 넣은 root 키(`model` 등)와 BOM/CRLF 는 보존한다.
24
+ - **Codex `SessionEnd` 훅** — `session-end` 를 Codex 에도 등록. Stop 없이 끝나는 세션의 학습 추출 트리거.
25
+ Codex rollout 의 실제 사용자 프롬프트(`event_msg`/`user_message`)를 raw 바이트 스캔으로 센다.
26
+ - **Claude `verify` 스킬** — `forgen install claude` 가 `~/.claude/skills/verify/SKILL.md` 를 설치한다. Claude Code
27
+ 2.1.286+ 는 project/user 스킬에 `verify` 가 있으면 코드 커밋 직전에 실행하도록 모델에 안내한다 (플러그인 스킬
28
+ `forgen:verify` 는 대상이 아님). 본문은 "프로젝트 자체 레시피 우선 → 실제 build/type-check/lint/test 실행 →
29
+ confirmed / refuted / unverified 판정, mock 통과는 증거 아님". 사용자가 만든 `verify` 스킬은 덮어쓰지 않고,
30
+ `forgen uninstall` 은 forgen 이 설치한 것만 제거한다. 끄기: `--no-verify-skill`. (npm postinstall 은 설치하지 않음 —
31
+ 명시적 `forgen install claude` 에서만.)
32
+ - `forgen install` 플래그 `--no-notify`, `--no-verify-skill`.
33
+
34
+ ### Fixed
35
+ - **`forgen install codex` 재실행이 Codex 훅 신뢰를 전부 지우던 결함 (0.5.3~).** 신규 설치는 MCP 마커 블록을
36
+ config.toml 끝에 붙이는데, Codex 는 `/hooks` 승인 시 `[hooks.state]` 테이블을 파일 끝 주석(= forgen 의 END 마커)
37
+ *앞* 에 써 넣는다 — 즉 블록 안. 재설치가 블록을 통째로 교체하면서 22개 신뢰 기록, `[features]`, MCP 서버의
38
+ `enabled = false` 등이 사라졌다. 이제 forgen 이 쓴 줄만 다시 쓰고 사이에 끼어든 내용은 블록 뒤로 옮겨 보존한다.
39
+ (실 Codex 0.153.4 app-server 로 재현·수정 확인: 재설치 후 22/22 유지.)
40
+ - Codex 추출 run(`codex exec --ephemeral`)에 `FORGEN_NESTED_RUN=1` 이 전달되지 않던 것 — forgen 훅이 추출 세션에서
41
+ 발화하지 않도록 Claude 분기와 동일하게 표식.
42
+ - `[mcp_servers.forgen-compound]` 가 마커 없이 이미 있으면 중복 테이블을 append 하지 않는다.
43
+
44
+ ### Changed
45
+ - **Codex 훅 신뢰 감사가 해시를 대조한다.** `auditCodexHookTrust` 가 Codex 0.153.4 의 핸들러 단위 trust 해시를
46
+ 계산해 `trusted` / `modified` / `untrusted` 를 구분한다. 이전엔 `hooks.state` 키의 존재만 봐서, 핸들러가 바뀌어
47
+ Codex 가 skip 하는 훅을 "trusted" 로 표시했다. `/hooks` 에서 끈 훅(`enabled = false`)은 `disabled` 로 따로 센다.
48
+ 읽기 전용 대조이며 신뢰 기록은 쓰지 않는다.
49
+ - **`session-recovery` 의 Codex 핸들러에 `additionalContextLimit: 0`.** Codex 는 약 10KB 를 넘는
50
+ `additionalContext` 를 임시 파일로 스필하고 모델에는 잘린 미리보기만 준다 — 한국어 룰 블록(상한 15,000자)은 이를
51
+ 쉽게 넘는다.
52
+ - `post-tool-failure` 를 Codex hooks.json 에서 제외 (`PostToolUseFailure` 는 Codex 이벤트가 아니라 무시되던 죽은 엔트리).
53
+ - `forgen install codex` 는 config.toml 내용이 바뀔 때만 파일을 쓴다.
54
+ - CI: 태그 푸시 발행을 npm Trusted Publishing(OIDC) 으로 전환, `release.yml`/`compat.yml` 에 누락됐던
55
+ `hooks/hooks.json` 생성 단계 추가 (v0.5.0 이후 태그 발행이 계속 실패하던 원인).
56
+
57
+ ### Not done (정직 표기 — ADR-016)
58
+ - `async: true` 훅: Codex 는 같은 이벤트의 핸들러를 이미 동시 실행한다. 어댑터 경유 훅 1회 91~137ms 실측 →
59
+ 이득 상한 ≈130ms/이벤트. 반면 async 는 block/deny 가 적용되지 않고 핸들러마다 재승인이 든다.
60
+ - `PostCompact` / `Interrupt` 등록: 둘 다 컨텍스트 주입이 불가능하고 forgen 이 거기서 할 일이 없다.
61
+ - 사용자 `notify` 자동 체인: TOML 임의 배열 재작성 + 원복 보장 불가로 수동 체인 안내만 제공.
62
+
63
+ ### Verified
64
+ - vitest 3161 통과 (신규: trust 해시·notify 블록 upsert·notify 폴백 분기·rollout 카운터·verify 스킬 install/uninstall).
65
+ - trust 해시: 실머신 `~/.codex/config.toml` 의 trusted_hash 20건과 일치 (fixture 8건 vendoring), 격리 `CODEX_HOME` 에서
66
+ forgen 계산 해시를 기록한 뒤 Codex `hooks/list` 가 22/22 `trusted` 로 판정 (`additionalContextLimit:0`·SessionEnd 포함).
67
+ - 격리 Codex 0.153.4 실세션 (`codex exec`):
68
+ - 훅 미승인: notify 발화 → silent 기록, `forgen doctor` 가 "forgen 훅 미발화" 표시.
69
+ - 훅 승인: alive 마커(Stop) 기록 → notify 가 silent 를 지움, hook-timing 에 `SessionEnd:session-end (rt: codex)`.
70
+ - 스필: 21KB SessionStart 컨텍스트 — 기본값은 "Full hook output saved to" + 중간 내용 미전달, `additionalContextLimit:0`
71
+ 은 전문 전달.
72
+ - notify 폴백의 auto-compound 트리거: 실 바이너리 + 12-프롬프트 rollout 으로 러너 인자(cwd, rollout, session id, 12)와
73
+ `FORGEN_RUNTIME=codex` 전달, 재호출 시 in-flight 게이트로 skip 확인. (러너 자체는 스텁으로 대체 — LLM 추출은 이 검증 범위 밖.)
74
+ - fresh-context critic 리뷰 1라운드: CRITICAL 2 · MAJOR 1 · MINOR 10 → 위 Fixed 항목 포함 전부 반영 또는 한계로 명시
75
+ (ADR-016 Review 절). 리뷰어의 실 Codex 재현 스크립트를 수정 후 빌드에 다시 돌려 통과 확인.
76
+ - 이 머신의 실 `~/.codex` 에는 아직 설치하지 않았다 (재승인 전까지 룰 주입이 멈추므로 오너 결정 후).
77
+
78
+
10
79
  ## [0.5.5] — 2026-10-01 — Codex Stop 분기 복구 (Stop 트리거 auto-compound·finalizeSession)
11
80
 
12
81
  ### Fixed
package/README.ja.md CHANGED
@@ -192,6 +192,10 @@ forgen install both # 3択インタラクティブ: claude / codex / bo
192
192
  forgen install claude
193
193
  forgen install codex
194
194
  # Codex のみ: codex 内で `/hooks` から forgen フックを一度承認 — Codex は未承認フックをスキップ (forgen doctor の [Codex Hooks] で確認)
195
+ # v0.5.6 へのアップグレード: `forgen install codex` を再実行するとフック 2 件 (session_start, session_end) が変わる — `/hooks` でもう一度承認。
196
+ # config.toml に `notify` フォールバックも登録 (既に `notify` を定義していれば変更しない; 無効化: --no-notify)。
197
+ # Claude のみ: `verify` スキルを ~/.claude/skills/verify にインストール — Claude Code がコードのコミット直前に実行
198
+ # (自作の `verify` スキルは上書きしない; 無効化: --no-verify-skill)。
195
199
 
196
200
  # 3. 初回実行 — 4問オンボーディング (英語/韓国語選択)
197
201
  forgen # デフォルト: Claude
package/README.ko.md CHANGED
@@ -152,6 +152,10 @@ forgen install both # 3지선다 인터랙티브: claude / codex / both
152
152
  forgen install claude
153
153
  forgen install codex
154
154
  # Codex 만: codex 안에서 `/hooks` 로 forgen 훅을 한 번 승인 — Codex 는 미승인 훅을 skip (forgen doctor 의 [Codex Hooks] 로 확인)
155
+ # v0.5.6 업그레이드: `forgen install codex` 를 다시 돌리면 훅 2개(session_start, session_end)가 바뀜 — `/hooks` 에서 한 번 더 승인.
156
+ # config.toml 에 `notify` 폴백도 등록됨 (이미 `notify` 를 쓰고 있으면 건드리지 않음; 끄기: --no-notify).
157
+ # Claude 만: `verify` 스킬을 ~/.claude/skills/verify 에 설치 — Claude Code 가 코드 커밋 직전에 실행
158
+ # (직접 만든 `verify` 스킬은 덮어쓰지 않음; 끄기: --no-verify-skill).
155
159
 
156
160
  # 3. 첫 실행 — 4문항 온보딩 (영어/한국어 선택)
157
161
  forgen # 기본: Claude
@@ -670,7 +674,7 @@ forgen는 설치 시 다른 Claude Code 플러그인(oh-my-claudecode, superpowe
670
674
 
671
675
  | 문서 | 설명 |
672
676
  |------|------|
673
- | [훅 레퍼런스](docs/reference/hooks-reference.md) | 3개 계층의 19개 훅 — 이벤트, 타임아웃, 동작 |
677
+ | [훅 레퍼런스](docs/reference/hooks-reference.md) | 3개 계층의 23개 훅 — 이벤트, 타임아웃, 동작 |
674
678
  | [공존 가이드](docs/guides/with-omc.md) | oh-my-claudecode와 forgen 함께 사용하기 |
675
679
  | [CHANGELOG](CHANGELOG.md) | 버전 히스토리 및 릴리즈 노트 |
676
680
 
package/README.md CHANGED
@@ -245,6 +245,10 @@ forgen install both # 3-choice interactive: claude / codex / both
245
245
  forgen install claude
246
246
  forgen install codex
247
247
  # Codex only: trust the forgen hooks once with `/hooks` inside codex — Codex skips untrusted hooks (forgen doctor shows [Codex Hooks])
248
+ # Upgrading to v0.5.6: re-running `forgen install codex` changes 2 hooks (session_start, session_end) — approve them in `/hooks` once more.
249
+ # It also registers a `notify` fallback in config.toml (left alone if you already define `notify`; opt out: --no-notify).
250
+ # Claude only: installs a `verify` skill to ~/.claude/skills/verify — Claude Code runs it right before code commits
251
+ # (your own `verify` skill is never overwritten; opt out: --no-verify-skill).
248
252
 
249
253
  # 3. First run — 4-question onboarding (English or Korean)
250
254
  forgen # default: Claude
@@ -924,7 +928,7 @@ See [Coexistence Guide](docs/guides/with-omc.md) for the full plugin-detection m
924
928
 
925
929
  | Document | Description |
926
930
  |----------|-------------|
927
- | [Hooks Reference](docs/reference/hooks-reference.md) | 19 hooks across 3 tiers — events, timeouts, behavior |
931
+ | [Hooks Reference](docs/reference/hooks-reference.md) | 23 hooks across 3 tiers — events, timeouts, behavior |
928
932
  | [Coexistence Guide](docs/guides/with-omc.md) | Using forgen alongside oh-my-claudecode |
929
933
  | [forgen-eval testbed](packages/forgen-eval/) | Alpha self-measurement package — multi-host parity, 7-axis metrics, drift detection (private workspace, v0.4.3+) |
930
934
  | [Multi-host core design](docs/superpowers/specs/2026-04-27-forgen-multi-host-core-design.md) | Codex/Claude symmetric host adapter spec |
package/README.zh.md CHANGED
@@ -192,6 +192,10 @@ forgen install both # 三选交互: claude / codex / both
192
192
  forgen install claude
193
193
  forgen install codex
194
194
  # 仅 Codex: 在 codex 内用 `/hooks` 信任一次 forgen 钩子 — Codex 会跳过未信任的钩子 (用 forgen doctor 的 [Codex Hooks] 查看)
195
+ # 升级到 v0.5.6: 重新运行 `forgen install codex` 会改动 2 个钩子 (session_start, session_end) — 在 `/hooks` 中再信任一次。
196
+ # 同时在 config.toml 注册 `notify` 兜底 (如果你已定义 `notify` 则不改动; 关闭: --no-notify)。
197
+ # 仅 Claude: 将 `verify` 技能安装到 ~/.claude/skills/verify — Claude Code 在提交代码前运行它
198
+ # (不会覆盖你自己的 `verify` 技能; 关闭: --no-verify-skill)。
195
199
 
196
200
  # 3. 首次运行 — 4题引导问卷 (英语/韩语选择)
197
201
  forgen # 默认: Claude
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: verify
3
+ description: Confirm a code change actually works using real execution evidence — run the project's own build, type-check, lint and tests, exercise the change itself, and report a confirmed / refuted / unverified verdict. Use right before committing a code change, or when asked to verify that something works.
4
+ ---
5
+
6
+ <!-- forgen-managed -->
7
+
8
+ # verify — prove the change works before it is committed
9
+
10
+ "Saying it is done and proving it is done are different things."
11
+
12
+ This is the forgen default verification recipe. Claude Code runs a skill named `verify` right before
13
+ commits that touch code (docs-only and tests-only commits are skipped).
14
+
15
+ ## 0. Prefer the repository's own recipe
16
+
17
+ If this repository documents how to verify changes — its own `.claude/skills/verify/SKILL.md`, or
18
+ verification/test commands in `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md` or the README — follow that
19
+ recipe first. It knows the project better than this generic one. Use the steps below for whatever it
20
+ does not cover, and keep the evidence rules in step 3 either way.
21
+
22
+ ## 1. Scope the change
23
+
24
+ - `git status --short` and `git diff --stat` (staged and unstaged) to see what is about to be committed.
25
+ - State in one sentence what the change is supposed to do. That sentence is the claim you are verifying.
26
+
27
+ ## 2. Run the project's real checks
28
+
29
+ Find the commands the project actually uses (`package.json` scripts, `Makefile`, `justfile`,
30
+ `pyproject.toml`, `Cargo.toml`, `go.mod`, CI workflow files) and run the ones relevant to the change:
31
+
32
+ 1. build / compile
33
+ 2. type-check
34
+ 3. lint
35
+ 4. tests — the tests covering the changed code first, then the full suite when it is reasonably fast
36
+
37
+ Keep it proportional: a one-line fix needs its targeted test and a build, not a 20-minute end-to-end run.
38
+ Say which checks you skipped and why.
39
+
40
+ ## 3. Evidence rules
41
+
42
+ - Only output from commands you ran **now** counts. "It should pass" and results quoted from earlier
43
+ in the session are not evidence.
44
+ - A passing test that mocks or stubs the unit you changed does not show the change works. Look for,
45
+ or add, a check that executes the real code path.
46
+ - Exercise the change itself where feasible: run the CLI command, call the endpoint, load the page,
47
+ import and call the function. A green test suite that never touches the new behaviour is not enough.
48
+ - Do not attach confidence percentages you did not measure.
49
+
50
+ ## 4. Verdict
51
+
52
+ Report exactly one of:
53
+
54
+ - **confirmed** — the commands ran and their output supports the claim. Quote each command and the
55
+ 1–3 lines of output that matter.
56
+ - **refuted** — something failed or the behaviour is wrong. Do **not** commit. Show the failing output,
57
+ fix the problem, then run this skill again.
58
+ - **unverified** — you could not run what was needed (missing dependency, no test covers it, needs
59
+ credentials). Say precisely what could not be checked and ask before committing; never round
60
+ unverified up to confirmed.
61
+
62
+ ## 5. Riskier changes
63
+
64
+ For changes that touch persistence, authentication, money, concurrency, or public interfaces, hand the
65
+ claim to a fresh verifier sub-agent (`forgen-verify` or `ch-verifier` when available) and ask it to try
66
+ to break the claim rather than confirm it. Include its verdict in your report.
@@ -24,7 +24,10 @@
24
24
  "matcher": "*",
25
25
  "script": "hooks/session-recovery.js",
26
26
  "timeout": 3,
27
- "compoundCritical": true
27
+ "compoundCritical": true,
28
+ "codex": {
29
+ "additionalContextLimit": 0
30
+ }
28
31
  },
29
32
  {
30
33
  "name": "post-tool-use",
@@ -168,7 +171,10 @@
168
171
  "matcher": "*",
169
172
  "script": "hooks/post-tool-failure.js",
170
173
  "timeout": 3,
171
- "compoundCritical": false
174
+ "compoundCritical": false,
175
+ "hosts": [
176
+ "claude"
177
+ ]
172
178
  },
173
179
  {
174
180
  "name": "solution-injector",
@@ -206,7 +212,8 @@
206
212
  "timeout": 3,
207
213
  "compoundCritical": true,
208
214
  "hosts": [
209
- "claude"
215
+ "claude",
216
+ "codex"
210
217
  ]
211
218
  }
212
219
  ]
package/dist/cli.js CHANGED
@@ -181,19 +181,23 @@ const commands = [
181
181
  },
182
182
  {
183
183
  name: 'install',
184
- description: 'Install forgen into a host. Usage: forgen install [claude|codex|opencode|both] [--dry-run] [--no-mcp]',
184
+ description: 'Install forgen into a host. Usage: forgen install [claude|codex|opencode|both] [--dry-run] [--no-mcp] [--no-notify] [--no-verify-skill]',
185
185
  handler: async (args) => {
186
186
  const knownSubs = new Set(['claude', 'codex', 'opencode', 'both']);
187
187
  const target = args[0] && knownSubs.has(args[0]) ? args[0] : args[0]?.startsWith('--') ? undefined : args[0];
188
188
  if (target !== undefined && !knownSubs.has(target)) {
189
- console.log('Usage:\n forgen install [claude|codex|opencode|both] [--dry-run] [--no-mcp]\n\n No arg → interactive 3-choice (Claude/Codex/Both). opencode: 명시 타겟(P1 실험적).');
189
+ console.log('Usage:\n forgen install [claude|codex|opencode|both] [--dry-run] [--no-mcp] [--no-notify] [--no-verify-skill]\n\n No arg → interactive 3-choice (Claude/Codex/Both). opencode: 명시 타겟(P1 실험적).\n --no-notify: Codex config.toml 에 notify 폴백을 등록하지 않음.\n --no-verify-skill: Claude ~/.claude/skills/verify 를 설치하지 않음.');
190
190
  return;
191
191
  }
192
192
  const dryRun = args.includes('--dry-run');
193
193
  const registerMcp = !args.includes('--no-mcp');
194
194
  const { runInstall, renderResult, resolvePkgRootFromBinary } = await import('./host/install-orchestrator.js');
195
195
  const pkgRoot = resolvePkgRootFromBinary(import.meta.url);
196
- const result = await runInstall({ target, pkgRoot, dryRun, registerMcp });
196
+ const result = await runInstall({
197
+ target, pkgRoot, dryRun, registerMcp,
198
+ registerNotify: !args.includes('--no-notify'),
199
+ installVerifySkill: !args.includes('--no-verify-skill'),
200
+ });
197
201
  if (result === null) {
198
202
  console.log('\n [forgen] Install skipped.');
199
203
  return;
@@ -74,7 +74,15 @@ async function renderCodexHookTrust() {
74
74
  console.log(` △ ${t.total} forgen hooks registered, Codex trust 기록 없음 — codex 안에서 /hooks 로 승인 (미승인 훅은 skip 됨)`);
75
75
  }
76
76
  else {
77
- console.log(` ✗ ${t.untrusted.length}/${t.total} forgen hooks untrusted (${t.untrusted.slice(0, 4).join(', ')}${t.untrusted.length > 4 ? ', …' : ''}) — codex 안에서 /hooks 로 승인`);
77
+ const pending = [...t.modified.map((k) => `${k} modified`), ...t.untrusted.map((k) => `${k} new`), ...t.disabled.map((k) => `${k} disabled`)];
78
+ console.log(` ✗ ${pending.length}/${t.total} forgen hooks skipped by Codex (${pending.slice(0, 4).join(', ')}${pending.length > 4 ? ', …' : ''}) — codex 안에서 /hooks 로 승인`);
79
+ }
80
+ // ADR-016 D1 — notify 폴백이 관측한 "턴은 끝났는데 forgen 훅이 돌지 않음"
81
+ const { readCodexHooksSilent } = await import('../host/codex-notify.js');
82
+ const silent = readCodexHooksSilent();
83
+ // 1회 관측은 Stop 훅이 돌지 않는 내부 서브세션(/review 등)의 notify 일 수 있다 — 연속 2회부터 표시.
84
+ if (silent && silent.count >= 2) {
85
+ console.log(` ✗ notify 폴백 관측: 최근 Codex 턴 ${silent.count}회에서 forgen 훅 미발화 (마지막 ${silent.detectedAt}, session ${silent.sessionId.slice(0, 8)}) — /hooks 승인 필요`);
78
86
  }
79
87
  }
80
88
  catch { /* fail-open */ }
@@ -1,3 +1,10 @@
1
+ /** ADR-016 D3 — ~/.claude/skills/verify 제거 (forgen-managed 마커가 있는 것만; 사용자 스킬은 보존) */
2
+ export declare function cleanVerifySkill(homeDir?: string): boolean;
3
+ /**
4
+ * ADR-016 D1 — Codex config.toml 의 forgen notify 블록 제거. 남겨 두면 Codex 가 매 턴 사라진 스크립트를
5
+ * spawn 한다. (Codex hooks.json / MCP 블록 정리는 아직 uninstall 범위 밖 — 알려진 갭.)
6
+ */
7
+ export declare function cleanCodexNotify(codexHome?: string): Promise<boolean>;
1
8
  /** forgen uninstall 메인 */
2
9
  export declare function handleUninstall(cwd: string, options: {
3
10
  force?: boolean;
@@ -96,6 +96,47 @@ function cleanSlashCommands() {
96
96
  console.log(' - No forgen-managed slash commands found');
97
97
  }
98
98
  }
99
+ /** ADR-016 D3 — ~/.claude/skills/verify 제거 (forgen-managed 마커가 있는 것만; 사용자 스킬은 보존) */
100
+ export function cleanVerifySkill(homeDir = os.homedir()) {
101
+ const dir = path.join(homeDir, '.claude', 'skills', 'verify');
102
+ const file = path.join(dir, 'SKILL.md');
103
+ try {
104
+ if (fs.lstatSync(dir).isSymbolicLink() || fs.lstatSync(file).isSymbolicLink())
105
+ return false;
106
+ const content = fs.readFileSync(file, 'utf-8');
107
+ if (!/^---\n[\s\S]*?\n---\n\s*<!-- forgen-managed -->/.test(content))
108
+ return false;
109
+ fs.unlinkSync(file);
110
+ try {
111
+ if (fs.readdirSync(dir).length === 0)
112
+ fs.rmdirSync(dir);
113
+ }
114
+ catch { /* ignore */ }
115
+ return true;
116
+ }
117
+ catch {
118
+ return false; // 없음
119
+ }
120
+ }
121
+ /**
122
+ * ADR-016 D1 — Codex config.toml 의 forgen notify 블록 제거. 남겨 두면 Codex 가 매 턴 사라진 스크립트를
123
+ * spawn 한다. (Codex hooks.json / MCP 블록 정리는 아직 uninstall 범위 밖 — 알려진 갭.)
124
+ */
125
+ export async function cleanCodexNotify(codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex')) {
126
+ const configPath = path.join(codexHome, 'config.toml');
127
+ try {
128
+ const current = fs.readFileSync(configPath, 'utf-8');
129
+ const { removeNotifyBlock } = await import('../host/install-codex.js');
130
+ const r = removeNotifyBlock(current);
131
+ if (!r.removed)
132
+ return false;
133
+ fs.writeFileSync(configPath, r.content, 'utf-8');
134
+ return true;
135
+ }
136
+ catch {
137
+ return false; // Codex 미사용
138
+ }
139
+ }
99
140
  /** 사용자에게 y/n 확인 */
100
141
  function confirm(message) {
101
142
  return new Promise((resolve) => {
@@ -301,7 +342,7 @@ export async function handleUninstall(cwd, options) {
301
342
  console.log(' 2. Delete .claude/agents/ch-*.md agent files');
302
343
  console.log(' 3. Delete .claude/rules/ rule files (project-context, routing, forge-*)');
303
344
  console.log(' 4. Remove forgen block from CLAUDE.md');
304
- console.log(' 5. Remove slash commands (~/.claude/commands/forgen/)');
345
+ console.log(' 5. Remove slash commands (~/.claude/commands/forgen/) and the forgen-managed verify skill (~/.claude/skills/verify/)');
305
346
  console.log(' 6. Remove plugin artifacts (cache, installed_plugins.json, plugin directory)');
306
347
  if (options.purge) {
307
348
  console.log(' 7. --purge: Delete ~/.forgen/ entirely (rules, me/, state/, solutions/, behavior/)');
@@ -330,6 +371,10 @@ export async function handleUninstall(cwd, options) {
330
371
  cleanCompoundRules(cwd);
331
372
  cleanClaudeMd(cwd);
332
373
  cleanSlashCommands();
374
+ if (cleanVerifySkill())
375
+ console.log(' ✓ Removed forgen verify skill (~/.claude/skills/verify/)');
376
+ if (await cleanCodexNotify())
377
+ console.log(' ✓ Removed forgen notify block from Codex config.toml');
333
378
  cleanPluginArtifacts();
334
379
  if (options.purge) {
335
380
  try {
@@ -57,6 +57,9 @@ export declare function effectiveCooldownMs(parsed: {
57
57
  userPatternFound?: boolean;
58
58
  promptCount?: number;
59
59
  }, currentPromptCount?: number): number;
60
+ export declare function maybeSpawnAutoCompound(sessionId: string, transcriptPath: string | undefined, promptCount: number,
61
+ /** ADR-016 D1: notify 폴백은 훅 env(FORGEN_CWD) 없이 돌므로 페이로드의 cwd 를 넘긴다. */
62
+ cwdOverride?: string): Promise<boolean>;
60
63
  /**
61
64
  * forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
62
65
  * OMC의 persistent-mode.cjs 패턴 참고.
@@ -439,9 +439,11 @@ export function effectiveCooldownMs(parsed, currentPromptCount = 0) {
439
439
  const grew = currentPromptCount - (parsed.promptCount ?? 0) >= AUTO_COMPOUND_GROWTH_THRESHOLD;
440
440
  return grew ? AUTO_COMPOUND_COOLDOWN_MS : AUTO_COMPOUND_BARREN_COOLDOWN_MS;
441
441
  }
442
- async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
442
+ export async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount,
443
+ /** ADR-016 D1: notify 폴백은 훅 env(FORGEN_CWD) 없이 돌므로 페이로드의 cwd 를 넘긴다. */
444
+ cwdOverride) {
443
445
  if (!transcriptPath || promptCount < 10)
444
- return;
446
+ return false;
445
447
  const markerPath = path.join(STATE_DIR, 'last-auto-compound.json');
446
448
  try {
447
449
  const raw = fs.readFileSync(markerPath, 'utf-8');
@@ -450,12 +452,12 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
450
452
  const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
451
453
  const cooldown = effectiveCooldownMs(parsed, promptCount);
452
454
  if (Number.isFinite(last) && Date.now() - last < cooldown)
453
- return;
455
+ return false;
454
456
  }
455
457
  }
456
458
  catch { /* first time or corrupt — proceed */ }
457
459
  const { spawn: spawnProcess } = await import('node:child_process');
458
- const cwd = process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
460
+ const cwd = cwdOverride ?? process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
459
461
  // 기본: 번들된 auto-compound-runner. 프로덕션 빌드는 이 경로만 실행.
460
462
  const defaultRunner = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'core', 'auto-compound-runner.js');
461
463
  // 테스트 주입 경로 — FORGEN_TEST=1 게이트 + 경로 containment (~/.forgen 또는 /tmp 하위만 허용).
@@ -486,7 +488,7 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
486
488
  const { claimAutoCompoundInflight, releaseAutoCompoundInflight } = await import('../core/spawn.js');
487
489
  if (!claimAutoCompoundInflight(sessionId)) {
488
490
  log.debug('Stop-triggered auto-compound skip: 세션 in-flight');
489
- return;
491
+ return false;
490
492
  }
491
493
  try {
492
494
  const child = spawnProcess('node', [runnerPath, cwd, transcriptPath, sessionId, String(promptCount)], {
@@ -495,10 +497,12 @@ async function maybeSpawnAutoCompound(sessionId, transcriptPath, promptCount) {
495
497
  });
496
498
  child.unref();
497
499
  log.debug(`Stop-triggered auto-compound 시작: ${sessionId} (${promptCount} prompts)`);
500
+ return true;
498
501
  }
499
502
  catch (e) {
500
503
  releaseAutoCompoundInflight(sessionId); // spawn 실패 → 재시도 가능
501
504
  log.debug('Stop-triggered auto-compound spawn 실패', e);
505
+ return false;
502
506
  }
503
507
  }
504
508
  // forge-loop 차단 안전 상한 (무한 루프 방지)
@@ -32,6 +32,17 @@ export interface HookEntry {
32
32
  * 훅 신뢰(trust) 와 묶여 있으므로, Claude 에만 추가하는 이벤트는 `["claude"]` 로 한정한다.
33
33
  */
34
34
  hosts?: Array<'claude' | 'codex' | 'opencode'>;
35
+ /**
36
+ * ADR-016 D2: Codex hooks.json 핸들러에만 붙는 필드. Codex 의 trust 해시는 핸들러 단위라
37
+ * 여기 값을 바꾸면 *그 핸들러만* `/hooks` 재승인이 필요하다 (다른 핸들러는 영향 없음).
38
+ */
39
+ codex?: {
40
+ /**
41
+ * `additionalContext` 를 임시 파일로 스필하는 임계(근사 토큰, 기본 2500 ≈ 10KB). 0 = 스필 안 함.
42
+ * Codex 는 PreToolUse/PostToolUse/SessionStart/UserPromptSubmit/SubagentStart 에서만 인정한다.
43
+ */
44
+ additionalContextLimit?: number;
45
+ };
35
46
  }
36
47
  export declare const HOOK_REGISTRY: HookEntry[];
37
48
  /** 티어별 훅 목록 조회 */
@@ -14,6 +14,8 @@ interface HookCommand {
14
14
  type: 'command';
15
15
  command: string;
16
16
  timeout: number;
17
+ /** Codex 전용 (ADR-016 D2). Claude 산출물에는 넣지 않는다. */
18
+ additionalContextLimit?: number;
17
19
  }
18
20
  interface HookMatcher {
19
21
  matcher: string;
@@ -92,7 +92,11 @@ export function generateHooksJson(options) {
92
92
  matcher,
93
93
  hooks: matcherEntries.map(h => {
94
94
  const command = buildHookCommand(pluginRoot, h.script, runtime);
95
- return { type: 'command', command, timeout: h.timeout };
95
+ const handler = { type: 'command', command, timeout: h.timeout };
96
+ if (runtime === 'codex' && typeof h.codex?.additionalContextLimit === 'number') {
97
+ handler.additionalContextLimit = h.codex.additionalContextLimit;
98
+ }
99
+ return handler;
96
100
  }),
97
101
  }));
98
102
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Forgen — SessionEnd Hook (ADR-015 C-G6, Claude Code 전용)
3
+ * Forgen — SessionEnd Hook (ADR-015 C-G6 Claude, ADR-016 D2 Codex)
4
4
  *
5
5
  * Claude Code `SessionEnd` (reason: clear|resume|logout|prompt_input_exit|other, 기본 예산 1.5s)
6
6
  * 에서 이전 세션 transcript 를 auto-compound 러너에 넘긴다. 기존 트리거(Stop / PreCompact /
@@ -10,8 +10,10 @@
10
10
  *
11
11
  * - 예산(기본 1.5s, registry timeout 3s 로 상향) 안에 끝나야 하므로: stdin 파싱 → user 메시지 수
12
12
  * (앞 200KB 만 읽음, 대용량 transcript 보호) → detached spawn.
13
- * - Codex 에는 등록하지 않는다 (hooks.json 바이트 동일성 = 훅 신뢰 유지). Codex 의 SessionEnd 는
14
- * hooks.json 변경이 필요한 다음 메이저에서 함께 추가.
13
+ * - Codex 0.153+ 에도 등록한다 (ADR-016 D2). Codex 의 SessionEnd 는 stdin 이
14
+ * `session_id`/`transcript_path`/`cwd`/`reason`(항상 "other") 이고 stdout 을 무시하며 타임아웃을 1~3s 로
15
+ * clamp 한다 — 같은 "bounded count → detached spawn" 경로가 그대로 맞는다. trust 해시가 핸들러 단위라
16
+ * 새 이벤트 추가는 기존 훅의 신뢰를 깨지 않는다 (이 훅 1개만 `/hooks` 승인 필요).
15
17
  * - fail-open: 어떤 실패도 종료를 막지 않는다.
16
18
  */
17
19
  /** session-recovery 와 동일 임계값 — 짧은 세션은 compound 가치가 없다. */
@@ -29,6 +31,11 @@ export declare const COUNT_SCAN_BYTES: number;
29
31
  * 임계값(MIN_USER_MESSAGES) 판정에만 쓰이므로 하한 추정으로 충분하다. 큰 transcript 에서도 O(200KB).
30
32
  */
31
33
  export declare function countUserMessagesBounded(transcriptPath: string, maxBytes?: number): number;
34
+ /**
35
+ * host 별 user 메시지 수. Codex rollout 은 스키마가 달라 전용 카운터를 쓴다 (ADR-016 D2 — 실제 사용자
36
+ * 프롬프트만, raw 바이트 스캔). codex-adapter 가 delegate 훅에 FORGEN_RUNTIME=codex 를 주입한다.
37
+ */
38
+ export declare function countSessionUserMessages(transcriptPath: string, runtime?: string | undefined): Promise<number>;
32
39
  /** 순수 판정: 이 입력으로 auto-compound 를 띄울지. (테스트 대상) */
33
40
  export declare function shouldRunSessionEndCompound(input: SessionEndInput | null, userMessageCount: number): boolean;
34
41
  export declare function main(): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Forgen — SessionEnd Hook (ADR-015 C-G6, Claude Code 전용)
3
+ * Forgen — SessionEnd Hook (ADR-015 C-G6 Claude, ADR-016 D2 Codex)
4
4
  *
5
5
  * Claude Code `SessionEnd` (reason: clear|resume|logout|prompt_input_exit|other, 기본 예산 1.5s)
6
6
  * 에서 이전 세션 transcript 를 auto-compound 러너에 넘긴다. 기존 트리거(Stop / PreCompact /
@@ -10,8 +10,10 @@
10
10
  *
11
11
  * - 예산(기본 1.5s, registry timeout 3s 로 상향) 안에 끝나야 하므로: stdin 파싱 → user 메시지 수
12
12
  * (앞 200KB 만 읽음, 대용량 transcript 보호) → detached spawn.
13
- * - Codex 에는 등록하지 않는다 (hooks.json 바이트 동일성 = 훅 신뢰 유지). Codex 의 SessionEnd 는
14
- * hooks.json 변경이 필요한 다음 메이저에서 함께 추가.
13
+ * - Codex 0.153+ 에도 등록한다 (ADR-016 D2). Codex 의 SessionEnd 는 stdin 이
14
+ * `session_id`/`transcript_path`/`cwd`/`reason`(항상 "other") 이고 stdout 을 무시하며 타임아웃을 1~3s 로
15
+ * clamp 한다 — 같은 "bounded count → detached spawn" 경로가 그대로 맞는다. trust 해시가 핸들러 단위라
16
+ * 새 이벤트 추가는 기존 훅의 신뢰를 깨지 않는다 (이 훅 1개만 `/hooks` 승인 필요).
15
17
  * - fail-open: 어떤 실패도 종료를 막지 않는다.
16
18
  */
17
19
  import * as fs from 'node:fs';
@@ -52,6 +54,17 @@ export function countUserMessagesBounded(transcriptPath, maxBytes = COUNT_SCAN_B
52
54
  fs.closeSync(fd);
53
55
  }
54
56
  }
57
+ /**
58
+ * host 별 user 메시지 수. Codex rollout 은 스키마가 달라 전용 카운터를 쓴다 (ADR-016 D2 — 실제 사용자
59
+ * 프롬프트만, raw 바이트 스캔). codex-adapter 가 delegate 훅에 FORGEN_RUNTIME=codex 를 주입한다.
60
+ */
61
+ export async function countSessionUserMessages(transcriptPath, runtime = process.env.FORGEN_RUNTIME) {
62
+ if (runtime === 'codex') {
63
+ const { countCodexUserPrompts } = await import('../host/codex-rollout.js');
64
+ return countCodexUserPrompts(transcriptPath);
65
+ }
66
+ return countUserMessagesBounded(transcriptPath);
67
+ }
55
68
  /** 순수 판정: 이 입력으로 auto-compound 를 띄울지. (테스트 대상) */
56
69
  export function shouldRunSessionEndCompound(input, userMessageCount) {
57
70
  if (!input || typeof input.transcript_path !== 'string' || input.transcript_path.length === 0)
@@ -71,7 +84,7 @@ export async function main() {
71
84
  let count = 0;
72
85
  if (transcript && fs.existsSync(transcript)) {
73
86
  try {
74
- count = countUserMessagesBounded(transcript);
87
+ count = await countSessionUserMessages(transcript);
75
88
  }
76
89
  catch (e) {
77
90
  log.debug('user message count 실패', e);
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { spawnSync } from 'node:child_process';
14
14
  import { projectCodexToClaude } from './projection.js';
15
+ import { markCodexHookAlive } from './codex-hook-alive.js';
15
16
  function lastJSONObjectFromText(raw) {
16
17
  const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
17
18
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -54,6 +55,10 @@ async function main() {
54
55
  return {};
55
56
  }
56
57
  })();
58
+ // ADR-016 D1 — "Codex 가 forgen 훅을 실제로 실행 중" 마커 (notify 폴백이 신선도를 본다).
59
+ // forgen 자신의 추출용 중첩 실행은 사용자 세션이 아니므로 제외.
60
+ if (process.env.FORGEN_NESTED_RUN !== '1')
61
+ markCodexHookAlive(input);
57
62
  try {
58
63
  // 0.4.6 fix — delegate hook 이 자기가 codex runtime context 인지 알 수 있게
59
64
  // FORGEN_RUNTIME=codex 명시 주입. 이전엔 buildEnv (forgen wrapper) 경로로만
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Codex 훅 생존 마커 — ADR-016 D1
3
+ *
4
+ * Codex 는 미승인(untrusted)/변경된(modified) 훅을 조용히 skip 한다. "forgen 훅이 실제로 실행되고 있는가"
5
+ * 를 관측하는 유일한 choke point 는 codex-adapter 다 (Codex 가 forgen 훅을 실행하면 반드시 거친다).
6
+ * 어댑터가 턴 경계 이벤트에서 전역 마커를 갱신하고, notify 폴백(codex-notify)이 그 신선도를 본다.
7
+ *
8
+ * 훅 신뢰 상태는 hooks.json 단위로 모든 세션이 공유하므로 마커는 세션별이 아니라 전역 1개다.
9
+ * 어댑터가 매 훅마다 import 하므로 의존성은 node 내장 + paths 만 둔다.
10
+ */
11
+ export declare const CODEX_HOOK_ALIVE_PATH: string;
12
+ /** notify 는 Stop 훅 직후 발화한다. Stop 훅 타임아웃(10s) 대비 넉넉한 창. */
13
+ export declare const CODEX_HOOK_ALIVE_WINDOW_MS = 120000;
14
+ export interface CodexHookAliveMarker {
15
+ at: number;
16
+ event: string;
17
+ sessionId?: string;
18
+ }
19
+ /** 어댑터 입력(stdin JSON)으로 마커 갱신. 대상 이벤트가 아니면 no-op. 실패는 삼킨다 (fail-open). */
20
+ export declare function markCodexHookAlive(input: unknown, now?: number): boolean;
21
+ export declare function readCodexHookAlive(): CodexHookAliveMarker | null;
22
+ /** 최근 window 안에 forgen 훅이 Codex 에서 실행됐는가. */
23
+ export declare function isCodexHookAlive(now?: number, windowMs?: number): boolean;