huddle60 0.1.0 → 0.1.1
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 +50 -67
- package/dist/action.js +87 -49
- package/dist/action.js.map +2 -2
- package/dist/cli.js +220 -80
- package/dist/cli.js.map +4 -4
- package/package.json +6 -6
- package/templates/.github/ISSUE_TEMPLATE/request.yml +4 -4
- package/templates/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- package/templates/.github/{ai-dlc → huddle60}/policy.yml +2 -2
- package/templates/.github/workflows/{ai-dlc.yml → huddle60.yml} +5 -6
- package/templates/AGENTS.md +2 -2
- package/ui/app.js +1 -1
- package/dist/planner-server.js +0 -1912
- package/dist/planner-server.js.map +0 -7
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ bằng hash → ai sửa yêu cầu sau khi chốt, PR tự đỏ.**
|
|
|
22
22
|
Bỏ buổi grooming 60 phút. Thay bằng một **huddle 60 giây**.
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
25
|
-
<img src="docs/assets/
|
|
25
|
+
<img src="docs/assets/huddle60-flow.svg" width="100%"
|
|
26
26
|
alt="PO gõ một câu, AI đề xuất cây Epic/Story/Task, PO xác nhận, DEV làm trong IDE theo baseline, PR được kiểm tra tự động, PO nghiệm thu">
|
|
27
27
|
</p>
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ Bỏ buổi grooming 60 phút. Thay bằng một **huddle 60 giây**.
|
|
|
30
30
|
|
|
31
31
|
| Giây | Ai | Việc |
|
|
32
32
|
|---|---|---|
|
|
33
|
-
| 0–10 | **PO** | Gõ _“Lập trình login sử dụng
|
|
33
|
+
| 0–10 | **PO** | Gõ _“Lập trình login sử dụng Google OAuth”_ |
|
|
34
34
|
| 10–30 | **AI** | Soạn Epic → Story → Task, AC dạng Given/When/Then, câu hỏi chặn và giả định |
|
|
35
35
|
| 30–45 | **PO** | Trả lời một câu hỏi, bấm **create issues**, rồi **approve** Story |
|
|
36
36
|
| 45–60 | **DEV** | Chọn lane, bấm **ready**, bấm **context** → dán vào Copilot/Claude/Gemini trong IDE |
|
|
@@ -40,21 +40,26 @@ Từ đó trở đi, mọi PR đều phải khớp đúng phiên bản yêu cầ
|
|
|
40
40
|
### Vì sao developer sẽ star
|
|
41
41
|
|
|
42
42
|
- **`package-lock.json` cho yêu cầu.** Mỗi Story được chốt thành baseline có SHA-256 (RFC 8785). Issue bị sửa →
|
|
43
|
-
hash lệch → check `
|
|
43
|
+
hash lệch → check `huddle60/baseline` chuyển đỏ. Giống hệt lockfile, nhưng cho nghiệp vụ.
|
|
44
44
|
- **Zero infra.** Không server, không database, không SaaS. Một lệnh `npx` trên máy bạn + GitHub Actions.
|
|
45
45
|
- **Agent-native.** `huddle60 context --issue 15` xuất đúng baseline đã duyệt cho bất kỳ coding agent nào, kèm
|
|
46
46
|
khối dán vào PR.
|
|
47
47
|
- **AI đề xuất, người quyết định.** Gemini không có quyền GitHub. App không có quyền hơn bạn. Mọi quyết định
|
|
48
48
|
được Actions kiểm tra theo user ID và ghi vào branch bằng chứng.
|
|
49
|
-
- **GitHub là nguồn sự thật.** Issues, Projects, PR
|
|
49
|
+
- **GitHub là nguồn sự thật.** Tận dụng Issues, Projects, PR quen thuộc của repository.
|
|
50
50
|
|
|
51
|
-
###
|
|
51
|
+
### Định vị: huddle60 & AI-DLC
|
|
52
52
|
|
|
53
|
-
huddle60 là
|
|
54
|
-
phạm vi được giao, con người định hướng và
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
**huddle60** là bộ công cụ và governance engine mã nguồn mở thực thi quy trình **AI-DLC (AI Development Life Cycle)** trực tiếp trên GitHub:
|
|
54
|
+
- **AI-DLC** là phương pháp luận và chuẩn quy trình: AI tham gia phát triển phần mềm trong phạm vi được giao, con người định hướng và phê duyệt, mọi bước đều có bằng chứng số (evidence hash-locked).
|
|
55
|
+
- **huddle60** là công cụ hiện thực hóa quy trình này một cách tự nhiên nhất vào GitHub:
|
|
56
|
+
- Lệnh tương tác: `/huddle60` (hỗ trợ tương thích ngược với `/ai-dlc`)
|
|
57
|
+
- Nhãn phân loại: `huddle60:*` (như `huddle60:request`, `huddle60:plan-draft`...)
|
|
58
|
+
- PR verification gate: status check `huddle60/baseline`
|
|
59
|
+
- Bằng chứng kiểm toán: nhánh `huddle60-evidence`
|
|
60
|
+
- Cấu hình chính sách: `.github/huddle60/policy.yml`
|
|
61
|
+
|
|
62
|
+
Đặc tả đầy đủ: [tài liệu kỹ thuật](docs/technical-implementation.md).
|
|
58
63
|
|
|
59
64
|
> Thấy hữu ích? Hãy **star** repo và thử trên một repo thử nghiệm. Dự án đang alpha và cần người chạy thật nhất.
|
|
60
65
|
|
|
@@ -62,7 +67,7 @@ Giao thức AI-DLC là phần bạn thấy trên GitHub: lệnh `/ai-dlc`, label
|
|
|
62
67
|
|
|
63
68
|
| | Hôm nay | Với huddle60 |
|
|
64
69
|
|---|---|---|
|
|
65
|
-
| **PO** | Ngồi viết từng ticket, AC, chia việc | Gõ _“Lập trình login sử dụng
|
|
70
|
+
| **PO** | Ngồi viết từng ticket, AC, chia việc | Gõ _“Lập trình login sử dụng Google OAuth”_ → AI soạn cả cây công việc kèm AC. PO chỉ trả lời câu hỏi và xác nhận |
|
|
66
71
|
| **PO** | Không chắc DEV/AI đang làm theo phiên bản yêu cầu nào | Mỗi PR gắn baseline có hash; sửa yêu cầu sau khi chốt thì PR **tự bị chặn** |
|
|
67
72
|
| **DEV** | Ticket mơ hồ, hỏi đi hỏi lại | Nhận Story có AC rõ ràng; câu hỏi chặn đã được PO trả lời **trước** khi Issue được tạo |
|
|
68
73
|
| **DEV** | Copy yêu cầu vào agent bằng tay, dễ sót | `huddle60 context --issue 15` đưa đúng baseline vào agent trong IDE của bạn |
|
|
@@ -73,17 +78,17 @@ Giao thức AI-DLC là phần bạn thấy trên GitHub: lệnh `/ai-dlc`, label
|
|
|
73
78
|
Chạy `huddle60 ui`, gõ yêu cầu vào tab **new plan**, bấm **generate plan** (rút gọn):
|
|
74
79
|
|
|
75
80
|
> ## Kế hoạch đề xuất (revision 1)
|
|
76
|
-
> Đăng nhập bằng
|
|
81
|
+
> Đăng nhập bằng Google OAuth cho ứng dụng web.
|
|
77
82
|
>
|
|
78
|
-
> - **Epic** `epic-auth`: Xác thực người dùng bằng
|
|
79
|
-
> - **Story** `story-login`: Đăng nhập bằng
|
|
80
|
-
> - AC-01: Given tài khoản hợp lệ When đăng nhập Then vào trang chính
|
|
81
|
-
> - AC-02: Given
|
|
82
|
-
> - **Task** `task-
|
|
83
|
-
> - **Task** `task-login-ui`: Giao diện đăng nhập _(blocked by task-
|
|
83
|
+
> - **Epic** `epic-auth`: Xác thực người dùng bằng Google OAuth
|
|
84
|
+
> - **Story** `story-login`: Đăng nhập bằng tài khoản Google
|
|
85
|
+
> - AC-01: Given tài khoản Google hợp lệ When đăng nhập Then vào trang chính
|
|
86
|
+
> - AC-02: Given người dùng hủy cấp quyền When đăng nhập Then thấy thông báo lỗi phù hợp
|
|
87
|
+
> - **Task** `task-oauth-setup`: Cấu hình Google OAuth Client
|
|
88
|
+
> - **Task** `task-login-ui`: Giao diện đăng nhập _(blocked by task-oauth-setup)_
|
|
84
89
|
> - **Story** `story-logout`: Đăng xuất
|
|
85
90
|
>
|
|
86
|
-
> **Câu hỏi khác:** Có cần
|
|
91
|
+
> **Câu hỏi khác:** Có cần lưu session vào cookie không?
|
|
87
92
|
|
|
88
93
|
PO bấm **create 5 issues** → Issues được tạo, liên kết cha–con và nằm sẵn trên Project board. Chưa đồng ý → gõ một dòng
|
|
89
94
|
vào ô **refine**, AI soạn lại. Có câu hỏi chặn thì nút tạo bị khóa đến khi PO trả lời.
|
|
@@ -107,7 +112,7 @@ App mở trình duyệt tại `http://127.0.0.1:4317/#token=…`:
|
|
|
107
112
|
- **new plan**: mô tả nhu cầu → cây công việc, câu hỏi chặn, giả định → refine → tạo Issues.
|
|
108
113
|
- **activity**: lệnh đã gửi và phản hồi của GitHub Actions.
|
|
109
114
|
|
|
110
|
-
Mỗi nút chỉ đăng một comment `/
|
|
115
|
+
Mỗi nút chỉ đăng một comment `/huddle60 …` **bằng tài khoản của bạn**; GitHub Actions kiểm tra role, hash, phụ thuộc rồi
|
|
111
116
|
ghi bằng chứng. App không có quyền gì hơn bạn và không tự quyết thay ai.
|
|
112
117
|
|
|
113
118
|
Bảo mật app: chỉ nghe `127.0.0.1`, token phiên ngẫu nhiên mỗi lần chạy, kiểm tra `Host`/`Origin` (chống DNS rebinding,
|
|
@@ -120,31 +125,30 @@ flowchart LR
|
|
|
120
125
|
PO[PO] -->|huddle60 ui| L[Local app<br/>127.0.0.1]
|
|
121
126
|
DEV[DEV] -->|huddle60 ui / context| L
|
|
122
127
|
L <-->|structured JSON| G[Gemini API]
|
|
123
|
-
L -->|Issues + comment /
|
|
128
|
+
L -->|Issues + comment /huddle60<br/>bằng token của user| GH[GitHub Issues/Projects]
|
|
124
129
|
GH -->|issue_comment| A[GitHub Action<br/>baseline/approve/ready/accept]
|
|
125
130
|
A -->|evidence + phản hồi| GH
|
|
126
131
|
DEV --> PR[Pull Request]
|
|
127
|
-
A -->|status
|
|
132
|
+
A -->|status huddle60/baseline| PR
|
|
128
133
|
```
|
|
129
134
|
|
|
130
135
|
| Thành phần | Chạy ở đâu | Làm gì |
|
|
131
136
|
|---|---|---|
|
|
132
137
|
| **Local app** (`src/app`, `ui/`) | Máy PO/DEV (`huddle60 ui`) | Soạn kế hoạch với Gemini, tạo cây Issues, nút approve/ready/accept, context cho IDE agent |
|
|
133
|
-
| **Workflow gates** (`src/workflow`) | GitHub Actions (`action.yml`) | Baseline có hash, quyết định PO/DEV, check `
|
|
138
|
+
| **Workflow gates** (`src/workflow`) | GitHub Actions (`action.yml`) | Baseline có hash, quyết định PO/DEV, check `huddle60/baseline` cho PR, nghiệm thu build |
|
|
134
139
|
| **CLI** (`src/entry/cli.ts`) | Máy DEV | `context` cho IDE agent, `init`, kiểm tra policy, thử planner |
|
|
135
140
|
| **Lõi** (`src/core`) | Dùng chung | Hash canonical (RFC 8785), parser, schema kế hoạch, kiểm tra quyết định — không phụ thuộc mạng |
|
|
136
|
-
| _Planner service_ (`src/planner`, `src/server`) | _Tùy chọn_: Cloud Run + Cloud Tasks + Firestore | Cho team muốn PO làm hoàn toàn trên GitHub bằng lệnh `/ai-dlc refine`, `create-plan` |
|
|
137
141
|
|
|
138
142
|
Nguyên tắc: **AI đề xuất, con người quyết định.** Gemini không có quyền gọi GitHub; mọi quyết định được GitHub Actions
|
|
139
|
-
kiểm tra theo GitHub user ID trong `.github/
|
|
140
|
-
[
|
|
143
|
+
kiểm tra theo GitHub user ID trong `.github/huddle60/policy.yml`. Thiết kế đầy đủ:
|
|
144
|
+
[tài liệu kỹ thuật](docs/technical-implementation.md).
|
|
141
145
|
|
|
142
146
|
## Luồng sử dụng
|
|
143
147
|
|
|
144
|
-
1. **PO** mở `huddle60 ui` → **new plan**, gõ: _“
|
|
148
|
+
1. **PO** mở `huddle60 ui` → **new plan**, gõ: _“Xây dựng tính năng đăng nhập Google OAuth”_ → trả lời câu hỏi chặn → **create issues**.
|
|
145
149
|
2. PO và DEV chỉnh nội dung Story trên GitHub nếu cần. Khi Story đã rõ: PO bấm **approve** (chốt đúng nội dung hiện tại).
|
|
146
150
|
3. DEV chọn lane và bấm **ready**. Task kế thừa approve của Story cha.
|
|
147
|
-
4. DEV bấm **context** (hoặc chạy CLI) để đưa baseline vào IDE agent, rồi mở PR kèm khối
|
|
151
|
+
4. DEV bấm **context** (hoặc chạy CLI) để đưa baseline vào IDE agent, rồi mở PR kèm khối Huddle60 trong context:
|
|
148
152
|
```bash
|
|
149
153
|
npx huddle60 context --issue 15
|
|
150
154
|
```
|
|
@@ -152,21 +156,17 @@ kiểm tra theo GitHub user ID trong `.github/ai-dlc/policy.yml`. Thiết kế
|
|
|
152
156
|
|
|
153
157
|
Không dùng app vẫn được: mọi nút tương ứng một lệnh comment bên dưới.
|
|
154
158
|
|
|
155
|
-
### Các lệnh
|
|
159
|
+
### Các lệnh qua GitHub Comment (xử lý bởi GitHub Action)
|
|
156
160
|
|
|
157
|
-
| Lệnh |
|
|
158
|
-
|
|
159
|
-
| `
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
| `ready baseline=<id> hash=<h> lane=<fast\|standard\|strict>` | DEV | Action | Xác nhận Story/Task sẵn sàng, trả khối cho PR |
|
|
165
|
-
| `accept baseline=<id> run=<run_id> commit=<sha>` | PO | Action | Nghiệm thu Story/Epic trên build `main`, đóng Issue |
|
|
166
|
-
| `revoke decision=<id> reason="..."` | Tác giả hoặc cùng role | Action | Thu hồi quyết định |
|
|
161
|
+
| Lệnh | Vai trò | Tác dụng |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `/huddle60 baseline` | PO, DEV | Chụp snapshot yêu cầu của Issue, tính hash SHA-256 |
|
|
164
|
+
| `/huddle60 approve baseline=<id> hash=<h>` | PO | Duyệt nghiệp vụ của Story (Task kế thừa duyệt của Story cha) |
|
|
165
|
+
| `/huddle60 ready baseline=<id> hash=<h> lane=<fast\|standard\|strict>` | DEV | Xác nhận sẵn sàng code, sinh khối trace cho PR |
|
|
166
|
+
| `/huddle60 accept baseline=<id> run=<run_id> commit=<sha>` | PO | Nghiệm thu Story/Epic trên build thành công của branch `main`, đóng Issue |
|
|
167
|
+
| `/huddle60 revoke decision=<id> reason="..."` | Tác giả hoặc cùng role | Thu hồi quyết định đã ban hành |
|
|
167
168
|
|
|
168
|
-
Lệnh phải nằm ở **dòng đầu** của comment. Mọi quyết định được kiểm tra
|
|
169
|
-
mất role, sửa Issue sau baseline hoặc thu hồi đều làm quyết định mất hiệu lực.
|
|
169
|
+
*Lưu ý: Lệnh phải nằm ở **dòng đầu tiên** của comment (tiền tố `/huddle60`). Mọi quyết định đều được kiểm tra tính hợp lệ và ghi vào nhánh bằng chứng `huddle60-evidence`.*
|
|
170
170
|
|
|
171
171
|
## Phát triển
|
|
172
172
|
|
|
@@ -175,7 +175,7 @@ Yêu cầu: Node.js 22.
|
|
|
175
175
|
```bash
|
|
176
176
|
npm ci
|
|
177
177
|
npm run check # typecheck + test + format
|
|
178
|
-
npm run build # dist/
|
|
178
|
+
npm run build # dist/action.js, dist/cli.js
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
Chạy app với dữ liệu thật từ mã nguồn: `npm run build && node dist/cli.js ui --repo owner/name`.
|
|
@@ -183,58 +183,41 @@ Chạy app với dữ liệu thật từ mã nguồn: `npm run build && node dis
|
|
|
183
183
|
Thử planner với Gemini thật mà không cần GitHub (tốn quota của bạn):
|
|
184
184
|
|
|
185
185
|
```bash
|
|
186
|
-
GEMINI_API_KEY=... node dist/cli.js plan-preview "
|
|
186
|
+
GEMINI_API_KEY=... node dist/cli.js plan-preview "Xây dựng tính năng đăng nhập Google OAuth"
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
Chạy planner service tùy chọn cục bộ (store và queue trong bộ nhớ): sao chép `.env.example` thành `.env`, điền giá trị,
|
|
190
|
-
rồi `node --env-file=.env dist/planner-server.js`. Dùng công cụ như `smee.io` để chuyển webhook từ GitHub App về máy.
|
|
191
|
-
|
|
192
189
|
### Cấu trúc
|
|
193
190
|
|
|
194
191
|
```text
|
|
195
192
|
src/core/ Lõi thuần: canonical hash, policy, command, issue form, plan, baseline, decision, render
|
|
196
193
|
src/app/ Local app: LocalApp (plan, items, decisions), HTTP server bảo vệ bằng token/Host/Origin
|
|
197
194
|
ui/ Giao diện tĩnh (HTML/CSS/JS thuần, không build step)
|
|
198
|
-
src/planner/ Prompt, generatePlan, IssueTreeCreator
|
|
195
|
+
src/planner/ Prompt, generatePlan, IssueTreeCreator
|
|
199
196
|
src/workflow/ WorkflowService (baseline, approve, ready, accept, revoke, PR gate), agent context
|
|
200
|
-
src/adapters/ GitHub (Octokit), evidence branch, Gemini,
|
|
201
|
-
src/
|
|
202
|
-
src/entry/ planner-server, action, cli
|
|
197
|
+
src/adapters/ GitHub (Octokit), evidence branch, Gemini, in-memory
|
|
198
|
+
src/entry/ action, cli
|
|
203
199
|
templates/ Tệp cho repository áp dụng: Issue forms, policy, workflow, PR template, AGENTS.md
|
|
204
|
-
infra/cloud-run/ Script triển khai planner
|
|
205
200
|
tests/ Unit + kịch bản đầu-cuối với FakeGitHub/FakeModel
|
|
206
201
|
```
|
|
207
202
|
|
|
208
203
|
## Cài đặt vào một repository
|
|
209
204
|
|
|
210
|
-
> Thử trên **repository kiểm thử** trước. Cách mặc định (local app + Actions) không cần Google Cloud.
|
|
205
|
+
> Thử trên **repository kiểm thử** trước. Cách mặc định (local app + Actions) không cần Google Cloud hay bất kỳ server nào.
|
|
211
206
|
|
|
212
207
|
1. **GitHub App** để Actions ghi bằng chứng (Organization settings → Developer settings → GitHub Apps):
|
|
213
208
|
- Repository permissions: Contents **RW**, Issues **RW**, Commit statuses **RW**, Pull requests **R**, Actions **R**, Metadata **R**.
|
|
214
209
|
- Organization permissions: Projects **RW** (bỏ qua nếu không dùng Project).
|
|
215
210
|
- **Không cần webhook** (bỏ chọn _Active_). Tải private key; cài App vào repository.
|
|
216
|
-
2. **Repository áp dụng**: sao chép nội dung `templates/` vào repository
|
|
217
|
-
`gh api users/<login> --jq .id`), thay `your-org/huddle60@v0` bằng commit SHA của repository này.
|
|
211
|
+
2. **Repository áp dụng**: sao chép nội dung `templates/` vào repository (hoặc chạy `npx huddle60 init`).
|
|
218
212
|
3. Tạo repository variable `AI_DLC_APP_ID` và secret `AI_DLC_APP_PRIVATE_KEY`.
|
|
219
213
|
4. Tạo labels và evidence branch: `npx huddle60 init --repo owner/name`, rồi tạo ruleset chặn
|
|
220
|
-
force-push/xóa cho branch `
|
|
221
|
-
5. Ruleset cho `main`: bắt buộc PR, review, CODEOWNERS cho `.github/**`, required status check `
|
|
214
|
+
force-push/xóa cho branch `huddle60-evidence` và chỉ cho App ghi.
|
|
215
|
+
5. Ruleset cho `main`: bắt buộc PR, review, CODEOWNERS cho `.github/**`, required status check `huddle60/baseline`.
|
|
222
216
|
6. Trong Project: **tắt** workflow tự chuyển Done khi Issue đóng hoặc PR merge.
|
|
223
217
|
7. Mỗi PO/DEV: `gh auth login`, rồi `npx huddle60 ui`.
|
|
224
218
|
|
|
225
|
-
**Tùy chọn — planner service trên Cloud Run** (PO làm hoàn toàn trên GitHub bằng lệnh, không cần app local): bật
|
|
226
|
-
webhook của App cho sự kiện **Issues** và **Issue comment**, rồi
|
|
227
|
-
```bash
|
|
228
|
-
GCP_PROJECT=my-project GITHUB_APP_ID=123456 \
|
|
229
|
-
GITHUB_APP_PRIVATE_KEY_FILE=./app.pem GEMINI_API_KEY_FILE=./gemini.key \
|
|
230
|
-
./infra/cloud-run/deploy.sh
|
|
231
|
-
```
|
|
232
|
-
Đặt Webhook URL và secret mà script in ra vào GitHub App.
|
|
233
|
-
|
|
234
219
|
## Giới hạn đã biết
|
|
235
220
|
|
|
236
|
-
- **Chưa kiểm chứng đầu-cuối** với GitHub, Gemini, Firestore và Cloud Tasks thật. Các adapter được typecheck theo SDK
|
|
237
|
-
nhưng chỉ logic dùng fake mới có test. Đây là việc cần cộng đồng nhất.
|
|
238
221
|
- Local app: bản nháp kế hoạch chỉ nằm trong bộ nhớ của tiến trình; tắt app trước khi tạo Issues thì phải soạn lại.
|
|
239
222
|
Chỉ hỗ trợ github.com (chưa GHES). Mỗi PO cần Gemini key riêng hoặc key do tổ chức cấp.
|
|
240
223
|
- API **sub-issues** và **Projects v2** phụ thuộc gói/quyền GitHub; khi không khả dụng, Issue vẫn được tạo và tham chiếu
|
package/dist/action.js
CHANGED
|
@@ -69,8 +69,18 @@ import { retry } from "@octokit/plugin-retry";
|
|
|
69
69
|
import { Octokit } from "@octokit/rest";
|
|
70
70
|
var RetryingOctokit = Octokit.plugin(retry);
|
|
71
71
|
var USER_AGENT = "huddle60/0.1.0";
|
|
72
|
+
var defaultLog = process.env.AI_DLC_DEBUG ? void 0 : {
|
|
73
|
+
debug: () => {
|
|
74
|
+
},
|
|
75
|
+
info: () => {
|
|
76
|
+
},
|
|
77
|
+
warn: () => {
|
|
78
|
+
},
|
|
79
|
+
error: () => {
|
|
80
|
+
}
|
|
81
|
+
};
|
|
72
82
|
function octokitFromToken(token) {
|
|
73
|
-
return new RetryingOctokit({ auth: token, userAgent: USER_AGENT });
|
|
83
|
+
return new RetryingOctokit({ auth: token, userAgent: USER_AGENT, log: defaultLog });
|
|
74
84
|
}
|
|
75
85
|
function isStatus(error, ...statuses) {
|
|
76
86
|
return typeof error === "object" && error !== null && statuses.includes(error.status ?? -1);
|
|
@@ -426,7 +436,7 @@ var GitBranchEvidenceStore = class {
|
|
|
426
436
|
});
|
|
427
437
|
const commit = await this.octokit.rest.git.createCommit({
|
|
428
438
|
...this.base,
|
|
429
|
-
message: `
|
|
439
|
+
message: `huddle60: add ${path}`,
|
|
430
440
|
tree: tree.data.sha,
|
|
431
441
|
parents: [head.sha]
|
|
432
442
|
});
|
|
@@ -489,7 +499,7 @@ var GitBranchEvidenceStore = class {
|
|
|
489
499
|
});
|
|
490
500
|
const commit = await this.octokit.rest.git.createCommit({
|
|
491
501
|
...this.base,
|
|
492
|
-
message: "
|
|
502
|
+
message: "huddle60: initialize evidence branch",
|
|
493
503
|
tree: tree.data.sha,
|
|
494
504
|
parents: []
|
|
495
505
|
});
|
|
@@ -549,7 +559,7 @@ var PolicySchema = z.object({
|
|
|
549
559
|
strict: { requires_po_approval: true }
|
|
550
560
|
}),
|
|
551
561
|
sensitive_paths: z.array(z.string().min(1)).default([]),
|
|
552
|
-
evidence: z.object({ branch: z.string().min(1).default("
|
|
562
|
+
evidence: z.object({ branch: z.string().min(1).default("huddle60-evidence") }).default({ branch: "huddle60-evidence" }),
|
|
553
563
|
acceptance: z.object({ build_workflows: z.array(z.string().min(1)).min(1) }).default({ build_workflows: [".github/workflows/build.yml"] }),
|
|
554
564
|
planner: z.object({
|
|
555
565
|
max_nodes: z.number().int().min(1).max(200).default(60),
|
|
@@ -563,7 +573,8 @@ var PolicySchema = z.object({
|
|
|
563
573
|
ctx.addIssue({ code: "custom", message: `Bot account ${bot.login} cannot hold a role` });
|
|
564
574
|
}
|
|
565
575
|
});
|
|
566
|
-
var POLICY_PATH = ".github/
|
|
576
|
+
var POLICY_PATH = ".github/huddle60/policy.yml";
|
|
577
|
+
var LEGACY_POLICY_PATH = ".github/ai-dlc/policy.yml";
|
|
567
578
|
function parsePolicy(source) {
|
|
568
579
|
let raw;
|
|
569
580
|
try {
|
|
@@ -676,13 +687,13 @@ function parseIssueRefs(text) {
|
|
|
676
687
|
|
|
677
688
|
// src/core/vocabulary.ts
|
|
678
689
|
var LABELS = {
|
|
679
|
-
request: "
|
|
680
|
-
planDraft: "
|
|
681
|
-
planCreated: "
|
|
682
|
-
planPartial: "
|
|
683
|
-
fromPlan: "
|
|
684
|
-
stale: "
|
|
685
|
-
exempt: "
|
|
690
|
+
request: "huddle60:request",
|
|
691
|
+
planDraft: "huddle60:plan-draft",
|
|
692
|
+
planCreated: "huddle60:plan-created",
|
|
693
|
+
planPartial: "huddle60:plan-partial",
|
|
694
|
+
fromPlan: "huddle60:from-plan",
|
|
695
|
+
stale: "huddle60:stale",
|
|
696
|
+
exempt: "huddle60:exempt",
|
|
686
697
|
epic: "type:epic",
|
|
687
698
|
story: "type:story",
|
|
688
699
|
task: "type:task"
|
|
@@ -815,7 +826,8 @@ function baselinePath(repositoryId, issueNumber, baselineId) {
|
|
|
815
826
|
}
|
|
816
827
|
|
|
817
828
|
// src/core/command.ts
|
|
818
|
-
var COMMAND_PREFIX = "/
|
|
829
|
+
var COMMAND_PREFIX = "/huddle60";
|
|
830
|
+
var LEGACY_COMMAND_PREFIX = "/ai-dlc";
|
|
819
831
|
var COMMAND_SPECS = {
|
|
820
832
|
baseline: { required: [], optional: [], handler: "deterministic" },
|
|
821
833
|
approve: { required: ["baseline", "hash"], optional: [], handler: "deterministic" },
|
|
@@ -845,13 +857,18 @@ function parseCommand(body) {
|
|
|
845
857
|
const normalized = body.replace(/\r\n?/gu, "\n");
|
|
846
858
|
const [head = "", ...rest] = normalized.split("\n");
|
|
847
859
|
const line = head.trim();
|
|
848
|
-
|
|
860
|
+
let prefix = "";
|
|
861
|
+
if (line.startsWith(COMMAND_PREFIX)) {
|
|
862
|
+
prefix = COMMAND_PREFIX;
|
|
863
|
+
} else if (line.startsWith(LEGACY_COMMAND_PREFIX)) {
|
|
864
|
+
prefix = LEGACY_COMMAND_PREFIX;
|
|
865
|
+
} else {
|
|
849
866
|
throw new AiDlcError(
|
|
850
867
|
"E_SYNTAX",
|
|
851
868
|
`Commands must start with \`${COMMAND_PREFIX}\` on the first line.`
|
|
852
869
|
);
|
|
853
870
|
}
|
|
854
|
-
const remainder = line.slice(
|
|
871
|
+
const remainder = line.slice(prefix.length).trim();
|
|
855
872
|
const [rawName = "", ...argParts] = remainder.split(/\s+/u);
|
|
856
873
|
if (!(rawName in COMMAND_SPECS)) {
|
|
857
874
|
throw new AiDlcError(
|
|
@@ -917,45 +934,59 @@ function evaluateReceipt(receipt, facts) {
|
|
|
917
934
|
}
|
|
918
935
|
|
|
919
936
|
// src/core/pr-block.ts
|
|
920
|
-
var PR_BLOCK_BEGIN = "<!--
|
|
921
|
-
var PR_BLOCK_END = "<!--
|
|
937
|
+
var PR_BLOCK_BEGIN = "<!-- huddle60:begin -->";
|
|
938
|
+
var PR_BLOCK_END = "<!-- huddle60:end -->";
|
|
939
|
+
var LEGACY_PR_BLOCK_BEGIN = "<!-- ai-dlc:begin -->";
|
|
940
|
+
var LEGACY_PR_BLOCK_END = "<!-- ai-dlc:end -->";
|
|
922
941
|
function parsePrBlock(body) {
|
|
923
942
|
const text = (body ?? "").replace(/\r\n?/gu, "\n");
|
|
924
|
-
|
|
943
|
+
let begin = text.indexOf(PR_BLOCK_BEGIN);
|
|
944
|
+
let beginMarker = PR_BLOCK_BEGIN;
|
|
945
|
+
let endMarker = PR_BLOCK_END;
|
|
946
|
+
if (begin === -1) {
|
|
947
|
+
begin = text.indexOf(LEGACY_PR_BLOCK_BEGIN);
|
|
948
|
+
if (begin !== -1) {
|
|
949
|
+
beginMarker = LEGACY_PR_BLOCK_BEGIN;
|
|
950
|
+
endMarker = LEGACY_PR_BLOCK_END;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
925
953
|
if (begin === -1) return null;
|
|
926
|
-
if (text.indexOf(
|
|
927
|
-
throw new AiDlcError("E_PR_BLOCK", "PR body contains more than one
|
|
954
|
+
if (text.indexOf(beginMarker, begin + 1) !== -1) {
|
|
955
|
+
throw new AiDlcError("E_PR_BLOCK", "PR body contains more than one huddle60 block.");
|
|
928
956
|
}
|
|
929
|
-
const end = text.indexOf(
|
|
930
|
-
if (end === -1) throw new AiDlcError("E_PR_BLOCK", "
|
|
957
|
+
const end = text.indexOf(endMarker, begin);
|
|
958
|
+
if (end === -1) throw new AiDlcError("E_PR_BLOCK", "huddle60 block is not closed.");
|
|
931
959
|
const fields = /* @__PURE__ */ new Map();
|
|
932
|
-
for (const line of text.slice(begin +
|
|
933
|
-
const match = /^AI-DLC-([A-Za-z]+):\s*(.*)$/
|
|
960
|
+
for (const line of text.slice(begin + beginMarker.length, end).split("\n")) {
|
|
961
|
+
const match = /^(?:HUDDLE60|AI-DLC)-([A-Za-z]+):\s*(.*)$/iu.exec(line.trim());
|
|
934
962
|
if (match) fields.set(match[1].toLowerCase(), match[2].trim());
|
|
935
963
|
}
|
|
936
964
|
const issue = /^#(\d+)$/u.exec(fields.get("issue") ?? "");
|
|
937
965
|
const baselineId = fields.get("baseline") ?? "";
|
|
938
966
|
const hash = fields.get("hash") ?? "";
|
|
939
967
|
if (!issue)
|
|
940
|
-
throw new AiDlcError(
|
|
968
|
+
throw new AiDlcError(
|
|
969
|
+
"E_PR_BLOCK",
|
|
970
|
+
"HUDDLE60-Issue must be `#<number>` in the same repository."
|
|
971
|
+
);
|
|
941
972
|
if (!/^bl-\d+-[0-9a-f]{12}$/u.test(baselineId))
|
|
942
|
-
throw new AiDlcError("E_PR_BLOCK", "
|
|
973
|
+
throw new AiDlcError("E_PR_BLOCK", "HUDDLE60-Baseline is missing or invalid.");
|
|
943
974
|
if (!/^sha256:[0-9a-f]{64}$/u.test(hash))
|
|
944
|
-
throw new AiDlcError("E_PR_BLOCK", "
|
|
975
|
+
throw new AiDlcError("E_PR_BLOCK", "HUDDLE60-Hash is missing or invalid.");
|
|
945
976
|
const acceptanceCriteria = (fields.get("ac") ?? "").split(/[\s,]+/u).filter((value) => value.length > 0);
|
|
946
977
|
for (const id of acceptanceCriteria) {
|
|
947
978
|
if (!/^AC-\d{2,3}$/u.test(id))
|
|
948
|
-
throw new AiDlcError("E_PR_BLOCK", `Invalid AC id \`${id}\` in
|
|
979
|
+
throw new AiDlcError("E_PR_BLOCK", `Invalid AC id \`${id}\` in HUDDLE60-AC.`);
|
|
949
980
|
}
|
|
950
981
|
return { issueNumber: Number(issue[1]), baselineId, hash, acceptanceCriteria };
|
|
951
982
|
}
|
|
952
983
|
function renderPrBlock(block) {
|
|
953
984
|
return [
|
|
954
985
|
PR_BLOCK_BEGIN,
|
|
955
|
-
`
|
|
956
|
-
`
|
|
957
|
-
`
|
|
958
|
-
`
|
|
986
|
+
`HUDDLE60-Issue: #${block.issueNumber}`,
|
|
987
|
+
`HUDDLE60-Baseline: ${block.baselineId}`,
|
|
988
|
+
`HUDDLE60-Hash: ${block.hash}`,
|
|
989
|
+
`HUDDLE60-AC: ${block.acceptanceCriteria.join(", ")}`,
|
|
959
990
|
PR_BLOCK_END
|
|
960
991
|
].join("\n");
|
|
961
992
|
}
|
|
@@ -995,14 +1026,14 @@ var ModelPlanSchema = z2.object({
|
|
|
995
1026
|
|
|
996
1027
|
// src/core/render.ts
|
|
997
1028
|
function sanitize(text) {
|
|
998
|
-
return text.replace(/<!--/gu, "<!--").replace(/</gu, "<").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/ai-dlc/gmu, "\\/
|
|
1029
|
+
return text.replace(/<!--/gu, "<!--").replace(/</gu, "<").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/(?:huddle60|ai-dlc)/gmu, "\\/huddle60");
|
|
999
1030
|
}
|
|
1000
1031
|
function marker(name, value) {
|
|
1001
|
-
return `<!--
|
|
1032
|
+
return `<!-- huddle60:${name}:${value} -->`;
|
|
1002
1033
|
}
|
|
1003
1034
|
|
|
1004
1035
|
// src/workflow/service.ts
|
|
1005
|
-
var STATUS_CONTEXT = "
|
|
1036
|
+
var STATUS_CONTEXT = "huddle60/baseline";
|
|
1006
1037
|
var LANES = ["fast", "standard", "strict"];
|
|
1007
1038
|
var WorkflowService = class {
|
|
1008
1039
|
constructor(deps) {
|
|
@@ -1016,7 +1047,10 @@ var WorkflowService = class {
|
|
|
1016
1047
|
async policy() {
|
|
1017
1048
|
if (this.policyCache) return this.policyCache;
|
|
1018
1049
|
const branch = await this.deps.github.getDefaultBranch();
|
|
1019
|
-
|
|
1050
|
+
let source = await this.deps.github.readFile(POLICY_PATH, branch);
|
|
1051
|
+
if (source === null) {
|
|
1052
|
+
source = await this.deps.github.readFile(LEGACY_POLICY_PATH, branch);
|
|
1053
|
+
}
|
|
1020
1054
|
if (source === null)
|
|
1021
1055
|
throw new AiDlcError("E_POLICY_INVALID", `Missing ${POLICY_PATH} on ${branch}.`);
|
|
1022
1056
|
this.policyCache = parsePolicy(source);
|
|
@@ -1106,11 +1140,11 @@ var WorkflowService = class {
|
|
|
1106
1140
|
"",
|
|
1107
1141
|
"PO duy\u1EC7t nghi\u1EC7p v\u1EE5:",
|
|
1108
1142
|
"```text",
|
|
1109
|
-
`/
|
|
1143
|
+
`/huddle60 approve baseline=${snapshot.baselineId} hash=${snapshot.contentHash}`,
|
|
1110
1144
|
"```",
|
|
1111
1145
|
"DEV x\xE1c nh\u1EADn s\u1EB5n s\xE0ng (lane: fast | standard | strict):",
|
|
1112
1146
|
"```text",
|
|
1113
|
-
`/
|
|
1147
|
+
`/huddle60 ready baseline=${snapshot.baselineId} hash=${snapshot.contentHash} lane=standard`,
|
|
1114
1148
|
"```"
|
|
1115
1149
|
].join("\n");
|
|
1116
1150
|
}
|
|
@@ -1156,7 +1190,7 @@ var WorkflowService = class {
|
|
|
1156
1190
|
if (this.snapshotOf(issue).contentHash !== snapshot.contentHash) {
|
|
1157
1191
|
throw new AiDlcError(
|
|
1158
1192
|
"E_STALE",
|
|
1159
|
-
"The Issue changed after this baseline was created. Run `/
|
|
1193
|
+
"The Issue changed after this baseline was created. Run `/huddle60 baseline` again and decide on the new baseline."
|
|
1160
1194
|
);
|
|
1161
1195
|
}
|
|
1162
1196
|
}
|
|
@@ -1381,7 +1415,7 @@ var WorkflowService = class {
|
|
|
1381
1415
|
};
|
|
1382
1416
|
}) : {
|
|
1383
1417
|
state: "pending",
|
|
1384
|
-
description: "
|
|
1418
|
+
description: "huddle60 is disabled by the operator",
|
|
1385
1419
|
details: []
|
|
1386
1420
|
};
|
|
1387
1421
|
await this.deps.github.setCommitStatus(pr.headSha, {
|
|
@@ -1412,12 +1446,12 @@ var WorkflowService = class {
|
|
|
1412
1446
|
if (!block)
|
|
1413
1447
|
throw new AiDlcError(
|
|
1414
1448
|
"E_PR_BLOCK",
|
|
1415
|
-
"PR body has no
|
|
1449
|
+
"PR body has no huddle60 block. Run the context CLI or use the ready reply."
|
|
1416
1450
|
);
|
|
1417
1451
|
const issue = await github.getIssue(block.issueNumber);
|
|
1418
1452
|
const snapshot = await this.loadSnapshot(issue.number, block.baselineId);
|
|
1419
1453
|
if (snapshot.contentHash !== block.hash)
|
|
1420
|
-
throw new AiDlcError("E_HASH_MISMATCH", "
|
|
1454
|
+
throw new AiDlcError("E_HASH_MISMATCH", "HUDDLE60-Hash does not match the baseline.");
|
|
1421
1455
|
if (snapshot.kind === "epic")
|
|
1422
1456
|
throw new AiDlcError("E_PR_BLOCK", "A PR must reference a story or task, not an epic.");
|
|
1423
1457
|
this.assertCurrent(issue, snapshot);
|
|
@@ -1498,7 +1532,7 @@ var WorkflowService = class {
|
|
|
1498
1532
|
}
|
|
1499
1533
|
};
|
|
1500
1534
|
function errorReply(error) {
|
|
1501
|
-
return [`**
|
|
1535
|
+
return [`**huddle60: \`${error.code}\`**`, "", sanitize(error.message)].join("\n");
|
|
1502
1536
|
}
|
|
1503
1537
|
function firstLine(text) {
|
|
1504
1538
|
return text.split("\n")[0] ?? "";
|
|
@@ -1512,17 +1546,21 @@ async function main() {
|
|
|
1512
1546
|
const eventName = requireEnv("GITHUB_EVENT_NAME");
|
|
1513
1547
|
const payload = JSON.parse(await readFile(requireEnv("GITHUB_EVENT_PATH"), "utf8"));
|
|
1514
1548
|
const [owner, repo] = requireEnv("GITHUB_REPOSITORY").split("/");
|
|
1515
|
-
const logger = createJsonLogger({ component: "
|
|
1516
|
-
const
|
|
1549
|
+
const logger = createJsonLogger({ component: "huddle60-action", event: eventName });
|
|
1550
|
+
const token = process.env.HUDDLE60_TOKEN || process.env.AI_DLC_TOKEN || requireEnv("HUDDLE60_TOKEN or AI_DLC_TOKEN");
|
|
1551
|
+
const octokit = octokitFromToken(token);
|
|
1517
1552
|
const github = await connectRepository(octokit, owner, repo);
|
|
1518
|
-
|
|
1519
|
-
|
|
1553
|
+
let policySource = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
|
|
1554
|
+
if (!policySource) {
|
|
1555
|
+
policySource = await github.readFile(LEGACY_POLICY_PATH, await github.getDefaultBranch());
|
|
1556
|
+
}
|
|
1557
|
+
const branch = policySource ? parsePolicy(policySource).evidence.branch : "huddle60-evidence";
|
|
1520
1558
|
const workflow = new WorkflowService({
|
|
1521
1559
|
github,
|
|
1522
1560
|
evidence: new GitBranchEvidenceStore(octokit, owner, repo, branch),
|
|
1523
1561
|
clock: systemClock,
|
|
1524
1562
|
logger,
|
|
1525
|
-
enabled: process.env.AI_DLC_ENABLED !== "false"
|
|
1563
|
+
enabled: process.env.HUDDLE60_ENABLED !== "false" && process.env.AI_DLC_ENABLED !== "false"
|
|
1526
1564
|
});
|
|
1527
1565
|
if (eventName === "issue_comment") {
|
|
1528
1566
|
if (!payload.issue || payload.issue.pull_request || !payload.comment || payload.sender?.type === "Bot")
|
|
@@ -1534,7 +1572,7 @@ async function main() {
|
|
|
1534
1572
|
if (eventName === "pull_request_target" && payload.pull_request) {
|
|
1535
1573
|
const result = await workflow.checkPullRequest(payload.pull_request.number);
|
|
1536
1574
|
await summary([
|
|
1537
|
-
`###
|
|
1575
|
+
`### huddle60/baseline: ${result.state}`,
|
|
1538
1576
|
"",
|
|
1539
1577
|
result.description,
|
|
1540
1578
|
...result.details.map((line) => `> ${line}`)
|