@kienntpixon/dieu-phoi 0.7.0 → 0.7.1
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 +36 -0
- package/hook.cjs +233 -11
- package/lib/aggregate.cjs +44 -2
- package/lib/choose.cjs +41 -25
- package/lib/commands.cjs +49 -12
- package/lib/knowledge.cjs +74 -18
- package/lib/setup.cjs +16 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.7.1 (2026-10-26)
|
|
4
|
+
|
|
5
|
+
Đề xuất core C1–C6 của retro 2026-10-26 (run 4b3mohix, task fgcq8dl9). `CORE_VERSION` 0.7.1; đi cùng skill 1.6.1.
|
|
6
|
+
**Thứ tự phát hành:** deploy Code.gs bản mới (cột `batch`, C5) TRƯỚC, rồi `npm i -g` + `dieu-phoi setup`.
|
|
7
|
+
|
|
8
|
+
- **`setup`: matcher `Agent|Task`** (task y5lcc5vx): hook PostToolUse/PreToolUse đăng ký cho cả tên công cụ `Agent` (Claude Code hiện tại) lẫn `Task`
|
|
9
|
+
(bản cũ). Đăng ký cũ chỉ `Agent` không còn tính là "đã đăng ký" → `setup` viết lại (không nhân đôi); `doctor` nhận cả hai.
|
|
10
|
+
|
|
11
|
+
- **C5 — verifier gộp là ô riêng** (task meq4adm7): agent `role=verify` có `batch:true` (thẻ `|batch`) không còn cộng chi phí/thời gian
|
|
12
|
+
vào task mang `task_id` của nó (tức ô `task_type × tier` của bước fast cuối) — ở cả `aggregate` lẫn `sheet/Code.gs` (cột mới `batch`).
|
|
13
|
+
`stats --json`/`retro --json` thêm `batch_verify: [{model, effort, n, tasks, cost_total_usd, cost_mean_usd, cost_p50_usd,
|
|
14
|
+
time_mean_min, time_p50_min}]` (CLI-only, ngoài parity); `stats` in bảng "verifier gộp", `retro` thêm một dòng. Thống kê theo mode
|
|
15
|
+
(`verify_min`…), `sub_usd_total`, `totals.cost_usd` vẫn tính nó. Dữ liệu cũ (không `batch`) không đổi số. **Sink: dán lại Code.gs**
|
|
16
|
+
(≈ 39 KB → minify khi deploy qua gws); Code.gs cũ làm rơi `batch` nên ô sink còn lệch CLI tới khi deploy.
|
|
17
|
+
- **C6 — delta knowledge chưa gộp + `repo:` tên ngắn** (task 1l9as0f3): `start` (khối hook, bản chữ, `--json` `unmerged_deltas`) và `state` (dòng cuối, `--json`)
|
|
18
|
+
cảnh báo `knowledge_delta: N file delta chưa gộp quá 24h …` khi có `knowledge-delta*.md` tuổi 24 h–30 ngày mà hash không nằm trong sổ `applied` của kho nào
|
|
19
|
+
(quét ~20 ms, chỉ đọc file). `knowledge merge`: header `repo:` nhận tên ngắn/basename (`pff-tailor`) nếu khớp đúng một kho đã biết (`name`, tên thư mục kho hoặc
|
|
20
|
+
basename của `roots`); HEAD từ `head:`, `roots` (mới: `meta.json` ghi checkout chính sau mỗi lần gộp có đổi) hoặc `--repo`; khớp ≥ 2 kho hoặc không khớp → lỗi rõ ràng.
|
|
21
|
+
File delta mà mọi mục đều "không đổi" nay cũng được ghi vào `applied` (để hết cảnh báo; test `knowledge-idem` cập nhật: meta.json chỉ thêm sổ `applied`).
|
|
22
|
+
Delta cũ gộp trước khi có sổ sẽ còn bị cảnh báo đến khi chạy `knowledge merge` một lần (idempotent, thành "không đổi").
|
|
23
|
+
- **C4 — sàn tier theo sở thích (`tier_floor`)**: profile.json có thể ghi `tier_floor {task_type: "model/effort"}`; `choose` lấy
|
|
24
|
+
max(sàn này, sàn an toàn, leo thang), không thăm dò xuống dưới, Thompson không chọn ô thấp hơn dù dữ liệu nghiêng về ô rẻ hơn.
|
|
25
|
+
Chỉ siết (trên trần → kẹp opus/medium; ngoài thang → bỏ). `--override`/`--approved` thấp hơn sàn vẫn giữ, có ghi chú.
|
|
26
|
+
- **C3 — telemetry của `choose`**: kết quả có thêm `score`, `reason_code` (`thompson | floor | escalation | prior | user-prior | explore |
|
|
27
|
+
plan | override | ask | unknown-type`), `thompson_score`; bản ghi `data/suggest/*.json` lưu `choose_task_type`, `choose_score`, `explored`,
|
|
28
|
+
`reason_code`, `thompson_score`, dispatch chép thành `choose_*` (chỉ ở máy). Thẻ ghi loại khác loại đã hỏi `choose` → `choose_type_mismatch:true`
|
|
29
|
+
+ PostToolUse nhắc điều phối viên.
|
|
30
|
+
- **C2 — bí danh task_type trong hook**: bảng bí danh dời sang hook.cjs (`TYPE_ALIASES`, `choose` dùng chung) và thêm
|
|
31
|
+
`scan`/`read`/`code-read`/`explore`/`scout`/`lookup` → `search`. Thẻ dispatch mang nhãn có bí danh (vd. `architecture`, `shell`, `config`,
|
|
32
|
+
`deploy`) được ghi `task_type` = loại đích, `tag_issue=aliased` + `type_alias_from` thay cho `bad-type` (số liệu gộp vào ô đúng).
|
|
33
|
+
- **C1 — trần phiên/chuỗi handoff**: cảnh báo PostToolUse dời về handoff **thứ 2** (`HANDOFF_CHAIN_WARN=2`); hook PreToolUse(Agent) mới
|
|
34
|
+
(setup đăng ký thêm, `doctor` mục `chain-gate`) **từ chối** lượt exec tiếp nối của task đã handoff ≥ 3 lần (sự kiện `gate` `deny`,
|
|
35
|
+
`reason_code=handoff-chain`, `agent_id=task:<id>`) trừ khi thẻ có `|chain_ok` (dispatch ghi `chain_ok:true`); trong subagent không áp.
|
|
36
|
+
`ctx`/`choose`: context > 400k hoặc ≥ 5 lần nén → `new_session:true` (`choose`: `orch_new_session`), `over:true`, advice "mở phiên mới
|
|
37
|
+
+ `state`" thay cho `/compact`. Thẻ cũng nhận cờ `|batch` (C5). **Cần chạy lại `dieu-phoi setup`.**
|
|
38
|
+
|
|
3
39
|
## 0.7.0 (2026-10-23)
|
|
4
40
|
|
|
5
41
|
`CORE_VERSION` 0.7.0; đi cùng skill 1.6.0. Chế độ `fast | normal | strict` theo yêu cầu (nhánh `fast-mode`, kế hoạch hzgqonzr; egm0w7mx, ue15wumm, d1m3rg7s). Sự kiện `mode` chỉ ở máy, không gửi sink. `sheet/Code.gs` ĐỔI ở phần dữ liệu học fast (d1m3rg7s): **deploy Apps Script trước khi phát hành CLI** (sink cũ đọc `by=auto` thành verdict agent).
|
package/hook.cjs
CHANGED
|
@@ -26,7 +26,7 @@ const path = require('path');
|
|
|
26
26
|
const os = require('os');
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
|
|
29
|
-
const CORE_VERSION = '0.7.
|
|
29
|
+
const CORE_VERSION = '0.7.1';
|
|
30
30
|
const SCHEMA = 1;
|
|
31
31
|
const TASK_TYPES = [
|
|
32
32
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -270,6 +270,10 @@ function parseTag(text) {
|
|
|
270
270
|
tag.role = mm[1];
|
|
271
271
|
} else if ((mm = /^parent=([A-Za-z0-9_-]{1,32})$/.exec(p))) {
|
|
272
272
|
tag.parent_task_id = mm[1]; // core 0.5.0: gen-2 helper -> the dp-exec step that spawned it
|
|
273
|
+
} else if (p === 'chain_ok') {
|
|
274
|
+
tag.chain_ok = true; // core 0.7.1 (C1): the user agreed to continue a long handoff chain
|
|
275
|
+
} else if (p === 'batch') {
|
|
276
|
+
tag.batch = true; // core 0.7.1 (C5): the one batched verifier of a fast request
|
|
273
277
|
}
|
|
274
278
|
}
|
|
275
279
|
return tag;
|
|
@@ -311,6 +315,24 @@ function taskIdError(id) {
|
|
|
311
315
|
|
|
312
316
|
const DEFAULT_EXTRA_TYPES = ['art-gen'];
|
|
313
317
|
|
|
318
|
+
// Ad-hoc labels seen in real logs → the closest fixed type (core 0.4.0 in choose; core 0.7.1: shared with the hook so a
|
|
319
|
+
// dispatch tag with such a label is mapped too, tag_issue=aliased instead of bad-type). A label maps only when its target
|
|
320
|
+
// is known (fixed list or the profile's extra_task_types), e.g. art-fix → art-gen needs art-gen in the profile.
|
|
321
|
+
const TYPE_ALIASES = {
|
|
322
|
+
'bug-fix': 'debug', bugfix: 'debug', fix: 'debug', bug: 'debug',
|
|
323
|
+
config: 'mechanical-edit', shell: 'mechanical-edit', deploy: 'mechanical-edit', ops: 'mechanical-edit', edit: 'mechanical-edit',
|
|
324
|
+
docs: 'content-write', doc: 'content-write', documentation: 'content-write', writing: 'content-write', copy: 'content-write',
|
|
325
|
+
design: 'plan-design', plan: 'plan-design', planning: 'plan-design', architecture: 'plan-design',
|
|
326
|
+
research: 'research-synth', synth: 'research-synth',
|
|
327
|
+
test: 'test-write', tests: 'test-write',
|
|
328
|
+
analysis: 'data-analysis', data: 'data-analysis',
|
|
329
|
+
impl: 'feature-impl', feature: 'feature-impl', implement: 'feature-impl',
|
|
330
|
+
'code-review': 'review',
|
|
331
|
+
'art-fix': 'art-gen', art: 'art-gen',
|
|
332
|
+
// retro 2026-10-26 (C2): scout/read labels
|
|
333
|
+
scan: 'search', read: 'search', 'code-read': 'search', explore: 'search', scout: 'search', lookup: 'search',
|
|
334
|
+
};
|
|
335
|
+
|
|
314
336
|
/** Fixed task types + the profile's extra_task_types (art-gen when no profile is readable). */
|
|
315
337
|
function knownTaskTypes(ctx) {
|
|
316
338
|
const out = TASK_TYPES.concat(DEFAULT_EXTRA_TYPES);
|
|
@@ -350,10 +372,19 @@ function resolveTag(text, known) {
|
|
|
350
372
|
// an id, not a label: id format, not a known type, and a digit or a run-file / task= mention ("planning" is a label)
|
|
351
373
|
const named = new RegExp(`(?:dp-out-|handoff-|task=)${escRe(head)}(?![a-z0-9])`).test(body.slice(first.length));
|
|
352
374
|
const idLike = TASK_ID_RE.test(head) && !types.includes(head) && (/\d/.test(head) || named);
|
|
375
|
+
let aliased = null;
|
|
353
376
|
if (idLike) issues.push('type-is-id');
|
|
354
377
|
else if (!types.includes(head)) {
|
|
355
|
-
|
|
356
|
-
|
|
378
|
+
const target = Object.prototype.hasOwnProperty.call(TYPE_ALIASES, head) ? TYPE_ALIASES[head] : null;
|
|
379
|
+
if (target && types.includes(target)) {
|
|
380
|
+
// core 0.7.1 (C2): a known ad-hoc label maps to its fixed type; the data pools instead of being lost
|
|
381
|
+
issues.push('aliased');
|
|
382
|
+
aliased = target;
|
|
383
|
+
tag.type_alias_from = parts[0];
|
|
384
|
+
} else {
|
|
385
|
+
issues.push('bad-type');
|
|
386
|
+
tag.bad_type = parts[0]; // the offending token (task_type may be replaced by a valid altType below)
|
|
387
|
+
}
|
|
357
388
|
}
|
|
358
389
|
if (!tag.task_id) {
|
|
359
390
|
issues.push('missing-task');
|
|
@@ -362,7 +393,8 @@ function resolveTag(text, known) {
|
|
|
362
393
|
tag.task_id_inferred = true;
|
|
363
394
|
}
|
|
364
395
|
}
|
|
365
|
-
if (
|
|
396
|
+
if (aliased) tag.task_type = aliased;
|
|
397
|
+
else if (idLike || tag.task_type === 'untagged') tag.task_type = altType || 'untagged';
|
|
366
398
|
else if (!types.includes(tag.task_type) && altType) tag.task_type = altType;
|
|
367
399
|
if (issues.length) tag.tag_issue = issues.join('+');
|
|
368
400
|
return tag;
|
|
@@ -375,6 +407,7 @@ function tagIssueMessage(tag, known) {
|
|
|
375
407
|
const codes = tag.tag_issue.split('+');
|
|
376
408
|
if (codes.includes('no-tag')) what.push('agent dp-* được giao mà không có thẻ `[dp:...]` ở dòng đầu prompt');
|
|
377
409
|
if (codes.includes('type-is-id')) what.push('ô đầu của thẻ là một task id, không phải loại task');
|
|
410
|
+
if (codes.includes('aliased')) what.push(`loại "${tag.type_alias_from}" không cố định — hook đã ghi thành "${tag.task_type}" (dùng đúng loại này trong thẻ)`);
|
|
378
411
|
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ệ`);
|
|
379
412
|
if (codes.includes('missing-task')) what.push('thiếu `task=<id>`');
|
|
380
413
|
const got = tag.task_id_inferred
|
|
@@ -1545,11 +1578,14 @@ function startInfo(ctx, cwd) {
|
|
|
1545
1578
|
};
|
|
1546
1579
|
const k = startKnowledgeOf(cwd);
|
|
1547
1580
|
if (k) info.knowledge = k;
|
|
1581
|
+
const d = scanUnmergedDeltas();
|
|
1582
|
+
if (d.count) info.unmerged_deltas = d;
|
|
1548
1583
|
return info;
|
|
1549
1584
|
}
|
|
1550
1585
|
|
|
1551
1586
|
function knowledgeLines(info) {
|
|
1552
|
-
|
|
1587
|
+
const own = info && info.knowledge && Array.isArray(info.knowledge.lines) ? info.knowledge.lines : [];
|
|
1588
|
+
return own.concat(unmergedDeltaLines(info && info.unmerged_deltas));
|
|
1553
1589
|
}
|
|
1554
1590
|
|
|
1555
1591
|
function orchLessonLines(info) {
|
|
@@ -1633,6 +1669,12 @@ function recordSuggestion(ctx, rec) {
|
|
|
1633
1669
|
source: prev && prev.suggested_tier ? prev.source : (rec.source || 'choose'),
|
|
1634
1670
|
ts: nowIso(),
|
|
1635
1671
|
};
|
|
1672
|
+
// core 0.7.1 (retro 2026-10-26, C3): why choose answered that tier, copied onto the dispatch as choose_*
|
|
1673
|
+
for (const [k, ok] of [['choose_task_type', (v) => typeof v === 'string' && v.length <= 40],
|
|
1674
|
+
['choose_score', (v) => typeof v === 'number' && isFinite(v)], ['explored', (v) => typeof v === 'boolean'],
|
|
1675
|
+
['reason_code', (v) => typeof v === 'string' && v.length <= 24], ['thompson_score', (v) => typeof v === 'number' && isFinite(v)]]) {
|
|
1676
|
+
if (ok(rec[k])) out[k] = rec[k];
|
|
1677
|
+
}
|
|
1636
1678
|
try {
|
|
1637
1679
|
fs.mkdirSync(ctx.paths.suggest, { recursive: true });
|
|
1638
1680
|
writeFileAtomic(file, `${JSON.stringify(out)}\n`);
|
|
@@ -1666,9 +1708,11 @@ function pruneSuggest(ctx) {
|
|
|
1666
1708
|
// ------------------------------------------------------------------ core 0.5.3: handoff chains (retro 2026-10-22)
|
|
1667
1709
|
//
|
|
1668
1710
|
// 15/63 feature-impl tasks handed off >= 2 times (19% of feature-impl cost); one chain ran 10 handoffs ($70) and
|
|
1669
|
-
// failed.
|
|
1711
|
+
// failed. Core 0.7.1 (retro 2026-10-26, C1): the warning moves to the 2nd handoff (matching the lesson "stop after the
|
|
1712
|
+
// 2nd"), and from the 3rd the global PreToolUse(Agent) gate denies another exec continuation unless the tag has chain_ok.
|
|
1670
1713
|
|
|
1671
|
-
const HANDOFF_CHAIN_WARN =
|
|
1714
|
+
const HANDOFF_CHAIN_WARN = 2;
|
|
1715
|
+
const HANDOFF_CHAIN_DENY = 3;
|
|
1672
1716
|
|
|
1673
1717
|
/** Number of agents whose `complete` event has handoff:true for this task (substring scan of events.jsonl). */
|
|
1674
1718
|
function countTaskHandoffs(ctx, taskId) {
|
|
@@ -1692,7 +1736,7 @@ function countTaskHandoffs(ctx, taskId) {
|
|
|
1692
1736
|
}
|
|
1693
1737
|
|
|
1694
1738
|
function handoffChainMessage(taskId, n) {
|
|
1695
|
-
return `dieu-phoi: task ${taskId} đã handoff ${n} lần (
|
|
1739
|
+
return `dieu-phoi: task ${taskId} đã handoff ${n} lần (cảnh báo từ ${HANDOFF_CHAIN_WARN}; từ ${HANDOFF_CHAIN_DENY} hook chặn lượt exec tiếp nối, trừ thẻ có \`chain_ok\` khi người dùng đồng ý). Đừ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\`).`;
|
|
1696
1740
|
}
|
|
1697
1741
|
|
|
1698
1742
|
// ------------------------------------------------------------------ core 0.7.0: mode per request (fast | normal | strict)
|
|
@@ -2421,6 +2465,135 @@ function relatedStatus(r) {
|
|
|
2421
2465
|
return { name: r.name, rel: r.rel, n, counts: freshCounts(hit.sections), text: `${knowledgeSummary({ sections: hit.sections }, n)} (cache)` };
|
|
2422
2466
|
}
|
|
2423
2467
|
|
|
2468
|
+
// ---- retro 2026-10-26 (C6): knowledge deltas that nobody merged
|
|
2469
|
+
|
|
2470
|
+
const DELTA_WARN_MS = 24 * 3600 * 1000; // younger than this: the session may still be about to accept/merge
|
|
2471
|
+
const DELTA_WINDOW_MS = 30 * 24 * 3600 * 1000; // older than this: stale history, not a warning any more
|
|
2472
|
+
const DELTA_SCAN_MAX = 300;
|
|
2473
|
+
|
|
2474
|
+
/** sha256 of a delta file as `knowledge merge` records it in meta.json `applied` (BOM and CRLF ignored). */
|
|
2475
|
+
function deltaHash(text) {
|
|
2476
|
+
return crypto.createHash('sha256').update(String(text).replace(/^/, '').replace(/\r\n?/g, '\n')).digest('hex');
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/** The `repo:` header of a delta text (before its first `## `), unquoted; null when absent. */
|
|
2480
|
+
function deltaRepoOf(text) {
|
|
2481
|
+
for (const line of String(text).replace(/^/, '').split(/\r?\n/)) {
|
|
2482
|
+
if (/^##\s/.test(line)) return null;
|
|
2483
|
+
const m = /^\s*repo\s*:\s*(.+?)\s*$/i.exec(line);
|
|
2484
|
+
if (m) return m[1].replace(/[.;,]+$/, '').replace(/^[`"'](.*)[`"']$/, '$1').trim() || null;
|
|
2485
|
+
}
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
/** A `repo:` value that is a short name (no path separator, no drive), e.g. `pff-tailor`. */
|
|
2490
|
+
function isBareRepoName(v) {
|
|
2491
|
+
return typeof v === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(v.trim());
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/** Every knowledge store under projectsDir(): { dir_name, dir, name, key, roots, applied:Set<sha256> } — file reads only. */
|
|
2495
|
+
function storeCatalog() {
|
|
2496
|
+
const root = projectsDir();
|
|
2497
|
+
let names;
|
|
2498
|
+
try { names = fs.readdirSync(root); } catch (_) { return []; }
|
|
2499
|
+
const out = [];
|
|
2500
|
+
for (const n of names) {
|
|
2501
|
+
const dir = path.join(root, n);
|
|
2502
|
+
if (!isStoreDirName(n) || !isDir(dir)) continue;
|
|
2503
|
+
const meta = readJsonFile(path.join(dir, 'meta.json'), null);
|
|
2504
|
+
const idx = readJsonFile(path.join(dir, 'index.json'), null);
|
|
2505
|
+
if (!meta && !idx) continue;
|
|
2506
|
+
const m = meta && typeof meta === 'object' ? meta : {};
|
|
2507
|
+
const i = idx && typeof idx === 'object' ? idx : {};
|
|
2508
|
+
out.push({
|
|
2509
|
+
dir_name: n,
|
|
2510
|
+
dir: toSlash(dir),
|
|
2511
|
+
name: String(i.name || m.name || n.replace(/-[0-9a-f]{8}$/, '')),
|
|
2512
|
+
key: i.key || (Array.isArray(m.keys) ? m.keys[0] : null) || null,
|
|
2513
|
+
roots: Array.isArray(m.roots) ? m.roots.filter((r) => typeof r === 'string' && r) : [],
|
|
2514
|
+
applied: new Set((Array.isArray(m.applied) ? m.applied : []).map((a) => a && a.sha256).filter(Boolean)),
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
return out;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Known store a bare repo name refers to: its `name`, its directory name, or the basename of a recorded root equals
|
|
2522
|
+
* `name` (case-insensitive). { matches, store } — store only when exactly one store matches.
|
|
2523
|
+
*/
|
|
2524
|
+
function storeByName(name, stores) {
|
|
2525
|
+
const want = String(name).trim().toLowerCase();
|
|
2526
|
+
const matches = (stores || storeCatalog()).filter((s) => s.name.toLowerCase() === want || s.dir_name.toLowerCase() === want
|
|
2527
|
+
|| s.roots.some((r) => path.basename(r).toLowerCase() === want));
|
|
2528
|
+
return { matches, store: matches.length === 1 ? matches[0] : null };
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
/**
|
|
2532
|
+
* Delta files in run dirs that are 24 h .. 30 d old and that no store has applied (hash in meta.json `applied`).
|
|
2533
|
+
* { count, runs: [{ run, n, oldest_ms, repos: { name: n } }], truncated } — never throws; count 0 when all is merged.
|
|
2534
|
+
*/
|
|
2535
|
+
function scanUnmergedDeltas(opts) {
|
|
2536
|
+
const o = opts || {};
|
|
2537
|
+
const now = o.now || Date.now();
|
|
2538
|
+
const warnMs = o.warnMs === undefined ? DELTA_WARN_MS : o.warnMs;
|
|
2539
|
+
const windowMs = o.windowMs === undefined ? DELTA_WINDOW_MS : o.windowMs;
|
|
2540
|
+
const res = { count: 0, runs: [], truncated: false };
|
|
2541
|
+
try {
|
|
2542
|
+
const runs = runsDir();
|
|
2543
|
+
const applied = new Set();
|
|
2544
|
+
for (const s of storeCatalog()) for (const h of s.applied) applied.add(h);
|
|
2545
|
+
let seen = 0;
|
|
2546
|
+
for (const run of fs.readdirSync(runs)) {
|
|
2547
|
+
const dir = path.join(runs, run);
|
|
2548
|
+
let st;
|
|
2549
|
+
try { st = fs.statSync(dir); } catch (_) { continue; }
|
|
2550
|
+
if (!st.isDirectory() || now - st.mtimeMs > windowMs) continue;
|
|
2551
|
+
let names;
|
|
2552
|
+
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
2553
|
+
const row = { run, n: 0, oldest_ms: 0, repos: {} };
|
|
2554
|
+
for (const f of names) {
|
|
2555
|
+
if (!/^knowledge-delta.*\.md$/i.test(f)) continue;
|
|
2556
|
+
const file = path.join(dir, f);
|
|
2557
|
+
let fst;
|
|
2558
|
+
try { fst = fs.statSync(file); } catch (_) { continue; }
|
|
2559
|
+
const age = now - fst.mtimeMs;
|
|
2560
|
+
if (age < warnMs || age > windowMs) continue;
|
|
2561
|
+
if (seen++ >= DELTA_SCAN_MAX) { res.truncated = true; break; }
|
|
2562
|
+
const text = readTextFile(file);
|
|
2563
|
+
if (text == null || applied.has(deltaHash(text))) continue;
|
|
2564
|
+
const repo = deltaRepoOf(text);
|
|
2565
|
+
const label = repo ? (isBareRepoName(repo) ? repo : path.basename(repo.replace(/[\\/]+$/, ''))) : '?';
|
|
2566
|
+
row.n++;
|
|
2567
|
+
row.oldest_ms = Math.max(row.oldest_ms, age);
|
|
2568
|
+
row.repos[label] = (row.repos[label] || 0) + 1;
|
|
2569
|
+
}
|
|
2570
|
+
if (row.n) { res.runs.push(row); res.count += row.n; }
|
|
2571
|
+
if (res.truncated) break;
|
|
2572
|
+
}
|
|
2573
|
+
res.runs.sort((a, b) => b.n - a.n || b.oldest_ms - a.oldest_ms);
|
|
2574
|
+
} catch (e) {
|
|
2575
|
+
res.error = e.message;
|
|
2576
|
+
}
|
|
2577
|
+
return res;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
function ageText(ms) {
|
|
2581
|
+
return ms >= 48 * 3600 * 1000 ? `${Math.floor(ms / 86400000)} ngày` : `${Math.floor(ms / 3600000)} giờ`;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/** One warning line (as an array, empty when nothing is pending) for the start block and `state`. */
|
|
2585
|
+
function unmergedDeltaLines(scan) {
|
|
2586
|
+
if (!scan || !scan.count) return [];
|
|
2587
|
+
const oldest = Math.max(...scan.runs.map((r) => r.oldest_ms));
|
|
2588
|
+
const repos = {};
|
|
2589
|
+
for (const r of scan.runs) for (const [k, v] of Object.entries(r.repos)) repos[k] = (repos[k] || 0) + v;
|
|
2590
|
+
const repoText = Object.entries(repos).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, v]) => `${k} ×${v}`).join(', ');
|
|
2591
|
+
const runText = scan.runs.slice(0, 3).map((r) => `${r.run} ×${r.n}`).join(', ') + (scan.runs.length > 3 ? ` (+${scan.runs.length - 3} run)` : '');
|
|
2592
|
+
const more = scan.truncated ? ' (chỉ quét một phần)' : '';
|
|
2593
|
+
return [`knowledge_delta: ${scan.count} file delta chưa gộp quá 24h${more} — run ${runText}; kho ${repoText}; cũ nhất ${ageText(oldest)}. `
|
|
2594
|
+
+ `Gộp: \`dieu-phoi knowledge merge --run ${scan.runs[0].run} --dry-run\` rồi bỏ --dry-run (hoặc verdict accept); bỏ qua nếu cố ý không gộp.`];
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2424
2597
|
/**
|
|
2425
2598
|
* Knowledge lines for the start block of the repo at `cwd`; null outside a repo or when the repo has no store.
|
|
2426
2599
|
* Refreshes this repo only (at most one git diff-tree, on a cache miss, bounded by HOOK_DIFF_TIMEOUT_MS; never git
|
|
@@ -2518,6 +2691,9 @@ function buildDispatch(p, ctx) {
|
|
|
2518
2691
|
...(tag.task_id && !isValidTaskId(tag.task_id) ? { task_id_invalid: true } : {}),
|
|
2519
2692
|
...(tag.task_id_inferred ? { task_id_inferred: true } : {}),
|
|
2520
2693
|
...(tag.tag_issue ? { tag_issue: tag.tag_issue } : {}),
|
|
2694
|
+
...(tag.type_alias_from ? { type_alias_from: tag.type_alias_from } : {}),
|
|
2695
|
+
...(tag.chain_ok ? { chain_ok: true } : {}),
|
|
2696
|
+
...(tag.batch ? { batch: true } : {}),
|
|
2521
2697
|
task_type: tag.task_type,
|
|
2522
2698
|
score: tag.score,
|
|
2523
2699
|
urgency: tag.urgency,
|
|
@@ -2543,6 +2719,11 @@ function buildDispatch(p, ctx) {
|
|
|
2543
2719
|
commonFields(ctx));
|
|
2544
2720
|
}
|
|
2545
2721
|
|
|
2722
|
+
/** core 0.7.1 (C3): the tier came from `choose` for another task type than the tag carries. */
|
|
2723
|
+
function chooseTypeMismatchMessage(ev) {
|
|
2724
|
+
return `dieu-phoi: task ${ev.task_id} được \`choose\` tính cho loại "${ev.choose_task_type}" nhưng thẻ ghi "${ev.task_type}" — tier ${ev.choose_tier} có thể không hợp loại này. Lần sau gọi \`choose --type ${ev.task_type}\` (hoặc sửa thẻ cho đúng loại choose trả).`;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2546
2727
|
/** core 0.7.0: mode of the task at the moment of this dispatch (kept by later tries), only when the tag carries a task_id. */
|
|
2547
2728
|
function modeFields(ctx, tag) {
|
|
2548
2729
|
if (!tag || !tag.task_id || !isValidTaskId(tag.task_id)) return {};
|
|
@@ -2561,6 +2742,15 @@ function tierFields(ctx, tag, model, effort) {
|
|
|
2561
2742
|
dispatched_tier: dispatched,
|
|
2562
2743
|
};
|
|
2563
2744
|
if (dispatched && !dispatched.includes('?')) out.tier_mismatch = dispatched !== out.choose_tier;
|
|
2745
|
+
// core 0.7.1 (C3): choose's own inputs/reason, and whether choose was asked about another task type
|
|
2746
|
+
if (typeof s.choose_task_type === 'string') {
|
|
2747
|
+
out.choose_task_type = s.choose_task_type;
|
|
2748
|
+
if (tag.task_type && tag.task_type !== s.choose_task_type) out.choose_type_mismatch = true;
|
|
2749
|
+
}
|
|
2750
|
+
if (typeof s.choose_score === 'number') out.choose_score = s.choose_score;
|
|
2751
|
+
if (typeof s.explored === 'boolean') out.choose_explored = s.explored;
|
|
2752
|
+
if (typeof s.reason_code === 'string') out.choose_reason = s.reason_code;
|
|
2753
|
+
if (typeof s.thompson_score === 'number') out.choose_thompson_score = s.thompson_score;
|
|
2564
2754
|
return out;
|
|
2565
2755
|
}
|
|
2566
2756
|
|
|
@@ -2579,6 +2769,7 @@ function handlePostToolUse(p, ctx) {
|
|
|
2579
2769
|
const tag = resolveTag(ti.prompt, known) || { tag_issue: ev.tag_issue, task_id: null, task_type: ev.task_type };
|
|
2580
2770
|
notes.push(tagIssueMessage(tag, known));
|
|
2581
2771
|
}
|
|
2772
|
+
if (ev.choose_type_mismatch) notes.push(chooseTypeMismatchMessage(ev));
|
|
2582
2773
|
if (ev.task_id && ev.role === 'exec') {
|
|
2583
2774
|
const n = countTaskHandoffs(ctx, ev.task_id);
|
|
2584
2775
|
if (n) ev.task_handoffs = n;
|
|
@@ -3287,6 +3478,36 @@ function gateReserve(dir, child) {
|
|
|
3287
3478
|
return 0;
|
|
3288
3479
|
}
|
|
3289
3480
|
|
|
3481
|
+
/**
|
|
3482
|
+
* Core 0.7.1 (C1): global PreToolUse(Agent) in the orchestrating session. An exec dispatch of a task that already
|
|
3483
|
+
* handed off HANDOFF_CHAIN_DENY times is denied (the 4th, 5th… segment of one chain cost $15-36 per task in session
|
|
3484
|
+
* 36cee993) unless the tag carries `chain_ok`. Inside a subagent (agent_id) this does nothing: --gate handles gen-2.
|
|
3485
|
+
*/
|
|
3486
|
+
function handleChainGate(p, ctx) {
|
|
3487
|
+
if (p.tool_name !== 'Agent' && p.tool_name !== 'Task') return null;
|
|
3488
|
+
if (watchAgentId(p)) return null;
|
|
3489
|
+
const ti = p.tool_input && typeof p.tool_input === 'object' ? p.tool_input : {};
|
|
3490
|
+
const tag = resolveTag(ti.prompt, knownTaskTypes(ctx));
|
|
3491
|
+
if (!tag || tag.role !== 'exec' || !tag.task_id || !isValidTaskId(tag.task_id) || tag.chain_ok) return null;
|
|
3492
|
+
const n = countTaskHandoffs(ctx, tag.task_id);
|
|
3493
|
+
if (n < HANDOFF_CHAIN_DENY) return null;
|
|
3494
|
+
const ev = logGate(ctx, p, `task:${tag.task_id}`, { task_id: tag.task_id, role: 'exec' },
|
|
3495
|
+
{ gate_action: 'deny', reason_code: 'handoff-chain', children: n });
|
|
3496
|
+
return {
|
|
3497
|
+
denied: true,
|
|
3498
|
+
event: ev,
|
|
3499
|
+
stdout: JSON.stringify({
|
|
3500
|
+
hookSpecificOutput: {
|
|
3501
|
+
hookEventName: 'PreToolUse',
|
|
3502
|
+
permissionDecision: 'deny',
|
|
3503
|
+
permissionDecisionReason: `dieu-phoi: task ${tag.task_id} đã handoff ${n} lần (trần ${HANDOFF_CHAIN_DENY}) — không giao thêm exec tiếp nối. `
|
|
3504
|
+
+ `Giao planner (dp-scout) đọc handoff-${tag.task_id}.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\`). `
|
|
3505
|
+
+ 'Chỉ khi người dùng đồng ý nối tiếp: thêm `|chain_ok` vào thẻ.',
|
|
3506
|
+
},
|
|
3507
|
+
}),
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3290
3511
|
/** Mark the reservation of `child` as a real spawn. Returns true when its slot was found. */
|
|
3291
3512
|
function gateConfirm(ctx, key, child) {
|
|
3292
3513
|
if (!key || !child) return false;
|
|
@@ -3443,6 +3664,7 @@ function runHook(opts) {
|
|
|
3443
3664
|
if (o.mode === 'watch') return handleWatch(p, ctx, o); // { stdout } or null
|
|
3444
3665
|
if (o.mode === 'gate') return handleGate(p, ctx, o); // { stdout } or null
|
|
3445
3666
|
if (o.mode === 'gate-release') return handleGateRelease(p, ctx); // { released } or null, prints nothing
|
|
3667
|
+
if (name === 'PreToolUse') return handleChainGate(p, ctx); // { stdout } (deny) or null
|
|
3446
3668
|
if (name === 'SessionStart') return handleSessionStart(p, ctx); // { stdout } or null, never an event
|
|
3447
3669
|
if (name === 'UserPromptSubmit') return handleUserPromptSubmit(p, ctx); // { stdout } or null
|
|
3448
3670
|
if (name === 'SubagentStop') ev = handleSubagentStop(p, ctx);
|
|
@@ -3600,16 +3822,16 @@ module.exports = {
|
|
|
3600
3822
|
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS, MODE_ASK_LINE, isMode, rootModeFromEvents, rootModeFromText, dispatchModeFromEvents, taskModeInfo, planOfTask, modeFields,
|
|
3601
3823
|
modeSuggestion, modeSuggestLine, fastFailRate, rootTaskIds, FAST_FAIL_WINDOW, FAST_FAIL_MIN_N, FAST_FAIL_RATE,
|
|
3602
3824
|
testCommandInfo, testTimeOf, testSFromText,
|
|
3603
|
-
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED,
|
|
3825
|
+
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED, TYPE_ALIASES,
|
|
3604
3826
|
sha8, nowIso, readJsonFile, writeFileAtomic, renameWithRetry, acquireLock, acquireFileLock, withFileLock, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
3605
3827
|
logError, computeUserHash, userHash, commonFields, parseTag, resolveTag, knownTaskTypes, tagIssueMessage, DEFAULT_EXTRA_TYPES,
|
|
3606
|
-
recordSuggestion, readSuggestion, suggestFile, SUGGEST_MAX_AGE_MS, countTaskHandoffs, handoffChainMessage, HANDOFF_CHAIN_WARN, tierFields, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|
|
3828
|
+
recordSuggestion, readSuggestion, suggestFile, SUGGEST_MAX_AGE_MS, countTaskHandoffs, handoffChainMessage, HANDOFF_CHAIN_WARN, HANDOFF_CHAIN_DENY, handleChainGate, tierFields, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|
|
3607
3829
|
priceFor, usageBuckets, requestCost, parseJsonl, readTranscript, lastUuidOf,
|
|
3608
3830
|
firstPromptText, tagFromTranscript, summarizeTranscript,
|
|
3609
3831
|
RESULT_ROLES, BLOCKED_RESULT_NAME_RE, resultFileName, findResultFiles, resultRunDir, finalAssistantText, persistInlineResult, orchestratorUsage, orchCacheFile, findSessionTranscript, SESSION_ID_RE, HOOK_COLD_MAX_BYTES, appendEvent, EVENTS_LOCK_WAIT_MS, eventLinesFrom, readEventLines, readEvents, readEventsBuffer, archiveFiles, readArchiveLines,
|
|
3610
3832
|
loadSyncOffset, saveSyncOffset, recordRejected, unsyncedInfo, loadSegments, saveSegments, segmentFile,
|
|
3611
3833
|
HANDOFF_MARKER, HANDOFF_THRESHOLDS, watchThresholds, GATE_MAX_CHILDREN, HELPER_AGENT_TYPE, watchAgentId, subagentTranscriptPath, readHeadTag, lastContextTokens, watchState, handoffMessage, handleWatch, handleGate, handleGateRelease, gateDir, gateSlots, gateReserve, gateConfirm, GATE_PENDING_MS,
|
|
3612
|
-
globMatch, pathspecOf, diffShas, dirtyFiles, computeFreshness, addDirty, indexStamp, freshnessCacheFile, readFreshCache, cachedFor, freshCounts, knowledgeSummary, shortFileList, readStoreIndex, relatedStatus, startKnowledge, FRESH_CACHE_MAX, DIFF_TIMEOUT_MS, HOOK_DIFF_TIMEOUT_MS, DIRTY_TIMEOUT_MS,
|
|
3834
|
+
globMatch, pathspecOf, diffShas, dirtyFiles, computeFreshness, addDirty, indexStamp, freshnessCacheFile, readFreshCache, cachedFor, freshCounts, knowledgeSummary, shortFileList, readStoreIndex, relatedStatus, startKnowledge, deltaHash, deltaRepoOf, isBareRepoName, storeCatalog, storeByName, scanUnmergedDeltas, unmergedDeltaLines, DELTA_WARN_MS, FRESH_CACHE_MAX, DIFF_TIMEOUT_MS, HOOK_DIFF_TIMEOUT_MS, DIRTY_TIMEOUT_MS,
|
|
3613
3835
|
projectsDir, findGit, originUrlFromConfig, normalizeRemoteUrl, resolveProject, readHead, repoFromBrief, projectFields, nativePath, isStoreDirName, aliasesFile, loadAliases, keyDirName,
|
|
3614
3836
|
extractAgentId, hasAssistantUsage, hasDispatch, buildDispatch, handlePostToolUse, handleSubagentStop, maybeSpawnSync, runHook, runSync,
|
|
3615
3837
|
};
|
package/lib/aggregate.cjs
CHANGED
|
@@ -173,6 +173,8 @@ function buildAgents(events) {
|
|
|
173
173
|
task_id: pick('task_id') || (verdict && verdict.task_id) || (a.steers[0] && a.steers[0].task_id) || null,
|
|
174
174
|
try: Number(pick('try')) || 1,
|
|
175
175
|
role: pick('role') || 'exec',
|
|
176
|
+
// core 0.7.1 (C5): the one batched verifier of a fast request (tag `|batch`, dispatch `batch:true`)
|
|
177
|
+
batch: pick('batch') === true,
|
|
176
178
|
score: d.score !== undefined ? d.score : null,
|
|
177
179
|
urgency: d.urgency || null,
|
|
178
180
|
model: core.normalizeModel(cl.model) || core.normalizeModel(d.model) || null,
|
|
@@ -284,7 +286,9 @@ function buildTasks(agents, userVerdicts, autoVerdicts) {
|
|
|
284
286
|
const autoOverturned = autoAccept && user.some((v) => v.outcome === 'reject' && tsOf(v) > Math.min(...autoTs));
|
|
285
287
|
const lastUser = user[user.length - 1] || null;
|
|
286
288
|
const accepted = lastUser ? lastUser.outcome === 'accept' : attempts.some((a) => a.outcome === 'pass');
|
|
287
|
-
|
|
289
|
+
// core 0.7.1 (C5): the batched verifier checks every fast step of a request; it is billed to its own cell
|
|
290
|
+
// (batchVerifyCells), never to the task_type x tier of the fast step whose task_id it carries.
|
|
291
|
+
const costed = t.agents.filter((a) => (a.completed || a.outcome) && !isBatchVerify(a));
|
|
288
292
|
tasks.push({
|
|
289
293
|
key: t.key,
|
|
290
294
|
task_id: t.task_id,
|
|
@@ -315,6 +319,43 @@ function buildTasks(agents, userVerdicts, autoVerdicts) {
|
|
|
315
319
|
return tasks;
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/** core 0.7.1 (C5): a role=verify agent dispatched with `batch:true`. */
|
|
323
|
+
function isBatchVerify(a) {
|
|
324
|
+
return !!a && a.role === 'verify' && a.batch === true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* core 0.7.1 (C5), CLI only: batched verifiers grouped by their own model x effort. Agents with a complete or a
|
|
329
|
+
* verdict; cost/time per agent = sum over its complete segments (null when unknown, left out of the percentiles).
|
|
330
|
+
*/
|
|
331
|
+
function batchVerifyCells(agents) {
|
|
332
|
+
const groups = new Map();
|
|
333
|
+
for (const a of agents || []) {
|
|
334
|
+
if (!isBatchVerify(a) || !(a.completed || a.outcome)) continue;
|
|
335
|
+
const key = `${a.model || '?'}|${a.effort || '?'}`;
|
|
336
|
+
if (!groups.has(key)) groups.set(key, { model: a.model, effort: a.effort, agents: [] });
|
|
337
|
+
groups.get(key).agents.push(a);
|
|
338
|
+
}
|
|
339
|
+
const out = [];
|
|
340
|
+
for (const g of groups.values()) {
|
|
341
|
+
const costs = g.agents.map((a) => a.cost_usd);
|
|
342
|
+
const times = g.agents.map((a) => (a.duration_s === null ? null : a.duration_s / 60));
|
|
343
|
+
out.push({
|
|
344
|
+
model: g.model,
|
|
345
|
+
effort: g.effort,
|
|
346
|
+
n: g.agents.length,
|
|
347
|
+
tasks: [...new Set(g.agents.map((a) => a.task_id).filter(Boolean))].length,
|
|
348
|
+
cost_total_usd: round6(costs.reduce((s, c) => s + (typeof c === 'number' && isFinite(c) ? c : 0), 0)),
|
|
349
|
+
cost_mean_usd: round6(mean(costs)),
|
|
350
|
+
cost_p50_usd: round6(percentile(costs, 0.5)),
|
|
351
|
+
time_mean_min: round6(mean(times)),
|
|
352
|
+
time_p50_min: round6(percentile(times, 0.5)),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
out.sort((a, b) => String(a.model).localeCompare(String(b.model)) || String(a.effort).localeCompare(String(b.effort)));
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
318
359
|
function cellKey(type, model, effort) {
|
|
319
360
|
return `${type}|${model || '?'}|${effort || '?'}`;
|
|
320
361
|
}
|
|
@@ -528,12 +569,13 @@ function analyze(events, opts) {
|
|
|
528
569
|
return {
|
|
529
570
|
agents, tasks, cells, overrides, userVerdicts, autoVerdicts, steerEvents: steers, steers: buildSteers(tasks, steers, agents),
|
|
530
571
|
orchestration: orchestrationSummary(events),
|
|
572
|
+
batch_verify: batchVerifyCells(agents),
|
|
531
573
|
};
|
|
532
574
|
}
|
|
533
575
|
|
|
534
576
|
module.exports = {
|
|
535
577
|
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, ABORTED, QUOTA_REASON_RE, HANDOFF_REASON_RE, discardedTaskIds, withoutDiscarded, CANONICAL_CELL_FIELDS, normalizeVerdict, isSelf, isInternalComplete, buildAgents, buildTasks,
|
|
536
|
-
buildCells, buildSteers, canonicalCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
578
|
+
buildCells, buildSteers, canonicalCells, isBatchVerify, batchVerifyCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
537
579
|
};
|
|
538
580
|
|
|
539
581
|
// ------------------------------------------------------------------ core 0.7.0 (s2): stats split by mode
|
package/lib/choose.cjs
CHANGED
|
@@ -132,9 +132,19 @@ function loadProfile(skillDir) {
|
|
|
132
132
|
const v = Number(r.orch_ctx_soft_cap);
|
|
133
133
|
if (isFinite(v) && v > 0) softCap = Math.min(DEFAULT_ORCH_CTX_SOFT_CAP, Math.floor(v));
|
|
134
134
|
}
|
|
135
|
+
// core 0.7.1 (retro 2026-10-26, C4): per-type tier floor from the user's preference (e.g. art-gen -> opus/medium).
|
|
136
|
+
// Only tightens: choose takes max(floor, safety floors, escalation); an entry off the ladder is ignored.
|
|
137
|
+
const tierFloor = {};
|
|
138
|
+
if (r.tier_floor && typeof r.tier_floor === 'object' && !Array.isArray(r.tier_floor)) {
|
|
139
|
+
for (const [k, v] of Object.entries(r.tier_floor)) {
|
|
140
|
+
if (k.startsWith('_')) continue;
|
|
141
|
+
const idx = parsePrior(v);
|
|
142
|
+
if (idx !== null) tierFloor[String(k).toLowerCase()] = idx;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
135
145
|
return {
|
|
136
146
|
source, dept: r.dept || null, priors, lambda, explore_rate: explore, time_cap_min: timeCap, extra_task_types: extra,
|
|
137
|
-
orch_ctx_soft_cap: softCap,
|
|
147
|
+
orch_ctx_soft_cap: softCap, tier_floor: tierFloor,
|
|
138
148
|
};
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -302,20 +312,8 @@ function estimateFor(idx, cell, pricing) {
|
|
|
302
312
|
|
|
303
313
|
// ------------------------------------------------------------------ task type guard
|
|
304
314
|
|
|
305
|
-
// Ad-hoc labels
|
|
306
|
-
|
|
307
|
-
const TYPE_ALIASES = {
|
|
308
|
-
'bug-fix': 'debug', bugfix: 'debug', fix: 'debug', bug: 'debug',
|
|
309
|
-
config: 'mechanical-edit', shell: 'mechanical-edit', deploy: 'mechanical-edit', ops: 'mechanical-edit', edit: 'mechanical-edit',
|
|
310
|
-
docs: 'content-write', doc: 'content-write', documentation: 'content-write', writing: 'content-write', copy: 'content-write',
|
|
311
|
-
design: 'plan-design', plan: 'plan-design', planning: 'plan-design', architecture: 'plan-design',
|
|
312
|
-
research: 'research-synth', synth: 'research-synth',
|
|
313
|
-
test: 'test-write', tests: 'test-write',
|
|
314
|
-
analysis: 'data-analysis', data: 'data-analysis',
|
|
315
|
-
impl: 'feature-impl', feature: 'feature-impl', implement: 'feature-impl',
|
|
316
|
-
'code-review': 'review',
|
|
317
|
-
'art-fix': 'art-gen', art: 'art-gen',
|
|
318
|
-
};
|
|
315
|
+
// Ad-hoc labels → the closest fixed type: the table lives in hook.cjs (core 0.7.1) so dispatch tags map the same way.
|
|
316
|
+
const TYPE_ALIASES = core.TYPE_ALIASES;
|
|
319
317
|
|
|
320
318
|
/**
|
|
321
319
|
* { type, known, mapped_from, warning }: a fixed/extra type passes; an ad-hoc label with a known alias target
|
|
@@ -451,6 +449,11 @@ function choose(opts) {
|
|
|
451
449
|
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'); }
|
|
452
450
|
if (!reversible && role === 'exec') { safetyFloor = Math.max(safetyFloor, OPUS_MEDIUM); floorWhy.push('khó hoàn tác → tối thiểu opus/medium'); }
|
|
453
451
|
|
|
452
|
+
// Preference floor from the profile (tier_floor): never lowers a safety floor (max), never above the ceiling.
|
|
453
|
+
const prefFloor = known && profile.tier_floor && Number.isInteger(profile.tier_floor[type])
|
|
454
|
+
? Math.min(TOP, profile.tier_floor[type]) : 0;
|
|
455
|
+
const prefWhy = prefFloor > 0 ? `tier_floor của profile: ${type} tối thiểu ${tierName(prefFloor)}` : null;
|
|
456
|
+
|
|
454
457
|
const noExploreWhy = [];
|
|
455
458
|
if (score >= 9) noExploreWhy.push(`score ${score}≥9`);
|
|
456
459
|
if (!verifiable) noExploreWhy.push('không kiểm chứng bằng máy');
|
|
@@ -489,6 +492,7 @@ function choose(opts) {
|
|
|
489
492
|
tier: tierName(idx),
|
|
490
493
|
role,
|
|
491
494
|
task_type: type,
|
|
495
|
+
score,
|
|
492
496
|
mode,
|
|
493
497
|
verify_plan: vp ? vp.verify_plan : null,
|
|
494
498
|
verify_reason: vp ? vp.verify_reason : null,
|
|
@@ -501,7 +505,7 @@ function choose(opts) {
|
|
|
501
505
|
|
|
502
506
|
// Unknown task type (not fixed, not in the profile, no alias): its stats would never pool — ask first.
|
|
503
507
|
if (!known) {
|
|
504
|
-
return finish(Math.max(prior, safetyFloor), rt.warning, false, { action: 'ask_user', unknown_type: true });
|
|
508
|
+
return finish(Math.max(prior, safetyFloor), rt.warning, false, { action: 'ask_user', unknown_type: true, reason_code: 'unknown-type' });
|
|
505
509
|
}
|
|
506
510
|
|
|
507
511
|
if (overrideIdx !== null && overrideIdx < safetyFloor) {
|
|
@@ -509,34 +513,37 @@ function choose(opts) {
|
|
|
509
513
|
}
|
|
510
514
|
// Escalation beats override/approval: the user must decide first.
|
|
511
515
|
if (esc.ask) {
|
|
512
|
-
const userIdx = overrideIdx !== null ? overrideIdx : (approvedIdx !== null ? approvedIdx :
|
|
516
|
+
const userIdx = overrideIdx !== null ? overrideIdx : (approvedIdx !== null ? approvedIdx : prefFloor);
|
|
513
517
|
const idx = Math.max(userIdx, esc.floor, safetyFloor);
|
|
514
|
-
return finish(idx, esc.note, false, { action: 'ask_user', fails: esc.fails, tried: esc.tried });
|
|
518
|
+
return finish(idx, esc.note, false, { action: 'ask_user', fails: esc.fails, tried: esc.tried, reason_code: 'ask' });
|
|
515
519
|
}
|
|
516
520
|
|
|
517
521
|
// Tier approved by the user in the plan table: no Thompson/exploration; only floors can raise it.
|
|
518
522
|
if (approvedIdx !== null) {
|
|
519
523
|
const forced = Math.max(safetyFloor, esc.floor);
|
|
520
524
|
if (approvedIdx >= forced) {
|
|
521
|
-
|
|
522
|
-
|
|
525
|
+
let n = esc.note ? `; ${esc.note}` : '';
|
|
526
|
+
if (approvedIdx < prefFloor) n += `; dưới ${prefWhy} (người dùng duyệt nên giữ)`;
|
|
527
|
+
return finish(approvedIdx, `người dùng đã duyệt ${tierName(approvedIdx)} trong bảng phân công${n}`, false, { deviation: false, reason_code: 'plan' });
|
|
523
528
|
}
|
|
524
529
|
const why = [];
|
|
525
530
|
if (safetyFloor > approvedIdx) why.push(`sàn an toàn ${tierName(safetyFloor)} (${floorWhy.join(', ')})`);
|
|
526
531
|
if (esc.floor > approvedIdx) why.push(esc.note || `leo thang → ${tierName(esc.floor)}`);
|
|
527
|
-
return finish(forced, `lệch khỏi tier đã duyệt ${tierName(approvedIdx)} → ${tierName(forced)}: ${why.join('; ')}`, false, { deviation: true, approved_tier: tierName(approvedIdx) });
|
|
532
|
+
return finish(forced, `lệch khỏi tier đã duyệt ${tierName(approvedIdx)} → ${tierName(forced)}: ${why.join('; ')}`, false, { deviation: true, approved_tier: tierName(approvedIdx), reason_code: 'plan' });
|
|
528
533
|
}
|
|
529
534
|
if (esc.note && !esc.ask) notes.push(esc.note);
|
|
530
535
|
|
|
531
|
-
const floor = Math.max(safetyFloor, esc.ask ? 0 : esc.floor);
|
|
536
|
+
const floor = Math.max(safetyFloor, esc.ask ? 0 : esc.floor, prefFloor);
|
|
532
537
|
const base = Math.min(TOP, Math.max(prior, floor));
|
|
533
538
|
if (base > prior) {
|
|
534
539
|
const why = floorWhy.slice();
|
|
535
540
|
if (esc.floor > prior && esc.floor >= safetyFloor && !esc.ask) why.push(`leo thang → ${tierName(esc.floor)}`);
|
|
541
|
+
if (prefFloor > prior) why.push(prefWhy);
|
|
536
542
|
notes.unshift(`sàn ${tierName(base)} cao hơn ${priorWhy} ${tierName(prior)}: ${why.join(', ') || 'luật an toàn'}`);
|
|
537
543
|
} else if (floorWhy.length) {
|
|
538
544
|
notes.push(`sàn an toàn: ${floorWhy.join(', ')}`);
|
|
539
545
|
}
|
|
546
|
+
if (prefFloor > 0 && base <= prior) notes.push(prefWhy);
|
|
540
547
|
|
|
541
548
|
const minIdx = noExplore ? base : floor;
|
|
542
549
|
const candidates = [];
|
|
@@ -552,10 +559,15 @@ function choose(opts) {
|
|
|
552
559
|
|
|
553
560
|
let chosen = base;
|
|
554
561
|
let why;
|
|
562
|
+
// core 0.7.1 (C3): why this tier, as a code — thompson | floor | escalation | prior | user-prior | explore | plan | override | ask | unknown-type
|
|
563
|
+
let code;
|
|
564
|
+
let tsScore = null;
|
|
555
565
|
const baseCell = cellAt(base);
|
|
556
566
|
const scored = pool.filter((x) => x.cell && x.cell.enough);
|
|
557
567
|
if (!baseCell || !baseCell.enough || !scored.length) {
|
|
558
568
|
why = `${priorWhy} ${tierName(base)} (${baseCell ? `n=${baseCell.n}` : 'n=0'}, chưa đủ dữ liệu)`;
|
|
569
|
+
if (base > prior) code = esc.floor === base && esc.floor > Math.max(safetyFloor, prefFloor) ? 'escalation' : 'floor';
|
|
570
|
+
else code = priorWhy === 'prior' ? 'prior' : 'user-prior';
|
|
559
571
|
} else {
|
|
560
572
|
let best = null;
|
|
561
573
|
for (const x of scored) {
|
|
@@ -568,6 +580,8 @@ function choose(opts) {
|
|
|
568
580
|
if (!best || s < best.s) best = { i: x.i, s, p };
|
|
569
581
|
}
|
|
570
582
|
chosen = best.i;
|
|
583
|
+
code = 'thompson';
|
|
584
|
+
tsScore = Math.round(best.s * 1000) / 1000;
|
|
571
585
|
why = `Thompson: ${tierName(chosen)} điểm ${best.s.toFixed(3)} (p≈${best.p.toFixed(2)}, ${scored.length} ô đủ dữ liệu)`;
|
|
572
586
|
}
|
|
573
587
|
|
|
@@ -576,17 +590,19 @@ function choose(opts) {
|
|
|
576
590
|
if (!noExplore && chosen - 1 >= floor && u < profile.explore_rate) {
|
|
577
591
|
chosen -= 1;
|
|
578
592
|
explored = true;
|
|
593
|
+
code = 'explore';
|
|
579
594
|
why = `thăm dò xuống ${tierName(chosen)} (xác suất ${profile.explore_rate}); ${why}`;
|
|
580
595
|
}
|
|
581
596
|
|
|
582
597
|
if (overrideIdx !== null) {
|
|
583
598
|
const sugg = tierName(chosen);
|
|
584
|
-
|
|
585
|
-
|
|
599
|
+
const below = overrideIdx < prefFloor ? `; dưới ${prefWhy} (người dùng chọn nên giữ)` : '';
|
|
600
|
+
return finish(overrideIdx, `người dùng chọn ${tierName(overrideIdx)} (đề xuất là ${sugg}: ${why})${below}`, false, {
|
|
601
|
+
overridden: overrideIdx !== chosen, suggested_tier: sugg, reason_code: 'override', suggested_reason_code: code, thompson_score: tsScore,
|
|
586
602
|
});
|
|
587
603
|
}
|
|
588
604
|
if (noExplore) notes.push(`không thăm dò: ${noExploreWhy.join(', ')}`);
|
|
589
|
-
return finish(chosen, [why].concat(notes).join('; '), explored);
|
|
605
|
+
return finish(chosen, [why].concat(notes).join('; '), explored, { reason_code: code, thompson_score: tsScore });
|
|
590
606
|
}
|
|
591
607
|
|
|
592
608
|
module.exports = {
|
package/lib/commands.cjs
CHANGED
|
@@ -65,7 +65,7 @@ function cmdSetup(opts) {
|
|
|
65
65
|
const lines = [
|
|
66
66
|
`Đã cài hook: ${res.hook}`,
|
|
67
67
|
res.settings_changed
|
|
68
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
68
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit + PreToolUse(Agent) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
69
69
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
70
70
|
`Agent dp-*: ${res.agents.map((a) => `${a.name}=${a.action}`).join(', ')}${res.agents.some((a) => a.action === 'skipped-foreign') ? ' (file trùng tên không phải của dieu-phoi: giữ nguyên, đổi tên rồi chạy lại setup)' : ''}`,
|
|
71
71
|
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
@@ -128,6 +128,9 @@ async function cmdDoctor(opts) {
|
|
|
128
128
|
add(reg.UserPromptSubmit, 'prompt-hook', reg.UserPromptSubmit
|
|
129
129
|
? 'UserPromptSubmit có — prompt "điều phối" được tạo sẵn task_id + run dir, lượt đầu là scout'
|
|
130
130
|
: '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');
|
|
131
|
+
add(reg.PreToolUse, 'chain-gate', reg.PreToolUse
|
|
132
|
+
? 'PreToolUse(Agent) có — chặn exec tiếp nối từ handoff thứ 3 (trừ thẻ chain_ok)'
|
|
133
|
+
: 'thiếu hook PreToolUse(Agent) (core 0.7.1) — chuỗi handoff dài không bị chặn; chạy lại `dieu-phoi setup`', 'CẢNH BÁO');
|
|
131
134
|
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
132
135
|
add(permOk, 'run-permissions', permOk
|
|
133
136
|
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
@@ -726,19 +729,27 @@ function measureOrch(opts, ctx, prof) {
|
|
|
726
729
|
u = core.orchestratorUsage(file, core.loadPricing(ctx), { cacheFile: core.orchCacheFile(ctx.paths, sid, file) });
|
|
727
730
|
}
|
|
728
731
|
} catch (_) { /* measurement never fails the caller */ }
|
|
729
|
-
|
|
732
|
+
// core 0.7.1 (retro 2026-10-26, C1): past ORCH_NEW_SESSION_TOKENS or ORCH_NEW_SESSION_COMPACTIONS a /compact no longer
|
|
733
|
+
// helps (session 36cee993: 946k, 20 compactions, 0 user verdicts) -> advise a new session instead.
|
|
734
|
+
const newSession = (typeof u.ctx_tokens === 'number' && u.ctx_tokens > ORCH_NEW_SESSION_TOKENS)
|
|
735
|
+
|| (typeof u.compactions === 'number' && u.compactions >= ORCH_NEW_SESSION_COMPACTIONS);
|
|
736
|
+
const over = (typeof u.ctx_growth === 'number' && u.ctx_growth > softCap) || newSession;
|
|
730
737
|
let advice;
|
|
731
738
|
if (!sid) advice = 'Không đo được: không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).';
|
|
732
739
|
else if (u.ctx_tokens === null) advice = 'Không đo được: không thấy transcript của session này.';
|
|
733
|
-
else if (
|
|
740
|
+
else if (newSession) {
|
|
741
|
+
advice = `Phiên điều phối đã quá lớn (context ${u.ctx_tokens} token, nén ${u.compactions === null ? '?' : u.compactions} lần; ngưỡng ${ORCH_NEW_SESSION_TOKENS} token hoặc ${ORCH_NEW_SESSION_COMPACTIONS} lần nén): đừng /compact thêm — ghi trạng thái (\`dieu-phoi state --session ${sid}\` + file trạng thái trong run dir), trình cổng 3 phần đã xong, rồi MỞ PHIÊN MỚI và nạp lại từ \`state\`.`;
|
|
742
|
+
} else if (over) advice = `Context điều phối đã tăng ${u.ctx_growth} token (nền ${u.ctx_baseline}, nay ${u.ctx_tokens}) vượt trần mềm ${softCap}: gõ /compact để nén phiên — trạng thái điều phối sẽ được nạp lại tự động.`;
|
|
734
743
|
else advice = `Context điều phối tăng ${u.ctx_growth}/${softCap} token so với nền ${u.ctx_baseline}, còn trong trần mềm.`;
|
|
735
744
|
return {
|
|
736
745
|
session_id: sid, ctx_tokens: u.ctx_tokens, ctx_baseline: u.ctx_baseline, ctx_growth: u.ctx_growth,
|
|
737
|
-
cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, advice,
|
|
746
|
+
cum_usd: u.cum_usd, compactions: u.compactions, soft_cap: softCap, over, new_session: newSession, advice,
|
|
738
747
|
};
|
|
739
748
|
}
|
|
740
749
|
|
|
741
750
|
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
751
|
+
const ORCH_NEW_SESSION_TOKENS = 400000;
|
|
752
|
+
const ORCH_NEW_SESSION_COMPACTIONS = 5;
|
|
742
753
|
|
|
743
754
|
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
744
755
|
function ctxWarnLevel(growth, cap) {
|
|
@@ -808,13 +819,18 @@ function cmdState(opts) {
|
|
|
808
819
|
const ctx = ctxOf(opts);
|
|
809
820
|
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
810
821
|
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
822
|
+
// retro C6: knowledge deltas older than 24 h that no store has applied (file reads only, never throws)
|
|
823
|
+
const deltas = core.scanUnmergedDeltas();
|
|
824
|
+
const deltaLines = core.unmergedDeltaLines(deltas);
|
|
811
825
|
if (!sid) {
|
|
812
|
-
if (opts.json)
|
|
813
|
-
|
|
826
|
+
if (opts.json) {
|
|
827
|
+
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, deltas.count ? { unmerged_deltas: deltas } : {})) };
|
|
828
|
+
}
|
|
829
|
+
return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines).join('\n') };
|
|
814
830
|
}
|
|
815
831
|
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
816
|
-
if (opts.json) return { code: 0, out: JSON.stringify(state, null, 2) };
|
|
817
|
-
return { code: 0, out: core.formatSessionState(state, core.STATE_TEXT_CAP) };
|
|
832
|
+
if (opts.json) return { code: 0, out: JSON.stringify(deltas.count ? Object.assign({}, state, { unmerged_deltas: deltas }) : state, null, 2) };
|
|
833
|
+
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines).join('\n') };
|
|
818
834
|
}
|
|
819
835
|
|
|
820
836
|
// ------------------------------------------------------------------ stats / retro
|
|
@@ -832,13 +848,13 @@ function cmdStats(opts) {
|
|
|
832
848
|
const ctx = ctxOf(opts);
|
|
833
849
|
const { lambda } = lambdaOf(opts, ctx);
|
|
834
850
|
const events = core.readEvents(ctx);
|
|
835
|
-
const { cells, steers, orchestration, tasks } = agg.analyze(events, { lambda });
|
|
851
|
+
const { cells, steers, orchestration, tasks, batch_verify: batchVerify } = agg.analyze(events, { lambda });
|
|
836
852
|
const modes = agg.modeSummary(events, tasks);
|
|
837
853
|
const type = typeof opts.type === 'string' ? opts.type : null;
|
|
838
854
|
const rows = type ? cells.filter((c) => c.task_type === type) : cells;
|
|
839
855
|
const steerRows = type ? steers.filter((s) => s.task_type === type) : steers;
|
|
840
856
|
if (opts.json) {
|
|
841
|
-
return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration, modes }, null, 2) };
|
|
857
|
+
return { code: 0, out: JSON.stringify({ lambda_usd_per_min: lambda, min_n: agg.MIN_N, cells: rows, steers: steerRows, orchestration, modes, batch_verify: batchVerify }, null, 2) };
|
|
842
858
|
}
|
|
843
859
|
if (!rows.length) {
|
|
844
860
|
return { code: 0, out: orchestration.sessions ? `Chưa có dữ liệu theo ô.\n${orchLine(orchestration)}` : 'Chưa có dữ liệu.' };
|
|
@@ -865,7 +881,7 @@ function cmdStats(opts) {
|
|
|
865
881
|
}
|
|
866
882
|
return {
|
|
867
883
|
code: 0,
|
|
868
|
-
out: `${table(t)}\n\n${table(s)}\n\n${modeTable(modes)}${orchLine(orchestration)}\n\n`
|
|
884
|
+
out: `${table(t)}\n\n${table(s)}\n\n${modeTable(modes)}${batchVerifyTable(batchVerify)}${orchLine(orchestration)}\n\n`
|
|
869
885
|
+ 'n = task tính tỉ lệ (/số đã giao, (-k) = bị loại: reject-scope hoặc lượt exec đầu bị steer plan/redirect). '
|
|
870
886
|
+ 'accept% = thành công theo verdict người dùng đầu tiên, chưa có thì verdict agent; agent_pass% = chỉ tầng agent. '
|
|
871
887
|
+ `E[cost], E[time] tính trọn mọi lượt + review của task. score = E[cost] + λ·p50[time] (trung vị, không dùng TB: thời gian chờ thổi phồng TB), λ=${lambda} USD/phút.`,
|
|
@@ -890,6 +906,20 @@ function modeTable(modes) {
|
|
|
890
906
|
return `${table(t)}\n\n`;
|
|
891
907
|
}
|
|
892
908
|
|
|
909
|
+
/** core 0.7.1 (C5): the batched verifiers' own cell (model/effort), kept out of the task cells; '' when none. */
|
|
910
|
+
function batchVerifyTable(rows) {
|
|
911
|
+
if (!rows || !rows.length) return '';
|
|
912
|
+
const t = [['verifier gộp', 'lần', 'task', 'Σ cost', 'E[cost]', 'p50 cost', 'p50 time']];
|
|
913
|
+
for (const r of rows) {
|
|
914
|
+
t.push([
|
|
915
|
+
`${chooser.shortModel(r.model) || r.model || '?'}/${r.effort || '?'}`, r.n, r.tasks,
|
|
916
|
+
fmtUsd(r.cost_total_usd), fmtUsd(r.cost_mean_usd), fmtUsd(r.cost_p50_usd),
|
|
917
|
+
r.time_p50_min === null ? '-' : `${fmtNum(r.time_p50_min)}m`,
|
|
918
|
+
]);
|
|
919
|
+
}
|
|
920
|
+
return `${table(t)}\n(chi phí verifier gộp là ô riêng, không cộng vào ô task_type × tier của bước fast mang task_id của nó)\n\n`;
|
|
921
|
+
}
|
|
922
|
+
|
|
893
923
|
/** One retro line: per mode with tasks. */
|
|
894
924
|
function modeRetroLine(modes) {
|
|
895
925
|
const parts = agg.MODE_KEYS.filter((m) => modes[m].tasks).map((m) => {
|
|
@@ -906,7 +936,7 @@ function cmdRetro(opts) {
|
|
|
906
936
|
const ctx = ctxOf(opts);
|
|
907
937
|
const { prof, lambda } = lambdaOf(opts, ctx);
|
|
908
938
|
const events = core.readEvents(ctx);
|
|
909
|
-
const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration } = agg.analyze(events, { lambda });
|
|
939
|
+
const { agents, tasks, cells, steers, userVerdicts, overrides, orchestration, batch_verify: batchVerify } = agg.analyze(events, { lambda });
|
|
910
940
|
const modes = agg.modeSummary(events, tasks);
|
|
911
941
|
const reasons = {};
|
|
912
942
|
const outcomes = {};
|
|
@@ -964,6 +994,7 @@ function cmdRetro(opts) {
|
|
|
964
994
|
overrides: overrides.map((o) => ({ ts: o.ts, task_type: o.task_type, suggested_tier: o.suggested_tier, chosen_tier: o.chosen_tier })),
|
|
965
995
|
orchestration,
|
|
966
996
|
modes,
|
|
997
|
+
batch_verify: batchVerify,
|
|
967
998
|
cells,
|
|
968
999
|
};
|
|
969
1000
|
if (opts.json) return { code: 0, out: JSON.stringify(summary, null, 2) };
|
|
@@ -977,6 +1008,7 @@ function cmdRetro(opts) {
|
|
|
977
1008
|
`Steer nhiều (brainstorm hỏi quá ít): ${highSteer.map((s) => `${s.task_type} ${pct(s.steer_rate)}`).join(', ') || '-'}`,
|
|
978
1009
|
orchLine(orchestration),
|
|
979
1010
|
modeRetroLine(modes),
|
|
1011
|
+
...(batchVerify.length ? [`Verifier gộp (ô riêng): ${batchVerify.map((r) => `${chooser.shortModel(r.model) || r.model || '?'}/${r.effort || '?'} ×${r.n}, Σ ${fmtUsd(r.cost_total_usd)}, p50 ${r.time_p50_min === null ? '-' : `${fmtNum(r.time_p50_min)}m`}`).join(' · ')}`] : []),
|
|
980
1012
|
'Chi tiết theo ô: `dieu-phoi stats`; đưa cho LLM: `dieu-phoi retro --json`.',
|
|
981
1013
|
];
|
|
982
1014
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -1062,6 +1094,7 @@ function cmdChoose(opts) {
|
|
|
1062
1094
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
1063
1095
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
1064
1096
|
res.orch_ctx_over = orch.over;
|
|
1097
|
+
res.orch_new_session = orch.new_session === true;
|
|
1065
1098
|
res.orch_ctx_warn = ctxWarnOnce(ctx, orch); // over AND a new threshold crossed since the last warning
|
|
1066
1099
|
if (res.overridden) {
|
|
1067
1100
|
const tid = input.taskId;
|
|
@@ -1072,6 +1105,8 @@ function cmdChoose(opts) {
|
|
|
1072
1105
|
core.recordSuggestion(ctx, {
|
|
1073
1106
|
task_id: input.taskId, role: res.role || input.role, suggested_tier: res.suggested_tier || res.tier, choose_tier: res.tier,
|
|
1074
1107
|
source: 'choose', keep_suggested: input.approved !== null && !res.overridden,
|
|
1108
|
+
choose_task_type: res.task_type, choose_score: res.score, explored: res.explored, reason_code: res.reason_code,
|
|
1109
|
+
thompson_score: res.thompson_score === null ? undefined : res.thompson_score,
|
|
1075
1110
|
});
|
|
1076
1111
|
}
|
|
1077
1112
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
@@ -1257,6 +1292,8 @@ function recordPlanSuggestions(ctx, plan) {
|
|
|
1257
1292
|
if (!r.task_id || !r.tier || r.action !== 'dispatch') continue;
|
|
1258
1293
|
const ok = core.recordSuggestion(ctx, {
|
|
1259
1294
|
task_id: r.task_id, role: r.role || 'exec', suggested_tier: r.suggested_tier || r.tier, choose_tier: r.tier, source: 'plan',
|
|
1295
|
+
choose_task_type: r.task_type, choose_score: r.score, explored: r.explored, reason_code: r.reason_code,
|
|
1296
|
+
thompson_score: r.thompson_score === null ? undefined : r.thompson_score,
|
|
1260
1297
|
});
|
|
1261
1298
|
if (ok) n++;
|
|
1262
1299
|
}
|
package/lib/knowledge.cjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Delta (written by agents into a run dir as knowledge-delta*.md, merged at gate 3):
|
|
16
16
|
// repo: <path of the repo/worktree the delta was checked against> (header, before the first `## `)
|
|
17
|
+
// or a short name (`pff-tailor`) that matches exactly one known store (its name / dir name / a recorded root)
|
|
17
18
|
// head: <sha> (optional; else HEAD read from repo:)
|
|
18
19
|
// ## +<id> new section ## <id> replace an existing one
|
|
19
20
|
// title: / tags: a, b / paths: glob, glob / base: <sha>#<rev> (meta lines right under the heading)
|
|
@@ -27,7 +28,6 @@
|
|
|
27
28
|
const fs = require('fs');
|
|
28
29
|
const path = require('path');
|
|
29
30
|
const cp = require('child_process');
|
|
30
|
-
const crypto = require('crypto');
|
|
31
31
|
const core = require('../hook.cjs');
|
|
32
32
|
const { repoOf } = require('./args.cjs');
|
|
33
33
|
|
|
@@ -55,7 +55,8 @@ const KNOWLEDGE_HELP = `Cách dùng: dieu-phoi knowledge <lệnh> [--repo <đư
|
|
|
55
55
|
merge --run <id|thư mục run> [--task <id>[,<id>] [--untagged]] [--dry-run] [--json]
|
|
56
56
|
gộp mọi knowledge-delta*.md của run (--task: chỉ knowledge-delta-<task_id>[-r<n>].md của các
|
|
57
57
|
task đó, --untagged: thêm file không mang task_id; verdict accept tự gọi bước này) vào kho của repo ghi ở header "repo:" (không có thì
|
|
58
|
-
--repo/cwd
|
|
58
|
+
--repo/cwd; "repo:" là tên ngắn như pff-tailor thì khớp kho đã biết nếu duy nhất — HEAD lấy từ repo đã ghi
|
|
59
|
+
nhớ (meta.json roots), --repo hoặc dòng head:): khoá knowledge.lock, ghi tmp+rename, bản cũ vào history/, sha = HEAD của repo đó.
|
|
59
60
|
"## +id" thêm mục (bắt buộc paths:), "## id" thay mục (bắt buộc base: <sha>#<rev>); base cũ
|
|
60
61
|
→ xung đột, không ghi đè; mục > 40 dòng bị từ chối. Có xung đột/từ chối → exit 2.
|
|
61
62
|
Idempotent: file delta đã gộp (hash trong meta.json) và mục trùng một rev cũ trong history/ → "không đổi"
|
|
@@ -609,8 +610,25 @@ function sameContent(cur, body, next) {
|
|
|
609
610
|
const APPLIED_MAX = 300;
|
|
610
611
|
|
|
611
612
|
/** sha256 of the delta text, BOM and CRLF normalised so a CRLF checkout hashes like the LF original. */
|
|
612
|
-
|
|
613
|
-
|
|
613
|
+
const deltaHash = core.deltaHash; // shared with the unmerged-delta scan in hook.cjs
|
|
614
|
+
|
|
615
|
+
/** Main checkout of the repo `info` describes (a linked worktree is deleted later; the checkout stays). */
|
|
616
|
+
function mainRootOf(info) {
|
|
617
|
+
const common = info && info.common_dir ? String(info.common_dir) : '';
|
|
618
|
+
if (common && path.basename(common) === '.git') return slash(path.dirname(common));
|
|
619
|
+
return info && info.root ? slash(info.root) : null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Remember where a store's repo lives (meta.json `roots`, newest first, <= 5) so a bare `repo: <name>` can find HEAD. */
|
|
623
|
+
function noteRoot(dir, info) {
|
|
624
|
+
const root = mainRootOf(info);
|
|
625
|
+
if (!root || !isDir(dir)) return;
|
|
626
|
+
const file = path.join(dir, 'meta.json');
|
|
627
|
+
const meta = core.readJsonFile(file, {}) || {};
|
|
628
|
+
const roots = Array.isArray(meta.roots) ? meta.roots : [];
|
|
629
|
+
if (roots[0] === root) return;
|
|
630
|
+
const next = [root].concat(roots.filter((r) => r !== root)).slice(0, 5);
|
|
631
|
+
core.writeFileAtomic(file, `${JSON.stringify(Object.assign({}, meta, { roots: next }), null, 2)}\n`);
|
|
614
632
|
}
|
|
615
633
|
|
|
616
634
|
function appliedOf(dir) {
|
|
@@ -741,9 +759,11 @@ function applyDelta(delta, info, sha, dry, src) {
|
|
|
741
759
|
core.writeFileAtomic(path.join(dir, 'index.json'), `${JSON.stringify(idx, null, 2)}\n`);
|
|
742
760
|
ensureMeta(dir, info);
|
|
743
761
|
}
|
|
744
|
-
if (
|
|
762
|
+
if (!dry && src && isDir(dir) && !results.some((r) => r.op === 'conflict' || r.op === 'reject')) {
|
|
763
|
+
// also when every section was already merged ("same"): the file counts as handled, not as an unmerged delta
|
|
745
764
|
recordApplied(dir, info, { sha256: src.hash, file: src.file, run: src.run, at: now.toISOString() });
|
|
746
765
|
}
|
|
766
|
+
if (!dry && changed && info.common_dir) noteRoot(dir, info);
|
|
747
767
|
return { results };
|
|
748
768
|
}
|
|
749
769
|
|
|
@@ -766,6 +786,45 @@ function describe(r) {
|
|
|
766
786
|
return `${OP_MARK[r.op] || '?'} ${r.id} BỊ TỪ CHỐI: ${r.why}`;
|
|
767
787
|
}
|
|
768
788
|
|
|
789
|
+
/**
|
|
790
|
+
* Where a delta goes: { target, info } — target = repo dir to read HEAD from (null when only the store is known),
|
|
791
|
+
* info = project identity (resolveProject, or the bare store entry for a short `repo:` name). Order: a short name that
|
|
792
|
+
* matches exactly one known store, then the path, then --repo as the fallback for a repo that no longer exists.
|
|
793
|
+
*/
|
|
794
|
+
function targetOf(delta, opts, rep) {
|
|
795
|
+
const err = (error) => ({ error });
|
|
796
|
+
if (!delta.repo) return finish(repoOf(opts));
|
|
797
|
+
if (core.isBareRepoName(delta.repo)) {
|
|
798
|
+
const hit = core.storeByName(delta.repo);
|
|
799
|
+
if (hit.matches.length > 1) {
|
|
800
|
+
return err(`repo: ${delta.repo} khớp ${hit.matches.length} kho (${hit.matches.map((m) => m.dir_name).join(', ')}) — ghi đường dẫn đầy đủ hoặc chạy với --repo`);
|
|
801
|
+
}
|
|
802
|
+
if (hit.store) {
|
|
803
|
+
const st = hit.store;
|
|
804
|
+
rep.note = `repo: ${delta.repo} → kho ${st.dir_name}`;
|
|
805
|
+
const cands = st.roots.map((r) => core.nativePath(r)).concat(opts.repo !== undefined ? [repoOf(opts)] : []);
|
|
806
|
+
for (const c of cands) {
|
|
807
|
+
if (!isDir(c)) continue;
|
|
808
|
+
const pi = core.resolveProject(c);
|
|
809
|
+
if (pi && pi.dir === st.dir) return { target: path.resolve(c), info: pi };
|
|
810
|
+
}
|
|
811
|
+
return { target: null, info: { dir: st.dir, dir_name: st.dir_name, key: st.key, name: st.name } };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (delta.repo) {
|
|
815
|
+
const p = path.resolve(core.nativePath(delta.repo));
|
|
816
|
+
if (isDir(p)) return finish(p);
|
|
817
|
+
if (opts.repo !== undefined) { rep.note = `repo: ${delta.repo} không còn — dùng --repo`; return finish(repoOf(opts)); }
|
|
818
|
+
const bare = core.isBareRepoName(delta.repo);
|
|
819
|
+
return err(`repo: ${delta.repo} ${bare ? 'không khớp kho nào đã biết và không phải thư mục' : 'không tồn tại (worktree đã xoá?)'} — chạy lại với --repo <đường dẫn repo>`);
|
|
820
|
+
}
|
|
821
|
+
return err('không xác định được repo');
|
|
822
|
+
function finish(target) {
|
|
823
|
+
const info = core.resolveProject(target);
|
|
824
|
+
return info ? { target, info } : err(`không phải repo git: ${slash(target)}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
769
828
|
function cmdMerge(opts) {
|
|
770
829
|
const rd = runDirOf(opts.run);
|
|
771
830
|
if (rd.error) return { code: 1, out: rd.error };
|
|
@@ -786,23 +845,20 @@ function cmdMerge(opts) {
|
|
|
786
845
|
src = { hash: deltaHash(text), file: path.basename(file), run: path.basename(rd.dir) };
|
|
787
846
|
} catch (e) { rep.error = `không đọc được: ${e.message}`; totals.error++; continue; }
|
|
788
847
|
if (!delta.sections.length) { rep.error = 'không có mục "## <id>" nào'; totals.error++; continue; }
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
if (isDir(p)) target = p;
|
|
793
|
-
else if (opts.repo !== undefined) { target = repoOf(opts); rep.note = `repo: ${delta.repo} không còn — dùng --repo`; } else {
|
|
794
|
-
rep.error = `repo: ${delta.repo} không tồn tại (worktree đã xoá?) — chạy lại với --repo <đường dẫn repo>`; totals.error++; continue;
|
|
795
|
-
}
|
|
796
|
-
} else target = repoOf(opts);
|
|
797
|
-
const info = core.resolveProject(target);
|
|
798
|
-
if (!info) { rep.error = `không phải repo git: ${slash(target)}`; totals.error++; continue; }
|
|
848
|
+
const t = targetOf(delta, opts, rep);
|
|
849
|
+
if (t.error) { rep.error = t.error; totals.error++; continue; }
|
|
850
|
+
const { target, info } = t;
|
|
799
851
|
let sha = null;
|
|
800
852
|
if (delta.head) {
|
|
801
853
|
if (!/^[0-9a-f]{7,64}$/i.test(delta.head)) { rep.error = `head: không phải sha: ${delta.head}`; totals.error++; continue; }
|
|
802
854
|
sha = delta.head.toLowerCase();
|
|
803
|
-
} else sha = headShaOf(target);
|
|
804
|
-
if (!sha) {
|
|
805
|
-
|
|
855
|
+
} else if (target) sha = headShaOf(target);
|
|
856
|
+
if (!sha) {
|
|
857
|
+
rep.error = target ? `không xác định được HEAD của ${slash(target)} (repo chưa có commit?) — thêm dòng "head: <sha>" vào delta`
|
|
858
|
+
: `repo: ${delta.repo} khớp kho ${info.dir_name} nhưng chưa biết repo nằm đâu để đọc HEAD — thêm dòng "head: <sha>" vào delta hoặc chạy với --repo <đường dẫn repo>`;
|
|
859
|
+
totals.error++; continue;
|
|
860
|
+
}
|
|
861
|
+
Object.assign(rep, { repo: target ? slash(target) : null, store: info.dir, key: info.key, sha });
|
|
806
862
|
let out;
|
|
807
863
|
if (dry) out = applyDelta(delta, info, sha, true, src);
|
|
808
864
|
else {
|
package/lib/setup.cjs
CHANGED
|
@@ -57,7 +57,11 @@ function stripOurHooks(settings) {
|
|
|
57
57
|
return removed;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// core 0.7.1: the subagent tool is `Agent` in current Claude Code and `Task` in older builds — match both
|
|
61
|
+
// (an old `Agent`-only registration is not "already registered", so `setup` rewrites it).
|
|
62
|
+
const AGENT_MATCHER = 'Agent|Task';
|
|
63
|
+
|
|
64
|
+
/** True when settings already hold exactly our five entries with this command. */
|
|
61
65
|
function alreadyRegistered(settings, cmd) {
|
|
62
66
|
const hooks = settings && settings.hooks;
|
|
63
67
|
if (!hooks || typeof hooks !== 'object') return false;
|
|
@@ -69,14 +73,15 @@ function alreadyRegistered(settings, cmd) {
|
|
|
69
73
|
if (!isOurs(h)) continue;
|
|
70
74
|
ours++;
|
|
71
75
|
if (h.command !== cmd) continue;
|
|
72
|
-
if (event === 'PostToolUse' && g.matcher ===
|
|
76
|
+
if (event === 'PostToolUse' && g.matcher === AGENT_MATCHER) good++;
|
|
73
77
|
else if (event === 'SubagentStop') good++;
|
|
74
78
|
else if (event === 'SessionStart' && g.matcher === 'compact') good++;
|
|
75
79
|
else if (event === 'UserPromptSubmit' && !g.matcher) good++;
|
|
80
|
+
else if (event === 'PreToolUse' && g.matcher === AGENT_MATCHER) good++;
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
|
-
return ours ===
|
|
84
|
+
return ours === 5 && good === 5;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
function addOurHooks(settings, hookPath) {
|
|
@@ -84,7 +89,7 @@ function addOurHooks(settings, hookPath) {
|
|
|
84
89
|
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
85
90
|
const h = settings.hooks;
|
|
86
91
|
if (!Array.isArray(h.PostToolUse)) h.PostToolUse = [];
|
|
87
|
-
h.PostToolUse.push({ matcher:
|
|
92
|
+
h.PostToolUse.push({ matcher: AGENT_MATCHER, hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
88
93
|
if (!Array.isArray(h.SubagentStop)) h.SubagentStop = [];
|
|
89
94
|
h.SubagentStop.push({ hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
|
|
90
95
|
// after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
|
|
@@ -93,6 +98,9 @@ function addOurHooks(settings, hookPath) {
|
|
|
93
98
|
// on a dieu-phoi prompt: pre-create the run dir + id so the first tool call can be the scout (no shell)
|
|
94
99
|
if (!Array.isArray(h.UserPromptSubmit)) h.UserPromptSubmit = [];
|
|
95
100
|
h.UserPromptSubmit.push({ hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
|
|
101
|
+
// core 0.7.1 (C1): deny another exec continuation of a task that already handed off 3 times (unless chain_ok)
|
|
102
|
+
if (!Array.isArray(h.PreToolUse)) h.PreToolUse = [];
|
|
103
|
+
h.PreToolUse.push({ matcher: AGENT_MATCHER, hooks: [{ type: 'command', command: cmd, timeout: 5 }] });
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
// ------------------------------------------------------------------ run-dir permissions
|
|
@@ -413,7 +421,7 @@ function registration(claudeDir) {
|
|
|
413
421
|
const perms = permissionEntries(runsDir());
|
|
414
422
|
const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
|
|
415
423
|
const out = {
|
|
416
|
-
file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, commands: [], permissions: false, permission_entries: perms,
|
|
424
|
+
file, PostToolUse: false, SubagentStop: false, SessionStart: false, UserPromptSubmit: false, PreToolUse: false, commands: [], permissions: false, permission_entries: perms,
|
|
417
425
|
runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
|
|
418
426
|
};
|
|
419
427
|
let obj;
|
|
@@ -423,11 +431,11 @@ function registration(claudeDir) {
|
|
|
423
431
|
out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
|
|
424
432
|
|| (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
|
|
425
433
|
const hooks = obj.hooks || {};
|
|
426
|
-
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit']) {
|
|
434
|
+
for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart', 'UserPromptSubmit', 'PreToolUse']) {
|
|
427
435
|
for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
|
|
428
436
|
for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
|
|
429
437
|
if (!isOurs(h)) continue;
|
|
430
|
-
if (ev === 'PostToolUse' && g.matcher && !/\
|
|
438
|
+
if ((ev === 'PostToolUse' || ev === 'PreToolUse') && g.matcher && !/\b(Agent|Task)\b/.test(g.matcher)) continue;
|
|
431
439
|
if (ev === 'SessionStart' && g.matcher !== 'compact') continue;
|
|
432
440
|
out[ev] = true;
|
|
433
441
|
out.commands.push(h.command);
|
|
@@ -438,6 +446,6 @@ function registration(claudeDir) {
|
|
|
438
446
|
}
|
|
439
447
|
|
|
440
448
|
module.exports = {
|
|
441
|
-
MARKER, AGENT_NAMES, AGENT_MARKER, renderAgent, agentStatus, nodePathOf, installAgents, removeAgents, agentsDirOf, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand,
|
|
449
|
+
MARKER, AGENT_NAMES, AGENT_MARKER, renderAgent, agentStatus, nodePathOf, installAgents, removeAgents, agentsDirOf, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand, AGENT_MATCHER,
|
|
442
450
|
skillVersionOf, findSkillDir, tsStamp, runsDir, legacyRunsDirOf, permissionEntries, addOurPermissions, stripOurPermissions, hasOurPermissions,
|
|
443
451
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kienntpixon/dieu-phoi",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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>",
|