@wooojin/forgen 0.5.6 → 0.5.8
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/plugin.json +1 -1
- package/CHANGELOG.md +89 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/core/health-cli.js +11 -12
- package/dist/core/paths.d.ts +1 -1
- package/dist/core/settings-lock.d.ts +7 -1
- package/dist/core/settings-lock.js +19 -4
- package/dist/core/status-cli.d.ts +1 -1
- package/dist/core/trust-layer-intent.d.ts +2 -2
- package/dist/core/uninstall.d.ts +8 -3
- package/dist/core/uninstall.js +51 -17
- package/dist/engine/solution-fixup.js +2 -2
- package/dist/engine/solution-format.js +4 -4
- package/dist/engine/solution-quarantine.js +2 -2
- package/dist/host/install-claude.js +2 -3
- package/dist/host/install-codex.d.ts +39 -6
- package/dist/host/install-codex.js +383 -105
- package/dist/host/managed-marker.d.ts +23 -0
- package/dist/host/managed-marker.js +78 -0
- package/dist/host/opencode/plugin/forgen.d.ts +1 -1
- package/dist/host/uninstall-codex.d.ts +58 -0
- package/dist/host/uninstall-codex.js +246 -0
- package/package.json +5 -6
- package/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,95 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.8] — 2026-10-02 — Codex 가 재배치한 마커에서도 설치·제거가 정확하게 (Codex 0.160 확인)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Codex 가 config.toml 을 다시 쓰면서 forgen 마커를 뒤집어 놓은 형태에서 `forgen uninstall` 이 MCP 서버 등록을 지우지
|
|
14
|
+
못하던 것 (0.5.7).** 실머신에서 `/hooks` 승인 후 관측: `# >>> forgen-managed-mcp` 는 forgen 테이블과 함께 파일 끝으로 가고
|
|
15
|
+
`# <<< forgen-managed-mcp` 는 앞쪽에 고아로 남는다. 이제 마커를 *범위* 가 아니라 *표식* 으로만 쓰고, 범위는 TOML 구조
|
|
16
|
+
(테이블 헤더 ~ 다음 헤더, notify 한 줄)로 정한다. 재설치는 고아 마커를 걷어 테이블 위아래로 정규화하고, 제거는 테이블·하위
|
|
17
|
+
테이블·모든 마커 줄을 지운다. notify 블록도 END 마커가 옮겨지거나 사라져도 동작한다.
|
|
18
|
+
- 사용자가 forgen MCP 테이블만 지우고 마커가 남은 경우, 재설치가 고아 마커를 정리하고 블록을 한 번만 쓴다.
|
|
19
|
+
- **`codex mcp add <다른 서버>` 뒤에는 forgen 테이블을 알아보지 못하던 것.** Codex 가 mcp_servers 를 다시 쓰며 시작 마커를
|
|
20
|
+
없앤다. 소유 판정을 "헤더 바로 위의 마커 또는 forgen 내용 시그니처(args 의 `dist/mcp/server.js` + `--host=codex`,
|
|
21
|
+
notify 의 `dist/host/codex-notify.js`)" 로 바꿨다 — 마커가 사라져도 경로 갱신과 제거가 된다.
|
|
22
|
+
- CRLF config.toml 에서 블록 제거 후 파일이 `\r` 로만 끝나 Codex 가 로드하지 못할 수 있던 것 (0.5.8 작업 중 회귀로 발견, 미출시).
|
|
23
|
+
- forgen 테이블 뒤·다음 테이블 헤더 위의 사용자 주석을 forgen 본문으로 취급하지 않는다.
|
|
24
|
+
- Codex 가 forgen notify 블록 안의 값을 사용자의 notifier 로 바꿔 놓은 경우 그 값을 보존한다 (이전엔 재설치가 덮어썼다).
|
|
25
|
+
- 같은 MCP 서버가 다른 TOML 표기(따옴표·공백 헤더, inline table, dotted key)로 정의돼 있으면 중복 정의를 추가하지 않는다
|
|
26
|
+
(추가하면 Codex 가 `duplicate key` 로 로드 실패).
|
|
27
|
+
- 블록 제거가 "지웠다" 고 보고하는 것은 실제로 테이블/notify 줄을 지웠을 때만.
|
|
28
|
+
|
|
29
|
+
### Verified
|
|
30
|
+
- vitest 3222 통과 (신규: 실머신에서 채취한 재배치 형태, CRLF/BOM, 마커 소실, 사용자 주석, 다른 TOML 표기).
|
|
31
|
+
- fresh-context critic 리뷰: CRITICAL 1 · MAJOR 2 · MINOR 6 → 전부 반영. 리뷰어의 케이스 68건 + 퍼징 14,000건에서 Codex 로드
|
|
32
|
+
불가 출력·사용자 데이터 변경·비멱등 0건, 실 Codex 0.160 재현 스크립트(`codex mcp add` 후 설치/제거, CRLF 왕복) 통과.
|
|
33
|
+
- 이 머신의 실 `~/.codex/config.toml` 사본에 적용: 재설치 diff 는 고아 END 마커 한 줄 이동뿐(hooks.state 31개 보존),
|
|
34
|
+
제거 후 forgen 항목 0건, 두 결과 모두 Codex 0.160.0 이 정상 파싱.
|
|
35
|
+
- **Codex 0.160.0 호환 확인**: 훅 출력 스키마 11종이 vendoring 한 0.153.4 사본과 동일, trust 해시 일치(실머신 22/22 trusted,
|
|
36
|
+
Codex `hooks/list` 30/30), 실세션에서 SessionStart(룰 블록 주입, 스필 없음)·UserPromptSubmit·Stop·SessionEnd 훅 발화, 오류 0건.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## [0.5.7] — 2026-10-02 — `forgen uninstall` 의 Codex 정리 · 의존성 메이저 업그레이드
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **`forgen uninstall` 이 Codex 등록분도 되돌린다 (ADR-016 D4).** hooks.json 의 forgen 훅, config.toml 의 MCP/notify 블록,
|
|
43
|
+
forgen-managed `skills/` · `agents/ch-*.toml`, cwd 의 AGENTS.md 블록을 제거한다. 이전엔 Codex 쪽이 그대로 남아
|
|
44
|
+
패키지를 지우면 Codex 가 매 훅 이벤트마다 사라진 스크립트를 실행하려 했다.
|
|
45
|
+
- **다른 도구의 훅 신뢰를 지킨다.** Codex 의 trust 키는 그룹 인덱스 기반이라 forgen 그룹을 지워 인덱스가 당겨지면
|
|
46
|
+
뒤따르는 훅이 재승인 전까지 조용히 skip 된다. 그런 위치에는 빈 그룹(`{"hooks": []}`)을 남겨 인덱스를 유지한다
|
|
47
|
+
(Codex 0.153.4 `hooks/list` 로 확인: 경고 없음, 뒤 그룹 trusted 유지). forgen 은 `[hooks.state]` 를 고쳐 쓰지 않는다.
|
|
48
|
+
- 사용자가 만든 스킬/에이전트/마커 없는 MCP 테이블/자기 `notify`, Codex 가 블록 사이에 써 넣은 설정은 보존.
|
|
49
|
+
forgen notify 뒤에 체인해 둔 사용자 notifier 는 그 argv 만으로 `notify` 를 되돌려 놓는다.
|
|
50
|
+
- uninstall 후 재설치하면 빈 그룹 자리를 다시 채워 forgen 훅이 원래 인덱스(= 이미 승인된 trust 키)로 돌아간다.
|
|
51
|
+
- 각 단계는 독립 — 하나가 실패(읽기 전용 파일, 깨진 hooks.json)해도 나머지는 진행하고 실패를 출력한다.
|
|
52
|
+
- 한계: 다른 프로젝트의 AGENTS.md 블록(실행한 cwd 것만 정리), `forgen install opencode` 산출물, 낡은 `[hooks.state]`
|
|
53
|
+
항목, 빈 디렉토리/빈 config.toml 은 남는다.
|
|
54
|
+
- `forgen uninstall` 의 Claude 쪽 누락 보강: `~/.claude.json` 의 `forgen-compound` MCP 등록(install 은 여기에 쓰는데 uninstall 은
|
|
55
|
+
settings.json 만 정리했다), 패키지가 제공한 dev-guide 스킬(`~/.claude/skills/forgen-<stack>-<skill>`).
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- **손으로 여러 줄로 고친 forgen notify 블록을 제거(`--no-notify`, uninstall)하면 config.toml 이 깨지던 결함 (0.5.6).**
|
|
59
|
+
배열의 첫 줄만 지우고 나머지를 남겨 Codex 가 기동하지 못했다. 이제 그런 블록은 건드리지 않고 알린다.
|
|
60
|
+
- **훅 소유 판정이 너무 넓던 것.** command 에 `dist/hooks/<아무이름>.js` 가 있거나 forgen 설치 경로를 *부분문자열* 로
|
|
61
|
+
포함하면 forgen 훅으로 봤다 — 다른 프로젝트의 `…/dist/hooks/pre-commit.js` 나 `<pkgRoot>-fork/…` 가 재설치 시 교체
|
|
62
|
+
대상이 될 수 있었다. 이제 forgen 설치 경로의 `dist/` 아래, codex-adapter 경유, registry 에 있는 forgen 훅 이름일 때만.
|
|
63
|
+
- 소유 마커 판정 강화: SKILL.md 는 frontmatter *바로 뒤* 의 마커만 인정(본문의 `---` 뒤 마커 인용을 오인하던 것),
|
|
64
|
+
agent TOML 은 첫 줄이 정확히 `# forgen-managed` 일 때만.
|
|
65
|
+
- `~/.claude.json` / settings.json 을 다시 쓸 때 기존 파일 권한(0600)과 심링크를 보존 (이전엔 0644 로 넓어지고 심링크가
|
|
66
|
+
일반 파일로 바뀌었다).
|
|
67
|
+
- `forgen status` 점수: 총점을 표시되는 항목 점수의 합으로 계산 (반올림 순서 때문에 항목 합과 총점이 1 어긋날 수 있었다).
|
|
68
|
+
|
|
69
|
+
### Changed — dependencies
|
|
70
|
+
- **js-yaml 4 → 5** (런타임 의존성). v5 ESM 빌드는 default export 가 없어 namespace import 로 전환, dump 옵션
|
|
71
|
+
`quotingType` → `quoteStyle`, 타입 내장으로 `@types/js-yaml` 제거. **스키마를 `JSON_SCHEMA` → `CORE_SCHEMA` 로 변경**:
|
|
72
|
+
v5 의 `JSON_SCHEMA` 는 strict JSON 이라 v4 가 읽던 `confidence: .5`, `supersedes: ~`, `True` 등을 문자열로 읽어
|
|
73
|
+
솔루션이 조용히 탈락한다. `CORE_SCHEMA` 가 v4 와 같은 해석을 준다 (회귀 테스트 추가). 실 솔루션/룰 frontmatter 87개에서
|
|
74
|
+
v4 와 파싱·직렬화 결과 바이트 동일. 남는 차이: `1_000`, `0b11` 형태는 v5 에서 문자열로 읽히고 따옴표 없이 직렬화된다
|
|
75
|
+
(forgen 이 쓰는 값에는 나타나지 않는 형태).
|
|
76
|
+
- **TypeScript 5.9 → 7.0** (dev). TS 6+ 는 `@types/*` 를 자동 포함하지 않아 tsconfig 에 `"types": ["node"]` 명시.
|
|
77
|
+
TS 7 의 `tsc` 는 Node 20.0 에서 실행되지 않으므로 CI 의 훅 포터빌리티 잡을 "Node 22 로 빌드 → 대상 Node 로 로드" 로 분리
|
|
78
|
+
(배포물은 빌드된 dist 이므로 사용자 영향 없음).
|
|
79
|
+
- **vitest 4 → 5** + `@vitest/coverage-v8` 5 (dev, 함께 올려야 함).
|
|
80
|
+
- `@modelcontextprotocol/sdk` 1.31.0, `zod` 4.6.5, `@types/node` 26.6.4, `@biomejs/biome` 2.5.15, `npm audit fix`
|
|
81
|
+
(advisory 13 → 0), `actions/setup-node` v7.
|
|
82
|
+
|
|
83
|
+
### Docs
|
|
84
|
+
- `packages/forgen-eval/reports/persistence/` 원시 리포트 6건 커밋 — `docs/release/v0.5.0-persistence-delta.md` 가 재현
|
|
85
|
+
근거로 인용하던 파일이 저장소에 없었다.
|
|
86
|
+
|
|
87
|
+
### Verified
|
|
88
|
+
- vitest 3204 통과 (신규: Codex uninstall 왕복·신뢰 인덱스 보존·config 보존·소유 판정 오탐·손편집 notify 블록,
|
|
89
|
+
Claude 쪽 MCP/dev-guide 정리·권한/심링크 보존, js-yaml 스칼라 해석).
|
|
90
|
+
- 격리 `HOME` 에서 CLI `install both` → `uninstall --force` 왕복: forgen 이 등록한 훅/블록/스킬/에이전트가 제거되고, 다른
|
|
91
|
+
도구 훅은 실 Codex `hooks/list` 에서 uninstall 후에도 `trusted`, `codex mcp list` 가 config 를 정상 파싱 (손편집된
|
|
92
|
+
여러 줄 notify 블록이 있는 경우 포함).
|
|
93
|
+
- fresh-context critic 리뷰: MAJOR 2 · MINOR 12 → 위 Fixed/Changed 에 반영 또는 한계로 명시. 리뷰어가 확인한 것: TS 7 빌드의
|
|
94
|
+
`.js` 227개가 TS 5.9 빌드와 바이트 동일, 실 `~/.codex` 사본에서 uninstall 후 다른 도구 훅 8개 trusted 유지.
|
|
95
|
+
- CI: ubuntu/macOS/arm 테스트 + Node 20.0.0~22 훅 로드 14개 검사 통과.
|
|
96
|
+
- 0.5.6 을 이 머신의 실 `~/.codex` 에 설치한 뒤 실 Codex 세션 1회: Stop 훅 발화·alive 마커 갱신, silent 플래그 없음.
|
|
97
|
+
|
|
98
|
+
|
|
10
99
|
## [0.5.6] — 2026-10-02 — Codex notify 폴백 · 훅 번들(재승인 2건) · Claude verify 스킬 (ADR-016)
|
|
11
100
|
|
|
12
101
|
> **Codex 사용자 — 재승인 필요**: 업그레이드 후 `forgen install codex` 를 다시 실행하면 훅 2개
|
package/README.ja.md
CHANGED
|
@@ -564,7 +564,7 @@ forgen mcp list # インストール済み MCP サーバーを
|
|
|
564
564
|
forgen mcp add <名前> # テンプレートから MCP サーバーを追加
|
|
565
565
|
forgen mcp templates # 利用可能なテンプレートを表示
|
|
566
566
|
forgen notepad show # セッションノートパッドを表示
|
|
567
|
-
forgen uninstall # forgen をきれいに削除
|
|
567
|
+
forgen uninstall # forgen をきれいに削除 (Claude Code + Codex の登録分)
|
|
568
568
|
```
|
|
569
569
|
|
|
570
570
|
### MCP ツール(セッション中に Claude が使用可能)
|
package/README.ko.md
CHANGED
|
@@ -549,7 +549,7 @@ forgen mcp list # 설치된 MCP 서버 목록
|
|
|
549
549
|
forgen mcp add <이름> # 템플릿에서 MCP 서버 추가
|
|
550
550
|
forgen mcp templates # 사용 가능한 템플릿 목록
|
|
551
551
|
forgen notepad show # 세션 노트패드 보기
|
|
552
|
-
forgen uninstall # forgen 깔끔하게 제거
|
|
552
|
+
forgen uninstall # forgen 깔끔하게 제거 (Claude Code + Codex 등록분)
|
|
553
553
|
```
|
|
554
554
|
|
|
555
555
|
### MCP 도구 (세션 중 Claude가 사용)
|
package/README.md
CHANGED
|
@@ -750,7 +750,7 @@ forgen mcp list # List installed MCP servers
|
|
|
750
750
|
forgen mcp add <name> # Add MCP server from template
|
|
751
751
|
forgen mcp templates # Show available templates
|
|
752
752
|
forgen notepad show # View session notepad
|
|
753
|
-
forgen uninstall # Remove forgen cleanly
|
|
753
|
+
forgen uninstall # Remove forgen cleanly (Claude Code + Codex registrations)
|
|
754
754
|
```
|
|
755
755
|
|
|
756
756
|
### Rule lifecycle (v0.4.0, ADR-001/002)
|
package/README.zh.md
CHANGED
|
@@ -533,7 +533,7 @@ forgen mcp list # 列出已安装的 MCP 服务器
|
|
|
533
533
|
forgen mcp add <名称> # 从模板添加 MCP 服务器
|
|
534
534
|
forgen mcp templates # 显示可用模板
|
|
535
535
|
forgen notepad show # 查看会话记事本
|
|
536
|
-
forgen uninstall # 干净地卸载 forgen
|
|
536
|
+
forgen uninstall # 干净地卸载 forgen (Claude Code + Codex 的注册项)
|
|
537
537
|
```
|
|
538
538
|
|
|
539
539
|
### MCP 工具(会话中 Claude 可使用)
|
package/dist/core/health-cli.js
CHANGED
|
@@ -44,19 +44,18 @@ export function computeHealth() {
|
|
|
44
44
|
if (Object.keys(s.philosophy.axisScores).length >= 4)
|
|
45
45
|
profile += 3;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
coverage: Math.round(coverage),
|
|
56
|
-
profile: Math.round(profile),
|
|
57
|
-
},
|
|
58
|
-
grade,
|
|
47
|
+
// 총점은 *표시되는* 항목 점수의 합이어야 한다. 이전엔 합을 반올림(round(Σ))하고 항목은 따로 반올림해서
|
|
48
|
+
// 실 상태에 따라 "항목 합 60 ≠ 총점 61" 이 나왔다 (tests/health-cli 가 머신 상태에 따라 간헐 실패).
|
|
49
|
+
const components = {
|
|
50
|
+
utilization: Math.round(utilization),
|
|
51
|
+
effectiveness: Math.round(effectiveness),
|
|
52
|
+
growth: Math.round(growth),
|
|
53
|
+
coverage: Math.round(coverage),
|
|
54
|
+
profile: Math.round(profile),
|
|
59
55
|
};
|
|
56
|
+
const total = components.utilization + components.effectiveness + components.growth + components.coverage + components.profile;
|
|
57
|
+
const grade = total >= 80 ? 'A' : total >= 60 ? 'B' : total >= 40 ? 'C' : total >= 20 ? 'D' : 'F';
|
|
58
|
+
return { total, components, grade };
|
|
60
59
|
}
|
|
61
60
|
function gradeColor(grade) {
|
|
62
61
|
if (grade === 'A')
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export declare const V1_SESSIONS_DIR: string;
|
|
|
91
91
|
/** ~/.forgen/state/raw-logs/ — Raw Log */
|
|
92
92
|
export declare const V1_RAW_LOGS_DIR: string;
|
|
93
93
|
/** 모든 실행 모드 이름 (cancel/recovery 시 사용) */
|
|
94
|
-
export declare const ALL_MODES: readonly [
|
|
94
|
+
export declare const ALL_MODES: readonly ['ralph', 'autopilot', 'ultrawork', 'team', 'pipeline', 'ccg', 'ralplan', 'deep-interview', 'forge-loop', 'ship', 'retro', 'learn', 'calibrate'];
|
|
95
95
|
/** {repo}/.compound/ — 프로젝트 로컬 디렉토리 */
|
|
96
96
|
export declare function projectDir(cwd: string): string;
|
|
97
97
|
/** {repo}/.compound/pack.link — 팀 팩 연결 파일 */
|
|
@@ -26,7 +26,13 @@ export declare function acquireLock(): void;
|
|
|
26
26
|
* 다른 프로세스의 lock을 존중한다.
|
|
27
27
|
*/
|
|
28
28
|
export declare function releaseLock(): void;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* 임시파일에 쓴 후 rename으로 원자적 교체.
|
|
31
|
+
*
|
|
32
|
+
* 0.5.7 (critic): 기존 파일의 권한과 심링크를 보존한다. 이전엔 새 파일을 기본 mode(0644)로 만들어 rename 해서
|
|
33
|
+
* 0600 이던 `~/.claude.json`(MCP env 에 비밀값이 들어갈 수 있다)이 0644 로 넓어졌고, 심링크였으면 링크가
|
|
34
|
+
* 일반 파일로 바뀌고 원본은 갱신되지 않았다.
|
|
35
|
+
*/
|
|
30
36
|
export declare function atomicWriteFileSync(targetPath: string, data: string): void;
|
|
31
37
|
/**
|
|
32
38
|
* settings.json 안전 읽기.
|
|
@@ -96,11 +96,26 @@ export function releaseLock() {
|
|
|
96
96
|
}
|
|
97
97
|
catch { /* 이미 없으면 무시 */ }
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* 임시파일에 쓴 후 rename으로 원자적 교체.
|
|
101
|
+
*
|
|
102
|
+
* 0.5.7 (critic): 기존 파일의 권한과 심링크를 보존한다. 이전엔 새 파일을 기본 mode(0644)로 만들어 rename 해서
|
|
103
|
+
* 0600 이던 `~/.claude.json`(MCP env 에 비밀값이 들어갈 수 있다)이 0644 로 넓어졌고, 심링크였으면 링크가
|
|
104
|
+
* 일반 파일로 바뀌고 원본은 갱신되지 않았다.
|
|
105
|
+
*/
|
|
100
106
|
export function atomicWriteFileSync(targetPath, data) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
let realTarget = targetPath;
|
|
108
|
+
let mode;
|
|
109
|
+
try {
|
|
110
|
+
realTarget = fs.realpathSync(targetPath);
|
|
111
|
+
mode = fs.statSync(realTarget).mode & 0o777;
|
|
112
|
+
}
|
|
113
|
+
catch { /* 새 파일 — 기본 mode */ }
|
|
114
|
+
const tmpPath = `${realTarget}.tmp.${process.pid}`;
|
|
115
|
+
fs.writeFileSync(tmpPath, data, mode !== undefined ? { mode } : undefined);
|
|
116
|
+
if (mode !== undefined)
|
|
117
|
+
fs.chmodSync(tmpPath, mode); // umask 로 깎였을 수 있어 명시
|
|
118
|
+
fs.renameSync(tmpPath, realTarget);
|
|
104
119
|
}
|
|
105
120
|
/**
|
|
106
121
|
* settings.json 안전 읽기.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* forgen status --blocks [N] 최근 차단 N건 (rule·사유·해결)
|
|
14
14
|
* forgen status --live 실시간 훅 이벤트 스트림
|
|
15
15
|
*/
|
|
16
|
-
declare const VIEWS: readonly [
|
|
16
|
+
declare const VIEWS: readonly ['--compound', '--profile', '--rules', '--blocks', '--live', '--overview'];
|
|
17
17
|
type View = (typeof VIEWS)[number];
|
|
18
18
|
export declare function resolveView(args: string[]): View | null;
|
|
19
19
|
export declare function handleStatus(args: string[]): Promise<void>;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 1원칙: Claude semantics 가 reference. 본 enum 의 의미는 Claude Hook schema 의 행동을 그대로 사용한다.
|
|
9
9
|
*/
|
|
10
|
-
export declare const TRUST_LAYER_INTENTS: readonly [
|
|
10
|
+
export declare const TRUST_LAYER_INTENTS: readonly ['block-completion', 'block-tool-use', 'inject-context', 'observe-only', 'secret-filter', 'forge-loop-state-inject', 'self-evidence-record'];
|
|
11
11
|
export type TrustLayerIntent = (typeof TRUST_LAYER_INTENTS)[number];
|
|
12
12
|
export type CapabilityStatus = 'supported' | 'partial' | 'unsupported';
|
|
13
13
|
export interface CapabilityDeclaration {
|
|
@@ -25,7 +25,7 @@ export interface CapabilityDeclaration {
|
|
|
25
25
|
* `(HOST_IDS as readonly string[]).includes(host)` 로 "유효 host 인지" 를 판정하라
|
|
26
26
|
* (W3-3 리뷰 SEV-3 #5: Record<HostId> 는 Record 리터럴만 강제, 자유비교는 미포착).
|
|
27
27
|
*/
|
|
28
|
-
export declare const HOST_IDS: readonly [
|
|
28
|
+
export declare const HOST_IDS: readonly ['claude', 'codex', 'opencode'];
|
|
29
29
|
export type HostId = (typeof HOST_IDS)[number];
|
|
30
30
|
/**
|
|
31
31
|
* 능력 선언의 검증 수준 (W3-3 리뷰 SEV-3 #1).
|
package/dist/core/uninstall.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/** ADR-016 D3 — ~/.claude/skills/verify 제거 (forgen-managed 마커가 있는 것만; 사용자 스킬은 보존) */
|
|
2
2
|
export declare function cleanVerifySkill(homeDir?: string): boolean;
|
|
3
3
|
/**
|
|
4
|
-
* ADR-016
|
|
5
|
-
*
|
|
4
|
+
* ADR-016 D4 — `~/.claude/skills/forgen-<stack>-<skill>/` dev-guide 스킬 제거.
|
|
5
|
+
* 패키지가 실제로 제공하는 이름만 지운다 (사용자가 만든 `forgen-react-mine` 같은 스킬은 보존).
|
|
6
6
|
*/
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function cleanDevGuideSkills(pkgRoot: string, homeDir?: string): number;
|
|
8
|
+
/**
|
|
9
|
+
* ADR-016 D4 — `~/.claude.json` 의 `mcpServers["forgen-compound"]` 제거. install 은 settings.json 이 아니라
|
|
10
|
+
* 여기에 등록하는데 uninstall 은 settings.json 만 정리하고 있었다. forgen 서버 경로를 가리킬 때만 지운다.
|
|
11
|
+
*/
|
|
12
|
+
export declare function cleanClaudeJsonMcp(homeDir?: string): boolean;
|
|
8
13
|
/** forgen uninstall 메인 */
|
|
9
14
|
export declare function handleUninstall(cwd: string, options: {
|
|
10
15
|
force?: boolean;
|
package/dist/core/uninstall.js
CHANGED
|
@@ -2,6 +2,8 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as readline from 'node:readline';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { hasManagedSkillMarker, listDevGuideSkillNames, removeSkillFile } from '../host/managed-marker.js';
|
|
5
7
|
import { SETTINGS_PATH, acquireLock, releaseLock, atomicWriteFileSync, } from './settings-lock.js';
|
|
6
8
|
/** 플러그인 제거 (plugin-installer.ts 삭제 후 인라인) */
|
|
7
9
|
function uninstallPlugin() {
|
|
@@ -103,8 +105,7 @@ export function cleanVerifySkill(homeDir = os.homedir()) {
|
|
|
103
105
|
try {
|
|
104
106
|
if (fs.lstatSync(dir).isSymbolicLink() || fs.lstatSync(file).isSymbolicLink())
|
|
105
107
|
return false;
|
|
106
|
-
|
|
107
|
-
if (!/^---\n[\s\S]*?\n---\n\s*<!-- forgen-managed -->/.test(content))
|
|
108
|
+
if (!hasManagedSkillMarker(fs.readFileSync(file, 'utf-8')))
|
|
108
109
|
return false;
|
|
109
110
|
fs.unlinkSync(file);
|
|
110
111
|
try {
|
|
@@ -119,22 +120,41 @@ export function cleanVerifySkill(homeDir = os.homedir()) {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
/**
|
|
122
|
-
* ADR-016
|
|
123
|
-
*
|
|
123
|
+
* ADR-016 D4 — `~/.claude/skills/forgen-<stack>-<skill>/` dev-guide 스킬 제거.
|
|
124
|
+
* 패키지가 실제로 제공하는 이름만 지운다 (사용자가 만든 `forgen-react-mine` 같은 스킬은 보존).
|
|
124
125
|
*/
|
|
125
|
-
export
|
|
126
|
-
const
|
|
126
|
+
export function cleanDevGuideSkills(pkgRoot, homeDir = os.homedir()) {
|
|
127
|
+
const skillsDir = path.join(homeDir, '.claude', 'skills');
|
|
128
|
+
let removed = 0;
|
|
129
|
+
for (const name of listDevGuideSkillNames(pkgRoot)) {
|
|
130
|
+
if (removeSkillFile(skillsDir, name, false))
|
|
131
|
+
removed += 1;
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* ADR-016 D4 — `~/.claude.json` 의 `mcpServers["forgen-compound"]` 제거. install 은 settings.json 이 아니라
|
|
137
|
+
* 여기에 등록하는데 uninstall 은 settings.json 만 정리하고 있었다. forgen 서버 경로를 가리킬 때만 지운다.
|
|
138
|
+
*/
|
|
139
|
+
export function cleanClaudeJsonMcp(homeDir = os.homedir()) {
|
|
140
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
127
141
|
try {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (!r.removed)
|
|
142
|
+
const data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
|
|
143
|
+
const entry = data.mcpServers?.['forgen-compound'];
|
|
144
|
+
if (!entry)
|
|
132
145
|
return false;
|
|
133
|
-
|
|
146
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
147
|
+
const isForgenServer = args.some((a) => typeof a === 'string' && /[\\/]dist[\\/]mcp[\\/]server\.js$/.test(a));
|
|
148
|
+
if (!isForgenServer)
|
|
149
|
+
return false; // 사용자가 같은 이름으로 다른 서버를 등록
|
|
150
|
+
delete data.mcpServers?.['forgen-compound'];
|
|
151
|
+
if (data.mcpServers && Object.keys(data.mcpServers).length === 0)
|
|
152
|
+
delete data.mcpServers;
|
|
153
|
+
atomicWriteFileSync(claudeJsonPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
134
154
|
return true;
|
|
135
155
|
}
|
|
136
156
|
catch {
|
|
137
|
-
return false; //
|
|
157
|
+
return false; // 파일 없음 / 파싱 실패 — 건드리지 않는다
|
|
138
158
|
}
|
|
139
159
|
}
|
|
140
160
|
/** 사용자에게 y/n 확인 */
|
|
@@ -343,9 +363,10 @@ export async function handleUninstall(cwd, options) {
|
|
|
343
363
|
console.log(' 3. Delete .claude/rules/ rule files (project-context, routing, forge-*)');
|
|
344
364
|
console.log(' 4. Remove forgen block from CLAUDE.md');
|
|
345
365
|
console.log(' 5. Remove slash commands (~/.claude/commands/forgen/) and the forgen-managed verify skill (~/.claude/skills/verify/)');
|
|
346
|
-
console.log(' 6. Remove plugin artifacts (cache, installed_plugins.json, plugin directory)');
|
|
366
|
+
console.log(' 6. Remove plugin artifacts (cache, installed_plugins.json, plugin directory), dev-guide skills, and the forgen-compound MCP entry in ~/.claude.json');
|
|
367
|
+
console.log(' 7. Codex (if $CODEX_HOME or ~/.codex exists): forgen hooks in hooks.json, the MCP/notify blocks in config.toml, forgen skills, ch-*.toml agents, and the AGENTS.md block — hooks and files from other tools are kept');
|
|
347
368
|
if (options.purge) {
|
|
348
|
-
console.log('
|
|
369
|
+
console.log(' 8. --purge: Delete ~/.forgen/ entirely (rules, me/, state/, solutions/, behavior/)');
|
|
349
370
|
console.log(' WARNING: this erases all accumulated corrections, rules, drift, and lifecycle history.');
|
|
350
371
|
}
|
|
351
372
|
else {
|
|
@@ -373,8 +394,21 @@ export async function handleUninstall(cwd, options) {
|
|
|
373
394
|
cleanSlashCommands();
|
|
374
395
|
if (cleanVerifySkill())
|
|
375
396
|
console.log(' ✓ Removed forgen verify skill (~/.claude/skills/verify/)');
|
|
376
|
-
|
|
377
|
-
|
|
397
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
398
|
+
const devGuide = cleanDevGuideSkills(pkgRoot);
|
|
399
|
+
if (devGuide > 0)
|
|
400
|
+
console.log(` ✓ Removed ${devGuide} dev-guide skill(s) (~/.claude/skills/forgen-*)`);
|
|
401
|
+
if (cleanClaudeJsonMcp())
|
|
402
|
+
console.log(' ✓ Removed forgen-compound MCP server from ~/.claude.json');
|
|
403
|
+
// ADR-016 D4 — Codex 에 등록한 것도 되돌린다 (다른 도구의 훅과 그 신뢰는 보존)
|
|
404
|
+
try {
|
|
405
|
+
const { planCodexUninstall, renderCodexUninstall } = await import('../host/uninstall-codex.js');
|
|
406
|
+
for (const line of renderCodexUninstall(planCodexUninstall({ pkgRoot })))
|
|
407
|
+
console.log(line);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
console.error(' ✗ Codex cleanup failed:', e instanceof Error ? e.message : String(e));
|
|
411
|
+
}
|
|
378
412
|
cleanPluginArtifacts();
|
|
379
413
|
if (options.purge) {
|
|
380
414
|
try {
|
|
@@ -392,5 +426,5 @@ export async function handleUninstall(cwd, options) {
|
|
|
392
426
|
console.log(` ✗ ~/.forgen/ deletion failed: ${e.message}`);
|
|
393
427
|
}
|
|
394
428
|
}
|
|
395
|
-
console.log('\n[forgen] Uninstall complete. Restart Claude Code for a clean state.\n');
|
|
429
|
+
console.log('\n[forgen] Uninstall complete. Restart Claude Code (and Codex) for a clean state.\n');
|
|
396
430
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
4
|
import { DEFAULT_EVIDENCE } from './solution-format.js';
|
|
5
5
|
import { diagnoseFromRawContent } from './solution-quarantine.js';
|
|
6
6
|
import { createLogger } from '../core/logger.js';
|
|
@@ -83,7 +83,7 @@ function tryFix(content, initialErrors) {
|
|
|
83
83
|
const body = trimmed.slice(endIdx + 3);
|
|
84
84
|
let fm;
|
|
85
85
|
try {
|
|
86
|
-
const parsed = yaml.load(fmRaw, { schema: yaml.
|
|
86
|
+
const parsed = yaml.load(fmRaw, { schema: yaml.CORE_SCHEMA });
|
|
87
87
|
if (parsed == null || typeof parsed !== 'object') {
|
|
88
88
|
return { changed: false, added, remaining: initialErrors, content };
|
|
89
89
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import yaml from 'js-yaml';
|
|
1
|
+
import * as yaml from 'js-yaml';
|
|
2
2
|
export const DEFAULT_EVIDENCE = {
|
|
3
3
|
injected: 0,
|
|
4
4
|
reflected: 0,
|
|
@@ -106,7 +106,7 @@ export function parseFrontmatterOnly(content) {
|
|
|
106
106
|
const anchorCount = (raw.match(/(?<=\s|^)&\w+/g) ?? []).length;
|
|
107
107
|
if (anchorCount > 3)
|
|
108
108
|
return null;
|
|
109
|
-
const parsed = yaml.load(raw, { schema: yaml.
|
|
109
|
+
const parsed = yaml.load(raw, { schema: yaml.CORE_SCHEMA });
|
|
110
110
|
if (!validateFrontmatter(parsed))
|
|
111
111
|
return null;
|
|
112
112
|
return parsed;
|
|
@@ -156,8 +156,8 @@ export function parseSolutionV3(content) {
|
|
|
156
156
|
export function serializeSolutionV3(solution) {
|
|
157
157
|
const yamlStr = yaml.dump(solution.frontmatter, {
|
|
158
158
|
lineWidth: -1,
|
|
159
|
-
|
|
160
|
-
schema: yaml.
|
|
159
|
+
quoteStyle: 'double',
|
|
160
|
+
schema: yaml.CORE_SCHEMA,
|
|
161
161
|
});
|
|
162
162
|
return `---\n${yamlStr}---\n\n## Context\n${solution.context}\n\n## Content\n${solution.content}\n`;
|
|
163
163
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
4
|
import { SOLUTION_QUARANTINE_PATH, STATE_DIR } from '../core/paths.js';
|
|
5
5
|
import { diagnoseFrontmatter } from './solution-format.js';
|
|
6
6
|
import { createLogger } from '../core/logger.js';
|
|
@@ -24,7 +24,7 @@ export function diagnoseFromRawContent(content) {
|
|
|
24
24
|
return ['frontmatter too large (>5000 chars — YAML bomb guard)'];
|
|
25
25
|
let parsed;
|
|
26
26
|
try {
|
|
27
|
-
parsed = yaml.load(raw, { schema: yaml.
|
|
27
|
+
parsed = yaml.load(raw, { schema: yaml.CORE_SCHEMA });
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
|
30
30
|
return [`YAML parse error: ${e instanceof Error ? e.message : String(e)}`];
|
|
@@ -18,6 +18,7 @@ import * as fs from 'node:fs';
|
|
|
18
18
|
import * as os from 'node:os';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { generateHooksJson } from '../hooks/hooks-generator.js';
|
|
21
|
+
import { hasManagedSkillMarker } from './managed-marker.js';
|
|
21
22
|
const PLUGIN_KEY = 'forgen@forgen-local';
|
|
22
23
|
const FORGEN_MANAGED_MARKER = '<!-- forgen-managed -->';
|
|
23
24
|
function readPkgVersion(pkgRoot) {
|
|
@@ -303,8 +304,6 @@ function installDevGuideSkills(opts) {
|
|
|
303
304
|
return { skillsPath: skillsDir, skillsInstalled: installed, skillsRemoved: removed };
|
|
304
305
|
}
|
|
305
306
|
// ── 6. verify skill (ADR-016 D3) ───────────────────────────────────────
|
|
306
|
-
/** frontmatter 직후에 forgen-managed 마커가 있는 SKILL.md 만 forgen 소유로 본다. */
|
|
307
|
-
const MANAGED_SKILL_RE = /^---\n[\s\S]*?\n---\n\s*<!-- forgen-managed -->/;
|
|
308
307
|
/** `<skillsDir>/verify` 가 사용자 소유인가 (심링크 / 마커 없는 SKILL.md / SKILL.md 없이 다른 파일만 있음). */
|
|
309
308
|
export function isUserOwnedVerifySkill(skillsDir) {
|
|
310
309
|
const dir = path.join(skillsDir, 'verify');
|
|
@@ -319,7 +318,7 @@ export function isUserOwnedVerifySkill(skillsDir) {
|
|
|
319
318
|
try {
|
|
320
319
|
if (fs.lstatSync(file).isSymbolicLink())
|
|
321
320
|
return true;
|
|
322
|
-
return !
|
|
321
|
+
return !hasManagedSkillMarker(fs.readFileSync(file, 'utf-8'));
|
|
323
322
|
}
|
|
324
323
|
catch {
|
|
325
324
|
// SKILL.md 없음 — 디렉토리에 다른 것이 있으면 사용자가 만들던 것
|
|
@@ -90,26 +90,49 @@ export interface CodexHookTrustAudit {
|
|
|
90
90
|
/** config.toml 자체가 없거나 hooks.state 가 전혀 없으면 true (Codex 가 아직 한 번도 훅을 review 안 함) */
|
|
91
91
|
noStateRecorded: boolean;
|
|
92
92
|
}
|
|
93
|
+
export declare const FORGEN_SKILL_MARKER = "<!-- forgen-managed -->";
|
|
94
|
+
/** 훅 command 문자열이 forgen 소유인가. */
|
|
95
|
+
export declare function isForgenHookCommand(command: unknown, pkgRoot: string): boolean;
|
|
96
|
+
export declare function readJsonFile<T>(p: string): T | null;
|
|
97
|
+
/**
|
|
98
|
+
* forgen MCP 블록 제거 (uninstall, ADR-016 D4). forgen 테이블(본문 + 하위 테이블)과 마커 줄만 걷어낸다.
|
|
99
|
+
* 마커도 시그니처도 없는 같은 이름의 테이블(사용자 관리)은 건드리지 않는다.
|
|
100
|
+
* `removed` 는 테이블을 실제로 지웠을 때만 true — 고아 마커만 치운 경우는 false.
|
|
101
|
+
*/
|
|
102
|
+
export declare function removeMcpBlock(currentToml: string): {
|
|
103
|
+
content: string;
|
|
104
|
+
removed: boolean;
|
|
105
|
+
};
|
|
93
106
|
/**
|
|
94
107
|
* config.toml 에 forgen notify 블록을 upsert.
|
|
95
108
|
*
|
|
96
109
|
* - Codex 의 `notify` 는 top-level 단일 argv 다. 사용자가 이미 정의했으면 **건드리지 않는다** — 그리고
|
|
97
|
-
* forgen
|
|
110
|
+
* forgen 줄이 남아 있으면 제거한다 (중복 키 = config.toml 파싱 실패 → Codex 기동 불가).
|
|
98
111
|
* - top-level 키는 첫 테이블 헤더 앞에 와야 하므로 블록은 항상 파일 최상단(BOM 뒤)에 둔다.
|
|
99
|
-
* - 사용자가 forgen
|
|
112
|
+
* - 사용자가 forgen argv 뒤에 `"--", "<prog>", …` 로 자기 notifier 를 체인해 뒀으면 그 꼬리를 보존.
|
|
100
113
|
* 블록의 notify 줄을 한 줄 JSON 으로 읽을 수 없으면(여러 줄 배열 등 손편집) 아무것도 바꾸지 않는다.
|
|
101
|
-
* -
|
|
114
|
+
* - forgen 이 쓴 줄 외에는 원래 순서 그대로 둔다 (Codex 가 사이에 끼워 넣은 root 키 포함).
|
|
102
115
|
*/
|
|
103
116
|
export declare function upsertNotifyBlock(currentToml: string, pkgRoot: string): {
|
|
104
117
|
content: string;
|
|
105
118
|
status: CodexNotifyStatus;
|
|
106
119
|
};
|
|
107
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* forgen notify 블록 제거 (`--no-notify`, uninstall).
|
|
122
|
+
*
|
|
123
|
+
* - 사용자가 블록의 notify 줄을 여러 줄 배열 등으로 손편집했으면(`custom`) **건드리지 않는다** — 첫 줄만
|
|
124
|
+
* 지우면 남은 줄이 깨진 TOML 이 되어 Codex 가 기동하지 못한다 (critic 2026-10-02).
|
|
125
|
+
* - `"--"` 뒤에 사용자가 체인해 둔 자기 notifier 가 있으면 그 argv 만으로 `notify` 를 되돌려 놓는다.
|
|
126
|
+
* - `removed` 는 forgen notify 줄을 실제로 지웠을 때만 true. 고아 마커/주석만 치운 경우는 false
|
|
127
|
+
* (내용은 정리된 것을 돌려준다).
|
|
128
|
+
*/
|
|
108
129
|
export declare function removeNotifyBlock(currentToml: string): {
|
|
109
130
|
content: string;
|
|
110
131
|
removed: boolean;
|
|
132
|
+
custom: boolean;
|
|
133
|
+
restoredChain: string[];
|
|
111
134
|
};
|
|
112
|
-
interface HooksFile {
|
|
135
|
+
export interface HooksFile {
|
|
113
136
|
description?: string;
|
|
114
137
|
hooks: Record<string, Array<unknown>>;
|
|
115
138
|
}
|
|
@@ -154,6 +177,8 @@ export declare function auditCodexHookTrust(opts: {
|
|
|
154
177
|
hooksFile?: HooksFile | null;
|
|
155
178
|
configToml?: string;
|
|
156
179
|
}): CodexHookTrustAudit;
|
|
180
|
+
export declare const AGENT_TOML_MARKER = "# forgen-managed";
|
|
181
|
+
export declare const AGENT_NAME_PREFIX = "ch-";
|
|
157
182
|
/**
|
|
158
183
|
* Claude agent .md → Codex agent role TOML.
|
|
159
184
|
* 공식 스키마(필수 name/description/developer_instructions, 선택 model_reasoning_effort/sandbox_mode)
|
|
@@ -163,6 +188,7 @@ export declare function renderCodexAgentToml(file: string, raw: string): {
|
|
|
163
188
|
name: string;
|
|
164
189
|
toml: string;
|
|
165
190
|
} | null;
|
|
191
|
+
export declare const DEV_GUIDE_SKILL_PATTERN: RegExp;
|
|
166
192
|
/** ADR-014 D3 — Codex 스킬에는 `$ARGUMENTS` 치환 변수가 없다. */
|
|
167
193
|
export declare function adaptSkillBodyForCodex(body: string): string;
|
|
168
194
|
export declare function resolveAgentsMdPath(_pkgRoot: string): string;
|
|
@@ -173,4 +199,11 @@ export declare function upsertForgenRulesInAgentsMd(opts: {
|
|
|
173
199
|
}): {
|
|
174
200
|
injected: boolean;
|
|
175
201
|
};
|
|
176
|
-
|
|
202
|
+
/** AGENTS.md 의 forgen 블록 제거 (uninstall). 블록뿐이던 파일은 삭제한다. */
|
|
203
|
+
export declare function removeForgenRulesFromAgentsMd(opts: {
|
|
204
|
+
agentsMdPath: string;
|
|
205
|
+
dryRun: boolean;
|
|
206
|
+
}): {
|
|
207
|
+
removed: boolean;
|
|
208
|
+
fileDeleted: boolean;
|
|
209
|
+
};
|