@kienntpixon/dieu-phoi 0.6.1 → 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 +72 -0
- package/bin/dieu-phoi.cjs +20 -7
- package/hook.cjs +625 -15
- package/lib/aggregate.cjs +181 -16
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +101 -27
- package/lib/commands.cjs +243 -22
- package/lib/knowledge.cjs +74 -18
- package/lib/setup.cjs +16 -8
- package/package.json +1 -1
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.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
|
|
@@ -1118,6 +1151,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1118
1151
|
for (const id of [...planOfRoot.keys(), ...stepOf.keys()]) {
|
|
1119
1152
|
if (!seenTask.has(id) && !discarded.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
1120
1153
|
}
|
|
1154
|
+
const modeEvents = new Map();
|
|
1155
|
+
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);
|
|
1156
|
+
const modeOfRoot = (root) => (modeEvents.has(root) ? modeEvents.get(root).mode : DEFAULT_MODE);
|
|
1121
1157
|
const per = new Map(taskIds.map((id) => [id, { dispatches: [], user: [], steers: [], overrides: [], agentIds: new Set() }]));
|
|
1122
1158
|
events.forEach((ev, i) => {
|
|
1123
1159
|
if (!ev || typeof ev !== 'object') return;
|
|
@@ -1168,6 +1204,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1168
1204
|
const pApproved = pov && evMs(pov) > (Date.parse(ps.plan.approved_at) || 0) ? pov.chosen_tier || null : null;
|
|
1169
1205
|
tasks.push(Object.assign({
|
|
1170
1206
|
task_id: id, task_type: ps.step.type || null, gate: 2, pending: true, verify_only: false, round: 0,
|
|
1207
|
+
mode: isMode(ps.step.mode) ? ps.step.mode : modeOfRoot(ps.plan.root_task_id),
|
|
1171
1208
|
}, planInfo, {
|
|
1172
1209
|
approved_tier: pApproved || ps.step.tier || null, last_tier: null, attempts: [], user_verdict: null, last_steer: null,
|
|
1173
1210
|
run_dir: planDir, last_ts: null,
|
|
@@ -1225,6 +1262,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1225
1262
|
pending: false,
|
|
1226
1263
|
verify_only: attempts.length > 0 && attempts.every((a) => a.role === 'verify'),
|
|
1227
1264
|
round,
|
|
1265
|
+
mode: dispatchModeFromEvents(t.dispatches, id) || (ps && isMode(ps.step.mode) ? ps.step.mode : modeOfRoot(plan ? plan.root_task_id : id)),
|
|
1228
1266
|
}, planInfo, {
|
|
1229
1267
|
approved_tier: approved,
|
|
1230
1268
|
last_tier: lastExecAny ? tierOf(lastExecAny.model, lastExecAny.effort) : null,
|
|
@@ -1235,10 +1273,26 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1235
1273
|
last_ts: lastMs ? new Date(lastMs).toISOString() : null,
|
|
1236
1274
|
}));
|
|
1237
1275
|
}
|
|
1276
|
+
// modes of the roots this session works on: plan roots, tasks' own roots, and roots whose mode event was written here
|
|
1277
|
+
const sidHash = sha8(sessionId);
|
|
1278
|
+
const modeRoots = new Set(plans.map((p) => p.root_task_id));
|
|
1279
|
+
for (const id of taskIds) if (modeEvents.has(id)) modeRoots.add(id);
|
|
1280
|
+
for (const [root, ev] of modeEvents) if (ev.session_id === sessionId || ev.session === sidHash) modeRoots.add(root);
|
|
1281
|
+
const modes = Array.from(modeRoots).map((root) => {
|
|
1282
|
+
const ev = modeEvents.get(root);
|
|
1283
|
+
const mode = ev ? ev.mode : DEFAULT_MODE;
|
|
1284
|
+
// core 0.7.0 (s2): suggestion only — the mode itself never changes here
|
|
1285
|
+
const sug = modeSuggestion(events, root, { mode, taskIds: rootTaskIds(allPlans, root) });
|
|
1286
|
+
return {
|
|
1287
|
+
root_task_id: root, mode, since: ev ? ev.ts : null, source: ev ? ev.source || null : 'default', reason_code: ev ? ev.reason_code || null : null,
|
|
1288
|
+
mode_suggest: sug.mode_suggest, mode_suggest_reason: sug.mode_suggest_reason,
|
|
1289
|
+
};
|
|
1290
|
+
});
|
|
1238
1291
|
return {
|
|
1239
1292
|
session_id: sessionId,
|
|
1240
1293
|
generated_at: nowIso(),
|
|
1241
1294
|
runs_dir: `${runs}/`,
|
|
1295
|
+
modes,
|
|
1242
1296
|
plans: plans.map((p) => ({
|
|
1243
1297
|
root_task_id: p.root_task_id,
|
|
1244
1298
|
approval: Number.isInteger(p.approval) ? p.approval : (Number.isInteger(p.round) ? p.round : null),
|
|
@@ -1268,7 +1322,8 @@ function taskLines(t) {
|
|
|
1268
1322
|
+ `${t.round ? ` · vòng ${t.round}` : ''}${t.approved_tier ? ` · duyệt ${t.approved_tier}` : ''}`
|
|
1269
1323
|
+ `${t.last_tier && t.last_tier !== t.approved_tier ? ` · tier cuối ${t.last_tier}` : ''}`
|
|
1270
1324
|
+ `${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}` : ''}
|
|
1325
|
+
+ `${t.last_steer ? ` · steer: ${t.last_steer}` : ''}`
|
|
1326
|
+
+ `${t.mode && t.mode !== DEFAULT_MODE ? ` · mode ${t.mode}` : ''}`;
|
|
1272
1327
|
const att = t.attempts.map((a) => `${a.agent_id} ${a.role}${a.try ? ` try${a.try}` : ''} ${tierOf(a.model, a.effort) || '?'} ${a.status}`);
|
|
1273
1328
|
const out = [head];
|
|
1274
1329
|
if (att.length) out.push(`${indent} ${att.join(' | ')}`);
|
|
@@ -1310,11 +1365,26 @@ function compactSystemMessage(state) {
|
|
|
1310
1365
|
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
1366
|
}
|
|
1312
1367
|
|
|
1368
|
+
/** "\nMode: <root>=<mode> …" for the roots of a session (last 6); '' when the state carries none. */
|
|
1369
|
+
function modesLine(state) {
|
|
1370
|
+
const ms = Array.isArray(state.modes) ? state.modes : [];
|
|
1371
|
+
if (!ms.length) return '';
|
|
1372
|
+
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('');
|
|
1373
|
+
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}`;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** One line telling the orchestrator to ASK the user about returning to normal (never switch by itself). */
|
|
1377
|
+
function modeSuggestLine(root, mode, reason) {
|
|
1378
|
+
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}); `
|
|
1379
|
+
+ `chỉ đổi khi người dùng đồng ý: \`dieu-phoi mode --root ${root || '<root>'} normal\`. KHÔNG tự đổi mode.`;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1313
1382
|
function formatSessionState(state, cap) {
|
|
1314
1383
|
const limit = cap || STATE_TEXT_CAP;
|
|
1315
1384
|
const { open, pending, running } = stateCounts(state);
|
|
1316
1385
|
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)
|
|
1386
|
+
+ `Thư mục chạy: ${state.runs_dir} (chỉ trỏ tới, không tự đọc).`
|
|
1387
|
+
+ modesLine(state);
|
|
1318
1388
|
if (!state.tasks.length) return `${header}\nChưa có task nào được giao trong phiên này.`;
|
|
1319
1389
|
const byAge = (a, b) => String(a.last_ts || '').localeCompare(String(b.last_ts || ''));
|
|
1320
1390
|
const dropOrder = state.tasks.filter((t) => t.gate === 'done').sort(byAge)
|
|
@@ -1503,16 +1573,19 @@ function startInfo(ctx, cwd) {
|
|
|
1503
1573
|
const run = startRun();
|
|
1504
1574
|
const health = quickHealth(ctx, run.runs);
|
|
1505
1575
|
const info = {
|
|
1506
|
-
id: run.id, run_dir: run.run_dir, step_ids: stepIds(run.id, START_STEP_IDS), health, scout_brief: scoutBriefPath(ctx),
|
|
1576
|
+
id: run.id, run_dir: run.run_dir, step_ids: stepIds(run.id, START_STEP_IDS), health, scout_brief: scoutBriefPath(ctx), mode: null,
|
|
1507
1577
|
orch_lessons: orchLessons(ctx),
|
|
1508
1578
|
};
|
|
1509
1579
|
const k = startKnowledgeOf(cwd);
|
|
1510
1580
|
if (k) info.knowledge = k;
|
|
1581
|
+
const d = scanUnmergedDeltas();
|
|
1582
|
+
if (d.count) info.unmerged_deltas = d;
|
|
1511
1583
|
return info;
|
|
1512
1584
|
}
|
|
1513
1585
|
|
|
1514
1586
|
function knowledgeLines(info) {
|
|
1515
|
-
|
|
1587
|
+
const own = info && info.knowledge && Array.isArray(info.knowledge.lines) ? info.knowledge.lines : [];
|
|
1588
|
+
return own.concat(unmergedDeltaLines(info && info.unmerged_deltas));
|
|
1516
1589
|
}
|
|
1517
1590
|
|
|
1518
1591
|
function orchLessonLines(info) {
|
|
@@ -1520,6 +1593,11 @@ function orchLessonLines(info) {
|
|
|
1520
1593
|
return ls.length ? ['bài học cho điều phối viên (profile/lessons.md, [orch]):'].concat(ls.map((l) => ` - ${l}`)) : [];
|
|
1521
1594
|
}
|
|
1522
1595
|
|
|
1596
|
+
/** Gate-1 mode line: the chosen mode, or the reminder to ASK the user (default normal). */
|
|
1597
|
+
function modeLine(info) {
|
|
1598
|
+
return info && isMode(info.mode) ? `mode: ${info.mode} (đã ghi cho root ${info.id}; đổi: dieu-phoi mode --root ${info.id} <mode>)` : MODE_ASK_LINE;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1523
1601
|
function formatStartBlock(info) {
|
|
1524
1602
|
return [
|
|
1525
1603
|
START_PREFIX,
|
|
@@ -1528,6 +1606,7 @@ function formatStartBlock(info) {
|
|
|
1528
1606
|
`step_ids: ${info.step_ids.join(' ')}`,
|
|
1529
1607
|
`health: ${healthLine(info.health)}`,
|
|
1530
1608
|
`scout_brief: ${info.scout_brief || '(không thấy — dùng references/brief-template.md mục E)'}`,
|
|
1609
|
+
modeLine(info),
|
|
1531
1610
|
...orchLessonLines(info),
|
|
1532
1611
|
...knowledgeLines(info),
|
|
1533
1612
|
'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.',
|
|
@@ -1590,6 +1669,12 @@ function recordSuggestion(ctx, rec) {
|
|
|
1590
1669
|
source: prev && prev.suggested_tier ? prev.source : (rec.source || 'choose'),
|
|
1591
1670
|
ts: nowIso(),
|
|
1592
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
|
+
}
|
|
1593
1678
|
try {
|
|
1594
1679
|
fs.mkdirSync(ctx.paths.suggest, { recursive: true });
|
|
1595
1680
|
writeFileAtomic(file, `${JSON.stringify(out)}\n`);
|
|
@@ -1623,9 +1708,11 @@ function pruneSuggest(ctx) {
|
|
|
1623
1708
|
// ------------------------------------------------------------------ core 0.5.3: handoff chains (retro 2026-10-22)
|
|
1624
1709
|
//
|
|
1625
1710
|
// 15/63 feature-impl tasks handed off >= 2 times (19% of feature-impl cost); one chain ran 10 handoffs ($70) and
|
|
1626
|
-
// 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.
|
|
1627
1713
|
|
|
1628
|
-
const HANDOFF_CHAIN_WARN =
|
|
1714
|
+
const HANDOFF_CHAIN_WARN = 2;
|
|
1715
|
+
const HANDOFF_CHAIN_DENY = 3;
|
|
1629
1716
|
|
|
1630
1717
|
/** Number of agents whose `complete` event has handoff:true for this task (substring scan of events.jsonl). */
|
|
1631
1718
|
function countTaskHandoffs(ctx, taskId) {
|
|
@@ -1649,7 +1736,204 @@ function countTaskHandoffs(ctx, taskId) {
|
|
|
1649
1736
|
}
|
|
1650
1737
|
|
|
1651
1738
|
function handoffChainMessage(taskId, n) {
|
|
1652
|
-
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\`).`;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// ------------------------------------------------------------------ core 0.7.0: mode per request (fast | normal | strict)
|
|
1743
|
+
//
|
|
1744
|
+
// A request (root_task_id) has a mode; the user picks it at gate 1 (`start --mode`, `plan-table` file, or
|
|
1745
|
+
// `dieu-phoi mode --root R <mode>` at any time). Every change is one local-only event kind=mode. A step not yet
|
|
1746
|
+
// dispatched follows the root's current mode (or its own `mode` override in the approved plan); a step already
|
|
1747
|
+
// dispatched keeps the mode its first exec dispatch recorded. Nothing here ever changes a mode by itself.
|
|
1748
|
+
|
|
1749
|
+
const MODES = ['fast', 'normal', 'strict'];
|
|
1750
|
+
const DEFAULT_MODE = 'normal';
|
|
1751
|
+
/** Event kinds that stay on this machine: runSync does not send them (an older sink would answer bad_kind). */
|
|
1752
|
+
const LOCAL_ONLY_KINDS = new Set(['mode']);
|
|
1753
|
+
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, '
|
|
1754
|
+
+ '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>`';
|
|
1755
|
+
|
|
1756
|
+
function isMode(m) {
|
|
1757
|
+
return typeof m === 'string' && MODES.includes(m);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/** Last mode event of `root` in an events array (null when none). */
|
|
1761
|
+
function rootModeFromEvents(events, root) {
|
|
1762
|
+
let last = null;
|
|
1763
|
+
for (const ev of events) {
|
|
1764
|
+
if (ev && ev.kind === 'mode' && ev.root_task_id === root && isMode(ev.mode)) last = ev;
|
|
1765
|
+
}
|
|
1766
|
+
return last;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/** Same, from the text of events.jsonl (hook hot path: substring scan, no full parse). */
|
|
1770
|
+
function rootModeFromText(txt, root) {
|
|
1771
|
+
const needle = '"kind":"mode"';
|
|
1772
|
+
const rootNeedle = `"root_task_id":${JSON.stringify(root)}`;
|
|
1773
|
+
let last = null;
|
|
1774
|
+
let i = txt.indexOf(needle);
|
|
1775
|
+
while (i >= 0) {
|
|
1776
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
1777
|
+
const end = txt.indexOf('\n', i);
|
|
1778
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1779
|
+
if (line.includes(rootNeedle)) {
|
|
1780
|
+
try {
|
|
1781
|
+
const ev = JSON.parse(line);
|
|
1782
|
+
if (ev && ev.kind === 'mode' && isMode(ev.mode)) last = ev;
|
|
1783
|
+
} catch (_) { /* corrupt line */ }
|
|
1784
|
+
}
|
|
1785
|
+
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1786
|
+
}
|
|
1787
|
+
return last;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/** Mode the first exec dispatch of `taskId` recorded (a dispatched step keeps its mode), from an events array. */
|
|
1791
|
+
function dispatchModeFromEvents(events, taskId) {
|
|
1792
|
+
for (const ev of events) {
|
|
1793
|
+
if (ev && ev.kind === 'dispatch' && ev.task_id === taskId && (ev.role || 'exec') === 'exec' && isMode(ev.mode)) return ev.mode;
|
|
1794
|
+
}
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function dispatchModeFromText(txt, taskId) {
|
|
1799
|
+
const needle = `"task_id":${JSON.stringify(taskId)}`;
|
|
1800
|
+
let i = txt.indexOf(needle);
|
|
1801
|
+
while (i >= 0) {
|
|
1802
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
1803
|
+
const end = txt.indexOf('\n', i);
|
|
1804
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1805
|
+
if (line.includes('"kind":"dispatch"') && !line.includes('"role":"verify"') && !line.includes('"role":"plan"') && !line.includes('"role":"helper"')) {
|
|
1806
|
+
const m = /"mode":"(fast|normal|strict)"/.exec(line);
|
|
1807
|
+
if (m) return m[1];
|
|
1808
|
+
}
|
|
1809
|
+
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1810
|
+
}
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/** Approved plan holding `taskId` as a step (or as its root): { root, step|null } or null. */
|
|
1815
|
+
function planOfTask(ctx, taskId) {
|
|
1816
|
+
for (const p of loadPlans(ctx.paths.plans)) {
|
|
1817
|
+
const step = p.steps.find((s) => s && s.task_id === taskId);
|
|
1818
|
+
if (step) return { root: p.root_task_id, step };
|
|
1819
|
+
}
|
|
1820
|
+
for (const p of loadPlans(ctx.paths.plans)) if (p.root_task_id === taskId) return { root: taskId, step: null };
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Mode a task runs in: its first exec dispatch (locked) > its step `mode` in the approved plan > the root's current
|
|
1826
|
+
* mode event > normal. `events` (array) is for the CLI; without it the hook scans events.jsonl as text.
|
|
1827
|
+
* Returns { mode, source: 'dispatched'|'step'|'root'|'default', root }.
|
|
1828
|
+
*/
|
|
1829
|
+
function taskModeInfo(ctx, taskId, events) {
|
|
1830
|
+
let txt = null;
|
|
1831
|
+
if (!events) { try { txt = fs.readFileSync(ctx.paths.events, 'utf8'); } catch (_) { txt = ''; } }
|
|
1832
|
+
const pl = taskId ? planOfTask(ctx, taskId) : null;
|
|
1833
|
+
const root = pl ? pl.root : (taskId || null);
|
|
1834
|
+
if (taskId) {
|
|
1835
|
+
const locked = events ? dispatchModeFromEvents(events, taskId) : dispatchModeFromText(txt, taskId);
|
|
1836
|
+
if (locked) return { mode: locked, source: 'dispatched', root };
|
|
1837
|
+
}
|
|
1838
|
+
if (pl && pl.step && isMode(pl.step.mode)) return { mode: pl.step.mode, source: 'step', root };
|
|
1839
|
+
if (root) {
|
|
1840
|
+
const ev = events ? rootModeFromEvents(events, root) : rootModeFromText(txt, root);
|
|
1841
|
+
if (ev) return { mode: ev.mode, source: 'root', root };
|
|
1842
|
+
}
|
|
1843
|
+
return { mode: DEFAULT_MODE, source: 'default', root };
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// core 0.7.0 (s2): signals for SUGGESTING a return to normal. The CLI never switches the mode itself; only the
|
|
1847
|
+
// user's `dieu-phoi mode` does. A suggestion is computed only while the root is in fast.
|
|
1848
|
+
const FAST_FAIL_WINDOW = 10; // last N fast exec steps (all roots, file order of their first exec dispatch)
|
|
1849
|
+
const FAST_FAIL_MIN_N = 5; // fewer fast steps than this -> no rate signal yet
|
|
1850
|
+
const FAST_FAIL_RATE = 0.15; // >= this share failed -> suggest normal
|
|
1851
|
+
const BATCH_VERIFY_RE = /^batch-verify(?:-|$)/;
|
|
1852
|
+
const QUOTA_VERDICT_RE = /^(?:quota-pause|usage-limit|auto-quota-pause|auto-usage-limit|paused-for-quota)(?:-|$)/;
|
|
1853
|
+
|
|
1854
|
+
/** A user reject that counts as a quality signal (legacy `rework` too); scope rejects (request changed) do not. */
|
|
1855
|
+
function isQualityReject(ev) {
|
|
1856
|
+
if (ev.outcome === 'rework' && !ev.by) return true;
|
|
1857
|
+
return ev.by === 'user' && ev.outcome === 'reject' && ev.cause !== 'scope';
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/** Agent-layer fail (fail / timeout, never a quota pause or aborted). */
|
|
1861
|
+
function isAgentFail(ev) {
|
|
1862
|
+
if (ev.by === 'user' || ev.by === 'auto') return false;
|
|
1863
|
+
if (ev.outcome === 'fail') return true;
|
|
1864
|
+
return ev.outcome === 'timeout' && !QUOTA_VERDICT_RE.test(String(ev.reason_code || ''));
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Fail rate over the last FAST_FAIL_WINDOW fast exec steps (any root): a step is a task whose first exec dispatch
|
|
1869
|
+
* recorded mode=fast; it failed when it has an agent-layer fail or a quality reject. Discarded tasks are left out.
|
|
1870
|
+
* Returns { n, failed, rate } (rate null when n = 0).
|
|
1871
|
+
*/
|
|
1872
|
+
function fastFailRate(events) {
|
|
1873
|
+
const gone = new Set();
|
|
1874
|
+
for (const ev of events) if (ev && ev.kind === 'discard' && typeof ev.task_id === 'string') gone.add(ev.task_id);
|
|
1875
|
+
const order = [];
|
|
1876
|
+
const seen = new Set();
|
|
1877
|
+
for (const ev of events) {
|
|
1878
|
+
if (!ev || ev.kind !== 'dispatch' || typeof ev.task_id !== 'string' || !ev.task_id) continue;
|
|
1879
|
+
if ((ev.role || 'exec') !== 'exec' || seen.has(ev.task_id)) continue;
|
|
1880
|
+
seen.add(ev.task_id);
|
|
1881
|
+
if (ev.mode === 'fast' && !gone.has(ev.task_id)) order.push(ev.task_id);
|
|
1882
|
+
}
|
|
1883
|
+
const win = new Set(order.slice(-FAST_FAIL_WINDOW));
|
|
1884
|
+
const failed = new Set();
|
|
1885
|
+
for (const ev of events) {
|
|
1886
|
+
if (ev && ev.kind === 'verdict' && win.has(ev.task_id) && (isAgentFail(ev) || isQualityReject(ev))) failed.add(ev.task_id);
|
|
1887
|
+
}
|
|
1888
|
+
return { n: win.size, failed: failed.size, rate: win.size ? failed.size / win.size : null };
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Should the orchestrator SUGGEST (never apply) a return to normal for `root`? Only while `mode` (the root's current
|
|
1893
|
+
* mode) is fast. Signals, each with a short Vietnamese reason:
|
|
1894
|
+
* - batch-verify: an agent verdict fail with reason batch-verify on a task of the root, after the root's last mode event;
|
|
1895
|
+
* - reject: a quality reject (user) on a task of the root, after that mode event;
|
|
1896
|
+
* - fail-rate: >= 15% failed among the last 10 fast steps (all roots; needs >= 5 steps).
|
|
1897
|
+
* `taskIds` = the root and its plan's step task ids. Returns { mode_suggest: 'normal'|null, mode_suggest_reason,
|
|
1898
|
+
* mode_suggest_signals[] }.
|
|
1899
|
+
*/
|
|
1900
|
+
function modeSuggestion(events, root, o) {
|
|
1901
|
+
const opts = o || {};
|
|
1902
|
+
const none = { mode_suggest: null, mode_suggest_reason: null, mode_suggest_signals: [] };
|
|
1903
|
+
if (opts.mode !== 'fast') return none;
|
|
1904
|
+
const ids = new Set(opts.taskIds || []);
|
|
1905
|
+
if (root) ids.add(root);
|
|
1906
|
+
const last = root ? rootModeFromEvents(events, root) : null;
|
|
1907
|
+
const since = last ? (Date.parse(last.ts) || 0) : 0;
|
|
1908
|
+
const signals = [];
|
|
1909
|
+
const reasons = [];
|
|
1910
|
+
let batch = null;
|
|
1911
|
+
let reject = null;
|
|
1912
|
+
for (const ev of events) {
|
|
1913
|
+
if (!ev || ev.kind !== 'verdict' || !ids.has(ev.task_id)) continue;
|
|
1914
|
+
const t = Date.parse(ev.ts) || 0;
|
|
1915
|
+
if (t < since) continue;
|
|
1916
|
+
if (!batch && ev.outcome === 'fail' && BATCH_VERIFY_RE.test(String(ev.reason_code || ''))) batch = ev.task_id;
|
|
1917
|
+
if (!reject && isQualityReject(ev)) reject = ev.task_id;
|
|
1918
|
+
}
|
|
1919
|
+
if (batch) { signals.push('batch-verify'); reasons.push(`verifier gộp báo fail (task ${batch})`); }
|
|
1920
|
+
if (reject) { signals.push('reject'); reasons.push(`người dùng reject (task ${reject})`); }
|
|
1921
|
+
const fr = fastFailRate(events);
|
|
1922
|
+
if (fr.n >= FAST_FAIL_MIN_N && fr.rate >= FAST_FAIL_RATE) {
|
|
1923
|
+
signals.push('fail-rate');
|
|
1924
|
+
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)}%)`);
|
|
1925
|
+
}
|
|
1926
|
+
if (!signals.length) return none;
|
|
1927
|
+
return { mode_suggest: 'normal', mode_suggest_reason: reasons.join('; '), mode_suggest_signals: signals };
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/** Task ids of a root: the root itself plus the step task ids of its approved plan (if any). */
|
|
1931
|
+
function rootTaskIds(plans, root) {
|
|
1932
|
+
const out = new Set(root ? [root] : []);
|
|
1933
|
+
for (const p of plans || []) {
|
|
1934
|
+
if (p && p.root_task_id === root) for (const s of p.steps || []) if (s && s.task_id) out.add(s.task_id);
|
|
1935
|
+
}
|
|
1936
|
+
return out;
|
|
1653
1937
|
}
|
|
1654
1938
|
|
|
1655
1939
|
// ------------------------------------------------------------------ core 0.6.0: project identity
|
|
@@ -2181,6 +2465,135 @@ function relatedStatus(r) {
|
|
|
2181
2465
|
return { name: r.name, rel: r.rel, n, counts: freshCounts(hit.sections), text: `${knowledgeSummary({ sections: hit.sections }, n)} (cache)` };
|
|
2182
2466
|
}
|
|
2183
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
|
+
|
|
2184
2597
|
/**
|
|
2185
2598
|
* Knowledge lines for the start block of the repo at `cwd`; null outside a repo or when the repo has no store.
|
|
2186
2599
|
* Refreshes this repo only (at most one git diff-tree, on a cache miss, bounded by HOOK_DIFF_TIMEOUT_MS; never git
|
|
@@ -2278,6 +2691,9 @@ function buildDispatch(p, ctx) {
|
|
|
2278
2691
|
...(tag.task_id && !isValidTaskId(tag.task_id) ? { task_id_invalid: true } : {}),
|
|
2279
2692
|
...(tag.task_id_inferred ? { task_id_inferred: true } : {}),
|
|
2280
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 } : {}),
|
|
2281
2697
|
task_type: tag.task_type,
|
|
2282
2698
|
score: tag.score,
|
|
2283
2699
|
urgency: tag.urgency,
|
|
@@ -2296,12 +2712,24 @@ function buildDispatch(p, ctx) {
|
|
|
2296
2712
|
orch_cum_usd: orch.cum_usd,
|
|
2297
2713
|
}, tag.parent_task_id ? { parent_task_id: tag.parent_task_id } : {},
|
|
2298
2714
|
projectFields(p),
|
|
2715
|
+
modeFields(ctx, tag),
|
|
2299
2716
|
tierFields(ctx, tag, normalizeModel(trObj.resolvedModel) || normalizeModel(ti.model) || null, explicitEffort || parentEffort || null),
|
|
2300
2717
|
// core 0.5.0: an Agent call made inside a subagent (gen-2) carries the caller's agent_id
|
|
2301
2718
|
typeof p.agent_id === 'string' && p.agent_id ? { parent_agent_id: p.agent_id } : {},
|
|
2302
2719
|
commonFields(ctx));
|
|
2303
2720
|
}
|
|
2304
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
|
+
|
|
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. */
|
|
2728
|
+
function modeFields(ctx, tag) {
|
|
2729
|
+
if (!tag || !tag.task_id || !isValidTaskId(tag.task_id)) return {};
|
|
2730
|
+
try { return { mode: taskModeInfo(ctx, tag.task_id).mode }; } catch (_) { return {}; }
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2305
2733
|
/** suggested_tier / choose_tier / suggested_source / dispatched_tier / tier_mismatch, only when choose left a record. */
|
|
2306
2734
|
function tierFields(ctx, tag, model, effort) {
|
|
2307
2735
|
const s = tag.task_id ? readSuggestion(ctx, tag.task_id, tag.role) : null;
|
|
@@ -2314,6 +2742,15 @@ function tierFields(ctx, tag, model, effort) {
|
|
|
2314
2742
|
dispatched_tier: dispatched,
|
|
2315
2743
|
};
|
|
2316
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;
|
|
2317
2754
|
return out;
|
|
2318
2755
|
}
|
|
2319
2756
|
|
|
@@ -2332,6 +2769,7 @@ function handlePostToolUse(p, ctx) {
|
|
|
2332
2769
|
const tag = resolveTag(ti.prompt, known) || { tag_issue: ev.tag_issue, task_id: null, task_type: ev.task_type };
|
|
2333
2770
|
notes.push(tagIssueMessage(tag, known));
|
|
2334
2771
|
}
|
|
2772
|
+
if (ev.choose_type_mismatch) notes.push(chooseTypeMismatchMessage(ev));
|
|
2335
2773
|
if (ev.task_id && ev.role === 'exec') {
|
|
2336
2774
|
const n = countTaskHandoffs(ctx, ev.task_id);
|
|
2337
2775
|
if (n) ev.task_handoffs = n;
|
|
@@ -2548,6 +2986,16 @@ function handleSubagentStop(p, ctx) {
|
|
|
2548
2986
|
}
|
|
2549
2987
|
// exec/verify/plan only: true = it returned without writing its dp-out file (the hook saved the .inline.md)
|
|
2550
2988
|
if (tag && RESULT_ROLES.includes(tag.role)) ev.inline_result = Boolean(inline);
|
|
2989
|
+
// core 0.7.0 (s2): seconds spent running tests in this segment (transcript; else the dp-out `TEST_S:` line)
|
|
2990
|
+
const tt = exists && sum ? testTimeOf(lines.slice(sum.segment_start)) : null;
|
|
2991
|
+
ev.test_s = tt ? tt.test_s : null;
|
|
2992
|
+
ev.test_runs = tt ? tt.test_runs : null;
|
|
2993
|
+
ev.full_suite_runs = tt ? tt.full_suite_runs : null;
|
|
2994
|
+
ev.test_s_source = ev.test_s !== null ? 'transcript' : null;
|
|
2995
|
+
if (ev.test_s === null && exists && tag) {
|
|
2996
|
+
const v = testSFromResultFile(lines, tag);
|
|
2997
|
+
if (v !== null) { ev.test_s = v; ev.test_s_source = 'dp-out'; }
|
|
2998
|
+
}
|
|
2551
2999
|
// Record the segment before appending so a racing duplicate fire sees it.
|
|
2552
3000
|
if (sum && sum.last_uuid) {
|
|
2553
3001
|
saveSegments(ctx, agentId, {
|
|
@@ -2630,6 +3078,134 @@ function completionStatus(text, abortWhy) {
|
|
|
2630
3078
|
return 'none';
|
|
2631
3079
|
}
|
|
2632
3080
|
|
|
3081
|
+
// ------------------------------------------------------------------ core 0.7.0 (s2): time spent running tests
|
|
3082
|
+
//
|
|
3083
|
+
// Measured at SubagentStop from the subagent transcript: every Bash/PowerShell tool_use whose command runs tests is
|
|
3084
|
+
// paired with its tool_result line; test_s = sum of (result timestamp - tool_use timestamp). This is the same span a
|
|
3085
|
+
// PreToolUse/PostToolUse(Bash) pair would give, without registering hooks on every Bash call of every session (the
|
|
3086
|
+
// dp-* agents only hook PostToolUse, which carries no start time). Backgrounded commands return at once, so they are
|
|
3087
|
+
// counted (test_runs) but not timed. Nothing timed -> fall back to the `TEST_S:` line of the agent's dp-out file.
|
|
3088
|
+
|
|
3089
|
+
const TEST_TOOL_NAMES = new Set(['Bash', 'PowerShell']);
|
|
3090
|
+
const PKG_RUNNERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
|
|
3091
|
+
const TEST_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|[\\/])tests?[\\/][^\s]*\.[cm]?[jt]sx?$/;
|
|
3092
|
+
|
|
3093
|
+
/** Positional args (not flags) of a token list. */
|
|
3094
|
+
function positionals(toks) {
|
|
3095
|
+
return toks.filter((t) => t && !t.startsWith('-'));
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/** One command segment (no && ; |) -> null (not a test run) or { full } (full = whole suite, no file/filter args). */
|
|
3099
|
+
function testSegmentInfo(seg) {
|
|
3100
|
+
let s = ` ${seg} `.replace(/\s\d?>>?&?\s*[^\s]+/g, ' ').trim();
|
|
3101
|
+
for (let k = 0; k < 6; k++) {
|
|
3102
|
+
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*)/, '');
|
|
3103
|
+
if (n === s) break;
|
|
3104
|
+
s = n;
|
|
3105
|
+
}
|
|
3106
|
+
const toks = s.split(/\s+/).filter(Boolean).map((t) => t.replace(/^["']|["']$/g, ''));
|
|
3107
|
+
if (!toks.length) return null;
|
|
3108
|
+
const head = toks[0].replace(/\.(?:cmd|exe)$/i, '').split(/[\\/]/).pop();
|
|
3109
|
+
if (PKG_RUNNERS.has(head)) {
|
|
3110
|
+
let i = 1;
|
|
3111
|
+
if (toks[i] === 'run' || toks[i] === 'run-script') i++;
|
|
3112
|
+
const script = toks[i] || '';
|
|
3113
|
+
if (script !== 'test' && script !== 't' && !/^test:/.test(script)) return null;
|
|
3114
|
+
// `npm test` alone = the whole suite; a `test:<x>` script or extra args (`npm test -- file`) = a subset
|
|
3115
|
+
return { full: (script === 'test' || script === 't') && positionals(toks.slice(i + 1)).length === 0 };
|
|
3116
|
+
}
|
|
3117
|
+
if (head === 'node') {
|
|
3118
|
+
if (toks.includes('--test')) return { full: !positionals(toks.slice(1)).some((t) => /\.[cm]?[jt]sx?$/.test(t)) };
|
|
3119
|
+
const file = positionals(toks.slice(1))[0] || '';
|
|
3120
|
+
return TEST_FILE_RE.test(file) ? { full: false } : null;
|
|
3121
|
+
}
|
|
3122
|
+
if (head === 'pytest' || head === 'py.test') return { full: positionals(toks.slice(1)).length === 0 };
|
|
3123
|
+
if (/^python[0-9.]*$|^py$/.test(head) && toks[1] === '-m' && toks[2] === 'pytest') return { full: positionals(toks.slice(3)).length === 0 };
|
|
3124
|
+
if (head === 'jest') return { full: positionals(toks.slice(1)).length === 0 };
|
|
3125
|
+
if (head === 'vitest') {
|
|
3126
|
+
const rest = toks.slice(1);
|
|
3127
|
+
if (rest[0] === 'run') rest.shift();
|
|
3128
|
+
return { full: positionals(rest).length === 0 };
|
|
3129
|
+
}
|
|
3130
|
+
if (head === 'go' && toks[1] === 'test') return { full: toks.slice(2).includes('./...') };
|
|
3131
|
+
if ((head === 'cargo' || head === 'dotnet') && toks[1] === 'test') return { full: positionals(toks.slice(2)).length === 0 };
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/** A shell command -> null (runs no tests) or { full } (true when any test segment runs a whole suite). */
|
|
3136
|
+
function testCommandInfo(cmd) {
|
|
3137
|
+
if (typeof cmd !== 'string' || !cmd.trim()) return null;
|
|
3138
|
+
let hit = null;
|
|
3139
|
+
for (const seg of cmd.split(/&&|\|\||;|\||\r?\n/)) {
|
|
3140
|
+
const t = seg.trim();
|
|
3141
|
+
if (!t) continue;
|
|
3142
|
+
const info = testSegmentInfo(t);
|
|
3143
|
+
if (info) hit = { full: Boolean((hit && hit.full) || info.full) };
|
|
3144
|
+
}
|
|
3145
|
+
return hit;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
/**
|
|
3149
|
+
* Test runs in transcript lines (one segment). Returns { test_s, test_runs, full_suite_runs }: test_s = seconds summed
|
|
3150
|
+
* over the timed (foreground, with a result) test commands, null when none was timed; counts include backgrounded runs.
|
|
3151
|
+
*/
|
|
3152
|
+
function testTimeOf(lines) {
|
|
3153
|
+
const open = new Map();
|
|
3154
|
+
let runs = 0;
|
|
3155
|
+
let full = 0;
|
|
3156
|
+
let timed = 0;
|
|
3157
|
+
let ms = 0;
|
|
3158
|
+
for (const l of lines || []) {
|
|
3159
|
+
if (!l || typeof l !== 'object' || !l.message || !Array.isArray(l.message.content)) continue;
|
|
3160
|
+
const t = typeof l.timestamp === 'string' ? Date.parse(l.timestamp) : NaN;
|
|
3161
|
+
if (l.type === 'assistant') {
|
|
3162
|
+
for (const b of l.message.content) {
|
|
3163
|
+
if (!b || b.type !== 'tool_use' || !TEST_TOOL_NAMES.has(b.name) || !b.input) continue;
|
|
3164
|
+
const info = testCommandInfo(b.input.command);
|
|
3165
|
+
if (!info) continue;
|
|
3166
|
+
runs++;
|
|
3167
|
+
if (info.full) full++;
|
|
3168
|
+
if (b.id && b.input.run_in_background !== true && !isNaN(t)) open.set(b.id, t);
|
|
3169
|
+
}
|
|
3170
|
+
} else if (l.type === 'user') {
|
|
3171
|
+
for (const b of l.message.content) {
|
|
3172
|
+
if (!b || b.type !== 'tool_result' || !open.has(b.tool_use_id)) continue;
|
|
3173
|
+
const t0 = open.get(b.tool_use_id);
|
|
3174
|
+
open.delete(b.tool_use_id);
|
|
3175
|
+
if (!isNaN(t) && t >= t0) { ms += t - t0; timed++; }
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
return { test_s: timed ? Math.round(ms / 100) / 10 : null, test_runs: runs, full_suite_runs: full };
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
/** Seconds from a `TEST_S: <n>[s|m|phút]` line of a result file's text (first 200 lines), or null. */
|
|
3183
|
+
function testSFromText(text) {
|
|
3184
|
+
const head = String(text || '').split(/\r?\n/).slice(0, 200);
|
|
3185
|
+
for (const raw of head) {
|
|
3186
|
+
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);
|
|
3187
|
+
if (!m) continue;
|
|
3188
|
+
const n = Number(m[1].replace(',', '.'));
|
|
3189
|
+
if (!isFinite(n) || n < 0) return null;
|
|
3190
|
+
const unit = (m[2] || 's').toLowerCase();
|
|
3191
|
+
return Math.round((unit === 's' || unit === 'giây' ? n : n * 60) * 10) / 10;
|
|
3192
|
+
}
|
|
3193
|
+
return null;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
/** 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. */
|
|
3197
|
+
function testSFromResultFile(lines, tag) {
|
|
3198
|
+
try {
|
|
3199
|
+
if (!tag || !RESULT_ROLES.includes(tag.role) || !tag.task_id) return null;
|
|
3200
|
+
const dir = resultRunDir(firstPromptText(lines), tag.task_id);
|
|
3201
|
+
for (const f of findResultFiles(dir, tag.task_id, tag.role, tag.try)) {
|
|
3202
|
+
const v = testSFromText(readTextFile(f));
|
|
3203
|
+
if (v !== null) return v;
|
|
3204
|
+
}
|
|
3205
|
+
} catch (_) { /* best effort */ }
|
|
3206
|
+
return null;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
2633
3209
|
// ------------------------------------------------------------------ core 0.5.0: context watchdog + gen-2 gate
|
|
2634
3210
|
//
|
|
2635
3211
|
// Both run only from the hooks of the dp-* agent files (~/.claude/agents, installed by `dieu-phoi setup`), so a session
|
|
@@ -2902,6 +3478,36 @@ function gateReserve(dir, child) {
|
|
|
2902
3478
|
return 0;
|
|
2903
3479
|
}
|
|
2904
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
|
+
|
|
2905
3511
|
/** Mark the reservation of `child` as a real spawn. Returns true when its slot was found. */
|
|
2906
3512
|
function gateConfirm(ctx, key, child) {
|
|
2907
3513
|
if (!key || !child) return false;
|
|
@@ -3058,6 +3664,7 @@ function runHook(opts) {
|
|
|
3058
3664
|
if (o.mode === 'watch') return handleWatch(p, ctx, o); // { stdout } or null
|
|
3059
3665
|
if (o.mode === 'gate') return handleGate(p, ctx, o); // { stdout } or null
|
|
3060
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
|
|
3061
3668
|
if (name === 'SessionStart') return handleSessionStart(p, ctx); // { stdout } or null, never an event
|
|
3062
3669
|
if (name === 'UserPromptSubmit') return handleUserPromptSubmit(p, ctx); // { stdout } or null
|
|
3063
3670
|
if (name === 'SubagentStop') ev = handleSubagentStop(p, ctx);
|
|
@@ -3175,7 +3782,7 @@ async function runSync(ctx, opts) {
|
|
|
3175
3782
|
for (const l of batch) {
|
|
3176
3783
|
try {
|
|
3177
3784
|
const r = JSON.parse(l.text);
|
|
3178
|
-
if (r && typeof r === 'object' && !Array.isArray(r)) {
|
|
3785
|
+
if (r && typeof r === 'object' && !Array.isArray(r) && !LOCAL_ONLY_KINDS.has(r.kind)) {
|
|
3179
3786
|
delete r.synced;
|
|
3180
3787
|
records.push(r);
|
|
3181
3788
|
}
|
|
@@ -3212,16 +3819,19 @@ async function runSync(ctx, opts) {
|
|
|
3212
3819
|
|
|
3213
3820
|
module.exports = {
|
|
3214
3821
|
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,
|
|
3215
|
-
|
|
3822
|
+
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS, MODE_ASK_LINE, isMode, rootModeFromEvents, rootModeFromText, dispatchModeFromEvents, taskModeInfo, planOfTask, modeFields,
|
|
3823
|
+
modeSuggestion, modeSuggestLine, fastFailRate, rootTaskIds, FAST_FAIL_WINDOW, FAST_FAIL_MIN_N, FAST_FAIL_RATE,
|
|
3824
|
+
testCommandInfo, testTimeOf, testSFromText,
|
|
3825
|
+
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED, TYPE_ALIASES,
|
|
3216
3826
|
sha8, nowIso, readJsonFile, writeFileAtomic, renameWithRetry, acquireLock, acquireFileLock, withFileLock, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
3217
3827
|
logError, computeUserHash, userHash, commonFields, parseTag, resolveTag, knownTaskTypes, tagIssueMessage, DEFAULT_EXTRA_TYPES,
|
|
3218
|
-
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,
|
|
3219
3829
|
priceFor, usageBuckets, requestCost, parseJsonl, readTranscript, lastUuidOf,
|
|
3220
3830
|
firstPromptText, tagFromTranscript, summarizeTranscript,
|
|
3221
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,
|
|
3222
3832
|
loadSyncOffset, saveSyncOffset, recordRejected, unsyncedInfo, loadSegments, saveSegments, segmentFile,
|
|
3223
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,
|
|
3224
|
-
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,
|
|
3225
3835
|
projectsDir, findGit, originUrlFromConfig, normalizeRemoteUrl, resolveProject, readHead, repoFromBrief, projectFields, nativePath, isStoreDirName, aliasesFile, loadAliases, keyDirName,
|
|
3226
3836
|
extractAgentId, hasAssistantUsage, hasDispatch, buildDispatch, handlePostToolUse, handleSubagentStop, maybeSpawnSync, runHook, runSync,
|
|
3227
3837
|
};
|