@kienntpixon/dieu-phoi 0.3.0 → 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 +21 -0
- package/README.md +7 -3
- package/bin/dieu-phoi.cjs +7 -2
- package/hook.cjs +112 -9
- package/lib/aggregate.cjs +72 -13
- package/lib/choose.cjs +74 -18
- package/lib/commands.cjs +199 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## 0.3.0
|
|
4
25
|
|
|
5
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.
|
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:
|
|
@@ -36,8 +38,10 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
36
38
|
|---|---|
|
|
37
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` |
|
|
38
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` |
|
|
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 |
|
|
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 |
|
|
@@ -53,7 +57,7 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
53
57
|
### `choose`
|
|
54
58
|
|
|
55
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`.
|
|
56
|
-
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.
|
|
57
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).
|
|
58
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.
|
|
59
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.
|
|
@@ -85,7 +89,7 @@ Kết quả `--json`: `{"action":"dispatch","model":"sonnet","model_id":"claude-
|
|
|
85
89
|
|
|
86
90
|
## Dữ liệu
|
|
87
91
|
|
|
88
|
-
- 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`.
|
|
89
93
|
- Lỗi của hook: `~/.claude/dieu-phoi/hook-errors.log`.
|
|
90
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).
|
|
91
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
|
@@ -13,10 +13,14 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
13
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
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
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> [--verify tests|lint|review|judgment|none] [--reason short-kebab]
|
|
17
|
-
nghiệm thu tầng agent (by=agent)
|
|
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)
|
|
18
18
|
verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n] [--reason R]
|
|
19
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)
|
|
20
24
|
steer <agent_id> --kind minor|plan|redirect [--reason R]
|
|
21
25
|
steer --task <task_id> --kind minor|plan|redirect [--reason R]
|
|
22
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)
|
|
@@ -68,6 +72,7 @@ async function main(argv) {
|
|
|
68
72
|
start: c.cmdStart,
|
|
69
73
|
verdict: c.cmdVerdict,
|
|
70
74
|
steer: c.cmdSteer,
|
|
75
|
+
discard: c.cmdDiscard,
|
|
71
76
|
'plan-table': c.cmdPlanTable,
|
|
72
77
|
'new-id': c.cmdNewId,
|
|
73
78
|
stats: c.cmdStats,
|
package/hook.cjs
CHANGED
|
@@ -20,7 +20,7 @@ const path = require('path');
|
|
|
20
20
|
const os = require('os');
|
|
21
21
|
const crypto = require('crypto');
|
|
22
22
|
|
|
23
|
-
const CORE_VERSION = '0.
|
|
23
|
+
const CORE_VERSION = '0.4.0';
|
|
24
24
|
const SCHEMA = 1;
|
|
25
25
|
const TASK_TYPES = [
|
|
26
26
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -737,10 +737,30 @@ function findSessionTranscript(claudeDir, sessionId) {
|
|
|
737
737
|
|
|
738
738
|
// ------------------------------------------------------------------ events (append-only)
|
|
739
739
|
|
|
740
|
-
/**
|
|
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
|
+
*/
|
|
741
748
|
function appendEvent(ctx, ev) {
|
|
742
749
|
fs.mkdirSync(ctx.paths.data, { recursive: true });
|
|
743
|
-
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
|
+
}
|
|
744
764
|
}
|
|
745
765
|
|
|
746
766
|
function readEventsBuffer(ctx) {
|
|
@@ -859,7 +879,10 @@ function verdictLayer(ev) {
|
|
|
859
879
|
const by = ev.by === 'user' ? 'user' : 'agent';
|
|
860
880
|
let cause = ev.cause || null;
|
|
861
881
|
if (by === 'user' && ev.outcome === 'reject' && cause !== 'scope') cause = 'quality';
|
|
862
|
-
|
|
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 };
|
|
863
886
|
}
|
|
864
887
|
|
|
865
888
|
function dispatchInSession(ev, sessionId) {
|
|
@@ -893,7 +916,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
893
916
|
const runs = toPosix(o.runsDir || runsDir());
|
|
894
917
|
const taskIds = [];
|
|
895
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));
|
|
896
920
|
for (const ev of events) {
|
|
921
|
+
if (ev && discarded.has(ev.task_id)) continue;
|
|
897
922
|
if (dispatchInSession(ev, sessionId) && typeof ev.task_id === 'string' && ev.task_id && !seenTask.has(ev.task_id)) {
|
|
898
923
|
seenTask.add(ev.task_id);
|
|
899
924
|
taskIds.push(ev.task_id);
|
|
@@ -908,7 +933,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
908
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 });
|
|
909
934
|
}
|
|
910
935
|
for (const id of [...planOfRoot.keys(), ...stepOf.keys()]) {
|
|
911
|
-
if (!seenTask.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
936
|
+
if (!seenTask.has(id) && !discarded.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
912
937
|
}
|
|
913
938
|
const per = new Map(taskIds.map((id) => [id, { dispatches: [], user: [], steers: [], overrides: [], agentIds: new Set() }]));
|
|
914
939
|
events.forEach((ev, i) => {
|
|
@@ -925,7 +950,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
925
950
|
const agentVerdicts = new Map();
|
|
926
951
|
for (const ev of events) {
|
|
927
952
|
if (!ev || typeof ev !== 'object') continue;
|
|
928
|
-
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));
|
|
929
954
|
if (ev.kind === 'verdict') {
|
|
930
955
|
const v = verdictLayer(ev);
|
|
931
956
|
if (v.by === 'user') {
|
|
@@ -970,7 +995,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
970
995
|
const vs = (agentVerdicts.get(d.agent_id) || []).slice().sort(byTime);
|
|
971
996
|
const last = vs[vs.length - 1];
|
|
972
997
|
let status;
|
|
973
|
-
|
|
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;
|
|
974
1001
|
else status = completes.get(d.agent_id) ? 'done' : 'running';
|
|
975
1002
|
return {
|
|
976
1003
|
agent_id: d.agent_id, role: d.role || 'exec', try: d.try === undefined ? null : d.try,
|
|
@@ -1209,6 +1236,32 @@ function scoutBriefPath(ctx) {
|
|
|
1209
1236
|
return fs.existsSync(f) ? toPosix(f) : null;
|
|
1210
1237
|
}
|
|
1211
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
|
+
|
|
1212
1265
|
/** Local, network-free health: config, profile, run dir, cached sink status. */
|
|
1213
1266
|
function quickHealth(ctx, runs) {
|
|
1214
1267
|
const problems = [];
|
|
@@ -1256,7 +1309,15 @@ function stepIds(root, n) {
|
|
|
1256
1309
|
function startInfo(ctx) {
|
|
1257
1310
|
const run = startRun();
|
|
1258
1311
|
const health = quickHealth(ctx, run.runs);
|
|
1259
|
-
return {
|
|
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}`)) : [];
|
|
1260
1321
|
}
|
|
1261
1322
|
|
|
1262
1323
|
function formatStartBlock(info) {
|
|
@@ -1267,6 +1328,7 @@ function formatStartBlock(info) {
|
|
|
1267
1328
|
`step_ids: ${info.step_ids.join(' ')}`,
|
|
1268
1329
|
`health: ${healthLine(info.health)}`,
|
|
1269
1330
|
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
1331
|
+
...orchLessonLines(info),
|
|
1270
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.',
|
|
1271
1333
|
].join('\n');
|
|
1272
1334
|
}
|
|
@@ -1425,9 +1487,50 @@ function handleSubagentStop(p, ctx) {
|
|
|
1425
1487
|
});
|
|
1426
1488
|
}
|
|
1427
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
|
+
}
|
|
1428
1499
|
return ev;
|
|
1429
1500
|
}
|
|
1430
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
|
+
|
|
1431
1534
|
function maybeSpawnSync(ctx) {
|
|
1432
1535
|
if (!ctx.config.sink_url || !ctx.config.sink_token) return false;
|
|
1433
1536
|
if (process.env.DIEU_PHOI_NO_SPAWN === '1') return false;
|
|
@@ -1632,7 +1735,7 @@ async function runSync(ctx, opts) {
|
|
|
1632
1735
|
// ------------------------------------------------------------------ exports / main
|
|
1633
1736
|
|
|
1634
1737
|
module.exports = {
|
|
1635
|
-
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, START_PREFIX, 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,
|
|
1636
1739
|
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED,
|
|
1637
1740
|
sha8, nowIso, readJsonFile, writeFileAtomic, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
1638
1741
|
logError, computeUserHash, userHash, commonFields, parseTag, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|
package/lib/aggregate.cjs
CHANGED
|
@@ -9,6 +9,12 @@ const core = require('../hook.cjs');
|
|
|
9
9
|
const MIN_N = 5;
|
|
10
10
|
const FAIL_OUTCOMES = new Set(['fail', 'timeout']);
|
|
11
11
|
const HEAVY_STEERS = new Set(['plan', 'redirect']);
|
|
12
|
+
// core 0.4.0: an agent stopped by a usage-limit/quota pause is `aborted` — neither pass nor fail. Legacy
|
|
13
|
+
// rows recorded such pauses as `timeout` with a quota-ish reason_code; they are read as `aborted` too.
|
|
14
|
+
const ABORTED = 'aborted';
|
|
15
|
+
// Exact markers only (anchored): `quota-pause`, `usage-limit`, the hook's `auto-*` codes and the legacy
|
|
16
|
+
// `paused-for-quota-*` codes the orchestrator wrote before 0.4.0. A reason that merely mentions a rate limit stays a timeout.
|
|
17
|
+
const QUOTA_REASON_RE = /^(?:quota-pause|usage-limit|auto-quota-pause|auto-usage-limit|paused-for-quota)(?:-|$)/;
|
|
12
18
|
|
|
13
19
|
function tsOf(ev) {
|
|
14
20
|
const t = Date.parse(ev && ev.ts);
|
|
@@ -56,7 +62,33 @@ function normalizeVerdict(ev) {
|
|
|
56
62
|
const by = ev.by === 'user' ? 'user' : 'agent';
|
|
57
63
|
let cause = ev.cause || null;
|
|
58
64
|
if (by === 'user' && ev.outcome === 'reject' && cause !== 'scope') cause = 'quality';
|
|
59
|
-
|
|
65
|
+
let outcome = ev.outcome;
|
|
66
|
+
if (by === 'agent' && outcome === 'timeout' && typeof ev.reason_code === 'string' && QUOTA_REASON_RE.test(ev.reason_code)) {
|
|
67
|
+
outcome = ABORTED;
|
|
68
|
+
}
|
|
69
|
+
return { by, outcome, cause, round: Number(ev.round) || null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Task ids withdrawn with `dieu-phoi discard --task` (kind=discard). Every event of such a task — and every
|
|
74
|
+
* event of an agent dispatched for it — is left out of tasks, cells and steers (CONTRACT "Sự kiện cục bộ").
|
|
75
|
+
*/
|
|
76
|
+
function discardedTaskIds(events) {
|
|
77
|
+
const out = new Set();
|
|
78
|
+
for (const ev of events) if (ev && ev.kind === 'discard' && typeof ev.task_id === 'string' && ev.task_id) out.add(ev.task_id);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Events with discarded tasks (and the discard records themselves) replaced by null; order kept. */
|
|
83
|
+
function withoutDiscarded(events) {
|
|
84
|
+
const gone = discardedTaskIds(events);
|
|
85
|
+
const agents = new Set(events.filter((ev) => ev && ev.kind === 'dispatch' && gone.has(ev.task_id)).map((ev) => ev.agent_id));
|
|
86
|
+
return events.map((ev) => {
|
|
87
|
+
if (!ev || typeof ev !== 'object' || ev.kind === 'discard') return null;
|
|
88
|
+
if (gone.has(ev.task_id) || agents.has(ev.agent_id)) return null;
|
|
89
|
+
if (typeof ev.agent_id === 'string' && ev.agent_id.startsWith('task:') && gone.has(ev.agent_id.slice(5))) return null;
|
|
90
|
+
return ev;
|
|
91
|
+
});
|
|
60
92
|
}
|
|
61
93
|
|
|
62
94
|
function blank(v) {
|
|
@@ -111,7 +143,9 @@ function buildAgents(events) {
|
|
|
111
143
|
const d = a.dispatch || {};
|
|
112
144
|
const c0 = a.completes[0] || {};
|
|
113
145
|
const cl = a.completes[a.completes.length - 1] || {};
|
|
114
|
-
const
|
|
146
|
+
const vAll = a.verdicts.slice().sort((x, y) => tsOf(x) - tsOf(y) || x._idx - y._idx);
|
|
147
|
+
// `aborted` (quota pause) is neither pass nor fail: it never sets the outcome nor counts as a result.
|
|
148
|
+
const v = vAll.filter((x) => x.outcome !== ABORTED);
|
|
115
149
|
const verdict = v.length ? v[v.length - 1] : null;
|
|
116
150
|
const firstVerdictTs = v.length ? tsOf(v[0]) : Infinity;
|
|
117
151
|
const pick = (k) => (d[k] !== undefined && d[k] !== null ? d[k] : (c0[k] !== undefined && c0[k] !== null ? c0[k] : null));
|
|
@@ -137,6 +171,9 @@ function buildAgents(events) {
|
|
|
137
171
|
tool_uses: a.completes.length ? sumOrNull(a.completes.map((x) => x.tool_uses)) : null,
|
|
138
172
|
outcome: verdict ? verdict.outcome : null,
|
|
139
173
|
first_outcome: v.length ? v[0].outcome : null,
|
|
174
|
+
// every non-aborted agent verdict in time order: a retry by SendMessage to the same agent is a new result
|
|
175
|
+
verdict_seq: v.map((x) => ({ outcome: x.outcome, ts: tsOf(x), idx: x._idx })),
|
|
176
|
+
aborted: vAll.length > 0 && vAll[vAll.length - 1].outcome === ABORTED,
|
|
140
177
|
verify: verdict ? verdict.verify : null,
|
|
141
178
|
reason_code: verdict ? verdict.reason_code : null,
|
|
142
179
|
steers: a.steers.map((s) => s.steer_kind).filter(Boolean),
|
|
@@ -175,16 +212,25 @@ function buildTasks(agents, userVerdicts) {
|
|
|
175
212
|
const first = t.agents.filter((a) => a.role === 'exec').sort(byTry)[0] || null;
|
|
176
213
|
const user = t.user.slice().sort((x, y) => ((x.round || 0) - (y.round || 0)) || (tsOf(x) - tsOf(y)) || (x._idx - y._idx));
|
|
177
214
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
215
|
+
// Results for escalation. Each agent verdict of an attempt is one result at its tier (a retry by
|
|
216
|
+
// SendMessage to the same agent is a new result); a user verdict adds a result to the latest attempt
|
|
217
|
+
// dispatched before it (reject/quality = fail, accept = pass). eff_outcome = the attempt's latest result.
|
|
218
|
+
for (const a of attempts) {
|
|
219
|
+
a.results = (a.verdict_seq || [])
|
|
220
|
+
.filter((x) => x.outcome === 'pass' || FAIL_OUTCOMES.has(x.outcome))
|
|
221
|
+
.map((x) => ({ outcome: x.outcome === 'pass' ? 'pass' : 'fail', ts: x.ts, idx: x.idx }));
|
|
222
|
+
a.eff_outcome = a.results.length ? a.results[a.results.length - 1].outcome : null;
|
|
223
|
+
}
|
|
181
224
|
for (const v of user) {
|
|
182
225
|
if (v.cause === 'scope') continue;
|
|
183
226
|
const vt = tsOf(v);
|
|
184
227
|
let target = null;
|
|
185
228
|
for (const a of attempts) if (a.ts <= vt && (!target || a.ts >= target.ts)) target = a;
|
|
186
229
|
if (!target) target = attempts[attempts.length - 1] || null;
|
|
187
|
-
if (target)
|
|
230
|
+
if (target) {
|
|
231
|
+
target.eff_outcome = v.outcome === 'accept' ? 'pass' : 'fail';
|
|
232
|
+
target.results.push({ outcome: target.eff_outcome, ts: vt, idx: v._idx, user: true });
|
|
233
|
+
}
|
|
188
234
|
}
|
|
189
235
|
|
|
190
236
|
const firstUser = user[0] || null;
|
|
@@ -307,13 +353,19 @@ function canonicalCells(cells) {
|
|
|
307
353
|
return cells.map((c) => Object.fromEntries(CANONICAL_CELL_FIELDS.map((k) => [k, c[k] === undefined ? null : c[k]])));
|
|
308
354
|
}
|
|
309
355
|
|
|
310
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Steering per task_type, counted in distinct user actions (steer_id), not events. Core 0.4.0: only
|
|
358
|
+
* exec-phase steers count — a steer on a role=exec agent, or a task-level steer on a task that has an exec
|
|
359
|
+
* attempt. Steers on scouts/planners (gate-1 Q&A) and on verifiers go to `plan_phase_actions` and never
|
|
360
|
+
* touch actions / steer_rate.
|
|
361
|
+
*/
|
|
311
362
|
function buildSteers(tasks, steers, agents) {
|
|
312
363
|
const typeOfTask = new Map(tasks.map((t) => [t.task_id, t.task_type]));
|
|
364
|
+
const execTask = new Set(tasks.filter((t) => t.first && t.task_id).map((t) => t.task_id));
|
|
313
365
|
const agentById = new Map(agents.map((a) => [a.agent_id, a]));
|
|
314
366
|
const by = {};
|
|
315
367
|
const row = (type) => by[type] || (by[type] = {
|
|
316
|
-
task_type: type, tasks: 0, steered_tasks: 0, actions: 0, minor: 0, plan: 0, redirect: 0, _tasks: new Set(), _ids: new Set(),
|
|
368
|
+
task_type: type, tasks: 0, steered_tasks: 0, actions: 0, minor: 0, plan: 0, redirect: 0, _tasks: new Set(), _ids: new Set(), _plan: new Set(),
|
|
317
369
|
});
|
|
318
370
|
for (const t of tasks) if (t.first) row(t.task_type).tasks++;
|
|
319
371
|
for (const s of steers) {
|
|
@@ -322,6 +374,8 @@ function buildSteers(tasks, steers, agents) {
|
|
|
322
374
|
const type = (taskId && typeOfTask.get(taskId)) || s.task_type || (a && a.task_type) || 'untagged';
|
|
323
375
|
const r = row(type);
|
|
324
376
|
const id = steerIdOf(s);
|
|
377
|
+
const execPhase = a ? a.role === 'exec' : !(taskId && typeOfTask.has(taskId) && !execTask.has(taskId));
|
|
378
|
+
if (!execPhase) { r._plan.add(id); continue; }
|
|
325
379
|
if (r._ids.has(id)) continue;
|
|
326
380
|
r._ids.add(id);
|
|
327
381
|
r.actions++;
|
|
@@ -334,6 +388,7 @@ function buildSteers(tasks, steers, agents) {
|
|
|
334
388
|
task_type: r.task_type, tasks: r.tasks, steered_tasks: steered, actions: r.actions,
|
|
335
389
|
minor: r.minor, plan: r.plan, redirect: r.redirect,
|
|
336
390
|
steer_rate: r.tasks ? round6(Math.min(1, steered / r.tasks)) : 0,
|
|
391
|
+
plan_phase_actions: Array.from(r._plan).filter((x) => !r._ids.has(x)).length,
|
|
337
392
|
};
|
|
338
393
|
return outRow;
|
|
339
394
|
}).sort((a, b) => b.steer_rate - a.steer_rate || a.task_type.localeCompare(b.task_type));
|
|
@@ -345,7 +400,8 @@ function finiteNum(v) {
|
|
|
345
400
|
|
|
346
401
|
/**
|
|
347
402
|
* Orchestration cost (CONTRACT "Thước đo" mục 6). Sessions = distinct non-empty `session_id` over
|
|
348
|
-
* dispatch events. Per session: ctx = max orch_ctx_tokens, usd = max orch_cum_usd
|
|
403
|
+
* dispatch events. Per session: ctx = max orch_ctx_tokens, usd = max orch_cum_usd minus orch_cum_usd of the
|
|
404
|
+
* session's first dispatch carrying a number (core 0.4.0; never negative), growth = max of
|
|
349
405
|
* max(0, orch_ctx_tokens - orch_ctx_baseline) over dispatches carrying both, comp = max orch_compactions
|
|
350
406
|
* (numbers only). compactions_total = sum of per-session comp, compactions_max = max.
|
|
351
407
|
* Each agent belongs to the session of its FIRST dispatch (file order); sub_usd_total sums the
|
|
@@ -357,11 +413,14 @@ function orchestrationSummary(events) {
|
|
|
357
413
|
for (const ev of events) {
|
|
358
414
|
if (!ev || ev.kind !== 'dispatch' || typeof ev.session_id !== 'string' || !ev.session_id) continue;
|
|
359
415
|
const sid = ev.session_id;
|
|
360
|
-
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, growth: null, comp: null });
|
|
416
|
+
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, base: null, growth: null, comp: null });
|
|
361
417
|
const s = sessions.get(sid);
|
|
362
418
|
const c = finiteNum(ev.orch_ctx_tokens);
|
|
363
419
|
const u = finiteNum(ev.orch_cum_usd);
|
|
364
420
|
if (c !== null && (s.ctx === null || c > s.ctx)) s.ctx = c;
|
|
421
|
+
// core 0.4.0: orchestrator cost counts from the session's FIRST dispatch carrying a number (file order),
|
|
422
|
+
// not from session start — whatever the session did before it started dispatching is not orchestration.
|
|
423
|
+
if (u !== null && s.base === null) s.base = u;
|
|
365
424
|
if (u !== null && (s.usd === null || u > s.usd)) s.usd = u;
|
|
366
425
|
const k = finiteNum(ev.orch_compactions);
|
|
367
426
|
if (k !== null && (s.comp === null || k > s.comp)) s.comp = k;
|
|
@@ -386,7 +445,7 @@ function orchestrationSummary(events) {
|
|
|
386
445
|
if (s.ctx !== null) ctxs.push(s.ctx);
|
|
387
446
|
if (s.growth !== null) growths.push(s.growth);
|
|
388
447
|
if (s.comp !== null) comps.push(s.comp);
|
|
389
|
-
if (s.usd !== null) orch += s.usd;
|
|
448
|
+
if (s.usd !== null) orch += Math.max(0, s.usd - s.base);
|
|
390
449
|
}
|
|
391
450
|
return {
|
|
392
451
|
sessions: sessions.size,
|
|
@@ -403,7 +462,7 @@ function orchestrationSummary(events) {
|
|
|
403
462
|
}
|
|
404
463
|
|
|
405
464
|
function analyze(events, opts) {
|
|
406
|
-
const { agents, userVerdicts, overrides, steers } = buildAgents(events);
|
|
465
|
+
const { agents, userVerdicts, overrides, steers } = buildAgents(withoutDiscarded(events));
|
|
407
466
|
const tasks = buildTasks(agents, userVerdicts);
|
|
408
467
|
const cells = buildCells(tasks, opts);
|
|
409
468
|
return {
|
|
@@ -413,6 +472,6 @@ function analyze(events, opts) {
|
|
|
413
472
|
}
|
|
414
473
|
|
|
415
474
|
module.exports = {
|
|
416
|
-
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
475
|
+
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, ABORTED, QUOTA_REASON_RE, discardedTaskIds, withoutDiscarded, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
417
476
|
buildCells, buildSteers, canonicalCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
418
477
|
};
|
package/lib/choose.cjs
CHANGED
|
@@ -19,7 +19,8 @@ const LADDER = [
|
|
|
19
19
|
const TOP = LADDER.length - 1;
|
|
20
20
|
const EFFORT_RANK = { low: 0, medium: 1, high: 2, xhigh: 3, max: 4 };
|
|
21
21
|
|
|
22
|
-
// Built-in defaults = skill/profile/profile.json (
|
|
22
|
+
// Built-in defaults = skill/profile/profile.json for the fixed task types (time caps as tuned 2026-10-12: feature-impl 80,
|
|
23
|
+
// content-write 40, debug 55 — the old ones sat below the observed p50); used only when no profile is found.
|
|
23
24
|
const DEFAULT_PRIORS = {
|
|
24
25
|
search: 'haiku/medium',
|
|
25
26
|
extract: 'haiku/medium',
|
|
@@ -38,13 +39,13 @@ const DEFAULT_TIME_CAP_MIN = {
|
|
|
38
39
|
search: 3,
|
|
39
40
|
extract: 5,
|
|
40
41
|
'mechanical-edit': 10,
|
|
41
|
-
'feature-impl':
|
|
42
|
-
debug:
|
|
42
|
+
'feature-impl': 80,
|
|
43
|
+
debug: 55,
|
|
43
44
|
refactor: 30,
|
|
44
45
|
'test-write': 15,
|
|
45
46
|
review: 10,
|
|
46
47
|
'research-synth': 20,
|
|
47
|
-
'content-write':
|
|
48
|
+
'content-write': 40,
|
|
48
49
|
'plan-design': 15,
|
|
49
50
|
'data-analysis': 15,
|
|
50
51
|
};
|
|
@@ -187,25 +188,35 @@ const OPUS_MEDIUM = 5;
|
|
|
187
188
|
* - one fail at a tier -> same tier again (retry with the error);
|
|
188
189
|
* - two fails at the same tier -> next tier up;
|
|
189
190
|
* - if that next tier would exceed the opus/medium ceiling -> ask the user (no auto re-plan).
|
|
190
|
-
* Fail = agent fail|timeout or user reject cause=quality
|
|
191
|
-
*
|
|
191
|
+
* Fail = agent fail|timeout or user reject cause=quality. Core 0.4.0: every result counts, so a retry by
|
|
192
|
+
* SendMessage to the same agent (fail, then fail again) is two fails at that tier; `aborted` (quota pause)
|
|
193
|
+
* is never a result. Attempts steered plan/redirect by the user are ignored.
|
|
192
194
|
*/
|
|
193
195
|
function escalation(task, role) {
|
|
194
196
|
const none = { floor: 0, ask: false, note: null, fails: 0, tried: [] };
|
|
195
197
|
if (!task) return none;
|
|
196
198
|
const att = task.attempts
|
|
197
|
-
.filter((a) => a.
|
|
198
|
-
.map((a) => ({
|
|
199
|
-
|
|
199
|
+
.filter((a) => a.role !== 'verify' && !a.steered)
|
|
200
|
+
.map((a) => ({
|
|
201
|
+
a,
|
|
202
|
+
i: tierIndex(a.model, a.effort),
|
|
203
|
+
results: Array.isArray(a.results) ? a.results : (a.eff_outcome ? [{ outcome: a.eff_outcome, ts: a.ts, idx: 0 }] : []),
|
|
204
|
+
}))
|
|
205
|
+
.filter((x) => x.i !== null && x.results.length)
|
|
200
206
|
.sort((x, y) => (x.a.try - y.a.try) || (x.a.ts - y.a.ts));
|
|
201
207
|
if (!att.length) return none;
|
|
208
|
+
const res = [];
|
|
209
|
+
for (const x of att) {
|
|
210
|
+
const rs = x.results.slice().sort((p, q) => (p.ts - q.ts) || ((p.idx || 0) - (q.idx || 0)));
|
|
211
|
+
for (const r of rs) res.push({ i: x.i, outcome: r.outcome });
|
|
212
|
+
}
|
|
202
213
|
const tried = Array.from(new Set(att.map((x) => tierName(x.i))));
|
|
203
|
-
const totalFails =
|
|
204
|
-
const last =
|
|
214
|
+
const totalFails = res.filter((r) => r.outcome === 'fail').length;
|
|
215
|
+
const last = res[res.length - 1];
|
|
205
216
|
const cur = last.i;
|
|
206
217
|
const base = { tried, fails: totalFails, ask: false };
|
|
207
|
-
if (last.
|
|
208
|
-
const fails =
|
|
218
|
+
if (last.outcome !== 'fail') return Object.assign(base, { floor: cur, note: null });
|
|
219
|
+
const fails = res.filter((r) => r.i === cur && r.outcome === 'fail').length;
|
|
209
220
|
if (fails < 2) {
|
|
210
221
|
return Object.assign(base, { floor: cur, note: `task fail 1 lần ở ${tierName(cur)} → làm lại cùng tier kèm lỗi` });
|
|
211
222
|
}
|
|
@@ -287,6 +298,40 @@ function estimateFor(idx, cell, pricing) {
|
|
|
287
298
|
return roughEstimate(idx, pricing);
|
|
288
299
|
}
|
|
289
300
|
|
|
301
|
+
// ------------------------------------------------------------------ task type guard
|
|
302
|
+
|
|
303
|
+
// Ad-hoc labels seen in real logs → the closest fixed type (core 0.4.0). A label maps only when its target
|
|
304
|
+
// is known (fixed list or the profile's extra_task_types), e.g. art-fix → art-gen needs art-gen in the profile.
|
|
305
|
+
const TYPE_ALIASES = {
|
|
306
|
+
'bug-fix': 'debug', bugfix: 'debug', fix: 'debug', bug: 'debug',
|
|
307
|
+
config: 'mechanical-edit', shell: 'mechanical-edit', deploy: 'mechanical-edit', ops: 'mechanical-edit', edit: 'mechanical-edit',
|
|
308
|
+
docs: 'content-write', doc: 'content-write', documentation: 'content-write', writing: 'content-write', copy: 'content-write',
|
|
309
|
+
design: 'plan-design', plan: 'plan-design', planning: 'plan-design', architecture: 'plan-design',
|
|
310
|
+
research: 'research-synth', synth: 'research-synth',
|
|
311
|
+
test: 'test-write', tests: 'test-write',
|
|
312
|
+
analysis: 'data-analysis', data: 'data-analysis',
|
|
313
|
+
impl: 'feature-impl', feature: 'feature-impl', implement: 'feature-impl',
|
|
314
|
+
'code-review': 'review',
|
|
315
|
+
'art-fix': 'art-gen', art: 'art-gen',
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* { type, known, mapped_from, warning }: a fixed/extra type passes; an ad-hoc label with a known alias target
|
|
320
|
+
* maps to it (with a warning); anything else stays unknown (choose asks the user to pick a fixed type).
|
|
321
|
+
*/
|
|
322
|
+
function resolveTaskType(raw, profile) {
|
|
323
|
+
const t = String(raw || '').toLowerCase();
|
|
324
|
+
const extra = (profile && profile.extra_task_types) || [];
|
|
325
|
+
const isKnown = (x) => core.TASK_TYPES.includes(x) || extra.includes(x);
|
|
326
|
+
if (isKnown(t)) return { type: t, known: true, mapped_from: null, warning: null };
|
|
327
|
+
const target = TYPE_ALIASES[t];
|
|
328
|
+
if (target && isKnown(target)) {
|
|
329
|
+
return { type: target, known: true, mapped_from: t, warning: `loại "${t}" không cố định → dùng "${target}" (ghi --type ${target} trong thẻ)` };
|
|
330
|
+
}
|
|
331
|
+
const list = core.TASK_TYPES.concat(extra).join(', ');
|
|
332
|
+
return { type: t, known: false, mapped_from: null, warning: `loại "${t}" không có trong danh sách cố định (${list}) — chọn loại gần nhất hoặc hỏi người dùng` };
|
|
333
|
+
}
|
|
334
|
+
|
|
290
335
|
// ------------------------------------------------------------------ choose
|
|
291
336
|
|
|
292
337
|
/**
|
|
@@ -296,8 +341,8 @@ function estimateFor(idx, cell, pricing) {
|
|
|
296
341
|
* Throws when an override goes below a safety floor or off the ladder.
|
|
297
342
|
*/
|
|
298
343
|
function choose(opts) {
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
344
|
+
const rawType = String(opts.type || '').toLowerCase();
|
|
345
|
+
if (!rawType) throw new Error('Thiếu --type');
|
|
301
346
|
const score = Number(opts.score);
|
|
302
347
|
if (opts.score === undefined || opts.score === null || opts.score === true || !isFinite(score)
|
|
303
348
|
|| score < 0 || score > 12) throw new Error('--score phải trong 0–12');
|
|
@@ -327,12 +372,15 @@ function choose(opts) {
|
|
|
327
372
|
const notes = [];
|
|
328
373
|
|
|
329
374
|
const analysis = agg.analyze(opts.events || [], { lambda });
|
|
330
|
-
const
|
|
375
|
+
const rt = resolveTaskType(rawType, profile);
|
|
376
|
+
const type = rt.type;
|
|
377
|
+
const known = rt.known;
|
|
378
|
+
if (rt.warning) notes.push(rt.warning);
|
|
331
379
|
let prior = profile.priors[type];
|
|
332
380
|
let priorWhy = 'prior';
|
|
333
381
|
if (prior === undefined || prior === null) {
|
|
334
382
|
prior = OPUS_MEDIUM;
|
|
335
|
-
notes.push(
|
|
383
|
+
if (known) notes.push('không có prior, dùng opus/medium');
|
|
336
384
|
}
|
|
337
385
|
const ov = overridePrior(analysis.overrides, type);
|
|
338
386
|
if (ov && ov.tier !== prior) {
|
|
@@ -382,10 +430,17 @@ function choose(opts) {
|
|
|
382
430
|
explored,
|
|
383
431
|
tier: tierName(idx),
|
|
384
432
|
role,
|
|
433
|
+
task_type: type,
|
|
385
434
|
budget_s: budgetFor(cell),
|
|
386
|
-
}, estimateFor(idx, cell, opts.pricing), { profile_source: src },
|
|
435
|
+
}, estimateFor(idx, cell, opts.pricing), { profile_source: src },
|
|
436
|
+
rt.mapped_from ? { mapped_from: rt.mapped_from } : {}, rt.warning ? { type_warning: rt.warning } : {}, extra || {});
|
|
387
437
|
};
|
|
388
438
|
|
|
439
|
+
// Unknown task type (not fixed, not in the profile, no alias): its stats would never pool — ask first.
|
|
440
|
+
if (!known) {
|
|
441
|
+
return finish(Math.max(prior, safetyFloor), rt.warning, false, { action: 'ask_user', unknown_type: true });
|
|
442
|
+
}
|
|
443
|
+
|
|
389
444
|
if (overrideIdx !== null && overrideIdx < safetyFloor) {
|
|
390
445
|
throw new Error(`Từ chối override ${tierName(overrideIdx)}: dưới sàn an toàn ${tierName(safetyFloor)} (${floorWhy.join(', ')}).`);
|
|
391
446
|
}
|
|
@@ -474,4 +529,5 @@ module.exports = {
|
|
|
474
529
|
DEFAULT_ORCH_CTX_SOFT_CAP,
|
|
475
530
|
LADDER, DEFAULT_PRIORS, DEFAULT_LAMBDA, DEFAULT_TIME_CAP_MIN, ROUGH, tierIndex, tierName, parsePrior,
|
|
476
531
|
parseTier, loadProfile, makeRng, betaSample, escalation, overridePrior, roughEstimate, choose, shortModel,
|
|
532
|
+
TYPE_ALIASES, resolveTaskType,
|
|
477
533
|
};
|
package/lib/commands.cjs
CHANGED
|
@@ -10,7 +10,7 @@ const chooser = require('./choose.cjs');
|
|
|
10
10
|
const setupLib = require('./setup.cjs');
|
|
11
11
|
const { claudeDirOf, ctxOf, yesNo } = require('./args.cjs');
|
|
12
12
|
|
|
13
|
-
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout'];
|
|
13
|
+
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
|
|
14
14
|
const USER_OUTCOMES = ['accept', 'reject'];
|
|
15
15
|
const CAUSES = ['quality', 'scope'];
|
|
16
16
|
const STEER_KINDS = ['minor', 'plan', 'redirect'];
|
|
@@ -145,6 +145,15 @@ async function cmdDoctor(opts) {
|
|
|
145
145
|
add(!badIds.length, 'task-ids', badIds.length
|
|
146
146
|
? `${badIds.length} dispatch trong ${INVALID_ID_WINDOW_DAYS} ngày gần đây có task_id sai định dạng (${Array.from(new Set(badIds.map((e) => e.task_id))).slice(0, 3).map((x) => `"${x}"`).join(', ')}) — task_id phải đúng 8 ký tự a-z0-9, lấy bằng \`dieu-phoi new-id\``
|
|
147
147
|
: `mọi dispatch trong ${INVALID_ID_WINDOW_DAYS} ngày gần đây có task_id đúng định dạng`, 'CẢNH BÁO');
|
|
148
|
+
const tmp = sweepStaleTmp(ctx);
|
|
149
|
+
add(!tmp.left.length, 'tmp-files', tmp.left.length
|
|
150
|
+
? `${tmp.left.length} file tạm còn mới (đang ghi?): ${tmp.left.slice(0, 3).join(', ')}`
|
|
151
|
+
: `không còn file *.tmp-* bỏ dở${tmp.removed ? ` (đã xoá ${tmp.removed} file cũ hơn 1 giờ)` : ''}`, 'CẢNH BÁO');
|
|
152
|
+
let badPlans = [];
|
|
153
|
+
try { badPlans = fs.readdirSync(ctx.paths.plans).filter((n) => n.endsWith('.json') && !core.isValidTaskId(n.slice(0, -5))); } catch (_) { /* none */ }
|
|
154
|
+
add(!badPlans.length, 'plans', badPlans.length
|
|
155
|
+
? `${badPlans.length} kế hoạch có root id sai định dạng (${badPlans.slice(0, 3).join(', ')}) — \`state\` vẫn đọc; loại task bằng \`dieu-phoi discard --task <id>\` hoặc xoá file trong ${ctx.paths.plans}`
|
|
156
|
+
: 'mọi kế hoạch đã duyệt có root id đúng định dạng', 'CẢNH BÁO');
|
|
148
157
|
const orch = measureOrch(opts, ctx, profileOrNull(opts, ctx));
|
|
149
158
|
if (orch.ctx_tokens !== null) {
|
|
150
159
|
add(!orch.over, 'orch-ctx', `session ${orch.session_id}: ${orch.ctx_tokens} token, tăng ${orch.ctx_growth}/${orch.soft_cap}, ≈ ${fmtUsd(orch.cum_usd)} — ${orch.advice}`, 'CẢNH BÁO');
|
|
@@ -166,6 +175,33 @@ async function cmdDoctor(opts) {
|
|
|
166
175
|
return { code: failed ? 1 : 0, out };
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
const TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
|
|
179
|
+
const TMP_STALE_MS = 3600 * 1000;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Leftovers of writeFileAtomic (tmp written, rename never happened: process killed, or rename lost a race on
|
|
183
|
+
* Windows) under data/, data/orch, data/segments, data/plans. Older than 1 h → removed; newer → reported.
|
|
184
|
+
*/
|
|
185
|
+
function sweepStaleTmp(ctx) {
|
|
186
|
+
let removed = 0;
|
|
187
|
+
const left = [];
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans]) {
|
|
190
|
+
let names = [];
|
|
191
|
+
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
192
|
+
for (const n of names) {
|
|
193
|
+
if (!TMP_RE.test(n)) continue;
|
|
194
|
+
const f = path.join(dir, n);
|
|
195
|
+
try {
|
|
196
|
+
const st = fs.statSync(f);
|
|
197
|
+
if (!st.isFile()) continue; // a directory (or anything else) with a tmp-like name: not ours, leave it alone
|
|
198
|
+
if (now - st.mtimeMs > TMP_STALE_MS) { fs.unlinkSync(f); removed++; } else left.push(n);
|
|
199
|
+
} catch (_) { left.push(n); }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { removed, left };
|
|
203
|
+
}
|
|
204
|
+
|
|
169
205
|
/** Live sink check (view=stats); result cached in data/sink-check.json for 24 h. */
|
|
170
206
|
async function checkSinkLive(ctx) {
|
|
171
207
|
let ok = false;
|
|
@@ -211,6 +247,7 @@ function cmdStart(opts) {
|
|
|
211
247
|
`step_ids: ${info.step_ids.join(' ')}`,
|
|
212
248
|
`health: ${core.healthLine(h)}`,
|
|
213
249
|
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
250
|
+
...core.orchLessonLines(info),
|
|
214
251
|
];
|
|
215
252
|
return { code: 0, out: out.join('\n') };
|
|
216
253
|
}
|
|
@@ -248,7 +285,10 @@ function resolveAgentId(events, ref, now) {
|
|
|
248
285
|
const verdicted = new Set();
|
|
249
286
|
events.forEach((e, i) => {
|
|
250
287
|
if (!e.agent_id) return;
|
|
251
|
-
if (e.kind === 'verdict'
|
|
288
|
+
if (e.kind === 'verdict') {
|
|
289
|
+
const nv = agg.normalizeVerdict(e);
|
|
290
|
+
if (nv.by === 'agent' && nv.outcome !== agg.ABORTED) verdicted.add(e.agent_id); // aborted still needs a verdict
|
|
291
|
+
}
|
|
252
292
|
if (e.kind !== 'complete') return;
|
|
253
293
|
const t = Date.parse(e.ts) || 0;
|
|
254
294
|
const cur = latest.get(e.agent_id);
|
|
@@ -285,8 +325,9 @@ function kebab(s) {
|
|
|
285
325
|
function verdictAgent(opts, ctx, events) {
|
|
286
326
|
const [ref, outcome] = opts._;
|
|
287
327
|
if (!ref || !outcome) {
|
|
288
|
-
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout> [--verify V] [--reason R]\n'
|
|
289
|
-
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]'
|
|
328
|
+
throw new Error('Cú pháp: dieu-phoi verdict <agent_id|last> <pass|fail|timeout|aborted> [--verify V] [--reason R]\n'
|
|
329
|
+
+ ' dieu-phoi verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n]\n'
|
|
330
|
+
+ ' dieu-phoi verdict --root <root_task_id> accept [--steps a,b]');
|
|
290
331
|
}
|
|
291
332
|
if (outcome === 'rework') {
|
|
292
333
|
throw new Error('"rework" đã bỏ. Người dùng không nhận kết quả → dieu-phoi verdict --task <task_id> reject --cause quality');
|
|
@@ -344,9 +385,46 @@ function verdictUser(opts, ctx, events) {
|
|
|
344
385
|
}, core.commonFields(ctx));
|
|
345
386
|
}
|
|
346
387
|
|
|
388
|
+
/**
|
|
389
|
+
* `verdict --root <root> accept [--steps a,b]` (core 0.4.0): the user accepts the steps of the latest approved plan
|
|
390
|
+
* in one call. Covers every step whose task has an exec dispatch and no user verdict yet (a step already rejected
|
|
391
|
+
* or accepted is left alone: reject the bad ones per task first, then accept the rest by root).
|
|
392
|
+
*/
|
|
393
|
+
function verdictRoot(opts, ctx, events) {
|
|
394
|
+
const root = requireTaskId(String(opts.root));
|
|
395
|
+
if (opts._[0] !== 'accept') {
|
|
396
|
+
throw new Error('verdict --root chỉ nhận accept; bước không đạt → dieu-phoi verdict --task <task_id> reject --cause quality|scope');
|
|
397
|
+
}
|
|
398
|
+
const plan = core.readJsonFile(path.join(ctx.paths.plans, `${root}.json`), null);
|
|
399
|
+
if (!plan || !Array.isArray(plan.steps)) throw new Error(`Không thấy kế hoạch đã duyệt của root ${root} (plan-table --approve --root ${root}).`);
|
|
400
|
+
let steps = plan.steps.filter((x) => x && x.task_id);
|
|
401
|
+
if (typeof opts.steps === 'string') {
|
|
402
|
+
const want = new Set(opts.steps.split(',').map((x) => x.trim()).filter(Boolean));
|
|
403
|
+
const ids = new Set(steps.map((x) => String(x.id)));
|
|
404
|
+
const missing = Array.from(want).filter((x) => !ids.has(x));
|
|
405
|
+
if (missing.length) throw new Error(`--steps: không có bước ${missing.join(', ')} trong kế hoạch ${root}`);
|
|
406
|
+
steps = steps.filter((x) => want.has(String(x.id)));
|
|
407
|
+
}
|
|
408
|
+
const execTasks = new Set(events.filter((e) => e.kind === 'dispatch' && (e.role || 'exec') === 'exec').map((e) => e.task_id));
|
|
409
|
+
const judged = new Set(events.filter((e) => e.kind === 'verdict' && agg.normalizeVerdict(e).by === 'user').map((e) => e.task_id));
|
|
410
|
+
const gone = agg.discardedTaskIds(events);
|
|
411
|
+
const todo = steps.filter((x) => execTasks.has(x.task_id) && !judged.has(x.task_id) && !gone.has(x.task_id));
|
|
412
|
+
if (!todo.length) throw new Error(`Không có bước nào của ${root} cần nghiệm thu (chưa giao exec, hoặc đã có verdict người dùng).`);
|
|
413
|
+
return todo.map((x) => ({
|
|
414
|
+
step: String(x.id),
|
|
415
|
+
ev: verdictUser(Object.assign({}, opts, { task: x.task_id, _: ['accept'], round: undefined, cause: undefined }), ctx, events),
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
|
|
347
419
|
function cmdVerdict(opts) {
|
|
348
420
|
const ctx = ctxOf(opts);
|
|
349
421
|
const events = core.readEvents(ctx);
|
|
422
|
+
if (typeof opts.root === 'string' && typeof opts.task !== 'string') {
|
|
423
|
+
const list = verdictRoot(opts, ctx, events);
|
|
424
|
+
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
425
|
+
if (opts.json) return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
426
|
+
return { code: 0, out: `Đã ghi người dùng accept cho ${list.length} bước của ${opts.root}: ${list.map((x) => `${x.step}=${x.ev.task_id}`).join(', ')}.` };
|
|
427
|
+
}
|
|
350
428
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
351
429
|
core.appendEvent(ctx, ev);
|
|
352
430
|
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
@@ -356,6 +434,29 @@ function cmdVerdict(opts) {
|
|
|
356
434
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
357
435
|
}
|
|
358
436
|
|
|
437
|
+
// ------------------------------------------------------------------ discard
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* `dieu-phoi discard --task <task_id> [--reason R]` (core 0.4.0): withdraw a mistaken task. Appends one
|
|
441
|
+
* `discard` event (synced like any other); stats, choose, state and the sink then leave out every event of the
|
|
442
|
+
* task and of its agents. events.jsonl is never edited — a hand edit once lost a dispatch that the sink kept.
|
|
443
|
+
*/
|
|
444
|
+
function cmdDiscard(opts) {
|
|
445
|
+
if (typeof opts.task !== 'string') throw new Error('Cú pháp: dieu-phoi discard --task <task_id> [--reason R]');
|
|
446
|
+
const taskId = requireTaskId(opts.task);
|
|
447
|
+
const ctx = ctxOf(opts);
|
|
448
|
+
const events = core.readEvents(ctx);
|
|
449
|
+
if (!events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task "${taskId}" trong log.`);
|
|
450
|
+
if (agg.discardedTaskIds(events).has(taskId)) return { code: 0, out: opts.json ? JSON.stringify({ already: true, task_id: taskId }) : `Task ${taskId} đã bị loại trước đó.` };
|
|
451
|
+
const ev = Object.assign({
|
|
452
|
+
schema: core.SCHEMA, kind: 'discard', ts: core.nowIso(), agent_id: `task:${taskId}`, task_id: taskId, reason_code: kebab(opts.reason),
|
|
453
|
+
}, core.commonFields(ctx));
|
|
454
|
+
core.appendEvent(ctx, ev);
|
|
455
|
+
const n = events.filter((e) => e.task_id === taskId).length;
|
|
456
|
+
if (opts.json) return { code: 0, out: JSON.stringify(ev) };
|
|
457
|
+
return { code: 0, out: `Đã loại task ${taskId} (${n} bản ghi) khỏi thống kê, choose và state. Log không bị sửa; sink nhận bản ghi discard ở lần sync kế tiếp.` };
|
|
458
|
+
}
|
|
459
|
+
|
|
359
460
|
// ------------------------------------------------------------------ steer
|
|
360
461
|
|
|
361
462
|
/**
|
|
@@ -383,7 +484,8 @@ function cmdSteer(opts) {
|
|
|
383
484
|
const taskId = String(opts.task);
|
|
384
485
|
const mine = agents.filter((a) => a.task_id === taskId);
|
|
385
486
|
if (!mine.length && !events.some((e) => e.task_id === taskId)) throw new Error(`Không thấy task ${taskId} trong log`);
|
|
386
|
-
|
|
487
|
+
// core 0.4.0: verifiers are never marked (a steer is about the work, not its check)
|
|
488
|
+
const inFlight = mine.filter((a) => a.dispatched && a.role !== 'verify' && !(a.completed && a.outcome));
|
|
387
489
|
const type = (mine.find((a) => a.role === 'exec') || mine[0] || {}).task_type || null;
|
|
388
490
|
out = inFlight.length ? inFlight.map((a) => mk(a.agent_id, taskId, a.task_type)) : [mk(`task:${taskId}`, taskId, type)];
|
|
389
491
|
} else {
|
|
@@ -427,6 +529,40 @@ function measureOrch(opts, ctx, prof) {
|
|
|
427
529
|
};
|
|
428
530
|
}
|
|
429
531
|
|
|
532
|
+
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
533
|
+
|
|
534
|
+
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
535
|
+
function ctxWarnLevel(growth, cap) {
|
|
536
|
+
if (!(typeof growth === 'number' && cap > 0 && growth > cap)) return 0;
|
|
537
|
+
return 1 + Math.floor(Math.log2(growth / cap));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Core 0.4.0: the over-cap advice fires once per threshold crossing (cap, 2×, 4×, …) per session, and again after
|
|
542
|
+
* a /compact that left the session over. State: data/ctx-warn.json {session_id: {level, compactions, t}}.
|
|
543
|
+
* Never throws (no state → warn).
|
|
544
|
+
*/
|
|
545
|
+
function ctxWarnOnce(ctx, m) {
|
|
546
|
+
if (!m || !m.over || !m.session_id) return false;
|
|
547
|
+
const file = path.join(ctx.paths.data, 'ctx-warn.json');
|
|
548
|
+
const level = ctxWarnLevel(m.ctx_growth, m.soft_cap);
|
|
549
|
+
try {
|
|
550
|
+
const st = core.readJsonFile(file, {}) || {};
|
|
551
|
+
const cur = st[m.session_id];
|
|
552
|
+
const comp = typeof m.compactions === 'number' ? m.compactions : null;
|
|
553
|
+
const warn = !cur || cur.compactions !== comp || level > (Number(cur.level) || 0);
|
|
554
|
+
if (warn) {
|
|
555
|
+
const now = Date.now();
|
|
556
|
+
st[m.session_id] = { level, compactions: comp, t: now };
|
|
557
|
+
for (const k of Object.keys(st)) if (!(now - Number(st[k] && st[k].t) < CTX_WARN_KEEP_MS)) delete st[k];
|
|
558
|
+
core.writeFileAtomic(file, `${JSON.stringify(st)}\n`);
|
|
559
|
+
}
|
|
560
|
+
return warn;
|
|
561
|
+
} catch (_) {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
430
566
|
function profileOrNull(opts, ctx) {
|
|
431
567
|
try { return chooser.loadProfile(skillDirOf(opts, ctx)); } catch (_) { return null; }
|
|
432
568
|
}
|
|
@@ -508,9 +644,9 @@ function cmdStats(opts) {
|
|
|
508
644
|
c.note,
|
|
509
645
|
]);
|
|
510
646
|
}
|
|
511
|
-
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer']];
|
|
647
|
+
const s = [['task_type', 'task', 'task có steer', 'lần steer', 'minor', 'plan', 'redirect', 'tỉ lệ steer', 'ngoài exec (không tính)']];
|
|
512
648
|
for (const r of steerRows) {
|
|
513
|
-
s.push([r.task_type, r.tasks, r.steered_tasks, r.actions, r.minor, r.plan, r.redirect, pct(r.steer_rate)]);
|
|
649
|
+
s.push([r.task_type, r.tasks, r.steered_tasks, r.actions, r.minor, r.plan, r.redirect, pct(r.steer_rate), r.plan_phase_actions || 0]);
|
|
514
650
|
}
|
|
515
651
|
return {
|
|
516
652
|
code: 0,
|
|
@@ -534,6 +670,7 @@ function cmdRetro(opts) {
|
|
|
534
670
|
const userOutcomes = {};
|
|
535
671
|
for (const a of agents) {
|
|
536
672
|
if (a.outcome) outcomes[a.outcome] = (outcomes[a.outcome] || 0) + 1;
|
|
673
|
+
if (a.aborted) outcomes.aborted = (outcomes.aborted || 0) + 1;
|
|
537
674
|
if (a.reason_code) reasons[a.reason_code] = (reasons[a.reason_code] || 0) + 1;
|
|
538
675
|
}
|
|
539
676
|
for (const v of userVerdicts) {
|
|
@@ -565,6 +702,7 @@ function cmdRetro(opts) {
|
|
|
565
702
|
no_verdict_agents: agents.filter((a) => a.completed && !a.outcome && a.role !== 'verify').length,
|
|
566
703
|
no_user_verdict_tasks: tasks.filter((t) => !t.user_verdicts.length).length,
|
|
567
704
|
multi_attempt_tasks: escalations,
|
|
705
|
+
discarded_tasks: agg.discardedTaskIds(events).size,
|
|
568
706
|
overrides: overrides.length,
|
|
569
707
|
cost_usd: agents.reduce((s, a) => s + (a.cost_usd || 0), 0),
|
|
570
708
|
},
|
|
@@ -614,6 +752,19 @@ function chooseInput(opts, ctx, events) {
|
|
|
614
752
|
};
|
|
615
753
|
}
|
|
616
754
|
|
|
755
|
+
function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
|
|
756
|
+
return Object.assign({
|
|
757
|
+
schema: core.SCHEMA,
|
|
758
|
+
kind: 'override',
|
|
759
|
+
ts: core.nowIso(),
|
|
760
|
+
agent_id: taskId ? `task:${taskId}` : `override-${crypto.randomBytes(6).toString('hex')}`,
|
|
761
|
+
task_id: taskId || null,
|
|
762
|
+
task_type: taskType,
|
|
763
|
+
suggested_tier: suggested,
|
|
764
|
+
chosen_tier: chosen,
|
|
765
|
+
}, core.commonFields(ctx));
|
|
766
|
+
}
|
|
767
|
+
|
|
617
768
|
function cmdChoose(opts) {
|
|
618
769
|
const ctx = ctxOf(opts);
|
|
619
770
|
if (opts.override !== undefined && typeof opts.task !== 'string') {
|
|
@@ -626,21 +777,14 @@ function cmdChoose(opts) {
|
|
|
626
777
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
627
778
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
628
779
|
res.orch_ctx_over = orch.over;
|
|
780
|
+
res.orch_ctx_warn = ctxWarnOnce(ctx, orch); // over AND a new threshold crossed since the last warning
|
|
629
781
|
if (res.overridden) {
|
|
630
782
|
const tid = input.taskId;
|
|
631
|
-
core.appendEvent(ctx,
|
|
632
|
-
schema: core.SCHEMA,
|
|
633
|
-
kind: 'override',
|
|
634
|
-
ts: core.nowIso(),
|
|
635
|
-
agent_id: tid ? `task:${tid}` : `override-${require('crypto').randomBytes(6).toString('hex')}`,
|
|
636
|
-
task_id: tid,
|
|
637
|
-
task_type: String(input.type).toLowerCase(),
|
|
638
|
-
suggested_tier: res.suggested_tier,
|
|
639
|
-
chosen_tier: res.tier,
|
|
640
|
-
}, core.commonFields(ctx)));
|
|
783
|
+
core.appendEvent(ctx, overrideEvent(ctx, tid, res.task_type || String(input.type).toLowerCase(), res.suggested_tier, res.tier));
|
|
641
784
|
}
|
|
642
785
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
643
|
-
const
|
|
786
|
+
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
787
|
+
const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
644
788
|
const est =`~${fmtUsd(res.est_cost_usd)} · ~${fmtNum(res.est_time_min)} phút (${res.est_source === 'data' ? 'dữ liệu' : 'ước lượng thô'})`;
|
|
645
789
|
if (res.action === 'ask_user') {
|
|
646
790
|
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
@@ -775,9 +919,35 @@ function planTaskIdProblems(plan, root) {
|
|
|
775
919
|
const same = plan.steps.filter((r) => r.task_id === root).map((r) => r.step);
|
|
776
920
|
if (same.length) problems.push(`task_id của bước ${same.join(', ')} trùng --root "${root}" (root dành cho scout/planner)`);
|
|
777
921
|
}
|
|
922
|
+
// core 0.4.0: a label outside the fixed list (and without an alias) would never pool its stats
|
|
923
|
+
for (const r of plan.steps) if (r.unknown_type) problems.push(`bước ${r.step}: ${r.type_warning}`);
|
|
778
924
|
return problems;
|
|
779
925
|
}
|
|
780
926
|
|
|
927
|
+
/** Mapped ad-hoc labels (bug-fix → debug, …): not an error, but the tag must carry the mapped type. */
|
|
928
|
+
function planTypeNotes(plan) {
|
|
929
|
+
return plan.steps.filter((r) => r.mapped_from).map((r) => `bước ${r.step}: ${r.type_warning}`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Overrides chosen in the steps file (step `override`) become override events when the plan is approved —
|
|
934
|
+
* also on a re-approval that changes a step's tier — so the override learning sees them (core 0.4.0; before,
|
|
935
|
+
* only `choose --override` logged one). One event per (task_id, chosen tier): re-approving the same tier or a
|
|
936
|
+
* `choose --override` already logged adds nothing.
|
|
937
|
+
*/
|
|
938
|
+
function planOverrideEvents(ctx, plan, events) {
|
|
939
|
+
const seen = new Set(events.filter((e) => e.kind === 'override').map((e) => `${e.task_id}|${e.chosen_tier}`));
|
|
940
|
+
const out = [];
|
|
941
|
+
for (const r of plan.steps) {
|
|
942
|
+
if (!r.overridden || !r.task_id || !r.tier || !r.suggested_tier) continue;
|
|
943
|
+
const k = `${r.task_id}|${r.tier}`;
|
|
944
|
+
if (seen.has(k)) continue;
|
|
945
|
+
seen.add(k);
|
|
946
|
+
out.push(overrideEvent(ctx, r.task_id, r.task_type || r.type, r.suggested_tier, r.tier));
|
|
947
|
+
}
|
|
948
|
+
return out;
|
|
949
|
+
}
|
|
950
|
+
|
|
781
951
|
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
782
952
|
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
783
953
|
const prev = core.readJsonFile(file, null);
|
|
@@ -793,7 +963,7 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
|
793
963
|
approved_at: core.nowIso(),
|
|
794
964
|
approval: prevApproval(prev) + 1,
|
|
795
965
|
steps: plan.steps.map((r) => ({
|
|
796
|
-
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.type || null,
|
|
966
|
+
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
|
|
797
967
|
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
798
968
|
})),
|
|
799
969
|
history,
|
|
@@ -840,9 +1010,15 @@ function cmdPlanTable(opts) {
|
|
|
840
1010
|
throw new Error(`plan-table --approve: không ghi kế hoạch — ${problems.join('; ')}. Mỗi bước cần task_id riêng (8 ký tự a-z0-9, lấy bằng \`dieu-phoi new-id\`), khác --root.`);
|
|
841
1011
|
}
|
|
842
1012
|
if (problems.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${problems.join('; ')} (cần sửa trước khi --approve)\n`);
|
|
1013
|
+
const typeNotes = planTypeNotes(plan);
|
|
1014
|
+
if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
|
|
843
1015
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1016
|
+
const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
|
|
1017
|
+
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
844
1018
|
if (opts.json) {
|
|
845
|
-
const out = rec ? Object.assign({}, plan, {
|
|
1019
|
+
const out = rec ? Object.assign({}, plan, {
|
|
1020
|
+
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length },
|
|
1021
|
+
}) : plan;
|
|
846
1022
|
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
847
1023
|
}
|
|
848
1024
|
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính |', '|---|---|---|---|---|'];
|
|
@@ -862,11 +1038,12 @@ function cmdPlanTable(opts) {
|
|
|
862
1038
|
process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${plan.refused.length} bước bị từ chối
|
|
863
1039
|
`);
|
|
864
1040
|
}
|
|
865
|
-
if (
|
|
1041
|
+
if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
|
|
1042
|
+
if (rec) lines.push('', `Đã ghi kế hoạch đã duyệt (root ${rec.root_task_id}, lần duyệt ${rec.approval})${ovs.length ? `; ghi ${ovs.length} override` : ''}`);
|
|
866
1043
|
return { code: 0, out: lines.join('\n') };
|
|
867
1044
|
}
|
|
868
1045
|
|
|
869
1046
|
module.exports = {
|
|
870
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1047
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
871
1048
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
872
1049
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Hook + CLI ghi log subagent của Claude Code và chọn model/effort theo chi phí + thời gian tới lúc nghiệm thu.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "kiennt <kiennt@pixon.games>",
|