agent-work-loop 0.0.0 → 0.6.23
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/README.md +272 -12
- package/dist/brief-Z3JKXEUP.js +181 -0
- package/dist/changelog-R7BNBF2C.js +62 -0
- package/dist/chunk-46HZN6UB.js +446 -0
- package/dist/chunk-4OCSYHYB.js +274 -0
- package/dist/chunk-6E7XEQOH.js +27 -0
- package/dist/chunk-7SYRDDTX.js +516 -0
- package/dist/chunk-BUWGQVHT.js +1243 -0
- package/dist/chunk-C7BR2DCS.js +96 -0
- package/dist/chunk-D5OINC3G.js +52 -0
- package/dist/chunk-DP4O5ME2.js +307 -0
- package/dist/chunk-F5LHXBH7.js +209 -0
- package/dist/chunk-G5LAJ5TV.js +453 -0
- package/dist/chunk-I77CXOEX.js +693 -0
- package/dist/chunk-IMB46O6S.js +286 -0
- package/dist/chunk-IXMAFR4Y.js +771 -0
- package/dist/chunk-QE2CLNBG.js +347 -0
- package/dist/chunk-UOPWVM2H.js +727 -0
- package/dist/chunk-YTAHVR4P.js +166 -0
- package/dist/chunk-ZE6HXOYG.js +904 -0
- package/dist/cli.js +374 -13
- package/dist/commit-APXIVOSD.js +411 -0
- package/dist/config-TFMW7O4T.js +34 -0
- package/dist/doctor-SSKNLPGH.js +29 -0
- package/dist/evolve-QPD7TWGO.js +38 -0
- package/dist/feedback-KAXNFMUY.js +125 -0
- package/dist/gotchas-MCA5Y76R.js +43 -0
- package/dist/hold-recheck-WN5EG7HD.js +133 -0
- package/dist/init-UDM5AXKI.js +79 -0
- package/dist/lane-DAZISODH.js +41 -0
- package/dist/loop-summary-XAI6KOGB.js +361 -0
- package/dist/metrics-WLRZZRTK.js +25 -0
- package/dist/record-UKDIUJ5T.js +68 -0
- package/dist/review-ZTHDJ47V.js +118 -0
- package/dist/rules-R2UZPIVW.js +33 -0
- package/dist/state-XM7NZ2HA.js +37 -0
- package/dist/status-L6U5KO6T.js +40 -0
- package/dist/uninstall-5DFEOFL5.js +545 -0
- package/dist/update-AYTBYAHI.js +61 -0
- package/dist/verify-L7ARTK42.js +37 -0
- package/dist/version-check-LKGU2DNF.js +14 -0
- package/dist/work-KEPTGZ6H.js +50 -0
- package/engine/skills/claude/awl-loop/SKILL.md +292 -0
- package/engine/skills/claude/awl-loop/reference.md +131 -0
- package/engine/skills/claude/awl-pipeline/SKILL.md +89 -0
- package/engine/skills/claude/awl-pipeline-exec/SKILL.md +200 -0
- package/engine/skills/claude/awl-pipeline-plan/SKILL.md +71 -0
- package/engine/skills/claude/awl-pipeline-review/SKILL.md +149 -0
- package/engine/skills/codex/AGENTS.awl.md +117 -0
- package/engine/templates/block-publish.mjs +9 -0
- package/engine/templates/pre-push.sample +7 -0
- package/engine/templates/related-cmd-examples.md +37 -0
- package/engine/version.json +2 -2
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -2,25 +2,285 @@
|
|
|
2
2
|
|
|
3
3
|
`agent-work-loop`(명령어: `awl`)은 AI 에이전트가 같은 실패를 두 번 반복하지 않도록 돕는 도구입니다. 에이전트가 작업하며 남긴 기록, 검증 결과, 완료 조건, 규칙, 교훈, 막힘을 파일로 관리합니다.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
이 README는 **실용 문서**입니다. 설치부터 첫 루프까지, 손으로 따라 할 수 있는 순서만 다룹니다. "왜 이렇게 만들었는가"(하네스 개념, 게이트 위치, narrative 실증 사례)는 [`docs/presentation/storyline.md`](docs/presentation/storyline.md)에 따로 정리했습니다. 명령어 하나하나의 자세한 사용법은 [`docs/presentation/commands.md`](docs/presentation/commands.md)를 참고하세요.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**awl 자체는 판단하지 않습니다.** LLM을 호출하지 않고, 파일과 상태만 결정적으로 다룹니다. 무엇을 배우고 어떤 규칙을 세울지는 이미 설치된 에이전트(Claude Code, Codex)가 스킬로 판단합니다.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
크로스 환경을 처음부터 전제로 합니다. macOS와 Windows, Claude Code와 Codex를 모두 지원하는 것을 목표로 합니다(Windows는 아직 macOS만큼 검증되지 않았습니다. [알려진 위험](docs/decisions.md)을 참고하세요).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 언제 쓰나
|
|
14
|
+
|
|
15
|
+
세 단계로 나뉩니다. 자세한 판단 기준은 [`storyline.md` 3절](docs/presentation/storyline.md)에 있고, 여기서는 요약만 둡니다.
|
|
16
|
+
|
|
17
|
+
1. **요구사항 하나** — 그냥 합니다. awl이 필요 없습니다.
|
|
18
|
+
2. **완료 조건이 여러 개고 되돌아볼 가치가 있는 작업** — `/awl-loop`. 조사→게이트1→자율 반복→게이트2→evolve를 한 세션이 진행합니다.
|
|
19
|
+
3. **여러 워크아이템을 동시에 격리해서 돌려야 할 때** — `/awl-pipeline`. `awl lane`으로 격리 워크트리를 만들고, 오케스트레이터가 plan/exec/review를 스폰합니다.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 설치
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g agent-work-loop
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
pnpm이나 yarn을 쓴다면:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add -g agent-work-loop
|
|
33
|
+
yarn global add agent-work-loop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> **pnpm 함정**: pnpm으로 전역 설치하면 `awl` 명령을 찾을 수 없다는 에러가 날 수 있습니다. pnpm의 전역 bin 디렉토리가 PATH에 없어서입니다. `pnpm setup`을 한 번 실행하고 터미널을 새로 열면 해결됩니다.
|
|
37
|
+
|
|
38
|
+
> **팀 내부 설치(지금 당장 필요하면)**: 이 README를 작성하는 시점 기준으로 npm 레지스트리의 `agent-work-loop`는 아직 `0.0.0`(이름 예약용 placeholder)만 배포돼 있어 `npm install -g agent-work-loop`로는 동작하는 CLI가 설치되지 않습니다. 지금 바로 써야 한다면 이 저장소를 클론해 직접 빌드·링크하세요.
|
|
39
|
+
> ```bash
|
|
40
|
+
> git clone <이 저장소> && cd agent-work-loop
|
|
41
|
+
> pnpm install && pnpm run build
|
|
42
|
+
> pnpm link --global # 또는: npm link
|
|
43
|
+
> ```
|
|
44
|
+
> `dist/cli.js`가 `awl` bin으로 등록됩니다. 정식 npm 배포가 되면 이 문단은 지웁니다.
|
|
45
|
+
|
|
46
|
+
설치가 됐는지 확인합니다:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
$ awl doctor
|
|
50
|
+
|
|
51
|
+
+- Agent Work Loop · 진단 ------------------------------------------------------------------+
|
|
52
|
+
| 환경
|
|
53
|
+
| |-- Node: v22.22.2 [ok]
|
|
54
|
+
| |-- 플랫폼: darwin arm64 [ok]
|
|
55
|
+
| `-- 터미널: 유니코드 미지원, 색 미지원 [ok]
|
|
56
|
+
|
|
|
57
|
+
| 전역 설치
|
|
58
|
+
| `-- ~/.awl: 없음 [x] awl init 을 실행하세요
|
|
59
|
+
|
|
|
60
|
+
| 이 프로젝트
|
|
61
|
+
| |-- 프로젝트 루트: (생략)
|
|
62
|
+
| |-- 브랜치: main
|
|
63
|
+
| `-- config.json: 없음 [x] awl init 을 실행하세요
|
|
64
|
+
|
|
|
65
|
+
| 에이전트
|
|
66
|
+
| |-- Claude Code: 없음
|
|
67
|
+
| |-- Codex: 없음
|
|
68
|
+
|
|
|
69
|
+
| [x] 문제 2개. awl init 을 실행하세요.
|
|
70
|
+
+-------------------------------------------------------------------------------------------+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
터미널이 유니코드/색을 지원하면(Windows Terminal, VS Code 터미널 등) 자동으로 감지해 상자 그림과 색을 씁니다. 지원 안 되면 위처럼 ASCII로 자동 전환됩니다. `doctor`는 아무것도 고치지 않습니다. 점검만 합니다.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 5분 시작
|
|
78
|
+
|
|
79
|
+
프로젝트 디렉토리에서 한 번만 실행합니다.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ cd my-project
|
|
83
|
+
$ awl init
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
질문 없이 감지된 값으로 바로 진행하려면 `awl init --yes`를 씁니다. 실제 출력(스크래치 프로젝트, `--yes`):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
+- 설정 완료 ---------------------------------------------------------------+
|
|
90
|
+
| ~/.awl 생성됨 |
|
|
91
|
+
| ~/.awl/engine 0.6.18 |
|
|
92
|
+
| .awl/config.json 생성됨 <- 커밋하세요. 팀원은 이 파일을 씁니다 |
|
|
93
|
+
| .awl/state.json gitignore 에 추가함 [ok] git push 차단 훅 설치 |
|
|
94
|
+
| 규칙 0개 · 교훈 0개 · 등록된 프로젝트 1개 · 1세대 |
|
|
95
|
+
+---------------------------------------------------------------------------+
|
|
96
|
+
|
|
97
|
+
+- 다음 단계 ----------------------------------------------------+
|
|
98
|
+
| Claude Code 를 열고 이렇게 말하세요. |
|
|
99
|
+
| |
|
|
100
|
+
| /awl-loop 페이지 편집기에 여백 시스템을 넣고 싶어 |
|
|
101
|
+
+----------------------------------------------------------------+
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
대화형 모드(`--yes` 없이)는 3단계 화면(1/3 주 언어 → 2/3 검증 명령어 → 3/3 규칙과 이 프로젝트의 성격)을 방향키(또는 번호)로 물은 뒤, 설치할 에이전트를 고르는 스킬 화면을 보여줍니다.
|
|
105
|
+
|
|
106
|
+
`awl init`이 만든 것:
|
|
107
|
+
|
|
108
|
+
- **`.awl/config.json`** — 검증 명령과 프로젝트 성격. **커밋하세요.** 팀원이 같이 씁니다.
|
|
109
|
+
- **`.awl/state.json`** — 지금 어느 워크아이템의 어느 단계인지. gitignore 대상입니다.
|
|
110
|
+
- **`.claude/skills/`** 또는 **`AGENTS.md`** — 작업 루프 스킬. 선택한 에이전트에 설치됩니다.
|
|
111
|
+
- **`.git/hooks/pre-push`** — `AWL_ALLOW_PUSH=1` 없이는 `git push`를 막는 훅. "push는 사람이 한다"는 규칙을 git 레벨에서도 강제합니다(사람이 push할 때는 `AWL_ALLOW_PUSH=1 git push`로 명시적으로 통과시킵니다).
|
|
112
|
+
|
|
113
|
+
이제 에이전트(Claude Code / Codex)를 열고 목표를 서술문으로 줍니다.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
/awl-loop 페이지 편집기에 여백 시스템을 넣고 싶어
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
스킬이 [조사]→[설계]→[명료화]→[스파이크]→[완료 조건]→게이트1→자율 반복→게이트2→evolve를 진행합니다. 왜 이 순서인지는 [`storyline.md` 6절](docs/presentation/storyline.md)에서 다룹니다.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
awl status # 지금 어디까지 왔는지 한눈에
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 명령어 요약
|
|
128
|
+
|
|
129
|
+
자주 쓰는 것만 적습니다. 25개 명령(사람용 17 + 스킬용 8) 전부와 실제 실행 예시는 [`commands.md`](docs/presentation/commands.md)에 있습니다.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
awl status # 지금 어디까지 왔는지
|
|
133
|
+
awl doctor # 설치·환경 점검 (아무것도 안 고침)
|
|
134
|
+
awl brief # 오늘(KST) 진행분 모아 보기
|
|
135
|
+
awl records # 쌓인 기록
|
|
136
|
+
awl gotchas # 아직 규칙이 안 된 교훈
|
|
137
|
+
awl rules # 이 프로젝트에 적용되는 규칙
|
|
138
|
+
awl metrics # 워크아이템 세대별 지표 추세
|
|
139
|
+
awl feedback # awl 도구 자체에 대한 피드백
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 설정(config) — 검증 명령과 모노레포
|
|
145
|
+
|
|
146
|
+
`.awl/config.json`은 프로젝트의 검증 명령을 담습니다. `awl verify`가 이 명령들을 실제로 실행해 통과/실패를 가릅니다.
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"project": "my-project",
|
|
151
|
+
"mainLanguage": "typescript",
|
|
152
|
+
"character": "디자인 토큰 강제, 여백은 자유 px 금지",
|
|
153
|
+
"verify": {
|
|
154
|
+
"typecheck": { "cmd": "tsc --noEmit" },
|
|
155
|
+
"lint": { "cmd": "biome check ." },
|
|
156
|
+
"test": { "cmd": "vitest run" },
|
|
157
|
+
"e2e": null
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`verify.<name>`이 `null`이면 그 검증은 건너뜁니다.
|
|
163
|
+
|
|
164
|
+
### 모노레포: 패키지별로 다른 위치에서 검증하기
|
|
165
|
+
|
|
166
|
+
`typecheck`/`lint`/`test`/`e2e` 각각에 `cwd`(작업 디렉토리)를 지정합니다. 상대경로 실행 파일(`../../node_modules/.bin/tsc` 같은 것)도 그 `cwd` 기준으로 풀립니다.
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
"verify": {
|
|
170
|
+
"typecheck": { "cmd": "tsc -p gallery/tsconfig.json", "cwd": "packages/page-harness" },
|
|
171
|
+
"test": { "cmd": "vitest run", "cwd": "packages/app" }
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`awl init`이 모노레포를 감지하면(루트에 검증 명령이 안 보이면) 어느 패키지를 검증할지 물어봅니다. 나중에 바꾸려면:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
$ awl config set verify.typecheck.cwd packages/app
|
|
179
|
+
저장했습니다: verify.typecheck.cwd = packages/app
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`cwd` 디렉토리가 실제로 없으면 저장을 거부합니다(`--force`로 강제 가능). 키를 생략하면 지금 설정 가능한 키와 현재 값을 전부 보여줍니다. TTY에서 `awl config`를 인자 없이 실행하면 항목을 골라 수정하는 화면이 뜹니다.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 무엇이 어디에 쌓이나
|
|
187
|
+
|
|
188
|
+
| | `~/.awl` (전역, 사람 기준) | `<project>/.awl` (프로젝트 기준) |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| 담는 것 | 엔진(스킬 템플릿), 규칙, 교훈(gotcha), 기록(records), 등록된 프로젝트 목록 | 이 프로젝트의 설정(config.json), 지금 루프 위치(state.json) |
|
|
191
|
+
| 누구 것인가 | **당신**. 프로젝트를 옮겨 다녀도 따라옵니다 | **이 프로젝트**. 리포지토리를 나가면 안 따라옵니다 |
|
|
192
|
+
| git 커밋 | 대상 아님(홈 디렉토리) | `config.json`은 커밋, `state.json`은 gitignore |
|
|
193
|
+
|
|
194
|
+
두 층으로 나눈 이유(다음 프로젝트에서 빈손으로 시작하지 않기 위해)와 규칙/교훈이 어떻게 승격되는지는 [`storyline.md` 4절·5절](docs/presentation/storyline.md)에서 다룹니다. `AWL_HOME` 환경변수로 전역 위치를 바꿀 수 있습니다(테스트·CI에서 유용합니다).
|
|
195
|
+
|
|
196
|
+
규칙 승격은 사람이 직접 합니다(자동 승격 없음):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
awl rules promote G-003 \
|
|
200
|
+
--applies "여백 값을 CSS/스타일에 쓸 때" \
|
|
201
|
+
--counter "이미 디자인 토큰 시스템이 없는 프로젝트"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
교훈(gotcha, `~/.awl/gotchas`)과 awl 도구 자체 피드백(`~/.awl/records`, `awl record awl-feedback`)은 다른 종류입니다 — 전자는 작업 대상 코드에 대한 것, 후자는 "awl commit이 이래서 아팠다" 같은 도구 자체의 문제입니다.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 워크아이템 병행
|
|
209
|
+
|
|
210
|
+
한 프로젝트에서 동시에 여러 작업을 오갈 때 `awl work`를 씁니다.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
awl work list # 등록된 워크아이템과 진행 상황
|
|
214
|
+
awl work new <ID> [설명] # 새 워크아이템을 만들고 전환(현재 것은 보관)
|
|
215
|
+
awl work switch <ID> # 보관된 워크아이템으로 전환
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`*`가 지금 작업 중인 워크아이템입니다. `awl work switch`로 다른 브랜치에서 만든 워크아이템으로 돌아가면, 그 워크아이템이 만들어질 때의 브랜치와 지금 브랜치가 다를 경우 경고합니다(막지는 않습니다). 완료 조건 사이에 순서가 있으면 `dependsOn`을 붙일 수 있고, `awl status`가 아직 안 끝난 선행 조건이 있는 완료 조건을 "블록됨"으로 보여줍니다.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 오케스트레이션 파이프라인 — 여러 레인을 나란히
|
|
223
|
+
|
|
224
|
+
위 "워크아이템 병행"이 한 워크트리 안에서 작업을 순차로 오가는 **작업 루프**라면, 오케스트레이션은 워크아이템마다 전용 워크트리(레인)를 두고 **나란히** 진행하는 층입니다. 세 조각으로 씁니다.
|
|
225
|
+
|
|
226
|
+
- **`awl lane`** — 격리 레인을 만듭니다. `awl lane new <이름>`은 전용 워크트리와 별도 `AWL_HOME`, 스킬 한 벌을 깔고 기동 안내를 찍습니다. `awl lane ls`로 현존 레인을 보고, `awl lane rm <이름>`으로 정리합니다.
|
|
227
|
+
- **`awl status --pipeline`** — 레인별 진행을 한 화면에 모읍니다. 각 레인 안의 plan/exec/review 단계 상태를 배지로 훑습니다.
|
|
228
|
+
- **역할 스킬(`awl-pipeline` 계열)** — 오케스트레이터(`/awl-pipeline`)가 plan 역할로 들어가 exec·review를 스폰합니다. `awl init`에서 함께 설치됩니다.
|
|
229
|
+
|
|
230
|
+
개념(왜 3단으로 나뉘는지, `pipeline-hold-recheck` 실증 사례)은 [`storyline.md` 3절·6절](docs/presentation/storyline.md)에서, 명령별 실행 예시는 [`commands.md`](docs/presentation/commands.md)에서 다룹니다.
|
|
231
|
+
|
|
232
|
+
**로드맵**: 지금은 오케스트레이터가 레인을 만들고 그 안에서 plan/exec/review 세션을 사람이 직접 엽니다. 목표 하나만 던지면 세션까지 자동으로 뜨는 auto-spawn은 설계만 있고 아직 탑재되지 않았습니다.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 문제 해결
|
|
237
|
+
|
|
238
|
+
**`awl: command not found` (pnpm 설치 후)**
|
|
239
|
+
`pnpm setup`을 실행하고 터미널을 새로 엽니다. pnpm 전역 bin이 PATH에 없어서입니다.
|
|
240
|
+
|
|
241
|
+
**`npm install -g agent-work-loop`를 했는데 `awl`이 아무것도 못 한다**
|
|
242
|
+
위 설치 섹션 참고 — 아직 npm에 `0.0.0` placeholder만 배포돼 있습니다. 저장소를 클론해 `pnpm install && pnpm run build && pnpm link --global`로 설치하세요.
|
|
243
|
+
|
|
244
|
+
**`awl --version`이 엔진 버전 불일치를 경고한다**
|
|
245
|
+
`awl`을 업그레이드했는데 `~/.awl/engine`은 예전 버전 그대로일 때 나옵니다. `awl update`로 엔진을 갱신하면 맞춰집니다(프로젝트 설정은 안 건드립니다). `awl version-check`로 어긋난 버전 쌍(package.json, 설치된 엔진, 프로젝트 config, 설치된 스킬)을 직접 봅니다. 프로젝트 config가 엔진보다 낡았다면 `awl init --yes`를 다시 실행합니다.
|
|
246
|
+
|
|
247
|
+
**업그레이드해도 프로젝트 설정이 사라지나요**
|
|
248
|
+
아니요. `awl init --yes`는 `config.engineVersion`만 지금 엔진에 맞추고, 팀이 정한 검증 명령 등 나머지 설정 필드는 그대로 둡니다.
|
|
249
|
+
|
|
250
|
+
**`awl doctor`가 검증 명령을 "명령을 찾을 수 없습니다"라고 한다**
|
|
251
|
+
`node_modules/.bin`이 지금 셸의 PATH에 없을 수 있습니다. `cwd`를 지정했다면 그 디렉토리 자체가 없는 건 아닌지도 확인하세요.
|
|
252
|
+
|
|
253
|
+
**모노레포에서 검증 명령이 상대경로 실행 파일을 못 찾는다**
|
|
254
|
+
`verify.<name>.cwd`를 그 패키지 디렉토리로 지정하세요. `cwd` 없이 프로젝트 루트에서 실행하면 `../../node_modules/.bin/tsc` 같은 상대경로가 안 풀립니다.
|
|
255
|
+
|
|
256
|
+
**`awl commit`이 "hunk가 남의 변경과 겹칠 수 있다"며 거부한다**
|
|
257
|
+
정확한 동작입니다 — 확신할 수 없으면 커밋하지 않고 사람에게 알리도록 설계했습니다. `git status`/`git diff`로 실제로 무엇이 섞였는지 확인하세요. 완료 조건마다 `awl commit <AC> -m`으로 닫고 넘어가는 습관을 들이세요(`awl commit --start`를 편집보다 먼저 호출하지 않으면 그 편집이 스냅샷에 흡수돼 격리 커밋이 무의미해집니다).
|
|
258
|
+
|
|
259
|
+
**`git push`가 막힌다**
|
|
260
|
+
`awl init`이 심는 pre-push 훅 때문입니다(위 5분 시작 참고). 정말 push하려면 `AWL_ALLOW_PUSH=1 git push`를 씁니다.
|
|
261
|
+
|
|
262
|
+
**게이트에서 스킬이 안 멈추고 그냥 진행한다**
|
|
263
|
+
스킬 문서가 게이트에서 반드시 도구를 호출하도록 지시돼 있습니다. 계속 안 멈추면 스킬이 최신인지 확인하세요 — `awl doctor`가 `claude-skill-vs-engine` 불일치를 경고하면 `awl init --yes`로 재설치합니다.
|
|
264
|
+
|
|
265
|
+
**awl을 완전히 제거하려면**
|
|
266
|
+
`awl uninstall`을 실행합니다 — 기본은 드라이런이라 먼저 무엇이 지워질지 보여주고, 실제로 지우려면 `--yes`를 붙입니다. 기본은 이 프로젝트만 지우고, `~/.awl`까지 지우려면 `--global`을 명시해야 합니다. 자세한 플래그와 실제 출력은 [`commands.md`의 `awl uninstall`](docs/presentation/commands.md#awl-uninstall)을 참고하세요.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 기여 / 라이선스
|
|
271
|
+
|
|
272
|
+
이 저장소는 pnpm을 사용합니다.
|
|
14
273
|
|
|
15
274
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
275
|
+
pnpm install # 의존성 설치
|
|
276
|
+
pnpm run build # dist/cli.js 빌드
|
|
277
|
+
pnpm test # vitest 실행
|
|
278
|
+
pnpm run lint # biome 검사
|
|
279
|
+
pnpm run typecheck
|
|
20
280
|
```
|
|
21
281
|
|
|
22
|
-
|
|
282
|
+
릴리스는 `pnpm release:patch` / `pnpm release:minor` / `pnpm release:major`로 합니다. 검증·빌드·tarball·`npm publish --dry-run`을 통과한 뒤 버전, CHANGELOG, 커밋, 태그, 원격 push를 한 번에 처리합니다. **`npm publish`만 사람이 실행합니다.**
|
|
23
283
|
|
|
24
|
-
|
|
284
|
+
npm에 `awl` 패키지가 이미 있어, 패키지 이름은 `agent-work-loop`로 배포하고 명령어 이름만 `awl`로 씁니다.
|
|
25
285
|
|
|
26
|
-
MIT
|
|
286
|
+
라이선스는 MIT입니다.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadProjectName,
|
|
3
|
+
readRecords
|
|
4
|
+
} from "./chunk-I77CXOEX.js";
|
|
5
|
+
import {
|
|
6
|
+
loadState
|
|
7
|
+
} from "./chunk-4OCSYHYB.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveProjectRoot,
|
|
10
|
+
run
|
|
11
|
+
} from "./chunk-UOPWVM2H.js";
|
|
12
|
+
import "./chunk-BUWGQVHT.js";
|
|
13
|
+
import "./chunk-7SYRDDTX.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/brief.ts
|
|
16
|
+
var KST_OFFSET_MS = 9 * 60 * 60 * 1e3;
|
|
17
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
18
|
+
function kstDateOf(utcMs) {
|
|
19
|
+
const shifted = new Date(utcMs + KST_OFFSET_MS);
|
|
20
|
+
const y = shifted.getUTCFullYear();
|
|
21
|
+
const m = String(shifted.getUTCMonth() + 1).padStart(2, "0");
|
|
22
|
+
const d = String(shifted.getUTCDate()).padStart(2, "0");
|
|
23
|
+
return `${y}-${m}-${d}`;
|
|
24
|
+
}
|
|
25
|
+
function kstDayRange(kstDate) {
|
|
26
|
+
const startMs = Date.parse(`${kstDate}T00:00:00.000+09:00`);
|
|
27
|
+
return { startMs, endMs: startMs + DAY_MS };
|
|
28
|
+
}
|
|
29
|
+
function recordsInKstDay(records, project, range) {
|
|
30
|
+
return records.filter((r) => {
|
|
31
|
+
if (project && r.project !== project) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const t = Date.parse(String(r.at));
|
|
35
|
+
if (Number.isNaN(t)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return t >= range.startMs && t < range.endMs;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function summarizeRecord(r) {
|
|
42
|
+
const type = String(r.type ?? "");
|
|
43
|
+
if (type === "gate") {
|
|
44
|
+
return `gate${r.gate ?? "?"} ${r.decision ?? ""}`.trim();
|
|
45
|
+
}
|
|
46
|
+
const pick = r.what ?? r.lesson ?? r.scope ?? r.question ?? r.kind ?? r.condition ?? r.\uC870\uAC74 ?? "";
|
|
47
|
+
const s = String(pick).trim();
|
|
48
|
+
return s !== "" ? s : type;
|
|
49
|
+
}
|
|
50
|
+
var UI_EXT_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
|
|
51
|
+
var UI_DIR_RE = /(^|\/)(editor|components?|views?|pages?)(\/|$)/i;
|
|
52
|
+
function isUiFile(f) {
|
|
53
|
+
return UI_EXT_RE.test(f) || UI_DIR_RE.test(f);
|
|
54
|
+
}
|
|
55
|
+
function extractVerifyItems(records, criteria, changedFiles) {
|
|
56
|
+
const items = [];
|
|
57
|
+
const pushExplicit = (o, source) => {
|
|
58
|
+
const explicit = o.manualVerify === true || typeof o.verifyHow === "string";
|
|
59
|
+
if (!explicit) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const what = String(o.what ?? o.\uC870\uAC74 ?? o.condition ?? o.id ?? "(\uC218\uB3D9 \uAC80\uC99D \uD56D\uBAA9)");
|
|
63
|
+
const how = typeof o.verifyHow === "string" ? o.verifyHow : void 0;
|
|
64
|
+
items.push({ what, how, source });
|
|
65
|
+
};
|
|
66
|
+
for (const r of records) {
|
|
67
|
+
pushExplicit(r, "record");
|
|
68
|
+
}
|
|
69
|
+
for (const c of criteria) {
|
|
70
|
+
pushExplicit(c, "criterion");
|
|
71
|
+
}
|
|
72
|
+
for (const f of changedFiles) {
|
|
73
|
+
if (isUiFile(f)) {
|
|
74
|
+
items.push({ what: `UI \uBCC0\uACBD: ${f}`, source: "heuristic" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return items;
|
|
78
|
+
}
|
|
79
|
+
function buildBrief(input) {
|
|
80
|
+
return {
|
|
81
|
+
date: input.date,
|
|
82
|
+
project: input.project,
|
|
83
|
+
records: input.records.map((r) => ({
|
|
84
|
+
type: String(r.type ?? ""),
|
|
85
|
+
workitem: typeof r.workitem === "string" ? r.workitem : void 0,
|
|
86
|
+
at: String(r.at ?? ""),
|
|
87
|
+
summary: summarizeRecord(r)
|
|
88
|
+
})),
|
|
89
|
+
commits: input.commits,
|
|
90
|
+
criteria: input.criteria.map((c) => ({
|
|
91
|
+
id: String(c.id ?? ""),
|
|
92
|
+
status: String(c.status ?? "")
|
|
93
|
+
})),
|
|
94
|
+
verifyItems: extractVerifyItems(input.records, input.criteria, input.changedFiles)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
var GIT_COMMIT_MARK = "@@C@@";
|
|
98
|
+
function parseGitLog(stdout, range) {
|
|
99
|
+
const commits = [];
|
|
100
|
+
const files = /* @__PURE__ */ new Set();
|
|
101
|
+
let inRange = false;
|
|
102
|
+
for (const line of stdout.split("\n")) {
|
|
103
|
+
if (line.startsWith(GIT_COMMIT_MARK)) {
|
|
104
|
+
const [hash, ctStr, subject] = line.slice(GIT_COMMIT_MARK.length).split("\0");
|
|
105
|
+
const ctMs = Number(ctStr) * 1e3;
|
|
106
|
+
inRange = Number.isFinite(ctMs) && ctMs >= range.startMs && ctMs < range.endMs;
|
|
107
|
+
if (inRange) {
|
|
108
|
+
commits.push({ hash: (hash ?? "").slice(0, 9), subject: subject ?? "" });
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const f = line.trim();
|
|
113
|
+
if (f !== "" && inRange) {
|
|
114
|
+
files.add(f);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { commits, changedFiles: [...files] };
|
|
118
|
+
}
|
|
119
|
+
async function gitLogInRange(root, range) {
|
|
120
|
+
try {
|
|
121
|
+
const r = await run({
|
|
122
|
+
cmd: "git",
|
|
123
|
+
args: [
|
|
124
|
+
"log",
|
|
125
|
+
`--since=${new Date(range.startMs).toISOString()}`,
|
|
126
|
+
`--until=${new Date(range.endMs).toISOString()}`,
|
|
127
|
+
"--name-only",
|
|
128
|
+
`--format=${GIT_COMMIT_MARK}%H%x00%ct%x00%s`
|
|
129
|
+
],
|
|
130
|
+
cwd: root,
|
|
131
|
+
timeoutMs: 1e4
|
|
132
|
+
});
|
|
133
|
+
if (r.exitCode !== 0) {
|
|
134
|
+
return { commits: [], changedFiles: [] };
|
|
135
|
+
}
|
|
136
|
+
return parseGitLog(r.stdout, range);
|
|
137
|
+
} catch {
|
|
138
|
+
return { commits: [], changedFiles: [] };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function runBrief(opts) {
|
|
142
|
+
const root = resolveProjectRoot();
|
|
143
|
+
if (!root) {
|
|
144
|
+
process.stderr.write("\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(.git/.awl \uC5C6\uC74C).\n");
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const project = loadProjectName(root);
|
|
149
|
+
const date = typeof opts.date === "string" && opts.date !== "" ? opts.date : kstDateOf(Date.now());
|
|
150
|
+
const range = kstDayRange(date);
|
|
151
|
+
if (Number.isNaN(range.startMs)) {
|
|
152
|
+
process.stderr.write(`\uB0A0\uC9DC \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4: ${date} (YYYY-MM-DD \uD544\uC694)
|
|
153
|
+
`);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const dayRecords = recordsInKstDay(readRecords(), project, range);
|
|
158
|
+
const { commits, changedFiles } = await gitLogInRange(root, range);
|
|
159
|
+
const state = loadState(root);
|
|
160
|
+
const criteria = Array.isArray(state.criteria) ? state.criteria : [];
|
|
161
|
+
const brief = buildBrief({ date, project, records: dayRecords, commits, criteria, changedFiles });
|
|
162
|
+
if (opts.json === true) {
|
|
163
|
+
process.stdout.write(`${JSON.stringify(brief, null, 2)}
|
|
164
|
+
`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write(
|
|
168
|
+
`${date} (KST) \u2014 records ${brief.records.length} \xB7 commits ${brief.commits.length} \xB7 criteria ${brief.criteria.length} \xB7 verify ${brief.verifyItems.length}
|
|
169
|
+
`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
buildBrief,
|
|
174
|
+
extractVerifyItems,
|
|
175
|
+
kstDateOf,
|
|
176
|
+
kstDayRange,
|
|
177
|
+
parseGitLog,
|
|
178
|
+
recordsInKstDay,
|
|
179
|
+
runBrief,
|
|
180
|
+
summarizeRecord
|
|
181
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readRecords
|
|
3
|
+
} from "./chunk-I77CXOEX.js";
|
|
4
|
+
import {
|
|
5
|
+
loadState
|
|
6
|
+
} from "./chunk-4OCSYHYB.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveProjectRoot
|
|
9
|
+
} from "./chunk-UOPWVM2H.js";
|
|
10
|
+
import "./chunk-BUWGQVHT.js";
|
|
11
|
+
import {
|
|
12
|
+
caps,
|
|
13
|
+
signal
|
|
14
|
+
} from "./chunk-7SYRDDTX.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/changelog.ts
|
|
17
|
+
function runChangelogDraft(opts) {
|
|
18
|
+
const root = resolveProjectRoot();
|
|
19
|
+
const current = root ? loadState(root) : {};
|
|
20
|
+
const workitem = opts.workitem ?? (typeof current.workitem === "string" ? current.workitem : void 0);
|
|
21
|
+
if (!workitem) {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
`
|
|
24
|
+
${signal(caps(), "error")} \uC6CC\uD06C\uC544\uC774\uD15C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --workitem <id> \uB97C \uC9C0\uC815\uD558\uC138\uC694.
|
|
25
|
+
`
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const records = readRecords({ workitem });
|
|
30
|
+
const details = (record) => record.data && typeof record.data === "object" ? record.data : {};
|
|
31
|
+
const gate2 = records.some((r) => {
|
|
32
|
+
const data = details(r);
|
|
33
|
+
return r.type === "gate" && data.gate === 2 && data.decision === "approved";
|
|
34
|
+
});
|
|
35
|
+
if (!gate2) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`
|
|
38
|
+
${signal(caps(), "warn")} Gate 2 \uC2B9\uC778 \uB4A4\uC5D0\uB9CC CHANGELOG \uCD08\uC548\uC744 \uB9CC\uB4ED\uB2C8\uB2E4.
|
|
39
|
+
`
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const entries = records.filter((r) => ["attempt", "blocked", "gotcha", "decision"].includes(String(r.type))).map((r) => {
|
|
44
|
+
const data = details(r);
|
|
45
|
+
return String(data.what ?? data.lesson ?? data.decision ?? r.type);
|
|
46
|
+
}).filter((text) => text && text !== "undefined");
|
|
47
|
+
const draft = `## [Unreleased]
|
|
48
|
+
|
|
49
|
+
### \uBCC0\uACBD
|
|
50
|
+
|
|
51
|
+
${entries.length ? entries.map((entry) => `- ${entry}`).join("\n") : `- ${workitem} \uC791\uC5C5 \uC644\uB8CC`}
|
|
52
|
+
`;
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
opts.json ? `${JSON.stringify({ workitem, draft }, null, 2)}
|
|
55
|
+
` : `
|
|
56
|
+
${draft}
|
|
57
|
+
`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
runChangelogDraft
|
|
62
|
+
};
|