@kienntpixon/dieu-phoi 0.2.2 → 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 CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## 0.2.2
4
12
 
5
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`).
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`) |
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 kiểm hook, node, config, sink
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 SubagentStop. It must stay
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: never write to stdout, always exit 0, no synchronous network.
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.2.2';
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/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
- try {
151
- const u = new URL(ctx.config.sink_url);
152
- u.searchParams.set('token', ctx.config.sink_token);
153
- u.searchParams.set('view', 'stats');
154
- const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(8000) });
155
- const txt = await res.text();
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 two entries with this command. */
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 === 3 && good === 3;
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.2.2",
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>",