@gon1942/ai-dev-external-framework 0.0.0-stage → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +476 -2
  3. package/bin/ai-dev.js +22 -0
  4. package/lib/init.js +32 -0
  5. package/lib/install.js +46 -0
  6. package/lib/repair.js +27 -0
  7. package/lib/status.js +16 -0
  8. package/package.json +34 -4
  9. package/scripts/external-install.py +101 -0
  10. package/scripts/publish-check.cjs +8 -0
  11. package/scripts/verification.py +40 -0
  12. package/scripts/workflow.py +165 -0
  13. package/templates/.ai-dev/spec-adapter.json +5 -0
  14. package/templates/.ai-dev/verification-profiles.json +37 -0
  15. package/templates/AGENTS.md +23 -0
  16. package/templates/ARCHITECTURE.md +3 -0
  17. package/templates/docs/DESIGN.md +3 -0
  18. package/templates/docs/FRONTEND.md +3 -0
  19. package/templates/docs/PLANS.md +23 -0
  20. package/templates/docs/PRODUCT_SENSE.md +3 -0
  21. package/templates/docs/QUALITY_SCORE.md +3 -0
  22. package/templates/docs/RELIABILITY.md +3 -0
  23. package/templates/docs/SECURITY.md +3 -0
  24. package/templates/docs/ai-workflow/EXTERNAL-SKILLS.md +13 -0
  25. package/templates/docs/ai-workflow/POLICY.md +8 -0
  26. package/templates/docs/design-docs/core-beliefs.md +3 -0
  27. package/templates/docs/design-docs/index.md +3 -0
  28. package/templates/docs/exec-plans/active/.gitkeep +0 -0
  29. package/templates/docs/exec-plans/completed/.gitkeep +0 -0
  30. package/templates/docs/exec-plans/tech-debt-tracker.md +4 -0
  31. package/templates/docs/generated/db-schema.md +3 -0
  32. package/templates/docs/product-specs/index.md +3 -0
  33. package/templates/docs/references/design-system-reference-llms.txt +1 -0
  34. package/templates/docs/references/nixpacks-llms.txt +1 -0
  35. package/templates/docs/references/uv-llms.txt +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,477 @@
