@kienntpixon/dieu-phoi 0.2.1 → 0.3.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 +14 -0
- package/README.md +5 -5
- package/bin/dieu-phoi.cjs +4 -1
- package/hook.cjs +149 -5
- package/lib/choose.cjs +7 -8
- package/lib/commands.cjs +60 -13
- package/lib/setup.cjs +8 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
- 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`.
|
|
7
|
+
- **`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.
|
|
8
|
+
- `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.
|
|
9
|
+
- Core export `isDieuPhoiPrompt`, `sinkStatusCached`, `quickHealth`, `startInfo`, `handleUserPromptSubmit`, … để CLI và hook dùng chung.
|
|
10
|
+
|
|
11
|
+
## 0.2.2
|
|
12
|
+
|
|
13
|
+
- **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`).
|
|
14
|
+
- Prior mặc định `plan-design` → `opus/medium`. Sàn score ≥ 9 → `tối thiểu opus/medium`.
|
|
15
|
+
- Leo thang quá trần → `ask_user` với ghi chú "hỏi người dùng: làm rõ yêu cầu hoặc đổi kế hoạch" (không còn gợi ý chạy `opus/high`). Bỏ ước tính thô `opus/high` khỏi `ROUGH`.
|
|
16
|
+
|
|
3
17
|
## 0.2.1
|
|
4
18
|
|
|
5
19
|
- **Định dạng `task_id` được kiểm:** đúng 8 ký tự `[a-z0-9]`. Một phiên thật đã tự bịa `almanac-cat-1791568325` làm root id mà không gì chặn. `plan-table --approve` (cả `--root` lẫn `task_id` từng bước), `choose --task`, `verdict --task`, `steer --task` giờ từ chối id sai: exit ≠ 0, lỗi tiếng Việt nêu id sai và gợi ý một id mới hợp lệ (`task_id "…" không hợp lệ: cần đúng 8 ký tự a-z0-9 (vd. k3f9x2ab)`), không ghi gì. `plan-table` không `--approve` chỉ cảnh báo trên stderr.
|
package/README.md
CHANGED
|
@@ -34,8 +34,8 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
34
34
|
|
|
35
35
|
| Lệnh | Việc |
|
|
36
36
|
|---|---|
|
|
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` |
|
|
37
|
+
| `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
|
+
| `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
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 |
|
|
40
40
|
| `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 |
|
|
41
41
|
| `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 |
|
|
@@ -44,6 +44,7 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
44
44
|
| `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
45
|
| `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
46
|
| `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` |
|
|
47
|
+
| `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
48
|
| `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
49
|
| `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
50
|
| `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`) |
|
|
@@ -51,13 +52,13 @@ Khởi động lại Claude Code sau khi cài. Kiểm tra bằng `dieu-phoi doct
|
|
|
51
52
|
|
|
52
53
|
### `choose`
|
|
53
54
|
|
|
54
|
-
1. **Luật an toàn** (cố định trong core): trần `opus/high
|
|
55
|
+
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
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`. Lượt bị steer `plan`/`redirect` không tính.
|
|
56
57
|
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
58
|
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
59
|
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.
|
|
59
60
|
|
|
60
|
-
Thang tier: `haiku/low < haiku/medium < haiku/high < sonnet/medium < sonnet/high < opus/medium
|
|
61
|
+
Thang tier: `haiku/low < haiku/medium < haiku/high < sonnet/medium < sonnet/high < opus/medium` (trần).
|
|
61
62
|
|
|
62
63
|
Kết quả `--json`: `{"action":"dispatch","model":"sonnet","model_id":"claude-sonnet-5-5","effort":"high","reason":"…","explored":false,"tier":"sonnet/high","budget_s":900,"est_cost_usd":0.2,"est_time_min":6,"est_source":"rough","profile_source":"builtin"}`. `model` là tên ngắn dùng thẳng cho tham số `model` của Agent tool; `budget_s` = 2 × trung vị thời gian của ô (chưa đủ dữ liệu thì lấy `time_cap_min`). `--seed` cho kết quả lặp lại được.
|
|
63
64
|
|
|
@@ -77,7 +78,6 @@ Kết quả `--json`: `{"action":"dispatch","model":"sonnet","model_id":"claude-
|
|
|
77
78
|
| sonnet/medium | 55k | 6k | 4 | 0.17 |
|
|
78
79
|
| sonnet/high | 60k | 8k | 6 | 0.20 |
|
|
79
80
|
| opus/medium | 60k | 8k | 8 | 0.40 |
|
|
80
|
-
| opus/high | 80k | 12k | 12 | 0.56 |
|
|
81
81
|
|
|
82
82
|
### `plan-table`
|
|
83
83
|
|
package/bin/dieu-phoi.cjs
CHANGED
|
@@ -10,7 +10,9 @@ 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
|
|
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
|
|
14
16
|
verdict <agent_id|last> <pass|fail|timeout> [--verify tests|lint|review|judgment|none] [--reason short-kebab]
|
|
15
17
|
nghiệm thu tầng agent (by=agent)
|
|
16
18
|
verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n] [--reason R]
|
|
@@ -63,6 +65,7 @@ async function main(argv) {
|
|
|
63
65
|
const table = {
|
|
64
66
|
setup: c.cmdSetup,
|
|
65
67
|
doctor: c.cmdDoctor,
|
|
68
|
+
start: c.cmdStart,
|
|
66
69
|
verdict: c.cmdVerdict,
|
|
67
70
|
steer: c.cmdSteer,
|
|
68
71
|
'plan-table': c.cmdPlanTable,
|
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.3.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'),
|
|
@@ -1140,6 +1142,147 @@ function handleSessionStart(p, ctx) {
|
|
|
1140
1142
|
};
|
|
1141
1143
|
}
|
|
1142
1144
|
|
|
1145
|
+
// ------------------------------------------------------------------ fast start (UserPromptSubmit + `start`)
|
|
1146
|
+
|
|
1147
|
+
const SINK_CACHE_TTL_MS = 24 * 3600 * 1000;
|
|
1148
|
+
const START_PREFIX = 'dieu-phoi: khởi động sẵn (hook UserPromptSubmit, không cần chạy shell trước scout):';
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Prompt that starts a NEW dieu-phoi orchestration. Conservative: the trigger must OPEN the prompt
|
|
1152
|
+
* (after trimming) — `/dieu-phoi`, "điều phối" (any case, with or without diacritics, optionally
|
|
1153
|
+
* followed by ":"), or one of the skill's distinctive phrases ("spawn subagents", "orchestrate
|
|
1154
|
+
* subagents", "chia việc cho agent", "giao cho subagent"). "điều phối" elsewhere in the prompt never
|
|
1155
|
+
* fires. Retro / state / stats / CLI-style requests and "điều phối viên …" never fire (not a gate 1).
|
|
1156
|
+
*/
|
|
1157
|
+
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:,.!?])/;
|
|
1158
|
+
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)/;
|
|
1159
|
+
const PROMPT_ANY_EXCLUDE_RE = /\b(?:retro|thong ke)\b/;
|
|
1160
|
+
|
|
1161
|
+
function foldPrompt(s) {
|
|
1162
|
+
return String(s).toLowerCase().replace(/đ/g, 'd').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
1163
|
+
.replace(/\s+/g, ' ');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function isDieuPhoiPrompt(prompt) {
|
|
1167
|
+
if (typeof prompt !== 'string' || !prompt) return false;
|
|
1168
|
+
const t = foldPrompt(prompt.trimStart().slice(0, 300)).trim();
|
|
1169
|
+
const m = PROMPT_START_RE.exec(t);
|
|
1170
|
+
if (!m) return false;
|
|
1171
|
+
if (PROMPT_REST_EXCLUDE_RE.test(t.slice(m[0].length))) return false;
|
|
1172
|
+
return !PROMPT_ANY_EXCLUDE_RE.test(t);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function sinkUrlKey(cfg) {
|
|
1176
|
+
return cfg && cfg.sink_url ? sha8(String(cfg.sink_url)) : null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Sink health without a network call: newest of data/sink-check.json (doctor's live check, same sink
|
|
1181
|
+
* URL) and data/last-sync.json (every background sync) within 24 h.
|
|
1182
|
+
* Returns { state: 'none'|'ok'|'fail'|'unknown', ts, source, detail }.
|
|
1183
|
+
*/
|
|
1184
|
+
function sinkStatusCached(ctx, nowMs) {
|
|
1185
|
+
const cfg = ctx.config || {};
|
|
1186
|
+
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ộ)' };
|
|
1187
|
+
const now = nowMs || Date.now();
|
|
1188
|
+
const cands = [];
|
|
1189
|
+
const chk = readJsonFile(ctx.paths.sinkCheck, null);
|
|
1190
|
+
if (chk && chk.url === sinkUrlKey(cfg) && Date.parse(chk.ts)) {
|
|
1191
|
+
cands.push({ state: chk.ok ? 'ok' : 'fail', ts: chk.ts, source: 'doctor', detail: chk.detail || null });
|
|
1192
|
+
}
|
|
1193
|
+
const ls = readJsonFile(ctx.paths.lastSync, null);
|
|
1194
|
+
// last-sync is only trusted for the sink it was written for (sink_url changed → stale)
|
|
1195
|
+
if (ls && ls.url === sinkUrlKey(cfg) && Date.parse(ls.ts)) {
|
|
1196
|
+
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') });
|
|
1197
|
+
}
|
|
1198
|
+
const fresh = cands.filter((c) => now - Date.parse(c.ts) <= SINK_CACHE_TTL_MS && Date.parse(c.ts) <= now + 60000)
|
|
1199
|
+
.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
1200
|
+
if (fresh.length) return fresh[0];
|
|
1201
|
+
return { state: 'unknown', ts: null, source: null, detail: 'chưa kiểm trong 24 giờ qua (`dieu-phoi doctor --net`)' };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** Absolute path of the ready-made scout brief of the installed skill, or null. */
|
|
1205
|
+
function scoutBriefPath(ctx) {
|
|
1206
|
+
const sd = ctx.config && ctx.config.skill_dir;
|
|
1207
|
+
if (!sd) return null;
|
|
1208
|
+
const f = path.join(sd, 'references', 'scout-brief.md');
|
|
1209
|
+
return fs.existsSync(f) ? toPosix(f) : null;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** Local, network-free health: config, profile, run dir, cached sink status. */
|
|
1213
|
+
function quickHealth(ctx, runs) {
|
|
1214
|
+
const problems = [];
|
|
1215
|
+
if (!fs.existsSync(ctx.paths.config)) problems.push('chưa có config — chạy `dieu-phoi setup --dept <phòng>`');
|
|
1216
|
+
const sd = ctx.config.skill_dir;
|
|
1217
|
+
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');
|
|
1218
|
+
if (runs && !fs.existsSync(runs)) problems.push(`chưa có thư mục ${toPosix(runs)}`);
|
|
1219
|
+
const sink = sinkStatusCached(ctx);
|
|
1220
|
+
if (sink.state === 'fail') problems.push(`sink lỗi lần cuối (${sink.ts}): ${sink.detail}`);
|
|
1221
|
+
return { ok: !problems.length, problems, sink };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/** Fresh root task id + its run dir, created. Retries on the (astronomically rare) existing dir. */
|
|
1225
|
+
function startRun() {
|
|
1226
|
+
const runs = runsDir();
|
|
1227
|
+
fs.mkdirSync(runs, { recursive: true });
|
|
1228
|
+
for (let i = 0; i < 5; i++) {
|
|
1229
|
+
const id = newTaskId();
|
|
1230
|
+
const dir = path.join(runs, id);
|
|
1231
|
+
try {
|
|
1232
|
+
fs.mkdirSync(dir);
|
|
1233
|
+
return { id, run_dir: toPosix(dir), runs };
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
if (e.code !== 'EEXIST') throw e;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
throw new Error('không tạo được run dir mới');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function healthLine(h) {
|
|
1242
|
+
const sink = h.sink.state === 'ok' ? `sink ok (${h.sink.source} ${h.sink.ts})`
|
|
1243
|
+
: h.sink.state === 'none' ? 'sink chưa cấu hình' : `sink ${h.sink.state === 'fail' ? 'LỖI' : 'chưa rõ'}`;
|
|
1244
|
+
return h.ok ? `ok — ${sink}` : `CẢNH BÁO — ${h.problems.join('; ')}${h.sink.state === 'fail' ? '' : ` — ${sink}`}`;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const START_STEP_IDS = 10;
|
|
1248
|
+
|
|
1249
|
+
/** Spare task_ids for the plan steps, distinct from the root, so the scout needs no `new-id` shell call. */
|
|
1250
|
+
function stepIds(root, n) {
|
|
1251
|
+
const ids = new Set();
|
|
1252
|
+
while (ids.size < n) { const id = newTaskId(); if (id !== root) ids.add(id); }
|
|
1253
|
+
return Array.from(ids);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function startInfo(ctx) {
|
|
1257
|
+
const run = startRun();
|
|
1258
|
+
const health = quickHealth(ctx, run.runs);
|
|
1259
|
+
return { id: run.id, run_dir: run.run_dir, step_ids: stepIds(run.id, START_STEP_IDS), health, scout_brief: scoutBriefPath(ctx) };
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function formatStartBlock(info) {
|
|
1263
|
+
return [
|
|
1264
|
+
START_PREFIX,
|
|
1265
|
+
`root_task_id: ${info.id}`,
|
|
1266
|
+
`run_dir: ${info.run_dir}/ (đã tạo)`,
|
|
1267
|
+
`step_ids: ${info.step_ids.join(' ')}`,
|
|
1268
|
+
`health: ${healthLine(info.health)}`,
|
|
1269
|
+
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
1270
|
+
'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
|
+
].join('\n');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** UserPromptSubmit: on a dieu-phoi prompt, pre-create the run and inject id/run dir/health/brief. */
|
|
1275
|
+
function handleUserPromptSubmit(p, ctx) {
|
|
1276
|
+
if (!isDieuPhoiPrompt(p.prompt)) return null;
|
|
1277
|
+
const info = startInfo(ctx);
|
|
1278
|
+
return {
|
|
1279
|
+
stdout: JSON.stringify({
|
|
1280
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: formatStartBlock(info) },
|
|
1281
|
+
}),
|
|
1282
|
+
info,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1143
1286
|
// ------------------------------------------------------------------ hook handlers
|
|
1144
1287
|
|
|
1145
1288
|
function extractAgentId(tr) {
|
|
@@ -1337,6 +1480,7 @@ function runHook(opts) {
|
|
|
1337
1480
|
let ev = null;
|
|
1338
1481
|
const name = p.hook_event_name;
|
|
1339
1482
|
if (name === 'SessionStart') return handleSessionStart(p, ctx); // { stdout } or null, never an event
|
|
1483
|
+
if (name === 'UserPromptSubmit') return handleUserPromptSubmit(p, ctx); // { stdout } or null
|
|
1340
1484
|
if (name === 'SubagentStop') ev = handleSubagentStop(p, ctx);
|
|
1341
1485
|
else if (name === 'PostToolUse' || (!name && p.tool_name)) ev = handlePostToolUse(p, ctx);
|
|
1342
1486
|
if (ev) {
|
|
@@ -1477,7 +1621,7 @@ async function runSync(ctx, opts) {
|
|
|
1477
1621
|
} finally {
|
|
1478
1622
|
try {
|
|
1479
1623
|
writeFileAtomic(ctx.paths.lastSync, `${JSON.stringify({
|
|
1480
|
-
ts: nowIso(), ok: result.ok, sent: result.sent, error: result.error,
|
|
1624
|
+
ts: nowIso(), ok: result.ok, sent: result.sent, error: result.error, url: sinkUrlKey(cfg),
|
|
1481
1625
|
})}\n`);
|
|
1482
1626
|
} catch (_) { /* best effort */ }
|
|
1483
1627
|
try { fs.unlinkSync(ctx.paths.lock); } catch (_) { /* gone */ }
|
|
@@ -1488,7 +1632,7 @@ async function runSync(ctx, opts) {
|
|
|
1488
1632
|
// ------------------------------------------------------------------ exports / main
|
|
1489
1633
|
|
|
1490
1634
|
module.exports = {
|
|
1491
|
-
runsDir, loadPlans, buildSessionState, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
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,
|
|
1492
1636
|
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED,
|
|
1493
1637
|
sha8, nowIso, readJsonFile, writeFileAtomic, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
1494
1638
|
logError, computeUserHash, userHash, commonFields, parseTag, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|
package/lib/choose.cjs
CHANGED
|
@@ -6,7 +6,8 @@ const crypto = require('crypto');
|
|
|
6
6
|
const core = require('../hook.cjs');
|
|
7
7
|
const agg = require('./aggregate.cjs');
|
|
8
8
|
|
|
9
|
-
// Tier ladder, cheapest first. Ceiling = opus/
|
|
9
|
+
// Tier ladder, cheapest first. Ceiling = opus/medium (core safety rule): anything above
|
|
10
|
+
// (opus/high, xhigh, max, Fable) clamps to opus/medium in tierIndex().
|
|
10
11
|
const LADDER = [
|
|
11
12
|
{ model: 'haiku', effort: 'low' },
|
|
12
13
|
{ model: 'haiku', effort: 'medium' },
|
|
@@ -14,7 +15,6 @@ const LADDER = [
|
|
|
14
15
|
{ model: 'sonnet', effort: 'medium' },
|
|
15
16
|
{ model: 'sonnet', effort: 'high' },
|
|
16
17
|
{ model: 'opus', effort: 'medium' },
|
|
17
|
-
{ model: 'opus', effort: 'high' },
|
|
18
18
|
];
|
|
19
19
|
const TOP = LADDER.length - 1;
|
|
20
20
|
const EFFORT_RANK = { low: 0, medium: 1, high: 2, xhigh: 3, max: 4 };
|
|
@@ -31,7 +31,7 @@ const DEFAULT_PRIORS = {
|
|
|
31
31
|
review: 'opus/medium',
|
|
32
32
|
'research-synth': 'opus/medium',
|
|
33
33
|
'content-write': 'sonnet/high',
|
|
34
|
-
'plan-design': 'opus/
|
|
34
|
+
'plan-design': 'opus/medium',
|
|
35
35
|
'data-analysis': 'sonnet/high',
|
|
36
36
|
};
|
|
37
37
|
const DEFAULT_TIME_CAP_MIN = {
|
|
@@ -186,7 +186,7 @@ const OPUS_MEDIUM = 5;
|
|
|
186
186
|
* Escalation within one task_id (CONTRACT "Luật an toàn"):
|
|
187
187
|
* - one fail at a tier -> same tier again (retry with the error);
|
|
188
188
|
* - two fails at the same tier -> next tier up;
|
|
189
|
-
* - if that next tier would exceed opus/medium
|
|
189
|
+
* - if that next tier would exceed the opus/medium ceiling -> ask the user (no auto re-plan).
|
|
190
190
|
* Fail = agent fail|timeout or user reject cause=quality (eff_outcome). Attempts steered
|
|
191
191
|
* plan/redirect by the user are ignored.
|
|
192
192
|
*/
|
|
@@ -213,7 +213,7 @@ function escalation(task, role) {
|
|
|
213
213
|
if (((role || 'exec') === 'exec' && next > OPUS_MEDIUM) || next > TOP) {
|
|
214
214
|
return Object.assign(base, {
|
|
215
215
|
floor: Math.min(TOP, next), ask: true,
|
|
216
|
-
note: `task đã fail ${totalFails} lần (đã thử ${tried.join(', ')}); tier kế vượt opus/medium — hỏi người dùng: làm rõ yêu cầu
|
|
216
|
+
note: `task đã fail ${totalFails} lần (đã thử ${tried.join(', ')}); tier kế vượt trần opus/medium — hỏi người dùng: làm rõ yêu cầu hoặc đổi kế hoạch`,
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
return Object.assign(base, { floor: next, note: `task fail ${fails} lần ở ${tierName(cur)} → lên ${tierName(next)}` });
|
|
@@ -264,7 +264,6 @@ const ROUGH = {
|
|
|
264
264
|
'sonnet/medium': { input: 55000, output: 6000, minutes: 4 },
|
|
265
265
|
'sonnet/high': { input: 60000, output: 8000, minutes: 6 },
|
|
266
266
|
'opus/medium': { input: 60000, output: 8000, minutes: 8 },
|
|
267
|
-
'opus/high': { input: 80000, output: 12000, minutes: 12 },
|
|
268
267
|
};
|
|
269
268
|
|
|
270
269
|
function roughEstimate(idx, pricing) {
|
|
@@ -311,7 +310,7 @@ function choose(opts) {
|
|
|
311
310
|
if (opts.override !== undefined && opts.override !== null) {
|
|
312
311
|
overrideIdx = parseTier(String(opts.override));
|
|
313
312
|
if (overrideIdx === null) {
|
|
314
|
-
throw new Error(`--override "${opts.override}" không nằm trên thang tier (trần opus/
|
|
313
|
+
throw new Error(`--override "${opts.override}" không nằm trên thang tier (trần opus/medium, không Fable): ${ladderList}`);
|
|
315
314
|
}
|
|
316
315
|
}
|
|
317
316
|
let approvedIdx = null;
|
|
@@ -345,7 +344,7 @@ function choose(opts) {
|
|
|
345
344
|
// Safety floors (core only; a profile can never lower them).
|
|
346
345
|
let safetyFloor = 0;
|
|
347
346
|
const floorWhy = [];
|
|
348
|
-
if (score >= 9) { safetyFloor = Math.max(safetyFloor, TOP); floorWhy.push(`score ${score}≥9 → tối thiểu opus/
|
|
347
|
+
if (score >= 9) { safetyFloor = Math.max(safetyFloor, TOP); floorWhy.push(`score ${score}≥9 → tối thiểu opus/medium`); }
|
|
349
348
|
if (!verifiable) { safetyFloor = Math.max(safetyFloor, OPUS_MEDIUM); floorWhy.push('không kiểm chứng bằng máy → tối thiểu opus/medium'); }
|
|
350
349
|
if (!reversible && role === 'exec') { safetyFloor = Math.max(safetyFloor, OPUS_MEDIUM); floorWhy.push('khó hoàn tác → tối thiểu opus/medium'); }
|
|
351
350
|
|
package/lib/commands.cjs
CHANGED
|
@@ -63,7 +63,7 @@ function cmdSetup(opts) {
|
|
|
63
63
|
const lines = [
|
|
64
64
|
`Đã cài hook: ${res.hook}`,
|
|
65
65
|
res.settings_changed
|
|
66
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
66
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
67
67
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
68
68
|
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
69
69
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
@@ -98,6 +98,9 @@ async function cmdDoctor(opts) {
|
|
|
98
98
|
const reg = setupLib.registration(claudeDir);
|
|
99
99
|
add(reg.PostToolUse && reg.SubagentStop && reg.SessionStart, 'settings',
|
|
100
100
|
`PostToolUse(Agent)=${reg.PostToolUse ? 'có' : 'thiếu'}, SubagentStop=${reg.SubagentStop ? 'có' : 'thiếu'}, SessionStart(compact)=${reg.SessionStart ? 'có' : 'thiếu'} (${reg.file})`);
|
|
101
|
+
add(reg.UserPromptSubmit, 'prompt-hook', reg.UserPromptSubmit
|
|
102
|
+
? 'UserPromptSubmit có — prompt "điều phối" được tạo sẵn task_id + run dir, lượt đầu là scout'
|
|
103
|
+
: 'thiếu hook UserPromptSubmit — mỗi lần điều phối phải chạy `dieu-phoi start` trước scout; chạy lại `dieu-phoi setup`', 'CẢNH BÁO');
|
|
101
104
|
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
102
105
|
add(permOk, 'run-permissions', permOk
|
|
103
106
|
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
@@ -147,17 +150,12 @@ async function cmdDoctor(opts) {
|
|
|
147
150
|
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');
|
|
148
151
|
}
|
|
149
152
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
let ok = res.ok;
|
|
157
|
-
try { const b = JSON.parse(txt); if (b && b.ok === false) ok = false; } catch (_) { ok = false; }
|
|
158
|
-
add(ok, 'sink', `${ctx.config.sink_url} → HTTP ${res.status}`);
|
|
159
|
-
} catch (e) {
|
|
160
|
-
add(false, 'sink', `${ctx.config.sink_url} → ${e.message}`);
|
|
153
|
+
const cached = opts.net ? null : core.sinkStatusCached(ctx);
|
|
154
|
+
if (cached && (cached.state === 'ok' || cached.state === 'fail')) {
|
|
155
|
+
add(cached.state === 'ok', 'sink', `${ctx.config.sink_url} → ${cached.state === 'ok' ? 'ok' : `lỗi: ${cached.detail}`} (bộ nhớ đệm: ${cached.source} lúc ${cached.ts}; \`doctor --net\` để kiểm trực tiếp)`);
|
|
156
|
+
} else {
|
|
157
|
+
const live = await checkSinkLive(ctx);
|
|
158
|
+
add(live.ok, 'sink', `${ctx.config.sink_url} → ${live.detail}`);
|
|
161
159
|
}
|
|
162
160
|
} else {
|
|
163
161
|
add(true, 'sink', 'chưa cấu hình (chỉ lưu cục bộ)');
|
|
@@ -168,6 +166,55 @@ async function cmdDoctor(opts) {
|
|
|
168
166
|
return { code: failed ? 1 : 0, out };
|
|
169
167
|
}
|
|
170
168
|
|
|
169
|
+
/** Live sink check (view=stats); result cached in data/sink-check.json for 24 h. */
|
|
170
|
+
async function checkSinkLive(ctx) {
|
|
171
|
+
let ok = false;
|
|
172
|
+
let detail;
|
|
173
|
+
try {
|
|
174
|
+
const u = new URL(ctx.config.sink_url);
|
|
175
|
+
u.searchParams.set('token', ctx.config.sink_token);
|
|
176
|
+
u.searchParams.set('view', 'stats');
|
|
177
|
+
const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(8000) });
|
|
178
|
+
const txt = await res.text();
|
|
179
|
+
ok = res.ok;
|
|
180
|
+
try { const b = JSON.parse(txt); if (b && b.ok === false) ok = false; } catch (_) { ok = false; }
|
|
181
|
+
detail = `HTTP ${res.status}`;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
detail = e.message;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
core.writeFileAtomic(ctx.paths.sinkCheck, `${JSON.stringify({ ts: core.nowIso(), ok, detail, url: core.sinkUrlKey(ctx.config) })}\n`);
|
|
187
|
+
} catch (_) { /* cache is best effort */ }
|
|
188
|
+
return { ok, detail };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ------------------------------------------------------------------ start
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `dieu-phoi start [--json]`: one fast, network-free call for gate 1 when the UserPromptSubmit hook
|
|
195
|
+
* did not run: local health (sink from cache), fresh root task id, run dir created, scout brief path.
|
|
196
|
+
*/
|
|
197
|
+
function cmdStart(opts) {
|
|
198
|
+
const ctx = ctxOf(opts);
|
|
199
|
+
const reg = setupLib.registration(claudeDirOf(opts));
|
|
200
|
+
const info = core.startInfo(ctx);
|
|
201
|
+
const h = info.health;
|
|
202
|
+
if (!fs.existsSync(ctx.paths.hook)) h.problems.unshift(`chưa cài hook (${ctx.paths.hook}) — chạy \`dieu-phoi setup --dept <phòng>\`, log không được ghi`);
|
|
203
|
+
else if (!(reg.PostToolUse && reg.SubagentStop && reg.SessionStart)) h.problems.unshift('hook chưa đăng ký đủ trong settings.json — chạy lại `dieu-phoi setup`, log/trạng thái không được ghi');
|
|
204
|
+
if (!reg.UserPromptSubmit) h.problems.push('thiếu hook UserPromptSubmit — chạy lại `dieu-phoi setup` để bỏ được lệnh này');
|
|
205
|
+
if (!reg.permissions || reg.legacy_permissions) h.problems.push(`thiếu quyền ghi cho ${reg.permission_entries.dir} — chạy lại \`dieu-phoi setup\` (agent nền có thể không ghi được report)`);
|
|
206
|
+
h.ok = !h.problems.length;
|
|
207
|
+
if (opts.json) return { code: 0, out: JSON.stringify(info, null, 2) };
|
|
208
|
+
const out = [
|
|
209
|
+
`root_task_id: ${info.id}`,
|
|
210
|
+
`run_dir: ${info.run_dir}/ (đã tạo)`,
|
|
211
|
+
`step_ids: ${info.step_ids.join(' ')}`,
|
|
212
|
+
`health: ${core.healthLine(h)}`,
|
|
213
|
+
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
214
|
+
];
|
|
215
|
+
return { code: 0, out: out.join('\n') };
|
|
216
|
+
}
|
|
217
|
+
|
|
171
218
|
// ------------------------------------------------------------------ hook / sync
|
|
172
219
|
|
|
173
220
|
function cmdHook(opts) {
|
|
@@ -820,6 +867,6 @@ function cmdPlanTable(opts) {
|
|
|
820
867
|
}
|
|
821
868
|
|
|
822
869
|
module.exports = {
|
|
823
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
870
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdStats, cmdRetro, cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
824
871
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
825
872
|
};
|
package/lib/setup.cjs
CHANGED
|
@@ -57,7 +57,7 @@ function stripOurHooks(settings) {
|
|
|
57
57
|
return removed;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** True when settings already hold exactly our
|
|
60
|
+
/** True when settings already hold exactly our four entries with this command. */
|
|
61
61
|
function alreadyRegistered(settings, cmd) {
|
|
62
62
|
const hooks = settings && settings.hooks;
|
|
63
63
|
if (!hooks || typeof hooks !== 'object') return false;
|
|
@@ -72,10 +72,11 @@ function alreadyRegistered(settings, cmd) {
|
|
|
72
72
|
if (event === 'PostToolUse' && g.matcher === 'Agent') good++;
|
|
73
73
|
else if (event === 'SubagentStop') good++;
|
|
74
74
|
else if (event === 'SessionStart' && g.matcher === 'compact') good++;
|
|
75
|
+
else if (event === 'UserPromptSubmit' && !g.matcher) good++;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
|
-
return ours ===
|
|
79
|
+
return ours === 4 && good === 4;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function addOurHooks(settings, hookPath) {
|
|
@@ -89,6 +90,9 @@ function addOurHooks(settings, hookPath) {
|
|
|
89
90
|
// after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
|
|
90
91
|
if (!Array.isArray(h.SessionStart)) h.SessionStart = [];
|
|
91
92
|
h.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
93
|
+
// on a dieu-phoi prompt: pre-create the run dir + id so the first tool call can be the scout (no shell)
|
|
94
|
+
if (!Array.isArray(h.UserPromptSubmit)) h.UserPromptSubmit = [];
|
|
95
|
+
h.UserPromptSubmit.push({ hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
// ------------------------------------------------------------------ run-dir permissions
|
|
@@ -340,7 +344,7 @@ function registration(claudeDir) {
|
|
|
340
344
|
const perms = permissionEntries(runsDir());
|
|
341
345
|
const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
|
|
342
346
|
const out = {
|
|
343
|
-
file, PostToolUse: false, SubagentStop: false, SessionStart: false, commands: [], permissions: false, permission_entries: perms,
|
|
347
|
+
file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, commands: [], permissions: false, permission_entries: perms,
|
|
344
348
|
runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
|
|
345
349
|
};
|
|
346
350
|
let obj;
|
|
@@ -350,7 +354,7 @@ function registration(claudeDir) {
|
|
|
350
354
|
out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
|
|
351
355
|
|| (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
|
|
352
356
|
const hooks = obj.hooks || {};
|
|
353
|
-
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart']) {
|
|
357
|
+
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit']) {
|
|
354
358
|
for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
|
|
355
359
|
for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
|
|
356
360
|
if (!isOurs(h)) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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>",
|