@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/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
|
}
|
|
@@ -980,6 +981,28 @@ function liveLine(live) {
|
|
|
980
981
|
return `live: phiên ${part(live.session || 0, lim.session)} · máy ${part(live.machine, lim.machine)}${tasks ? ` — ${tasks}` : ''}`;
|
|
981
982
|
}
|
|
982
983
|
|
|
984
|
+
/**
|
|
985
|
+
* core 0.7.4 (S1): steps of an approved plan (last 7 days) that run self+batch, whose exec has completed, and that have
|
|
986
|
+
* neither a batch verifier verdict (reason_code batch-verify) nor a user verdict yet. Gate 3 must not see them unchecked.
|
|
987
|
+
*/
|
|
988
|
+
function batchPendingOf(ctx, events, now) {
|
|
989
|
+
let files = [];
|
|
990
|
+
try { files = fs.readdirSync(ctx.paths.plans).filter((f) => f.endsWith('.json')); } catch (_) { return []; }
|
|
991
|
+
const out = [];
|
|
992
|
+
for (const f of files) {
|
|
993
|
+
const rec = core.readJsonFile(path.join(ctx.paths.plans, f), null);
|
|
994
|
+
if (!rec || !Array.isArray(rec.steps)) continue;
|
|
995
|
+
const fresh = Date.parse(rec.approved_at) > now - 7 * 24 * 3600 * 1000;
|
|
996
|
+
if (!fresh) continue;
|
|
997
|
+
const pending = rec.steps.filter((s) => s && s.verify_plan === 'self+batch' && s.task_id
|
|
998
|
+
&& events.some((e) => e.kind === 'complete' && e.task_id === s.task_id)
|
|
999
|
+
&& !events.some((e) => e.kind === 'verdict' && e.task_id === s.task_id
|
|
1000
|
+
&& (String(e.reason_code || '').startsWith('batch-verify') || e.by === 'user')));
|
|
1001
|
+
if (pending.length) out.push({ root_task_id: rec.root_task_id || f.replace(/\.json$/, ''), steps: pending.map((s) => s.id) });
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
983
1006
|
function cmdState(opts) {
|
|
984
1007
|
const ctx = ctxOf(opts);
|
|
985
1008
|
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
@@ -998,17 +1021,19 @@ function cmdState(opts) {
|
|
|
998
1021
|
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
999
1022
|
const live = core.liveSummary(ctx, sid); // core 0.7.3 (s1)
|
|
1000
1023
|
const stuck = core.stuckAgents(ctx, { sessionId: sid }); // s6
|
|
1024
|
+
const batchPending = batchPendingOf(ctx, core.readEvents(ctx), Date.now()); // core 0.7.4 (S1)
|
|
1001
1025
|
if (opts.json) {
|
|
1002
1026
|
const liveJ = {
|
|
1003
1027
|
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
1028
|
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
1029
|
};
|
|
1006
|
-
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }, { live: liveJ }), null, 2) };
|
|
1030
|
+
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
1031
|
}
|
|
1032
|
+
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
1033
|
const stuckLines = stuck.length
|
|
1009
1034
|
? [`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
1035
|
: [];
|
|
1011
|
-
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`], [liveLine(live)], stuckLines).join('\n') };
|
|
1036
|
+
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
1037
|
}
|
|
1013
1038
|
|
|
1014
1039
|
// ------------------------------------------------------------------ reap (core 0.7.3 s6)
|
|
@@ -1390,11 +1415,24 @@ function cmdRetro(opts) {
|
|
|
1390
1415
|
|
|
1391
1416
|
// ------------------------------------------------------------------ choose
|
|
1392
1417
|
|
|
1418
|
+
/**
|
|
1419
|
+
* A user-supplied mode value: fast | normal; the removed `strict` (0.7.4) is accepted with a warning and becomes
|
|
1420
|
+
* normal; anything else null.
|
|
1421
|
+
*/
|
|
1422
|
+
function userMode(raw) {
|
|
1423
|
+
if (raw === core.LEGACY_MODE) {
|
|
1424
|
+
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');
|
|
1425
|
+
return core.DEFAULT_MODE;
|
|
1426
|
+
}
|
|
1427
|
+
return core.isMode(raw) ? raw : null;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1393
1430
|
/** --mode value (validated) or undefined; a bare --mode is an error. */
|
|
1394
1431
|
function modeOpt(opts) {
|
|
1395
1432
|
if (opts.mode === undefined) return undefined;
|
|
1396
|
-
|
|
1397
|
-
|
|
1433
|
+
const m = typeof opts.mode === 'string' ? userMode(opts.mode) : null;
|
|
1434
|
+
if (!m) throw new Error(`--mode phải là ${core.MODES.join('|')}`);
|
|
1435
|
+
return m;
|
|
1398
1436
|
}
|
|
1399
1437
|
|
|
1400
1438
|
function chooseInput(opts, ctx, events) {
|
|
@@ -1432,15 +1470,39 @@ function overrideEvent(ctx, taskId, taskType, suggested, chosen) {
|
|
|
1432
1470
|
}
|
|
1433
1471
|
|
|
1434
1472
|
/**
|
|
1435
|
-
* core 0.7.
|
|
1436
|
-
*
|
|
1473
|
+
* core 0.7.4 (B1): the step of the approved plan (<plans>/<root>.json) whose task_id is taskId, or null. Its rubric
|
|
1474
|
+
* (score, verifiable, reversible, publish, dependents) is what `choose --task` takes the stricter of.
|
|
1475
|
+
*/
|
|
1476
|
+
function approvedPlanStep(ctx, taskId) {
|
|
1477
|
+
let files = [];
|
|
1478
|
+
try { files = fs.readdirSync(ctx.paths.plans).filter((f) => f.endsWith('.json')); } catch (_) { return null; }
|
|
1479
|
+
for (const f of files) {
|
|
1480
|
+
const rec = core.readJsonFile(path.join(ctx.paths.plans, f), null);
|
|
1481
|
+
if (!rec || !Array.isArray(rec.steps)) continue;
|
|
1482
|
+
const step = rec.steps.find((s) => s && s.task_id === taskId);
|
|
1483
|
+
if (!step) continue;
|
|
1484
|
+
const dependents = step.dependents === true
|
|
1485
|
+
|| rec.steps.some((o) => o && o !== step && Array.isArray(o.depends_on) && o.depends_on.includes(step.id));
|
|
1486
|
+
return Object.assign({}, step, { root: rec.root_task_id || f.replace(/\.json$/, ''), dependents });
|
|
1487
|
+
}
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* core 0.7.4 (B1): the stricter of the CLI flags and the approved step, field by field — publish, dependents, not
|
|
1493
|
+
* reversible, not verifiable, higher score. Mutates `input`; returns the names it raised (empty = flags already as strict).
|
|
1494
|
+
* Omitting a flag can therefore never loosen a step the plan made blocking.
|
|
1437
1495
|
*/
|
|
1438
|
-
function
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1496
|
+
function applyApprovedStep(input, step) {
|
|
1497
|
+
const raised = [];
|
|
1498
|
+
if (step.publish === true && input.publish !== true) { raised.push('publish'); input.publish = true; }
|
|
1499
|
+
if (step.dependents === true && input.dependents !== true) { raised.push('dependents'); input.dependents = true; }
|
|
1500
|
+
if (step.reversible === false && input.reversible !== false) { raised.push('reversible'); input.reversible = false; }
|
|
1501
|
+
if (step.verifiable === false && input.verifiable !== false) { raised.push('verifiable'); input.verifiable = false; }
|
|
1502
|
+
const planScore = Number(step.score);
|
|
1503
|
+
const flagScore = Number(input.score);
|
|
1504
|
+
if (Number.isFinite(planScore) && Number.isFinite(flagScore) && planScore > flagScore) { raised.push('score'); input.score = planScore; }
|
|
1505
|
+
return raised;
|
|
1444
1506
|
}
|
|
1445
1507
|
|
|
1446
1508
|
function cmdChoose(opts) {
|
|
@@ -1450,6 +1512,8 @@ function cmdChoose(opts) {
|
|
|
1450
1512
|
}
|
|
1451
1513
|
if (opts.task !== undefined) requireTaskId(typeof opts.task === 'string' ? opts.task : '');
|
|
1452
1514
|
const input = chooseInput(opts, ctx, core.readEvents(ctx));
|
|
1515
|
+
const planStep = input.taskId ? approvedPlanStep(ctx, input.taskId) : null;
|
|
1516
|
+
const raised = planStep ? applyApprovedStep(input, planStep) : [];
|
|
1453
1517
|
let modeSource = 'flag';
|
|
1454
1518
|
const mi = input.taskId ? core.taskModeInfo(ctx, input.taskId, input.events) : null;
|
|
1455
1519
|
if (input.mode === undefined) {
|
|
@@ -1463,7 +1527,12 @@ function cmdChoose(opts) {
|
|
|
1463
1527
|
}
|
|
1464
1528
|
const res = chooser.choose(input);
|
|
1465
1529
|
res.mode_source = modeSource;
|
|
1466
|
-
if (
|
|
1530
|
+
if (planStep) {
|
|
1531
|
+
res.plan_step = { root_task_id: planStep.root, step: planStep.id, verify_plan: planStep.verify_plan || null, raised };
|
|
1532
|
+
if (planStep.verify_plan && res.verify_plan && planStep.verify_plan !== res.verify_plan) {
|
|
1533
|
+
res.plan_deviation = `${planStep.verify_plan} → ${res.verify_plan}`;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1467
1536
|
const orch = measureOrch(opts, ctx, input.profile);
|
|
1468
1537
|
res.orch_ctx_tokens = orch.ctx_tokens;
|
|
1469
1538
|
res.orch_ctx_growth = orch.ctx_growth;
|
|
@@ -1485,8 +1554,10 @@ function cmdChoose(opts) {
|
|
|
1485
1554
|
}
|
|
1486
1555
|
if (opts.json) return { code: 0, out: JSON.stringify(res) };
|
|
1487
1556
|
const typeWarn = res.type_warning && !String(res.reason).includes(res.type_warning) ? `\nCẢNH BÁO: ${res.type_warning}` : '';
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1557
|
+
const planWarn = res.plan_step && res.plan_step.raised.length
|
|
1558
|
+
? `\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(', ')}` : '';
|
|
1559
|
+
const deviationWarn = res.plan_deviation ? `\nLỆCH kiểm khỏi kế hoạch đã duyệt: ${res.plan_deviation}` : '';
|
|
1560
|
+
const warn = `${typeWarn}${planWarn}${deviationWarn}${res.orch_ctx_warn ? `\nCẢNH BÁO: ${orch.advice}` : ''}`;
|
|
1490
1561
|
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
1562
|
if (res.action === 'ask_user') {
|
|
1492
1563
|
return { code: 0, out: `HỎI NGƯỜI DÙNG — ${res.reason}${warn}` };
|
|
@@ -1541,7 +1612,7 @@ function buildPlanTable(rawSteps, base) {
|
|
|
1541
1612
|
verifiable: s.verifiable === undefined ? true : yesNo(s.verifiable, true),
|
|
1542
1613
|
reversible: s.reversible === undefined ? true : yesNo(s.reversible, true),
|
|
1543
1614
|
publish: s.publish === undefined ? false : yesNo(s.publish, false),
|
|
1544
|
-
mode: s.mode === undefined || s.mode === null ? null : s.mode,
|
|
1615
|
+
mode: s.mode === undefined || s.mode === null ? null : (userMode(s.mode) || s.mode),
|
|
1545
1616
|
urgency: s.urgency,
|
|
1546
1617
|
deps: deps.map(String),
|
|
1547
1618
|
};
|
|
@@ -1570,6 +1641,8 @@ function buildPlanTable(rawSteps, base) {
|
|
|
1570
1641
|
const head = {
|
|
1571
1642
|
step: s.step, title: s.title, type: s.type, task_id: s.task || null, score: Number(s.score),
|
|
1572
1643
|
verifiable: s.verifiable, reversible: s.reversible, depends_on: s.deps,
|
|
1644
|
+
publish: s.publish === true || base.publish === true,
|
|
1645
|
+
dependents: steps.some((o) => o !== s && o.deps.includes(s.step)),
|
|
1573
1646
|
mode: s.mode || base.mode || core.DEFAULT_MODE, mode_override: s.mode || null,
|
|
1574
1647
|
};
|
|
1575
1648
|
try {
|
|
@@ -1635,6 +1708,78 @@ function planTaskIdProblems(plan, root) {
|
|
|
1635
1708
|
return problems;
|
|
1636
1709
|
}
|
|
1637
1710
|
|
|
1711
|
+
// 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%
|
|
1712
|
+
// over ≥ 10 tasks) is flagged in the table so the planner splits it. Advice only: never blocks --approve.
|
|
1713
|
+
const SPLIT_MIN_SCORE = 6;
|
|
1714
|
+
const SPLIT_MIN_RATE = 0.2;
|
|
1715
|
+
const SPLIT_MIN_N = 10;
|
|
1716
|
+
|
|
1717
|
+
/** Model family of a tier or a recorded model id (opus | sonnet | haiku | fable), as choose.cjs matches cells. */
|
|
1718
|
+
function modelFamily(m) {
|
|
1719
|
+
const s = String(m || '').toLowerCase();
|
|
1720
|
+
return ['opus', 'sonnet', 'haiku', 'fable'].find((k) => s.includes(k)) || null;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function planSplitWarnings(steps, events) {
|
|
1724
|
+
const samples = agg.handoffSamples(events);
|
|
1725
|
+
const out = [];
|
|
1726
|
+
for (const r of steps) {
|
|
1727
|
+
if (r.action === 'refused' || !r.tier || !(Number(r.score) >= SPLIT_MIN_SCORE)) continue;
|
|
1728
|
+
const type = r.task_type || r.type;
|
|
1729
|
+
const [model, effort] = String(r.tier).split('/');
|
|
1730
|
+
const same = samples.filter((s) => s.task_type === type && modelFamily(s.model) === modelFamily(model) && s.effort === effort);
|
|
1731
|
+
if (same.length < SPLIT_MIN_N) continue;
|
|
1732
|
+
const hand = same.filter((s) => s.handoff);
|
|
1733
|
+
const rate = hand.length / same.length;
|
|
1734
|
+
if (rate < SPLIT_MIN_RATE) continue;
|
|
1735
|
+
const clean = same.filter((s) => !s.handoff);
|
|
1736
|
+
out.push({
|
|
1737
|
+
step: r.step,
|
|
1738
|
+
task_type: type,
|
|
1739
|
+
tier: r.tier,
|
|
1740
|
+
n: same.length,
|
|
1741
|
+
handoff_rate: Math.round(rate * 1000) / 1000,
|
|
1742
|
+
cost_handoff_p50_usd: agg.percentile(hand.map((s) => s.total_cost), 0.5),
|
|
1743
|
+
cost_clean_p50_usd: agg.percentile(clean.map((s) => s.total_cost), 0.5),
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return out;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// core 0.7.4 (s2): TIER↑ — a mechanical type whose override sits above the tier choose would pick is flagged
|
|
1750
|
+
// (advice only; --approve is not blocked). The $p50 column and the warning read the same stats cells as `stats`.
|
|
1751
|
+
const TIER_UP_TYPES = ['mechanical-edit', 'extract', 'search', 'test-write'];
|
|
1752
|
+
|
|
1753
|
+
/** The stats cell (buildCells row) of one task_type × tier, or null. */
|
|
1754
|
+
function statCellOf(cells, type, tier) {
|
|
1755
|
+
const [model, effort] = String(tier).split('/');
|
|
1756
|
+
return cells.find((c) => c.task_type === type && modelFamily(c.model) === modelFamily(model) && c.effort === effort) || null;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function planTierWarnings(steps, cells) {
|
|
1760
|
+
const out = [];
|
|
1761
|
+
for (const r of steps) {
|
|
1762
|
+
if (r.action === 'refused' || !r.overridden || !r.suggested_tier || !r.tier) continue;
|
|
1763
|
+
if (!TIER_UP_TYPES.includes(r.task_type)) continue;
|
|
1764
|
+
const hi = chooser.parseTier(r.tier);
|
|
1765
|
+
const lo = chooser.parseTier(r.suggested_tier);
|
|
1766
|
+
if (hi === null || lo === null || hi <= lo) continue;
|
|
1767
|
+
const a = statCellOf(cells, r.task_type, r.tier);
|
|
1768
|
+
const b = statCellOf(cells, r.task_type, r.suggested_tier);
|
|
1769
|
+
out.push({
|
|
1770
|
+
step: r.step,
|
|
1771
|
+
task_type: r.task_type,
|
|
1772
|
+
tier: r.tier,
|
|
1773
|
+
suggested_tier: r.suggested_tier,
|
|
1774
|
+
cost_p50_usd: a && a.n ? a.cost_p50_usd : null,
|
|
1775
|
+
suggested_cost_p50_usd: b && b.n ? b.cost_p50_usd : null,
|
|
1776
|
+
accept_rate: a && a.n ? a.accept_rate : null,
|
|
1777
|
+
suggested_accept_rate: b && b.n ? b.accept_rate : null,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
return out;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1638
1783
|
/** Mapped ad-hoc labels (bug-fix → debug, …): not an error, but the tag must carry the mapped type. */
|
|
1639
1784
|
function planTypeNotes(plan) {
|
|
1640
1785
|
return plan.steps.filter((r) => r.mapped_from).map((r) => `bước ${r.step}: ${r.type_warning}`);
|
|
@@ -1692,6 +1837,10 @@ function recordApprovedPlan(ctx, root, stepsFile, plan) {
|
|
|
1692
1837
|
steps: plan.steps.map((r) => Object.assign({
|
|
1693
1838
|
id: r.step, task_id: r.task_id || null, title: r.title === undefined ? null : r.title, type: r.task_type || r.type || null,
|
|
1694
1839
|
tier: r.action === 'refused' ? null : (r.tier || null), depends_on: r.depends_on,
|
|
1840
|
+
// core 0.7.4 (B1): the rubric `choose --task` takes the stricter of its flags against
|
|
1841
|
+
score: Number.isFinite(Number(r.score)) ? Number(r.score) : null, verifiable: r.verifiable !== false,
|
|
1842
|
+
reversible: r.reversible !== false, publish: r.publish === true, dependents: r.dependents === true,
|
|
1843
|
+
verify_plan: r.verify_plan || null,
|
|
1695
1844
|
}, r.mode_override ? { mode: r.mode_override } : {})),
|
|
1696
1845
|
history,
|
|
1697
1846
|
};
|
|
@@ -1733,8 +1882,8 @@ function cmdPlanTable(opts) {
|
|
|
1733
1882
|
let fileMode;
|
|
1734
1883
|
if (raw && !Array.isArray(raw) && typeof raw === 'object' && Array.isArray(raw.steps)) {
|
|
1735
1884
|
if (raw.mode !== undefined && raw.mode !== null) {
|
|
1736
|
-
|
|
1737
|
-
fileMode
|
|
1885
|
+
fileMode = userMode(raw.mode);
|
|
1886
|
+
if (!fileMode) throw new Error(`plan-table: "mode" trong file phải là ${core.MODES.join('|')} (nhận "${raw.mode}")`);
|
|
1738
1887
|
}
|
|
1739
1888
|
raw = raw.steps;
|
|
1740
1889
|
}
|
|
@@ -1751,6 +1900,9 @@ function cmdPlanTable(opts) {
|
|
|
1751
1900
|
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
1901
|
const typeNotes = planTypeNotes(plan);
|
|
1753
1902
|
if (typeNotes.length) process.stderr.write(`dieu-phoi plan-table: cảnh báo — ${typeNotes.join('; ')}\n`);
|
|
1903
|
+
const splitWarn = planSplitWarnings(plan.steps, base.events);
|
|
1904
|
+
const statCells = agg.analyze(base.events, {}).cells;
|
|
1905
|
+
const tierWarn = planTierWarnings(plan.steps, statCells);
|
|
1754
1906
|
const rec = opts.approve ? recordApprovedPlan(ctx, String(opts.root), path.resolve(opts.file), plan) : null;
|
|
1755
1907
|
let modeLogged = false;
|
|
1756
1908
|
if (rec && (rootEv ? rootEv.mode !== plan.mode : plan.mode !== core.DEFAULT_MODE)) {
|
|
@@ -1761,29 +1913,31 @@ function cmdPlanTable(opts) {
|
|
|
1761
1913
|
for (const ev of ovs) core.appendEvent(ctx, ev);
|
|
1762
1914
|
if (rec) recordPlanSuggestions(ctx, plan);
|
|
1763
1915
|
if (opts.json) {
|
|
1764
|
-
const
|
|
1916
|
+
const steps = plan.steps.map((r) => {
|
|
1917
|
+
const c = r.action === 'refused' || !r.tier ? null : statCellOf(statCells, r.task_type || r.type, r.tier);
|
|
1918
|
+
return Object.assign({}, r, { stats_cost_p50_usd: c && c.n ? c.cost_p50_usd : null, stats_n: c ? c.n : 0 });
|
|
1919
|
+
});
|
|
1920
|
+
const out = Object.assign({}, plan, { steps, split_warn: splitWarn, tier_warn: tierWarn }, rec ? {
|
|
1765
1921
|
approved: { root_task_id: rec.root_task_id, approval: rec.approval, record: rec.file, overrides_logged: ovs.length, mode_logged: modeLogged },
|
|
1766
|
-
}
|
|
1922
|
+
} : {});
|
|
1767
1923
|
return { code: 0, out: JSON.stringify(out, null, 2) };
|
|
1768
1924
|
}
|
|
1769
|
-
const lines = ['| Bước | Model/effort | Rubric (điểm · kiểm chứng · đảo ngược) | Lý do | Ước tính | Kiểm |', '
|
|
1925
|
+
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
1926
|
for (const r of plan.steps) {
|
|
1771
1927
|
const tierCell = r.action === 'refused' ? '—' : (r.action === 'ask_user' ? `${r.tier} (hỏi người dùng)` : r.tier);
|
|
1772
1928
|
const yn = (v) => (v ? 'y' : 'n');
|
|
1773
1929
|
const rubric = `${Number.isFinite(r.score) ? r.score : '?'} · ${yn(r.verifiable)} · ${yn(r.reversible)}`;
|
|
1774
1930
|
const why = `${r.type}${r.depends_on.length ? `, sau ${r.depends_on.join(', ')}` : ''} — ${r.reason.replace(/; profile: .*$/, '')}`;
|
|
1775
1931
|
const est = r.action === 'refused' ? '-' : `~${fmtUsd(r.est_cost_usd)} · ~${fmtNum(r.est_time_min)} phút${r.est_source === 'rough' ? ' (thô)' : ''}`;
|
|
1932
|
+
const sc = r.action === 'refused' || !r.tier ? null : statCellOf(statCells, r.task_type || r.type, r.tier);
|
|
1933
|
+
const p50 = r.action === 'refused' ? '—' : (sc && sc.n ? `${fmtUsd(sc.cost_p50_usd)} (n ${sc.n})` : '-');
|
|
1776
1934
|
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)} |`);
|
|
1935
|
+
lines.push(`| ${mdCell(r.title ? `${r.step}. ${r.title}` : r.step)} | ${mdCell(tierCell)} | ${mdCell(rubric)} | ${mdCell(why)} | ${mdCell(est)} | ${mdCell(p50)} | ${mdCell(kiem)} |`);
|
|
1778
1936
|
}
|
|
1779
1937
|
lines.push('');
|
|
1780
1938
|
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` : ''}`);
|
|
1939
|
+
const modeText = modeChosen ? plan.mode : 'normal (mặc định)';
|
|
1940
|
+
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
1941
|
if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
|
|
1788
1942
|
if (plan.refused.length) {
|
|
1789
1943
|
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 +1945,13 @@ function cmdPlanTable(opts) {
|
|
|
1791
1945
|
`);
|
|
1792
1946
|
}
|
|
1793
1947
|
if (typeNotes.length) lines.push('', `**Loại task:** ${typeNotes.join('; ')}`);
|
|
1948
|
+
for (const w of splitWarn) {
|
|
1949
|
+
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`);
|
|
1950
|
+
}
|
|
1951
|
+
const fmtPct = (v) => (v === null || v === undefined ? '-' : `${Math.round(v * 100)}%`);
|
|
1952
|
+
for (const w of tierWarn) {
|
|
1953
|
+
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`);
|
|
1954
|
+
}
|
|
1794
1955
|
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
1956
|
return { code: 0, out: lines.join('\n') };
|
|
1796
1957
|
}
|
package/lib/knowledge.cjs
CHANGED
|
@@ -40,7 +40,9 @@ const KNOWLEDGE_HELP = `Cách dùng: dieu-phoi knowledge <lệnh> [--repo <đư
|
|
|
40
40
|
thư mục). Không --from thì tự lấy kho của khoá git-common-dir nếu có
|
|
41
41
|
list [--follow] [--dirty] [--json]
|
|
42
42
|
các mục của kho (id, tiêu đề, số dòng, sha#rev, tags, paths) + repo liên quan (related),
|
|
43
|
-
mỗi mục gắn ok / STALE / ? (độ tươi so với HEAD của worktree đang đứng
|
|
43
|
+
mỗi mục gắn ok / STALE / drift / ? (độ tươi so với HEAD của worktree đang đứng; drift = chỉ file
|
|
44
|
+
file đổi nhưng không làm mục stale: glob, bản version, CHANGELOG, dòng không nêu tên mục);
|
|
45
|
+
cuối dòng tổng: số mục không stale
|
|
44
46
|
status [--follow] [--dirty] [--no-cache] [--json]
|
|
45
47
|
chỉ độ tươi: mục nào stale (file nào đổi sau sha của mục), mục nào không xác định (sha không
|
|
46
48
|
có trong repo này…). Một tiến trình git (diff-tree --no-renames, pathspec = paths của các mục),
|
|
@@ -60,6 +62,8 @@ const KNOWLEDGE_HELP = `Cách dùng: dieu-phoi knowledge <lệnh> [--repo <đư
|
|
|
60
62
|
"## +id" thêm mục (bắt buộc paths:), "## id" thay mục (bắt buộc base: <sha>#<rev>); base cũ
|
|
61
63
|
→ xung đột, không ghi đè; mục > 40 dòng bị từ chối. Có xung đột/từ chối → exit 2.
|
|
62
64
|
Idempotent: file delta đã gộp (hash trong meta.json) và mục trùng một rev cũ trong history/ → "không đổi"
|
|
65
|
+
review <id> [--repo] đánh dấu mục đã xem xét: reviewed_sha = HEAD hiện tại (ghi dưới khoá kho); độ tươi chỉ tính thay đổi sau sha đó.
|
|
66
|
+
Mục bị thay bằng merge sẽ mất dấu này. Không đổi nội dung mục.
|
|
63
67
|
bootstrap [--children <mẫu tên>[,…]] [--json]
|
|
64
68
|
chỉ đọc: liệt kê nguồn để seed kho (CLAUDE.md/README/AGENTS.md của repo, context-r/contract/
|
|
65
69
|
handoff cũ trong runs có nhắc repo), mục đã có, mục nên có và mẫu delta. --children: repo anh em
|
|
@@ -272,7 +276,10 @@ function sectionOut(repo, sec, body, fr) {
|
|
|
272
276
|
const f = fr && fr.sections && fr.sections[sec.id];
|
|
273
277
|
if (f) {
|
|
274
278
|
out.fresh = f.state;
|
|
275
|
-
if (f.files) {
|
|
279
|
+
if (f.files) {
|
|
280
|
+
const k = f.state === 'drift' ? 'drift' : 'stale';
|
|
281
|
+
out[k] = f.files; out[`${k}_count`] = f.n_files || f.files.length;
|
|
282
|
+
}
|
|
276
283
|
if (f.why) out.fresh_why = f.why;
|
|
277
284
|
if (f.dirty) out.dirty = f.dirty;
|
|
278
285
|
}
|
|
@@ -299,6 +306,7 @@ function freshnessOf(storeDir, idx, start, opts) {
|
|
|
299
306
|
|
|
300
307
|
function freshLine(s) {
|
|
301
308
|
if (s.fresh === 'stale') return `STALE — ${s.stale_count} file đổi sau sha của mục: ${core.shortFileList(s.stale, 5)} (kiểm lại trước khi dùng)`;
|
|
309
|
+
if (s.fresh === 'drift') return `drift — ${s.drift_count} file đổi nhưng không tính stale: ${core.shortFileList(s.drift, 5)}`;
|
|
302
310
|
if (s.fresh === 'unknown') return `độ tươi: không xác định — ${s.fresh_why}`;
|
|
303
311
|
if (s.fresh === 'fresh') return 'độ tươi: tươi';
|
|
304
312
|
return null;
|
|
@@ -415,7 +423,7 @@ function cmdGet(opts) {
|
|
|
415
423
|
return { code, out: parts.join('\n\n') };
|
|
416
424
|
}
|
|
417
425
|
|
|
418
|
-
const FRESH_MARK = { fresh: 'ok ', stale: 'STALE', unknown: '? ' };
|
|
426
|
+
const FRESH_MARK = { fresh: 'ok ', stale: 'STALE', drift: 'drift', unknown: '? ' };
|
|
419
427
|
|
|
420
428
|
function listLines(idx, fr, dir) {
|
|
421
429
|
return idx.sections.map((s) => {
|
|
@@ -429,7 +437,8 @@ function listLines(idx, fr, dir) {
|
|
|
429
437
|
function freshHeader(fr, n) {
|
|
430
438
|
if (!fr) return '';
|
|
431
439
|
const head = fr.head ? `HEAD ${fr.head.slice(0, 7)}${fr.branch ? ` (${fr.branch})` : ''}` : 'HEAD ?';
|
|
432
|
-
|
|
440
|
+
const notStale = n - ((fr.counts && fr.counts.stale) || 0);
|
|
441
|
+
return `${head} · ${core.knowledgeSummary(fr, n)} · ${notStale} không stale · ${fr.cached ? 'cache' : `tính mới${fr.ms_git ? ` (git ${fr.ms_git} ms)` : ''}`}${fr.error ? ` · git lỗi: ${fr.error}` : ''}`;
|
|
433
442
|
}
|
|
434
443
|
|
|
435
444
|
/** Details under list/status: stale files, unknown reasons, uncommitted files (--dirty). */
|
|
@@ -440,6 +449,7 @@ function freshDetails(idx, fr) {
|
|
|
440
449
|
const f = fr.sections[s.id];
|
|
441
450
|
if (!f) continue;
|
|
442
451
|
if (f.state === 'stale') out.push(` ${s.id}: ${f.n_files || f.files.length} file đổi — ${core.shortFileList(f.files, 5)}`);
|
|
452
|
+
else if (f.state === 'drift') out.push(` ${s.id}: drift (không tính stale) — ${f.n_files || f.files.length} file đổi — ${core.shortFileList(f.files, 5)}`);
|
|
443
453
|
else if (f.state === 'unknown') out.push(` ${s.id}: không xác định — ${f.why}`);
|
|
444
454
|
if (f.dirty) out.push(` ${s.id}: chưa commit — ${core.shortFileList(f.dirty, 5)}`);
|
|
445
455
|
}
|
|
@@ -1035,6 +1045,38 @@ function cmdLink(opts) {
|
|
|
1035
1045
|
return { code: 0, out: `${verb}: ${other.name} là ${opts.rel === 'child' ? 'con' : 'cha'} của ${info.name}${a.changed || b.changed ? '' : ' (không đổi)'}.` };
|
|
1036
1046
|
}
|
|
1037
1047
|
|
|
1048
|
+
/**
|
|
1049
|
+
* Review mark (s6): records reviewed_sha = HEAD of the checkout around --repo on one section, under the store lock.
|
|
1050
|
+
* freshness then counts only the changes after that sha. Body, paths and base are untouched; a merge that replaces the
|
|
1051
|
+
* section writes a new entry without the mark, so new content is never shown as reviewed.
|
|
1052
|
+
*/
|
|
1053
|
+
function cmdReview(opts) {
|
|
1054
|
+
const { start, info } = infoOf(opts, false);
|
|
1055
|
+
if (!info) return notRepo(start);
|
|
1056
|
+
const ids = opts._.map(String).filter((t) => t.trim());
|
|
1057
|
+
if (ids.length !== 1 || !ID_RE.test(ids[0])) return { code: 1, out: `Cần đúng một <id> mục.\n\n${KNOWLEDGE_HELP.trimEnd()}` };
|
|
1058
|
+
const id = ids[0];
|
|
1059
|
+
const g = core.findGit(start);
|
|
1060
|
+
const h = g ? core.readHead(g) : null;
|
|
1061
|
+
const sha = h && h.sha ? h.sha : (h && h.unsupported ? headShaOf(start) : null);
|
|
1062
|
+
if (!sha || !/^[0-9a-f]{40,64}$/i.test(sha)) return { code: 1, out: 'Không đọc được HEAD đầy đủ của repo (chưa có commit?).' };
|
|
1063
|
+
const ri = readIndex(info.dir);
|
|
1064
|
+
if (ri.error) return { code: 1, out: ri.error };
|
|
1065
|
+
if (!ri.exists) return { code: 1, out: `Kho chưa có mục nào: ${info.dir}.` };
|
|
1066
|
+
const lk = withLock(path.join(info.dir, 'knowledge.lock'), () => {
|
|
1067
|
+
const cur = readIndex(info.dir);
|
|
1068
|
+
if (cur.error) return { code: 1, out: cur.error };
|
|
1069
|
+
const at = cur.idx.sections.findIndex((s) => s.id === id);
|
|
1070
|
+
if (at < 0) return { code: 1, out: `Chưa có mục ${id} trong kho ${info.name}.` };
|
|
1071
|
+
const prev = cur.idx.sections[at].reviewed_sha || null;
|
|
1072
|
+
cur.idx.sections[at] = Object.assign({}, cur.idx.sections[at], { reviewed_sha: sha.toLowerCase(), reviewed_at: new Date().toISOString() });
|
|
1073
|
+
core.writeFileAtomic(path.join(info.dir, 'index.json'), `${JSON.stringify(cur.idx, null, 2)}\n`);
|
|
1074
|
+
return { code: 0, out: `review ${id}: reviewed_sha ${shortSha(sha)}${prev ? ` (trước: ${shortSha(prev)})` : ''}` };
|
|
1075
|
+
}, LOCK_TRIES);
|
|
1076
|
+
if (lk.locked) return { code: 1, out: 'Kho đang bị khoá (knowledge.lock), thử lại.' };
|
|
1077
|
+
return lk.value;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1038
1080
|
function cmdKnowledge(opts) {
|
|
1039
1081
|
const sub = opts._.shift();
|
|
1040
1082
|
if (!sub || opts.help || opts.h || sub === 'help') {
|
|
@@ -1045,6 +1087,7 @@ function cmdKnowledge(opts) {
|
|
|
1045
1087
|
if (sub === 'get') return cmdGet(opts);
|
|
1046
1088
|
if (sub === 'list') return cmdList(opts);
|
|
1047
1089
|
if (sub === 'status') return cmdStatus(opts);
|
|
1090
|
+
if (sub === 'review') return cmdReview(opts);
|
|
1048
1091
|
if (sub === 'merge') return cmdMerge(opts);
|
|
1049
1092
|
if (sub === 'link') return cmdLink(opts);
|
|
1050
1093
|
if (sub === 'bootstrap') return cmdBootstrap(opts);
|