@kienntpixon/dieu-phoi 0.7.3 → 0.7.5
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 +25 -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 +13 -9
- package/hook.cjs +196 -129
- package/lib/aggregate.cjs +20 -2
- package/lib/args.cjs +1 -1
- package/lib/choose.cjs +27 -22
- package/lib/commands.cjs +198 -34
- package/lib/knowledge.cjs +66 -10
- package/lib/prune.cjs +81 -7
- package/lib/worktree.cjs +166 -7
- package/package.json +1 -1
- package/profile/lessons.md +0 -1
package/lib/choose.cjs
CHANGED
|
@@ -145,6 +145,7 @@ function loadProfile(skillDir) {
|
|
|
145
145
|
return {
|
|
146
146
|
source, dept: r.dept || null, priors, lambda, explore_rate: explore, time_cap_min: timeCap, extra_task_types: extra,
|
|
147
147
|
orch_ctx_soft_cap: softCap, tier_floor: tierFloor,
|
|
148
|
+
batch_verify_normal: r.batch_verify_normal !== false,
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -334,23 +335,26 @@ function resolveTaskType(raw, profile) {
|
|
|
334
335
|
|
|
335
336
|
// ------------------------------------------------------------------ verify plan (core 0.7.0, mode per request)
|
|
336
337
|
|
|
337
|
-
const VERIFY_PLANS = ['independent-blocking', 'independent-nonblocking', 'self+batch'
|
|
338
|
+
const VERIFY_PLANS = ['independent-blocking', 'independent-nonblocking', 'self+batch'];
|
|
338
339
|
/** fast: highest score still allowed to drop its own blocking verifier (s<=6 self+batch, s7-8 independent-nonblocking). */
|
|
339
340
|
const FAST_SELF_MAX = 6;
|
|
340
341
|
const FAST_SOFT_MAX = 8;
|
|
341
|
-
/**
|
|
342
|
-
const
|
|
343
|
-
|
|
342
|
+
/** normal (0.7.4, user decision Q1): s<=4 with no step depending on it -> self+batch (one verifier per group, not per step). */
|
|
343
|
+
const NORMAL_BATCH_MAX = 4;
|
|
344
344
|
/**
|
|
345
345
|
* How one exec step is checked, from the request mode and the step's rubric inputs.
|
|
346
346
|
* The safety floor is evaluated FIRST and wins in every mode: reversible=n, publish/deploy, verifiable=n, score>=9
|
|
347
|
-
* always get an independent blocking verifier. Only after that does
|
|
348
|
-
*
|
|
349
|
-
*
|
|
347
|
+
* always get an independent blocking verifier. Only after that does fast loosen it, and normal batch the s<=4 steps.
|
|
348
|
+
* Legacy mode `strict` (removed 0.7.4) reads as normal.
|
|
349
|
+
* opts: { mode, score, verifiable, reversible, publish, dependents, batchNormal, seed }
|
|
350
|
+
* batchNormal === false (profile.batch_verify_normal=false) switches the normal batch off (rollback switch).
|
|
351
|
+
* Returns { verify_plan, verify_reason, verify_floor: string[], test_scope: 'full' | 'selective' }. Step verification (exec, independent
|
|
352
|
+
* verifier, batch verifier) always runs the touched-module tests; the full suite runs once at the release gate, not here.
|
|
353
|
+
* test_scope is 'full' only for a publish/deploy step (that step is the release gate's trigger); every other step is 'selective'.
|
|
350
354
|
*/
|
|
351
355
|
function verifyPlanFor(opts) {
|
|
352
|
-
const mode = opts.mode || core.DEFAULT_MODE;
|
|
353
|
-
if (!
|
|
356
|
+
const mode = core.normMode(opts.mode || core.DEFAULT_MODE);
|
|
357
|
+
if (!mode) throw new Error(`--mode phải là ${core.MODES.join('|')}`);
|
|
354
358
|
const score = Number(opts.score);
|
|
355
359
|
const floor = [];
|
|
356
360
|
if (opts.reversible === false) floor.push('khó hoàn tác');
|
|
@@ -358,23 +362,24 @@ function verifyPlanFor(opts) {
|
|
|
358
362
|
if (opts.verifiable === false) floor.push('không kiểm chứng bằng máy');
|
|
359
363
|
if (score >= 9) floor.push(`score ${score}≥9`);
|
|
360
364
|
const out = (plan, reason, scope) => ({ verify_plan: plan, verify_reason: reason, verify_floor: floor, test_scope: scope });
|
|
361
|
-
if (floor.length) return out('independent-blocking', `sàn an toàn (${floor.join(', ')}) → verifier độc lập chặn bước, mọi mode`, 'full');
|
|
365
|
+
if (floor.length) return out('independent-blocking', `sàn an toàn (${floor.join(', ')}) → verifier độc lập chặn bước, mọi mode`, opts.publish === true ? 'full' : 'selective');
|
|
362
366
|
if (mode === 'fast') {
|
|
363
367
|
if (score <= FAST_SELF_MAX) {
|
|
364
|
-
return out('self+batch', `fast, s${score}≤${FAST_SELF_MAX}: exec tự kiểm (test chọn lọc theo file đổi, --verify self); verifier gộp chạy cả suite
|
|
368
|
+
return out('self+batch', `fast, s${score}≤${FAST_SELF_MAX}: exec tự kiểm (test chọn lọc theo file đổi, --verify self); verifier gộp chạy test chọn lọc của các file cả nhóm đã đụng; full suite chỉ ở cổng phát hành`, 'selective');
|
|
365
369
|
}
|
|
366
370
|
if (score <= FAST_SOFT_MAX) {
|
|
367
371
|
return opts.dependents === true
|
|
368
|
-
? out('independent-blocking', `fast, s${score}: bước sau phụ thuộc (depends_on) vào bước này → verifier độc lập vẫn chặn`, '
|
|
369
|
-
: out('independent-nonblocking', `fast, s${score}: verifier độc lập nhưng không chặn bước sau (không bước nào depends_on)`, '
|
|
372
|
+
? out('independent-blocking', `fast, s${score}: bước sau phụ thuộc (depends_on) vào bước này → verifier độc lập vẫn chặn`, 'selective')
|
|
373
|
+
: out('independent-nonblocking', `fast, s${score}: verifier độc lập nhưng không chặn bước sau (không bước nào depends_on)`, 'selective');
|
|
370
374
|
}
|
|
371
375
|
}
|
|
372
|
-
if (mode === '
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
+
if (mode === 'normal' && score <= NORMAL_BATCH_MAX && opts.batchNormal !== false) {
|
|
377
|
+
if (opts.dependents !== true) {
|
|
378
|
+
return out('self+batch', `normal, s${score}≤${NORMAL_BATCH_MAX}, không bước nào phụ thuộc: exec tự kiểm (test chọn lọc theo file đổi, --verify self); verifier gộp chạy test chọn lọc của các file cả nhóm đã đụng (một lần cuối nhóm, trước cổng 3); full suite chỉ ở cổng phát hành`, 'selective');
|
|
379
|
+
}
|
|
380
|
+
return out('independent-blocking', `normal, s${score}: bước sau phụ thuộc (depends_on) vào bước này → verifier độc lập chặn`, 'selective');
|
|
376
381
|
}
|
|
377
|
-
return out('independent-blocking', `${mode}: verifier độc lập chặn bước`, '
|
|
382
|
+
return out('independent-blocking', `${mode}: verifier độc lập chặn bước`, 'selective');
|
|
378
383
|
}
|
|
379
384
|
|
|
380
385
|
// ------------------------------------------------------------------ choose
|
|
@@ -412,14 +417,14 @@ function choose(opts) {
|
|
|
412
417
|
}
|
|
413
418
|
const reversible = opts.reversible !== false;
|
|
414
419
|
const verifiable = opts.verifiable !== false;
|
|
415
|
-
if (opts.mode !== undefined && opts.mode !== null && !core.
|
|
420
|
+
if (opts.mode !== undefined && opts.mode !== null && !core.normMode(opts.mode)) {
|
|
416
421
|
throw new Error(`--mode phải là ${core.MODES.join('|')} (nhận "${opts.mode}")`);
|
|
417
422
|
}
|
|
418
|
-
const mode = opts.mode || core.DEFAULT_MODE;
|
|
423
|
+
const mode = core.normMode(opts.mode || core.DEFAULT_MODE);
|
|
424
|
+
const profile = opts.profile || loadProfile(null);
|
|
419
425
|
const vp = role === 'exec'
|
|
420
|
-
? verifyPlanFor({ mode, score, verifiable, reversible, publish: opts.publish === true, dependents: opts.dependents === true, seed: opts.seed })
|
|
426
|
+
? verifyPlanFor({ mode, score, verifiable, reversible, publish: opts.publish === true, dependents: opts.dependents === true, batchNormal: profile.batch_verify_normal, seed: opts.seed })
|
|
421
427
|
: null;
|
|
422
|
-
const profile = opts.profile || loadProfile(null);
|
|
423
428
|
const lambda = profile.lambda[urgency];
|
|
424
429
|
const rng = makeRng(opts.seed);
|
|
425
430
|
const notes = [];
|
package/lib/commands.cjs
CHANGED
|
@@ -411,7 +411,7 @@ function cmdStart(opts) {
|
|
|
411
411
|
core.modeLine(info),
|
|
412
412
|
...core.orchLessonLines(info),
|
|
413
413
|
...core.knowledgeLines(info),
|
|
414
|
-
];
|
|
414
|
+
].filter((l) => l !== '');
|
|
415
415
|
return { code: 0, out: out.join('\n') };
|
|
416
416
|
}
|
|
417
417
|
|
|
@@ -435,23 +435,24 @@ function modeEvent(ctx, root, mode, source, reason, session) {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
/**
|
|
438
|
-
* `dieu-phoi mode --root <root_task_id> [fast|normal
|
|
438
|
+
* `dieu-phoi mode --root <root_task_id> [fast|normal] [--reason R] [--session S]`: show or change the mode of a
|
|
439
439
|
* request. One event per change; the same mode again writes nothing. Steps not yet dispatched follow the new mode,
|
|
440
440
|
* dispatched steps keep the mode they were given. Never changes a mode by itself.
|
|
441
441
|
*/
|
|
442
442
|
function cmdMode(opts) {
|
|
443
|
-
const usage = 'Cú pháp: dieu-phoi mode --root <root_task_id> [fast|normal
|
|
443
|
+
const usage = 'Cú pháp: dieu-phoi mode --root <root_task_id> [fast|normal] [--reason R] [--session S] [--json]';
|
|
444
444
|
if (typeof opts.root !== 'string') throw new Error(usage);
|
|
445
445
|
const root = requireTaskId(opts.root);
|
|
446
446
|
const ctx = ctxOf(opts);
|
|
447
447
|
const cur = core.rootModeFromEvents(core.readEvents(ctx), root);
|
|
448
448
|
const was = cur ? cur.mode : core.DEFAULT_MODE;
|
|
449
|
-
const
|
|
449
|
+
const wantRaw = opts._[0];
|
|
450
450
|
const show = (extra) => (opts.json ? JSON.stringify(Object.assign({ root_task_id: root, mode: was, previous: was, changed: false }, extra)) : null);
|
|
451
|
-
if (
|
|
451
|
+
if (wantRaw === undefined) {
|
|
452
452
|
return { code: 0, out: show({}) || `root ${root}: mode=${was}${cur ? ` (từ ${cur.ts}, nguồn ${cur.source || '?'})` : ' (mặc định, chưa đặt)'}` };
|
|
453
453
|
}
|
|
454
|
-
|
|
454
|
+
const want = userMode(wantRaw);
|
|
455
|
+
if (!want) throw new Error(`mode phải là ${core.MODES.join('|')} (nhận "${wantRaw}"). ${usage}`);
|
|
455
456
|
if (cur && was === want) {
|
|
456
457
|
return { code: 0, out: show({}) || `root ${root}: vẫn ${want} (không đổi, không ghi sự kiện)` };
|
|
457
458
|
}
|
|
@@ -753,6 +754,8 @@ function cmdVerdict(opts) {
|
|
|
753
754
|
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true, events);
|
|
754
755
|
// s17: accept của người dùng gỡ worktree có dấu của từng bước (sạch); còn thay đổi thì giữ và báo.
|
|
755
756
|
const wtLines = list.reduce((acc, x) => acc.concat(wt.cleanupTask(ctx, events, x.ev.task_id, opts.repo)), []);
|
|
757
|
+
// s4 (0.7.5): nhắc số worktree đã merge của repo; --clean-merged thì gỡ luôn (wt clean --merged --yes).
|
|
758
|
+
wtLines.push(...wt.mergedAfterAccept(ctx, list.map((x) => x.ev.task_id), opts.repo, opts['clean-merged'] === true));
|
|
756
759
|
if (!list.length) {
|
|
757
760
|
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
758
761
|
// Exit code follows the merge (0 merged or nothing new, 2 conflict or reject, 1 error); no delta at all keeps the old "nothing to do" error.
|
|
@@ -774,6 +777,7 @@ function cmdVerdict(opts) {
|
|
|
774
777
|
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false, events) : null;
|
|
775
778
|
// s17: người dùng accept → gỡ worktree có dấu của task (sạch); reject giữ nguyên để làm lại.
|
|
776
779
|
const wtLines = ev.by === 'user' && ev.outcome === 'accept' ? wt.cleanupTask(ctx, events, ev.task_id, opts.repo) : [];
|
|
780
|
+
if (ev.by === 'user' && ev.outcome === 'accept') wtLines.push(...wt.mergedAfterAccept(ctx, [ev.task_id], opts.repo, opts['clean-merged'] === true));
|
|
777
781
|
if (opts.json) {
|
|
778
782
|
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
779
783
|
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
@@ -980,6 +984,28 @@ function liveLine(live) {
|
|
|
980
984
|
return `live: phiên ${part(live.session || 0, lim.session)} · máy ${part(live.machine, lim.machine)}${tasks ? ` — ${tasks}` : ''}`;
|
|
981
985
|
}
|
|
982
986
|
|
|
987
|
+
/**
|
|
988
|
+
* core 0.7.4 (S1): steps of an approved plan (last 7 days) that run self+batch, whose exec has completed, and that have
|
|
989
|
+
* neither a batch verifier verdict (reason_code batch-verify) nor a user verdict yet. Gate 3 must not see them unchecked.
|
|
990
|
+
*/
|
|
991
|
+
function batchPendingOf(ctx, events, now) {
|
|
992
|
+
let files = [];
|
|
993
|
+
try { files = fs.readdirSync(ctx.paths.plans).filter((f) => f.endsWith('.json')); } catch (_) { return []; }
|
|
994
|
+
const out = [];
|
|
995
|
+
for (const f of files) {
|
|
996
|
+
const rec = core.readJsonFile(path.join(ctx.paths.plans, f), null);
|
|
997
|
+
if (!rec || !Array.isArray(rec.steps)) continue;
|
|
998
|
+
const fresh = Date.parse(rec.approved_at) > now - 7 * 24 * 3600 * 1000;
|
|
999
|
+
if (!fresh) continue;
|
|
1000
|
+
const pending = rec.steps.filter((s) => s && s.verify_plan === 'self+batch' && s.task_id
|
|
1001
|
+
&& events.some((e) => e.kind === 'complete' && e.task_id === s.task_id)
|
|
1002
|
+
&& !events.some((e) => e.kind === 'verdict' && e.task_id === s.task_id
|
|
1003
|
+
&& (String(e.reason_code || '').startsWith('batch-verify') || e.by === 'user')));
|
|
1004
|
+
if (pending.length) out.push({ root_task_id: rec.root_task_id || f.replace(/\.json$/, ''), steps: pending.map((s) => s.id) });
|
|
1005
|
+
}
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
983
1009
|
function cmdState(opts) {
|
|
984
1010
|
const ctx = ctxOf(opts);
|
|
985
1011
|
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
@@ -998,17 +1024,19 @@ function cmdState(opts) {
|
|
|
998
1024
|
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
999
1025
|
const live = core.liveSummary(ctx, sid); // core 0.7.3 (s1)
|
|
1000
1026
|
const stuck = core.stuckAgents(ctx, { sessionId: sid }); // s6
|
|
1027
|
+
const batchPending = batchPendingOf(ctx, core.readEvents(ctx), Date.now()); // core 0.7.4 (S1)
|
|
1001
1028
|
if (opts.json) {
|
|
1002
1029
|
const liveJ = {
|
|
1003
1030
|
session: live.session, machine: live.machine, limits: live.limits, free_session: freeSlots(live.limits.session, live.session), free_machine: freeSlots(live.limits.machine, live.machine), agents: live.agents,
|
|
1004
1031
|
stuck: stuck.map((a) => ({ agent_id: a.agent_id, task_id: a.task_id, role: a.role, gen: a.gen, state: a.state, reasons: a.reasons, idle_min: a.idle_min, age_min: a.age_min })),
|
|
1005
1032
|
};
|
|
1006
|
-
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }, { live: liveJ }), null, 2) };
|
|
1033
|
+
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }, { live: liveJ }, batchPending.length ? { batch_pending: batchPending } : {}), null, 2) };
|
|
1007
1034
|
}
|
|
1035
|
+
const batchLines = batchPending.map((p) => `CẢNH BÁO: ${p.root_task_id}: ${p.steps.length} bước self+batch (${p.steps.join(', ')}) chưa có verdict verifier gộp (batch-verify) — giao verifier gộp (brief H) trước cổng 3`);
|
|
1008
1036
|
const stuckLines = stuck.length
|
|
1009
1037
|
? [`stuck: ${stuck.slice(0, 6).map((a) => `${a.task_id || a.agent_id}:${a.role} ${a.state === 'orphan' ? 'mồ côi' : 'kẹt'} ${a.idle_min}'`).join(', ')} → TaskStop rồi \`dieu-phoi reap --yes\``]
|
|
1010
1038
|
: [];
|
|
1011
|
-
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`], [liveLine(live)], stuckLines).join('\n') };
|
|
1039
|
+
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`], [liveLine(live)], stuckLines, batchLines).join('\n') };
|
|
1012
1040
|
}
|
|
1013
1041
|
|
|
1014
1042
|
// ------------------------------------------------------------------ reap (core 0.7.3 s6)
|
|
@@ -1390,11 +1418,24 @@ function cmdRetro(opts) {
|
|
|
1390
1418
|
|
|
1391
1419
|
// ------------------------------------------------------------------ choose
|
|
1392
1420
|
|
|
1421
|
+
/**
|
|
1422
|
+
* A user-supplied mode value: fast | normal; the removed `strict` (0.7.4) is accepted with a warning and becomes
|
|
1423
|
+
* normal; anything else null.
|
|
1424
|
+
*/
|
|
1425
|
+
function userMode(raw) {
|
|
1426
|
+
if (raw === core.LEGACY_MODE) {
|
|
1427
|
+
process.stderr.write('dieu-phoi: mode strict đã bỏ từ 0.7.4 — dùng normal (sàn an toàn vẫn luôn có verifier độc lập chặn).\n');
|
|
1428
|
+
return core.DEFAULT_MODE;
|
|
1429
|
+
}
|
|
1430
|
+
return core.isMode(raw) ? raw : null;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1393
1433
|
/** --mode value (validated) or undefined; a bare --mode is an error. */
|
|
1394
1434
|
function modeOpt(opts) {
|
|
1395
1435
|
if (opts.mode === undefined) return undefined;
|
|
1396
|
-
|
|
1397
|
-
|
|
1436
|
+
const m = typeof opts.mode === 'string' ? userMode(opts.mode) : null;
|
|
1437
|
+
if (!m) throw new Error(`--mode phải là ${core.MODES.join('|')}`);
|
|
1438
|
+
return m;
|
|
1398
1439
|
}
|
|
1399
1440
|
|
|
1400
1441
|
function chooseInput(opts, ctx, events) {
|
|
@@ -1432,15 +1473,39 @@ function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
|
|
|
1432
1473
|
}
|
|
1433
1474
|
|
|
1434
1475
|
/**
|
|
1435
|
-
* core 0.7.
|
|
1436
|
-
*
|
|
1476
|
+
* core 0.7.4 (B1): the step of the approved plan (<plans>/<root>.json) whose task_id is taskId, or null. Its rubric
|
|
1477
|
+
* (score, verifiable, reversible, publish, dependents) is what `choose --task` takes the stricter of.
|
|
1478
|
+
*/
|
|
1479
|
+
function approvedPlanStep(ctx, taskId) {
|
|
1480
|
+
let files = [];
|
|
1481
|
+
try { files = fs.readdirSync(ctx.paths.plans).filter((f) => f.endsWith('.json')); } catch (_) { return null; }
|
|
1482
|
+
for (const f of files) {
|
|
1483
|
+
const rec = core.readJsonFile(path.join(ctx.paths.plans, f), null);
|
|
1484
|
+
if (!rec || !Array.isArray(rec.steps)) continue;
|
|
1485
|
+
const step = rec.steps.find((s) => s && s.task_id === taskId);
|
|
1486
|
+
if (!step) continue;
|
|
1487
|
+
const dependents = step.dependents === true
|
|
1488
|
+
|| rec.steps.some((o) => o && o !== step && Array.isArray(o.depends_on) && o.depends_on.includes(step.id));
|
|
1489
|
+
return Object.assign({}, step, { root: rec.root_task_id || f.replace(/\.json$/, ''), dependents });
|
|
1490
|
+
}
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* core 0.7.4 (B1): the stricter of the CLI flags and the approved step, field by field — publish, dependents, not
|
|
1496
|
+
* reversible, not verifiable, higher score. Mutates `input`; returns the names it raised (empty = flags already as strict).
|
|
1497
|
+
* Omitting a flag can therefore never loosen a step the plan made blocking.
|
|
1437
1498
|
*/
|
|
1438
|
-
function
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1499
|
+
function applyApprovedStep(input, step) {
|
|
1500
|
+
const raised = [];
|
|
1501
|
+
if (step.publish === true && input.publish !== true) { raised.push('publish'); input.publish = true; }
|
|
1502
|
+
if (step.dependents === true && input.dependents !== true) { raised.push('dependents'); input.dependents = true; }
|
|
1503
|
+
if (step.reversible === false && input.reversible !== false) { raised.push('reversible'); input.reversible = false; }
|
|
1504
|
+
if (step.verifiable === false && input.verifiable !== false) { raised.push('verifiable'); input.verifiable = false; }
|
|
1505
|
+
const planScore = Number(step.score);
|
|
1506
|
+
const flagScore = Number(input.score);
|
|
1507
|
+
if (Number.isFinite(planScore) && Number.isFinite(flagScore) && planScore > flagScore) { raised.push('score'); input.score = planScore; }
|
|
1508
|
+
return raised;
|
|
1444
1509
|
}
|
|
1445
1510
|
|
|
1446
1511
|
function cmdChoose(opts) {
|
|
@@ -1450,6 +1515,8 @@ function cmdChoose(opts) {
|
|
|
1450
1515
|
}
|
|
1451
1516
|
if (opts.task !== undefined) requireTaskId(typeof opts.task === 'string' ? opts.task : '');
|
|
1452
1517
|
const input = chooseInput(opts, ctx, core.readEvents(ctx));
|
|
1518
|
+
const planStep = input.taskId ? approvedPlanStep(ctx, input.taskId) : null;
|
|
1519
|
+
const raised = planStep ? applyApprovedStep(input, planStep) : [];
|
|
1453
1520
|
let modeSource = 'flag';
|
|
1454
1521
|
const mi = input.taskId ? core.taskModeInfo(ctx, input.taskId, input.events) : null;
|
|
1455
1522
|
if (input.mode === undefined) {
|
|
@@ -1463,7 +1530,12 @@ function cmdChoose(opts) {
|
|
|
1463
1530
|
}
|
|
1464
1531
|
const res = chooser.choose(input);
|
|
1465
1532
|
res.mode_source = modeSource;
|
|
1466
|
-
if (
|
|
1533
|
+
if (planStep) {
|
|
1534
|
+
res.plan_step = { root_task_id: planStep.root, step: planStep.id, verify_plan: planStep.verify_plan || null, raised };
|
|
1535
|
+
if (planStep.verify_plan && res.verify_plan && planStep.verify_plan !== res.verify_plan) {
|
|
1536
|
+
res.plan_deviation = `${planStep.verify_plan} → ${res.verify_plan}`;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1467
1539
|
const orch = measureOrch(opts, ctx, input.profile);
|
|
1468
1540
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
1469
1541
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
@@ -1485,8 +1557,10 @@ function cmdChoose(opts) {
|
|
|
1485
1557
|
}
|
|
1486
1558
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
1487
1559
|
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1560
|
+
const planWarn = res.plan_step && res.plan_step.raised.length
|
|
1561
|
+
? `\nKế hoạch đã duyệt (${res.plan_step.root_task_id} bước ${res.plan_step.step}): nâng theo ${res.plan_step.raised.join(', ')}` : '';
|
|
1562
|
+
const deviationWarn = res.plan_deviation ? `\nLỆCH kiểm khỏi kế hoạch đã duyệt: ${res.plan_deviation}` : '';
|
|
1563
|
+
const warn = `${typeWarn}${planWarn}${deviationWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
1490
1564
|
const est =`~${fmtUsd(res.est_cost_usd)} · ~${fmtNum(res.est_time_min)} phút (${res.est_source === 'data' ? 'dữ liệu' : 'ước lượng thô'})`;
|
|
1491
1565
|
if (res.action === 'ask_user') {
|
|
1492
1566
|
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
@@ -1541,7 +1615,7 @@ function buildPlanTable(rawSteps, base) {
|
|
|
1541
1615
|
verifiable: s.verifiable === undefined ? true : yesNo(s.verifiable, true),
|
|
1542
1616
|
reversible: s.reversible === undefined ? true : yesNo(s.reversible, true),
|
|
1543
1617
|
publish: s.publish === undefined ? false : yesNo(s.publish, false),
|
|
1544
|
-
mode: s.mode === undefined || s.mode === null ? null : s.mode,
|
|
1618
|
+
mode: s.mode === undefined || s.mode === null ? null : (userMode(s.mode) || s.mode),
|
|
1545
1619
|
urgency: s.urgency,
|
|
1546
1620
|
deps: deps.map(String),
|
|
1547
1621
|
};
|
|
@@ -1570,6 +1644,8 @@ function buildPlanTable(rawSteps, base) {
|
|
|
1570
1644
|
const head = {
|
|
1571
1645
|
step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
|
|
1572
1646
|
verifiable: s.verifiable, reversible: s.reversible, depends_on: s.deps,
|
|
1647
|
+
publish: s.publish === true || base.publish === true,
|
|
1648
|
+
dependents: steps.some((o) => o !== s && o.deps.includes(s.step)),
|
|
1573
1649
|
mode: s.mode || base.mode || core.DEFAULT_MODE, mode_override: s.mode || null,
|
|
1574
1650
|
};
|
|
1575
1651
|
try {
|
|
@@ -1635,6 +1711,78 @@ function planTaskIdProblems(plan, root) {
|
|
|
1635
1711
|
return problems;
|
|
1636
1712
|
}
|
|
1637
1713
|
|
|
1714
|
+
// core 0.7.4 (s1): TÁCH? — a step that is likely to hand off (score ≥ 6; its task_type × tier has handoff rate ≥ 20%
|
|
1715
|
+
// over ≥ 10 tasks) is flagged in the table so the planner splits it. Advice only: never blocks --approve.
|
|
1716
|
+
const SPLIT_MIN_SCORE = 6;
|
|
1717
|
+
const SPLIT_MIN_RATE = 0.2;
|
|
1718
|
+
const SPLIT_MIN_N = 10;
|
|
1719
|
+
|
|
1720
|
+
/** Model family of a tier or a recorded model id (opus | sonnet | haiku | fable), as choose.cjs matches cells. */
|
|
1721
|
+
function modelFamily(m) {
|
|
1722
|
+
const s = String(m || '').toLowerCase();
|
|
1723
|
+
return ['opus', 'sonnet', 'haiku', 'fable'].find((k) => s.includes(k)) || null;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function planSplitWarnings(steps, events) {
|
|
1727
|
+
const samples = agg.handoffSamples(events);
|
|
1728
|
+
const out = [];
|
|
1729
|
+
for (const r of steps) {
|
|
1730
|
+
if (r.action === 'refused' || !r.tier || !(Number(r.score) >= SPLIT_MIN_SCORE)) continue;
|
|
1731
|
+
const type = r.task_type || r.type;
|
|
1732
|
+
const [model, effort] = String(r.tier).split('/');
|
|
1733
|
+
const same = samples.filter((s) => s.task_type === type && modelFamily(s.model) === modelFamily(model) && s.effort === effort);
|
|
1734
|
+
if (same.length < SPLIT_MIN_N) continue;
|
|
1735
|
+
const hand = same.filter((s) => s.handoff);
|
|
1736
|
+
const rate = hand.length / same.length;
|
|
1737
|
+
if (rate < SPLIT_MIN_RATE) continue;
|
|
1738
|
+
const clean = same.filter((s) => !s.handoff);
|
|
1739
|
+
out.push({
|
|
1740
|
+
step: r.step,
|
|
1741
|
+
task_type: type,
|
|
1742
|
+
tier: r.tier,
|
|
1743
|
+
n: same.length,
|
|
1744
|
+
handoff_rate: Math.round(rate * 1000) / 1000,
|
|
1745
|
+
cost_handoff_p50_usd: agg.percentile(hand.map((s) => s.total_cost), 0.5),
|
|
1746
|
+
cost_clean_p50_usd: agg.percentile(clean.map((s) => s.total_cost), 0.5),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
return out;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// core 0.7.4 (s2): TIER↑ — a mechanical type whose override sits above the tier choose would pick is flagged
|
|
1753
|
+
// (advice only; --approve is not blocked). The $p50 column and the warning read the same stats cells as `stats`.
|
|
1754
|
+
const TIER_UP_TYPES = ['mechanical-edit', 'extract', 'search', 'test-write'];
|
|
1755
|
+
|
|
1756
|
+
/** The stats cell (buildCells row) of one task_type × tier, or null. */
|
|
1757
|
+
function statCellOf(cells, type, tier) {
|
|
1758
|
+
const [model, effort] = String(tier).split('/');
|
|
1759
|
+
return cells.find((c) => c.task_type === type && modelFamily(c.model) === modelFamily(model) && c.effort === effort) || null;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function planTierWarnings(steps, cells) {
|
|
1763
|
+
const out = [];
|
|
1764
|
+
for (const r of steps) {
|
|
1765
|
+
if (r.action === 'refused' || !r.overridden || !r.suggested_tier || !r.tier) continue;
|
|
1766
|
+
if (!TIER_UP_TYPES.includes(r.task_type)) continue;
|
|
1767
|
+
const hi = chooser.parseTier(r.tier);
|
|
1768
|
+
const lo = chooser.parseTier(r.suggested_tier);
|
|
1769
|
+
if (hi === null || lo === null || hi <= lo) continue;
|
|
1770
|
+
const a = statCellOf(cells, r.task_type, r.tier);
|
|
1771
|
+
const b = statCellOf(cells, r.task_type, r.suggested_tier);
|
|
1772
|
+
out.push({
|
|
1773
|
+
step: r.step,
|
|
1774
|
+
task_type: r.task_type,
|
|
1775
|
+
tier: r.tier,
|
|
1776
|
+
suggested_tier: r.suggested_tier,
|
|
1777
|
+
cost_p50_usd: a && a.n ? a.cost_p50_usd : null,
|
|
1778
|
+
suggested_cost_p50_usd: b && b.n ? b.cost_p50_usd : null,
|
|
1779
|
+
accept_rate: a && a.n ? a.accept_rate : null,
|
|
1780
|
+
suggested_accept_rate: b && b.n ? b.accept_rate : null,
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
return out;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1638
1786
|
/** Mapped ad-hoc labels (bug-fix → debug, …): not an error, but the tag must carry the mapped type. */
|
|
1639
1787
|
function planTypeNotes(plan) {
|
|
1640
1788
|
return plan.steps.filter((r) => r.mapped_from).map((r) => `bước ${r.step}: ${r.type_warning}`);
|
|
@@ -1692,6 +1840,10 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
|
1692
1840
|
steps: plan.steps.map((r) => Object.assign({
|
|
1693
1841
|
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
|
|
1694
1842
|
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
1843
|
+
// core 0.7.4 (B1): the rubric `choose --task` takes the stricter of its flags against
|
|
1844
|
+
score: Number.isFinite(Number(r.score)) ? Number(r.score) : null, verifiable: r.verifiable !== false,
|
|
1845
|
+
reversible: r.reversible !== false, publish: r.publish === true, dependents: r.dependents === true,
|
|
1846
|
+
verify_plan: r.verify_plan || null,
|
|
1695
1847
|
}, r.mode_override ? { mode: r.mode_override } : {})),
|
|
1696
1848
|
history,
|
|
1697
1849
|
};
|
|
@@ -1733,8 +1885,8 @@ function cmdPlanTable(opts) {
|
|
|
1733
1885
|
let fileMode;
|
|
1734
1886
|
if (raw && !Array.isArray(raw) && typeof raw === 'object' && Array.isArray(raw.steps)) {
|
|
1735
1887
|
if (raw.mode !== undefined && raw.mode !== null) {
|
|
1736
|
-
|
|
1737
|
-
fileMode
|
|
1888
|
+
fileMode = userMode(raw.mode);
|
|
1889
|
+
if (!fileMode) throw new Error(`plan-table: "mode" trong file phải là ${core.MODES.join('|')} (nhận "${raw.mode}")`);
|
|
1738
1890
|
}
|
|
1739
1891
|
raw = raw.steps;
|
|
1740
1892
|
}
|
|
@@ -1751,6 +1903,9 @@ function cmdPlanTable(opts) {
|
|
|
1751
1903
|
if (problems.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${problems.join('; ')} (cần sửa trước khi --approve)\n`);
|
|
1752
1904
|
const typeNotes = planTypeNotes(plan);
|
|
1753
1905
|
if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
|
|
1906
|
+
const splitWarn = planSplitWarnings(plan.steps, base.events);
|
|
1907
|
+
const statCells = agg.analyze(base.events, {}).cells;
|
|
1908
|
+
const tierWarn = planTierWarnings(plan.steps, statCells);
|
|
1754
1909
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1755
1910
|
let modeLogged = false;
|
|
1756
1911
|
if (rec && (rootEv ? rootEv.mode !== plan.mode : plan.mode !== core.DEFAULT_MODE)) {
|
|
@@ -1761,29 +1916,31 @@ function cmdPlanTable(opts) {
|
|
|
1761
1916
|
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
1762
1917
|
if (rec) recordPlanSuggestions(ctx, plan);
|
|
1763
1918
|
if (opts.json) {
|
|
1764
|
-
const
|
|
1919
|
+
const steps = plan.steps.map((r) => {
|
|
1920
|
+
const c = r.action === 'refused' || !r.tier ? null : statCellOf(statCells, r.task_type || r.type, r.tier);
|
|
1921
|
+
return Object.assign({}, r, { stats_cost_p50_usd: c && c.n ? c.cost_p50_usd : null, stats_n: c ? c.n : 0 });
|
|
1922
|
+
});
|
|
1923
|
+
const out = Object.assign({}, plan, { steps, split_warn: splitWarn, tier_warn: tierWarn }, rec ? {
|
|
1765
1924
|
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length, mode_logged: modeLogged },
|
|
1766
|
-
}
|
|
1925
|
+
} : {});
|
|
1767
1926
|
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
1768
1927
|
}
|
|
1769
|
-
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính | Kiểm |', '
|
|
1928
|
+
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính | $p50 | Kiểm |', '|---|---|---|---|---|---|---|'];
|
|
1770
1929
|
for (const r of plan.steps) {
|
|
1771
1930
|
const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
|
|
1772
1931
|
const yn = (v) => (v ? 'y' : 'n');
|
|
1773
1932
|
const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
|
|
1774
1933
|
const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
|
|
1775
1934
|
const est = r.action === 'refused' ? '-' : `~${fmtUsd(r.est_cost_usd)} · ~${fmtNum(r.est_time_min)} phút${r.est_source === 'rough' ? ' (thô)' : ''}`;
|
|
1935
|
+
const sc = r.action === 'refused' || !r.tier ? null : statCellOf(statCells, r.task_type || r.type, r.tier);
|
|
1936
|
+
const p50 = r.action === 'refused' ? '—' : (sc && sc.n ? `${fmtUsd(sc.cost_p50_usd)} (n ${sc.n})` : '-');
|
|
1776
1937
|
const kiem = r.action === 'refused' || !r.verify_plan ? '—' : `${r.verify_plan}${r.mode_override ? ` (mode ${r.mode_override})` : ''}`;
|
|
1777
|
-
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} | ${mdCell(kiem)} |`);
|
|
1938
|
+
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} | ${mdCell(p50)} | ${mdCell(kiem)} |`);
|
|
1778
1939
|
}
|
|
1779
1940
|
lines.push('');
|
|
1780
1941
|
lines.push(`**Tổng chi phí ước tính:** ~${fmtUsd(plan.total_cost_usd)} · **Thời gian (đường găng, chạy song song khi không phụ thuộc):** ~${fmtNum(plan.critical_path_min)} phút (cộng dồn ${fmtNum(plan.sum_time_min)} phút)`);
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const modeText = modeChosen ? plan.mode : (shortSteps >= 4
|
|
1784
|
-
? `HỎI người dùng chọn mode (${shortSteps} bước s≤6): fast | normal | strict (mặc định normal); đặt bằng \`dieu-phoi mode --root ${typeof opts.root === 'string' ? opts.root : '<root_task_id>'} <mode>\``
|
|
1785
|
-
: 'normal (không hỏi)');
|
|
1786
|
-
lines.push(`**Mode:** ${modeText}${plan.self_batch_steps ? ` · Kiểm (fast): bỏ ${plan.self_batch_steps} verifier riêng, 1 verify gộp (cả suite một lần) trước khi trình người dùng` : ''}`);
|
|
1942
|
+
const modeText = modeChosen ? plan.mode : 'normal (mặc định)';
|
|
1943
|
+
lines.push(`**Mode:** ${modeText}${plan.self_batch_steps ? ` · Kiểm (self+batch): bỏ ${plan.self_batch_steps} verifier riêng, 1 verify gộp (test chọn lọc các file cả nhóm; full suite ở cổng phát hành) trước khi trình người dùng` : ''}`);
|
|
1787
1944
|
if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
|
|
1788
1945
|
if (plan.refused.length) {
|
|
1789
1946
|
lines.push('', `**Cảnh báo:** ${plan.refused.length} bước bị từ chối (${plan.refused.map((x) => x.step).join(', ')}), không tính vào tổng — sửa rồi chạy lại.`);
|
|
@@ -1791,6 +1948,13 @@ function cmdPlanTable(opts) {
|
|
|
1791
1948
|
`);
|
|
1792
1949
|
}
|
|
1793
1950
|
if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
|
|
1951
|
+
for (const w of splitWarn) {
|
|
1952
|
+
lines.push('', `TÁCH?: ${w.step} — ${w.task_type} ${w.tier} handoff ${Math.round(w.handoff_rate * 100)}% (${w.n}), median ${fmtUsd(w.cost_handoff_p50_usd)} có handoff / ${fmtUsd(w.cost_clean_p50_usd)} không → cân nhắc tách thành ≥2 bước nhỏ hơn`);
|
|
1953
|
+
}
|
|
1954
|
+
const fmtPct = (v) => (v === null || v === undefined ? '-' : `${Math.round(v * 100)}%`);
|
|
1955
|
+
for (const w of tierWarn) {
|
|
1956
|
+
lines.push('', `TIER↑: ${w.step} ${w.tier} vs đề xuất ${w.suggested_tier} ($p50 ${fmtUsd(w.cost_p50_usd)} vs ${fmtUsd(w.suggested_cost_p50_usd)}, accept ${fmtPct(w.accept_rate)} vs ${fmtPct(w.suggested_accept_rate)}) — tier cao hơn choose đề xuất, kiểm lý do trước khi duyệt`);
|
|
1957
|
+
}
|
|
1794
1958
|
if (rec) lines.push('', `Đã ghi kế hoạch đã duyệt (root ${rec.root_task_id}, lần duyệt ${rec.approval})${ovs.length ? `; ghi ${ovs.length} override` : ''}${modeLogged ? `; ghi mode ${plan.mode}` : ''}`);
|
|
1795
1959
|
return { code: 0, out: lines.join('\n') };
|
|
1796
1960
|
}
|