@kienntpixon/dieu-phoi 0.2.2 → 0.4.0
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/CHANGELOG.md +29 -0
- package/README.md +10 -5
- package/bin/dieu-phoi.cjs +11 -3
- package/hook.cjs +258 -11
- package/lib/aggregate.cjs +72 -13
- package/lib/choose.cjs +74 -18
- package/lib/commands.cjs +258 -34
- package/lib/setup.cjs +8 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Theo bản rà soát telemetry tune0001 (2026-10-12: 886 sự kiện sạch, ~157 task, 8 phiên điều phối).
|
|
6
|
+
|
|
7
|
+
- **Nâng cấp:** **dán lại `sheet/Code.gs` và deploy bản mới TRƯỚC khi nâng CLI** — Code.gs cũ từ chối kind mới `discard` (rơi vào `sync-rejected.jsonl`), coi `aborted` như verdict thường và tính `orch_share` từ đầu phiên. Sau đó `npm i -g @kienntpixon/dieu-phoi@0.4.0` rồi `dieu-phoi setup`.
|
|
8
|
+
- **Chi phí điều phối tính từ dispatch đầu tiên** của mỗi phiên, không từ đầu phiên (`orchestration.orch_usd_total` / `orch_share`, CLI + sink, fixture parity cập nhật). Trước đây một phiên làm việc khác suốt buổi rồi mới giao một agent bị tính $540 "điều phối": `orch_share` báo 46% trong khi thật ~9,5%, làm tín hiệu retro mục 6 bắn sai.
|
|
9
|
+
- **Outcome mới `aborted`** cho agent dừng vì hết usage limit / quota: không phải pass hay fail, không thành mẫu, không tính leo thang. Hook `SubagentStop` tự ghi verdict `aborted` (`reason_code` `auto-usage-limit` khi dòng cuối là lỗi API "You've hit your … limit" / `rate_limit` / 429; `auto-quota-pause` khi dòng đầu báo cáo cuối là đúng dấu `STATUS: partial — quota-pause`). `verdict <agent_id> aborted` để ghi tay. Dữ liệu cũ: `timeout` có `reason_code` bắt đầu bằng `paused-for-quota` / `quota-pause` / `usage-limit` được đọc là `aborted` (5/18 lượt không pass trong log thật là quota pause, 3 trên sonnet/high). `state` hiện `aborted`, rồi `done` khi agent được làm tiếp và xong; `verdict last` vẫn coi agent `aborted` là chờ verdict.
|
|
10
|
+
- **Leo thang đếm cả làm lại bằng `SendMessage`:** mỗi verdict agent là một kết quả ở tier đó — fail, fail trên **cùng** một agent = 2 fail cùng tier → lên tier (trước đây chỉ verdict cuối của mỗi agent được tính, nên luật "2 fail cùng tier" không bao giờ bắn: 10 agent fail-rồi-pass, 2 agent fail-fail-pass).
|
|
11
|
+
- **`verdict --root <root_task_id> accept [--steps a,b]`:** người dùng nghiệm thu mọi bước đã giao exec của kế hoạch đã duyệt mà chưa có verdict người dùng, trong một lệnh (chỉ 16% task có verdict người dùng).
|
|
12
|
+
- **Steer chỉ tính pha exec:** steer trên scout/planner (trả lời câu hỏi cổng 1) và verifier không vào `steer_rate` / tín hiệu "brainstorm hỏi quá ít" nữa, mà vào cột mới `plan_phase_actions` (16/28 hành động lái trong log thật là trả lời cổng 1 cho scout). `steer --task` không đánh dấu verifier. Sink cũng bỏ steer trên agent không phải exec khỏi cột `steer_*`.
|
|
13
|
+
- **Bài học cho điều phối viên tới được điều phối viên:** `start` (`orch_lessons` trong `--json`) và khối UserPromptSubmit in 5 dòng cuối của `profile/lessons.md` có thẻ `[orch]` (hoặc `[orchestrator]` cũ), mỗi dòng ≤ 240 ký tự.
|
|
14
|
+
- **Override khi duyệt kế hoạch:** `plan-table --approve` ghi sự kiện `override` cho bước có `override` khác đề xuất (cả khi duyệt lại đổi tier); không nhân đôi khi đã có override cùng task + tier. Trước đây override trong file steps không bao giờ thành sự kiện (0 override trong log thật).
|
|
15
|
+
- **Chặn task_type tự đặt:** `choose` đổi nhãn quen sang loại cố định (`bug-fix`→`debug`, `config`/`shell`/`deploy`→`mechanical-edit`, `docs`→`content-write`, `design`/`plan`→`plan-design`, `research`→`research-synth`, `art-fix`→`art-gen` khi profile có, …) kèm `task_type`, `mapped_from`, `type_warning`; nhãn lạ khác → `action:"ask_user"`, `unknown_type:true`. `plan-table` cảnh báo, `--approve` từ chối nhãn lạ, bản ghi kế hoạch lưu loại đã đổi. (23 agent / 9 nhãn tự đặt trong log thật đã rơi về opus/medium.)
|
|
16
|
+
- **Ghi log an toàn hơn:** mỗi sự kiện là một lệnh ghi O_APPEND trọn dòng; file không kết thúc bằng `\n` (tiến trình chết giữa dòng) → ghi `\n` trước để mảnh dở không nuốt sự kiện mới. Điều tra dispatch `a955459fed35d6614` có trên Sheet mà mất ở `events.jsonl`: không phải tranh ghi — một agent theo yêu cầu người dùng đã **sửa tay** log để xoá một yêu cầu nhầm (task `63w6xmsh`, có bản sao `.bak-63w6xmsh`) sau khi dòng đó đã được đẩy lên sink. Sửa gốc: lệnh mới **`dieu-phoi discard --task <id> [--reason R]`** ghi sự kiện `discard` (kind mới, đồng bộ lên sink); CLI (stats/choose/state/verdict --root) và sink bỏ mọi sự kiện của task đó và của agent của nó; `orchestration` giữ nguyên (tiền đã tiêu).
|
|
17
|
+
- (sau verifier) Nhận diện `aborted` chỉ theo dấu chính xác: dòng đầu báo cáo `STATUS: partial — quota-pause`, hoặc dòng lỗi usage limit tổng hợp của Claude Code (`<synthetic>` + `rate_limit`/429/"You've hit your … limit"); `timeout` cũ chỉ đổi thành `aborted` khi `reason_code` bắt đầu bằng `quota-pause|usage-limit|auto-*|paused-for-quota`. `doctor` bỏ qua thư mục có tên giống file tạm.
|
|
18
|
+
- **Cảnh báo context bớt ồn:** `choose --json` thêm `orch_ctx_warn` — `true` chỉ khi vượt một ngưỡng mới của phiên (trần, 2×, 4×, …) hoặc vẫn vượt sau một lần `/compact`; dòng `CẢNH BÁO` của bản chữ chỉ in khi đó (trạng thái ở `data/ctx-warn.json`). `orch_ctx_over` giữ nghĩa cũ. Trước đây cảnh báo bắn ở 78% dispatch.
|
|
19
|
+
- `doctor`: dòng `tmp-files` xoá file `*.tmp-*` bỏ dở cũ hơn 1 giờ (6 file trong máy thật), dòng `plans` cảnh báo kế hoạch có root id sai định dạng.
|
|
20
|
+
- `retro --json`: `outcomes.aborted`, `totals.discarded_tasks`.
|
|
21
|
+
- `time_cap_min` mặc định của core (chỉ dùng khi không có profile) theo profile ai-lab đã tune: feature-impl 80, content-write 40, debug 55.
|
|
22
|
+
- **Không đổi:** priors, λ, `explore_rate`, `orch_ctx_soft_cap`, trần opus/medium, không Fable, không thăm dò task phán đoán / khó hoàn tác / gấp.
|
|
23
|
+
|
|
24
|
+
## 0.3.0
|
|
25
|
+
|
|
26
|
+
- **Khởi động không qua shell:** hook mới `UserPromptSubmit` (`setup` đăng ký, không matcher, timeout 5 s; chạy lại không nhân đôi; `uninstall` gỡ). Chỉ kích hoạt khi prompt **mở đầu** (sau khi cắt khoảng trắng) bằng `/dieu-phoi`, "điều phối" (không phân biệt hoa thường / có dấu hay không, có thể kèm ":") hoặc một cụm đặc trưng "spawn subagents", "orchestrate subagents", "chia việc cho agent", "giao cho subagent"; "điều phối" ở giữa câu ("trung tâm điều phối…", "không cần điều phối") không kích hoạt, yêu cầu kiểu retro / state / stats / lệnh CLI và "điều phối viên…" cũng không → hook tạo sẵn `root_task_id` (cùng bộ sinh với `new-id`), **tạo** run dir `~/.dieu-phoi/runs/<id>/` (theo `DIEU_PHOI_RUNS_DIR`), 10 `step_ids` dự phòng cho các bước, tình trạng sức khoẻ cục bộ và đường dẫn brief scout dựng sẵn, rồi tiêm vào `hookSpecificOutput.additionalContext` (dòng đầu `dieu-phoi: khởi động sẵn (hook UserPromptSubmit, …)`). Prompt khác → im lặng. Không gọi mạng, không ghi sự kiện, luôn exit 0 (lỗi → `hook-errors.log`). Đo: ~40 ms cả tiến trình node, < 5 ms phần xử lý. Lý do: lệnh shell đầu tiên của phiên Claude Code trong repo lớn có thể treo 6–11 phút trước khi có shell snapshot; trước đây `doctor; new-id` nằm đúng trên đường găng của cổng 1.
|
|
27
|
+
- Lệnh mới `dieu-phoi start [--json]`: dự phòng khi hook không chạy — một lệnh (~50 ms) kiểm cục bộ (hook đã cài/đăng ký, `UserPromptSubmit`, quyền run dir, config, profile, sink từ bộ nhớ đệm), sinh `root_task_id` + `step_ids`, tạo run dir, in đường dẫn brief scout. Không gọi mạng; luôn exit 0, vấn đề nằm trong `health`.
|
|
28
|
+
- **`doctor` thôi gọi mạng mỗi lần:** trạng thái sink lấy từ bộ nhớ đệm 24 giờ — `data/sink-check.json` (lần kiểm trực tiếp gần nhất của `doctor`, gắn với URL sink) hoặc `data/last-sync.json` (sync nền; từ 0.3.0 mang khoá `url` = sha256-8 của `sink_url`, khác URL hiện tại hoặc thiếu khoá → không dùng); không có bản nào còn hạn → kiểm trực tiếp một lần rồi lưu (kể cả khi lỗi). `doctor --net` buộc kiểm trực tiếp. Trước đây `doctor` mất 3,5–4,9 s, trong đó 3–4 s là request tới Apps Script.
|
|
29
|
+
- `doctor` có dòng `prompt-hook` (CẢNH BÁO khi thiếu hook `UserPromptSubmit` — chạy lại `setup`); dòng `settings` vẫn chỉ xét 3 hook cũ nên cài đặt 0.2.x không bị báo LỖI.
|
|
30
|
+
- Core export `isDieuPhoiPrompt`, `sinkStatusCached`, `quickHealth`, `startInfo`, `handleUserPromptSubmit`, … để CLI và hook dùng chung.
|
|
31
|
+
|
|
3
32
|
## 0.2.2
|
|
4
33
|
|
|
5
34
|
- **Trần model hạ xuống `opus/medium`** (trước là `opus/high`). Thang tier bỏ `opus/high`: `haiku/low … sonnet/high < opus/medium`. Mọi (model, effort) cao hơn (opus `high`/`xhigh`/`max`, Fable) bị kẹp về `opus/medium` khi đọc log và profile; `--override opus/high` / `--approved opus/high` bị từ chối (không nằm trên thang, lỗi nêu trần `opus/medium`).
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Hook + CLI cho skill **dieu-phoi**: ghi lại mỗi lần Claude Code giao việ
|
|
|
8
8
|
|
|
9
9
|
## Cài đặt
|
|
10
10
|
|
|
11
|
+
> Nâng cấp lên 0.4.0: **dán lại `sheet/Code.gs` và deploy bản mới TRƯỚC khi nâng CLI** — Code.gs cũ từ chối kind mới `discard` và tính `orch_share` theo cách cũ; sau đó `npm i -g @kienntpixon/dieu-phoi@0.4.0` rồi `dieu-phoi setup`.
|
|
12
|
+
>
|
|
11
13
|
> Nâng cấp lên 0.2.0: **dán lại `sheet/Code.gs` và deploy bản mới TRƯỚC khi nâng CLI** — Code.gs cũ bỏ qua (làm rơi) các trường mới `session_id, orch_ctx_tokens, orch_cum_usd, orch_ctx_baseline, orch_compactions` của bản ghi được đẩy lên trong lúc chưa cập nhật; sau đó `npm i -g @kienntpixon/dieu-phoi@0.2.0` rồi `dieu-phoi setup`.
|
|
12
14
|
|
|
13
15
|
Khuyến nghị cài toàn cục để skill gọi được `dieu-phoi …` trực tiếp:
|
|
@@ -34,16 +36,19 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
34
36
|
|
|
35
37
|
| Lệnh | Việc |
|
|
36
38
|
|---|---|
|
|
37
|
-
| `setup [--dept D] [--sink-url U --sink-token T] [--skill-dir P]` | cài hook, đăng ký `PostToolUse(Agent)` + `SubagentStop` + `SessionStart(compact)` trong settings, ghi config; tạo thư mục báo cáo `~/.dieu-phoi/runs` (env `DIEU_PHOI_RUNS_DIR` để đổi; ngoài `~/.claude` vì đó là protected path) và thêm quyền `Read`/`Edit(~/.dieu-phoi/runs/**)` + `additionalDirectories` để subagent nền ghi được báo cáo (không nhân đôi, có sao lưu); gỡ luật cũ của bản dev cho `~/.claude/dieu-phoi/runs` |
|
|
38
|
-
| `doctor [--json]` | kiểm node, hook, settings, config, profile, sink; context phiên điều phối hiện tại nếu đo được (`orch-ctx`); thư mục `~/.dieu-phoi/runs` + quyền của nó, không còn luật cũ (`run-permissions`); `--json` có `runs_dir` |
|
|
39
|
-
| `verdict <agent_id\|last> <pass\|fail\|timeout> [--verify tests\|lint\|review\|judgment\|none] [--reason short-kebab]` | nghiệm thu tầng agent (`by=agent`). `last` = agent hoàn thành gần nhất; từ chối (liệt kê ứng viên) nếu trong 30 phút qua có > 1 agent xong mà chưa có verdict. Được viết tắt agent_id nếu không trùng |
|
|
39
|
+
| `setup [--dept D] [--sink-url U --sink-token T] [--skill-dir P]` | cài hook, đăng ký `PostToolUse(Agent)` + `SubagentStop` + `SessionStart(compact)` + `UserPromptSubmit` trong settings, ghi config; tạo thư mục báo cáo `~/.dieu-phoi/runs` (env `DIEU_PHOI_RUNS_DIR` để đổi; ngoài `~/.claude` vì đó là protected path) và thêm quyền `Read`/`Edit(~/.dieu-phoi/runs/**)` + `additionalDirectories` để subagent nền ghi được báo cáo (không nhân đôi, có sao lưu); gỡ luật cũ của bản dev cho `~/.claude/dieu-phoi/runs` |
|
|
40
|
+
| `doctor [--net] [--json]` | kiểm node, hook, settings (+ `prompt-hook`), config, profile, sink (từ bộ nhớ đệm 24 giờ; `--net` kiểm trực tiếp); context phiên điều phối hiện tại nếu đo được (`orch-ctx`); thư mục `~/.dieu-phoi/runs` + quyền của nó, không còn luật cũ (`run-permissions`); `--json` có `runs_dir` |
|
|
41
|
+
| `verdict <agent_id\|last> <pass\|fail\|timeout\|aborted> [--verify tests\|lint\|review\|judgment\|none] [--reason short-kebab]` | nghiệm thu tầng agent (`by=agent`). `aborted` (0.4.0) = dừng vì usage limit/quota: không pass/fail, không leo thang; hook tự ghi khi bắt được. `last` = agent hoàn thành gần nhất; từ chối (liệt kê ứng viên) nếu trong 30 phút qua có > 1 agent xong mà chưa có verdict. Được viết tắt agent_id nếu không trùng |
|
|
40
42
|
| `verdict --task <task_id> <accept\|reject> [--cause quality\|scope] [--round n] [--reason R]` | nghiệm thu của **người dùng** (`by=user`). `reject` bắt buộc `--cause`: `quality` = làm sai (tính là fail), `scope` = yêu cầu đổi/thiếu (không tính cho model). `--round` mặc định tự tăng |
|
|
43
|
+
| `verdict --root <root_task_id> accept [--steps a,b]` | (0.4.0) người dùng nhận một lúc mọi bước đã giao exec của kế hoạch đã duyệt mà chưa có verdict người dùng |
|
|
44
|
+
| `discard --task <task_id> [--reason R]` | (0.4.0) rút một task nhầm khỏi stats/choose/state/sink bằng sự kiện `discard` — không bao giờ sửa tay `events.jsonl` |
|
|
41
45
|
| `steer <agent_id> \| --task <task_id> --kind minor\|plan\|redirect [--reason R]` | ghi **một** lần người dùng lái giữa chừng (một `steer_id`). `--task` chỉ đánh dấu agent đang chạy của task; không có agent nào đang chạy → bản ghi cấp task. `plan`/`redirect` trước verdict loại lượt đó khỏi tỉ lệ của tier (chi phí vẫn tính); steer không xoá fail đã ghi |
|
|
42
46
|
| `stats [--type T] [--json]` | các ô `task_type × model/effort` theo "Thước đo" của `CONTRACT.md`: n, n_excluded, accept_rate, agent_pass_rate, chi phí/thời gian mỗi task (TB, p50, p75); bảng steer theo task_type; tóm tắt điều phối (`orchestration`: số phiên, context p50/max, chi phí điều phối vs subagent, `orch_share`) |
|
|
43
47
|
| `choose --type T --score N [--urgency U] [--reversible y\|n] [--verifiable y\|n] [--role R] [--task ID] [--override M/E \| --approved M/E] [--skill-dir P] [--seed S] [--json]` | đề xuất model + effort kèm lý do, `action`, `role`, ước tính chi phí/thời gian. `--override` cần `--task`. Kèm `orch_ctx_tokens`, `orch_ctx_growth`, `orch_ctx_over` (như `ctx`); vượt trần mềm → thêm một dòng cảnh báo |
|
|
44
48
|
| `ctx [--session <id>] [--json]` | context phiên điều phối: `ctx_tokens` (request cuối), `ctx_baseline` (request đầu: system prompt + tools), `ctx_growth`, chi phí cộng dồn `cum_usd`, số lần đã nén `compactions`; `over` = `ctx_growth` > `orch_ctx_soft_cap` (profile, mặc định 60000 token tăng, chỉ được hạ). Session mặc định = `$CLAUDE_CODE_SESSION_ID`. JSON: `session_id, ctx_tokens, ctx_baseline, ctx_growth, cum_usd, compactions, soft_cap, over, advice`. Vượt trần → khuyên gõ `/compact` (trạng thái điều phối được nạp lại tự động). Đọc tăng dần qua cache `data/orch/`. Không đo được → exit 0, các số `null`, `over: false` |
|
|
45
49
|
| `state [--session <id>] [--json]` | trạng thái điều phối của phiên dựng lại chỉ từ log: mỗi task — cổng (1/2/3/xong), vòng, tier, các lượt (agent, role, try, model/effort, running/done/pass/fail/timeout), verdict người dùng, steer cuối, thư mục chạy; nhóm theo kế hoạch đã duyệt (tier đã duyệt, bước "chưa giao", thư mục chạy theo root). Hook `SessionStart(compact)` tự nạp bản này vào ngữ cảnh sau `/compact` hoặc auto-compact. Bản chữ ≤ 8.000 ký tự |
|
|
46
50
|
| `plan-table --file <steps.json> [--json] [--approve --root <root_task_id>]` | bảng phân công tiếng Việt (Bước \| Model/effort \| Rubric: điểm · kiểm chứng · đảo ngược \| Lý do \| Ước tính) + tổng chi phí + thời gian đường găng. Bước lỗi (override sai) hiện \"từ chối: …\", các bước khác vẫn hiện. Sau khi người dùng duyệt: `--approve --root R` ghi kế hoạch đã duyệt vào `data/plans/R.json` (cục bộ; `approval` = số lần duyệt, duyệt lại = +1, lần cũ vào `history`) và in thêm "Đã ghi kế hoạch đã duyệt (root R, lần duyệt n)". Từ chối (không ghi gì) khi bước thiếu `task_id`, `task_id` sai định dạng, `task_id` lặp, hoặc trùng `--root`; `--root` phải đúng 8 ký tự `a-z0-9` |
|
|
51
|
+
| `start [--json]` | khởi động cổng 1 trong một lệnh, không gọi mạng: kiểm cục bộ, `root_task_id` + `step_ids`, tạo run dir, đường dẫn brief scout. Thường không cần: hook `UserPromptSubmit` làm sẵn việc này khi prompt có "điều phối" hoặc bắt đầu bằng `/dieu-phoi` |
|
|
47
52
|
| `new-id [--count n] [--json]` | in task_id mới hợp lệ (đúng 8 ký tự `a-z0-9`), mỗi dòng một id (mặc định 1, tối đa 1000; `--json`: mảng). Lấy mọi `task_id` / `root_task_id` từ đây — không tự bịa. `choose/verdict/steer --task` và `plan-table --approve` từ chối id sai định dạng (kèm gợi ý id mới); hook vẫn ghi id cũ nhưng gắn `task_id_invalid: true`, `doctor` cảnh báo |
|
|
48
53
|
| `sync [--json]` | đẩy bản ghi chưa đồng bộ lên sink, lô ≤ 200, có thử lại. Bản ghi sink từ chối → `data/sync-rejected.jsonl`, `doctor` báo số lượng |
|
|
49
54
|
| `retro [--json]` | tóm tắt cho vòng retro: số liệu từng ô, đếm `reason_code`, danh sách loại task, tóm tắt điều phối (`orchestration`) |
|
|
@@ -52,7 +57,7 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
52
57
|
### `choose`
|
|
53
58
|
|
|
54
59
|
1. **Luật an toàn** (cố định trong core): trần `opus/medium` (opus/high, xhigh, max, Fable bị kẹp về opus/medium; `--override opus/high` bị từ chối). Sàn: score ≥ 9 → opus/medium; `--verifiable n` → tối thiểu opus/medium; `--reversible n` → tối thiểu opus/medium (role exec). Score ≥ 9, `--verifiable n`, `--reversible n` hoặc `urgent` → không thăm dò xuống tier thấp. Sàn được ghi rõ trong `reason`.
|
|
55
|
-
2. **Leo thang** với `--task <task_id>`: fail 1 lần → cùng tier (làm lại kèm lỗi); fail 2 lần cùng tier → lên một tier; tier kế vượt opus/medium (role exec) → **không tự lập lại kế hoạch**, trả `action: "ask_user"` kèm `fails` và `tried` để hỏi người dùng. Fail = agent `fail|timeout` hoặc người dùng `reject --cause quality
|
|
60
|
+
2. **Leo thang** với `--task <task_id>`: fail 1 lần → cùng tier (làm lại kèm lỗi); fail 2 lần cùng tier → lên một tier; tier kế vượt opus/medium (role exec) → **không tự lập lại kế hoạch**, trả `action: "ask_user"` kèm `fails` và `tried` để hỏi người dùng. Fail = agent `fail|timeout` hoặc người dùng `reject --cause quality`; mỗi verdict là một kết quả, nên làm lại bằng `SendMessage` cho cùng agent cũng được đếm (0.4.0); `aborted` không bao giờ tính. Lượt bị steer `plan`/`redirect` không tính.
|
|
56
61
|
3. **Prior** từ `<skill-dir>/profile/profile.json` (không thấy thì dùng bản sao dựng sẵn của `skill/profile/profile.json` v1.0.0). `profile_source` trong kết quả cho biết đang dùng file nào (`builtin` hoặc đường dẫn).
|
|
57
62
|
4. **Thompson sampling** khi ô của prior có ≥ 5 task đã xong + có verdict: rút p ~ Beta(1+pass, 1+fail) cho từng tier đủ dữ liệu, điểm = E[cost]/p + λ·E[time_phút]/p (urgent dùng p75 thời gian), chọn điểm thấp nhất. Tier có p75 vượt `time_cap_min` của loại task bị loại.
|
|
58
63
|
5. **Thăm dò** một tier thấp hơn với xác suất `explore_rate` (mặc định 0.1) khi luật an toàn cho phép.
|
|
@@ -84,7 +89,7 @@ Kết quả `--json`: `{"action":"dispatch","model":"sonnet","model_id":"claude-
|
|
|
84
89
|
|
|
85
90
|
## Dữ liệu
|
|
86
91
|
|
|
87
|
-
- Sự kiện: `~/.claude/dieu-phoi/data/events.jsonl` (`dispatch`, `complete`, `verdict`, `steer`, `override`; `schema: 1`), xem `CONTRACT.md`. File chỉ ghi nối (append-only), không bao giờ viết lại. Tiến độ đẩy lên sink nằm ở `data/sync-offset.json` (byte offset sau dòng cuối đã đẩy); trạng thái segment từng agent ở `data/segments/<agent_id>.json`.
|
|
92
|
+
- Sự kiện: `~/.claude/dieu-phoi/data/events.jsonl` (`dispatch`, `complete`, `verdict`, `steer`, `override`, `discard`; `schema: 1`), xem `CONTRACT.md`. File chỉ ghi nối (append-only), không bao giờ viết lại. Tiến độ đẩy lên sink nằm ở `data/sync-offset.json` (byte offset sau dòng cuối đã đẩy); trạng thái segment từng agent ở `data/segments/<agent_id>.json`.
|
|
88
93
|
- Lỗi của hook: `~/.claude/dieu-phoi/hook-errors.log`.
|
|
89
94
|
- Subagent nội bộ của Claude Code (không có transcript, không qua Agent tool) không được ghi; số lần bỏ qua ở `data/skipped-internal.json`. Log cũ của 0.1.0 có các `complete` rỗng loại này — `stats`/`retro` tự bỏ qua, không cần sửa file (từ 0.1.1).
|
|
90
95
|
- Người dùng chỉ lưu dạng sha256 rút gọn 8 ký tự (`git config user.email`, không có thì tên tài khoản máy). Phiên: `session` = sha256-8; từ 0.2.0 `dispatch` có thêm `session_id` thô (mã phiên, không phải nội dung) để nối với `dieu-phoi ctx`.
|
package/bin/dieu-phoi.cjs
CHANGED
|
@@ -10,11 +10,17 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
10
10
|
|
|
11
11
|
setup [--dept D] [--sink-url U --sink-token T] [--skill-dir P]
|
|
12
12
|
cài hook vào ~/.claude/dieu-phoi/bin, đăng ký trong settings.json
|
|
13
|
-
doctor
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
doctor [--net] kiểm hook, node, config, sink (sink lấy từ bộ nhớ đệm 24 giờ; --net kiểm trực tiếp)
|
|
14
|
+
start [--json] khởi động cổng 1 trong một lệnh (~150 ms, không gọi mạng): kiểm cục bộ, task_id gốc,
|
|
15
|
+
tạo run dir, đường dẫn brief scout — dùng khi hook UserPromptSubmit không chạy
|
|
16
|
+
verdict <agent_id|last> <pass|fail|timeout|aborted> [--verify tests|lint|review|judgment|none] [--reason short-kebab]
|
|
17
|
+
nghiệm thu tầng agent (by=agent); aborted = dừng vì hết quota/usage limit (không phải fail)
|
|
16
18
|
verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n] [--reason R]
|
|
17
19
|
nghiệm thu của người dùng (by=user); reject bắt buộc --cause
|
|
20
|
+
verdict --root <root_task_id> accept [--steps a,b]
|
|
21
|
+
người dùng nhận mọi bước đã giao exec của kế hoạch đã duyệt chưa có verdict người dùng
|
|
22
|
+
discard --task <task_id> [--reason R]
|
|
23
|
+
loại một task nhầm khỏi thống kê/choose/state (ghi sự kiện discard, không sửa log)
|
|
18
24
|
steer <agent_id> --kind minor|plan|redirect [--reason R]
|
|
19
25
|
steer --task <task_id> --kind minor|plan|redirect [--reason R]
|
|
20
26
|
ghi một lần người dùng lái giữa chừng (--task: chỉ các agent đang chạy của task)
|
|
@@ -63,8 +69,10 @@ async function main(argv) {
|
|
|
63
69
|
const table = {
|
|
64
70
|
setup: c.cmdSetup,
|
|
65
71
|
doctor: c.cmdDoctor,
|
|
72
|
+
start: c.cmdStart,
|
|
66
73
|
verdict: c.cmdVerdict,
|
|
67
74
|
steer: c.cmdSteer,
|
|
75
|
+
discard: c.cmdDiscard,
|
|
68
76
|
'plan-table': c.cmdPlanTable,
|
|
69
77
|
'new-id': c.cmdNewId,
|
|
70
78
|
stats: c.cmdStats,
|
package/hook.cjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* dieu-phoi hook + core helpers.
|
|
5
5
|
*
|
|
6
6
|
* This file is copied verbatim to ~/.claude/dieu-phoi/bin/hook.cjs by `dieu-phoi setup`
|
|
7
|
-
* and run by Claude Code on PostToolUse(Agent) and
|
|
7
|
+
* and run by Claude Code on PostToolUse(Agent), SubagentStop, SessionStart(compact) and UserPromptSubmit. It must stay
|
|
8
8
|
* self-contained: Node built-ins only, no requires of other package files.
|
|
9
9
|
*
|
|
10
|
-
* Hook rules:
|
|
10
|
+
* Hook rules: stdout only for the JSON of SessionStart(compact) / UserPromptSubmit, always exit 0,
|
|
11
|
+
* no network.
|
|
11
12
|
* Errors go to <home>/hook-errors.log.
|
|
12
13
|
*
|
|
13
14
|
* node hook.cjs -> read hook payload from stdin, append event
|
|
@@ -19,7 +20,7 @@ const path = require('path');
|
|
|
19
20
|
const os = require('os');
|
|
20
21
|
const crypto = require('crypto');
|
|
21
22
|
|
|
22
|
-
const CORE_VERSION = '0.
|
|
23
|
+
const CORE_VERSION = '0.4.0';
|
|
23
24
|
const SCHEMA = 1;
|
|
24
25
|
const TASK_TYPES = [
|
|
25
26
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -124,6 +125,7 @@ function pathsFor(home) {
|
|
|
124
125
|
syncOffset: path.join(data, 'sync-offset.json'),
|
|
125
126
|
syncSpawn: path.join(data, 'sync-spawn.json'),
|
|
126
127
|
lastSync: path.join(data, 'last-sync.json'),
|
|
128
|
+
sinkCheck: path.join(data, 'sink-check.json'),
|
|
127
129
|
syncRejected: path.join(data, 'sync-rejected.jsonl'),
|
|
128
130
|
lock: path.join(data, 'sync.lock'),
|
|
129
131
|
config: path.join(home, 'config.json'),
|
|
@@ -735,10 +737,30 @@ function findSessionTranscript(claudeDir, sessionId) {
|
|
|
735
737
|
|
|
736
738
|
// ------------------------------------------------------------------ events (append-only)
|
|
737
739
|
|
|
738
|
-
/**
|
|
740
|
+
/**
|
|
741
|
+
* Append one event. events.jsonl is append-only: never rewritten, sync progress lives elsewhere.
|
|
742
|
+
* Core 0.4.0: one O_APPEND write of the whole line (hooks of parallel agents may append at the same time:
|
|
743
|
+
* an O_APPEND write lands as one unit, so lines never interleave). If the file does not
|
|
744
|
+
* end with '\n' (a writer died mid-line), a '\n' is written first so the torn fragment cannot swallow this
|
|
745
|
+
* event — readers skip the fragment as one unparsable line. Withdrawing a task = `dieu-phoi discard`, never an
|
|
746
|
+
* edit of this file.
|
|
747
|
+
*/
|
|
739
748
|
function appendEvent(ctx, ev) {
|
|
740
749
|
fs.mkdirSync(ctx.paths.data, { recursive: true });
|
|
741
|
-
fs.
|
|
750
|
+
const fd = fs.openSync(ctx.paths.events, 'a+');
|
|
751
|
+
try {
|
|
752
|
+
let lead = '';
|
|
753
|
+
const size = fs.fstatSync(fd).size;
|
|
754
|
+
if (size > 0) {
|
|
755
|
+
const last = Buffer.alloc(1);
|
|
756
|
+
if (fs.readSync(fd, last, 0, 1, size - 1) === 1 && last[0] !== 0x0a) lead = '\n';
|
|
757
|
+
}
|
|
758
|
+
const buf = Buffer.from(`${lead}${JSON.stringify(ev)}\n`, 'utf8');
|
|
759
|
+
let off = 0;
|
|
760
|
+
while (off < buf.length) off += fs.writeSync(fd, buf, off, buf.length - off);
|
|
761
|
+
} finally {
|
|
762
|
+
fs.closeSync(fd);
|
|
763
|
+
}
|
|
742
764
|
}
|
|
743
765
|
|
|
744
766
|
function readEventsBuffer(ctx) {
|
|
@@ -857,7 +879,10 @@ function verdictLayer(ev) {
|
|
|
857
879
|
const by = ev.by === 'user' ? 'user' : 'agent';
|
|
858
880
|
let cause = ev.cause || null;
|
|
859
881
|
if (by === 'user' && ev.outcome === 'reject' && cause !== 'scope') cause = 'quality';
|
|
860
|
-
|
|
882
|
+
// legacy quota pause recorded as timeout → aborted (same reading as the stats)
|
|
883
|
+
const outcome = by === 'agent' && ev.outcome === 'timeout' && /^(?:quota-pause|usage-limit|auto-quota-pause|auto-usage-limit|paused-for-quota)(?:-|$)/.test(String(ev.reason_code || ''))
|
|
884
|
+
? 'aborted' : ev.outcome;
|
|
885
|
+
return { by, outcome, cause };
|
|
861
886
|
}
|
|
862
887
|
|
|
863
888
|
function dispatchInSession(ev, sessionId) {
|
|
@@ -891,7 +916,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
891
916
|
const runs = toPosix(o.runsDir || runsDir());
|
|
892
917
|
const taskIds = [];
|
|
893
918
|
const seenTask = new Set();
|
|
919
|
+
const discarded = new Set(events.filter((ev) => ev && ev.kind === 'discard' && ev.task_id).map((ev) => ev.task_id));
|
|
894
920
|
for (const ev of events) {
|
|
921
|
+
if (ev && discarded.has(ev.task_id)) continue;
|
|
895
922
|
if (dispatchInSession(ev, sessionId) && typeof ev.task_id === 'string' && ev.task_id && !seenTask.has(ev.task_id)) {
|
|
896
923
|
seenTask.add(ev.task_id);
|
|
897
924
|
taskIds.push(ev.task_id);
|
|
@@ -906,7 +933,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
906
933
|
for (const s of p.steps) if (s && s.task_id && !stepOf.has(s.task_id)) stepOf.set(s.task_id, { plan: p, step: s });
|
|
907
934
|
}
|
|
908
935
|
for (const id of [...planOfRoot.keys(), ...stepOf.keys()]) {
|
|
909
|
-
if (!seenTask.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
936
|
+
if (!seenTask.has(id) && !discarded.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
910
937
|
}
|
|
911
938
|
const per = new Map(taskIds.map((id) => [id, { dispatches: [], user: [], steers: [], overrides: [], agentIds: new Set() }]));
|
|
912
939
|
events.forEach((ev, i) => {
|
|
@@ -923,7 +950,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
923
950
|
const agentVerdicts = new Map();
|
|
924
951
|
for (const ev of events) {
|
|
925
952
|
if (!ev || typeof ev !== 'object') continue;
|
|
926
|
-
if (ev.kind === 'complete' && agentTask.has(ev.agent_id)) completes.set(ev.agent_id, (completes.get(ev.agent_id) ||
|
|
953
|
+
if (ev.kind === 'complete' && agentTask.has(ev.agent_id)) completes.set(ev.agent_id, Math.max(completes.get(ev.agent_id) || 1, evMs(ev) || 1));
|
|
927
954
|
if (ev.kind === 'verdict') {
|
|
928
955
|
const v = verdictLayer(ev);
|
|
929
956
|
if (v.by === 'user') {
|
|
@@ -968,7 +995,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
968
995
|
const vs = (agentVerdicts.get(d.agent_id) || []).slice().sort(byTime);
|
|
969
996
|
const last = vs[vs.length - 1];
|
|
970
997
|
let status;
|
|
971
|
-
|
|
998
|
+
// aborted (quota pause) then resumed and completed again, no verdict yet → done (waiting for a verdict)
|
|
999
|
+
if (last && last.outcome === 'aborted' && (completes.get(d.agent_id) || 0) > evMs(last)) status = 'done';
|
|
1000
|
+
else if (last) status = last.outcome;
|
|
972
1001
|
else status = completes.get(d.agent_id) ? 'done' : 'running';
|
|
973
1002
|
return {
|
|
974
1003
|
agent_id: d.agent_id, role: d.role || 'exec', try: d.try === undefined ? null : d.try,
|
|
@@ -1140,6 +1169,182 @@ function handleSessionStart(p, ctx) {
|
|
|
1140
1169
|
};
|
|
1141
1170
|
}
|
|
1142
1171
|
|
|
1172
|
+
// ------------------------------------------------------------------ fast start (UserPromptSubmit + `start`)
|
|
1173
|
+
|
|
1174
|
+
const SINK_CACHE_TTL_MS = 24 * 3600 * 1000;
|
|
1175
|
+
const START_PREFIX = 'dieu-phoi: khởi động sẵn (hook UserPromptSubmit, không cần chạy shell trước scout):';
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Prompt that starts a NEW dieu-phoi orchestration. Conservative: the trigger must OPEN the prompt
|
|
1179
|
+
* (after trimming) — `/dieu-phoi`, "điều phối" (any case, with or without diacritics, optionally
|
|
1180
|
+
* followed by ":"), or one of the skill's distinctive phrases ("spawn subagents", "orchestrate
|
|
1181
|
+
* subagents", "chia việc cho agent", "giao cho subagent"). "điều phối" elsewhere in the prompt never
|
|
1182
|
+
* fires. Retro / state / stats / CLI-style requests and "điều phối viên …" never fire (not a gate 1).
|
|
1183
|
+
*/
|
|
1184
|
+
const PROMPT_START_RE = /^(?:\/dieu-phoi|dieu phoi|spawn (?:\d+ )?sub-?agents?|orchestrate sub-?agents?|chia viec cho (?:cac )?(?:sub-?)?agents?|giao (?:viec )?cho (?:cac )?sub-?agents?)(?=$|[\s:,.!?])/;
|
|
1185
|
+
const PROMPT_REST_EXCLUDE_RE = /^[\s:,.!?-]*(?:vien\b|retro|state\b|stats?\b|status\b|trang thai|thong ke|ctx\b|doctor|setup\b|start\b|sync\b|uninstall|new-id|choose\b|plan-table|verdict|steer\b)/;
|
|
1186
|
+
const PROMPT_ANY_EXCLUDE_RE = /\b(?:retro|thong ke)\b/;
|
|
1187
|
+
|
|
1188
|
+
function foldPrompt(s) {
|
|
1189
|
+
return String(s).toLowerCase().replace(/đ/g, 'd').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
1190
|
+
.replace(/\s+/g, ' ');
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function isDieuPhoiPrompt(prompt) {
|
|
1194
|
+
if (typeof prompt !== 'string' || !prompt) return false;
|
|
1195
|
+
const t = foldPrompt(prompt.trimStart().slice(0, 300)).trim();
|
|
1196
|
+
const m = PROMPT_START_RE.exec(t);
|
|
1197
|
+
if (!m) return false;
|
|
1198
|
+
if (PROMPT_REST_EXCLUDE_RE.test(t.slice(m[0].length))) return false;
|
|
1199
|
+
return !PROMPT_ANY_EXCLUDE_RE.test(t);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function sinkUrlKey(cfg) {
|
|
1203
|
+
return cfg && cfg.sink_url ? sha8(String(cfg.sink_url)) : null;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Sink health without a network call: newest of data/sink-check.json (doctor's live check, same sink
|
|
1208
|
+
* URL) and data/last-sync.json (every background sync) within 24 h.
|
|
1209
|
+
* Returns { state: 'none'|'ok'|'fail'|'unknown', ts, source, detail }.
|
|
1210
|
+
*/
|
|
1211
|
+
function sinkStatusCached(ctx, nowMs) {
|
|
1212
|
+
const cfg = ctx.config || {};
|
|
1213
|
+
if (!cfg.sink_url || !cfg.sink_token) return { state: 'none', ts: null, source: null, detail: 'chưa cấu hình (chỉ lưu cục bộ)' };
|
|
1214
|
+
const now = nowMs || Date.now();
|
|
1215
|
+
const cands = [];
|
|
1216
|
+
const chk = readJsonFile(ctx.paths.sinkCheck, null);
|
|
1217
|
+
if (chk && chk.url === sinkUrlKey(cfg) && Date.parse(chk.ts)) {
|
|
1218
|
+
cands.push({ state: chk.ok ? 'ok' : 'fail', ts: chk.ts, source: 'doctor', detail: chk.detail || null });
|
|
1219
|
+
}
|
|
1220
|
+
const ls = readJsonFile(ctx.paths.lastSync, null);
|
|
1221
|
+
// last-sync is only trusted for the sink it was written for (sink_url changed → stale)
|
|
1222
|
+
if (ls && ls.url === sinkUrlKey(cfg) && Date.parse(ls.ts)) {
|
|
1223
|
+
cands.push({ state: ls.ok ? 'ok' : 'fail', ts: ls.ts, source: 'sync', detail: ls.ok ? `đẩy ${ls.sent || 0} bản ghi` : (ls.error || 'lỗi') });
|
|
1224
|
+
}
|
|
1225
|
+
const fresh = cands.filter((c) => now - Date.parse(c.ts) <= SINK_CACHE_TTL_MS && Date.parse(c.ts) <= now + 60000)
|
|
1226
|
+
.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
1227
|
+
if (fresh.length) return fresh[0];
|
|
1228
|
+
return { state: 'unknown', ts: null, source: null, detail: 'chưa kiểm trong 24 giờ qua (`dieu-phoi doctor --net`)' };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/** Absolute path of the ready-made scout brief of the installed skill, or null. */
|
|
1232
|
+
function scoutBriefPath(ctx) {
|
|
1233
|
+
const sd = ctx.config && ctx.config.skill_dir;
|
|
1234
|
+
if (!sd) return null;
|
|
1235
|
+
const f = path.join(sd, 'references', 'scout-brief.md');
|
|
1236
|
+
return fs.existsSync(f) ? toPosix(f) : null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const ORCH_LESSON_RE = /\[(?:orch|orchestrator)\]/i;
|
|
1240
|
+
const ORCH_LESSONS_MAX = 5;
|
|
1241
|
+
const ORCH_LESSON_CHARS = 240;
|
|
1242
|
+
const LESSONS_MAX_BYTES = 256 * 1024;
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Lessons addressed to the orchestrator (core 0.4.0): list lines of <skill_dir>/profile/lessons.md that carry the
|
|
1246
|
+
* tag `[orch]` (legacy `[orchestrator]`), the last ORCH_LESSONS_MAX of them, each cut to ORCH_LESSON_CHARS chars.
|
|
1247
|
+
* The orchestrator never reads lessons.md itself; `start` and the UserPromptSubmit block surface these lines.
|
|
1248
|
+
*/
|
|
1249
|
+
function orchLessons(ctx) {
|
|
1250
|
+
const sd = ctx.config && ctx.config.skill_dir;
|
|
1251
|
+
if (!sd) return [];
|
|
1252
|
+
let txt;
|
|
1253
|
+
try {
|
|
1254
|
+
const f = path.join(sd, 'profile', 'lessons.md');
|
|
1255
|
+
if (fs.statSync(f).size > LESSONS_MAX_BYTES) return [];
|
|
1256
|
+
txt = fs.readFileSync(f, 'utf8');
|
|
1257
|
+
} catch (_) { return []; }
|
|
1258
|
+
const out = txt.split(/\r?\n/).map((l) => l.trim())
|
|
1259
|
+
.filter((l) => /^[-*] /.test(l) && ORCH_LESSON_RE.test(l))
|
|
1260
|
+
.map((l) => l.replace(/^[-*] +/, ''))
|
|
1261
|
+
.map((l) => (l.length > ORCH_LESSON_CHARS ? `${l.slice(0, ORCH_LESSON_CHARS - 1)}…` : l));
|
|
1262
|
+
return out.slice(-ORCH_LESSONS_MAX);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** Local, network-free health: config, profile, run dir, cached sink status. */
|
|
1266
|
+
function quickHealth(ctx, runs) {
|
|
1267
|
+
const problems = [];
|
|
1268
|
+
if (!fs.existsSync(ctx.paths.config)) problems.push('chưa có config — chạy `dieu-phoi setup --dept <phòng>`');
|
|
1269
|
+
const sd = ctx.config.skill_dir;
|
|
1270
|
+
if (!sd || !fs.existsSync(path.join(sd, 'profile', 'profile.json'))) problems.push('không thấy profile của skill — `choose` dùng priors mặc định');
|
|
1271
|
+
if (runs && !fs.existsSync(runs)) problems.push(`chưa có thư mục ${toPosix(runs)}`);
|
|
1272
|
+
const sink = sinkStatusCached(ctx);
|
|
1273
|
+
if (sink.state === 'fail') problems.push(`sink lỗi lần cuối (${sink.ts}): ${sink.detail}`);
|
|
1274
|
+
return { ok: !problems.length, problems, sink };
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/** Fresh root task id + its run dir, created. Retries on the (astronomically rare) existing dir. */
|
|
1278
|
+
function startRun() {
|
|
1279
|
+
const runs = runsDir();
|
|
1280
|
+
fs.mkdirSync(runs, { recursive: true });
|
|
1281
|
+
for (let i = 0; i < 5; i++) {
|
|
1282
|
+
const id = newTaskId();
|
|
1283
|
+
const dir = path.join(runs, id);
|
|
1284
|
+
try {
|
|
1285
|
+
fs.mkdirSync(dir);
|
|
1286
|
+
return { id, run_dir: toPosix(dir), runs };
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
if (e.code !== 'EEXIST') throw e;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
throw new Error('không tạo được run dir mới');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function healthLine(h) {
|
|
1295
|
+
const sink = h.sink.state === 'ok' ? `sink ok (${h.sink.source} ${h.sink.ts})`
|
|
1296
|
+
: h.sink.state === 'none' ? 'sink chưa cấu hình' : `sink ${h.sink.state === 'fail' ? 'LỖI' : 'chưa rõ'}`;
|
|
1297
|
+
return h.ok ? `ok — ${sink}` : `CẢNH BÁO — ${h.problems.join('; ')}${h.sink.state === 'fail' ? '' : ` — ${sink}`}`;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const START_STEP_IDS = 10;
|
|
1301
|
+
|
|
1302
|
+
/** Spare task_ids for the plan steps, distinct from the root, so the scout needs no `new-id` shell call. */
|
|
1303
|
+
function stepIds(root, n) {
|
|
1304
|
+
const ids = new Set();
|
|
1305
|
+
while (ids.size < n) { const id = newTaskId(); if (id !== root) ids.add(id); }
|
|
1306
|
+
return Array.from(ids);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function startInfo(ctx) {
|
|
1310
|
+
const run = startRun();
|
|
1311
|
+
const health = quickHealth(ctx, run.runs);
|
|
1312
|
+
return {
|
|
1313
|
+
id: run.id, run_dir: run.run_dir, step_ids: stepIds(run.id, START_STEP_IDS), health, scout_brief: scoutBriefPath(ctx),
|
|
1314
|
+
orch_lessons: orchLessons(ctx),
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function orchLessonLines(info) {
|
|
1319
|
+
const ls = (info && info.orch_lessons) || [];
|
|
1320
|
+
return ls.length ? ['bài học cho điều phối viên (profile/lessons.md, [orch]):'].concat(ls.map((l) => ` - ${l}`)) : [];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function formatStartBlock(info) {
|
|
1324
|
+
return [
|
|
1325
|
+
START_PREFIX,
|
|
1326
|
+
`root_task_id: ${info.id}`,
|
|
1327
|
+
`run_dir: ${info.run_dir}/ (đã tạo)`,
|
|
1328
|
+
`step_ids: ${info.step_ids.join(' ')}`,
|
|
1329
|
+
`health: ${healthLine(info.health)}`,
|
|
1330
|
+
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
1331
|
+
...orchLessonLines(info),
|
|
1332
|
+
'Dùng khối này cho yêu cầu MỚI ở cổng 1 (không dùng nếu đang giữa một lần điều phối). Không chạy `dieu-phoi doctor` / `new-id` / mkdir: lượt đầu tiên là Agent scout + câu hỏi cổng 1 cho người dùng.',
|
|
1333
|
+
].join('\n');
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** UserPromptSubmit: on a dieu-phoi prompt, pre-create the run and inject id/run dir/health/brief. */
|
|
1337
|
+
function handleUserPromptSubmit(p, ctx) {
|
|
1338
|
+
if (!isDieuPhoiPrompt(p.prompt)) return null;
|
|
1339
|
+
const info = startInfo(ctx);
|
|
1340
|
+
return {
|
|
1341
|
+
stdout: JSON.stringify({
|
|
1342
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: formatStartBlock(info) },
|
|
1343
|
+
}),
|
|
1344
|
+
info,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1143
1348
|
// ------------------------------------------------------------------ hook handlers
|
|
1144
1349
|
|
|
1145
1350
|
function extractAgentId(tr) {
|
|
@@ -1282,9 +1487,50 @@ function handleSubagentStop(p, ctx) {
|
|
|
1282
1487
|
});
|
|
1283
1488
|
}
|
|
1284
1489
|
appendEvent(ctx, ev);
|
|
1490
|
+
// Core 0.4.0: a segment that ended on a usage-limit / quota pause gets an automatic agent verdict
|
|
1491
|
+
// `aborted` (never a fail, never counted toward escalation); the real verdict comes after the resume.
|
|
1492
|
+
const why = exists ? abortReasonOf(lines) : null;
|
|
1493
|
+
if (why) {
|
|
1494
|
+
appendEvent(ctx, Object.assign({
|
|
1495
|
+
schema: SCHEMA, kind: 'verdict', ts: nowIso(), agent_id: agentId, task_id: t.task_id, by: 'agent', outcome: 'aborted',
|
|
1496
|
+
verify: 'none', reason_code: `auto-${why}`, cause: null, round: null,
|
|
1497
|
+
}, commonFields(ctx)));
|
|
1498
|
+
}
|
|
1285
1499
|
return ev;
|
|
1286
1500
|
}
|
|
1287
1501
|
|
|
1502
|
+
// Exact marker an agent puts on the FIRST line of its final report when it stops at a safe point because of a quota
|
|
1503
|
+
// / usage limit (brief template A, SKILL.md). Nothing looser: "STATUS: partial" + "quota" can be a real failure
|
|
1504
|
+
// ("Drive storage quota exceeded", a half-built rate limiter).
|
|
1505
|
+
const QUOTA_PAUSE_MARKER = 'STATUS: partial — quota-pause';
|
|
1506
|
+
// Claude Code's own usage-limit stop text on its synthetic API-error line ("You've hit your session limit · resets 11am").
|
|
1507
|
+
const CC_LIMIT_TEXT_RE = /^You['’]ve hit your (?:[\w-]+ )?limit\b/;
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Why the agent's last segment stopped on a usage limit, or null. Reads only the LAST assistant line:
|
|
1511
|
+
* - Claude Code's synthetic API-error line (`model:"<synthetic>"` + `isApiErrorMessage:true`) whose `error` is
|
|
1512
|
+
* "rate_limit", or HTTP 429, or whose text starts "You've hit your … limit" → "usage-limit";
|
|
1513
|
+
* - the agent's final report whose first line starts with the exact marker QUOTA_PAUSE_MARKER → "quota-pause".
|
|
1514
|
+
* Only a short label is returned; no text is stored.
|
|
1515
|
+
*/
|
|
1516
|
+
function abortReasonOf(lines) {
|
|
1517
|
+
let last = null;
|
|
1518
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1519
|
+
const l = lines[i];
|
|
1520
|
+
if (l && l.type === 'assistant' && l.message) { last = l; break; }
|
|
1521
|
+
}
|
|
1522
|
+
if (!last) return null;
|
|
1523
|
+
const content = Array.isArray(last.message.content) ? last.message.content : [];
|
|
1524
|
+
const text = content.filter((c) => c && c.type === 'text' && typeof c.text === 'string').map((c) => c.text).join('\n').slice(0, 2000);
|
|
1525
|
+
if (last.isApiErrorMessage === true) {
|
|
1526
|
+
if (last.message.model !== '<synthetic>') return null;
|
|
1527
|
+
if (last.error === 'rate_limit' || last.apiErrorStatus === 429 || CC_LIMIT_TEXT_RE.test(text.trim())) return 'usage-limit';
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
if (text.trimStart().split('\n')[0].trim().startsWith(QUOTA_PAUSE_MARKER)) return 'quota-pause';
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1288
1534
|
function maybeSpawnSync(ctx) {
|
|
1289
1535
|
if (!ctx.config.sink_url || !ctx.config.sink_token) return false;
|
|
1290
1536
|
if (process.env.DIEU_PHOI_NO_SPAWN === '1') return false;
|
|
@@ -1337,6 +1583,7 @@ function runHook(opts) {
|
|
|
1337
1583
|
let ev = null;
|
|
1338
1584
|
const name = p.hook_event_name;
|
|
1339
1585
|
if (name === 'SessionStart') return handleSessionStart(p, ctx); // { stdout } or null, never an event
|
|
1586
|
+
if (name === 'UserPromptSubmit') return handleUserPromptSubmit(p, ctx); // { stdout } or null
|
|
1340
1587
|
if (name === 'SubagentStop') ev = handleSubagentStop(p, ctx);
|
|
1341
1588
|
else if (name === 'PostToolUse' || (!name && p.tool_name)) ev = handlePostToolUse(p, ctx);
|
|
1342
1589
|
if (ev) {
|
|
@@ -1477,7 +1724,7 @@ async function runSync(ctx, opts) {
|
|
|
1477
1724
|
} finally {
|
|
1478
1725
|
try {
|
|
1479
1726
|
writeFileAtomic(ctx.paths.lastSync, `${JSON.stringify({
|
|
1480
|
-
ts: nowIso(), ok: result.ok, sent: result.sent, error: result.error,
|
|
1727
|
+
ts: nowIso(), ok: result.ok, sent: result.sent, error: result.error, url: sinkUrlKey(cfg),
|
|
1481
1728
|
})}\n`);
|
|
1482
1729
|
} catch (_) { /* best effort */ }
|
|
1483
1730
|
try { fs.unlinkSync(ctx.paths.lock); } catch (_) { /* gone */ }
|
|
@@ -1488,7 +1735,7 @@ async function runSync(ctx, opts) {
|
|
|
1488
1735
|
// ------------------------------------------------------------------ exports / main
|
|
1489
1736
|
|
|
1490
1737
|
module.exports = {
|
|
1491
|
-
runsDir, loadPlans, buildSessionState, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
1738
|
+
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, orchLessons, orchLessonLines, abortReasonOf, QUOTA_PAUSE_MARKER, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, START_PREFIX, buildSessionState, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
1492
1739
|
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED,
|
|
1493
1740
|
sha8, nowIso, readJsonFile, writeFileAtomic, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
1494
1741
|
logError, computeUserHash, userHash, commonFields, parseTag, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|