1
- # Temporary Holding Version
1
+ # AI Dev External Framework
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **OpenCode · Codex · Claude Code에서 동일한 개발 절차를 적용하기 위한 외부 스킬 기반 공통 프레임워크**
4
+
5
+ > 버전: **0.8.2** · npm 패키지: **`@gon1942/ai-dev-external-framework`**
6
+ > 이 프로젝트는 별도의 AI 코딩 에이전트가 아니라, 기존 에이전트에 **원본 외부 스킬·작업 기록·검증 게이트·세션 상태 확인 설정**을 추가하는 설치 도구입니다.
7
+
8
+ ## 목차
9
+
10
+ 1. [무엇을 해결하나요?](#1-무엇을-해결하나요)
11
+ 2. [빠른 시작](#2-빠른-시작)
12
+ 3. [설치되는 구성 요소](#3-설치되는-구성-요소)
13
+ 4. [외부 스킬 18개](#4-외부-스킬-18개)
14
+ 5. [실제 개발 작업 흐름](#5-실제-개발-작업-흐름)
15
+ 6. [새 세션 시작과 상태 확인](#6-새-세션-시작과-상태-확인)
16
+ 7. [CLI 명령어](#7-cli-명령어)
17
+ 8. [기존 프로젝트 업그레이드](#8-기존-프로젝트-업그레이드)
18
+ 9. [오프라인·재현 가능한 설치](#9-오프라인재현-가능한-설치)
19
+ 10. [문제 해결](#10-문제-해결)
20
+ 11. [기술적 한계와 운영 시 주의사항](#11-기술적-한계와-운영-시-주의사항)
21
+ 12. [개발 및 npm 배포](#12-개발-및-npm-배포)
22
+
23
+ ---
24
+
25
+ ## 1. 무엇을 해결하나요?
26
+
27
+ AI 코딩 에이전트로 작업하면 도구마다 계획서 형식이 다르거나, 테스트를 실행하지 않고 완료를 보고하거나, 다음 세션에서 이전 작업 상태를 파악하지 못하는 문제가 생길 수 있습니다.
28
+
29
+ 이 프레임워크는 다음 목표를 갖습니다.
30
+
31
+ - **공통 작업 흐름:** 요구사항 분석 → 계획 → 사용자 승인 → 구현 → 검증 → 리뷰 → 최종 승인
32
+ - **외부 원본 스킬 재사용:** Matt Pocock, Superpowers, Ponytail의 스킬 파일을 원본 그대로 설치합니다. 자체 제작 대체 `analyze`/`plan`/`implement` 스킬을 만들지 않습니다.
33
+ - **증거 기반 검증:** 인수 조건(AC)에 연결된 실제 명령 실행 기록과 테스트 로그를 남깁니다.
34
+ - **세션 이어가기:** 새 세션에서 `active` 작업을 확인하여 진행 중인 작업을 이어갈 수 있게 돕습니다.
35
+ - **승인 경계 유지:** 구현 전 계획 승인과 최종 완료 승인은 사용자에게 있습니다. 에이전트가 임의로 승인했다고 간주하지 않아야 합니다.
36
+
37
+ ```text
38
+ 사용자 요청
39
+ ↓
40
+ 요구사항 분석 → 명세/AC → 실행 계획
41
+ ↓
42
+ 사용자 계획 승인
43
+ ↓
44
+ 구현 → AC별 검증 → 리뷰
45
+ ↓
46
+ 사용자 완료 승인
47
+ ↓
48
+ docs/exec-plans/completed/ 로 이동
49
+ ```
50
+
51
+ **역할 구분:** 원본 스킬은 에이전트의 작업 방법을 돕고, 프로젝트 안의 `scripts/workflow.py`는 작업 상태와 검증 기록을 관리합니다. 스킬을 설치했다는 사실만으로 검증이 통과하거나 Hook이 활성화되는 것은 아닙니다.
52
+
53
+ ## 2. 빠른 시작
54
+
55
+ ### 준비 환경
56
+
57
+ - **Node.js 20 이상**과 npm/npx
58
+ - **Python 3**, **Git**
59
+ - OpenCode, Codex, Claude Code 중 사용할 에이전트
60
+ - 기본 설치 시 GitHub에 접속할 인터넷 환경
61
+
62
+ ### 새 프로젝트에 설치
63
+
64
+ ```bash
65
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 init my-project
66
+ cd my-project
67
+ ```
68
+
69
+ `init`은 디렉터리가 없다면 생성하고 프로젝트 문서·상태 조회 파일·에이전트 설정·원본 외부 스킬 설치를 순서대로 시도합니다. 기본값은 `--agents all`입니다.
70
+
71
+ 설치가 정상인지 먼저 확인합니다.
72
+
73
+ ```bash
74
+ node .ai-dev/session-status.cjs
75
+ python3 scripts/workflow.py session
76
+
77
+ # 설치된 스킬 개수 확인
78
+ for dir in .agents .opencode .claude; do
79
+ printf '%s: ' "$dir"
80
+ find "$dir/skills" -name SKILL.md -type f 2>/dev/null | wc -l
81
+ done
82
+ ```
83
+
84
+ 각 에이전트를 모두 선택하고 18개 스킬 설치에 성공했다면 디렉터리별로 **18개**의 `SKILL.md`가 검색됩니다. 설치된 원본 저장소의 구성이나 다운로드 결과에 따라 실제 상태가 다를 수 있으므로 반드시 확인하세요.
85
+
86
+ 그다음 프로젝트 폴더에서 평소 사용하는 에이전트를 실행합니다.
87
+
88
+ ```bash
89
+ opencode
90
+ # 또는 codex
91
+ # 또는 claude
92
+ ```
93
+
94
+ 예시 요청: **“Todo CLI 기능을 만들어줘. 먼저 요구사항과 인수 조건, 구현 계획을 제시하고 승인받은 뒤 개발해.”**
95
+
96
+ ### 기존 프로젝트에 적용
97
+
98
+ ```bash
99
+ cd /path/to/existing-project
100
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 init .
101
+ ```
102
+
103
+ 기존 `AGENTS.md`가 있으면 보존합니다. 프레임워크 작업 지침이 자동 병합되지는 않으므로 설치 메시지와 파일 내용을 확인하세요. 관리 대상으로 취급되는 일부 파일이 이미 존재하며 내용이 다른 경우, 무단 덮어쓰기 대신 충돌 오류가 발생할 수 있습니다.
104
+
105
+ ### 설치 미리보기
106
+
107
+ ```bash
108
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 init my-project --dry-run
109
+ ```
110
+
111
+ 파일 변경과 원본 저장소 다운로드를 수행하지 않고 설치 예정 동작을 출력합니다. 새로 생성할 프로젝트라면 전체 파일 목록 대신 예상 동작만 표시될 수 있습니다.
112
+
113
+ ## 3. 설치되는 구성 요소
114
+
115
+ 대표적인 구조는 다음과 같습니다.
116
+
117
+ ```text
118
+ my-project/
119
+ ├── AGENTS.md # 세 에이전트에 적용할 공통 운영 지침
120
+ ├── ARCHITECTURE.md # 프로젝트 아키텍처 기록용 문서
121
+ ├── docs/
122
+ │ ├── PLANS.md # 계획 및 작업 지침
123
+ │ ├── ai-workflow/ # 공통 정책
124
+ │ └── exec-plans/
125
+ │ ├── active/ # 현재 진행 중인 작업
126
+ │ └── completed/ # 최종 완료된 작업
127
+ ├── scripts/
128
+ │ └── workflow.py # 작업 상태·승인·AC 증거·완료 처리
129
+ ├── .ai-dev/
130
+ │ ├── session-status.cjs # 프로젝트 로컬 세션 상태 조회
131
+ │ ├── status-engine.cjs # 상태 조회 처리 모듈
132
+ │ ├── spec-adapter.json # Markdown/OpenSpec 연결 설정
133
+ │ └── verification-profiles.json # 파일 유형별 검증 프로파일
134
+ ├── .agents/skills/ # Codex용 원본 스킬
135
+ ├── .opencode/
136
+ │ ├── skills/ # OpenCode용 원본 스킬
137
+ │ └── plugins/ai-dev-session.js # 세션 이벤트용 플러그인 파일
138
+ ├── .claude/
139
+ │ ├── skills/ # Claude Code용 원본 스킬
140
+ │ └── settings.json # 세션 시작 Hook 설정
141
+ ├── .codex/hooks.json # Codex용 Hook 설정 파일
142
+ └── .external-skills-lock.json # 원본 리비전·파일 해시 기록
143
+ ```
144
+
145
+ 위 구조는 설치되는 주요 요소를 설명하기 위한 것입니다. 일부 폴더와 파일은 설치 옵션, 첫 작업 생성 여부에 따라 달라질 수 있습니다. `.agents/skills`는 이 프레임워크의 **Codex용 설치 대상 경로**입니다.
146
+
147
+ ## 4. 외부 스킬 18개
148
+
149
+ 아래는 현재 설치 프로그램의 **지정 목록**입니다. 실제 GitHub 원본에 같은 경로의 `SKILL.md`가 있어야 설치가 성공합니다. 원본 프로젝트의 향후 변경으로 설치가 실패할 수 있습니다.
150
+
151
+ ### Matt Pocock — 8개
152
+
153
+ | 스킬 | 활용 목적 |
154
+ |---|---|
155
+ | `grill-with-docs` | 질문으로 요구사항과 누락 조건 구체화 |
156
+ | `diagnosing-bugs` | 버그 원인과 재현 조건 분석 |
157
+ | `to-spec` | 요구사항을 명세로 정리 |
158
+ | `to-tickets` | 명세를 실행 가능한 작업으로 분해 |
159
+ | `implement` | 정의된 작업의 구현 지원 |
160
+ | `implement-spec` | 명세에 근거한 구현 지원 |
161
+ | `code-review` | 코드 변경 사항 검토 |
162
+ | `retro` | 수행 결과 회고 |
163
+
164
+ 출처: [mattpocock/skills](https://github.com/mattpocock/skills)
165
+
166
+ ### Superpowers — 9개
167
+
168
+ | 스킬 | 활용 목적 |
169
+ |---|---|
170
+ | `brainstorming` | 구현 전에 문제와 설계 방향 구체화 |
171
+ | `writing-plans` | 구현 계획 구체화 |
172
+ | `systematic-debugging` | 체계적인 결함 추적 |
173
+ | `test-driven-development` | 실패 테스트 → 구현 → 개선 흐름 지원 |
174
+ | `using-git-worktrees` | 독립 작업 공간 활용 |
175
+ | `subagent-driven-development` | 하위 에이전트 작업 분담 |
176
+ | `verification-before-completion` | 완료 선언 전 실행 검증 |
177
+ | `requesting-code-review` | 리뷰 요청 |
178
+ | `receiving-code-review` | 리뷰 결과 확인 및 반영 |
179
+
180
+ 출처: [obra/superpowers](https://github.com/obra/superpowers)
181
+
182
+ ### Ponytail — 1개
183
+
184
+ | 스킬 | 활용 목적 |
185
+ |---|---|
186
+ | `ponytail` | 에이전트 개발 작업 보조 지침 |
187
+
188
+ 출처: [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)
189
+
190
+ **주의:** Ponytail은 이 패키지에서 **스킬 파일만 설치**합니다. 원본 Ponytail의 별도 플러그인, 실행 Hook, 모드 전환 등은 자동으로 활성화되지 않습니다. Superpowers나 다른 원본 저장소의 런타임 플러그인도 이 설치 도구가 자동으로 완성해 주는 것은 아닙니다.
191
+
192
+ ## 5. 실제 개발 작업 흐름
193
+
194
+ 사용자는 자연어로 요청하고, 에이전트가 프로젝트에 설치된 스킬과 로컬 `workflow.py`를 이용해 각 단계를 수행하는 방식입니다.
195
+
196
+ ### ① 신규 작업과 계획
197
+
198
+ ```bash
199
+ python3 scripts/workflow.py new --id TASK-TODO-CLI --title 'Todo CLI 개발'
200
+ ```
201
+
202
+ 다음과 같은 작업 디렉터리가 생성됩니다.
203
+
204
+ ```text
205
+ docs/exec-plans/active/TASK-TODO-CLI/
206
+ ├── analysis.md # 분석 결과
207
+ ├── spec.md # 요구사항·인수 조건
208
+ ├── plan.md # 실행 계획
209
+ ├── ac-tests.json # AC별 검증 명령 등록
210
+ └── status.json # 작업 상태
211
+ ```
212
+
213
+ 에이전트가 분석·명세·계획을 작성하고 사용자에게 승인 요청을 해야 합니다. 아래 승인 명령은 **실제 사용자 승인 이후에만** 실행해야 합니다.
214
+
215
+ ```bash
216
+ python3 scripts/workflow.py approve --id TASK-TODO-CLI --user-confirmed
217
+ ```
218
+
219
+ `--user-confirmed`는 에이전트가 사용자 승인이 있었다고 선언하는 플래그이지, 신원 인증이나 승인 사실을 자동 판별하는 기능이 아닙니다.
220
+
221
+ ### ② 구현 및 검증
222
+
223
+ 계획 승인 이후 구현을 시작합니다. 검증 전에 `ac-tests.json`에 AC별 실제 실행 명령을 등록해야 합니다.
224
+
225
+ ```bash
226
+ python3 scripts/workflow.py gate --id TASK-TODO-CLI --stage implement
227
+ python3 scripts/workflow.py run --id TASK-TODO-CLI --ac AC-01 -- npm test
228
+ python3 scripts/workflow.py gate --id TASK-TODO-CLI --stage review
229
+ ```
230
+
231
+ 위 `npm test`는 **예시**입니다. 실제 프로젝트에 존재하는 테스트 명령이어야 하며 `ac-tests.json`에도 해당 명령이 등록되어 있어야 합니다. `run`은 실행 결과·로그·소스 상태를 검증 증거로 기록합니다.
232
+
233
+ ### ③ 리뷰 및 최종 완료
234
+
235
+ ```bash
236
+ # 리뷰 결과 문서를 작성한 후에 실행
237
+ python3 scripts/workflow.py review --id TASK-TODO-CLI --verdict PASS --notes-file review-notes.md
238
+
239
+ # 완료 조건 검사
240
+ python3 scripts/workflow.py gate --id TASK-TODO-CLI --stage complete
241
+
242
+ # 사용자가 최종 완료를 명시적으로 승인한 뒤에만 실행
243
+ python3 scripts/workflow.py complete --id TASK-TODO-CLI --user-confirmed
244
+ ```
245
+
246
+ 정상적으로 완료 처리된 작업은 `active/TASK-TODO-CLI/`에서 `completed/TASK-TODO-CLI/`로 이동합니다. **테스트 파일이나 리뷰 기록이 존재한다는 이유만으로 자동 이동하지 않습니다.** `review` 명령에 PASS를 기록하는 것과 실질적으로 독립적인 코드 리뷰를 수행했다는 것은 서로 다릅니다.
247
+
248
+ ## 6. 새 세션 시작과 상태 확인
249
+
250
+ v0.8.1에서는 `AGENTS.md`가 `ai-dev status` 명령을 호출했지만, `npx`로 설치한 뒤에는 해당 명령이 PATH에 없어서 실패할 수 있었습니다.
251
+
252
+ **v0.8.2의 새 프로젝트 템플릿은 프로젝트 로컬 명령을 사용합니다.**
253
+
254
+ ```bash
255
+ node .ai-dev/session-status.cjs
256
+ ```
257
+
258
+ 보다 자세한 작업 관리 상태는 다음 명령으로 확인할 수 있습니다.
259
+
260
+ ```bash
261
+ python3 scripts/workflow.py session
262
+ python3 scripts/workflow.py session --json
263
+ ```
264
+
265
+ 세션 상태 조회는 **읽기 전용**입니다. 새 세션이 열렸다는 이유만으로 작업을 생성하거나, 승인 처리하거나, 테스트 완료로 표시하지 않습니다.
266
+
267
+ OpenCode 플러그인과 Claude/Codex Hook 설정 파일을 생성하지만, **각 호스트의 현재 버전에서 자동 실행되는지는 별도 확인이 필요**합니다. 특히 설정 파일이 있다는 사실만으로 실시간 Hook 동작이 검증된 것은 아닙니다.
268
+
269
+ ## 7. CLI 명령어
270
+
271
+ 패키지를 전역 설치하지 않아도 `npx`로 실행할 수 있습니다.
272
+
273
+ ```bash
274
+ # 사용 가능한 명령 보기
275
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 --help
276
+
277
+ # 프로젝트 생성 + 설정 + 원본 외부 스킬 설치
278
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 init my-project
279
+
280
+ # 기존 프로젝트에 공통 설정·Hook 설치 (스킬 다운로드는 별도)
281
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 setup --project . --agents all
282
+
283
+ # 원본 스킬 별도 설치
284
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 install-skills --project . --agents all
285
+
286
+ # 상태 조회 (현재 프로젝트 지정)
287
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 status --project .
288
+
289
+ # 설치 파일 점검
290
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 doctor --project .
291
+
292
+ # 과거 AGENTS.md의 세션 명령 복구
293
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 repair-session --project .
294
+ ```
295
+
296
+ | 명령 | 설명 | 참고 |
297
+ |---|---|---|
298
+ | `init <경로>` | 새/기존 프로젝트 초기화 및 원본 스킬 설치 | 권장 시작점 |
299
+ | `setup` | 문서·로컬 상태 조회 파일·Hook 설치 | 원본 스킬 다운로드 안 함 |
300
+ | `install-skills` | 원본 외부 스킬 다운로드·설치 | GitHub 또는 `--source-dir` 사용 |
301
+ | `status` | 현재 프로젝트의 작업 상태 요약 | 프로젝트 로컬 `node` 명령도 사용 가능 |
302
+ | `doctor` | 핵심 파일·스킬 디렉터리 존재 확인 | 실제 Hook 작동 여부까지 검증하지 않음 |
303
+ | `repair-session` | 구버전 `AGENTS.md`의 알려진 명령 수정 | 백업 생성, 기존 지침 보존 |
304
+ | `work ...` | 로컬 Python 작업 제어기로 인수 전달 | 에이전트 내부 사용에 적합 |
305
+
306
+ 에이전트는 `opencode`, `codex`, `claude`를 평소대로 실행하면 됩니다. 별도의 `ai-dev` 전역 명령이 필수인 구조가 아닙니다.
307
+
308
+ ## 8. 기존 프로젝트 업그레이드
309
+
310
+ ### v0.8.1에서 새 세션 오류 복구
311
+
312
+ 이미 설치한 프로젝트는 원본 외부 스킬을 다시 다운로드하지 않고 안내문만 복구할 수 있습니다.
313
+
314
+ ```bash
315
+ cd /path/to/my-project
316
+
317
+ # 변경 예상 확인
318
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 repair-session --dry-run
319
+
320
+ # AGENTS.md의 알려진 구버전 명령 교체
321
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 repair-session
322
+
323
+ # 복구된 로컬 명령 직접 검증
324
+ node .ai-dev/session-status.cjs
325
+ ```
326
+
327
+ `repair-session`은 `AGENTS.md`의 알려진 구버전 명령만 치환합니다. 변경 전 `AGENTS.md.pre-ai-dev-0.8.2.bak` 백업을 생성하며, 같은 이름의 백업이 이미 있으면 덮어쓰지 않습니다. **현재 파일에 로컬 상태 조회 실행 파일 자체가 없다면 `repair-session`만으로 복구할 수 없습니다.**
328
+
329
+ 기존 프로젝트에 `AGENTS.md`가 이미 있었고 최초 설치 시 보존되었다면, 그 내용은 자동으로 프레임워크 지침과 합쳐지지 않습니다. 필요한 지침을 직접 비교·병합해야 합니다.
330
+
331
+ ### 원본 스킬 갱신
332
+
333
+ ```bash
334
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 install-skills --project . --agents all --update
335
+ ```
336
+
337
+ 이 명령은 프레임워크가 이전에 설치했고 이후 수정되지 않은 관리 대상에 한해 업데이트를 허용합니다. 직접 수정한 스킬 파일을 덮어쓰는 용도로 사용하지 마세요.
338
+
339
+ ## 9. 오프라인·재현 가능한 설치
340
+
341
+ 기본 설치는 GitHub의 당시 최신 리비전을 가져오므로 설치일이 다르면 스킬 내용도 달라질 수 있습니다.
342
+
343
+ ### 오프라인 설치
344
+
345
+ ```bash
346
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 init my-project \
347
+ --source-dir /path/to/three-repos
348
+ ```
349
+
350
+ `--source-dir`는 다음 디렉터리가 준비되어 있어야 합니다.
351
+
352
+ ```text
353
+ /path/to/three-repos/
354
+ ├── mattpocock/
355
+ ├── superpowers/
356
+ └── ponytail/
357
+ ```
358
+
359
+ 각 소스는 설치 도구가 찾는 원본 `skills/**/<스킬명>/SKILL.md`를 포함해야 합니다. 또한 **패키지 자체를 npx가 처음 내려받는 과정은 별도**이므로, 완전 폐쇄망에서는 npm 패키지도 미리 확보해야 합니다.
360
+
361
+ ### 원본 커밋 고정
362
+
363
+ 팀이 같은 스킬을 쓰려면 공급자별 커밋 SHA를 지정할 수 있습니다.
364
+
365
+ ```json
366
+ {
367
+ "mattpocock": "<실제 40자리 커밋 SHA>",
368
+ "superpowers": "<실제 40자리 커밋 SHA>",
369
+ "ponytail": "<실제 40자리 커밋 SHA>"
370
+ }
371
+ ```
372
+
373
+ 위 `<...>` 값은 예시 자리표시자이므로 **실제 SHA로 교체한 뒤** 사용하세요.
374
+
375
+ ```bash
376
+ npx --yes @gon1942/ai-dev-external-framework@0.8.2 init my-project --pins pins.json
377
+ ```
378
+
379
+ 설치 기록은 프로젝트 루트의 `.external-skills-lock.json`에 저장됩니다. 이 파일은 어떤 원본 리비전과 파일이 설치되었는지 추적하는 데 유용합니다.
380
+
381
+ ## 10. 문제 해결
382
+
383
+ ### `ai-dev: command not found`가 표시됩니다
384
+
385
+ `npx init`은 CLI를 전역 설치하지 않습니다. 프로젝트 루트에서 다음을 실행하세요.
386
+
387
+ ```bash
388
+ node .ai-dev/session-status.cjs
389
+ ```
390
+
391
+ `AGENTS.md`가 여전히 `ai-dev status`를 요구한다면 [기존 프로젝트 업그레이드](#8-기존-프로젝트-업그레이드)의 `repair-session`을 적용하세요.
392
+
393
+ ### `npx` 실행 시 `E404`가 나옵니다
394
+
395
+ 먼저 패키지 이름·버전·Registry를 확인하세요.
396
+
397
+ ```bash
398
+ npm view @gon1942/ai-dev-external-framework@0.8.2 version \
399
+ --registry=https://registry.npmjs.org/
400
+ npm config get registry
401
+ ```
402
+
403
+ 공개 npm Registry에 해당 버전이 실제로 게시되어 있지 않으면 설치할 수 없습니다. **이 README가 포함된 소스가 있다고 해서 npm 배포가 자동 완료되는 것은 아닙니다.**
404
+
405
+ ### `Existing file differs` 또는 `Collision`이 나옵니다
406
+
407
+ 기존 프로젝트 또는 스킬 디렉터리와 내용이 충돌한 경우입니다. 설치 도구는 기본적으로 덮어쓰지 않습니다. 파일을 백업하고 차이를 확인한 후 수동으로 조정하세요. 관리 대상 스킬의 안전한 갱신에는 `install-skills --update`를 사용할 수 있습니다.
408
+
409
+ ### 설치는 되었는데 에이전트가 스킬을 사용하지 않습니다
410
+
411
+ 파일 존재 여부부터 확인하세요.
412
+
413
+ ```bash
414
+ find .agents/skills .opencode/skills .claude/skills -name SKILL.md -type f 2>/dev/null
415
+ ```
416
+
417
+ 스킬 파일 설치, 에이전트의 스킬 검색, 실제 스킬 호출은 서로 다른 단계입니다. 호스트별 스킬 활성화 방식과 플러그인 호환성을 확인해야 합니다.
418
+
419
+ ### `completed/`로 이동하지 않습니다
420
+
421
+ AC 증거와 리뷰가 완료 Gate를 통과했는지 확인하세요.
422
+
423
+ ```bash
424
+ python3 scripts/workflow.py gate --id TASK-TODO-CLI --stage complete
425
+ ```
426
+
427
+ Gate 통과 뒤에도 사용자의 최종 완료 승인이 있어야 `complete --user-confirmed` 명령으로 이동합니다. 직접 `mv`로 옮기면 상태 관리 절차를 우회할 수 있습니다.
428
+
429
+ ## 11. 기술적 한계와 운영 시 주의사항
430
+
431
+ - **원본 스킬 18개는 설치 대상 목록입니다.** 현재 원본 저장소에 모든 경로가 존재하고 각 에이전트가 스킬을 실제 호출할 수 있는지는 환경에서 확인해야 합니다.
432
+ - **Hook 실환경 동작은 보장되지 않습니다.** `.codex/hooks.json`, `.claude/settings.json`, `.opencode/plugins/`를 생성하지만 각 호스트의 지원 버전·권한·신뢰 설정에 따라 동작이 달라질 수 있습니다.
433
+ - **독립 리뷰의 진위는 자동으로 증명하지 못합니다.** 리뷰 PASS 기록과 실질적인 검토 품질은 구분해야 합니다.
434
+ - **테스트가 업무 요구를 충분히 검증하는지도 사람이 확인해야 합니다.** Gate는 명령 종료 코드·증거·소스 변경 여부 등의 구조적 조건을 검사합니다.
435
+ - **승인 플래그는 사용자의 신원을 인증하지 않습니다.** 사람의 명시적 승인 없이 에이전트가 `--user-confirmed`를 사용해서는 안 됩니다.
436
+ - **Ponytail 플러그인의 전체 기능은 설치하지 않습니다.** 원본 `ponytail` 스킬 파일만 설치합니다.
437
+ - **CRG(code-review-graph)는 별도 관리 대상**이며 이 패키지가 자동으로 설치하거나 활성화하지 않습니다.
438
+ - **`init`의 설치 후 상태 조회 자동 실행은 현재 구현에 포함되어 있지 않습니다.** 위 빠른 시작의 상태 조회 명령을 별도로 실행하여 점검하세요.
439
+ - 사용자의 명시적인 승인 없이 에이전트가 `commit`, `push`, `merge`하거나 정책/검증 게이트를 수정하지 않도록 운영해야 합니다.
440
+
441
+ ## 12. 개발 및 npm 배포
442
+
443
+ 이 저장소는 **npm 패키지 소스**입니다. npm에 공개하기 전 다음을 실행하세요.
444
+
445
+ ```bash
446
+ npm test
447
+ npm run publish:check
448
+ npm pack --dry-run
449
+ ```
450
+
451
+ 배포 권한이 있는 npm 계정으로 로그인한 뒤:
452
+
453
+ ```bash
454
+ npm login
455
+ npm publish --access public
456
+ ```
457
+
458
+ 패키지 이름은 `package.json`의 `@gon1942/ai-dev-external-framework`입니다. 동일한 **이름 + 버전 조합은 npm에 다시 배포할 수 없습니다.** README만 수정했더라도 이미 `0.8.2`를 게시했다면 버전을 올린 뒤 배포해야 합니다.
459
+
460
+ ```bash
461
+ # 0.8.2가 이미 배포된 경우에만 다음 버전으로 변경
462
+ npm version patch --no-git-tag-version
463
+ npm test
464
+ npm run publish:check
465
+ npm publish --access public
466
+ ```
467
+
468
+ 배포된 버전 확인:
469
+
470
+ ```bash
471
+ npm view @gon1942/ai-dev-external-framework version \
472
+ --registry=https://registry.npmjs.org/
473
+ ```
474
+
475
+ ---
476
+
477
+ **핵심 원칙:** 외부 스킬은 *작업을 어떻게 할지* 돕고, 프레임워크는 *무엇이 승인·검증·완료됐는지* 기록합니다. **기록이 있는 것과 기능이 실제로 올바르게 동작하는 것은 구분하여 검증해야 합니다.**
package/bin/ai-dev.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const path=require('node:path');const {spawnSync}=require('node:child_process');const fs=require('node:fs');const {setup,installSkills}=require('../lib/install');
4
+ const {init}=require('../lib/init');const {repairSession}=require('../lib/repair');const {sessionStatus,formatStatus}=require('../lib/status');
5
+ const [command,...argv]=process.argv.slice(2);const get=(k,d)=>{const i=argv.indexOf(k);return i<0?d:argv[i+1]};const has=(k)=>argv.includes(k);const project=path.resolve(get('--project',process.cwd()));
6
+ try{
7
+ if(!command||['help','--help'].includes(command)){console.log(`ai-dev v0.8.2\n init <PATH> [--agents all|opencode|codex|claude] [--dry-run] [--source-dir PATH] [--pins FILE]\n setup [--project PATH] [--agents all|opencode|codex|claude] [--dry-run]\n install-skills [--project PATH] [--agents all|...] [--source-dir PATH] [--dry-run] [--update] [--pins FILE]\n status [--project PATH] [--json]\n repair-session [--project PATH] [--dry-run]\n work <workflow.py arguments>\n doctor [--project PATH]\n\nNote: setup installs native hooks + docs; install-skills fetches unmodified upstream skills separately.`);process.exit(0)}
8
+ if(command==='init'){init(argv);process.exit(0)}
9
+ if(!fs.existsSync(project)||!fs.statSync(project).isDirectory())throw Error('Project directory must exist: '+project);
10
+ if(command==='setup'){for(const action of setup(project,{dry:has('--dry-run'),agents:get('--agents','all')}))console.log(action);console.log('Native hooks may need host trust approval; see README.');}
11
+ else if(command==='install-skills')installSkills(project,{agents:get('--agents','all'),sourceDir:get('--source-dir',undefined),dry:has('--dry-run'),update:has('--update'),pins:get('--pins',undefined)});
12
+ else if(command==='repair-session'){console.log(repairSession(project,{dry:has('--dry-run')}));}
13
+ else if(command==='status'){const s=sessionStatus(project);console.log(has('--json')?JSON.stringify(s,null,2):formatStatus(s));}
14
+ else if(command==='doctor'){
15
+ const paths=['AGENTS.md','scripts/workflow.py','.ai-dev/session-status.cjs','.claude/settings.json','.codex/hooks.json','.opencode/plugins/ai-dev-session.js'];let fail=0;
16
+ for(const p of paths){const good=fs.existsSync(path.join(project,p));console.log((good?'PASS ':'MISSING ')+p);if(!good)fail++}
17
+ for(const [name,dir] of Object.entries({codex:'.agents/skills',opencode:'.opencode/skills',claude:'.claude/skills'})){const full=path.join(project,dir);console.log((fs.existsSync(full)?'FOUND ':'NOT INSTALLED ')+`${name} upstream skills`)}
18
+ if(fail)process.exitCode=1;
19
+ }
20
+ else if(command==='work'){const rest=argv.filter((_,i)=>!(argv[i-1]==='--project'||argv[i]==='--project'));const r=spawnSync('python3',[path.join(project,'scripts','workflow.py'),...rest],{stdio:'inherit',cwd:project});process.exitCode=r.status??2;}
21
+ else throw Error('Unknown command: '+command);
22
+ }catch(e){console.error('ERROR:',e.message);process.exitCode=2}
package/lib/init.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const fs=require('node:fs');
3
+ const path=require('node:path');
4
+ const {setup,installSkills}=require('./install');
5
+ const {spawnSync}=require('node:child_process');
6
+ function init(argv){
7
+ const value=(key)=>{const i=argv.indexOf(key);if(i<0)return undefined;if(!argv[i+1]||argv[i+1].startsWith('--'))throw Error(`Missing value for ${key}`);return argv[i+1]};
8
+ const positional=argv.filter((x,i)=>!x.startsWith('--')&&!['--agents','--pins','--source-dir'].includes(argv[i-1]));
9
+ const target=positional[0];
10
+ if(!target||positional.length!==1)throw Error('Usage: ai-dev init <project-directory> [--agents all] [--dry-run]');
11
+ const project=path.resolve(target),dry=argv.includes('--dry-run');
12
+ const agents=value('--agents')||'all',sourceDir=value('--source-dir'),pins=value('--pins');
13
+ if(!['all','opencode','codex','claude'].includes(agents))throw Error('Unknown agents option: '+agents);
14
+ if(fs.existsSync(project)&&!fs.statSync(project).isDirectory())throw Error('Target exists and is not a directory: '+project);
15
+ if(!fs.existsSync(project)){
16
+ console.log(`${dry?'WOULD CREATE':'CREATE'} project ${project}`);
17
+ if(!dry)fs.mkdirSync(project,{recursive:true});
18
+ }
19
+ // All setup actions are planned before any upstream fetch. Existing project files are never overwritten.
20
+ // A failed upstream fetch leaves installed docs intact, so retrying `init` is possible.
21
+ if(!dry||fs.existsSync(project))for(const action of setup(project,{dry,agents}))console.log(action);
22
+ if(dry&&!fs.existsSync(project)){
23
+ console.log('Dry-run: new project does not yet exist; setup and upstream skills would be installed at '+project);
24
+ console.log('WOULD INSTALL original upstream skills (fetch required on real install)');return;
25
+ }
26
+ if(dry){console.log('Dry-run does not download upstream repositories.');return;}
27
+ console.log('Installing original upstream skills...');
28
+ installSkills(project,{agents,sourceDir,pins});
29
+ console.log('INIT COMPLETE '+project);
30
+ console.log('Run `opencode`, `codex`, or `claude` inside the project after checking the installation.');
31
+ }
32
+ module.exports={init};
package/lib/install.js ADDED
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+ const fs=require('node:fs');const path=require('node:path');const {spawnSync}=require('node:child_process');
3
+ const ROOT=path.resolve(__dirname,'..');
4
+ function walk(dir){const out=[];for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())out.push(...walk(p));else out.push(p)}return out;}
5
+ function put(file,data,dry,mode='managed'){
6
+ if(fs.existsSync(file)){
7
+ const old=fs.readFileSync(file);if(old.equals(Buffer.isBuffer(data)?data:Buffer.from(data)))return 'UNCHANGED';
8
+ if(mode==='managed')throw Error(`Existing file differs: ${file} (refusing overwrite)`);return 'PRESERVED';
9
+ }
10
+ if(!dry){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,data)}return dry?'WOULD_CREATE':'CREATED';
11
+ }
12
+ function mergeHooks(file, entry, dry){let obj={};if(fs.existsSync(file)){obj=JSON.parse(fs.readFileSync(file,'utf8'));if(!obj||Array.isArray(obj)||typeof obj!=='object')throw Error(`Invalid JSON: ${file}`)}
13
+ obj.hooks??={};obj.hooks.SessionStart??=[];const sig=JSON.stringify(entry);if(obj.hooks.SessionStart.some(x=>JSON.stringify(x)===sig))return 'UNCHANGED';obj.hooks.SessionStart.push(entry);
14
+ if(!dry){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,JSON.stringify(obj,null,2)+'\n')}return dry?'WOULD_MERGE':'MERGED';
15
+ }
16
+ function setup(project,{dry=false,agents='all'}={}){
17
+ const selected=agents==='all'?['opencode','codex','claude']:agents.split(',');for(const a of selected)if(!['opencode','codex','claude'].includes(a))throw Error(`Unknown agent ${a}`);
18
+ const actions=[];const source=path.join(ROOT,'templates');for(const f of walk(source)){
19
+ const rel=path.relative(source,f);if(rel==='AGENTS.md'&&fs.existsSync(path.join(project,rel))){actions.push(`PRESERVED ${rel} (manually merge the workflow guidance)`);continue}
20
+ const target=path.join(project,rel); actions.push(`${put(target,fs.readFileSync(f),dry,'project')} ${rel}`);
21
+ }
22
+ const py=path.join(ROOT,'scripts','workflow.py');
23
+ // Installed copies are self-contained and never overwritten without explicit user review.
24
+ actions.push(`${put(path.join(project,'scripts','workflow.py'),fs.readFileSync(py),dry)} scripts/workflow.py`);
25
+ const entryScript=path.join(project,'.ai-dev','session-status.cjs');
26
+ // The installed hook imports the exact same status engine used by `ai-dev status`.
27
+ const statusEngine=path.join(project,'.ai-dev','status-engine.cjs');
28
+ actions.push(`${put(statusEngine,fs.readFileSync(path.join(ROOT,'lib','status.js')),dry)} .ai-dev/status-engine.cjs`);
29
+ const portable=`const path=require('node:path');\nconst {sessionStatus,formatStatus}=require('./status-engine.cjs');\nprocess.stdout.write(formatStatus(sessionStatus(path.resolve(__dirname,'..')))+'\\n');\n`;
30
+ actions.push(`${put(entryScript,portable,dry)} .ai-dev/session-status.cjs`);
31
+ // hook command cwd is tool-defined; use absolute path resolved at setup, with robust JSON/shell quoting.
32
+ const cmd=`node ${JSON.stringify(entryScript)}`;
33
+ if(selected.includes('claude')){const target=path.join(project,'.claude','settings.json');actions.push(`${mergeHooks(target,{hooks:[{type:'command',command:cmd,timeout:15}]},dry)} .claude/settings.json (SessionStart)`)}
34
+ if(selected.includes('codex')){const target=path.join(project,'.codex','hooks.json');actions.push(`${mergeHooks(target,{hooks:[{type:'command',command:cmd,timeout:15}]},dry)} .codex/hooks.json (SessionStart)`)}
35
+ if(selected.includes('opencode')){
36
+ const target=path.join(project,'.opencode','plugins','ai-dev-session.js');
37
+ const script=`import { execFileSync } from 'node:child_process';\nexport const AiDevSession = async ({client}) => {\n const seen=new Set();\n return {event:async ({event})=>{\n if(event.type!=='session.created')return;\n const id=event.properties?.info?.id || event.properties?.sessionID; if(!id||seen.has(id))return;seen.add(id);\n try{ const status=execFileSync('node',[${JSON.stringify(entryScript)}],{encoding:'utf8',timeout:15000});\n await client.tui.showToast({body:{title:'AI work status',message:status.slice(0,900),variant:'info',duration:8000}});\n }catch(e){console.warn('[ai-dev] session status unavailable:',String(e));}\n }};\n};\n`;
38
+ actions.push(`${put(target,script,dry)} .opencode/plugins/ai-dev-session.js`);
39
+ }
40
+ return actions;
41
+ }
42
+ function installSkills(project,options={}){
43
+ const args=[path.join(ROOT,'scripts','external-install.py'),'--project',project,'--agents',options.agents||'all'];if(options.sourceDir)args.push('--source-dir',options.sourceDir);if(options.dry)args.push('--dry-run');if(options.update)args.push('--update');if(options.pins)args.push('--pins',options.pins);
44
+ const r=spawnSync('python3',args,{stdio:'inherit'});if(r.status!==0)throw Error(`Original upstream skill install failed (exit ${r.status})`);
45
+ }
46
+ module.exports={setup,installSkills};
package/lib/repair.js ADDED
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+ const fs=require('node:fs');const path=require('node:path');const {spawnSync}=require('node:child_process');
3
+ const oldStatus='`ai-dev status`';
4
+ function repairSession(project,{dry=false}={}){
5
+ const instructions=path.join(project,'AGENTS.md');
6
+ if(!fs.existsSync(instructions))throw Error('AGENTS.md not found: '+instructions);
7
+ const current=fs.readFileSync(instructions,'utf8');
8
+ const substitutions=[
9
+ ['`ai-dev status`','`node .ai-dev/session-status.cjs`'],
10
+ ['`ai-dev work new','`python3 scripts/workflow.py new'],
11
+ ['`ai-dev work approve','`python3 scripts/workflow.py approve'],
12
+ ['`ai-dev work gate','`python3 scripts/workflow.py gate']
13
+ ];
14
+ let next=current;for(const [from,to] of substitutions)next=next.split(from).join(to);
15
+ const status=path.join(project,'.ai-dev','session-status.cjs');
16
+ if(!fs.existsSync(status))throw Error('Local session status script is missing; initialize project first: '+status);
17
+ const check=spawnSync(process.execPath,[status],{cwd:project,encoding:'utf8',timeout:15000});
18
+ if(check.status!==0)throw Error('Local session status failed: '+(check.stderr||check.error?.message||'unknown'));
19
+ if(next===current)return 'UNCHANGED AGENTS.md; local session check PASS';
20
+ if(dry)return 'WOULD_REPAIR AGENTS.md; local session check PASS';
21
+ const backup=path.join(project,'AGENTS.md.pre-ai-dev-0.8.2.bak');
22
+ if(fs.existsSync(backup))throw Error('Backup already exists, inspect before retrying: '+backup);
23
+ fs.copyFileSync(instructions,backup,fs.constants.COPYFILE_EXCL);
24
+ fs.writeFileSync(instructions,next);
25
+ return 'REPAIRED AGENTS.md (backup: '+backup+'); local session check PASS';
26
+ }
27
+ module.exports={repairSession};
package/lib/status.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'); const path=require('node:path');
3
+ function sessionStatus(root){
4
+ const base=path.join(root,'docs','exec-plans','active');let items=[],warnings=[];
5
+ if(fs.existsSync(base))for(const ent of fs.readdirSync(base,{withFileTypes:true}).filter(x=>x.isDirectory()).sort((a,b)=>a.name.localeCompare(b.name))){
6
+ const file=path.join(base,ent.name,'status.json');if(!fs.existsSync(file))continue;
7
+ try {const s=JSON.parse(fs.readFileSync(file,'utf8')); const hasPlan=['spec.md','plan.md','ac-tests.json'].every(x=>fs.existsSync(path.join(base,ent.name,x)));let phase='PLAN_DRAFT';
8
+ if(s.approved===true||Boolean(s.approved_by)){phase=s.review?.verdict==='PASS'?'REVERIFY_OR_COMPLETION_CHECK':'IMPLEMENT_OR_VERIFY';}
9
+ else if(hasPlan){const ac=JSON.parse(fs.readFileSync(path.join(base,ent.name,'ac-tests.json'),'utf8'));if(Object.keys(ac).length)phase='PLAN_APPROVAL_REQUIRED';}
10
+ items.push({id:ent.name,title:s.title||'',phase,path:`docs/exec-plans/active/${ent.name}`});
11
+ }catch(e){warnings.push(`${ent.name}: ${e.message}`)}
12
+ }
13
+ return {project:root,active_count:items.length,tasks:items,warnings};
14
+ }
15
+ function formatStatus(s){const lines=['=== AI WORK SESSION STATUS ===',`Project: ${s.project}`,`Active tasks: ${s.active_count}`]; if(!s.tasks.length)lines.push('No active tasks. Do not auto-create work at session start.');for(const t of s.tasks)lines.push(`${t.id} | ${t.phase} | ${t.title}\n ${t.path}`);for(const w of s.warnings)lines.push(`WARNING: ${w}`);lines.push('Read existing task files before starting new work. Show this summary to the user.');return lines.join('\n');}
16
+ module.exports={sessionStatus,formatStatus};
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@gon1942/ai-dev-external-framework",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.8.2",
4
+ "private": false,
5
+ "description": "Cross-agent external skills installer, session status hooks and task tracking for OpenCode, Codex and Claude Code",
6
+ "bin": {
7
+ "ai-dev": "bin/ai-dev.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test test/*.test.js && python3 -m unittest discover -s test -p \"test_*.py\"",
14
+ "prepack": "npm test",
15
+ "publish:check": "node scripts/publish-check.cjs && npm pack --dry-run --json"
16
+ },
17
+ "license": "MIT",
18
+ "files": [
19
+ "bin/",
20
+ "lib/",
21
+ "scripts/",
22
+ "templates/",
23
+ "adapters/",
24
+ "external-skills/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "agent-skills",
30
+ "opencode",
31
+ "codex",
32
+ "claude-code",
33
+ "workflow",
34
+ "task-graph"
35
+ ]
36
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """Install unmodified upstream agent skills for Codex, OpenCode and Claude Code."""
3
+ import argparse, json, shutil, subprocess, tempfile, pathlib, hashlib, datetime, sys
4
+
5
+ ROOT=pathlib.Path(__file__).resolve().parent.parent
6
+ CATALOG={
7
+ 'mattpocock':('https://github.com/mattpocock/skills.git', ['grill-with-docs','to-spec','to-tickets','implement','implement-spec','code-review','retro','diagnosing-bugs']),
8
+ 'superpowers':('https://github.com/obra/superpowers.git', ['brainstorming','writing-plans','test-driven-development','systematic-debugging','verification-before-completion','requesting-code-review','receiving-code-review','using-git-worktrees','subagent-driven-development']),
9
+ 'ponytail':('https://github.com/DietrichGebert/ponytail.git', ['ponytail']),
10
+ }
11
+
12
+ def run(args):
13
+ p=subprocess.run(args,text=True,capture_output=True)
14
+ if p.returncode: raise RuntimeError(f"{' '.join(args)}\n{p.stderr.strip()}")
15
+ return p.stdout.strip()
16
+
17
+ def sha(b): return hashlib.sha256(b).hexdigest()
18
+
19
+ def main():
20
+ ap=argparse.ArgumentParser(description=__doc__)
21
+ ap.add_argument('--project',required=True)
22
+ ap.add_argument('--agents',choices=['all','codex','opencode','claude'],default='all')
23
+ ap.add_argument('--source-dir',help='Optional offline folder containing mattpocock/, superpowers/ and ponytail/ checkouts')
24
+ ap.add_argument('--dry-run',action='store_true')
25
+ ap.add_argument('--pins',help='JSON mapping vendor names to exact commit SHA; required for reproducible team installs')
26
+ ap.add_argument('--update',action='store_true',help='Only replace files owned by a prior install when upstream changes; never overwrite foreign modifications')
27
+ args=ap.parse_args()
28
+ project=pathlib.Path(args.project).expanduser().resolve()
29
+ if not project.is_dir(): ap.error('project directory must exist')
30
+ pins=json.loads(pathlib.Path(args.pins).read_text()) if args.pins else {}
31
+ if pins and (set(pins)!=set(CATALOG) or any(not isinstance(v,str) or len(v)!=40 or any(c not in '0123456789abcdefABCDEF' for c in v) for v in pins.values())):
32
+ raise RuntimeError('pins must map every vendor to a 40-character git SHA')
33
+ agents=['codex','opencode','claude'] if args.agents=='all' else [args.agents]
34
+ # OpenCode gets its own .opencode/skills; no implicit dependency on Codex
35
+ dests={'codex':project/'.agents/skills','opencode':project/'.opencode/skills','claude':project/'.claude/skills'}
36
+ manifest_path=project/'.external-skills-lock.json'
37
+ old=json.loads(manifest_path.read_text()) if manifest_path.exists() else {'files':{}}
38
+ new={'generated_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'sources':{},'files':dict(old.get('files',{}))}
39
+ with tempfile.TemporaryDirectory() as td:
40
+ src={}
41
+ for vendor,(url,skills) in CATALOG.items():
42
+ if args.source_dir:
43
+ repo=pathlib.Path(args.source_dir).expanduser().resolve()/vendor
44
+ if not repo.is_dir(): raise RuntimeError(f'Missing offline source: {repo}')
45
+ else:
46
+ repo=pathlib.Path(td)/vendor
47
+ print(f'FETCH {url}',flush=True)
48
+ run(['git','clone',url,str(repo)]) if vendor in pins else run(['git','clone','--depth','1',url,str(repo)])
49
+ if vendor in pins:run(['git','-C',str(repo),'checkout','--detach',pins[vendor]])
50
+ src[vendor]=repo
51
+ try: revision=run(['git','-C',str(repo),'rev-parse','HEAD'])
52
+ except RuntimeError: revision='local-unversioned'
53
+ if vendor in pins and revision.lower()!=pins[vendor].lower():raise RuntimeError(f'Pin mismatch for {vendor}: {revision} != {pins[vendor]}')
54
+ new['sources'][vendor]={'url':url,'revision':revision}
55
+ changes=[]
56
+ for vendor,(_,names) in CATALOG.items():
57
+ repo=src[vendor]
58
+ for name in names:
59
+ matches=sorted(x for x in (repo/'skills').rglob('SKILL.md') if x.parent.name==name)
60
+ if len(matches)!=1:
61
+ raise RuntimeError(f'Missing or ambiguous REQUIRED skill {vendor}/{name}: expected 1 exact source, found {len(matches)}')
62
+ source=matches[0].parent
63
+ if not (source/'SKILL.md').is_file() or not (source/'SKILL.md').read_text(encoding='utf-8').strip():
64
+ raise RuntimeError(f'Empty REQUIRED skill {vendor}/{name}')
65
+ for agent in agents:
66
+ # Namespace avoids collisions; upstream SKILL.md internal name remains unmodified
67
+ dest=dests[agent]/name
68
+ if dest.exists():
69
+ # Compare entire directory contents before accepting or replacing
70
+ existing={str(x.relative_to(dest)):sha(x.read_bytes()) for x in dest.rglob('*') if x.is_file()}
71
+ incoming={str(x.relative_to(source)):sha(x.read_bytes()) for x in source.rglob('*') if x.is_file()}
72
+ if existing==incoming:
73
+ print(f'SKIP IDENTICAL {agent}:{name}'); continue
74
+ previous={k[len(str(dest.relative_to(project)))+1:]:v for k,v in old['files'].items() if k.startswith(str(dest.relative_to(project))+'/')}
75
+ if not(args.update and previous and previous==existing):
76
+ raise RuntimeError(f'Collision: {dest}. Refusing to overwrite; backup/remove manually or use --update for unchanged managed files.')
77
+ changes.append((source,dest,agent,vendor,name))
78
+ for source,dest,agent,vendor,name in changes:
79
+ print(f'{"WOULD INSTALL" if args.dry_run else "INSTALL"} {agent}: {vendor}/{name} -> {dest}')
80
+ if args.dry_run: continue
81
+ if dest.exists(): shutil.rmtree(dest)
82
+ shutil.copytree(source,dest,symlinks=False)
83
+ if not args.dry_run:
84
+ for vendor,(_,names) in CATALOG.items():
85
+ repo=src[vendor]
86
+ for name in names:
87
+ matches=[x for x in (repo/'skills').rglob('SKILL.md') if x.parent.name==name]
88
+ if len(matches)!=1: continue
89
+ for agent in agents:
90
+ dest=dests[agent]/name
91
+ if dest.exists():
92
+ for f in dest.rglob('*'):
93
+ if f.is_file(): new['files'][str(f.relative_to(project))]=sha(f.read_bytes())
94
+ manifest_path.write_text(json.dumps(new,indent=2,ensure_ascii=False)+'\n')
95
+ else: print('Dry-run complete: no files changed')
96
+ print('NOTE: This installs skill files only. Ponytail skill is installed, but its native plugin, hooks, mode-switching and other runtime capabilities are NOT installed. See README. Other upstream plugins and runtimes are not installed.')
97
+
98
+ if __name__=='__main__':
99
+ try: main()
100
+ except (RuntimeError,OSError) as e:
101
+ print(f'ERROR: {e}',file=sys.stderr);sys.exit(2)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs=require('node:fs');const path=require('node:path');
4
+ const pkg=require('../package.json');
5
+ if(pkg.private===true) throw Error('package must be public');
6
+ if(!pkg.name||!pkg.bin?.['ai-dev']||!fs.existsSync(path.join(__dirname,'..',pkg.bin['ai-dev'])))throw Error('bin missing');
7
+ for(const p of ['scripts/external-install.py','scripts/workflow.py','templates/AGENTS.md','lib/install.js','lib/status.js'])if(!fs.existsSync(path.join(__dirname,'..',p)))throw Error('missing '+p);
8
+ console.log('PUBLISH_CHECK_PASS',pkg.name,pkg.version);
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """Declarative, project-owned verification profiles; never execute undocumented commands."""
3
+ import argparse, json, pathlib, fnmatch, subprocess, datetime, hashlib, sys
4
+
5
+ def load(project):
6
+ f=project/'.ai-dev/verification-profiles.json'
7
+ if not f.exists():raise ValueError(f'Missing verification profiles: {f}')
8
+ cfg=json.loads(f.read_text())
9
+ if not isinstance(cfg.get('profiles'),dict):raise ValueError('profiles object required')
10
+ for name,profile in cfg['profiles'].items():
11
+ if not isinstance(profile.get('patterns'),list) or not isinstance(profile.get('checks'),dict):raise ValueError('invalid profile '+name)
12
+ for check,cmd in profile['checks'].items():
13
+ if not isinstance(cmd,list) or not cmd or not all(isinstance(s,str) and s for s in cmd):raise ValueError('checks must be argv lists')
14
+ return cfg
15
+
16
+ def decide(cfg,files):
17
+ found={}
18
+ for name,profile in cfg['profiles'].items():
19
+ matching=[f for f in files if any(fnmatch.fnmatch(f,pat) for pat in profile['patterns'])]
20
+ if matching:found[name]={'files':matching,'checks':profile['checks']}
21
+ return found
22
+
23
+ def main():
24
+ ap=argparse.ArgumentParser();ap.add_argument('--project',default='.');ap.add_argument('--files',nargs='+',required=True);ap.add_argument('--run',action='store_true');args=ap.parse_args()
25
+ project=pathlib.Path(args.project).resolve();cfg=load(project);found=decide(cfg,args.files)
26
+ if not found:print(json.dumps({'profiles':{},'warning':'NO_MATCHING_PROFILE: manual verification plan required'},indent=2));return 2
27
+ if not args.run:print(json.dumps({'profiles':found,'note':'Preview only: choose appropriate AC mappings before execution'},indent=2));return 0
28
+ logs=project/'docs/generated/verification';logs.mkdir(parents=True,exist_ok=True)
29
+ results=[]
30
+ for name,data in found.items():
31
+ for check,cmd in data['checks'].items():
32
+ r=subprocess.run(cmd,cwd=project,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
33
+ key=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
34
+ f=logs/(key+'-'+name+'-'+check+'.log');f.write_text(r.stdout)
35
+ results.append({'profile':name,'check':check,'command':cmd,'exit_code':r.returncode,'log':str(f.relative_to(project)),'sha256':hashlib.sha256(f.read_bytes()).hexdigest()})
36
+ print(json.dumps({'results':results},indent=2))
37
+ return 1 if any(x['exit_code'] for x in results) else 0
38
+ if __name__=='__main__':
39
+ try:sys.exit(main())
40
+ except (ValueError,OSError,json.JSONDecodeError) as e:print('BLOCK:',e,file=sys.stderr);sys.exit(2)
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """Shared file-based gates for externally installed AI coding skills."""
3
+ import argparse, pathlib, json, datetime, hashlib, subprocess, sys, shutil, re
4
+ ROOT=pathlib.Path(__file__).resolve().parent.parent
5
+ BASE=ROOT/'docs'/'exec-plans'
6
+ def now():return datetime.datetime.now(datetime.timezone.utc).isoformat()
7
+ def get(task):
8
+ if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]{1,79}',task):raise ValueError('invalid task ID')
9
+ for state in ['active','completed']:
10
+ p=BASE/state/task
11
+ if p.is_dir():return p
12
+ raise ValueError(f'unknown task: {task}')
13
+ def meta(p):return json.loads((p/'status.json').read_text())
14
+ def save(p,data):(p/'status.json').write_text(json.dumps(data,indent=2,ensure_ascii=False)+'\n')
15
+ def digest():
16
+ h=hashlib.sha256()
17
+ skip={'.git','.agents','.opencode','.claude','node_modules','.venv','venv','.pytest_cache','__pycache__','.external-skills-lock.json'}
18
+ for f in sorted(ROOT.rglob('*')):
19
+ if not f.is_file() or any(c in skip for c in f.relative_to(ROOT).parts):continue
20
+ rel=f.relative_to(ROOT).as_posix()
21
+ if rel.startswith('docs/exec-plans/') or rel.startswith('docs/generated/') or rel.startswith('docs/ai-workflow/') or rel.startswith('scripts/'):continue
22
+ h.update(rel.encode());h.update(hashlib.sha256(f.read_bytes()).digest())
23
+ return h.hexdigest()
24
+ def acmap(p):
25
+ try:
26
+ data=json.loads((p/'ac-tests.json').read_text());assert isinstance(data,dict) and data
27
+ for k,v in data.items():assert re.fullmatch(r'AC-[0-9]+',k) and isinstance(v,list) and bool(v) and all(isinstance(x,str) and x.strip() for x in v)
28
+ return data
29
+ except (ValueError,AssertionError,OSError) as e:raise ValueError('ac-tests.json must contain AC IDs mapped to nonempty command descriptions') from e
30
+ def proof(p):
31
+ records=[json.loads(line) for line in (p/'evidence.jsonl').read_text().splitlines()] if (p/'evidence.jsonl').exists() else []
32
+ mapping=acmap(p);d=digest();issues=[]
33
+ for ac in mapping:
34
+ if not any(x.get('ac')==ac and x.get('exit_code')==0 and x.get('source_digest')==d and x.get('log_file') and (p/x['log_file']).is_file() and x.get('command') in [v.split() for v in mapping[ac]] and not x.get('source_changed_during_test',False) for x in records):issues.append(ac+': missing current successful recorded execution')
35
+ return issues
36
+ def gate(p,stage):
37
+ s=meta(p)
38
+ if stage=='implement':
39
+ if not (s.get('approved') is True or bool(s.get('approved_by'))):raise ValueError('plan is not human-approved')
40
+ if not (p/'spec.md').read_text().strip() or not (p/'plan.md').read_text().strip():raise ValueError('missing spec/plan')
41
+ acmap(p)
42
+ elif stage in ['review','complete']:
43
+ gate(p,'implement')
44
+ issues=proof(p)
45
+ if issues:raise ValueError('; '.join(issues))
46
+ if stage=='complete':
47
+ r=s.get('review',{})
48
+ if r.get('verdict')!='PASS' or r.get('source_digest')!=digest():raise ValueError('missing PASS review for current source')
49
+ else:raise ValueError('unknown stage')
50
+ def main():
51
+ parser=argparse.ArgumentParser();sub=parser.add_subparsers(dest='cmd',required=True)
52
+ sub.add_parser('status');sub.add_parser('next');se=sub.add_parser('session');se.add_argument('--json',action='store_true');se.add_argument('--all',action='store_true',help='Include completed tasks (default active only)');n=sub.add_parser('new');n.add_argument('--id',required=True);n.add_argument('--title',required=True)
53
+ for action in ['approve','gate','run','review','complete']:
54
+ a=sub.add_parser(action);a.add_argument('--id',required=True)
55
+ if action in ['approve','complete']:a.add_argument('--user-confirmed',action='store_true',help='Agent attests explicit user approval in current conversation; not identity authentication')
56
+ if action=='gate':a.add_argument('--stage',choices=['implement','review','complete'],required=True)
57
+ if action=='run':a.add_argument('--ac',required=True);a.add_argument('command',nargs=argparse.REMAINDER)
58
+ if action=='review':a.add_argument('--verdict',choices=['PASS','FAIL'],required=True);a.add_argument('--notes-file',required=True)
59
+ args=parser.parse_args()
60
+ if args.cmd=='session':
61
+ # Read-only snapshot for a fresh agent session; do not infer approvals from chat.
62
+ tasks=[];problems=[]
63
+ states=['active','completed'] if args.all else ['active']
64
+ for state in states:
65
+ for sf in sorted((BASE/state).glob('*/status.json')):
66
+ q=sf.parent
67
+ try:
68
+ d=meta(q);phase='COMPLETED' if state=='completed' else 'PLAN_APPROVAL_REQUIRED';issue=''
69
+ if state=='active':
70
+ if not d.get('approved') and not d.get('approved_by'):
71
+ # A newly created task may have no meaningful plan yet.
72
+ phase='PLAN_DRAFT' if not (q/'ac-tests.json').is_file() or (q/'ac-tests.json').read_text().strip()=='{}' else 'PLAN_APPROVAL_REQUIRED'
73
+ else:
74
+ try:gate(q,'review');phase='INDEPENDENT_REVIEW_REQUIRED'
75
+ except (ValueError,OSError,KeyError,json.JSONDecodeError) as e:phase='IMPLEMENT_OR_VERIFY';issue=str(e)
76
+ if d.get('review',{}).get('verdict')=='PASS':
77
+ try:gate(q,'complete');phase='COMPLETION_APPROVAL_REQUIRED';issue=''
78
+ except (ValueError,OSError,KeyError,json.JSONDecodeError) as e:phase='REVERIFY_OR_REVIEW';issue=str(e)
79
+ tasks.append({'id':q.name,'title':d.get('title',''), 'phase':phase,'path':str(q.relative_to(ROOT)),'created_at':d.get('created_at',''),'issue':issue})
80
+ except (ValueError,OSError,KeyError,json.JSONDecodeError) as e:
81
+ problems.append({'path':str(q.relative_to(ROOT)),'error':str(e)})
82
+ order={'REVERIFY_OR_REVIEW':0,'IMPLEMENT_OR_VERIFY':1,'INDEPENDENT_REVIEW_REQUIRED':2,'PLAN_DRAFT':3,'PLAN_APPROVAL_REQUIRED':4,'COMPLETION_APPROVAL_REQUIRED':5,'COMPLETED':6}
83
+ tasks.sort(key=lambda d:(order.get(d['phase'],99), d['id']))
84
+ payload={'project':str(ROOT),'active_count':sum(t['phase']!='COMPLETED' for t in tasks),'tasks':tasks,'warnings':problems}
85
+ if args.json:print(json.dumps(payload,indent=2,ensure_ascii=False));return
86
+ print('=== AI WORK SESSION STATUS ===')
87
+ print('Project:',ROOT)
88
+ print('Active tasks:',payload['active_count'])
89
+ if not tasks:print('No tracked active tasks. For a new user request, create a task; do not create one merely because a session started.')
90
+ for t in tasks:
91
+ print(f"- {t['id']} | {t['phase']} | {t['title']}")
92
+ print(' files:',t['path'])
93
+ if t['issue']:print(' attention:',t['issue'])
94
+ for err in problems:print('WARNING:',err['path'],err['error'])
95
+ print('Next: Read relevant active task files; report current status before new coding. Do not infer user approval.')
96
+ return
97
+ if args.cmd=='next':
98
+ tasks=[]
99
+ for state in ['active','completed']:
100
+ for sf in sorted((BASE/state).glob('*/status.json')):
101
+ q=sf.parent;d=meta(q);phase='COMPLETED' if state=='completed' else 'PLAN_APPROVAL_REQUIRED'
102
+ if state=='active' and d.get('approved') is True:
103
+ try:gate(q,'review');phase='INDEPENDENT_REVIEW_REQUIRED'
104
+ except (ValueError,OSError):phase='IMPLEMENT_OR_VERIFY'
105
+ if d.get('review',{}).get('verdict')=='PASS':
106
+ try:gate(q,'complete');phase='COMPLETION_APPROVAL_REQUIRED'
107
+ except (ValueError,OSError):phase='REVERIFY_OR_REVIEW'
108
+ tasks.append({'id':q.name,'title':d.get('title'),'phase':phase,'directory':str(q.relative_to(ROOT))})
109
+ print(json.dumps({'tasks':tasks},indent=2,ensure_ascii=False));return
110
+ if args.cmd=='status':
111
+ for state in ['active','completed']:
112
+ for p in sorted((BASE/state).glob('*/status.json')):
113
+ d=json.loads(p.read_text());print(state,p.parent.name,d.get('title'), 'approved='+str(d.get('approved') is True or bool(d.get('approved_by'))),'review='+str(d.get('review',{}).get('verdict')))
114
+ return
115
+ if args.cmd=='new':
116
+ task=args.id
117
+ if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]{1,79}',task):raise ValueError('invalid task ID')
118
+ if any((BASE/st/task).exists() for st in ['active','completed']):raise ValueError('task exists')
119
+ p=BASE/'active'/task;p.mkdir(parents=True)
120
+ (p/'analysis.md').write_text('# Analysis\n\n');(p/'spec.md').write_text('# Spec / Acceptance Criteria\n\n');(p/'plan.md').write_text('# Execution plan / Task graph\n\n');(p/'ac-tests.json').write_text('{}\n')
121
+ save(p,{'id':task,'title':args.title,'created_at':now(),'approved':False,'review':{}});print('CREATED',p);return
122
+ p=get(args.id);s=meta(p)
123
+ if args.cmd=='approve':
124
+ if not args.user_confirmed:raise ValueError('explicit user approval required; agent must pass --user-confirmed only after user authorizes plan')
125
+ if p.parent.name!='active':raise ValueError('completed task')
126
+ if not (p/'spec.md').read_text().strip() or not (p/'plan.md').read_text().strip():raise ValueError('missing spec/plan')
127
+ acmap(p)
128
+ adapter_file=ROOT/'.ai-dev/spec-adapter.json'
129
+ if adapter_file.exists():
130
+ adapter=json.loads(adapter_file.read_text())
131
+ if adapter.get('mode')=='openspec':
132
+ change=s.get('openspec_change_id')
133
+ if not change or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,100}',change) or not (ROOT/adapter.get('source','openspec/changes')/change).is_dir():raise ValueError('valid openspec_change_id pointing to existing change directory required')
134
+ s.pop('approved_by',None);s['approved']=True;s['approved_at']=now();save(p,s);print('APPROVED',p.name);return
135
+ if args.cmd=='gate':gate(p,args.stage);print('PASS',args.stage,p.name);return
136
+ if args.cmd=='run':
137
+ gate(p,'implement');mapping=acmap(p)
138
+ if args.ac not in mapping:raise ValueError('unknown AC')
139
+ cmd=args.command
140
+ if cmd and cmd[0]=='--':cmd=cmd[1:]
141
+ if not cmd:raise ValueError('command required after --')
142
+ if cmd not in [v.split() for v in mapping[args.ac]]:raise ValueError('command is not declared for this AC in ac-tests.json')
143
+ if cmd[0].startswith('-'):raise ValueError('command must begin with executable')
144
+ before=digest();proc=subprocess.run(cmd,cwd=ROOT,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
145
+ after=digest();logs=p/'logs';logs.mkdir(exist_ok=True)
146
+ tag=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ');name='logs/'+tag+'-'+args.ac+'.log'
147
+ (p/name).write_text('$ '+' '.join(cmd)+'\n'+proc.stdout)
148
+ row={'timestamp':now(),'ac':args.ac,'command':cmd,'exit_code':proc.returncode,'source_digest':after,'source_changed_during_test':before!=after,'log_file':name}
149
+ if before!=after:row['exit_code']=999
150
+ with (p/'evidence.jsonl').open('a') as f:f.write(json.dumps(row,ensure_ascii=False)+'\n')
151
+ print('RECORDED',args.ac,'exit',proc.returncode,'source_changed',before!=after,'log',p/name)
152
+ sys.exit(1 if before!=after else proc.returncode)
153
+ if args.cmd=='review':
154
+ gate(p,'review');notes=pathlib.Path(args.notes_file).resolve()
155
+ if not notes.is_file() or not notes.read_text().strip():raise ValueError('review notes required')
156
+ reviewdir=p/'reviews';reviewdir.mkdir(exist_ok=True);dest=reviewdir/('review-'+now().replace(':','-')+'.md');shutil.copyfile(notes,dest)
157
+ s['review']={'verdict':args.verdict,'at':now(),'notes':str(dest.relative_to(p)),'source_digest':digest()};save(p,s);print('REVIEW',args.verdict);return
158
+ if args.cmd=='complete':
159
+ if not args.user_confirmed:raise ValueError('explicit user completion approval required; agent must pass --user-confirmed only after user authorizes completion')
160
+ gate(p,'complete')
161
+ if p.parent.name!='active':raise ValueError('already completed')
162
+ s.pop('completed_by',None);s['completed_at']=now();save(p,s);dst=BASE/'completed'/p.name;p.rename(dst);print('COMPLETED',dst)
163
+ if __name__=='__main__':
164
+ try:main()
165
+ except (ValueError,OSError,json.JSONDecodeError) as e:print('BLOCK:',e,file=sys.stderr);sys.exit(2)
@@ -0,0 +1,5 @@
1
+ {
2
+ "mode": "markdown",
3
+ "source": "docs/product-specs",
4
+ "note": "Set mode=openspec and source=openspec/changes only if repository uses OpenSpec. Do not duplicate spec content."
5
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "profiles": {
3
+ "web": {
4
+ "patterns": [
5
+ "src/**/*.tsx",
6
+ "src/**/*.ts",
7
+ "web/**",
8
+ "src/*.ts",
9
+ "src/*.tsx"
10
+ ],
11
+ "checks": {
12
+ "lint": [
13
+ "npm",
14
+ "run",
15
+ "lint"
16
+ ],
17
+ "test": [
18
+ "npm",
19
+ "test"
20
+ ]
21
+ }
22
+ },
23
+ "python": {
24
+ "patterns": [
25
+ "**/*.py",
26
+ "*.py"
27
+ ],
28
+ "checks": {
29
+ "unit": [
30
+ "python3",
31
+ "-m",
32
+ "pytest"
33
+ ]
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,23 @@
1
+ # Shared AI development workflow — OpenCode / Codex / Claude Code
2
+
3
+ **Use upstream installed skills** (Matt Pocock and Superpowers), not home-grown substitutes. CRG is independently managed. Apply this file in all three agents; actual skill discovery/activation must be verified in each host.
4
+
5
+ ## On every new user development request / resumed session
6
+ 1. **At the start of EVERY NEW AGENT SESSION, before planning or coding**, run `node .ai-dev/session-status.cjs` from the project root. Display the active task summary to the user (ID, phase, next action), even if the user has not asked for a new task. Read applicable task files and resume rather than duplicating. This is required for OpenCode, Codex, and Claude Code. If invocation is unavailable, say the session check was not performed; never claim it ran.
7
+ 2. Read `docs/PLANS.md`, `docs/ai-workflow/POLICY.md`, and the applicable task files. Invoke upstream skills through the host's skill tool; merely reading SKILL.md is not equivalent to invoking a skill.
8
+ 3. If a new substantive request, **agent** creates task using `python3 scripts/workflow.py new --id TASK-... --title '...'`. Agent writes analysis.md, spec.md, plan.md, ac-tests.json using original upstream skills.
9
+ 4. Summarize plan and explicitly ask the human for approval. **Do not run approve before human approval.** An explicit natural-language approval (e.g. '계획 승인', '진행해') in response to the presented plan allows the **agent** to run `python3 scripts/workflow.py approve --id TASK-... --user-confirmed`. No person needs to type CLI commands.
10
+ 5. Before editing source, **agent** runs `python3 scripts/workflow.py gate --id TASK-... --stage implement`. Use upstream TDD/implementation skills. Never bypass a failed gate.
11
+ 6. **Agent** runs tests through `workflow.py run --id TASK-... --ac AC-.. -- <command>`, reviews real logs, and checks `workflow.py gate --id TASK-... --stage review`. Failed/insufficient coverage: fix and rerun.
12
+ 7. Invoke original `code-review` / reviewer skill in an independent context where possible, evaluate diff+spec+evidence; write substantial notes and **agent** records `workflow.py review --id TASK-... --verdict PASS|FAIL --notes-file <path>`. A recorded verdict is not proof of reviewer independence.
13
+ 8. When `workflow.py gate --id TASK-... --stage complete` passes, report `READY_FOR_REVIEW` and ask human to confirm completion. Only after explicit human confirmation may **agent** call `workflow.py complete --id TASK-... --user-confirmed`. No self-approval.
14
+ 9. For all stages, keep user interaction **natural-language only**; CLI is internal agent tooling. On session changes, any of the three agents resumes the same project files.
15
+
16
+ ## External skill mapping
17
+ Analyze: `grill-with-docs`, `systematic-debugging`, CRG if installed. Plan: `to-spec`, `to-tickets`, `writing-plans`. Implement: `implement`, `test-driven-development`, optional `implement-spec` only with compatible host runtime. Verify: `verification-before-completion`. Review: `code-review`, `requesting-code-review`.
18
+
19
+ ## Boundaries
20
+ No automatic commit, push, merge, or PR; never invent test/review evidence. An ambiguous 'yes' not clearly responding to a specific approval question is not consent. Agent runs no approval command on inference, and never claims person identity is verified. The CLI gates are structural; AC semantic adequacy and independent-review authenticity require critical inspection. If original skills aren't available, report that; don't claim successful invocation.
21
+
22
+ ## Native session status hooks
23
+ `npx --yes @gon1942/ai-dev-external-framework@0.8.2 setup` installs read-only SessionStart hooks (OpenCode plugin, Codex and Claude hook configs). Agents must report the injected work-state to user once per new session and read the work files before edits. Session status is a snapshot, not proof of current test evidence. Do not create a task just because a session starts.
@@ -0,0 +1,3 @@
1
+ # Architecture
2
+
3
+ Document actual module boundaries, dependencies and important interfaces here. Keep this file project-specific.
@@ -0,0 +1,3 @@
1
+ # Design principles
2
+
3
+ Project-specific UI/UX and design constraints.
@@ -0,0 +1,3 @@
1
+ # Frontend conventions
2
+
3
+ Document chosen frontend stack and interaction testing.
@@ -0,0 +1,23 @@
1
+ # Work tracking — CLI commands are **for agents**, not users
2
+
3
+ Agents start each session with `python3 scripts/workflow.py next`, then use the **original externally installed skills**. All three hosts share `docs/exec-plans/active/TASK-ID/`.
4
+
5
+ | Stage | Agent operation | Artifact / control |
6
+ |---|---|---|
7
+ | Analyze | `workflow.py new --id TASK-001 --title '...'` then upstream diagnostic skills | `analysis.md` |
8
+ | Plan | upstream `to-spec`, `to-tickets`, `writing-plans` | `spec.md`, `plan.md`, `ac-tests.json` |
9
+ | Approval | **ask user in chat**; only after explicit user plan approval: `workflow.py approve --id TASK-001 --user-confirmed` | timestamp/flag only; not user authentication |
10
+ | Implement | `workflow.py gate --id TASK-001 --stage implement`; upstream TDD skills | code, tests, optional RED record |
11
+ | Verify | `workflow.py run --id TASK-001 --ac AC-01 -- python3 -m pytest tests/test_feature.py` | timestamped execution log, exit code, source digest |
12
+ | Review | `workflow.py gate --id TASK-001 --stage review`, independent original `code-review`; `workflow.py review --id TASK-001 --verdict PASS --notes-file review.md` | substantive reviewer notes; independence not automatically verified |
13
+ | Completion | `workflow.py gate --id TASK-001 --stage complete`; **ask user in chat**; only after explicit completion approval run `workflow.py complete --id TASK-001 --user-confirmed` | moves task folder to `completed/` |
14
+
15
+ Natural language interaction with user is sufficient; the agent must issue internal commands without asking user to copy commands. `--user-confirmed` is an attestation by the agent, **not** proof of human identity. Never silently auto-approve. On review FAIL, return to implementation/verification and rerun tests; a stale review cannot complete.
16
+
17
+ `ac-tests.json` maps each AC ID to nonempty descriptions of test commands. Gate checks successful recorded command executions and current source digest, **not** semantic adequacy of test-to-AC mapping. Do not use a no-op or unrelated test as evidence.
18
+
19
+ ## New session — required first action
20
+
21
+ All agents run `python3 scripts/workflow.py session` on a new session and summarize active tasks in chat **before proceeding with any work**. It is read-only: session start alone never creates, approves, reviews, or completes tasks. `--json` supports machine parsing and `--all` includes completed plans. The user does not need to type the command. Native host startup execution is not assumed: use the supplied launcher for guaranteed terminal start-time printing; host prompt compliance still needs host-level testing.
22
+
23
+ When OpenSpec is active, do not duplicate its change spec under product-specs; store only task execution plan and evidence.
@@ -0,0 +1,3 @@
1
+ # Product sense
2
+
3
+ Define audience, user value, and non-goals.
@@ -0,0 +1,3 @@
1
+ # Quality criteria
2
+
3
+ Document lint, typecheck, unit, integration, and E2E expectations.
@@ -0,0 +1,3 @@
1
+ # Reliability
2
+
3
+ Document SLAs, alerting, failure handling, backups and recovery.
@@ -0,0 +1,3 @@
1
+ # Security
2
+
3
+ Document security requirements and prohibit secrets in logs.
@@ -0,0 +1,13 @@
1
+ # Upstream skills (installed on demand)
2
+
3
+ Matt Pocock: https://github.com/mattpocock/skills
4
+ Superpowers: https://github.com/obra/superpowers
5
+ Ponytail: https://github.com/DietrichGebert/ponytail
6
+
7
+ Analyze: grill-with-docs, diagnosing-bugs, systematic-debugging, brainstorming.
8
+ Plan: to-spec, to-tickets, writing-plans.
9
+ Implement: implement, implement-spec, test-driven-development, using-git-worktrees, subagent-driven-development, ponytail (minimal implementation guidance).
10
+ Verify: verification-before-completion.
11
+ Review: code-review, requesting-code-review, receiving-code-review, retro.
12
+
13
+ **Important:** Original skills can require extra capabilities/plugins/issues/subagents. Only the skill directories are installed by this package. Ponytail native plugin activation, mode switching and lifecycle hooks are NOT installed; for complete Ponytail functionality install its official host plugin following README instructions. Treat unsupported subagent and background functionality as unavailable, and use sequential fallback. CRG is intentionally not installed or modified by this package.
@@ -0,0 +1,8 @@
1
+ # Governance
2
+ - Original third-party skills are installed unmodified. External skill instructions never override repository approval rules.
3
+ - Human approval required before implementation and completion; no autonomous commit/push/merge.
4
+ - Verify actual exit status and retain evidence. Do not mark AC pass without a substantive coverage review.
5
+ - Avoid sensitive data in commands and output: verification logs are stored on disk.
6
+ - `workflow.py` is a deterministic state/file/evidence controller, not an AI skill or a substitute for actual testing.
7
+
8
+ - User operates in natural language; internal CLI is agent-invoked. Explicit human approval required for plan and final completion; `--user-confirmed` is a self-attestation, not authentication.
@@ -0,0 +1,3 @@
1
+ # Core beliefs
2
+
3
+ Evidence precedes completion claims. Favor minimal, reversible changes.
@@ -0,0 +1,3 @@
1
+ # Design documentation
2
+
3
+ Link architectural decisions and significant designs.
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ # Technical debt
2
+
3
+ | Item | Impact | Owner | Planned resolution |
4
+ | --- | --- | --- | --- |
@@ -0,0 +1,3 @@
1
+ # Generated schema (placeholder)
2
+
3
+ No schema extracted yet.
@@ -0,0 +1,3 @@
1
+ # Product specifications
2
+
3
+ One canonical spec lives inside each active/completed task folder; link notable specs here.
@@ -0,0 +1 @@
1
+ Placeholder: add trusted design-system reference here.
@@ -0,0 +1 @@
1
+ Placeholder: add trusted Nixpacks reference here.
@@ -0,0 +1 @@
1
+ Placeholder: add trusted uv reference here.