@mjasnikovs/pi-task 0.18.23 → 0.18.25
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/task/artifact-closure.d.ts +107 -0
- package/dist/task/artifact-closure.js +826 -0
- package/dist/task/auto-orchestrator.d.ts +1 -0
- package/dist/task/auto-orchestrator.js +72 -8
- package/dist/task/final-gate-fix.d.ts +11 -2
- package/dist/task/final-gate-fix.js +14 -8
- package/dist/task/final-gate.d.ts +24 -3
- package/dist/task/final-gate.js +70 -22
- package/dist/task/requirements.d.ts +6 -1
- package/dist/task/requirements.js +15 -2
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This module currently holds the planning half (AutoDeps + planAuto). The run
|
|
6
6
|
* loop, command handlers, and defaultDeps are added by the next task.
|
|
7
7
|
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
8
9
|
import * as fsp from 'node:fs/promises';
|
|
9
10
|
import * as path from 'node:path';
|
|
10
11
|
import { gateRunTask, markResumable } from './orchestrator.js';
|
|
@@ -35,6 +36,7 @@ import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, app
|
|
|
35
36
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
36
37
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
37
38
|
import { decideAdoption, groundedCoverage } from './coverage-loop.js';
|
|
39
|
+
import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, danglingCarryText } from './artifact-closure.js';
|
|
38
40
|
import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, keepGroundedScripts, appendDeclaredScripts } from './launch-contract.js';
|
|
39
41
|
// Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
|
|
40
42
|
// when the model emits NONE), but a model that never says NONE would otherwise
|
|
@@ -493,6 +495,25 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
493
495
|
catch {
|
|
494
496
|
// best-effort channel
|
|
495
497
|
}
|
|
498
|
+
// Artifact-production closure, plan side (mx5 run 13, PROMPT 2): runtime
|
|
499
|
+
// files the spec REFERENCES (server snippets, prose "serve the built
|
|
500
|
+
// index.html") that neither its file tree, its parsed build outputs, nor the
|
|
501
|
+
// existing scaffold produce. Sentence-grounded coverage credited the SERVING
|
|
502
|
+
// side and reported "0 unowned" while nothing ever CREATED the file — so
|
|
503
|
+
// these ride the coverage loop's `missing` list as unowned areas until some
|
|
504
|
+
// task title claims the artifact (grounded in titles, which the coverage-map
|
|
505
|
+
// model cannot fake — the run-12 lesson). Deterministic and best-effort.
|
|
506
|
+
let specDangling = [];
|
|
507
|
+
try {
|
|
508
|
+
specDangling = findSpecDanglingArtifacts(featureForModel, rel => existsSync(path.join(cwd, rel)));
|
|
509
|
+
if (specDangling.length > 0) {
|
|
510
|
+
logPlanDebug(cwd, `artifact closure: ${specDangling.length} dangling runtime artifact(s) in the `
|
|
511
|
+
+ `spec: ${specDangling.map(d => d.path).join(', ')}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// best-effort channel
|
|
516
|
+
}
|
|
496
517
|
// decompose
|
|
497
518
|
const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries));
|
|
498
519
|
// Parse + FIDELITY RECONCILIATION (mx5 run 11, goal B): ground each title's
|
|
@@ -589,6 +610,13 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
589
610
|
}
|
|
590
611
|
}
|
|
591
612
|
const missing = [...verdictMissing, ...(acc?.unmapped ?? []).map(e => `"${e.quote}"`)];
|
|
613
|
+
// Unclaimed dangling artifacts are unowned areas: they force a coverage
|
|
614
|
+
// round that assigns a producing task, and clear as soon as a title
|
|
615
|
+
// names the file.
|
|
616
|
+
for (const d of specDangling) {
|
|
617
|
+
if (!titlesCoverArtifact(titles, d))
|
|
618
|
+
missing.push(danglingMissingText(d));
|
|
619
|
+
}
|
|
592
620
|
return {
|
|
593
621
|
plan: { titles, covered, missing },
|
|
594
622
|
accounting: acc,
|
|
@@ -707,12 +735,23 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
707
735
|
const carriedCrossCutting = accounting?.crossCutting ?? [];
|
|
708
736
|
const carriedUnmapped = accounting?.unmapped ?? [];
|
|
709
737
|
const carriedJudge = best.judgeMissing;
|
|
710
|
-
|
|
711
|
-
|
|
738
|
+
// Dangling artifacts still unclaimed by any title of the SHIPPING plan are a
|
|
739
|
+
// fourth channel: the producing obligation travels verbatim into every task
|
|
740
|
+
// (whichever task builds the referencing side must also produce the file),
|
|
741
|
+
// and the final gate re-checks the shipped tree regardless.
|
|
742
|
+
const carriedDangling = specDangling
|
|
743
|
+
.filter(d => !titlesCoverArtifact(planTitles, d))
|
|
744
|
+
.map(danglingCarryText);
|
|
745
|
+
if (carriedCrossCutting.length > 0
|
|
746
|
+
|| carriedUnmapped.length > 0
|
|
747
|
+
|| carriedJudge.length > 0
|
|
748
|
+
|| carriedDangling.length > 0) {
|
|
749
|
+
await appendCarriedRequirements(cwd, carriedCrossCutting, carriedUnmapped, carriedJudge, carriedDangling);
|
|
712
750
|
const parts = [
|
|
713
751
|
carriedCrossCutting.length > 0 ? `${carriedCrossCutting.length} cross-cutting` : '',
|
|
714
752
|
carriedUnmapped.length > 0 ? `${carriedUnmapped.length} unowned` : '',
|
|
715
|
-
carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : ''
|
|
753
|
+
carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : '',
|
|
754
|
+
carriedDangling.length > 0 ? `${carriedDangling.length} dangling-artifact` : ''
|
|
716
755
|
].filter(p => p.length > 0);
|
|
717
756
|
ctx.ui.notify(`/task-auto: carrying ${parts.join(', ')} requirement(s) into every task`
|
|
718
757
|
+ ' — see .pi-tasks/requirements.md.', 'info');
|
|
@@ -913,13 +952,30 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
913
952
|
// Hand the parent plan (the task list) to the gate so it can tell a
|
|
914
953
|
// served app from a CLI: the boot check requires a listener only for
|
|
915
954
|
// the former (mx5 run 10 — a CSS watcher satisfied "still alive").
|
|
955
|
+
// Trail EVERY aggregated failure entry (mx5 run 13): the gate now
|
|
956
|
+
// runs all sections and ranks the list; a single sliced reason
|
|
957
|
+
// line would re-hide everything past the first entry.
|
|
958
|
+
const trailGateFail = async (f) => {
|
|
959
|
+
const list = f.failures ?? [f.reason];
|
|
960
|
+
if (list.length <= 1) {
|
|
961
|
+
await recGate(`final-gate: FAIL — ${f.reason.slice(0, 300)}`);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
await recGate(`final-gate: FAIL — ${list.length} failures (ranked, most load-bearing first)`);
|
|
965
|
+
for (const [i, entry] of list.entries()) {
|
|
966
|
+
await recGate(`final-gate FAIL ${i + 1}/${list.length}: ${entry.slice(0, 300)}`);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
916
969
|
let fin = await deps.finalGate(cwd, body);
|
|
917
970
|
// Record the outcome symmetrically (mx5 run 10 item 7): only FAIL was
|
|
918
971
|
// ever trailed, so a PASSing gate was indistinguishable from a gate
|
|
919
972
|
// that never ran. The PASS reason names the commands that were run.
|
|
920
|
-
|
|
921
|
-
`final-gate: PASS — ${fin.reason.slice(0, 300)}`
|
|
922
|
-
|
|
973
|
+
if (fin.ok) {
|
|
974
|
+
await recGate(`final-gate: PASS — ${fin.reason.slice(0, 300)}`);
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
await trailGateFail(fin);
|
|
978
|
+
}
|
|
923
979
|
// ACCEPT-debt re-check surfacing (mx5 run 4 B3 / run 8 TASK_0012):
|
|
924
980
|
// tasks the user accepted despite a verify-FAIL that the gate could
|
|
925
981
|
// not prove resolved against the current tree. Surface them at the
|
|
@@ -989,13 +1045,21 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
989
1045
|
active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
|
|
990
1046
|
// Work from the FRESH gate failure when the fix pass got
|
|
991
1047
|
// as far as re-running the gate; otherwise keep the last.
|
|
992
|
-
// The
|
|
993
|
-
// the
|
|
1048
|
+
// The full ranked list rides along (and is re-trailed
|
|
1049
|
+
// when fresh) so the next picker and the next fix seed
|
|
1050
|
+
// still carry every entry, not just the first. The debt
|
|
1051
|
+
// note is carried so the next picker still shows the
|
|
1052
|
+
// open claims (the seed never includes it).
|
|
994
1053
|
fin = {
|
|
995
1054
|
ok: false,
|
|
996
1055
|
reason: fix.gateReason ?? fin.reason,
|
|
1056
|
+
failures: fix.gateReason !== undefined ? fix.gateFailures : fin.failures,
|
|
997
1057
|
debtNote: fin.debtNote
|
|
998
1058
|
};
|
|
1059
|
+
if (fix.gateReason !== undefined
|
|
1060
|
+
&& (fix.gateFailures?.length ?? 0) > 1) {
|
|
1061
|
+
await trailGateFail(fin);
|
|
1062
|
+
}
|
|
999
1063
|
continue;
|
|
1000
1064
|
}
|
|
1001
1065
|
// Leave failed — the dismissal default, unchanged from the
|
|
@@ -42,7 +42,10 @@ export declare function extractFailingCommand(reason: string): string | null;
|
|
|
42
42
|
/**
|
|
43
43
|
* Build the fix child's prompt. Generic by construction: the only project facts
|
|
44
44
|
* in it are the gate's own failure text — the command comes from the project's
|
|
45
|
-
* discovered manifest, never from a hardcoded ecosystem.
|
|
45
|
+
* discovered manifest, never from a hardcoded ecosystem. The seed may carry
|
|
46
|
+
* SEVERAL failures (the gate aggregates every section since mx5 run 13, ranked
|
|
47
|
+
* most load-bearing first); convergence means the WHOLE list is empty, so the
|
|
48
|
+
* child is told to fix all of them.
|
|
46
49
|
*/
|
|
47
50
|
export declare function buildFinalFixPrompt(failReason: string): string;
|
|
48
51
|
/**
|
|
@@ -63,6 +66,9 @@ export interface FinalFixResult {
|
|
|
63
66
|
/** On a did-not-converge outcome: the FRESH gate failure, so the caller's next
|
|
64
67
|
* picker (and next fix attempt) works from the current state, not the stale one. */
|
|
65
68
|
gateReason?: string;
|
|
69
|
+
/** The fresh gate's individual ranked failures (see FinalGateOutcome.failures),
|
|
70
|
+
* so the caller can trail each entry — never just the first. */
|
|
71
|
+
gateFailures?: string[];
|
|
66
72
|
}
|
|
67
73
|
export interface FinalFixDeps {
|
|
68
74
|
cwd: string;
|
|
@@ -72,10 +78,13 @@ export interface FinalFixDeps {
|
|
|
72
78
|
failReason: string;
|
|
73
79
|
/** Run the fix child; same closure shape the other gate children use. */
|
|
74
80
|
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
75
|
-
/** Re-run the final integration gate — the only arbiter of convergence.
|
|
81
|
+
/** Re-run the final integration gate — the only arbiter of convergence.
|
|
82
|
+
* Converges only when the gate's FULL aggregated failure list is empty
|
|
83
|
+
* (ok=true); `failures` rides through so the caller sees every entry. */
|
|
76
84
|
gate: (cwd: string) => Promise<{
|
|
77
85
|
ok: boolean;
|
|
78
86
|
reason: string;
|
|
87
|
+
failures?: string[];
|
|
79
88
|
}>;
|
|
80
89
|
/** Labels of every currently-discoverable gate command (static + integration),
|
|
81
90
|
* for the shrink guard. Pure discovery — nothing is executed. */
|
|
@@ -96,19 +96,24 @@ export function extractFailingCommand(reason) {
|
|
|
96
96
|
/**
|
|
97
97
|
* Build the fix child's prompt. Generic by construction: the only project facts
|
|
98
98
|
* in it are the gate's own failure text — the command comes from the project's
|
|
99
|
-
* discovered manifest, never from a hardcoded ecosystem.
|
|
99
|
+
* discovered manifest, never from a hardcoded ecosystem. The seed may carry
|
|
100
|
+
* SEVERAL failures (the gate aggregates every section since mx5 run 13, ranked
|
|
101
|
+
* most load-bearing first); convergence means the WHOLE list is empty, so the
|
|
102
|
+
* child is told to fix all of them.
|
|
100
103
|
*/
|
|
101
104
|
export function buildFinalFixPrompt(failReason) {
|
|
102
105
|
return [
|
|
103
106
|
'You are a bounded fix pass for a FAILED whole-repo integration gate.',
|
|
104
107
|
'Every task in this run is complete and committed; then the project’s own',
|
|
105
|
-
'integration
|
|
108
|
+
'integration commands were run against the assembled repository and failed:',
|
|
106
109
|
'',
|
|
107
110
|
failReason.trim(),
|
|
108
111
|
'',
|
|
109
|
-
'Your ONLY job is to
|
|
112
|
+
'Your ONLY job is to fix the DEFECT(s) those failures reveal. When several',
|
|
113
|
+
'failures are listed they are ranked most load-bearing first — fix ALL of',
|
|
114
|
+
'them; the gate only converges when every one passes.',
|
|
110
115
|
'',
|
|
111
|
-
'1. Re-run
|
|
116
|
+
'1. Re-run each exact failing command first and read its full output.',
|
|
112
117
|
'2. Diagnose the root cause, then fix it with the smallest correct change.',
|
|
113
118
|
' The project’s own manifests, configs and conventions define what',
|
|
114
119
|
' correct means — follow them, do not invent new structure.',
|
|
@@ -129,9 +134,9 @@ export function buildFinalFixPrompt(failReason) {
|
|
|
129
134
|
' revert, stash, clean). The work in this repository is finished and',
|
|
130
135
|
' committed — reverting it is destroying the run, not fixing it.',
|
|
131
136
|
'',
|
|
132
|
-
'4. Re-run the failing command after your fix and confirm
|
|
133
|
-
' gate is re-run mechanically after you finish — your claim is not
|
|
134
|
-
' verdict, the real exit
|
|
137
|
+
'4. Re-run the failing command(s) after your fix and confirm they exit 0.',
|
|
138
|
+
' The gate is re-run mechanically after you finish — your claim is not',
|
|
139
|
+
' the verdict, the real exit codes are.',
|
|
135
140
|
'',
|
|
136
141
|
'End with exactly one line:',
|
|
137
142
|
' FINAL-GATE-FIX: DONE',
|
|
@@ -240,7 +245,8 @@ export async function runFinalGateAutofix(deps) {
|
|
|
240
245
|
return {
|
|
241
246
|
ok: false,
|
|
242
247
|
reason: `did not converge: ${fin.reason}`,
|
|
243
|
-
gateReason: fin.reason
|
|
248
|
+
gateReason: fin.reason,
|
|
249
|
+
gateFailures: fin.failures
|
|
244
250
|
};
|
|
245
251
|
}
|
|
246
252
|
return { ok: true, reason: fin.reason };
|
|
@@ -6,14 +6,25 @@ export interface FinalGateOutcome {
|
|
|
6
6
|
/** true → statics and every runnable integration command passed (or nothing to run). */
|
|
7
7
|
ok: boolean;
|
|
8
8
|
/**
|
|
9
|
-
* On a fail: the exact command,
|
|
10
|
-
* MECHANICAL
|
|
9
|
+
* On a fail: the exact command(s), exit code(s), and output tail(s) — the
|
|
10
|
+
* MECHANICAL failures only. The accept-debt note is deliberately NOT folded in
|
|
11
11
|
* here (mx5 run 11): this string seeds the final-gate AUTOFIX child's prompt,
|
|
12
12
|
* and a debt included there is read as an instruction — the run-11 fix child
|
|
13
13
|
* `rm`'d a sibling task's verified deliverable to satisfy a recorded claim. The
|
|
14
14
|
* child cannot act on text it never receives; debts travel in `debtNote`.
|
|
15
|
+
* With multiple failures this is the numbered, ranked list (see `failures`).
|
|
15
16
|
*/
|
|
16
17
|
reason: string;
|
|
18
|
+
/**
|
|
19
|
+
* On a fail: EVERY section failure individually, ranked most load-bearing
|
|
20
|
+
* first — boot/render ("the app does not serve/render") outranks any single
|
|
21
|
+
* test failure. The gate runs every section and aggregates rather than
|
|
22
|
+
* early-returning (mx5 run 13: a bun-test glob failure shadowed the boot +
|
|
23
|
+
* render probe, so the user accepted the FAIL having only ever seen 1 failing
|
|
24
|
+
* CT test while the shipped app 404'd on every non-API GET). Callers trail
|
|
25
|
+
* each entry and show the full list wherever an ACCEPT decision is made.
|
|
26
|
+
*/
|
|
27
|
+
failures?: string[];
|
|
17
28
|
/**
|
|
18
29
|
* Human-facing suffix listing the still-open accepted-defect claims (see
|
|
19
30
|
* buildAcceptDebtNote) — for the picker question and the trail, NEVER for the
|
|
@@ -146,6 +157,16 @@ export { taskThatIntroduced };
|
|
|
146
157
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
147
158
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
148
159
|
* the start command — whole-repo, verbatim, unaided. Deterministic (no model).
|
|
149
|
-
*
|
|
160
|
+
*
|
|
161
|
+
* EVERY section runs and failures AGGREGATE (mx5 run 13): the gate used to
|
|
162
|
+
* early-return on the first failing section, and the boot + render probe — built
|
|
163
|
+
* after run 11 exactly for "app serves blank/nothing" — was ordered last, so any
|
|
164
|
+
* earlier failure shadowed the most load-bearing signal. Run 13's user accepted
|
|
165
|
+
* the FAIL having seen only 1 failing CT test while the app 404'd on every
|
|
166
|
+
* non-API GET; boot/render never executed in any attempt. Now the outcome
|
|
167
|
+
* carries the full ranked failure list (boot/render first — "the app does not
|
|
168
|
+
* serve/render" outranks any single test), the ACCEPT decision is made on all of
|
|
169
|
+
* it, and autofix converges only when the whole list is empty. Per-section
|
|
170
|
+
* env-gap/INFRA_GAP skip semantics and orphan-port recovery are unchanged.
|
|
150
171
|
*/
|
|
151
172
|
export declare function runFinalIntegrationGate(cwd: string, timeoutMs?: number, bootGraceMs?: number, bootDeps?: BootDeps, planText?: string): Promise<FinalGateOutcome>;
|
package/dist/task/final-gate.js
CHANGED
|
@@ -47,6 +47,7 @@ import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts }
|
|
|
47
47
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
48
48
|
import { runRenderCheck } from './render-check.js';
|
|
49
49
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
50
|
+
import { findDanglingArtifacts, danglingGateFailureText } from './artifact-closure.js';
|
|
50
51
|
function packageScripts(cwd) {
|
|
51
52
|
try {
|
|
52
53
|
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -595,7 +596,17 @@ export { taskThatIntroduced };
|
|
|
595
596
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
596
597
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
597
598
|
* the start command — whole-repo, verbatim, unaided. Deterministic (no model).
|
|
598
|
-
*
|
|
599
|
+
*
|
|
600
|
+
* EVERY section runs and failures AGGREGATE (mx5 run 13): the gate used to
|
|
601
|
+
* early-return on the first failing section, and the boot + render probe — built
|
|
602
|
+
* after run 11 exactly for "app serves blank/nothing" — was ordered last, so any
|
|
603
|
+
* earlier failure shadowed the most load-bearing signal. Run 13's user accepted
|
|
604
|
+
* the FAIL having seen only 1 failing CT test while the app 404'd on every
|
|
605
|
+
* non-API GET; boot/render never executed in any attempt. Now the outcome
|
|
606
|
+
* carries the full ranked failure list (boot/render first — "the app does not
|
|
607
|
+
* serve/render" outranks any single test), the ACCEPT decision is made on all of
|
|
608
|
+
* it, and autofix converges only when the whole list is empty. Per-section
|
|
609
|
+
* env-gap/INFRA_GAP skip semantics and orphan-port recovery are unchanged.
|
|
599
610
|
*/
|
|
600
611
|
export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGraceMs = 10_000, bootDeps = {}, planText) {
|
|
601
612
|
const stat = runRepoHealthCheck(cwd);
|
|
@@ -630,8 +641,15 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
630
641
|
...(debtNote ? { debtNote } : {}),
|
|
631
642
|
openDebts
|
|
632
643
|
});
|
|
644
|
+
// Aggregated failures across ALL sections (mx5 run 13 — see the function doc).
|
|
645
|
+
// rank 0 = boot/render ("does not serve/render" is the most load-bearing
|
|
646
|
+
// signal); rank 1 = everything else, kept in execution order by stable sort.
|
|
647
|
+
const failures = [];
|
|
648
|
+
const fail = (text, rank = 1) => {
|
|
649
|
+
failures.push({ rank, text });
|
|
650
|
+
};
|
|
633
651
|
if (!stat.ok)
|
|
634
|
-
|
|
652
|
+
fail(`static checks: ${stat.reason}`);
|
|
635
653
|
// Launch-contract diff (mx5 run 10 item 4): the design declared `migrate`/`seed`
|
|
636
654
|
// scripts that fell through decompose and shipped missing, unchecked. Diff the
|
|
637
655
|
// plan-time-extracted declared scripts against the manifest; a missing one is a
|
|
@@ -640,16 +658,13 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
640
658
|
if (declared.length > 0) {
|
|
641
659
|
const missing = missingDeclaredScripts(declared, Object.keys(packageScripts(cwd)));
|
|
642
660
|
if (missing.length > 0) {
|
|
643
|
-
|
|
644
|
-
ok: false,
|
|
645
|
-
reason: `launch contract: the design declares script(s) the shipped package.json does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`
|
|
646
|
-
});
|
|
661
|
+
fail(`launch contract: the design declares script(s) the shipped package.json does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`);
|
|
647
662
|
}
|
|
648
663
|
}
|
|
649
664
|
const lockCmds = discoverLockfileChecks(cwd);
|
|
650
665
|
const { cmds } = discoverIntegrationCommands(cwd);
|
|
651
666
|
const boot = discoverBootCommand(cwd);
|
|
652
|
-
if (lockCmds.length === 0 && cmds.length === 0 && !boot) {
|
|
667
|
+
if (lockCmds.length === 0 && cmds.length === 0 && !boot && failures.length === 0) {
|
|
653
668
|
return withDebts({ ok: true, reason: 'no integration command found (statics passed)' });
|
|
654
669
|
}
|
|
655
670
|
const ran = [];
|
|
@@ -663,10 +678,8 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
663
678
|
if (r.outcome === 'skip')
|
|
664
679
|
continue;
|
|
665
680
|
if (r.outcome === 'fail') {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
reason: `${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`
|
|
669
|
-
});
|
|
681
|
+
fail(`${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
|
|
682
|
+
continue;
|
|
670
683
|
}
|
|
671
684
|
ran.push(label);
|
|
672
685
|
}
|
|
@@ -685,7 +698,13 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
685
698
|
if (declared.length > 0) {
|
|
686
699
|
const covered = cmds.flatMap(([bin, args]) => (bin === 'bun' || bin === 'npm') && args[0] === 'run' && args[1] ? [args[1]] : []);
|
|
687
700
|
const skippedLaunch = [];
|
|
701
|
+
// A declared script the manifest doesn't expose is already a launch-contract
|
|
702
|
+
// failure above; executing it too would double-report (pre-aggregation the
|
|
703
|
+
// contract diff early-returned, so this loop could assume presence).
|
|
704
|
+
const present = new Set(Object.keys(packageScripts(cwd)).map(s => s.toLowerCase()));
|
|
688
705
|
for (const name of runnableDeclaredScripts(declared, covered)) {
|
|
706
|
+
if (!present.has(name.toLowerCase()))
|
|
707
|
+
continue;
|
|
689
708
|
const cmd = ['bun', ['run', name]];
|
|
690
709
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
691
710
|
const r = runGateCommand(cwd, cmd, Math.min(timeoutMs, 180_000), INFRA_GAP_OUTPUT_RE);
|
|
@@ -694,10 +713,8 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
694
713
|
continue;
|
|
695
714
|
}
|
|
696
715
|
if (r.outcome === 'fail') {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
reason: `launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`
|
|
700
|
-
});
|
|
716
|
+
fail(`launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`);
|
|
717
|
+
continue;
|
|
701
718
|
}
|
|
702
719
|
ran.push(label);
|
|
703
720
|
}
|
|
@@ -714,6 +731,9 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
714
731
|
}
|
|
715
732
|
}
|
|
716
733
|
}
|
|
734
|
+
// Boot + render ALWAYS runs (mx5 run 13): it is independent of test results by
|
|
735
|
+
// construction, and it carries the run's most load-bearing signal — earlier
|
|
736
|
+
// failures no longer shadow it. Its failures rank FIRST in the aggregate.
|
|
717
737
|
if (boot) {
|
|
718
738
|
const label = `${boot[0]} ${boot[1].join(' ')}`;
|
|
719
739
|
const expectServer = detectsServedApp(cwd, planText);
|
|
@@ -734,21 +754,18 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
734
754
|
b = await recoverOrphanPort(cwd, boot, b, bootGraceMs, bootDepsWithRender, expectServer);
|
|
735
755
|
}
|
|
736
756
|
if (b.outcome === 'fail') {
|
|
737
|
-
|
|
757
|
+
fail(`boot check: \`${label}\` ${b.detail}`, 0);
|
|
738
758
|
}
|
|
739
|
-
if (b.outcome === 'orphan-port') {
|
|
759
|
+
else if (b.outcome === 'orphan-port') {
|
|
740
760
|
// Could not clear the port. Distinct HARNESS diagnosis, never a bare app
|
|
741
761
|
// FAIL: name the port and (when known) the process squatting on it.
|
|
742
762
|
const holder = b.port !== null ? (bootDeps.findPortHolder ?? defaultFindPortHolder)(b.port) : null;
|
|
743
763
|
const who = holder ? ` — held by an orphaned process (pid ${holder.pid}: ${holder.command})`
|
|
744
764
|
: b.port !== null ? ` — port ${b.port} is held by another process`
|
|
745
765
|
: '';
|
|
746
|
-
|
|
747
|
-
ok: false,
|
|
748
|
-
reason: `boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`
|
|
749
|
-
});
|
|
766
|
+
fail(`boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`, 0);
|
|
750
767
|
}
|
|
751
|
-
if (b.outcome === 'pass') {
|
|
768
|
+
else if (b.outcome === 'pass') {
|
|
752
769
|
ran.push(label);
|
|
753
770
|
// A listener that served, but whose page could not be OBSERVED to render
|
|
754
771
|
// (no browser, undeterminable port) → UNOBSERVED warning, not a silent pass.
|
|
@@ -756,6 +773,37 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
756
773
|
warnings.push(b.renderNote);
|
|
757
774
|
}
|
|
758
775
|
}
|
|
776
|
+
// Artifact-production closure (mx5 run 13, PROMPT 2): a runtime file
|
|
777
|
+
// reference with NO producer anywhere ships silently — the server read
|
|
778
|
+
// `Bun.file('dist/index.html')` while the build emitted only app.css +
|
|
779
|
+
// main.js, so every non-API GET 404'd behind 32/32 green checkoffs.
|
|
780
|
+
// Deterministic scan of the shipped tree (literal refs only, positive
|
|
781
|
+
// producer evidence required — see artifact-closure.ts); each dangle is a
|
|
782
|
+
// ranked failure naming referencer + missing path. Rank 0: "the app cannot
|
|
783
|
+
// serve what it references" is the same load-bearing class as boot/render.
|
|
784
|
+
try {
|
|
785
|
+
for (const d of findDanglingArtifacts(cwd))
|
|
786
|
+
fail(danglingGateFailureText(d), 0);
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// best-effort scan — a scanner fault must never break the gate
|
|
790
|
+
}
|
|
791
|
+
if (failures.length > 0) {
|
|
792
|
+
// Stable sort: boot/render (rank 0) leads, everything else keeps execution
|
|
793
|
+
// order. One failure keeps the exact single-failure wording; several become
|
|
794
|
+
// a numbered list so the trail, the ACCEPT picker, and the autofix seed all
|
|
795
|
+
// carry the complete ranked picture.
|
|
796
|
+
const texts = [...failures].sort((a, b) => a.rank - b.rank).map(f => f.text);
|
|
797
|
+
return withDebts({
|
|
798
|
+
ok: false,
|
|
799
|
+
reason: texts.length === 1 ?
|
|
800
|
+
texts[0]
|
|
801
|
+
: `${texts.length} failures (ranked, most load-bearing first):\n${texts
|
|
802
|
+
.map((t, i) => `${i + 1}. ${t}`)
|
|
803
|
+
.join('\n')}`,
|
|
804
|
+
failures: texts
|
|
805
|
+
});
|
|
806
|
+
}
|
|
759
807
|
const warningNote = warnings.length > 0 ? ` — WARNING: ${warnings.join('; WARNING: ')}` : '';
|
|
760
808
|
return withDebts({
|
|
761
809
|
ok: true,
|
|
@@ -88,8 +88,13 @@ export declare function readRequirements(cwd: string): Promise<string>;
|
|
|
88
88
|
* was warned-about then dropped). These are plain strings, not quotes of the
|
|
89
89
|
* source; marked distinctly so a task can tell an inferred area from a verbatim
|
|
90
90
|
* obligation.
|
|
91
|
+
* • `danglingArtifacts` — runtime files the spec references but nothing
|
|
92
|
+
* produces (mx5 run 13: the served `index.html` no task, tree entry, or
|
|
93
|
+
* build output ever created), still unclaimed by any title at coverage
|
|
94
|
+
* exhaustion. Deterministically extracted (artifact-closure.ts), so like
|
|
95
|
+
* judge areas they are host-authored strings, not source quotes.
|
|
91
96
|
*/
|
|
92
|
-
export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[], judgeFlagged?: string[]): Promise<void>;
|
|
97
|
+
export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[], judgeFlagged?: string[], danglingArtifacts?: string[]): Promise<void>;
|
|
93
98
|
/**
|
|
94
99
|
* The read-only block refine/compose receive when carried requirements exist.
|
|
95
100
|
* Verbatim content travels with every task (the directive pattern that works),
|
|
@@ -320,10 +320,19 @@ function formatEntry(e, marker) {
|
|
|
320
320
|
* was warned-about then dropped). These are plain strings, not quotes of the
|
|
321
321
|
* source; marked distinctly so a task can tell an inferred area from a verbatim
|
|
322
322
|
* obligation.
|
|
323
|
+
* • `danglingArtifacts` — runtime files the spec references but nothing
|
|
324
|
+
* produces (mx5 run 13: the served `index.html` no task, tree entry, or
|
|
325
|
+
* build output ever created), still unclaimed by any title at coverage
|
|
326
|
+
* exhaustion. Deterministically extracted (artifact-closure.ts), so like
|
|
327
|
+
* judge areas they are host-authored strings, not source quotes.
|
|
323
328
|
*/
|
|
324
|
-
export async function appendCarriedRequirements(cwd, crossCutting, unresolved = [], judgeFlagged = []) {
|
|
325
|
-
if (crossCutting.length === 0
|
|
329
|
+
export async function appendCarriedRequirements(cwd, crossCutting, unresolved = [], judgeFlagged = [], danglingArtifacts = []) {
|
|
330
|
+
if (crossCutting.length === 0
|
|
331
|
+
&& unresolved.length === 0
|
|
332
|
+
&& judgeFlagged.length === 0
|
|
333
|
+
&& danglingArtifacts.length === 0) {
|
|
326
334
|
return;
|
|
335
|
+
}
|
|
327
336
|
try {
|
|
328
337
|
const existing = (await readRequirements(cwd)).split('\n').filter(l => l.trim().length > 0);
|
|
329
338
|
const seen = new Set(existing.map(l => {
|
|
@@ -337,6 +346,10 @@ export async function appendCarriedRequirements(cwd, crossCutting, unresolved =
|
|
|
337
346
|
[
|
|
338
347
|
judgeFlagged.map(q => ({ quote: q, anchor: '' })),
|
|
339
348
|
'judge-flagged uncovered area, no task owns this — surfaced at plan time'
|
|
349
|
+
],
|
|
350
|
+
[
|
|
351
|
+
danglingArtifacts.map(q => ({ quote: q, anchor: '' })),
|
|
352
|
+
'dangling runtime artifact, nothing produces it — surfaced at plan time'
|
|
340
353
|
]
|
|
341
354
|
]) {
|
|
342
355
|
for (const e of entries) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.25",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|