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 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/ai-dlc-flow.svg" width="100%"
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 Firebase”_ |
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 `ai-dlc/baseline` chuyển đỏ. Giống hệt lockfile, nhưng cho nghiệp vụ.
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 như team đang dùng. Không board thứ hai, không Jira.
49
+ - **GitHub là nguồn sự thật.** Tận dụng Issues, Projects, PR quen thuộc của repository.
50
50
 
51
- ### Built on AI-DLC
51
+ ### Định vị: huddle60 & AI-DLC
52
52
 
53
- huddle60 là bản hiện thực mở của **AI-DLC (AI-Driven Development Lifecycle)** trên GitHub: AI thực thi trong
54
- phạm vi được giao, con người định hướng và quyết định, mọi bước có bằng chứng.
55
- Giao thức AI-DLC là phần bạn thấy trên GitHub: lệnh `/ai-dlc`, label `ai-dlc:*`, check `ai-dlc/baseline`, branch
56
- `ai-dlc-evidence`. huddle60 là cách nhanh nhất để dùng giao thức đó. Đặc tả đầy đủ:
57
- [workflow AI-DLC](webapp_quanly.md) · [tài liệu kỹ thuật](docs/technical-implementation.md).
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 Firebase”_ → 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 |
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 Firebase Authentication cho ứng dụng web.
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 Firebase
79
- > - **Story** `story-login`: Đăng nhập bằng email và mật khẩu
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 sai mật khẩu When đăng nhập Then thấy thông báo lỗi chung
82
- > - **Task** `task-firebase-setup`: Cấu hình Firebase Authentication
83
- > - **Task** `task-login-ui`: Giao diện đăng nhập _(blocked by task-firebase-setup)_
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 đăng nhập bằng Google không?
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 `/ai-dlc …` **bằng tài khoản của bạn**; GitHub Actions kiểm tra role, hash, phụ thuộc rồi
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 /ai-dlc<br/>bằng token của user| GH[GitHub Issues/Projects]
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 ai-dlc/baseline| PR
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 `ai-dlc/baseline` cho PR, nghiệm thu build |
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/ai-dlc/policy.yml`. Thiết kế đầy đủ:
140
- [đặc tả workflow](webapp_quanly.md) · [tài liệu kỹ thuật](docs/technical-implementation.md).
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õ: _“Lập trình login sử dụng Firebase”_ → trả lời câu hỏi chặn → **create issues**.
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 AI-DLC trong context:
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 | Ai | Xử lý bởi | Tác dụng |
158
- |---|---|---|---|
159
- | `refine plan=<id>` + ghi chú | PO | Planner service | Tạo revision mới của kế hoạch |
160
- | `create-plan plan=<id> hash=<h>` | PO | Planner service | Tạo cây Issues; lặp lại an toàn nếu bị gián đoạn |
161
- | `resume-plan plan=<id>` | PO, maintainer | Planner service | Tiếp tục tạo Issues dở dang, không tạo trùng |
162
- | `baseline` | PO, DEV | Action | Chụp snapshot yêu cầu, trả id + hash (tùy chọn: approve/ready tự chụp) |
163
- | `approve baseline=<id> hash=<h>` | PO | Action | Duyệt nghiệp vụ của Story (Task kế thừa duyệt của Story cha) |
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 lại mỗi lần dùng: sửa/xóa comment,
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/planner-server.js, dist/action.js, dist/cli.js
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 "Lập trình login sử dụng Firebase"
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, PlannerService (dịch vụ tùy chọn)
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, Firestore, Cloud Tasks, in-memory
201
- src/server/ HTTP của planner service: xác minh HMAC webhook, chống trùng delivery
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; sửa `policy.yml` (numeric user id từ
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 `ai-dlc-evidence` và chỉ cho App ghi.
221
- 5. Ruleset cho `main`: bắt buộc PR, review, CODEOWNERS cho `.github/**`, required status check `ai-dlc/baseline`.
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: `ai-dlc: add ${path}`,
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: "ai-dlc: initialize evidence branch",
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("ai-dlc-evidence") }).default({ branch: "ai-dlc-evidence" }),
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/ai-dlc/policy.yml";
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: "ai-dlc:request",
680
- planDraft: "ai-dlc:plan-draft",
681
- planCreated: "ai-dlc:plan-created",
682
- planPartial: "ai-dlc:plan-partial",
683
- fromPlan: "ai-dlc:from-plan",
684
- stale: "ai-dlc:stale",
685
- exempt: "ai-dlc: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 = "/ai-dlc";
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
- if (!line.startsWith(COMMAND_PREFIX)) {
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(COMMAND_PREFIX.length).trim();
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 = "<!-- ai-dlc:begin -->";
921
- var PR_BLOCK_END = "<!-- ai-dlc: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
- const begin = text.indexOf(PR_BLOCK_BEGIN);
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(PR_BLOCK_BEGIN, begin + 1) !== -1) {
927
- throw new AiDlcError("E_PR_BLOCK", "PR body contains more than one AI-DLC block.");
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(PR_BLOCK_END, begin);
930
- if (end === -1) throw new AiDlcError("E_PR_BLOCK", "AI-DLC block is not closed.");
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 + PR_BLOCK_BEGIN.length, end).split("\n")) {
933
- const match = /^AI-DLC-([A-Za-z]+):\s*(.*)$/u.exec(line.trim());
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("E_PR_BLOCK", "AI-DLC-Issue must be `#<number>` in the same repository.");
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", "AI-DLC-Baseline is missing or invalid.");
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", "AI-DLC-Hash is missing or invalid.");
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 AI-DLC-AC.`);
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
- `AI-DLC-Issue: #${block.issueNumber}`,
956
- `AI-DLC-Baseline: ${block.baselineId}`,
957
- `AI-DLC-Hash: ${block.hash}`,
958
- `AI-DLC-AC: ${block.acceptanceCriteria.join(", ")}`,
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, "&lt;!--").replace(/</gu, "&lt;").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/ai-dlc/gmu, "\\/ai-dlc");
1029
+ return text.replace(/<!--/gu, "&lt;!--").replace(/</gu, "&lt;").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/(?:huddle60|ai-dlc)/gmu, "\\/huddle60");
999
1030
  }
1000
1031
  function marker(name, value) {
1001
- return `<!-- ai-dlc:${name}:${value} -->`;
1032
+ return `<!-- huddle60:${name}:${value} -->`;
1002
1033
  }
1003
1034
 
1004
1035
  // src/workflow/service.ts
1005
- var STATUS_CONTEXT = "ai-dlc/baseline";
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
- const source = await this.deps.github.readFile(POLICY_PATH, branch);
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
- `/ai-dlc approve baseline=${snapshot.baselineId} hash=${snapshot.contentHash}`,
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
- `/ai-dlc ready baseline=${snapshot.baselineId} hash=${snapshot.contentHash} lane=standard`,
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 `/ai-dlc baseline` again and decide on the new baseline."
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: "AI-DLC is disabled by the operator",
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 AI-DLC block. Run the context CLI or use the ready reply."
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", "AI-DLC-Hash does not match the baseline.");
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 [`**AI-DLC: \`${error.code}\`**`, "", sanitize(error.message)].join("\n");
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: "ai-dlc-action", event: eventName });
1516
- const octokit = octokitFromToken(requireEnv("AI_DLC_TOKEN"));
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
- const policySource = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
1519
- const branch = policySource ? parsePolicy(policySource).evidence.branch : "ai-dlc-evidence";
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
- `### ai-dlc/baseline: ${result.state}`,
1575
+ `### huddle60/baseline: ${result.state}`,
1538
1576
  "",
1539
1577
  result.description,
1540
1578
  ...result.details.map((line) => `> ${line}`)