@kienntpixon/dieu-phoi 0.5.2 → 0.5.3
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 +11 -0
- package/hook.cjs +235 -13
- package/lib/commands.cjs +24 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.5.3 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
Đề xuất core 1–3 của retro 2026-10-22 (`~/.dieu-phoi/runs/258ajcrr/retro-2026-10-22.md`). `CORE_VERSION` 0.5.3; đi cùng skill 1.4.3. Không cần deploy lại `sheet/Code.gs`.
|
|
6
|
+
|
|
7
|
+
- **Thẻ sai được gắn cờ thay vì mất task_id:** 76 dp-exec ($407, 25% chi phí 10-19..21) mất `task_id` vì brief đặt id vào ô loại (`[dp:<id>|…]`, thiếu `task=`). Hook mới có `resolveTag` (`parseTag` giữ nguyên, gate gen-2 vẫn dùng bản chặt): `dispatch`/`complete` mang `tag_issue` = `type-is-id|bad-type|missing-task|no-tag` (nối `+`); thiếu `task=` mà ô loại là id (đúng định dạng, không phải loại đã biết, có chữ số hoặc được nhắc trong `dp-out-<id>`/`handoff-<id>`/`task=<id>` của brief) → `task_id` = id đó + `task_id_inferred: true`, loại lấy từ phần khác của thẻ nếu có. Id bắt đầu bằng chữ số (trước đây `parseTag` trả null, agent thành `untagged` và không có watchdog) giờ cũng được nhận. Loại hợp lệ = 12 loại cố định + `art-gen` + `extra_task_types` của profile.
|
|
8
|
+
- **Cảnh báo trả về điều phối viên:** PostToolUse(Agent) in `additionalContext` khi thẻ bị gắn cờ (nêu lỗi, id đã suy hoặc "không vào stats", mẫu thẻ đúng, danh sách loại). Agent không phải `dp-*` và không có thẻ: không cờ, không in.
|
|
9
|
+
- **Dispatch ghi tier `choose` đã đề xuất:** `choose --task` và `plan-table --approve` để lại `data/suggest/<task>-<role>.json`; dispatch kế tiếp của task + role đó ghi `suggested_tier` (tier choose tự chọn, trước override), `choose_tier` (tier choose trả về), `suggested_source`, `dispatched_tier`, `tier_mismatch`. Bản ghi > 14 ngày bị bỏ qua.
|
|
10
|
+
- **Trần chuỗi handoff:** dispatch exec ghi `task_handoffs` (số agent đã handoff của task); từ handoff thứ 3 hook cảnh báo dừng tiếp nối và giao planner chia phần còn lại (retro: 15/63 task feature-impl handoff ≥ 2 lần; cq7yn1ws 10 handoff, $70, fail).
|
|
11
|
+
- **Sửa hai lỗi nhỏ của cảnh báo thẻ:** `bad-type` nêu đúng token sai (trường `bad_type`) kể cả khi thẻ có thêm một loại hợp lệ (`[dp:foo|review|…]` báo "foo", không phải "review"); watchdog (`readHeadTag`) nay truyền `extra_task_types` của profile nên loại của profile không bị coi là `bad-type`.
|
|
12
|
+
- Sink không thêm cột (các trường mới là chẩn đoán cục bộ; `suggested_tier` trùng tên cột sẵn có của `override`). Test mới `test/retro-1022.test.cjs`.
|
|
13
|
+
|
|
3
14
|
## 0.5.2
|
|
4
15
|
|
|
5
16
|
- **`dieu-phoi prune --after-retro <file> [--yes]`:** dọn sau retro, dry-run mặc định. Run dir của task đã có verdict người dùng và mọi sự kiện ≤ mốc cắt (dòng `cutoff:` trong file retro, không có thì mtime) chỉ còn `retro-*.md` + dp-out exec/verify cuối (giữ = try cao nhất VÀ file mới nhất theo mtime cho mỗi task+role; file legacy không phân loại được và kết quả inline cuối cũng giữ); xoá file plan hỏng (tên không phải task id hoặc không đọc được); task còn mở không đụng. Event của task đã dọn chuyển vào `data/archive/events-<ngày>.jsonl.gz` (ghi + xác minh archive trước, rồi ghi lại `events.jsonl` qua file tạm + rename, giữ khoá sync); `readEvents` đọc cả archive (chống trùng nếu prune chết giữa chừng) nên retro/stats/choose/state không đổi số; sync chỉ đọc `events.jsonl`, offset được dịch theo phần đã bỏ, event chưa đẩy lên sink (khi có sink) không bị chuyển. Run dir có file mới hơn mốc cắt (việc đang chạy chưa vào log) không bị dọn. Từ chối gốc ổ đĩa, không đi theo symlink/junction, chỉ đụng runs dir + data dir.
|
package/hook.cjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* handoff watchdog), `--gate` (PreToolUse(Agent), gen-2 helper rules) and `--gate-release`. It must stay
|
|
10
10
|
* self-contained: Node built-ins only, no requires of other package files.
|
|
11
11
|
*
|
|
12
|
-
* Hook rules: stdout only for the JSON of SessionStart(compact) / UserPromptSubmit
|
|
12
|
+
* Hook rules: stdout only for the JSON of SessionStart(compact) / UserPromptSubmit / a PostToolUse(Agent) warning
|
|
13
|
+
* (core 0.5.3: bad dp tag, handoff chain), always exit 0,
|
|
13
14
|
* no network.
|
|
14
15
|
* Errors go to <home>/hook-errors.log.
|
|
15
16
|
*
|
|
@@ -25,7 +26,7 @@ const path = require('path');
|
|
|
25
26
|
const os = require('os');
|
|
26
27
|
const crypto = require('crypto');
|
|
27
28
|
|
|
28
|
-
const CORE_VERSION = '0.5.
|
|
29
|
+
const CORE_VERSION = '0.5.3';
|
|
29
30
|
const SCHEMA = 1;
|
|
30
31
|
const TASK_TYPES = [
|
|
31
32
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -126,6 +127,7 @@ function pathsFor(home) {
|
|
|
126
127
|
events: path.join(data, 'events.jsonl'),
|
|
127
128
|
segments: path.join(data, 'segments'),
|
|
128
129
|
plans: path.join(data, 'plans'),
|
|
130
|
+
suggest: path.join(data, 'suggest'),
|
|
129
131
|
skippedInternal: path.join(data, 'skipped-internal.json'),
|
|
130
132
|
syncOffset: path.join(data, 'sync-offset.json'),
|
|
131
133
|
syncSpawn: path.join(data, 'sync-spawn.json'),
|
|
@@ -263,6 +265,87 @@ function taskIdError(id) {
|
|
|
263
265
|
return `task_id "${id}" không hợp lệ: cần đúng 8 ký tự a-z0-9 (vd. ${newTaskId()}) — lấy id bằng \`dieu-phoi new-id\``;
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
// ------------------------------------------------------------------ tag check (core 0.5.3, retro 2026-10-22)
|
|
269
|
+
//
|
|
270
|
+
// 76 dp-exec ($407, 25% of 10-19..21 cost) lost their task_id because the brief put the id in the type slot
|
|
271
|
+
// (`[dp:<id>|…]`, no `task=`). parseTag stays strict (the gen-2 gate relies on it); resolveTag diagnoses the tag,
|
|
272
|
+
// flags it (`tag_issue`) and recovers the task_id only when the guess is safe.
|
|
273
|
+
|
|
274
|
+
const DEFAULT_EXTRA_TYPES = ['art-gen'];
|
|
275
|
+
|
|
276
|
+
/** Fixed task types + the profile's extra_task_types (art-gen when no profile is readable). */
|
|
277
|
+
function knownTaskTypes(ctx) {
|
|
278
|
+
const out = TASK_TYPES.concat(DEFAULT_EXTRA_TYPES);
|
|
279
|
+
const sd = ctx && ctx.config && ctx.config.skill_dir;
|
|
280
|
+
if (sd) {
|
|
281
|
+
const prof = readJsonFile(path.join(sd, 'profile', 'profile.json'), null);
|
|
282
|
+
const extra = prof && Array.isArray(prof.extra_task_types) ? prof.extra_task_types : [];
|
|
283
|
+
for (const t of extra) if (typeof t === 'string' && !out.includes(t)) out.push(t);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* parseTag + diagnosis. null when the first line has no `[dp:` tag at all. Otherwise a tag (never dropped), plus:
|
|
290
|
+
* - `tag_issue`: `type-is-id` (an 8-char [a-z0-9] id in the type slot), `bad-type` (type not a known task type),
|
|
291
|
+
* `missing-task` (no `task=`), joined with `+`;
|
|
292
|
+
* - `task_id_inferred: true` + `task_id` = the type-slot token, only when `task=` is missing, the token matches the
|
|
293
|
+
* task_id format, is not a known type, and either holds a digit or the prompt names it in a run-file path /
|
|
294
|
+
* `task=` (`dp-out-<id>`, `handoff-<id>`) — the same test that tells `type-is-id` from `bad-type` (an 8-letter label
|
|
295
|
+
* such as "planning" stays a label). Then task_type = another tag part that is a known type, else `untagged`.
|
|
296
|
+
*/
|
|
297
|
+
function resolveTag(text, known) {
|
|
298
|
+
if (typeof text !== 'string') return null;
|
|
299
|
+
const body = text.replace(/^/, '');
|
|
300
|
+
const first = body.split(/\r?\n/, 1)[0].trim();
|
|
301
|
+
const m = /^\[dp:([^\]]*)\]/.exec(first);
|
|
302
|
+
if (!m) return null;
|
|
303
|
+
const types = Array.isArray(known) && known.length ? known : TASK_TYPES.concat(DEFAULT_EXTRA_TYPES);
|
|
304
|
+
const parts = m[1].split('|').map((s) => s.trim());
|
|
305
|
+
const head = (parts[0] || '').toLowerCase();
|
|
306
|
+
const rest = parts.slice(1);
|
|
307
|
+
let tag = parseTag(body);
|
|
308
|
+
// type slot unusable for parseTag (e.g. an id starting with a digit): parse the other parts on their own
|
|
309
|
+
if (!tag) tag = parseTag(`[dp:untagged|${rest.join('|')}]`);
|
|
310
|
+
const issues = [];
|
|
311
|
+
const altType = rest.map((p) => p.toLowerCase()).find((p) => types.includes(p)) || null;
|
|
312
|
+
// an id, not a label: id format, not a known type, and a digit or a run-file / task= mention ("planning" is a label)
|
|
313
|
+
const named = new RegExp(`(?:dp-out-|handoff-|task=)${escRe(head)}(?![a-z0-9])`).test(body.slice(first.length));
|
|
314
|
+
const idLike = TASK_ID_RE.test(head) && !types.includes(head) && (/\d/.test(head) || named);
|
|
315
|
+
if (idLike) issues.push('type-is-id');
|
|
316
|
+
else if (!types.includes(head)) {
|
|
317
|
+
issues.push('bad-type');
|
|
318
|
+
tag.bad_type = parts[0]; // the offending token (task_type may be replaced by a valid altType below)
|
|
319
|
+
}
|
|
320
|
+
if (!tag.task_id) {
|
|
321
|
+
issues.push('missing-task');
|
|
322
|
+
if (idLike) {
|
|
323
|
+
tag.task_id = head;
|
|
324
|
+
tag.task_id_inferred = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (idLike || tag.task_type === 'untagged') tag.task_type = altType || 'untagged';
|
|
328
|
+
else if (!types.includes(tag.task_type) && altType) tag.task_type = altType;
|
|
329
|
+
if (issues.length) tag.tag_issue = issues.join('+');
|
|
330
|
+
return tag;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** One Vietnamese line for the orchestrator about a flagged tag (null when the tag is fine). */
|
|
334
|
+
function tagIssueMessage(tag, known) {
|
|
335
|
+
if (!tag || !tag.tag_issue) return null;
|
|
336
|
+
const what = [];
|
|
337
|
+
const codes = tag.tag_issue.split('+');
|
|
338
|
+
if (codes.includes('no-tag')) what.push('agent dp-* được giao mà không có thẻ `[dp:...]` ở dòng đầu prompt');
|
|
339
|
+
if (codes.includes('type-is-id')) what.push('ô đầu của thẻ là một task id, không phải loại task');
|
|
340
|
+
if (codes.includes('bad-type')) what.push(`loại "${tag.bad_type || tag.task_type}" không nằm trong danh sách loại hợp lệ`);
|
|
341
|
+
if (codes.includes('missing-task')) what.push('thiếu `task=<id>`');
|
|
342
|
+
const got = tag.task_id_inferred
|
|
343
|
+
? ` Hook đã suy task_id=${tag.task_id} từ ô loại (log ghi \`task_id_inferred\`), loại ghi là "${tag.task_type}".`
|
|
344
|
+
: (tag.task_id ? '' : ' Lượt này KHÔNG có task_id: không vào stats/choose/retro.');
|
|
345
|
+
const types = (Array.isArray(known) && known.length ? known : TASK_TYPES.concat(DEFAULT_EXTRA_TYPES)).join(', ');
|
|
346
|
+
return `dieu-phoi: thẻ của agent vừa giao sai (tag_issue=${tag.tag_issue}): ${what.join('; ')}.${got} Lần giao sau dùng đúng \`[dp:<task_type>|s<n>|<urgency>|try<n>|task=<id>|role=<role>]\`, task_type là một trong: ${types}; id lấy bằng \`dieu-phoi new-id\`.`;
|
|
347
|
+
}
|
|
348
|
+
|
|
266
349
|
// ------------------------------------------------------------------ models / pricing
|
|
267
350
|
|
|
268
351
|
function normalizeModel(m) {
|
|
@@ -387,10 +470,13 @@ function firstPromptText(lines) {
|
|
|
387
470
|
return null;
|
|
388
471
|
}
|
|
389
472
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
473
|
+
/**
|
|
474
|
+
* Tag from the first user message of the transcript (the original prompt). Core 0.5.3: resolveTag, so a tag with the
|
|
475
|
+
* id in the type slot still yields its task_id (complete events, watchdog handoff file name, inline results).
|
|
476
|
+
*/
|
|
477
|
+
function tagFromTranscript(lines, known) {
|
|
392
478
|
const text = firstPromptText(lines);
|
|
393
|
-
return text === null ? null :
|
|
479
|
+
return text === null ? null : resolveTag(text, known);
|
|
394
480
|
}
|
|
395
481
|
|
|
396
482
|
/**
|
|
@@ -1409,11 +1495,105 @@ function extractAgentId(tr) {
|
|
|
1409
1495
|
return m ? m[1] : null;
|
|
1410
1496
|
}
|
|
1411
1497
|
|
|
1498
|
+
// ------------------------------------------------------------------ core 0.5.3: choose's tier per task (retro 2026-10-22)
|
|
1499
|
+
//
|
|
1500
|
+
// `choose --task <id>` and `plan-table --approve` leave data/suggest/<task>-<role>.json; the next dispatch of that
|
|
1501
|
+
// task + role copies it as `suggested_tier` (choose's own pick, before a user override) and `choose_tier` (what
|
|
1502
|
+
// choose answered), next to the tier actually dispatched. Older than SUGGEST_MAX_AGE_MS -> ignored.
|
|
1503
|
+
|
|
1504
|
+
const SUGGEST_MAX_AGE_MS = 14 * 24 * 3600 * 1000;
|
|
1505
|
+
|
|
1506
|
+
function suggestFile(ctx, taskId, role) {
|
|
1507
|
+
return path.join(ctx.paths.suggest, `${safeName(taskId)}-${safeName(role || 'exec')}.json`);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* rec: { task_id, role?, suggested_tier, choose_tier?, source: 'choose'|'plan', keep_suggested? }. keep_suggested:
|
|
1512
|
+
* a `choose --approved` answer carries no Thompson pick, so an earlier record's suggested_tier (from the plan table)
|
|
1513
|
+
* is kept. Best effort: returns the written record or null.
|
|
1514
|
+
*/
|
|
1515
|
+
function recordSuggestion(ctx, rec) {
|
|
1516
|
+
if (!rec || !isValidTaskId(rec.task_id) || typeof rec.suggested_tier !== 'string') return null;
|
|
1517
|
+
const role = rec.role || 'exec';
|
|
1518
|
+
const file = suggestFile(ctx, rec.task_id, role);
|
|
1519
|
+
const prev = rec.keep_suggested ? readSuggestion(ctx, rec.task_id, role) : null;
|
|
1520
|
+
const out = {
|
|
1521
|
+
task_id: rec.task_id,
|
|
1522
|
+
role,
|
|
1523
|
+
suggested_tier: prev && prev.suggested_tier ? prev.suggested_tier : rec.suggested_tier,
|
|
1524
|
+
choose_tier: rec.choose_tier || rec.suggested_tier,
|
|
1525
|
+
source: prev && prev.suggested_tier ? prev.source : (rec.source || 'choose'),
|
|
1526
|
+
ts: nowIso(),
|
|
1527
|
+
};
|
|
1528
|
+
try {
|
|
1529
|
+
fs.mkdirSync(ctx.paths.suggest, { recursive: true });
|
|
1530
|
+
writeFileAtomic(file, `${JSON.stringify(out)}\n`);
|
|
1531
|
+
pruneSuggest(ctx);
|
|
1532
|
+
} catch (e) {
|
|
1533
|
+
logError(ctx.home, e, 'suggest');
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
return out;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function readSuggestion(ctx, taskId, role) {
|
|
1540
|
+
if (!isValidTaskId(taskId)) return null;
|
|
1541
|
+
const r = readJsonFile(suggestFile(ctx, taskId, role), null);
|
|
1542
|
+
if (!r || typeof r !== 'object' || typeof r.suggested_tier !== 'string') return null;
|
|
1543
|
+
const t = Date.parse(r.ts);
|
|
1544
|
+
if (!(Date.now() - t <= SUGGEST_MAX_AGE_MS)) return null;
|
|
1545
|
+
return r;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function pruneSuggest(ctx) {
|
|
1549
|
+
let names = [];
|
|
1550
|
+
try { names = fs.readdirSync(ctx.paths.suggest); } catch (_) { return; }
|
|
1551
|
+
const now = Date.now();
|
|
1552
|
+
for (const n of names) {
|
|
1553
|
+
const f = path.join(ctx.paths.suggest, n);
|
|
1554
|
+
try { if (now - fs.statSync(f).mtimeMs > 2 * SUGGEST_MAX_AGE_MS) fs.unlinkSync(f); } catch (_) { /* gone */ }
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// ------------------------------------------------------------------ core 0.5.3: handoff chains (retro 2026-10-22)
|
|
1559
|
+
//
|
|
1560
|
+
// 15/63 feature-impl tasks handed off >= 2 times (19% of feature-impl cost); one chain ran 10 handoffs ($70) and
|
|
1561
|
+
// failed. From the 3rd handoff of a task the orchestrator is told to stop continuing and have a planner re-split.
|
|
1562
|
+
|
|
1563
|
+
const HANDOFF_CHAIN_WARN = 3;
|
|
1564
|
+
|
|
1565
|
+
/** Number of agents whose `complete` event has handoff:true for this task (substring scan of events.jsonl). */
|
|
1566
|
+
function countTaskHandoffs(ctx, taskId) {
|
|
1567
|
+
if (!taskId) return 0;
|
|
1568
|
+
let txt;
|
|
1569
|
+
try { txt = fs.readFileSync(ctx.paths.events, 'utf8'); } catch (_) { return 0; }
|
|
1570
|
+
const needle = `"task_id":${JSON.stringify(taskId)}`;
|
|
1571
|
+
const agents = new Set();
|
|
1572
|
+
let i = txt.indexOf(needle);
|
|
1573
|
+
while (i >= 0) {
|
|
1574
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
1575
|
+
const end = txt.indexOf('\n', i);
|
|
1576
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1577
|
+
if (line.includes('"kind":"complete"') && line.includes('"handoff":true')) {
|
|
1578
|
+
const m = /"agent_id":("(?:[^"\\]|\\.)*")/.exec(line);
|
|
1579
|
+
agents.add(m ? m[1] : `@${start}`);
|
|
1580
|
+
}
|
|
1581
|
+
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1582
|
+
}
|
|
1583
|
+
return agents.size;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function handoffChainMessage(taskId, n) {
|
|
1587
|
+
return `dieu-phoi: task ${taskId} đã handoff ${n} lần (trần ${HANDOFF_CHAIN_WARN}). Đừng nối tiếp thêm: nếu agent vừa giao là agent tiếp nối, cân nhắc dừng nó và giao planner (dp-scout) đọc handoff-${taskId}.md rồi chia phần còn lại thành bước mới (task_id mới từ \`dieu-phoi new-id\`).`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1412
1590
|
function buildDispatch(p, ctx) {
|
|
1413
1591
|
const ti = p.tool_input && typeof p.tool_input === 'object' ? p.tool_input : {};
|
|
1414
1592
|
const tr = p.tool_response;
|
|
1415
1593
|
const trObj = tr && typeof tr === 'object' && !Array.isArray(tr) ? tr : {};
|
|
1416
|
-
const
|
|
1594
|
+
const subagentType = typeof ti.subagent_type === 'string' && ti.subagent_type ? ti.subagent_type : 'general-purpose';
|
|
1595
|
+
let tag = resolveTag(ti.prompt, knownTaskTypes(ctx));
|
|
1596
|
+
if (!tag) tag = /^dp-/.test(subagentType) ? Object.assign({}, UNTAGGED, { tag_issue: 'no-tag' }) : UNTAGGED;
|
|
1417
1597
|
const explicitEffort = typeof ti.effort === 'string' && ti.effort ? ti.effort : null;
|
|
1418
1598
|
const parentEffort = p.effort && typeof p.effort === 'object' ? p.effort.level : null;
|
|
1419
1599
|
// agent_id must never be empty (the sink keys on it): agentId -> tool_use_id -> random.
|
|
@@ -1432,6 +1612,8 @@ function buildDispatch(p, ctx) {
|
|
|
1432
1612
|
session: p.session_id ? sha8(p.session_id) : null,
|
|
1433
1613
|
task_id: tag.task_id,
|
|
1434
1614
|
...(tag.task_id && !isValidTaskId(tag.task_id) ? { task_id_invalid: true } : {}),
|
|
1615
|
+
...(tag.task_id_inferred ? { task_id_inferred: true } : {}),
|
|
1616
|
+
...(tag.tag_issue ? { tag_issue: tag.tag_issue } : {}),
|
|
1435
1617
|
task_type: tag.task_type,
|
|
1436
1618
|
score: tag.score,
|
|
1437
1619
|
urgency: tag.urgency,
|
|
@@ -1440,7 +1622,7 @@ function buildDispatch(p, ctx) {
|
|
|
1440
1622
|
model: normalizeModel(trObj.resolvedModel) || normalizeModel(ti.model) || null,
|
|
1441
1623
|
effort: explicitEffort || parentEffort || null,
|
|
1442
1624
|
effort_inherited: !explicitEffort,
|
|
1443
|
-
subagent_type:
|
|
1625
|
+
subagent_type: subagentType,
|
|
1444
1626
|
background: ti.run_in_background === true || trObj.isAsync === true,
|
|
1445
1627
|
// Orchestrator (main session) at the moment of this dispatch. Raw session_id joins with `dieu-phoi ctx`.
|
|
1446
1628
|
session_id: typeof p.session_id === 'string' && p.session_id ? p.session_id : null,
|
|
@@ -1449,15 +1631,54 @@ function buildDispatch(p, ctx) {
|
|
|
1449
1631
|
orch_compactions: orch.compactions,
|
|
1450
1632
|
orch_cum_usd: orch.cum_usd,
|
|
1451
1633
|
}, tag.parent_task_id ? { parent_task_id: tag.parent_task_id } : {},
|
|
1634
|
+
tierFields(ctx, tag, normalizeModel(trObj.resolvedModel) || normalizeModel(ti.model) || null, explicitEffort || parentEffort || null),
|
|
1452
1635
|
// core 0.5.0: an Agent call made inside a subagent (gen-2) carries the caller's agent_id
|
|
1453
1636
|
typeof p.agent_id === 'string' && p.agent_id ? { parent_agent_id: p.agent_id } : {},
|
|
1454
1637
|
commonFields(ctx));
|
|
1455
1638
|
}
|
|
1456
1639
|
|
|
1640
|
+
/** suggested_tier / choose_tier / suggested_source / dispatched_tier / tier_mismatch, only when choose left a record. */
|
|
1641
|
+
function tierFields(ctx, tag, model, effort) {
|
|
1642
|
+
const s = tag.task_id ? readSuggestion(ctx, tag.task_id, tag.role) : null;
|
|
1643
|
+
if (!s) return {};
|
|
1644
|
+
const dispatched = tierOf(model, effort);
|
|
1645
|
+
const out = {
|
|
1646
|
+
suggested_tier: s.suggested_tier,
|
|
1647
|
+
choose_tier: s.choose_tier || s.suggested_tier,
|
|
1648
|
+
suggested_source: s.source || null,
|
|
1649
|
+
dispatched_tier: dispatched,
|
|
1650
|
+
};
|
|
1651
|
+
if (dispatched && !dispatched.includes('?')) out.tier_mismatch = dispatched !== out.choose_tier;
|
|
1652
|
+
return out;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Core 0.5.3: besides the dispatch event, a dieu-phoi dispatch with a flagged tag (tag_issue) or a task already on
|
|
1657
|
+
* its HANDOFF_CHAIN_WARN-th handoff gets a warning back to the caller as PostToolUse additionalContext (the
|
|
1658
|
+
* returned event carries it as a non-enumerable `stdout`, never written to the log).
|
|
1659
|
+
*/
|
|
1457
1660
|
function handlePostToolUse(p, ctx) {
|
|
1458
1661
|
if (p.tool_name !== 'Agent' && p.tool_name !== 'Task') return null;
|
|
1459
1662
|
const ev = buildDispatch(p, ctx);
|
|
1663
|
+
const notes = [];
|
|
1664
|
+
if (ev.tag_issue) {
|
|
1665
|
+
const ti = p.tool_input && typeof p.tool_input === 'object' ? p.tool_input : {};
|
|
1666
|
+
const known = knownTaskTypes(ctx);
|
|
1667
|
+
const tag = resolveTag(ti.prompt, known) || { tag_issue: ev.tag_issue, task_id: null, task_type: ev.task_type };
|
|
1668
|
+
notes.push(tagIssueMessage(tag, known));
|
|
1669
|
+
}
|
|
1670
|
+
if (ev.task_id && ev.role === 'exec') {
|
|
1671
|
+
const n = countTaskHandoffs(ctx, ev.task_id);
|
|
1672
|
+
if (n) ev.task_handoffs = n;
|
|
1673
|
+
if (n >= HANDOFF_CHAIN_WARN) notes.push(handoffChainMessage(ev.task_id, n));
|
|
1674
|
+
}
|
|
1460
1675
|
appendEvent(ctx, ev);
|
|
1676
|
+
if (notes.length) {
|
|
1677
|
+
Object.defineProperty(ev, 'stdout', {
|
|
1678
|
+
enumerable: false,
|
|
1679
|
+
value: JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: notes.join('\n') } }),
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1461
1682
|
return ev;
|
|
1462
1683
|
}
|
|
1463
1684
|
|
|
@@ -1626,7 +1847,7 @@ function handleSubagentStop(p, ctx) {
|
|
|
1626
1847
|
const last = lastUuidOf(lines);
|
|
1627
1848
|
if (last && seen.has(last)) return null; // same segment reported twice
|
|
1628
1849
|
sum = summarizeTranscript(lines, { afterUuids: seen, pricing: loadPricing(ctx) });
|
|
1629
|
-
tag = tagFromTranscript(lines);
|
|
1850
|
+
tag = tagFromTranscript(lines, knownTaskTypes(ctx));
|
|
1630
1851
|
}
|
|
1631
1852
|
const t = tag || UNTAGGED;
|
|
1632
1853
|
const ev = Object.assign({
|
|
@@ -1648,7 +1869,7 @@ function handleSubagentStop(p, ctx) {
|
|
|
1648
1869
|
tokens: sum ? sum.tokens : null,
|
|
1649
1870
|
cost_usd: sum ? sum.cost_usd : null,
|
|
1650
1871
|
model: sum ? sum.model : null,
|
|
1651
|
-
}, commonFields(ctx));
|
|
1872
|
+
}, t.task_id_inferred ? { task_id_inferred: true } : {}, t.tag_issue ? { tag_issue: t.tag_issue } : {}, commonFields(ctx));
|
|
1652
1873
|
// Core 0.5.2 (retro 2026-10-18): how the segment ended, measured here instead of re-reading transcripts later.
|
|
1653
1874
|
// Labels and numbers only, never text.
|
|
1654
1875
|
const why = exists ? abortReasonOf(lines) : null;
|
|
@@ -1821,7 +2042,7 @@ function readSlice(fd, start, len) {
|
|
|
1821
2042
|
* Tag of the subagent's first prompt, read from the head of its transcript only.
|
|
1822
2043
|
* Returns { found: false } while no user line is on disk yet (transcript written async), else { found: true, tag|null }.
|
|
1823
2044
|
*/
|
|
1824
|
-
function readHeadTag(file) {
|
|
2045
|
+
function readHeadTag(file, known) {
|
|
1825
2046
|
let fd;
|
|
1826
2047
|
try { fd = fs.openSync(file, 'r'); } catch (_) { return { found: false, tag: null }; }
|
|
1827
2048
|
try {
|
|
@@ -1831,7 +2052,7 @@ function readHeadTag(file) {
|
|
|
1831
2052
|
const text = readSlice(fd, 0, len).toString('utf8');
|
|
1832
2053
|
const complete = len === size ? text : text.slice(0, text.lastIndexOf('\n') + 1);
|
|
1833
2054
|
const lines = parseJsonl(complete);
|
|
1834
|
-
if (lines.some((l) => l && l.type === 'user' && l.message)) return { found: true, tag: tagFromTranscript(lines), prompt: firstPromptText(lines) };
|
|
2055
|
+
if (lines.some((l) => l && l.type === 'user' && l.message)) return { found: true, tag: tagFromTranscript(lines, known), prompt: firstPromptText(lines) };
|
|
1835
2056
|
if (len >= size || len >= WATCH_HEAD_MAX) return { found: false, tag: null };
|
|
1836
2057
|
}
|
|
1837
2058
|
} finally {
|
|
@@ -1902,7 +2123,7 @@ function watchState(p, ctx, agentId) {
|
|
|
1902
2123
|
if (!st || typeof st !== 'object') {
|
|
1903
2124
|
st = null;
|
|
1904
2125
|
if (tpath) {
|
|
1905
|
-
const h = readHeadTag(tpath);
|
|
2126
|
+
const h = readHeadTag(tpath, knownTaskTypes(ctx));
|
|
1906
2127
|
if (h.found) {
|
|
1907
2128
|
st = h.tag
|
|
1908
2129
|
? { dp: true, task_id: h.tag.task_id, run_id: h.tag.task_id ? null : runIdFromPrompt(h.prompt), role: h.tag.role, parent_task_id: h.tag.parent_task_id || null, fired: 0, t: Date.now() }
|
|
@@ -2328,7 +2549,8 @@ module.exports = {
|
|
|
2328
2549
|
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, orchLessons, orchLessonLines, abortReasonOf, completionStatus, logGate, QUOTA_PAUSE_MARKER, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, START_PREFIX, buildSessionState, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
2329
2550
|
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED,
|
|
2330
2551
|
sha8, nowIso, readJsonFile, writeFileAtomic, renameWithRetry, acquireLock, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
2331
|
-
logError, computeUserHash, userHash, commonFields, parseTag,
|
|
2552
|
+
logError, computeUserHash, userHash, commonFields, parseTag, resolveTag, knownTaskTypes, tagIssueMessage, DEFAULT_EXTRA_TYPES,
|
|
2553
|
+
recordSuggestion, readSuggestion, suggestFile, SUGGEST_MAX_AGE_MS, countTaskHandoffs, handoffChainMessage, HANDOFF_CHAIN_WARN, tierFields, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|
|
2332
2554
|
priceFor, usageBuckets, requestCost, parseJsonl, readTranscript, lastUuidOf,
|
|
2333
2555
|
firstPromptText, tagFromTranscript, summarizeTranscript,
|
|
2334
2556
|
RESULT_ROLES, BLOCKED_RESULT_NAME_RE, resultFileName, findResultFiles, resultRunDir, finalAssistantText, persistInlineResult, orchestratorUsage, orchCacheFile, findSessionTranscript, SESSION_ID_RE, HOOK_COLD_MAX_BYTES, appendEvent, eventLinesFrom, readEventLines, readEvents, readEventsBuffer, archiveFiles, readArchiveLines,
|
package/lib/commands.cjs
CHANGED
|
@@ -186,13 +186,13 @@ const TMP_STALE_MS = 3600 * 1000;
|
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
188
|
* Leftovers of writeFileAtomic (tmp written, rename never happened: process killed, or rename lost a race on
|
|
189
|
-
* Windows) under data/, data/orch, data/segments, data/plans. Older than 1 h → removed; newer → reported.
|
|
189
|
+
* Windows) under data/, data/orch, data/segments, data/plans, data/suggest. Older than 1 h → removed; newer → reported.
|
|
190
190
|
*/
|
|
191
191
|
function sweepStaleTmp(ctx) {
|
|
192
192
|
let removed = 0;
|
|
193
193
|
const left = [];
|
|
194
194
|
const now = Date.now();
|
|
195
|
-
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans]) {
|
|
195
|
+
for (const dir of [ctx.paths.data, path.join(ctx.paths.data, 'orch'), ctx.paths.segments, ctx.paths.plans, ctx.paths.suggest]) {
|
|
196
196
|
let names = [];
|
|
197
197
|
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
198
198
|
for (const n of names) {
|
|
@@ -788,6 +788,13 @@ function cmdChoose(opts) {
|
|
|
788
788
|
const tid = input.taskId;
|
|
789
789
|
core.appendEvent(ctx, overrideEvent(ctx, tid, res.task_type || String(input.type).toLowerCase(), res.suggested_tier, res.tier));
|
|
790
790
|
}
|
|
791
|
+
// core 0.5.3: the next dispatch of this task + role logs suggested_tier / choose_tier next to the tier it ran on
|
|
792
|
+
if (input.taskId && res.action === 'dispatch' && res.tier) {
|
|
793
|
+
core.recordSuggestion(ctx, {
|
|
794
|
+
task_id: input.taskId, role: res.role || input.role, suggested_tier: res.suggested_tier || res.tier, choose_tier: res.tier,
|
|
795
|
+
source: 'choose', keep_suggested: input.approved !== null && !res.overridden,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
791
798
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
792
799
|
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
793
800
|
const warn = `${typeWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
@@ -954,6 +961,19 @@ function planOverrideEvents(ctx, plan, events) {
|
|
|
954
961
|
return out;
|
|
955
962
|
}
|
|
956
963
|
|
|
964
|
+
/** Core 0.5.3: each approved step with a task_id leaves its choose tier for the step's dispatch (data/suggest/). */
|
|
965
|
+
function recordPlanSuggestions(ctx, plan) {
|
|
966
|
+
let n = 0;
|
|
967
|
+
for (const r of plan.steps) {
|
|
968
|
+
if (!r.task_id || !r.tier || r.action !== 'dispatch') continue;
|
|
969
|
+
const ok = core.recordSuggestion(ctx, {
|
|
970
|
+
task_id: r.task_id, role: r.role || 'exec', suggested_tier: r.suggested_tier || r.tier, choose_tier: r.tier, source: 'plan',
|
|
971
|
+
});
|
|
972
|
+
if (ok) n++;
|
|
973
|
+
}
|
|
974
|
+
return n;
|
|
975
|
+
}
|
|
976
|
+
|
|
957
977
|
function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
958
978
|
const file = path.join(ctx.paths.plans, `${root}.json`);
|
|
959
979
|
const prev = core.readJsonFile(file, null);
|
|
@@ -1021,6 +1041,7 @@ function cmdPlanTable(opts) {
|
|
|
1021
1041
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1022
1042
|
const ovs = rec ? planOverrideEvents(ctx, plan, base.events) : [];
|
|
1023
1043
|
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
1044
|
+
if (rec) recordPlanSuggestions(ctx, plan);
|
|
1024
1045
|
if (opts.json) {
|
|
1025
1046
|
const out = rec ? Object.assign({}, plan, {
|
|
1026
1047
|
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length },
|
|
@@ -1050,6 +1071,6 @@ function cmdPlanTable(opts) {
|
|
|
1050
1071
|
}
|
|
1051
1072
|
|
|
1052
1073
|
module.exports = {
|
|
1053
|
-
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1074
|
+
cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdCtx, cmdState, measureOrch,
|
|
1054
1075
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
1055
1076
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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>",
|