@mjasnikovs/pi-task 0.37.7 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shared/child-output.d.ts +19 -3
- package/dist/shared/child-output.js +21 -5
- package/dist/shared/git-runner.d.ts +39 -0
- package/dist/shared/git-runner.js +38 -0
- package/dist/task/accept-debt.d.ts +27 -58
- package/dist/task/accept-debt.js +60 -130
- package/dist/task/auto-orchestrator.d.ts +7 -57
- package/dist/task/auto-orchestrator.js +25 -499
- package/dist/task/child-runner.d.ts +2 -0
- package/dist/task/child-runner.js +74 -70
- package/dist/task/enforce-guidelines.d.ts +1 -1
- package/dist/task/enforce-guidelines.js +2 -2
- package/dist/task/external-context.d.ts +85 -7
- package/dist/task/external-context.js +100 -63
- package/dist/task/file-inventory.js +22 -41
- package/dist/task/final-gate.d.ts +80 -0
- package/dist/task/final-gate.js +102 -49
- package/dist/task/gate-deps.js +6 -23
- package/dist/task/git-state-guard.d.ts +1 -1
- package/dist/task/git-state-guard.js +1 -7
- package/dist/task/phases.js +40 -83
- package/dist/task/run-final-gate.d.ts +127 -0
- package/dist/task/run-final-gate.js +492 -0
- package/dist/task/task-gates.d.ts +20 -57
- package/dist/task/task-gates.js +11 -11
- package/dist/task/verify-work.d.ts +40 -32
- package/dist/task/verify-work.js +301 -241
- package/dist/workers/docs-core.d.ts +14 -0
- package/dist/workers/docs-core.js +28 -16
- package/dist/workers/fetch-core.d.ts +6 -1
- package/dist/workers/fetch-core.js +26 -33
- package/dist/workers/focused-extractor.d.ts +73 -0
- package/dist/workers/focused-extractor.js +72 -0
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +48 -42
- package/dist/workers/pi-worker-fetch.js +6 -8
- package/dist/workers/typeonly-log.d.ts +13 -0
- package/package.json +1 -1
|
@@ -538,6 +538,86 @@ export declare function deriveOpenDebts(cwd: string, staticOk: boolean): Promise
|
|
|
538
538
|
* assumption rather than an observation.
|
|
539
539
|
*/
|
|
540
540
|
export declare function rerunDebtVerifyCommand(cwd: string, command: string): VerifyRerunResult;
|
|
541
|
+
/**
|
|
542
|
+
* Where in the gate a closure scan runs. The two stages are NOT interchangeable
|
|
543
|
+
* and neither is a scheduling preference:
|
|
544
|
+
*
|
|
545
|
+
* - `pre-discovery` runs before the zero-discovery early return, so a project
|
|
546
|
+
* with no runnable command at all still FAILS the scan instead of returning
|
|
547
|
+
* UNOBSERVED. A static check needs no runner; that is the whole point of
|
|
548
|
+
* deciding it in exactly the environment where every dynamic probe went blind.
|
|
549
|
+
* - `post-boot` runs after the dynamic sections, which is where these scans'
|
|
550
|
+
* failures have always landed relative to command/launch/boot failures.
|
|
551
|
+
* Execution order is the aggregate's tiebreak within a rank, so moving a row
|
|
552
|
+
* between stages MOVES it in the user-visible failure list.
|
|
553
|
+
*/
|
|
554
|
+
type ClosureScanStage = 'pre-discovery' | 'post-boot';
|
|
555
|
+
/** Everything a closure scan may look at. Static and synchronous by construction:
|
|
556
|
+
* a check that had to spawn something would belong in the dynamic sections
|
|
557
|
+
* above, not here — these run on trees where nothing is runnable. */
|
|
558
|
+
interface ClosureScanInput {
|
|
559
|
+
cwd: string;
|
|
560
|
+
planText?: string;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* One run-level closure scan: "the shipped tree references/requires something it
|
|
564
|
+
* does not contain". Each row owns its scan, its formatting, its rank and its
|
|
565
|
+
* position; the driver owns the fault isolation.
|
|
566
|
+
*/
|
|
567
|
+
interface ClosureScan {
|
|
568
|
+
/** Stable identity — how the driver and its tests address a row. */
|
|
569
|
+
id: string;
|
|
570
|
+
stage: ClosureScanStage;
|
|
571
|
+
/**
|
|
572
|
+
* Rank in the aggregated failure list. All three rows are 0 because all three
|
|
573
|
+
* say some form of "the app cannot be started / cannot serve what it
|
|
574
|
+
* references / cannot be configured at all", which is the same load-bearing
|
|
575
|
+
* class as boot/render. It is a per-row FIELD rather than a shared constant
|
|
576
|
+
* so that a future scan whose finding is NOT of that class can say so in the
|
|
577
|
+
* table instead of by getting a hand-typed argument right at a call site.
|
|
578
|
+
*/
|
|
579
|
+
rank: number;
|
|
580
|
+
/**
|
|
581
|
+
* Scan and format in one pass, yielding one ready-to-emit failure text per
|
|
582
|
+
* finding.
|
|
583
|
+
*
|
|
584
|
+
* A GENERATOR rather than a function returning an array, for two reasons.
|
|
585
|
+
* First, the driver's try/catch wraps the ITERATION, so a scan that produces
|
|
586
|
+
* two findings and then faults still emits those two — precisely what the
|
|
587
|
+
* three hand-written `for (… ) fail(…)` loops inside try blocks did. Second,
|
|
588
|
+
* the three scans do not share a result shape (one nullable finding; a list;
|
|
589
|
+
* a list plus the template set its formatter also needs), and folding scan
|
|
590
|
+
* and format together lets each row keep its own arity instead of forcing a
|
|
591
|
+
* lowest-common-denominator result type on all of them.
|
|
592
|
+
*/
|
|
593
|
+
run: (input: ClosureScanInput) => Iterable<string>;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The run-level closure scans, in emission order within their stage.
|
|
597
|
+
*
|
|
598
|
+
* ONLY checks of this shape belong here. Four other checks in this gate are
|
|
599
|
+
* deliberately NOT rows: repo-health returns {ok, reason} and formats inline;
|
|
600
|
+
* the launch-contract diff branches on manifest kind and can emit a NOTE instead
|
|
601
|
+
* of a failure; the launch config-gap produces neither a failure nor a note but
|
|
602
|
+
* UN-COUNTS a dynamic observation; and the boot check is an async, stateful,
|
|
603
|
+
* port-binding exercise. Squeezing any of those in would mean a row type with
|
|
604
|
+
* more escape hatches than content.
|
|
605
|
+
*/
|
|
606
|
+
declare const CLOSURE_SCANS: ClosureScan[];
|
|
607
|
+
/**
|
|
608
|
+
* Run every closure scan belonging to `stage`, in table order, feeding each
|
|
609
|
+
* finding to `fail` at the row's own rank.
|
|
610
|
+
*
|
|
611
|
+
* FAULT ISOLATION IS PER ROW, not per stage: each row gets its own try/catch, so
|
|
612
|
+
* a scanner that throws contributes nothing and every LATER row still runs. One
|
|
613
|
+
* shared try would silently turn a fault in the first scan into blanket silence
|
|
614
|
+
* from the rest — the gate would keep working and keep missing defects it can
|
|
615
|
+
* decide. `scans` is injectable so that property can be tested with a row built
|
|
616
|
+
* to throw, without mocking the real scanners.
|
|
617
|
+
*/
|
|
618
|
+
declare function runClosureScans(stage: ClosureScanStage, input: ClosureScanInput, fail: (text: string, rank: number) => void, scans?: readonly ClosureScan[]): void;
|
|
619
|
+
export { CLOSURE_SCANS, runClosureScans };
|
|
620
|
+
export type { ClosureScan, ClosureScanInput, ClosureScanStage };
|
|
541
621
|
/**
|
|
542
622
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
543
623
|
* checks, then the discovered integration commands, then one boot exercise of
|
package/dist/task/final-gate.js
CHANGED
|
@@ -1339,6 +1339,98 @@ export function rerunDebtVerifyCommand(cwd, command) {
|
|
|
1339
1339
|
}
|
|
1340
1340
|
return { outcome: 'pass' };
|
|
1341
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* The run-level closure scans, in emission order within their stage.
|
|
1344
|
+
*
|
|
1345
|
+
* ONLY checks of this shape belong here. Four other checks in this gate are
|
|
1346
|
+
* deliberately NOT rows: repo-health returns {ok, reason} and formats inline;
|
|
1347
|
+
* the launch-contract diff branches on manifest kind and can emit a NOTE instead
|
|
1348
|
+
* of a failure; the launch config-gap produces neither a failure nor a note but
|
|
1349
|
+
* UN-COUNTS a dynamic observation; and the boot check is an async, stateful,
|
|
1350
|
+
* port-binding exercise. Squeezing any of those in would mean a row type with
|
|
1351
|
+
* more escape hatches than content.
|
|
1352
|
+
*/
|
|
1353
|
+
const CLOSURE_SCANS = [
|
|
1354
|
+
{
|
|
1355
|
+
// Serve-entry closure (mx5 run 18, nexttask 2B): the tree builds a server
|
|
1356
|
+
// app, expects to serve (SPA fallback / static read / a design clause), and
|
|
1357
|
+
// NOTHING anywhere starts a listener — `src/server/index.ts` ended at
|
|
1358
|
+
// `export {app}`, so the product could not be started at all while every
|
|
1359
|
+
// dynamic probe went blind on a docker-less box. Static, deterministic,
|
|
1360
|
+
// milliseconds, and — unlike the boot check — decidable in exactly the
|
|
1361
|
+
// environment where the boot skipped. Hence `pre-discovery`: a project with
|
|
1362
|
+
// no runnable command at all must still fail this, not report UNOBSERVED.
|
|
1363
|
+
id: 'serve-entry',
|
|
1364
|
+
stage: 'pre-discovery',
|
|
1365
|
+
rank: 0,
|
|
1366
|
+
*run({ cwd, planText }) {
|
|
1367
|
+
const found = findMissingServeEntry(cwd, planText);
|
|
1368
|
+
if (found)
|
|
1369
|
+
yield serveEntryGateFailureText(found);
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
// Artifact-production closure (mx5 run 13, PROMPT 2): a runtime file
|
|
1374
|
+
// reference with NO producer anywhere ships silently — the server read
|
|
1375
|
+
// `Bun.file('dist/index.html')` while the build emitted only app.css +
|
|
1376
|
+
// main.js, so every non-API GET 404'd behind 32/32 green checkoffs.
|
|
1377
|
+
// Deterministic scan of the shipped tree (literal refs only, positive
|
|
1378
|
+
// producer evidence required — see artifact-closure.ts); each dangle names
|
|
1379
|
+
// referencer + missing path.
|
|
1380
|
+
id: 'dangling-artifact',
|
|
1381
|
+
stage: 'post-boot',
|
|
1382
|
+
rank: 0,
|
|
1383
|
+
*run({ cwd }) {
|
|
1384
|
+
for (const d of findDanglingArtifacts(cwd))
|
|
1385
|
+
yield danglingGateFailureText(d);
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
// Env-template closure (mx5 run 19, nexttask 10): a shipped source file
|
|
1390
|
+
// requires an env var the shipped template never mentions. `seed.ts` read
|
|
1391
|
+
// `process.env.ADMIN_PHONE`/`ADMIN_PASSWORD`, `.env.example` declared
|
|
1392
|
+
// neither, `bun run seed` exited 1, and the autofix "fixed" it by writing
|
|
1393
|
+
// the GITIGNORED `.env` — so the committed tree still cannot seed and
|
|
1394
|
+
// nothing at run end said why. Same shape and rank as the dangling-artifact
|
|
1395
|
+
// scan above: naming the ARTIFACT that is wrong, statically, instead of only
|
|
1396
|
+
// the command that failed. The formatter needs the template set as well as
|
|
1397
|
+
// the finding, which is why scan and format are folded into one row.
|
|
1398
|
+
// Inert on any tree with no tracked template (ENOENT = pass).
|
|
1399
|
+
id: 'env-template',
|
|
1400
|
+
stage: 'post-boot',
|
|
1401
|
+
rank: 0,
|
|
1402
|
+
*run({ cwd }) {
|
|
1403
|
+
const env = findMissingEnvDeclarations(cwd);
|
|
1404
|
+
for (const m of env.missing)
|
|
1405
|
+
yield envGateFailureText(m, env.templates);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
];
|
|
1409
|
+
/**
|
|
1410
|
+
* Run every closure scan belonging to `stage`, in table order, feeding each
|
|
1411
|
+
* finding to `fail` at the row's own rank.
|
|
1412
|
+
*
|
|
1413
|
+
* FAULT ISOLATION IS PER ROW, not per stage: each row gets its own try/catch, so
|
|
1414
|
+
* a scanner that throws contributes nothing and every LATER row still runs. One
|
|
1415
|
+
* shared try would silently turn a fault in the first scan into blanket silence
|
|
1416
|
+
* from the rest — the gate would keep working and keep missing defects it can
|
|
1417
|
+
* decide. `scans` is injectable so that property can be tested with a row built
|
|
1418
|
+
* to throw, without mocking the real scanners.
|
|
1419
|
+
*/
|
|
1420
|
+
function runClosureScans(stage, input, fail, scans = CLOSURE_SCANS) {
|
|
1421
|
+
for (const scan of scans) {
|
|
1422
|
+
if (scan.stage !== stage)
|
|
1423
|
+
continue;
|
|
1424
|
+
try {
|
|
1425
|
+
for (const text of scan.run(input))
|
|
1426
|
+
fail(text, scan.rank);
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
// best-effort scan — a scanner fault must never break the gate
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
export { CLOSURE_SCANS, runClosureScans };
|
|
1342
1434
|
/**
|
|
1343
1435
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
1344
1436
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
@@ -1403,23 +1495,9 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1403
1495
|
}
|
|
1404
1496
|
}
|
|
1405
1497
|
}
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1409
|
-
// the product could not be started at all while every dynamic probe went blind on
|
|
1410
|
-
// a docker-less box. Static, deterministic, milliseconds, and — unlike the boot
|
|
1411
|
-
// check — decidable in exactly the environment where the boot skipped. Rank 0:
|
|
1412
|
-
// "the app cannot be started" is the same load-bearing class as boot/render.
|
|
1413
|
-
// Placed BEFORE the zero-discovery early return on purpose: a project with no
|
|
1414
|
-
// runnable command at all must still fail this, not report UNOBSERVED.
|
|
1415
|
-
try {
|
|
1416
|
-
const noServeEntry = findMissingServeEntry(cwd, planText);
|
|
1417
|
-
if (noServeEntry)
|
|
1418
|
-
fail(serveEntryGateFailureText(noServeEntry), 0);
|
|
1419
|
-
}
|
|
1420
|
-
catch {
|
|
1421
|
-
// best-effort scan — a scanner fault must never break the gate
|
|
1422
|
-
}
|
|
1498
|
+
// Run-level closure scans that must be decided BEFORE the zero-discovery early
|
|
1499
|
+
// return below — a static check needs no runner (CLOSURE_SCANS: 'pre-discovery').
|
|
1500
|
+
runClosureScans('pre-discovery', { cwd, planText }, fail);
|
|
1423
1501
|
const lockCmds = discoverLockfileChecks(cwd);
|
|
1424
1502
|
const { cmds } = discoverIntegrationCommands(cwd);
|
|
1425
1503
|
const boot = discoverBootCommand(cwd);
|
|
@@ -1486,7 +1564,8 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1486
1564
|
const warnings = [];
|
|
1487
1565
|
/** UNOBSERVED notes for launch scripts reclassified as CONFIG GAPS (run 20).
|
|
1488
1566
|
* They ride in `unobserved`, not `warnings`, so the caller's existing
|
|
1489
|
-
*
|
|
1567
|
+
* `recordDebt(cwd, id, fin.unobserved, 'final-gate')` writes the debt —
|
|
1568
|
+
* never a PASS. */
|
|
1490
1569
|
const configGapNotes = [];
|
|
1491
1570
|
if (declared.length > 0) {
|
|
1492
1571
|
const covered = cmds.flatMap(([bin, args]) => (bin === 'bun' || bin === 'npm') && args[0] === 'run' && args[1] ? [args[1]] : []);
|
|
@@ -1672,37 +1751,11 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1672
1751
|
});
|
|
1673
1752
|
if (gap)
|
|
1674
1753
|
fail(gap, 0);
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
|
|
1680
|
-
// producer evidence required — see artifact-closure.ts); each dangle is a
|
|
1681
|
-
// ranked failure naming referencer + missing path. Rank 0: "the app cannot
|
|
1682
|
-
// serve what it references" is the same load-bearing class as boot/render.
|
|
1683
|
-
try {
|
|
1684
|
-
for (const d of findDanglingArtifacts(cwd))
|
|
1685
|
-
fail(danglingGateFailureText(d), 0);
|
|
1686
|
-
}
|
|
1687
|
-
catch {
|
|
1688
|
-
// best-effort scan — a scanner fault must never break the gate
|
|
1689
|
-
}
|
|
1690
|
-
// Env-template closure (mx5 run 19, nexttask 10): a shipped source file
|
|
1691
|
-
// requires an env var the shipped template never mentions. `seed.ts` read
|
|
1692
|
-
// `process.env.ADMIN_PHONE`/`ADMIN_PASSWORD`, `.env.example` declared neither,
|
|
1693
|
-
// `bun run seed` exited 1, and the autofix "fixed" it by writing the GITIGNORED
|
|
1694
|
-
// `.env` — so the committed tree still cannot seed and nothing at run end said
|
|
1695
|
-
// why. Same shape and rank as the dangling-artifact scan one layer up: naming
|
|
1696
|
-
// the ARTIFACT that is wrong, statically, instead of only the command that
|
|
1697
|
-
// failed. Inert on any tree with no tracked template (ENOENT = pass).
|
|
1698
|
-
try {
|
|
1699
|
-
const env = findMissingEnvDeclarations(cwd);
|
|
1700
|
-
for (const m of env.missing)
|
|
1701
|
-
fail(envGateFailureText(m, env.templates), 0);
|
|
1702
|
-
}
|
|
1703
|
-
catch {
|
|
1704
|
-
// best-effort scan — a scanner fault must never break the gate
|
|
1705
|
-
}
|
|
1754
|
+
// The remaining run-level closure scans — "the shipped tree references or
|
|
1755
|
+
// requires something it does not contain" — after every dynamic section, so
|
|
1756
|
+
// their failures keep their historical place in the aggregate (CLOSURE_SCANS:
|
|
1757
|
+
// 'post-boot').
|
|
1758
|
+
runClosureScans('post-boot', { cwd, planText }, fail);
|
|
1706
1759
|
if (failures.length > 0) {
|
|
1707
1760
|
// Stable sort: boot/render (rank 0) leads, everything else keeps execution
|
|
1708
1761
|
// order. One failure keeps the exact single-failure wording; several become
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -22,7 +22,7 @@ import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-
|
|
|
22
22
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
23
23
|
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
24
24
|
import { readContracts } from './contracts.js';
|
|
25
|
-
import {
|
|
25
|
+
import { recordDebt } from './accept-debt.js';
|
|
26
26
|
import { recordRepairCandidate } from './root-cause-repair.js';
|
|
27
27
|
import { runRepoHealthCheck, runRepoHealthCheckAsync } from './repo-health-check.js';
|
|
28
28
|
import { runFinalIntegrationGate, discoverGateCommandLabels, discoverGateCommandBodies } from './final-gate.js';
|
|
@@ -610,28 +610,11 @@ export function buildGateDeps(params) {
|
|
|
610
610
|
// Durable per-task gate trail: every verdict/decision lands in the task
|
|
611
611
|
// file's `## gates` section so gate behavior is auditable from artifacts.
|
|
612
612
|
record: (cwd2, taskId, line) => appendGateRecord(cwd2, taskId, line),
|
|
613
|
-
// Durable
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// Same ledger, the KEPT disposition (mx5 run 18 / nexttask 4): the enforce
|
|
619
|
-
// re-verify FAILed on a check the enforce diff cannot reach, so the edits
|
|
620
|
-
// stayed and only the defect was recorded.
|
|
621
|
-
recordEnforceKeptDebt: (cwd2, taskId, reason) => recordEnforceKeptDebt(cwd2, taskId, reason),
|
|
622
|
-
// Durable cross-task-contradiction ledger (PROMPT 1 layer B): a repo-health
|
|
623
|
-
// FAIL whose only fix is an edit to a path this task's spec froze — recorded
|
|
624
|
-
// when the gate loop routes it to the picker, re-checked by the final gate.
|
|
625
|
-
recordFrozenBlockedDebt: (cwd2, taskId, reason) => recordFrozenBlockedDebt(cwd2, taskId, reason),
|
|
626
|
-
// Durable cross-task-deletion ledger (PROMPT 2): a sibling's committed
|
|
627
|
-
// deliverable this task's diff deletes, ACCEPTed into a commit anyway —
|
|
628
|
-
// the final gate re-checks it (resolved iff the file is back in the tree).
|
|
629
|
-
recordCrossTaskDeletionDebt: (cwd2, taskId, deletion) => recordCrossTaskDeletionDebt(cwd2, taskId, deletion),
|
|
630
|
-
// ROOT-CAUSE channel (mx5 run 14 item 5): a FAIL another task's untouched
|
|
631
|
-
// file caused is recorded as its own debt class and queued as a scoped
|
|
632
|
-
// repair task, instead of being blamed on — and reverted out of — the task
|
|
633
|
-
// that merely tripped over it.
|
|
634
|
-
recordRootCauseDebt: (cwd2, taskId, reason) => recordRootCauseDebt(cwd2, taskId, reason),
|
|
613
|
+
// Durable defect ledger under .pi-tasks/ (survives discardEdits): every
|
|
614
|
+
// recorded class — accepted, yolo-accepted, enforce-revert, enforce-kept,
|
|
615
|
+
// frozen-blocked, cross-task-deletion, root-cause — lands here with its
|
|
616
|
+
// origin, and the final integration gate re-checks each one at run end.
|
|
617
|
+
recordDebt,
|
|
635
618
|
recordRepairCandidate: (cwd2, candidate) => recordRepairCandidate(cwd2, candidate),
|
|
636
619
|
// file → introducing task, the provenance half of the discriminator.
|
|
637
620
|
introducedBy: (cwd2, rel) => Promise.resolve(taskThatIntroduced(cwd2, rel)),
|
|
@@ -42,7 +42,7 @@ import { readFileSync } from 'node:fs';
|
|
|
42
42
|
import * as fsp from 'node:fs/promises';
|
|
43
43
|
import * as os from 'node:os';
|
|
44
44
|
import * as path from 'node:path';
|
|
45
|
-
import {
|
|
45
|
+
import { makeGit } from '../shared/git-runner.js';
|
|
46
46
|
import { isRegenerableArtifact } from './regenerable-artifacts.js';
|
|
47
47
|
/** Keep the gate machinery's own artifacts out of the snapshot and the restore. */
|
|
48
48
|
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
@@ -109,12 +109,6 @@ function isAlwaysRegenerable(relPath, ctCacheDirs) {
|
|
|
109
109
|
return true;
|
|
110
110
|
return ctCacheDirs.some(d => p === d || p.startsWith(d + '/'));
|
|
111
111
|
}
|
|
112
|
-
function makeGit(cwd, signal, spawnFn) {
|
|
113
|
-
return async (args, env) => {
|
|
114
|
-
const r = await runChildDefault({ command: 'git', args, ...(env ? { env: { ...process.env, ...env } } : {}) }, cwd, signal, { mode: 'text' }, spawnFn);
|
|
115
|
-
return { stdout: r.stdout, exitCode: r.exitCode };
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
112
|
/**
|
|
119
113
|
* Snapshot the worktree content into a tree object via a THROWAWAY index file, so
|
|
120
114
|
* neither the real index nor the stash is touched. Returns null when git cannot
|
package/dist/task/phases.js
CHANGED
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { docsFocused } from '../workers/docs-core.js';
|
|
7
7
|
import { fetchFocused } from '../workers/fetch-core.js';
|
|
8
|
-
import { formatNpmVersionSection } from '../workers/npm-version.js';
|
|
9
8
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
10
9
|
import { findPhantomImports, formatApiCorrections, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
11
|
-
import { search as defaultSearch } from '../workers/search-core.js';
|
|
12
|
-
import { extractEnrichTargets } from './enrichment.js';
|
|
13
10
|
import { fanoutTimeoutPolicy, workerCarryForward, workerProgressCeilingMs, projectDocsBudget, projectDocsBudgetNotice } from './research-fanout-budget.js';
|
|
14
11
|
import { isIntegrationUnknown } from './unknown-routing.js';
|
|
15
12
|
import { extractUserDirectives, preserveDirectivesBlock, enforceDirectives } from './user-directives.js';
|
|
@@ -20,8 +17,7 @@ import { buildOrientation, orientationTier } from './orientation.js';
|
|
|
20
17
|
import { getConfig } from '../config/config.js';
|
|
21
18
|
import { readFile } from 'node:fs/promises';
|
|
22
19
|
import { resolve } from 'node:path';
|
|
23
|
-
import {
|
|
24
|
-
import { gatherExternalContext } from './external-context.js';
|
|
20
|
+
import { buildExternalContext, gatherExternalContext } from './external-context.js';
|
|
25
21
|
import { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, MAX_GRILL_QUESTIONS, appendNoThink } from './prompts.js';
|
|
26
22
|
import { appendGateRecord, readSection, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
27
23
|
import { applyRefutations } from './refuted-constraint.js';
|
|
@@ -1054,83 +1050,44 @@ export async function phaseAutoAnswer(deps, refined, research, question, autoDep
|
|
|
1054
1050
|
const docsFocusedFn = autoDeps.docsFocused ?? docsFocused;
|
|
1055
1051
|
const fetchFocusedFn = autoDeps.fetchFocused ?? fetchFocused;
|
|
1056
1052
|
try {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}).catch(() => null);
|
|
1096
|
-
if (r?.answer) {
|
|
1097
|
-
docSections[idx] = `### url: ${t.url}\n${r.answer}`;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
})),
|
|
1101
|
-
Promise.all(cappedServices.map(s => searchFn({
|
|
1102
|
-
query: `${s.name} ${s.query}`,
|
|
1103
|
-
count: 3,
|
|
1104
|
-
signal: deps.signal
|
|
1105
|
-
}).catch(() => null)))
|
|
1106
|
-
]);
|
|
1107
|
-
const serviceSections = [];
|
|
1108
|
-
const skipped = [];
|
|
1109
|
-
for (let i = 0; i < cappedServices.length; i++) {
|
|
1110
|
-
const s = cappedServices[i];
|
|
1111
|
-
const r = serviceResults[i];
|
|
1112
|
-
if (r === null)
|
|
1113
|
-
continue;
|
|
1114
|
-
if (r.kind === 'no_key') {
|
|
1115
|
-
skipped.push(s.name);
|
|
1116
|
-
continue;
|
|
1117
|
-
}
|
|
1118
|
-
if (r.kind === 'error')
|
|
1119
|
-
continue;
|
|
1120
|
-
serviceSections.push(formatServiceBlock(s.name, `${s.name} ${s.query}`, r.results));
|
|
1121
|
-
}
|
|
1122
|
-
if (skipped.length > 0) {
|
|
1123
|
-
serviceSections.push(formatFreshnessSkippedBlock(skipped));
|
|
1124
|
-
}
|
|
1125
|
-
// npm blocks lead so the model anchors on live version data first.
|
|
1126
|
-
const contextSections = [
|
|
1127
|
-
...npmSections.filter(Boolean),
|
|
1128
|
-
...docSections.filter(Boolean),
|
|
1129
|
-
...serviceSections
|
|
1130
|
-
];
|
|
1131
|
-
const externalContext = contextSections.length > 0 ?
|
|
1132
|
-
`EXTERNAL CONTEXT\n${contextSections.join('\n\n')}\n\n`
|
|
1133
|
-
: '';
|
|
1053
|
+
// Same assembly as the research phase (see external-context.ts); what
|
|
1054
|
+
// differs is POLICY and the worker variant, and both are arguments now.
|
|
1055
|
+
// The caps exist because this runs per grill question in front of a
|
|
1056
|
+
// waiting user; there is deliberately no version-lookup fan-out, no body
|
|
1057
|
+
// truncation (the focused child already answers in a paragraph) and no
|
|
1058
|
+
// timing sub-step here.
|
|
1059
|
+
//
|
|
1060
|
+
// `groundingBodies` counts the doc/url bodies that made it into the block:
|
|
1061
|
+
// the surviving-unknown routing below asks "did any doc/fetch worker
|
|
1062
|
+
// produce a grounding section?", which is exactly this and nothing about
|
|
1063
|
+
// npm-version or service blocks.
|
|
1064
|
+
let groundingBodies = 0;
|
|
1065
|
+
const countBody = (body) => {
|
|
1066
|
+
if (body !== undefined)
|
|
1067
|
+
groundingBodies += 1;
|
|
1068
|
+
return body;
|
|
1069
|
+
};
|
|
1070
|
+
const externalContext = await buildExternalContext(question, deps, {
|
|
1071
|
+
docs: async (pkg) => {
|
|
1072
|
+
const r = await docsFocusedFn({
|
|
1073
|
+
pkg,
|
|
1074
|
+
query: question,
|
|
1075
|
+
cwd: deps.cwd,
|
|
1076
|
+
signal: deps.signal
|
|
1077
|
+
});
|
|
1078
|
+
return { npmVersion: r.npmVersion, body: countBody(r.answer || undefined) };
|
|
1079
|
+
},
|
|
1080
|
+
url: async (url) => {
|
|
1081
|
+
const r = await fetchFocusedFn({
|
|
1082
|
+
url,
|
|
1083
|
+
query: question,
|
|
1084
|
+
cwd: deps.cwd,
|
|
1085
|
+
signal: deps.signal
|
|
1086
|
+
});
|
|
1087
|
+
return { body: countBody(r.answer || undefined) };
|
|
1088
|
+
},
|
|
1089
|
+
search: autoDeps.searchFn
|
|
1090
|
+
}, { targetCap: 2, serviceCap: 2 });
|
|
1134
1091
|
const basePrompt = externalContext + GRILL_AUTO_ANSWER_PROMPT(refined, research, question);
|
|
1135
1092
|
let text = await runPhaseChild(deps, 'grill-auto', 'read', basePrompt);
|
|
1136
1093
|
if (!autoAnswerHasTag(text)) {
|
|
@@ -1192,7 +1149,7 @@ export async function phaseAutoAnswer(deps, refined, research, question, autoDep
|
|
|
1192
1149
|
// refuse the guess and surface it to the user — carrying the model's
|
|
1193
1150
|
// best-effort answer as the pre-filled recommendation so the user accepts
|
|
1194
1151
|
// it with one keystroke or overrides it. Benign unknowns are untouched.
|
|
1195
|
-
const docResolved =
|
|
1152
|
+
const docResolved = groundingBodies > 0;
|
|
1196
1153
|
if (parsed.kind === 'answered' && !docResolved && isIntegrationUnknown(question)) {
|
|
1197
1154
|
deps.logDebug?.(`grill-auto: integration unknown unresolved by fetch — surfacing to user `
|
|
1198
1155
|
+ `instead of auto-answering: ${question.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-final-gate — the RUN-LEVEL final integration gate, the twin of task-gates.ts.
|
|
3
|
+
*
|
|
4
|
+
* task-gates.ts gates ONE task against its own spec. This gates the WHOLE REPO once,
|
|
5
|
+
* when every task in a /task-auto run is checked off and BEFORE the run is declared
|
|
6
|
+
* complete: every task passed its own per-slice gates, but per-slice green has shipped
|
|
7
|
+
* a dead app twice (mx5 runs 3 & 5 — statics clean, every protected route 500ing). So
|
|
8
|
+
* the project's OWN whole-repo commands run once here, unaided.
|
|
9
|
+
*
|
|
10
|
+
* The stage owns four things the per-task gate has no equivalent of:
|
|
11
|
+
*
|
|
12
|
+
* 1. THREE verdicts, not two — PASS / FAIL / UNOBSERVED. A gate that observed
|
|
13
|
+
* nothing dynamic is never announced as a pass (final-gate.ts unobservedVerdict).
|
|
14
|
+
* 2. The run-end ACCEPT-debt report — every defect a task was allowed to ship with,
|
|
15
|
+
* surfaced at the last moment anyone will look, and RE-DERIVED against the final
|
|
16
|
+
* tree after a converged autofix so the last word is about the tree that shipped.
|
|
17
|
+
* 3. The resolution loop — Leave-failed (recommended) / Autofix (a bounded,
|
|
18
|
+
* model-driven fix pass + gate re-run) / Accept, with the autofix card withdrawn
|
|
19
|
+
* after MAX_FINAL_GATE_AUTOFIX so a non-converging fix pass cannot loop forever.
|
|
20
|
+
* 4. The stranded sub-fixes a non-converging fix pass leaves in the working tree —
|
|
21
|
+
* committed on EVERY terminal outcome, because both of them end the run and the
|
|
22
|
+
* next `git checkout` would destroy real repairs (mx5 run 14: 13 of them).
|
|
23
|
+
*
|
|
24
|
+
* Every terminal outcome is RETURNED, never announced here — same contract as
|
|
25
|
+
* {@link runGatesForTask}'s GateResult: the caller owns the parent task file's state
|
|
26
|
+
* and its own resume wording. Nothing in this module touches per-task loop state, so
|
|
27
|
+
* a resume simply re-enters it and re-runs the gate.
|
|
28
|
+
*/
|
|
29
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
30
|
+
import type { CommitResult } from './auto-commit.js';
|
|
31
|
+
import type { FinalGateOutcome } from './final-gate.js';
|
|
32
|
+
import type { FinalGateFixFn } from './gate-deps.js';
|
|
33
|
+
import { type AcceptDebt } from './accept-debt.js';
|
|
34
|
+
/**
|
|
35
|
+
* The seams this stage drives. A strict subset of what /task-auto builds, and
|
|
36
|
+
* deliberately narrow: every optional dep absent degrades to a documented earlier
|
|
37
|
+
* behaviour, so a test supplies only the ones its scenario is about.
|
|
38
|
+
*/
|
|
39
|
+
export interface FinalGateStageDeps {
|
|
40
|
+
/**
|
|
41
|
+
* Whole-repo FINAL integration gate: the project's own static checks plus its own
|
|
42
|
+
* test/build commands, unaided (see final-gate.ts). Absent (tests / gate off) →
|
|
43
|
+
* the run completes without one, as it did before the gate existed.
|
|
44
|
+
*/
|
|
45
|
+
finalGate?: (cwd: string, planText?: string) => Promise<FinalGateOutcome>;
|
|
46
|
+
/**
|
|
47
|
+
* Bounded model-driven fix pass for a FAIL (see final-gate-fix.ts), offered as the
|
|
48
|
+
* picker's third option. Runs the fix child, applies the command-shrink guard, and
|
|
49
|
+
* re-runs the gate; the result's `ok` means the gate now passes. Absent (tests / no
|
|
50
|
+
* fix wiring) → the picker keeps only Leave-failed / Accept, exactly the
|
|
51
|
+
* pre-autofix behavior.
|
|
52
|
+
*/
|
|
53
|
+
finalGateFix?: FinalGateFixFn;
|
|
54
|
+
/**
|
|
55
|
+
* Paths currently uncommitted in the working tree (`git status` shape), used to
|
|
56
|
+
* detect SUB-FIXES a non-converging fix pass left behind (mx5 run 13 PROMPT 4 item
|
|
57
|
+
* 3). Every task is committed by the time this stage runs, so anything dirty here
|
|
58
|
+
* is the fix pass's own work. Absent → the stranded-fix handling is skipped
|
|
59
|
+
* entirely (prior behavior).
|
|
60
|
+
*/
|
|
61
|
+
pendingChanges?: (cwd: string) => Promise<string[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Re-derive the still-open ACCEPT-debt ledger against the tree AS IT IS NOW
|
|
64
|
+
* (final-gate.ts `deriveOpenDebts`). Needed because the run's "N recorded
|
|
65
|
+
* verify-FAIL defect(s) are STILL unresolved" report used to be built from the
|
|
66
|
+
* FIRST gate result and the converged-autofix path then rebuilt the gate outcome
|
|
67
|
+
* as a bare `{ok, reason}` — so `openDebts` was not merely un-actioned, it was GONE
|
|
68
|
+
* from the value, and no code path could ever clear, re-check or act on it (mx5 run
|
|
69
|
+
* 18: four defects reported STILL OPEN at 14:58, one of them fixed by the autofix
|
|
70
|
+
* that converged at 15:03, and the report never moved).
|
|
71
|
+
*
|
|
72
|
+
* `staticOk` is the caller's PROOF about the current statics, never a guess: it is
|
|
73
|
+
* passed true only where the gate itself just passed them. Absent (tests) → the
|
|
74
|
+
* post-autofix re-check is skipped and the pre-autofix report stands, exactly the
|
|
75
|
+
* prior behavior.
|
|
76
|
+
*/
|
|
77
|
+
recheckOpenDebts?: (cwd: string, staticOk: boolean) => Promise<{
|
|
78
|
+
openDebts: AcceptDebt[];
|
|
79
|
+
debtNote?: string;
|
|
80
|
+
trail?: string[];
|
|
81
|
+
}>;
|
|
82
|
+
/** Snapshot the working tree into one commit — used for a converged autofix and
|
|
83
|
+
* for the stranded sub-fixes either terminal outcome would otherwise abandon. */
|
|
84
|
+
commit: (cwd: string, message: string) => Promise<CommitResult>;
|
|
85
|
+
/** Append one line to the RUN's durable gate trail (`## gates` on the parent task
|
|
86
|
+
* file) — the same auditability contract the per-task records carry. Best-effort:
|
|
87
|
+
* absent in tests → skipped; a failure never breaks the gate. */
|
|
88
|
+
record?: (cwd: string, taskId: string, line: string) => Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/** Inputs that vary per caller. */
|
|
91
|
+
export interface FinalGateStageParams {
|
|
92
|
+
cwd: string;
|
|
93
|
+
/** The parent /task-auto id: trail target, notify prefix, commit-message tag. */
|
|
94
|
+
runId: string;
|
|
95
|
+
/** The parent plan (the task list), handed to the gate so it can tell a served app
|
|
96
|
+
* from a CLI — the boot check requires a listener only for the former (mx5 run 10:
|
|
97
|
+
* a CSS watcher satisfied "still alive"). */
|
|
98
|
+
planText: string;
|
|
99
|
+
/** How many tasks the run completed — for the completion announcement only. */
|
|
100
|
+
taskCount: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* How the run ended. The caller announces each one with its own resume wording and
|
|
104
|
+
* owns the parent task file's state — `cancelled` changes no state (a resume
|
|
105
|
+
* re-enters this stage and runs the gate then), `failed` marks the run failed,
|
|
106
|
+
* `completed` marks it complete.
|
|
107
|
+
*/
|
|
108
|
+
export type FinalGateStageResult = {
|
|
109
|
+
kind: 'cancelled';
|
|
110
|
+
message: string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: 'failed';
|
|
113
|
+
message: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: 'completed';
|
|
116
|
+
message: string;
|
|
117
|
+
level: 'info' | 'warning';
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Run the whole-repo gate and resolve its verdict with the user.
|
|
121
|
+
*
|
|
122
|
+
* Lifted verbatim out of /task-auto's run loop, which it never shared state with:
|
|
123
|
+
* the stage reads no per-task variable and writes none. Never throws for a gate
|
|
124
|
+
* outcome — only a user cancel inside a gate child propagates (the caller's
|
|
125
|
+
* USER_CANCELLED path handles it).
|
|
126
|
+
*/
|
|
127
|
+
export declare function runFinalGateStage(active: ExtensionCommandContext, deps: FinalGateStageDeps, p: FinalGateStageParams): Promise<FinalGateStageResult>;
|