@kienntpixon/dieu-phoi 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/bin/dieu-phoi.cjs +20 -7
- package/hook.cjs +393 -5
- package/lib/aggregate.cjs +137 -14
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +60 -2
- package/lib/commands.cjs +197 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog — @kienntpixon/dieu-phoi
|
|
2
2
|
|
|
3
|
+
## 0.7.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
`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).
|
|
6
|
+
|
|
7
|
+
- **Mode theo yêu cầu (root_task_id)**: `start --mode fast|normal|strict` và lệnh mới `dieu-phoi mode --root <id> [mode] [--reason R] [--session S]`
|
|
8
|
+
(không có giá trị = xem). Mỗi lần đổi ghi một sự kiện `kind=mode` cục bộ (`runSync` bỏ qua kind này); cùng mode thì không ghi. Bước đã giao giữ
|
|
9
|
+
mode lúc giao (`dispatch.mode`), bước chưa giao áp mode mới; thứ tự: lần giao exec đầu > `mode` của bước trong kế hoạch > mode root > normal.
|
|
10
|
+
Không có logic tự đổi mode. `start` (text) và khối hook khởi động luôn in dòng nhắc HỎI người dùng chọn mode (`--json`: `mode:null`).
|
|
11
|
+
- **`choose` trả `verify_plan`** (`independent-blocking | independent-nonblocking | self+batch | spot`) cùng `verify_reason`, `verify_floor`,
|
|
12
|
+
`test_scope` (`selective|full`), `mode`, `mode_source`; thêm `--mode`, `--publish`, `--dependents`. Sàn an toàn (reversible=n, publish,
|
|
13
|
+
verifiable=n, s>=9) xét trước và thắng ở mọi mode. fast: s<=6 → `self+batch` (test chọn lọc, không verifier riêng, một verify gộp);
|
|
14
|
+
s7-8 → `independent-nonblocking` (blocking nếu có bước phụ thuộc); strict: 1/3 `spot`, luôn chạy cả suite.
|
|
15
|
+
- **`verdict ... --verify self`** hợp lệ (exec tự kiểm).
|
|
16
|
+
- **`plan-table`**: file là mảng (như cũ) hoặc `{"mode":…,"steps":[…]}`, bước có thể ghi đè `mode`/`publish`; thêm cột **Kiểm** (cuối bảng),
|
|
17
|
+
dòng `**Mode:**` và "bỏ N verifier riêng, 1 verify gộp"; cờ `--mode`; `--approve` ghi sự kiện mode (source plan) khi mode hiệu lực khác
|
|
18
|
+
mode hiện tại của root.
|
|
19
|
+
- **`state`** dựng lại mode: `modes[]` + `task.mode` (--json), dòng "Mode (theo yêu cầu…)" (text).
|
|
20
|
+
- Test: `test/mode.test.cjs` (8 ca); cập nhật kỳ vọng cũ cho khối `start` và khoá `state --json`.
|
|
21
|
+
- **Gợi ý quay về normal, không tự đổi** (task ue15wumm): khi root đang fast, `choose` (exec) và `state` trả `mode_suggest:"normal"` +
|
|
22
|
+
`mode_suggest_reason` (bản chữ: dòng `GỢI Ý mode: … hỏi người dùng … KHÔNG tự đổi mode`) khi có verdict fail `batch-verify`, reject chất
|
|
23
|
+
lượng (từ sự kiện mode mới nhất của root), hoặc ≥ 15% fail trong 10 bước fast gần nhất (cần ≥ 5 bước). Không ghi sự kiện mode nào.
|
|
24
|
+
- **`complete.test_s`** (+ `test_runs`, `full_suite_runs`, `test_s_source`): hook SubagentStop đo thời gian lệnh test từ transcript subagent
|
|
25
|
+
(tool_use Bash/PowerShell → tool_result), không có thì đọc dòng `TEST_S:` trong dp-out; lệnh nền chỉ đếm.
|
|
26
|
+
- **`stats`/`retro` tách theo mode** (`modes` trong `--json`, bảng/dòng "Theo mode" ở bản chữ): catch rate, reject, phút verify, test_s,
|
|
27
|
+
số lần chạy cả suite. Hàm mới `aggregate.modeSummary` (ngoài parity CLI ↔ sink).
|
|
28
|
+
- Test: `test/mode-suggest.test.cjs` (7 ca).
|
|
29
|
+
- **Dữ liệu học của fast (task d1m3rg7s)**: verdict `--verify self` không vào `agent_pass_rate` / verdict agent đầu (`first_outcome`)
|
|
30
|
+
nhưng vẫn là kết quả leo thang; số phụ theo ô `n_self`, `self_pass`, `auto_accept`, `auto_overturned`, note "ít bằng chứng" khi
|
|
31
|
+
self chiếm > 50% verdict của ô. Verifier gộp ghi `verdict <exec_agent> fail|pass --verify tests --reason batch-verify`.
|
|
32
|
+
- **`verdict --task <id> accept --auto`**: nghiệm thu tự động (`by=auto`, `verify=tests`, round null) — chỉ accept, chỉ task mode fast,
|
|
33
|
+
chưa có verdict người dùng, score exec ≤ 6 nếu có; `reject --auto`, `--root … --auto`, `--round/--cause` kèm `--auto` bị từ chối.
|
|
34
|
+
Verdict người dùng sau đó thắng; reject sau auto accept đếm `auto_overturned`. `retro` totals thêm `self_verdicts`, `auto_accept`,
|
|
35
|
+
`auto_overturned` + dòng "Fast: …".
|
|
36
|
+
- **`sheet/Code.gs`** cùng logic (normalizeVerdict_ giữ `auto`, firstOutcome bỏ self, số phụ trên) — parity fixture +ptask201–205.
|
|
37
|
+
- Test: `test/fast-learning.test.cjs` (4 ca).
|
|
38
|
+
|
|
3
39
|
## 0.6.1 (2026-10-22)
|
|
4
40
|
|
|
5
41
|
`CORE_VERSION` 0.6.1; đi cùng skill 1.5.1. codebase-memory-mcp là **khuyến nghị, không bắt buộc** (nhánh `cbm-support`, task 7s6mouxg). `sheet/` không đổi, không cần deploy lại `Code.gs`.
|
package/bin/dieu-phoi.cjs
CHANGED
|
@@ -18,12 +18,20 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
18
18
|
kiểm hook, node, config, sink (sink lấy từ bộ nhớ đệm 24 giờ; --net kiểm trực tiếp) và
|
|
19
19
|
codebase-memory-mcp (khuyến nghị: cài/MCP/index của repo; --cbm hoặc --net mới chạy \`daemon status\` ~3 s,
|
|
20
20
|
kết quả nhớ 10 phút; --net còn so bản mới nhất)
|
|
21
|
-
start [--json]
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
start [--mode fast|normal|strict] [--json]
|
|
22
|
+
khởi động cổng 1 trong một lệnh (~150 ms, không gọi mạng): kiểm cục bộ, task_id gốc,
|
|
23
|
+
tạo run dir, đường dẫn brief scout — dùng khi hook UserPromptSubmit không chạy;
|
|
24
|
+
không có --mode thì in dòng nhắc HỎI người dùng chọn mode (mặc định normal)
|
|
25
|
+
mode --root <root_task_id> [fast|normal|strict] [--reason R] [--session S] [--json]
|
|
26
|
+
xem/đổi mode của một yêu cầu (một sự kiện mỗi lần đổi, cục bộ); bước chưa giao áp mode mới,
|
|
27
|
+
bước đã giao giữ mode lúc giao; không bao giờ tự đổi
|
|
28
|
+
verdict <agent_id|last> <pass|fail|timeout|aborted> [--verify tests|lint|review|judgment|self|none] [--reason short-kebab]
|
|
24
29
|
nghiệm thu tầng agent (by=agent); aborted = dừng vì hết quota/usage limit (không phải fail)
|
|
25
30
|
verdict --task <task_id> <accept|reject> [--cause quality|scope] [--round n] [--reason R]
|
|
26
31
|
nghiệm thu của người dùng (by=user); reject bắt buộc --cause
|
|
32
|
+
verdict --task <task_id> accept --auto [--reason R]
|
|
33
|
+
tự nghiệm thu bước fast (score ≤ 6) sau verifier gộp (by=auto): không vào accept_rate;
|
|
34
|
+
người dùng vẫn duyệt cuối bằng --root, reject sau đó thắng auto
|
|
27
35
|
verdict --root <root_task_id> accept [--steps a,b]
|
|
28
36
|
người dùng nhận mọi bước đã giao exec của kế hoạch đã duyệt chưa có verdict người dùng
|
|
29
37
|
accept (--task/--root) tự gộp knowledge-delta-<task_id>*.md của run vào kho (--no-knowledge: bỏ)
|
|
@@ -35,15 +43,19 @@ Cách dùng: dieu-phoi <lệnh> [tuỳ chọn]
|
|
|
35
43
|
stats [--type T] [--json]
|
|
36
44
|
bảng task_type × model/effort: n, accept%, agent_pass%, chi phí/thời gian (TB, p50, p75), steer
|
|
37
45
|
choose --type T --score N [--urgency relaxed|normal|urgent] [--reversible y|n]
|
|
38
|
-
[--verifiable y|n] [--
|
|
46
|
+
[--verifiable y|n] [--publish y|n] [--dependents y|n] [--mode fast|normal|strict]
|
|
47
|
+
[--role exec|verify|plan|helper] [--task ID]
|
|
39
48
|
[--override model/effort (cần --task) | --approved model/effort] [--skill-dir P] [--seed S] [--json]
|
|
40
|
-
đề xuất model + effort, lý do, action (dispatch|ask_user), ước tính chi phí/thời gian
|
|
49
|
+
đề xuất model + effort, lý do, action (dispatch|ask_user), ước tính chi phí/thời gian,
|
|
50
|
+
verify_plan (independent-blocking|independent-nonblocking|self+batch|spot) theo mode
|
|
51
|
+
(--mode mặc định: mode của root của --task, rồi normal)
|
|
41
52
|
ctx [--session <id>] [--json]
|
|
42
53
|
context + chi phí của phiên điều phối (mặc định $CLAUDE_CODE_SESSION_ID) so với trần mềm
|
|
43
54
|
state [--session <id>] [--json]
|
|
44
55
|
trạng thái điều phối của phiên dựng lại từ log (cũng được nạp tự động sau /compact)
|
|
45
|
-
plan-table --file <steps.json> [--json] [--approve --root <root_task_id>]
|
|
46
|
-
bảng phân công: Bước | Model/effort | Lý do | Ước tính + tổng + đường găng
|
|
56
|
+
plan-table --file <steps.json> [--mode M] [--json] [--approve --root <root_task_id>]
|
|
57
|
+
bảng phân công: Bước | Model/effort | Kiểm | Lý do | Ước tính + tổng + đường găng;
|
|
58
|
+
file là mảng bước hoặc { "mode": "fast", "steps": [...] } (bước có thể ghi đè "mode")
|
|
47
59
|
new-id [--count n] [--json]
|
|
48
60
|
in task_id mới hợp lệ (8 ký tự a-z0-9), mỗi dòng một id — không tự bịa id
|
|
49
61
|
sync đẩy bản ghi chưa đồng bộ lên sink
|
|
@@ -96,6 +108,7 @@ async function main(argv) {
|
|
|
96
108
|
setup: c.cmdSetup,
|
|
97
109
|
doctor: c.cmdDoctor,
|
|
98
110
|
start: c.cmdStart,
|
|
111
|
+
mode: c.cmdMode,
|
|
99
112
|
verdict: c.cmdVerdict,
|
|
100
113
|
steer: c.cmdSteer,
|
|
101
114
|
discard: c.cmdDiscard,
|
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.
|
|
29
|
+
const CORE_VERSION = '0.7.0';
|
|
30
30
|
const SCHEMA = 1;
|
|
31
31
|
const TASK_TYPES = [
|
|
32
32
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -1118,6 +1118,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1118
1118
|
for (const id of [...planOfRoot.keys(), ...stepOf.keys()]) {
|
|
1119
1119
|
if (!seenTask.has(id) && !discarded.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
1120
1120
|
}
|
|
1121
|
+
const modeEvents = new Map();
|
|
1122
|
+
for (const ev of events) if (ev && ev.kind === 'mode' && isMode(ev.mode) && typeof ev.root_task_id === 'string') modeEvents.set(ev.root_task_id, ev);
|
|
1123
|
+
const modeOfRoot = (root) => (modeEvents.has(root) ? modeEvents.get(root).mode : DEFAULT_MODE);
|
|
1121
1124
|
const per = new Map(taskIds.map((id) => [id, { dispatches: [], user: [], steers: [], overrides: [], agentIds: new Set() }]));
|
|
1122
1125
|
events.forEach((ev, i) => {
|
|
1123
1126
|
if (!ev || typeof ev !== 'object') return;
|
|
@@ -1168,6 +1171,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1168
1171
|
const pApproved = pov && evMs(pov) > (Date.parse(ps.plan.approved_at) || 0) ? pov.chosen_tier || null : null;
|
|
1169
1172
|
tasks.push(Object.assign({
|
|
1170
1173
|
task_id: id, task_type: ps.step.type || null, gate: 2, pending: true, verify_only: false, round: 0,
|
|
1174
|
+
mode: isMode(ps.step.mode) ? ps.step.mode : modeOfRoot(ps.plan.root_task_id),
|
|
1171
1175
|
}, planInfo, {
|
|
1172
1176
|
approved_tier: pApproved || ps.step.tier || null, last_tier: null, attempts: [], user_verdict: null, last_steer: null,
|
|
1173
1177
|
run_dir: planDir, last_ts: null,
|
|
@@ -1225,6 +1229,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1225
1229
|
pending: false,
|
|
1226
1230
|
verify_only: attempts.length > 0 && attempts.every((a) => a.role === 'verify'),
|
|
1227
1231
|
round,
|
|
1232
|
+
mode: dispatchModeFromEvents(t.dispatches, id) || (ps && isMode(ps.step.mode) ? ps.step.mode : modeOfRoot(plan ? plan.root_task_id : id)),
|
|
1228
1233
|
}, planInfo, {
|
|
1229
1234
|
approved_tier: approved,
|
|
1230
1235
|
last_tier: lastExecAny ? tierOf(lastExecAny.model, lastExecAny.effort) : null,
|
|
@@ -1235,10 +1240,26 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1235
1240
|
last_ts: lastMs ? new Date(lastMs).toISOString() : null,
|
|
1236
1241
|
}));
|
|
1237
1242
|
}
|
|
1243
|
+
// modes of the roots this session works on: plan roots, tasks' own roots, and roots whose mode event was written here
|
|
1244
|
+
const sidHash = sha8(sessionId);
|
|
1245
|
+
const modeRoots = new Set(plans.map((p) => p.root_task_id));
|
|
1246
|
+
for (const id of taskIds) if (modeEvents.has(id)) modeRoots.add(id);
|
|
1247
|
+
for (const [root, ev] of modeEvents) if (ev.session_id === sessionId || ev.session === sidHash) modeRoots.add(root);
|
|
1248
|
+
const modes = Array.from(modeRoots).map((root) => {
|
|
1249
|
+
const ev = modeEvents.get(root);
|
|
1250
|
+
const mode = ev ? ev.mode : DEFAULT_MODE;
|
|
1251
|
+
// core 0.7.0 (s2): suggestion only — the mode itself never changes here
|
|
1252
|
+
const sug = modeSuggestion(events, root, { mode, taskIds: rootTaskIds(allPlans, root) });
|
|
1253
|
+
return {
|
|
1254
|
+
root_task_id: root, mode, since: ev ? ev.ts : null, source: ev ? ev.source || null : 'default', reason_code: ev ? ev.reason_code || null : null,
|
|
1255
|
+
mode_suggest: sug.mode_suggest, mode_suggest_reason: sug.mode_suggest_reason,
|
|
1256
|
+
};
|
|
1257
|
+
});
|
|
1238
1258
|
return {
|
|
1239
1259
|
session_id: sessionId,
|
|
1240
1260
|
generated_at: nowIso(),
|
|
1241
1261
|
runs_dir: `${runs}/`,
|
|
1262
|
+
modes,
|
|
1242
1263
|
plans: plans.map((p) => ({
|
|
1243
1264
|
root_task_id: p.root_task_id,
|
|
1244
1265
|
approval: Number.isInteger(p.approval) ? p.approval : (Number.isInteger(p.round) ? p.round : null),
|
|
@@ -1268,7 +1289,8 @@ function taskLines(t) {
|
|
|
1268
1289
|
+ `${t.round ? ` · vòng ${t.round}` : ''}${t.approved_tier ? ` · duyệt ${t.approved_tier}` : ''}`
|
|
1269
1290
|
+ `${t.last_tier && t.last_tier !== t.approved_tier ? ` · tier cuối ${t.last_tier}` : ''}`
|
|
1270
1291
|
+ `${t.user_verdict ? ` · người dùng: ${t.user_verdict.outcome}${t.user_verdict.cause ? `/${t.user_verdict.cause}` : ''}` : ''}`
|
|
1271
|
-
+ `${t.last_steer ? ` · steer: ${t.last_steer}` : ''}
|
|
1292
|
+
+ `${t.last_steer ? ` · steer: ${t.last_steer}` : ''}`
|
|
1293
|
+
+ `${t.mode && t.mode !== DEFAULT_MODE ? ` · mode ${t.mode}` : ''}`;
|
|
1272
1294
|
const att = t.attempts.map((a) => `${a.agent_id} ${a.role}${a.try ? ` try${a.try}` : ''} ${tierOf(a.model, a.effort) || '?'} ${a.status}`);
|
|
1273
1295
|
const out = [head];
|
|
1274
1296
|
if (att.length) out.push(`${indent} ${att.join(' | ')}`);
|
|
@@ -1310,11 +1332,26 @@ function compactSystemMessage(state) {
|
|
|
1310
1332
|
return `dieu-phoi: đã nạp lại trạng thái điều phối sau khi nén (${c.tasks} task, ${c.running} agent đang chạy, ${c.pending} bước chưa giao)`;
|
|
1311
1333
|
}
|
|
1312
1334
|
|
|
1335
|
+
/** "\nMode: <root>=<mode> …" for the roots of a session (last 6); '' when the state carries none. */
|
|
1336
|
+
function modesLine(state) {
|
|
1337
|
+
const ms = Array.isArray(state.modes) ? state.modes : [];
|
|
1338
|
+
if (!ms.length) return '';
|
|
1339
|
+
const sug = ms.slice(-6).filter((m) => m.mode_suggest).map((m) => `\n${modeSuggestLine(m.root_task_id, m.mode, m.mode_suggest_reason)}`).join('');
|
|
1340
|
+
return `\nMode (theo yêu cầu, đổi bằng \`dieu-phoi mode --root <id> <mode>\`): ${ms.slice(-6).map((m) => `${m.root_task_id}=${m.mode}`).join(', ')}.${sug}`;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** One line telling the orchestrator to ASK the user about returning to normal (never switch by itself). */
|
|
1344
|
+
function modeSuggestLine(root, mode, reason) {
|
|
1345
|
+
return `GỢI Ý mode: ${root ? `${root} đang ${mode}` : `đang ${mode}`} — hỏi người dùng có muốn quay về normal không (${reason}); `
|
|
1346
|
+
+ `chỉ đổi khi người dùng đồng ý: \`dieu-phoi mode --root ${root || '<root>'} normal\`. KHÔNG tự đổi mode.`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1313
1349
|
function formatSessionState(state, cap) {
|
|
1314
1350
|
const limit = cap || STATE_TEXT_CAP;
|
|
1315
1351
|
const { open, pending, running } = stateCounts(state);
|
|
1316
1352
|
const header = `Phiên ${state.session_id}: ${state.tasks.length} task (${open.length} chưa xong, ${pending} bước chưa giao, ${running} agent đang chạy). `
|
|
1317
|
-
+ `Thư mục chạy: ${state.runs_dir} (chỉ trỏ tới, không tự đọc)
|
|
1353
|
+
+ `Thư mục chạy: ${state.runs_dir} (chỉ trỏ tới, không tự đọc).`
|
|
1354
|
+
+ modesLine(state);
|
|
1318
1355
|
if (!state.tasks.length) return `${header}\nChưa có task nào được giao trong phiên này.`;
|
|
1319
1356
|
const byAge = (a, b) => String(a.last_ts || '').localeCompare(String(b.last_ts || ''));
|
|
1320
1357
|
const dropOrder = state.tasks.filter((t) => t.gate === 'done').sort(byAge)
|
|
@@ -1503,7 +1540,7 @@ function startInfo(ctx, cwd) {
|
|
|
1503
1540
|
const run = startRun();
|
|
1504
1541
|
const health = quickHealth(ctx, run.runs);
|
|
1505
1542
|
const info = {
|
|
1506
|
-
id: run.id, run_dir: run.run_dir, step_ids: stepIds(run.id, START_STEP_IDS), health, scout_brief: scoutBriefPath(ctx),
|
|
1543
|
+
id: run.id, run_dir: run.run_dir, step_ids: stepIds(run.id, START_STEP_IDS), health, scout_brief: scoutBriefPath(ctx), mode: null,
|
|
1507
1544
|
orch_lessons: orchLessons(ctx),
|
|
1508
1545
|
};
|
|
1509
1546
|
const k = startKnowledgeOf(cwd);
|
|
@@ -1520,6 +1557,11 @@ function orchLessonLines(info) {
|
|
|
1520
1557
|
return ls.length ? ['bài học cho điều phối viên (profile/lessons.md, [orch]):'].concat(ls.map((l) => ` - ${l}`)) : [];
|
|
1521
1558
|
}
|
|
1522
1559
|
|
|
1560
|
+
/** Gate-1 mode line: the chosen mode, or the reminder to ASK the user (default normal). */
|
|
1561
|
+
function modeLine(info) {
|
|
1562
|
+
return info && isMode(info.mode) ? `mode: ${info.mode} (đã ghi cho root ${info.id}; đổi: dieu-phoi mode --root ${info.id} <mode>)` : MODE_ASK_LINE;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1523
1565
|
function formatStartBlock(info) {
|
|
1524
1566
|
return [
|
|
1525
1567
|
START_PREFIX,
|
|
@@ -1528,6 +1570,7 @@ function formatStartBlock(info) {
|
|
|
1528
1570
|
`step_ids: ${info.step_ids.join(' ')}`,
|
|
1529
1571
|
`health: ${healthLine(info.health)}`,
|
|
1530
1572
|
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
1573
|
+
modeLine(info),
|
|
1531
1574
|
...orchLessonLines(info),
|
|
1532
1575
|
...knowledgeLines(info),
|
|
1533
1576
|
'Dùng khối này cho yêu cầu MỚI ở cổng 1 (không dùng nếu đang giữa một lần điều phối). Không chạy `dieu-phoi doctor` / `new-id` / mkdir: lượt đầu tiên là Agent scout + câu hỏi cổng 1 cho người dùng.',
|
|
@@ -1652,6 +1695,203 @@ function handoffChainMessage(taskId, n) {
|
|
|
1652
1695
|
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\`).`;
|
|
1653
1696
|
}
|
|
1654
1697
|
|
|
1698
|
+
// ------------------------------------------------------------------ core 0.7.0: mode per request (fast | normal | strict)
|
|
1699
|
+
//
|
|
1700
|
+
// A request (root_task_id) has a mode; the user picks it at gate 1 (`start --mode`, `plan-table` file, or
|
|
1701
|
+
// `dieu-phoi mode --root R <mode>` at any time). Every change is one local-only event kind=mode. A step not yet
|
|
1702
|
+
// dispatched follows the root's current mode (or its own `mode` override in the approved plan); a step already
|
|
1703
|
+
// dispatched keeps the mode its first exec dispatch recorded. Nothing here ever changes a mode by itself.
|
|
1704
|
+
|
|
1705
|
+
const MODES = ['fast', 'normal', 'strict'];
|
|
1706
|
+
const DEFAULT_MODE = 'normal';
|
|
1707
|
+
/** Event kinds that stay on this machine: runSync does not send them (an older sink would answer bad_kind). */
|
|
1708
|
+
const LOCAL_ONLY_KINDS = new Set(['mode']);
|
|
1709
|
+
const MODE_ASK_LINE = 'mode: HỎI người dùng chọn mode cho yêu cầu này — fast (bỏ verifier riêng ở bước s≤6, test chọn lọc, '
|
|
1710
|
+
+ 'một verifier gộp) | normal | strict (mặc định normal nếu không trả lời); đặt bằng `dieu-phoi mode --root <root_task_id> <mode>`';
|
|
1711
|
+
|
|
1712
|
+
function isMode(m) {
|
|
1713
|
+
return typeof m === 'string' && MODES.includes(m);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** Last mode event of `root` in an events array (null when none). */
|
|
1717
|
+
function rootModeFromEvents(events, root) {
|
|
1718
|
+
let last = null;
|
|
1719
|
+
for (const ev of events) {
|
|
1720
|
+
if (ev && ev.kind === 'mode' && ev.root_task_id === root && isMode(ev.mode)) last = ev;
|
|
1721
|
+
}
|
|
1722
|
+
return last;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/** Same, from the text of events.jsonl (hook hot path: substring scan, no full parse). */
|
|
1726
|
+
function rootModeFromText(txt, root) {
|
|
1727
|
+
const needle = '"kind":"mode"';
|
|
1728
|
+
const rootNeedle = `"root_task_id":${JSON.stringify(root)}`;
|
|
1729
|
+
let last = null;
|
|
1730
|
+
let i = txt.indexOf(needle);
|
|
1731
|
+
while (i >= 0) {
|
|
1732
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
1733
|
+
const end = txt.indexOf('\n', i);
|
|
1734
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1735
|
+
if (line.includes(rootNeedle)) {
|
|
1736
|
+
try {
|
|
1737
|
+
const ev = JSON.parse(line);
|
|
1738
|
+
if (ev && ev.kind === 'mode' && isMode(ev.mode)) last = ev;
|
|
1739
|
+
} catch (_) { /* corrupt line */ }
|
|
1740
|
+
}
|
|
1741
|
+
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1742
|
+
}
|
|
1743
|
+
return last;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/** Mode the first exec dispatch of `taskId` recorded (a dispatched step keeps its mode), from an events array. */
|
|
1747
|
+
function dispatchModeFromEvents(events, taskId) {
|
|
1748
|
+
for (const ev of events) {
|
|
1749
|
+
if (ev && ev.kind === 'dispatch' && ev.task_id === taskId && (ev.role || 'exec') === 'exec' && isMode(ev.mode)) return ev.mode;
|
|
1750
|
+
}
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function dispatchModeFromText(txt, taskId) {
|
|
1755
|
+
const needle = `"task_id":${JSON.stringify(taskId)}`;
|
|
1756
|
+
let i = txt.indexOf(needle);
|
|
1757
|
+
while (i >= 0) {
|
|
1758
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
1759
|
+
const end = txt.indexOf('\n', i);
|
|
1760
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1761
|
+
if (line.includes('"kind":"dispatch"') && !line.includes('"role":"verify"') && !line.includes('"role":"plan"') && !line.includes('"role":"helper"')) {
|
|
1762
|
+
const m = /"mode":"(fast|normal|strict)"/.exec(line);
|
|
1763
|
+
if (m) return m[1];
|
|
1764
|
+
}
|
|
1765
|
+
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1766
|
+
}
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/** Approved plan holding `taskId` as a step (or as its root): { root, step|null } or null. */
|
|
1771
|
+
function planOfTask(ctx, taskId) {
|
|
1772
|
+
for (const p of loadPlans(ctx.paths.plans)) {
|
|
1773
|
+
const step = p.steps.find((s) => s && s.task_id === taskId);
|
|
1774
|
+
if (step) return { root: p.root_task_id, step };
|
|
1775
|
+
}
|
|
1776
|
+
for (const p of loadPlans(ctx.paths.plans)) if (p.root_task_id === taskId) return { root: taskId, step: null };
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Mode a task runs in: its first exec dispatch (locked) > its step `mode` in the approved plan > the root's current
|
|
1782
|
+
* mode event > normal. `events` (array) is for the CLI; without it the hook scans events.jsonl as text.
|
|
1783
|
+
* Returns { mode, source: 'dispatched'|'step'|'root'|'default', root }.
|
|
1784
|
+
*/
|
|
1785
|
+
function taskModeInfo(ctx, taskId, events) {
|
|
1786
|
+
let txt = null;
|
|
1787
|
+
if (!events) { try { txt = fs.readFileSync(ctx.paths.events, 'utf8'); } catch (_) { txt = ''; } }
|
|
1788
|
+
const pl = taskId ? planOfTask(ctx, taskId) : null;
|
|
1789
|
+
const root = pl ? pl.root : (taskId || null);
|
|
1790
|
+
if (taskId) {
|
|
1791
|
+
const locked = events ? dispatchModeFromEvents(events, taskId) : dispatchModeFromText(txt, taskId);
|
|
1792
|
+
if (locked) return { mode: locked, source: 'dispatched', root };
|
|
1793
|
+
}
|
|
1794
|
+
if (pl && pl.step && isMode(pl.step.mode)) return { mode: pl.step.mode, source: 'step', root };
|
|
1795
|
+
if (root) {
|
|
1796
|
+
const ev = events ? rootModeFromEvents(events, root) : rootModeFromText(txt, root);
|
|
1797
|
+
if (ev) return { mode: ev.mode, source: 'root', root };
|
|
1798
|
+
}
|
|
1799
|
+
return { mode: DEFAULT_MODE, source: 'default', root };
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// core 0.7.0 (s2): signals for SUGGESTING a return to normal. The CLI never switches the mode itself; only the
|
|
1803
|
+
// user's `dieu-phoi mode` does. A suggestion is computed only while the root is in fast.
|
|
1804
|
+
const FAST_FAIL_WINDOW = 10; // last N fast exec steps (all roots, file order of their first exec dispatch)
|
|
1805
|
+
const FAST_FAIL_MIN_N = 5; // fewer fast steps than this -> no rate signal yet
|
|
1806
|
+
const FAST_FAIL_RATE = 0.15; // >= this share failed -> suggest normal
|
|
1807
|
+
const BATCH_VERIFY_RE = /^batch-verify(?:-|$)/;
|
|
1808
|
+
const QUOTA_VERDICT_RE = /^(?:quota-pause|usage-limit|auto-quota-pause|auto-usage-limit|paused-for-quota)(?:-|$)/;
|
|
1809
|
+
|
|
1810
|
+
/** A user reject that counts as a quality signal (legacy `rework` too); scope rejects (request changed) do not. */
|
|
1811
|
+
function isQualityReject(ev) {
|
|
1812
|
+
if (ev.outcome === 'rework' && !ev.by) return true;
|
|
1813
|
+
return ev.by === 'user' && ev.outcome === 'reject' && ev.cause !== 'scope';
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/** Agent-layer fail (fail / timeout, never a quota pause or aborted). */
|
|
1817
|
+
function isAgentFail(ev) {
|
|
1818
|
+
if (ev.by === 'user' || ev.by === 'auto') return false;
|
|
1819
|
+
if (ev.outcome === 'fail') return true;
|
|
1820
|
+
return ev.outcome === 'timeout' && !QUOTA_VERDICT_RE.test(String(ev.reason_code || ''));
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Fail rate over the last FAST_FAIL_WINDOW fast exec steps (any root): a step is a task whose first exec dispatch
|
|
1825
|
+
* recorded mode=fast; it failed when it has an agent-layer fail or a quality reject. Discarded tasks are left out.
|
|
1826
|
+
* Returns { n, failed, rate } (rate null when n = 0).
|
|
1827
|
+
*/
|
|
1828
|
+
function fastFailRate(events) {
|
|
1829
|
+
const gone = new Set();
|
|
1830
|
+
for (const ev of events) if (ev && ev.kind === 'discard' && typeof ev.task_id === 'string') gone.add(ev.task_id);
|
|
1831
|
+
const order = [];
|
|
1832
|
+
const seen = new Set();
|
|
1833
|
+
for (const ev of events) {
|
|
1834
|
+
if (!ev || ev.kind !== 'dispatch' || typeof ev.task_id !== 'string' || !ev.task_id) continue;
|
|
1835
|
+
if ((ev.role || 'exec') !== 'exec' || seen.has(ev.task_id)) continue;
|
|
1836
|
+
seen.add(ev.task_id);
|
|
1837
|
+
if (ev.mode === 'fast' && !gone.has(ev.task_id)) order.push(ev.task_id);
|
|
1838
|
+
}
|
|
1839
|
+
const win = new Set(order.slice(-FAST_FAIL_WINDOW));
|
|
1840
|
+
const failed = new Set();
|
|
1841
|
+
for (const ev of events) {
|
|
1842
|
+
if (ev && ev.kind === 'verdict' && win.has(ev.task_id) && (isAgentFail(ev) || isQualityReject(ev))) failed.add(ev.task_id);
|
|
1843
|
+
}
|
|
1844
|
+
return { n: win.size, failed: failed.size, rate: win.size ? failed.size / win.size : null };
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Should the orchestrator SUGGEST (never apply) a return to normal for `root`? Only while `mode` (the root's current
|
|
1849
|
+
* mode) is fast. Signals, each with a short Vietnamese reason:
|
|
1850
|
+
* - batch-verify: an agent verdict fail with reason batch-verify on a task of the root, after the root's last mode event;
|
|
1851
|
+
* - reject: a quality reject (user) on a task of the root, after that mode event;
|
|
1852
|
+
* - fail-rate: >= 15% failed among the last 10 fast steps (all roots; needs >= 5 steps).
|
|
1853
|
+
* `taskIds` = the root and its plan's step task ids. Returns { mode_suggest: 'normal'|null, mode_suggest_reason,
|
|
1854
|
+
* mode_suggest_signals[] }.
|
|
1855
|
+
*/
|
|
1856
|
+
function modeSuggestion(events, root, o) {
|
|
1857
|
+
const opts = o || {};
|
|
1858
|
+
const none = { mode_suggest: null, mode_suggest_reason: null, mode_suggest_signals: [] };
|
|
1859
|
+
if (opts.mode !== 'fast') return none;
|
|
1860
|
+
const ids = new Set(opts.taskIds || []);
|
|
1861
|
+
if (root) ids.add(root);
|
|
1862
|
+
const last = root ? rootModeFromEvents(events, root) : null;
|
|
1863
|
+
const since = last ? (Date.parse(last.ts) || 0) : 0;
|
|
1864
|
+
const signals = [];
|
|
1865
|
+
const reasons = [];
|
|
1866
|
+
let batch = null;
|
|
1867
|
+
let reject = null;
|
|
1868
|
+
for (const ev of events) {
|
|
1869
|
+
if (!ev || ev.kind !== 'verdict' || !ids.has(ev.task_id)) continue;
|
|
1870
|
+
const t = Date.parse(ev.ts) || 0;
|
|
1871
|
+
if (t < since) continue;
|
|
1872
|
+
if (!batch && ev.outcome === 'fail' && BATCH_VERIFY_RE.test(String(ev.reason_code || ''))) batch = ev.task_id;
|
|
1873
|
+
if (!reject && isQualityReject(ev)) reject = ev.task_id;
|
|
1874
|
+
}
|
|
1875
|
+
if (batch) { signals.push('batch-verify'); reasons.push(`verifier gộp báo fail (task ${batch})`); }
|
|
1876
|
+
if (reject) { signals.push('reject'); reasons.push(`người dùng reject (task ${reject})`); }
|
|
1877
|
+
const fr = fastFailRate(events);
|
|
1878
|
+
if (fr.n >= FAST_FAIL_MIN_N && fr.rate >= FAST_FAIL_RATE) {
|
|
1879
|
+
signals.push('fail-rate');
|
|
1880
|
+
reasons.push(`${fr.failed}/${fr.n} bước fast gần nhất fail (${Math.round(fr.rate * 100)}% ≥ ${Math.round(FAST_FAIL_RATE * 100)}%)`);
|
|
1881
|
+
}
|
|
1882
|
+
if (!signals.length) return none;
|
|
1883
|
+
return { mode_suggest: 'normal', mode_suggest_reason: reasons.join('; '), mode_suggest_signals: signals };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/** Task ids of a root: the root itself plus the step task ids of its approved plan (if any). */
|
|
1887
|
+
function rootTaskIds(plans, root) {
|
|
1888
|
+
const out = new Set(root ? [root] : []);
|
|
1889
|
+
for (const p of plans || []) {
|
|
1890
|
+
if (p && p.root_task_id === root) for (const s of p.steps || []) if (s && s.task_id) out.add(s.task_id);
|
|
1891
|
+
}
|
|
1892
|
+
return out;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1655
1895
|
// ------------------------------------------------------------------ core 0.6.0: project identity
|
|
1656
1896
|
//
|
|
1657
1897
|
// A project (= one git repo, however many worktrees/clones) gets a stable key so every session and worktree can
|
|
@@ -2296,12 +2536,19 @@ function buildDispatch(p, ctx) {
|
|
|
2296
2536
|
orch_cum_usd: orch.cum_usd,
|
|
2297
2537
|
}, tag.parent_task_id ? { parent_task_id: tag.parent_task_id } : {},
|
|
2298
2538
|
projectFields(p),
|
|
2539
|
+
modeFields(ctx, tag),
|
|
2299
2540
|
tierFields(ctx, tag, normalizeModel(trObj.resolvedModel) || normalizeModel(ti.model) || null, explicitEffort || parentEffort || null),
|
|
2300
2541
|
// core 0.5.0: an Agent call made inside a subagent (gen-2) carries the caller's agent_id
|
|
2301
2542
|
typeof p.agent_id === 'string' && p.agent_id ? { parent_agent_id: p.agent_id } : {},
|
|
2302
2543
|
commonFields(ctx));
|
|
2303
2544
|
}
|
|
2304
2545
|
|
|
2546
|
+
/** 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
|
+
function modeFields(ctx, tag) {
|
|
2548
|
+
if (!tag || !tag.task_id || !isValidTaskId(tag.task_id)) return {};
|
|
2549
|
+
try { return { mode: taskModeInfo(ctx, tag.task_id).mode }; } catch (_) { return {}; }
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2305
2552
|
/** suggested_tier / choose_tier / suggested_source / dispatched_tier / tier_mismatch, only when choose left a record. */
|
|
2306
2553
|
function tierFields(ctx, tag, model, effort) {
|
|
2307
2554
|
const s = tag.task_id ? readSuggestion(ctx, tag.task_id, tag.role) : null;
|
|
@@ -2548,6 +2795,16 @@ function handleSubagentStop(p, ctx) {
|
|
|
2548
2795
|
}
|
|
2549
2796
|
// exec/verify/plan only: true = it returned without writing its dp-out file (the hook saved the .inline.md)
|
|
2550
2797
|
if (tag && RESULT_ROLES.includes(tag.role)) ev.inline_result = Boolean(inline);
|
|
2798
|
+
// core 0.7.0 (s2): seconds spent running tests in this segment (transcript; else the dp-out `TEST_S:` line)
|
|
2799
|
+
const tt = exists && sum ? testTimeOf(lines.slice(sum.segment_start)) : null;
|
|
2800
|
+
ev.test_s = tt ? tt.test_s : null;
|
|
2801
|
+
ev.test_runs = tt ? tt.test_runs : null;
|
|
2802
|
+
ev.full_suite_runs = tt ? tt.full_suite_runs : null;
|
|
2803
|
+
ev.test_s_source = ev.test_s !== null ? 'transcript' : null;
|
|
2804
|
+
if (ev.test_s === null && exists && tag) {
|
|
2805
|
+
const v = testSFromResultFile(lines, tag);
|
|
2806
|
+
if (v !== null) { ev.test_s = v; ev.test_s_source = 'dp-out'; }
|
|
2807
|
+
}
|
|
2551
2808
|
// Record the segment before appending so a racing duplicate fire sees it.
|
|
2552
2809
|
if (sum && sum.last_uuid) {
|
|
2553
2810
|
saveSegments(ctx, agentId, {
|
|
@@ -2630,6 +2887,134 @@ function completionStatus(text, abortWhy) {
|
|
|
2630
2887
|
return 'none';
|
|
2631
2888
|
}
|
|
2632
2889
|
|
|
2890
|
+
// ------------------------------------------------------------------ core 0.7.0 (s2): time spent running tests
|
|
2891
|
+
//
|
|
2892
|
+
// Measured at SubagentStop from the subagent transcript: every Bash/PowerShell tool_use whose command runs tests is
|
|
2893
|
+
// paired with its tool_result line; test_s = sum of (result timestamp - tool_use timestamp). This is the same span a
|
|
2894
|
+
// PreToolUse/PostToolUse(Bash) pair would give, without registering hooks on every Bash call of every session (the
|
|
2895
|
+
// dp-* agents only hook PostToolUse, which carries no start time). Backgrounded commands return at once, so they are
|
|
2896
|
+
// counted (test_runs) but not timed. Nothing timed -> fall back to the `TEST_S:` line of the agent's dp-out file.
|
|
2897
|
+
|
|
2898
|
+
const TEST_TOOL_NAMES = new Set(['Bash', 'PowerShell']);
|
|
2899
|
+
const PKG_RUNNERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
|
|
2900
|
+
const TEST_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|[\\/])tests?[\\/][^\s]*\.[cm]?[jt]sx?$/;
|
|
2901
|
+
|
|
2902
|
+
/** Positional args (not flags) of a token list. */
|
|
2903
|
+
function positionals(toks) {
|
|
2904
|
+
return toks.filter((t) => t && !t.startsWith('-'));
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
/** One command segment (no && ; |) -> null (not a test run) or { full } (full = whole suite, no file/filter args). */
|
|
2908
|
+
function testSegmentInfo(seg) {
|
|
2909
|
+
let s = ` ${seg} `.replace(/\s\d?>>?&?\s*[^\s]+/g, ' ').trim();
|
|
2910
|
+
for (let k = 0; k < 6; k++) {
|
|
2911
|
+
const n = s.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]*\s+|time\s+|env\s+|rtk\s+(?:bypass\s+)?|timeout\s+[0-9]+[a-z]*\s+|npx\s+(?:--yes\s+|-y\s+)?|&\s*)/, '');
|
|
2912
|
+
if (n === s) break;
|
|
2913
|
+
s = n;
|
|
2914
|
+
}
|
|
2915
|
+
const toks = s.split(/\s+/).filter(Boolean).map((t) => t.replace(/^["']|["']$/g, ''));
|
|
2916
|
+
if (!toks.length) return null;
|
|
2917
|
+
const head = toks[0].replace(/\.(?:cmd|exe)$/i, '').split(/[\\/]/).pop();
|
|
2918
|
+
if (PKG_RUNNERS.has(head)) {
|
|
2919
|
+
let i = 1;
|
|
2920
|
+
if (toks[i] === 'run' || toks[i] === 'run-script') i++;
|
|
2921
|
+
const script = toks[i] || '';
|
|
2922
|
+
if (script !== 'test' && script !== 't' && !/^test:/.test(script)) return null;
|
|
2923
|
+
// `npm test` alone = the whole suite; a `test:<x>` script or extra args (`npm test -- file`) = a subset
|
|
2924
|
+
return { full: (script === 'test' || script === 't') && positionals(toks.slice(i + 1)).length === 0 };
|
|
2925
|
+
}
|
|
2926
|
+
if (head === 'node') {
|
|
2927
|
+
if (toks.includes('--test')) return { full: !positionals(toks.slice(1)).some((t) => /\.[cm]?[jt]sx?$/.test(t)) };
|
|
2928
|
+
const file = positionals(toks.slice(1))[0] || '';
|
|
2929
|
+
return TEST_FILE_RE.test(file) ? { full: false } : null;
|
|
2930
|
+
}
|
|
2931
|
+
if (head === 'pytest' || head === 'py.test') return { full: positionals(toks.slice(1)).length === 0 };
|
|
2932
|
+
if (/^python[0-9.]*$|^py$/.test(head) && toks[1] === '-m' && toks[2] === 'pytest') return { full: positionals(toks.slice(3)).length === 0 };
|
|
2933
|
+
if (head === 'jest') return { full: positionals(toks.slice(1)).length === 0 };
|
|
2934
|
+
if (head === 'vitest') {
|
|
2935
|
+
const rest = toks.slice(1);
|
|
2936
|
+
if (rest[0] === 'run') rest.shift();
|
|
2937
|
+
return { full: positionals(rest).length === 0 };
|
|
2938
|
+
}
|
|
2939
|
+
if (head === 'go' && toks[1] === 'test') return { full: toks.slice(2).includes('./...') };
|
|
2940
|
+
if ((head === 'cargo' || head === 'dotnet') && toks[1] === 'test') return { full: positionals(toks.slice(2)).length === 0 };
|
|
2941
|
+
return null;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
/** A shell command -> null (runs no tests) or { full } (true when any test segment runs a whole suite). */
|
|
2945
|
+
function testCommandInfo(cmd) {
|
|
2946
|
+
if (typeof cmd !== 'string' || !cmd.trim()) return null;
|
|
2947
|
+
let hit = null;
|
|
2948
|
+
for (const seg of cmd.split(/&&|\|\||;|\||\r?\n/)) {
|
|
2949
|
+
const t = seg.trim();
|
|
2950
|
+
if (!t) continue;
|
|
2951
|
+
const info = testSegmentInfo(t);
|
|
2952
|
+
if (info) hit = { full: Boolean((hit && hit.full) || info.full) };
|
|
2953
|
+
}
|
|
2954
|
+
return hit;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* Test runs in transcript lines (one segment). Returns { test_s, test_runs, full_suite_runs }: test_s = seconds summed
|
|
2959
|
+
* over the timed (foreground, with a result) test commands, null when none was timed; counts include backgrounded runs.
|
|
2960
|
+
*/
|
|
2961
|
+
function testTimeOf(lines) {
|
|
2962
|
+
const open = new Map();
|
|
2963
|
+
let runs = 0;
|
|
2964
|
+
let full = 0;
|
|
2965
|
+
let timed = 0;
|
|
2966
|
+
let ms = 0;
|
|
2967
|
+
for (const l of lines || []) {
|
|
2968
|
+
if (!l || typeof l !== 'object' || !l.message || !Array.isArray(l.message.content)) continue;
|
|
2969
|
+
const t = typeof l.timestamp === 'string' ? Date.parse(l.timestamp) : NaN;
|
|
2970
|
+
if (l.type === 'assistant') {
|
|
2971
|
+
for (const b of l.message.content) {
|
|
2972
|
+
if (!b || b.type !== 'tool_use' || !TEST_TOOL_NAMES.has(b.name) || !b.input) continue;
|
|
2973
|
+
const info = testCommandInfo(b.input.command);
|
|
2974
|
+
if (!info) continue;
|
|
2975
|
+
runs++;
|
|
2976
|
+
if (info.full) full++;
|
|
2977
|
+
if (b.id && b.input.run_in_background !== true && !isNaN(t)) open.set(b.id, t);
|
|
2978
|
+
}
|
|
2979
|
+
} else if (l.type === 'user') {
|
|
2980
|
+
for (const b of l.message.content) {
|
|
2981
|
+
if (!b || b.type !== 'tool_result' || !open.has(b.tool_use_id)) continue;
|
|
2982
|
+
const t0 = open.get(b.tool_use_id);
|
|
2983
|
+
open.delete(b.tool_use_id);
|
|
2984
|
+
if (!isNaN(t) && t >= t0) { ms += t - t0; timed++; }
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return { test_s: timed ? Math.round(ms / 100) / 10 : null, test_runs: runs, full_suite_runs: full };
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
/** Seconds from a `TEST_S: <n>[s|m|phút]` line of a result file's text (first 200 lines), or null. */
|
|
2992
|
+
function testSFromText(text) {
|
|
2993
|
+
const head = String(text || '').split(/\r?\n/).slice(0, 200);
|
|
2994
|
+
for (const raw of head) {
|
|
2995
|
+
const m = /^[\s>*_`#-]*TEST_S\s*[*_`]*\s*[:=]\s*[*_`~≈]*\s*([0-9]+(?:[.,][0-9]+)?)\s*(giây|phút|ph|min|m|s)?\b/i.exec(raw);
|
|
2996
|
+
if (!m) continue;
|
|
2997
|
+
const n = Number(m[1].replace(',', '.'));
|
|
2998
|
+
if (!isFinite(n) || n < 0) return null;
|
|
2999
|
+
const unit = (m[2] || 's').toLowerCase();
|
|
3000
|
+
return Math.round((unit === 's' || unit === 'giây' ? n : n * 60) * 10) / 10;
|
|
3001
|
+
}
|
|
3002
|
+
return null;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
/** test_s from the agent's dp-out file(s) for (task, role, try) in the run dir its brief names, or null. Never throws. */
|
|
3006
|
+
function testSFromResultFile(lines, tag) {
|
|
3007
|
+
try {
|
|
3008
|
+
if (!tag || !RESULT_ROLES.includes(tag.role) || !tag.task_id) return null;
|
|
3009
|
+
const dir = resultRunDir(firstPromptText(lines), tag.task_id);
|
|
3010
|
+
for (const f of findResultFiles(dir, tag.task_id, tag.role, tag.try)) {
|
|
3011
|
+
const v = testSFromText(readTextFile(f));
|
|
3012
|
+
if (v !== null) return v;
|
|
3013
|
+
}
|
|
3014
|
+
} catch (_) { /* best effort */ }
|
|
3015
|
+
return null;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2633
3018
|
// ------------------------------------------------------------------ core 0.5.0: context watchdog + gen-2 gate
|
|
2634
3019
|
//
|
|
2635
3020
|
// Both run only from the hooks of the dp-* agent files (~/.claude/agents, installed by `dieu-phoi setup`), so a session
|
|
@@ -3175,7 +3560,7 @@ async function runSync(ctx, opts) {
|
|
|
3175
3560
|
for (const l of batch) {
|
|
3176
3561
|
try {
|
|
3177
3562
|
const r = JSON.parse(l.text);
|
|
3178
|
-
if (r && typeof r === 'object' && !Array.isArray(r)) {
|
|
3563
|
+
if (r && typeof r === 'object' && !Array.isArray(r) && !LOCAL_ONLY_KINDS.has(r.kind)) {
|
|
3179
3564
|
delete r.synced;
|
|
3180
3565
|
records.push(r);
|
|
3181
3566
|
}
|
|
@@ -3212,6 +3597,9 @@ async function runSync(ctx, opts) {
|
|
|
3212
3597
|
|
|
3213
3598
|
module.exports = {
|
|
3214
3599
|
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, startKnowledgeOf, knowledgeLines, 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,
|
|
3600
|
+
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS, MODE_ASK_LINE, isMode, rootModeFromEvents, rootModeFromText, dispatchModeFromEvents, taskModeInfo, planOfTask, modeFields,
|
|
3601
|
+
modeSuggestion, modeSuggestLine, fastFailRate, rootTaskIds, FAST_FAIL_WINDOW, FAST_FAIL_MIN_N, FAST_FAIL_RATE,
|
|
3602
|
+
testCommandInfo, testTimeOf, testSFromText,
|
|
3215
3603
|
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED,
|
|
3216
3604
|
sha8, nowIso, readJsonFile, writeFileAtomic, renameWithRetry, acquireLock, acquireFileLock, withFileLock, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
3217
3605
|
logError, computeUserHash, userHash, commonFields, parseTag, resolveTag, knownTaskTypes, tagIssueMessage, DEFAULT_EXTRA_TYPES,
|