@kienntpixon/dieu-phoi 0.7.3 → 0.7.4
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 +15 -0
- package/agents/dp-exec.md +6 -1
- package/agents/dp-helper.md +6 -0
- package/agents/dp-scout.md +7 -1
- package/agents/dp-verify.md +6 -0
- package/bin/dieu-phoi.cjs +8 -8
- package/hook.cjs +196 -129
- package/lib/aggregate.cjs +20 -2
- package/lib/choose.cjs +27 -22
- package/lib/commands.cjs +195 -34
- package/lib/knowledge.cjs +47 -4
- package/lib/prune.cjs +81 -7
- package/package.json +1 -1
- package/profile/lessons.md +0 -1
package/hook.cjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* This file is copied verbatim to ~/.claude/dieu-phoi/bin/hook.cjs by `dieu-phoi setup`
|
|
7
7
|
* and run by Claude Code on PostToolUse(Agent), SubagentStop, SessionStart(compact) and UserPromptSubmit; the dp-* agent
|
|
8
8
|
* files (`dieu-phoi setup` -> ~/.claude/agents) also run it inside those subagents as `--watch` (PostToolUse, context
|
|
9
|
-
* handoff watchdog), `--gate` (PreToolUse(Agent), gen-2 helper rules)
|
|
9
|
+
* handoff watchdog), `--gate` (PreToolUse(Agent), gen-2 helper rules), `--gate-bash` (PreToolUse(Bash), no
|
|
10
|
+
* `knowledge review|merge`) and `--gate-release`. It must stay
|
|
10
11
|
* self-contained: Node built-ins only, no requires of other package files.
|
|
11
12
|
*
|
|
12
13
|
* Hook rules: stdout only for the JSON of SessionStart(compact) / UserPromptSubmit / a PostToolUse(Agent) warning
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
* node hook.cjs --watch -> dp subagent PostToolUse: context watchdog (additionalContext JSON or nothing)
|
|
20
21
|
* node hook.cjs --gate -> dp-exec PreToolUse(Agent): allow / deny a gen-2 helper spawn
|
|
21
22
|
* node hook.cjs --gate-release -> dp-exec PostToolUseFailure(Agent): give a failed spawn's helper slot back
|
|
23
|
+
* node hook.cjs --gate-bash -> dp-* PreToolUse(Bash|PowerShell): deny `knowledge review|merge` in a subagent
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
const fs = require('fs');
|
|
@@ -26,7 +28,7 @@ const path = require('path');
|
|
|
26
28
|
const os = require('os');
|
|
27
29
|
const crypto = require('crypto');
|
|
28
30
|
|
|
29
|
-
const CORE_VERSION = '0.7.
|
|
31
|
+
const CORE_VERSION = '0.7.4';
|
|
30
32
|
const SCHEMA = 1;
|
|
31
33
|
const TASK_TYPES = [
|
|
32
34
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -1172,8 +1174,8 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1172
1174
|
if (!seenTask.has(id) && !discarded.has(id)) { seenTask.add(id); taskIds.push(id); }
|
|
1173
1175
|
}
|
|
1174
1176
|
const modeEvents = new Map();
|
|
1175
|
-
for (const ev of events) if (ev && ev.kind === 'mode' &&
|
|
1176
|
-
const modeOfRoot = (root) => (modeEvents.has(root) ? modeEvents.get(root).mode : DEFAULT_MODE);
|
|
1177
|
+
for (const ev of events) if (ev && ev.kind === 'mode' && normMode(ev.mode) && typeof ev.root_task_id === 'string') modeEvents.set(ev.root_task_id, ev);
|
|
1178
|
+
const modeOfRoot = (root) => (modeEvents.has(root) ? normMode(modeEvents.get(root).mode) : DEFAULT_MODE);
|
|
1177
1179
|
const per = new Map(taskIds.map((id) => [id, { dispatches: [], user: [], steers: [], overrides: [], agentIds: new Set() }]));
|
|
1178
1180
|
events.forEach((ev, i) => {
|
|
1179
1181
|
if (!ev || typeof ev !== 'object') return;
|
|
@@ -1225,7 +1227,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1225
1227
|
const pApproved = pov && evMs(pov) > (Date.parse(ps.plan.approved_at) || 0) ? pov.chosen_tier || null : null;
|
|
1226
1228
|
tasks.push(Object.assign({
|
|
1227
1229
|
task_id: id, task_type: ps.step.type || null, gate: 2, pending: true, verify_only: false, round: 0,
|
|
1228
|
-
mode:
|
|
1230
|
+
mode: normMode(ps.step.mode) || modeOfRoot(ps.plan.root_task_id),
|
|
1229
1231
|
}, planInfo, {
|
|
1230
1232
|
approved_tier: pApproved || ps.step.tier || null, last_tier: null, attempts: [], user_verdict: null, last_steer: null,
|
|
1231
1233
|
run_dir: planDir, last_ts: null,
|
|
@@ -1283,7 +1285,7 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1283
1285
|
pending: false,
|
|
1284
1286
|
verify_only: attempts.length > 0 && attempts.every((a) => a.role === 'verify'),
|
|
1285
1287
|
round,
|
|
1286
|
-
mode: dispatchModeFromEvents(t.dispatches, id) || (ps &&
|
|
1288
|
+
mode: dispatchModeFromEvents(t.dispatches, id) || (ps && normMode(ps.step.mode)) || modeOfRoot(plan ? plan.root_task_id : id),
|
|
1287
1289
|
}, planInfo, {
|
|
1288
1290
|
approved_tier: approved,
|
|
1289
1291
|
last_tier: lastExecAny ? tierOf(lastExecAny.model, lastExecAny.effort) : null,
|
|
@@ -1301,12 +1303,9 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1301
1303
|
for (const [root, ev] of modeEvents) if (ev.session_id === sessionId || ev.session === sidHash) modeRoots.add(root);
|
|
1302
1304
|
const modes = Array.from(modeRoots).map((root) => {
|
|
1303
1305
|
const ev = modeEvents.get(root);
|
|
1304
|
-
const mode = ev ? ev.mode : DEFAULT_MODE;
|
|
1305
|
-
// core 0.7.0 (s2): suggestion only — the mode itself never changes here
|
|
1306
|
-
const sug = modeSuggestion(events, root, { mode, taskIds: rootTaskIds(allPlans, root) });
|
|
1306
|
+
const mode = ev ? normMode(ev.mode) : DEFAULT_MODE;
|
|
1307
1307
|
return {
|
|
1308
1308
|
root_task_id: root, mode, since: ev ? ev.ts : null, source: ev ? ev.source || null : 'default', reason_code: ev ? ev.reason_code || null : null,
|
|
1309
|
-
mode_suggest: sug.mode_suggest, mode_suggest_reason: sug.mode_suggest_reason,
|
|
1310
1309
|
};
|
|
1311
1310
|
});
|
|
1312
1311
|
const orphans = findOrphanAgents(events, taskIds.filter((id) => !discarded.has(id)), { nowMs: o.nowMs });
|
|
@@ -1470,14 +1469,7 @@ function compactSystemMessage(state) {
|
|
|
1470
1469
|
function modesLine(state) {
|
|
1471
1470
|
const ms = Array.isArray(state.modes) ? state.modes : [];
|
|
1472
1471
|
if (!ms.length) return '';
|
|
1473
|
-
|
|
1474
|
-
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}`;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
/** One line telling the orchestrator to ASK the user about returning to normal (never switch by itself). */
|
|
1478
|
-
function modeSuggestLine(root, mode, reason) {
|
|
1479
|
-
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}); `
|
|
1480
|
-
+ `chỉ đổi khi người dùng đồng ý: \`dieu-phoi mode --root ${root || '<root>'} normal\`. KHÔNG tự đổi mode.`;
|
|
1472
|
+
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(', ')}.`;
|
|
1481
1473
|
}
|
|
1482
1474
|
|
|
1483
1475
|
function formatSessionState(state, cap) {
|
|
@@ -1695,9 +1687,10 @@ function orchLessonLines(info) {
|
|
|
1695
1687
|
return ls.length ? ['bài học cho điều phối viên (profile/lessons.md, [orch]):'].concat(ls.map((l) => ` - ${l}`)) : [];
|
|
1696
1688
|
}
|
|
1697
1689
|
|
|
1698
|
-
/** Gate-1 mode line:
|
|
1690
|
+
/** Gate-1 mode line: only when a mode was chosen (`start --mode fast`); '' otherwise (no mode question, default normal). */
|
|
1699
1691
|
function modeLine(info) {
|
|
1700
|
-
|
|
1692
|
+
const m = info && normMode(info.mode);
|
|
1693
|
+
return m ? `mode: ${m} (đã ghi cho root ${info.id}; đổi: dieu-phoi mode --root ${info.id} <mode>)` : '';
|
|
1701
1694
|
}
|
|
1702
1695
|
|
|
1703
1696
|
function formatStartBlock(info) {
|
|
@@ -1712,7 +1705,7 @@ function formatStartBlock(info) {
|
|
|
1712
1705
|
...orchLessonLines(info),
|
|
1713
1706
|
...knowledgeLines(info),
|
|
1714
1707
|
'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.',
|
|
1715
|
-
].join('\n');
|
|
1708
|
+
].filter((l) => l !== '').join('\n');
|
|
1716
1709
|
}
|
|
1717
1710
|
|
|
1718
1711
|
/** UserPromptSubmit: on a dieu-phoi prompt, pre-create the run and inject id/run dir/health/brief. */
|
|
@@ -1870,29 +1863,36 @@ function handoffChainMessage(taskId, n) {
|
|
|
1870
1863
|
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\`).`;
|
|
1871
1864
|
}
|
|
1872
1865
|
|
|
1873
|
-
// ------------------------------------------------------------------ core 0.7.0: mode per request (fast | normal
|
|
1866
|
+
// ------------------------------------------------------------------ core 0.7.0: mode per request (fast | normal; 0.7.4: strict removed)
|
|
1874
1867
|
//
|
|
1875
1868
|
// A request (root_task_id) has a mode; the user picks it at gate 1 (`start --mode`, `plan-table` file, or
|
|
1876
1869
|
// `dieu-phoi mode --root R <mode>` at any time). Every change is one local-only event kind=mode. A step not yet
|
|
1877
1870
|
// dispatched follows the root's current mode (or its own `mode` override in the approved plan); a step already
|
|
1878
1871
|
// dispatched keeps the mode its first exec dispatch recorded. Nothing here ever changes a mode by itself.
|
|
1879
1872
|
|
|
1880
|
-
const MODES = ['fast', 'normal'
|
|
1873
|
+
const MODES = ['fast', 'normal'];
|
|
1881
1874
|
const DEFAULT_MODE = 'normal';
|
|
1875
|
+
/** Removed in 0.7.4: old events/plans/configs may still carry it; every reader treats it as normal. */
|
|
1876
|
+
const LEGACY_MODE = 'strict';
|
|
1882
1877
|
/** Event kinds that stay on this machine: runSync does not send them (an older sink would answer bad_kind). */
|
|
1883
1878
|
const LOCAL_ONLY_KINDS = new Set(['mode', 'knowledge_merge', 'leftover', 'worktree', 'reap', 'reap_orphans']);
|
|
1884
|
-
|
|
1885
|
-
+ 'đặt bằng `dieu-phoi mode --root <root_task_id> <mode>`';
|
|
1886
|
-
|
|
1879
|
+
/** A mode the user may choose now (fast | normal). */
|
|
1887
1880
|
function isMode(m) {
|
|
1888
1881
|
return typeof m === 'string' && MODES.includes(m);
|
|
1889
1882
|
}
|
|
1890
1883
|
|
|
1884
|
+
/** Mode as stored/read: fast | normal; legacy 'strict' reads as normal; anything else null. */
|
|
1885
|
+
function normMode(m) {
|
|
1886
|
+
if (typeof m !== 'string') return null;
|
|
1887
|
+
if (m === LEGACY_MODE) return DEFAULT_MODE;
|
|
1888
|
+
return MODES.includes(m) ? m : null;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
1891
|
/** Last mode event of `root` in an events array (null when none). */
|
|
1892
1892
|
function rootModeFromEvents(events, root) {
|
|
1893
1893
|
let last = null;
|
|
1894
1894
|
for (const ev of events) {
|
|
1895
|
-
if (ev && ev.kind === 'mode' && ev.root_task_id === root &&
|
|
1895
|
+
if (ev && ev.kind === 'mode' && ev.root_task_id === root && normMode(ev.mode)) last = ev.mode === normMode(ev.mode) ? ev : Object.assign({}, ev, { mode: normMode(ev.mode) });
|
|
1896
1896
|
}
|
|
1897
1897
|
return last;
|
|
1898
1898
|
}
|
|
@@ -1910,7 +1910,7 @@ function rootModeFromText(txt, root) {
|
|
|
1910
1910
|
if (line.includes(rootNeedle)) {
|
|
1911
1911
|
try {
|
|
1912
1912
|
const ev = JSON.parse(line);
|
|
1913
|
-
if (ev && ev.kind === 'mode' &&
|
|
1913
|
+
if (ev && ev.kind === 'mode' && normMode(ev.mode)) last = ev.mode === normMode(ev.mode) ? ev : Object.assign({}, ev, { mode: normMode(ev.mode) });
|
|
1914
1914
|
} catch (_) { /* corrupt line */ }
|
|
1915
1915
|
}
|
|
1916
1916
|
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
@@ -1921,7 +1921,7 @@ function rootModeFromText(txt, root) {
|
|
|
1921
1921
|
/** Mode the first exec dispatch of `taskId` recorded (a dispatched step keeps its mode), from an events array. */
|
|
1922
1922
|
function dispatchModeFromEvents(events, taskId) {
|
|
1923
1923
|
for (const ev of events) {
|
|
1924
|
-
if (ev && ev.kind === 'dispatch' && ev.task_id === taskId && (ev.role || 'exec') === 'exec' &&
|
|
1924
|
+
if (ev && ev.kind === 'dispatch' && ev.task_id === taskId && (ev.role || 'exec') === 'exec' && normMode(ev.mode)) return normMode(ev.mode);
|
|
1925
1925
|
}
|
|
1926
1926
|
return null;
|
|
1927
1927
|
}
|
|
@@ -1935,7 +1935,7 @@ function dispatchModeFromText(txt, taskId) {
|
|
|
1935
1935
|
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1936
1936
|
if (line.includes('"kind":"dispatch"') && !line.includes('"role":"verify"') && !line.includes('"role":"plan"') && !line.includes('"role":"helper"')) {
|
|
1937
1937
|
const m = /"mode":"(fast|normal|strict)"/.exec(line);
|
|
1938
|
-
if (m) return m[1];
|
|
1938
|
+
if (m) return normMode(m[1]);
|
|
1939
1939
|
}
|
|
1940
1940
|
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1941
1941
|
}
|
|
@@ -1966,7 +1966,7 @@ function taskModeInfo(ctx, taskId, events) {
|
|
|
1966
1966
|
const locked = events ? dispatchModeFromEvents(events, taskId) : dispatchModeFromText(txt, taskId);
|
|
1967
1967
|
if (locked) return { mode: locked, source: 'dispatched', root };
|
|
1968
1968
|
}
|
|
1969
|
-
if (pl && pl.step &&
|
|
1969
|
+
if (pl && pl.step && normMode(pl.step.mode)) return { mode: normMode(pl.step.mode), source: 'step', root };
|
|
1970
1970
|
if (root) {
|
|
1971
1971
|
const ev = events ? rootModeFromEvents(events, root) : rootModeFromText(txt, root);
|
|
1972
1972
|
if (ev) return { mode: ev.mode, source: 'root', root };
|
|
@@ -1974,90 +1974,6 @@ function taskModeInfo(ctx, taskId, events) {
|
|
|
1974
1974
|
return { mode: DEFAULT_MODE, source: 'default', root };
|
|
1975
1975
|
}
|
|
1976
1976
|
|
|
1977
|
-
// core 0.7.0 (s2): signals for SUGGESTING a return to normal. The CLI never switches the mode itself; only the
|
|
1978
|
-
// user's `dieu-phoi mode` does. A suggestion is computed only while the root is in fast.
|
|
1979
|
-
const FAST_FAIL_WINDOW = 10; // last N fast exec steps (all roots, file order of their first exec dispatch)
|
|
1980
|
-
const FAST_FAIL_MIN_N = 5; // fewer fast steps than this -> no rate signal yet
|
|
1981
|
-
const FAST_FAIL_RATE = 0.15; // >= this share failed -> suggest normal
|
|
1982
|
-
const BATCH_VERIFY_RE = /^batch-verify(?:-|$)/;
|
|
1983
|
-
const QUOTA_VERDICT_RE = /^(?:quota-pause|usage-limit|auto-quota-pause|auto-usage-limit|paused-for-quota)(?:-|$)/;
|
|
1984
|
-
|
|
1985
|
-
/** A user reject that counts as a quality signal (legacy `rework` too); scope rejects (request changed) do not. */
|
|
1986
|
-
function isQualityReject(ev) {
|
|
1987
|
-
if (ev.outcome === 'rework' && !ev.by) return true;
|
|
1988
|
-
return ev.by === 'user' && ev.outcome === 'reject' && ev.cause !== 'scope';
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
/** Agent-layer fail (fail / timeout, never a quota pause or aborted). */
|
|
1992
|
-
function isAgentFail(ev) {
|
|
1993
|
-
if (ev.by === 'user' || ev.by === 'auto') return false;
|
|
1994
|
-
if (ev.outcome === 'fail') return true;
|
|
1995
|
-
return ev.outcome === 'timeout' && !QUOTA_VERDICT_RE.test(String(ev.reason_code || ''));
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
/**
|
|
1999
|
-
* Fail rate over the last FAST_FAIL_WINDOW fast exec steps (any root): a step is a task whose first exec dispatch
|
|
2000
|
-
* recorded mode=fast; it failed when it has an agent-layer fail or a quality reject. Discarded tasks are left out.
|
|
2001
|
-
* Returns { n, failed, rate } (rate null when n = 0).
|
|
2002
|
-
*/
|
|
2003
|
-
function fastFailRate(events) {
|
|
2004
|
-
const gone = new Set();
|
|
2005
|
-
for (const ev of events) if (ev && ev.kind === 'discard' && typeof ev.task_id === 'string') gone.add(ev.task_id);
|
|
2006
|
-
const order = [];
|
|
2007
|
-
const seen = new Set();
|
|
2008
|
-
for (const ev of events) {
|
|
2009
|
-
if (!ev || ev.kind !== 'dispatch' || typeof ev.task_id !== 'string' || !ev.task_id) continue;
|
|
2010
|
-
if ((ev.role || 'exec') !== 'exec' || seen.has(ev.task_id)) continue;
|
|
2011
|
-
seen.add(ev.task_id);
|
|
2012
|
-
if (ev.mode === 'fast' && !gone.has(ev.task_id)) order.push(ev.task_id);
|
|
2013
|
-
}
|
|
2014
|
-
const win = new Set(order.slice(-FAST_FAIL_WINDOW));
|
|
2015
|
-
const failed = new Set();
|
|
2016
|
-
for (const ev of events) {
|
|
2017
|
-
if (ev && ev.kind === 'verdict' && win.has(ev.task_id) && (isAgentFail(ev) || isQualityReject(ev))) failed.add(ev.task_id);
|
|
2018
|
-
}
|
|
2019
|
-
return { n: win.size, failed: failed.size, rate: win.size ? failed.size / win.size : null };
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
/**
|
|
2023
|
-
* Should the orchestrator SUGGEST (never apply) a return to normal for `root`? Only while `mode` (the root's current
|
|
2024
|
-
* mode) is fast. Signals, each with a short Vietnamese reason:
|
|
2025
|
-
* - batch-verify: an agent verdict fail with reason batch-verify on a task of the root, after the root's last mode event;
|
|
2026
|
-
* - reject: a quality reject (user) on a task of the root, after that mode event;
|
|
2027
|
-
* - fail-rate: >= 15% failed among the last 10 fast steps (all roots; needs >= 5 steps).
|
|
2028
|
-
* `taskIds` = the root and its plan's step task ids. Returns { mode_suggest: 'normal'|null, mode_suggest_reason,
|
|
2029
|
-
* mode_suggest_signals[] }.
|
|
2030
|
-
*/
|
|
2031
|
-
function modeSuggestion(events, root, o) {
|
|
2032
|
-
const opts = o || {};
|
|
2033
|
-
const none = { mode_suggest: null, mode_suggest_reason: null, mode_suggest_signals: [] };
|
|
2034
|
-
if (opts.mode !== 'fast') return none;
|
|
2035
|
-
const ids = new Set(opts.taskIds || []);
|
|
2036
|
-
if (root) ids.add(root);
|
|
2037
|
-
const last = root ? rootModeFromEvents(events, root) : null;
|
|
2038
|
-
const since = last ? (Date.parse(last.ts) || 0) : 0;
|
|
2039
|
-
const signals = [];
|
|
2040
|
-
const reasons = [];
|
|
2041
|
-
let batch = null;
|
|
2042
|
-
let reject = null;
|
|
2043
|
-
for (const ev of events) {
|
|
2044
|
-
if (!ev || ev.kind !== 'verdict' || !ids.has(ev.task_id)) continue;
|
|
2045
|
-
const t = Date.parse(ev.ts) || 0;
|
|
2046
|
-
if (t < since) continue;
|
|
2047
|
-
if (!batch && ev.outcome === 'fail' && BATCH_VERIFY_RE.test(String(ev.reason_code || ''))) batch = ev.task_id;
|
|
2048
|
-
if (!reject && isQualityReject(ev)) reject = ev.task_id;
|
|
2049
|
-
}
|
|
2050
|
-
if (batch) { signals.push('batch-verify'); reasons.push(`verifier gộp báo fail (task ${batch})`); }
|
|
2051
|
-
if (reject) { signals.push('reject'); reasons.push(`người dùng reject (task ${reject})`); }
|
|
2052
|
-
const fr = fastFailRate(events);
|
|
2053
|
-
if (fr.n >= FAST_FAIL_MIN_N && fr.rate >= FAST_FAIL_RATE) {
|
|
2054
|
-
signals.push('fail-rate');
|
|
2055
|
-
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)}%)`);
|
|
2056
|
-
}
|
|
2057
|
-
if (!signals.length) return none;
|
|
2058
|
-
return { mode_suggest: 'normal', mode_suggest_reason: reasons.join('; '), mode_suggest_signals: signals };
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
1977
|
/** Task ids of a root: the root itself plus the step task ids of its approved plan (if any). */
|
|
2062
1978
|
function rootTaskIds(plans, root) {
|
|
2063
1979
|
const out = new Set(root ? [root] : []);
|
|
@@ -2330,8 +2246,10 @@ function readHead(start) {
|
|
|
2330
2246
|
|
|
2331
2247
|
// ------------------------------------------------------------------ core 0.6.0, s3: knowledge freshness
|
|
2332
2248
|
//
|
|
2333
|
-
// A knowledge section is stale when a file
|
|
2334
|
-
// (`sha`) and the HEAD of the worktree asking.
|
|
2249
|
+
// A knowledge section is stale when a file named by one of its plain (non-glob) `paths` entries differs between the
|
|
2250
|
+
// commit it was merged at (`sha`) and the HEAD of the worktree asking. A file that only matches a glob entry (`**`,
|
|
2251
|
+
// `*`, `?`) gives state `drift`: shown lightly, never counted as stale (rule 2, 0.7.4 s5; cache entries carry `rule`).
|
|
2252
|
+
// Used by the start block here and by `knowledge get/list/status`
|
|
2335
2253
|
// (lib/knowledge.cjs). Lives in hook.cjs because the installed hook is a single file (builtins only). Rules, from
|
|
2336
2254
|
// measurements on the pixon-fast-forward repos (contract r1-2 §2):
|
|
2337
2255
|
// - ONE git process per check, whatever the number of distinct section shas: `git diff-tree --stdin` reads one
|
|
@@ -2350,8 +2268,69 @@ const FRESH_CACHE_MAX = 12; // HEADs remembered per store (worktrees of one repo
|
|
|
2350
2268
|
const DIFF_TIMEOUT_MS = 10000;
|
|
2351
2269
|
const HOOK_DIFF_TIMEOUT_MS = 1500;
|
|
2352
2270
|
const DIRTY_TIMEOUT_MS = 2000;
|
|
2271
|
+
// Tests only: DIEU_PHOI_GIT_STATUS_TIMEOUT_MS overrides the `git status` bound of --dirty (a busy Windows box overruns 2 s);
|
|
2272
|
+
// invalid or non-positive values are ignored, the product default stays DIRTY_TIMEOUT_MS.
|
|
2273
|
+
function dirtyTimeoutMs() {
|
|
2274
|
+
const v = Number(process.env.DIEU_PHOI_GIT_STATUS_TIMEOUT_MS);
|
|
2275
|
+
return Number.isFinite(v) && v > 0 ? v : DIRTY_TIMEOUT_MS;
|
|
2276
|
+
}
|
|
2353
2277
|
const PATHSPEC_MAX_CHARS = 6000; // beyond this: no pathspec (Windows command line limit), filter in JS only
|
|
2354
2278
|
const STALE_FILES_KEEP = 50; // stale files kept per section (the count stays exact)
|
|
2279
|
+
const FRESH_RULE = 4; // 4 (0.7.4 s9): rule 3 + identifiers compared as nameKey (kebab/camel match), >= 3 chars; cache of rule 3 dropped
|
|
2280
|
+
const GLOB_CHARS_RE = /[*?]/;
|
|
2281
|
+
const LARGE_FILE_BYTES = 30000; // above this a plain path counts only when a changed line names one of the entry's identifiers
|
|
2282
|
+
// A changed line that is only a version bump (package.json, SKILL.md frontmatter, a `const CORE_VERSION = '…'` in code) or blank: routine, never stale.
|
|
2283
|
+
const ROUTINE_LINE_RES = [/^\s*$/, /^\s*"version"\s*:\s*"[^"]*",?\s*$/, /^\s*version\s*:\s*[^\s#]+\s*$/,
|
|
2284
|
+
/^\s*(?:const|let|var)\s+\w*VERSION\w*\s*=\s*(['"])[^'"]*\1;?\s*$/];
|
|
2285
|
+
// Words too common in code to tell an entry's identifiers from ordinary lines.
|
|
2286
|
+
const NAME_STOPWORDS = new Set(['const', 'return', 'function', 'string', 'number', 'object', 'boolean', 'undefined',
|
|
2287
|
+
'files', 'file', 'json', 'path', 'paths', 'value', 'values', 'state', 'null', 'true', 'false', 'test', 'tests', 'node', 'cli']);
|
|
2288
|
+
|
|
2289
|
+
function plainPathsOf(s) { return (s.paths || []).filter((g) => !GLOB_CHARS_RE.test(g)); }
|
|
2290
|
+
function isChangelogPath(f) { return /(^|\/)CHANGELOG[^/]*$/i.test(f); }
|
|
2291
|
+
function entryAboutChangelog(s) { return [s.id, s.title, ...(s.tags || [])].some((x) => /changelog/i.test(String(x || ''))); }
|
|
2292
|
+
function isRoutineLine(t) { return ROUTINE_LINE_RES.some((re) => re.test(t)); }
|
|
2293
|
+
function isLargeFile(root, f) {
|
|
2294
|
+
try { return fs.statSync(path.join(root, f)).size > LARGE_FILE_BYTES; } catch (_) { return false; }
|
|
2295
|
+
}
|
|
2296
|
+
function readSectionBody(dir, id) {
|
|
2297
|
+
try { return fs.readFileSync(path.join(dir, 'sections', `${id}.md`), 'utf8'); } catch (_) { return ''; }
|
|
2298
|
+
}
|
|
2299
|
+
/** Comparable form of an identifier: lower-case letters and digits only (`plan-table` ~ `cmdPlanTable` ~ `plantable`). */
|
|
2300
|
+
function nameKey(t) { return String(t || '').toLowerCase().replace(/[^a-z0-9]/g, ''); }
|
|
2301
|
+
/** Identifiers an entry names: the pieces of its backtick spans and the words of its id (nameKey form, >= 3 chars). */
|
|
2302
|
+
function namesOf(s, body) {
|
|
2303
|
+
const out = new Set();
|
|
2304
|
+
for (const m of String(body || '').matchAll(/`([^`\n]{2,200})`/g)) {
|
|
2305
|
+
for (const t of m[1].split(/[\s(),;:=<>[\]{}'"|]+/)) {
|
|
2306
|
+
const low = nameKey(t);
|
|
2307
|
+
if (low.length >= 3 && /[a-z]/.test(low) && !NAME_STOPWORDS.has(low)) out.add(low);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
for (const p of String(s.id || '').split('-')) if (p.length >= 5) out.add(nameKey(p));
|
|
2311
|
+
return Array.from(out);
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Changed lines (added and removed text, no diff markers) of `files` between sha and head, one git diff with -U0.
|
|
2315
|
+
* Returns { byFile: Map<file, string[]> } or null when git fails.
|
|
2316
|
+
*/
|
|
2317
|
+
function changedLinesOf(root, sha, head, files, timeoutMs) {
|
|
2318
|
+
const r = runGit(root, ['--literal-pathspecs', 'diff', '-U0', '--no-renames', '--no-color', '--no-ext-diff', sha, head, '--', ...files], undefined, timeoutMs);
|
|
2319
|
+
if (!r.ok) return null;
|
|
2320
|
+
const byFile = new Map();
|
|
2321
|
+
let cur = null;
|
|
2322
|
+
let inHunk = false;
|
|
2323
|
+
for (const raw of r.stdout.split('\n')) {
|
|
2324
|
+
const line = raw.replace(/\r$/, '');
|
|
2325
|
+
if (line.startsWith('diff --git ')) {
|
|
2326
|
+
const m = / b\/(.*)$/.exec(line);
|
|
2327
|
+
cur = m ? m[1] : null; inHunk = false;
|
|
2328
|
+
if (cur && !byFile.has(cur)) byFile.set(cur, []);
|
|
2329
|
+
} else if (line.startsWith('@@')) inHunk = true;
|
|
2330
|
+
else if (inHunk && cur && (line.startsWith('+') || line.startsWith('-'))) byFile.get(cur).push(line.slice(1));
|
|
2331
|
+
}
|
|
2332
|
+
return { byFile };
|
|
2333
|
+
}
|
|
2355
2334
|
const FULL_SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
2356
2335
|
|
|
2357
2336
|
/**
|
|
@@ -2444,7 +2423,7 @@ function diffShas(root, shas, head, globs, timeoutMs) {
|
|
|
2444
2423
|
function dirtyFiles(root, globs, timeoutMs) {
|
|
2445
2424
|
const spec = pathspecOf(globs);
|
|
2446
2425
|
const base = ['status', '--porcelain=v1', '-z', '-uno', '--no-renames'];
|
|
2447
|
-
const r = runGit(root, spec ? base.concat('--', spec) : base, undefined, timeoutMs ||
|
|
2426
|
+
const r = runGit(root, spec ? base.concat('--', spec) : base, undefined, timeoutMs || dirtyTimeoutMs());
|
|
2448
2427
|
if (!r.ok) return { error: r.error, timedOut: !!r.timedOut, ms: r.ms };
|
|
2449
2428
|
const files = r.stdout.split('\0').filter((x) => x.length > 3).map((x) => x.slice(3));
|
|
2450
2429
|
return { files, ms: r.ms };
|
|
@@ -2470,7 +2449,7 @@ function readFreshCache(dir) {
|
|
|
2470
2449
|
|
|
2471
2450
|
function cachedFor(dir, head, stamp) {
|
|
2472
2451
|
if (!head || !stamp) return null;
|
|
2473
|
-
return readFreshCache(dir).entries.find((e) => e.head === head && e.index_mtime === stamp.mtime && e.index_size === stamp.size) || null;
|
|
2452
|
+
return readFreshCache(dir).entries.find((e) => e.rule === FRESH_RULE && e.head === head && e.index_mtime === stamp.mtime && e.index_size === stamp.size) || null;
|
|
2474
2453
|
}
|
|
2475
2454
|
|
|
2476
2455
|
/** Best effort (tmp+rename, last writer wins); a store that vanished or is read-only only loses the cache. */
|
|
@@ -2482,7 +2461,7 @@ function writeFreshCache(dir, entry) {
|
|
|
2482
2461
|
}
|
|
2483
2462
|
|
|
2484
2463
|
function freshCounts(sections) {
|
|
2485
|
-
const c = { total: 0, fresh: 0, stale: 0, unknown: 0 };
|
|
2464
|
+
const c = { total: 0, fresh: 0, stale: 0, drift: 0, unknown: 0 };
|
|
2486
2465
|
for (const s of Object.values(sections)) { c.total++; c[s.state] = (c[s.state] || 0) + 1; }
|
|
2487
2466
|
return c;
|
|
2488
2467
|
}
|
|
@@ -2509,7 +2488,8 @@ function computeFreshness(o) {
|
|
|
2509
2488
|
if (!head) why = o.head && o.head.unsupported ? `HEAD không đọc được từ file (${o.head.unsupported})` : 'không đọc được HEAD (repo chưa có commit?)';
|
|
2510
2489
|
else if (!o.root) why = 'repo không có thư mục làm việc (bare)';
|
|
2511
2490
|
for (const s of idx.sections) {
|
|
2512
|
-
|
|
2491
|
+
// review mark (s6): an entry reviewed at reviewed_sha (full sha) is judged from that sha on; a merge that replaces the entry drops the mark
|
|
2492
|
+
const sha = String(FULL_SHA_RE.test(String(s.reviewed_sha || '')) ? s.reviewed_sha : (s.sha || '')).toLowerCase();
|
|
2513
2493
|
if (why) sections[s.id] = { state: 'unknown', why };
|
|
2514
2494
|
else if (!sha || sha === 'none') sections[s.id] = { state: 'unknown', why: 'mục chưa có sha' };
|
|
2515
2495
|
else if (sha === head) sections[s.id] = { state: 'fresh' };
|
|
@@ -2523,19 +2503,65 @@ function computeFreshness(o) {
|
|
|
2523
2503
|
const d = diffShas(o.root, Array.from(pending.keys()), head, globs, o.timeoutMs || DIFF_TIMEOUT_MS);
|
|
2524
2504
|
out.ms_git = d.ms || 0;
|
|
2525
2505
|
if (d.error) error = d.error;
|
|
2506
|
+
// rule 3 (0.7.4 s5b): read the changed lines of the plain-path files that could make an entry stale (one git
|
|
2507
|
+
// diff per distinct sha, only on a cache miss); CHANGELOG files an entry is not about are never read
|
|
2508
|
+
const timeout = o.timeoutMs || DIFF_TIMEOUT_MS;
|
|
2509
|
+
const want = new Map(); // sha -> Set(file)
|
|
2510
|
+
for (const [sha, list] of pending) {
|
|
2511
|
+
if (d.error || d.missing.has(sha)) continue;
|
|
2512
|
+
for (const s of list) {
|
|
2513
|
+
const plain = plainPathsOf(s);
|
|
2514
|
+
for (const f of d.bySha.get(sha) || []) {
|
|
2515
|
+
if (!plain.some((g) => globMatch(g, f))) continue;
|
|
2516
|
+
if (isChangelogPath(f) && !entryAboutChangelog(s)) continue;
|
|
2517
|
+
if (!want.has(sha)) want.set(sha, new Set());
|
|
2518
|
+
want.get(sha).add(f);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const lines = new Map(); // `${sha}\0${file}` -> changed texts, or null when git could not say
|
|
2523
|
+
for (const [sha, set] of want) {
|
|
2524
|
+
const r = changedLinesOf(o.root, sha, head, Array.from(set), timeout);
|
|
2525
|
+
for (const f of set) lines.set(`${sha}\0${f}`, r ? (r.byFile.get(f) || []) : null);
|
|
2526
|
+
}
|
|
2527
|
+
const names = new Map(); // section id -> identifiers it names (read lazily)
|
|
2528
|
+
const namesFor = (s) => {
|
|
2529
|
+
if (!names.has(s.id)) names.set(s.id, namesOf(s, readSectionBody(o.dir, s.id)));
|
|
2530
|
+
return names.get(s.id);
|
|
2531
|
+
};
|
|
2532
|
+
const counts = (sha, s, f) => { // does this changed plain-path file make the entry stale?
|
|
2533
|
+
if (isChangelogPath(f) && !entryAboutChangelog(s)) return false;
|
|
2534
|
+
const texts = lines.get(`${sha}\0${f}`);
|
|
2535
|
+
if (!texts) return true; // content unknown: count the change, never hide it
|
|
2536
|
+
const real = texts.filter((t) => !isRoutineLine(t));
|
|
2537
|
+
if (!real.length) return false; // only version bumps / blank lines
|
|
2538
|
+
if (!isLargeFile(o.root, f)) return true;
|
|
2539
|
+
const nm = namesFor(s);
|
|
2540
|
+
return real.some((t) => { const low = nameKey(t); return nm.some((x) => low.includes(x)); });
|
|
2541
|
+
};
|
|
2526
2542
|
for (const [sha, list] of pending) {
|
|
2527
2543
|
for (const s of list) {
|
|
2528
2544
|
if (d.error) { sections[s.id] = { state: 'unknown', why: `git diff-tree lỗi: ${d.error}` }; continue; }
|
|
2529
2545
|
if (d.missing.has(sha)) { sections[s.id] = { state: 'unknown', why: `commit ${sha.slice(0, 7)} không có trong repo này (clone/nhánh khác chưa fetch?)` }; continue; }
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2546
|
+
const changed = d.bySha.get(sha) || [];
|
|
2547
|
+
const plain = plainPathsOf(s);
|
|
2548
|
+
const globPaths = (s.paths || []).filter((g) => GLOB_CHARS_RE.test(g));
|
|
2549
|
+
const named = [];
|
|
2550
|
+
const soft = []; // changed, but not counted as stale: glob-only, routine, CHANGELOG, lines the entry does not name
|
|
2551
|
+
for (const f of changed) {
|
|
2552
|
+
if (plain.some((g) => globMatch(g, f))) (counts(sha, s, f) ? named : soft).push(f);
|
|
2553
|
+
else if (globPaths.some((g) => globMatch(g, f))) soft.push(f);
|
|
2554
|
+
}
|
|
2555
|
+
if (named.length) sections[s.id] = { state: 'stale', files: named.slice(0, STALE_FILES_KEEP), n_files: named.length };
|
|
2556
|
+
else if (soft.length) sections[s.id] = { state: 'drift', files: soft.slice(0, STALE_FILES_KEEP), n_files: soft.length };
|
|
2557
|
+
else sections[s.id] = { state: 'fresh' };
|
|
2532
2558
|
}
|
|
2533
2559
|
}
|
|
2534
2560
|
}
|
|
2535
2561
|
Object.assign(out, { computed: new Date().toISOString(), sections, counts: freshCounts(sections) });
|
|
2536
2562
|
if (error) out.error = error;
|
|
2537
2563
|
if (head && !error && o.cache !== false && stamp) {
|
|
2538
|
-
writeFreshCache(o.dir, { head, index_mtime: stamp.mtime, index_size: stamp.size, computed: out.computed, sections });
|
|
2564
|
+
writeFreshCache(o.dir, { rule: FRESH_RULE, head, index_mtime: stamp.mtime, index_size: stamp.size, computed: out.computed, sections });
|
|
2539
2565
|
}
|
|
2540
2566
|
return out;
|
|
2541
2567
|
}
|
|
@@ -2560,12 +2586,14 @@ function shortFileList(files, n) {
|
|
|
2560
2586
|
return f.slice(0, k).join(', ') + (f.length > k ? ` (+${f.length - k})` : '');
|
|
2561
2587
|
}
|
|
2562
2588
|
|
|
2563
|
-
/** "N mục, k stale (ids), u không xác định (ids)". */
|
|
2589
|
+
/** "N mục, k stale (ids), d drift (ids), u không xác định (ids)"; drift only when present (it is not stale). */
|
|
2564
2590
|
function knowledgeSummary(fr, n) {
|
|
2565
2591
|
const ids = (st) => Object.keys(fr.sections).filter((id) => fr.sections[id].state === st);
|
|
2566
2592
|
const stale = ids('stale');
|
|
2593
|
+
const drift = ids('drift');
|
|
2567
2594
|
const unk = ids('unknown');
|
|
2568
2595
|
const parts = [`${n} mục`, `${stale.length} stale${stale.length ? ` (${shortFileList(stale, 6)})` : ''}`];
|
|
2596
|
+
if (drift.length) parts.push(`${drift.length} drift (${shortFileList(drift, 4)})`);
|
|
2569
2597
|
if (unk.length) parts.push(`${unk.length} không xác định (${shortFileList(unk, 4)})`);
|
|
2570
2598
|
return parts.join(', ');
|
|
2571
2599
|
}
|
|
@@ -4609,7 +4637,7 @@ function verifyOrphanGate(p, ctx, tag) {
|
|
|
4609
4637
|
denied: true,
|
|
4610
4638
|
event: ev,
|
|
4611
4639
|
stdout: preToolDeny(`dieu-phoi: verifier của task ${tag.task_id} không có lượt exec nào trong phiên này và không nằm trong kế hoạch đã duyệt — `
|
|
4612
|
-
+ 'không chạy verifier cho việc chưa giao. Giao exec của task trước; verifier gộp
|
|
4640
|
+
+ 'không chạy verifier cho việc chưa giao. Giao exec của task trước; verifier gộp (fast, normal s≤4) dùng thẻ `|batch`.'),
|
|
4613
4641
|
};
|
|
4614
4642
|
}
|
|
4615
4643
|
|
|
@@ -4747,6 +4775,43 @@ function logGate(ctx, p, agentId, st, fields) {
|
|
|
4747
4775
|
return ev;
|
|
4748
4776
|
}
|
|
4749
4777
|
|
|
4778
|
+
/**
|
|
4779
|
+
* `--gate-bash` (PreToolUse(Bash|PowerShell) inside dp-* subagents): `knowledge review|merge` writes knowledge
|
|
4780
|
+
* outside the accept gate (a subagent could clear a STALE mark on its own). Deny it; the coordinator runs it.
|
|
4781
|
+
* Matching (0.7.4 S4a): split on `| ; & newline`. In a segment, a token naming the CLI (`dieu-phoi`, `dieu-phoi.cjs|.cmd|.js`,
|
|
4782
|
+
* optional `@version`, optional path: `node D:/x/dieu-phoi.cjs`, `npx @kienntpixon/dieu-phoi@0.7.4`) comes before a `knowledge`
|
|
4783
|
+
* token; between `knowledge` and the subcommand only flags are allowed (`--repo x`, `--repo=x`, before or after `knowledge`,
|
|
4784
|
+
* a flag's one value skipped), then `review|merge`. Known limit: a shell variable or alias hiding the CLI name
|
|
4785
|
+
* (`$DP knowledge review`) is not seen; this gate is a guard for subagents, not a sandbox.
|
|
4786
|
+
*/
|
|
4787
|
+
const KNOWLEDGE_WRITE_SUBS = new Set(['review', 'merge']);
|
|
4788
|
+
const DP_CLI_TOKEN_RE = /(?:^|[\/\\])dieu-phoi(?:\.cjs|\.cmd|\.js)?(?:@\S*)?$/;
|
|
4789
|
+
function isKnowledgeWriteCall(cmd) {
|
|
4790
|
+
for (const seg of cmd.split(/[|;&\n]/)) {
|
|
4791
|
+
const toks = (seg.match(/"[^"]*"|'[^']*'|\S+/g) || []).map((t) => t.replace(/^["']|["']$/g, ''));
|
|
4792
|
+
for (let i = 0; i < toks.length; i++) {
|
|
4793
|
+
if (!DP_CLI_TOKEN_RE.test(toks[i])) continue;
|
|
4794
|
+
for (let j = i + 1; j < toks.length; j++) {
|
|
4795
|
+
if (toks[j] !== 'knowledge') continue;
|
|
4796
|
+
let flagValue = false; // previous token was a flag without `=`, so this word may be its value
|
|
4797
|
+
for (let k = j + 1; k < toks.length; k++) {
|
|
4798
|
+
const t = toks[k];
|
|
4799
|
+
if (KNOWLEDGE_WRITE_SUBS.has(t)) return true;
|
|
4800
|
+
if (t.startsWith('-')) { flagValue = !t.includes('='); continue; }
|
|
4801
|
+
if (!flagValue) break;
|
|
4802
|
+
flagValue = false;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
function handleGateBash(p) {
|
|
4810
|
+
const cmd = p.tool_input && typeof p.tool_input.command === 'string' ? p.tool_input.command : '';
|
|
4811
|
+
if (!isKnowledgeWriteCall(cmd)) return null;
|
|
4812
|
+
return { stdout: preToolDeny('dp-*: subagent không được chạy `knowledge review|merge` (đường ghi knowledge ngoài cổng accept). Báo điều phối viên thay vì tự chạy.') };
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4750
4815
|
/** `--gate-release` (PostToolUseFailure(Agent) inside dp-exec): the spawn failed, give its unconfirmed slot back. */
|
|
4751
4816
|
function handleGateRelease(p, ctx) {
|
|
4752
4817
|
if (p.tool_name !== 'Agent' && p.tool_name !== 'Task') return null;
|
|
@@ -4819,6 +4884,7 @@ function runHook(opts) {
|
|
|
4819
4884
|
if (o.mode === 'watch') return handleWatch(p, ctx, o); // { stdout } or null
|
|
4820
4885
|
if (o.mode === 'gate') return handleGate(p, ctx, o); // { stdout } or null
|
|
4821
4886
|
if (o.mode === 'gate-release') return handleGateRelease(p, ctx); // { released } or null, prints nothing
|
|
4887
|
+
if (o.mode === 'gate-bash') return handleGateBash(p); // { stdout } (deny) or null
|
|
4822
4888
|
if (name === 'PreToolUse') return handleChainGate(p, ctx); // { stdout } (deny / warn) or null
|
|
4823
4889
|
if (name === 'SubagentStart') return handleSubagentStart(p, ctx); // live record or null, prints nothing
|
|
4824
4890
|
if (name === 'SessionStart') return handleSessionStart(p, ctx); // { stdout } or null, never an event
|
|
@@ -4982,8 +5048,8 @@ async function runSync(ctx, opts) {
|
|
|
4982
5048
|
|
|
4983
5049
|
module.exports = {
|
|
4984
5050
|
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, findOrphanAgents, orphanLines, ORPHAN_MIN_MS, ORPHAN_NO_COMPLETE_MS, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
4985
|
-
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS,
|
|
4986
|
-
|
|
5051
|
+
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS, isMode, normMode, LEGACY_MODE, rootModeFromEvents, rootModeFromText, dispatchModeFromEvents, taskModeInfo, planOfTask, modeFields,
|
|
5052
|
+
rootTaskIds,
|
|
4987
5053
|
testCommandInfo, testTimeOf, testSFromText,
|
|
4988
5054
|
CORE_VERSION, SCHEMA, TASK_TYPES, URGENCIES, ROLES, MODEL_ALIASES, UNTAGGED, TYPE_ALIASES,
|
|
4989
5055
|
sha8, nowIso, readJsonFile, writeFileAtomic, renameWithRetry, acquireLock, acquireFileLock, withFileLock, defaultClaudeDir, resolveHome, pathsFor, loadConfig, makeCtx,
|
|
@@ -5012,7 +5078,8 @@ if (require.main === module) {
|
|
|
5012
5078
|
.finally(() => process.exit(0));
|
|
5013
5079
|
} else {
|
|
5014
5080
|
const mode = process.argv.includes('--watch') ? 'watch'
|
|
5015
|
-
: (process.argv.includes('--gate-release') ? 'gate-release'
|
|
5081
|
+
: (process.argv.includes('--gate-release') ? 'gate-release'
|
|
5082
|
+
: (process.argv.includes('--gate-bash') ? 'gate-bash' : (process.argv.includes('--gate') ? 'gate' : null)));
|
|
5016
5083
|
const r = runHook({ mode });
|
|
5017
5084
|
if (r && typeof r.stdout === 'string') process.stdout.write(r.stdout);
|
|
5018
5085
|
process.exitCode = 0;
|
package/lib/aggregate.cjs
CHANGED
|
@@ -574,9 +574,27 @@ function analyze(events, opts) {
|
|
|
574
574
|
};
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
/**
|
|
578
|
+
* core 0.7.4 (s1): one sample per task that has a first exec attempt (excluded tasks left out). `model`/`effort` are
|
|
579
|
+
* that attempt's (the cell buildCells uses), `handoff` = some segment of that attempt's chain ended on an auto-handoff,
|
|
580
|
+
* `total_cost` = the task's cost over all segments. Only plan-table's TÁCH? warning reads these; it is advice, not a gate.
|
|
581
|
+
*/
|
|
582
|
+
function handoffSamples(events) {
|
|
583
|
+
const { agents, userVerdicts, autoVerdicts } = buildAgents(withoutDiscarded(events));
|
|
584
|
+
return buildTasks(agents, userVerdicts, autoVerdicts)
|
|
585
|
+
.filter((t) => t.first && !t.excluded)
|
|
586
|
+
.map((t) => ({
|
|
587
|
+
task_type: t.task_type,
|
|
588
|
+
model: t.first.model,
|
|
589
|
+
effort: t.first.effort,
|
|
590
|
+
handoff: t.first_chain.some((a) => a.handoff),
|
|
591
|
+
total_cost: t.total_cost,
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
|
|
577
595
|
module.exports = {
|
|
578
596
|
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, ABORTED, QUOTA_REASON_RE, HANDOFF_REASON_RE, discardedTaskIds, withoutDiscarded, CANONICAL_CELL_FIELDS, normalizeVerdict, isSelf, isInternalComplete, buildAgents, buildTasks,
|
|
579
|
-
buildCells, buildSteers, canonicalCells, isBatchVerify, batchVerifyCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
597
|
+
buildCells, buildSteers, canonicalCells, isBatchVerify, batchVerifyCells, orchestrationSummary, analyze, handoffSamples, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
580
598
|
};
|
|
581
599
|
|
|
582
600
|
// ------------------------------------------------------------------ core 0.7.0 (s2): stats split by mode
|
|
@@ -584,7 +602,7 @@ module.exports = {
|
|
|
584
602
|
// Kept apart from the canonical cells (not part of the CLI <-> sink parity yet). A task's mode = `mode` of its first
|
|
585
603
|
// exec dispatch; tasks dispatched before modes existed (no `mode`) count as normal.
|
|
586
604
|
|
|
587
|
-
const MODE_KEYS = ['fast', 'normal'
|
|
605
|
+
const MODE_KEYS = ['fast', 'normal']; // legacy 'strict' dispatches count as normal (0.7.4)
|
|
588
606
|
const NO_CATCH_VERIFY = new Set(['self', 'none']);
|
|
589
607
|
|
|
590
608
|
/**
|