@mjasnikovs/pi-task 0.38.15 → 0.38.17
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/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
|
@@ -108,10 +108,10 @@ export async function buildExternalContext(source, deps, lookups, policy = {}) {
|
|
|
108
108
|
*
|
|
109
109
|
* Returns the `EXTERNAL CONTEXT\n…\n\n` block for the refined spec, or `''`.
|
|
110
110
|
*/
|
|
111
|
-
export async function gatherExternalContext(refined, deps
|
|
112
|
-
const docsRawFn =
|
|
113
|
-
const fetchRawFn =
|
|
114
|
-
const npmVersionFn =
|
|
111
|
+
export async function gatherExternalContext(refined, deps) {
|
|
112
|
+
const docsRawFn = deps.docsRaw ?? docsRaw;
|
|
113
|
+
const fetchRawFn = deps.fetchRaw ?? fetchRaw;
|
|
114
|
+
const npmVersionFn = deps.npmVersionLookup ?? npmVersionLookup;
|
|
115
115
|
const docsQuery = refined.split('\n').find(l => l.trim()) ?? refined;
|
|
116
116
|
return buildExternalContext(refined, deps, {
|
|
117
117
|
docs: async (pkg) => {
|
|
@@ -135,7 +135,7 @@ export async function gatherExternalContext(refined, deps, researchDeps = {}) {
|
|
|
135
135
|
const r = await fetchRawFn({ url, signal: deps.signal });
|
|
136
136
|
return { body: r.markdown.slice(0, RAW_BODY_LIMIT) };
|
|
137
137
|
},
|
|
138
|
-
search:
|
|
138
|
+
search: deps.searchFn
|
|
139
139
|
}, {
|
|
140
140
|
versionLookup: pkg => npmVersionFn(pkg, { signal: deps.signal }),
|
|
141
141
|
subStepLabel: 'enrichment',
|
|
@@ -12,4 +12,12 @@ export interface FailureClass {
|
|
|
12
12
|
level: NotifyLevel;
|
|
13
13
|
}
|
|
14
14
|
export declare function classifyFailure(err: unknown, aborted: boolean): FailureClass;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Persist, flash and announce a failure — and RETURN the classification.
|
|
17
|
+
*
|
|
18
|
+
* It used to return `void`, so the name it had just computed died here and the
|
|
19
|
+
* caller learned how the run ended by re-reading the task file's front matter and
|
|
20
|
+
* narrowing it to a boolean. Handing the value back is what lets `TaskRunner.run`
|
|
21
|
+
* say `RunEnd` instead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function handleFailure(err: unknown, ctx: ExtensionCommandContext, cwd: string, id: string, aborted: boolean): Promise<FailureClass>;
|
|
@@ -74,6 +74,14 @@ export function classifyFailure(err, aborted) {
|
|
|
74
74
|
level: 'error'
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Persist, flash and announce a failure — and RETURN the classification.
|
|
79
|
+
*
|
|
80
|
+
* It used to return `void`, so the name it had just computed died here and the
|
|
81
|
+
* caller learned how the run ended by re-reading the task file's front matter and
|
|
82
|
+
* narrowing it to a boolean. Handing the value back is what lets `TaskRunner.run`
|
|
83
|
+
* say `RunEnd` instead.
|
|
84
|
+
*/
|
|
77
85
|
export async function handleFailure(err, ctx, cwd, id, aborted) {
|
|
78
86
|
const c = classifyFailure(err, aborted);
|
|
79
87
|
await updateTaskFrontMatter(cwd, id, { state: c.state, reason: c.reason });
|
|
@@ -82,4 +90,5 @@ export async function handleFailure(err, ctx, cwd, id, aborted) {
|
|
|
82
90
|
// Mirror to remote viewers — ctx.ui.notify is terminal-only, so without this
|
|
83
91
|
// the remote view shows nothing when a task fails.
|
|
84
92
|
publishLifecycleNotice(`${id} ${c.notify}`, c.level);
|
|
93
|
+
return c;
|
|
85
94
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FinalGateOutcome } from './final-gate.js';
|
|
1
2
|
import { type TreeChangeSummary, type IgnoredSnapshot } from './write-guard.js';
|
|
2
3
|
/** Same bounded-fix contract as lint-fix: edit in place, bash exists to RUN the
|
|
3
4
|
* failing command (and the project's own tooling), not to mutate git state. */
|
|
@@ -97,22 +98,24 @@ export interface FinalFixResult {
|
|
|
97
98
|
ok: boolean;
|
|
98
99
|
/** Human-readable outcome (converged gate reason, or why the attempt failed). */
|
|
99
100
|
reason: string;
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
/**
|
|
102
|
+
* The re-run gate's OUTCOME, whole — present whenever the gate actually ran
|
|
103
|
+
* (absent only when the fix child self-declared blocked and the re-run was
|
|
104
|
+
* skipped).
|
|
105
|
+
*
|
|
106
|
+
* This used to be four flattened `gate*` mirrors, and the flattening lost
|
|
107
|
+
* things. `openDebts` never crossed at all, so `runFinalGateStage` rebuilt
|
|
108
|
+
* `fin` as a literal three times and each literal dropped it — the recorded
|
|
109
|
+
* mx5 run-18 defect, fixed by RE-DERIVING the field (`reconcileDebts`) rather
|
|
110
|
+
* than by keeping the value. Then 19A had to push `observedFailures` across the
|
|
111
|
+
* same wall as a third parallel field and re-pair it downstream by
|
|
112
|
+
* `gateObservedFailures?.includes(detail)` — a membership test that exists only
|
|
113
|
+
* because the pairing was broken in transit.
|
|
114
|
+
*/
|
|
115
|
+
gate?: FinalGateOutcome;
|
|
116
|
+
/** On a converged outcome: the UNOBSERVED note the CALLER should show. It is
|
|
117
|
+
* the gate's own note plus any downgrade this fix pass added (see the
|
|
118
|
+
* ignored-dependency probe below), so it is not simply `gate.unobserved`. */
|
|
116
119
|
unobserved?: string;
|
|
117
120
|
/** Gitignored path(s) this fix pass wrote, exempt classes already removed (see
|
|
118
121
|
* write-guard.ts). Present whether or not the gate converged — the caller
|
|
@@ -141,16 +144,9 @@ export interface FinalFixDeps {
|
|
|
141
144
|
/** Re-run the final integration gate — the only arbiter of convergence.
|
|
142
145
|
* Converges only when the gate's FULL aggregated failure list is empty
|
|
143
146
|
* (ok=true); `failures` rides through so the caller sees every entry. */
|
|
144
|
-
gate
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
failures?: string[];
|
|
148
|
-
/** Which of them a probe returned after OBSERVING (nexttask 19A) — carried
|
|
149
|
-
* through so the caller's demote decision can ask the probe's own verdict
|
|
150
|
-
* instead of re-deriving observability from the failure string. */
|
|
151
|
-
observedFailures?: string[];
|
|
152
|
-
unobserved?: string;
|
|
153
|
-
}>;
|
|
147
|
+
/** Returns the gate's own outcome type, not a structural copy of five of its
|
|
148
|
+
* fields — a re-declaration is how `openDebts` came to be silently absent. */
|
|
149
|
+
gate: (cwd: string) => Promise<FinalGateOutcome>;
|
|
154
150
|
/** Labels of every currently-discoverable gate command (static + integration),
|
|
155
151
|
* for the shrink guard. Pure discovery — nothing is executed. */
|
|
156
152
|
discoverLabels: (cwd: string) => string[];
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* legitimate whole-repo fix that run needed (migrate.ts — frozen by its own
|
|
44
44
|
* producing task). It activates only when a run-GLOBAL freeze source exists.
|
|
45
45
|
*/
|
|
46
|
-
import {
|
|
46
|
+
import { parseFixMarker, runFixChild } from './fix-child.js';
|
|
47
47
|
import { findForbiddenDeletions, diffIgnoredSnapshots, ignoredWriteTrailLine, ignoredWriteUnobservedNote } from './write-guard.js';
|
|
48
48
|
import { findNarrowedCommands, narrowingRejectionText } from './command-shrink.js';
|
|
49
49
|
/** Same bounded-fix contract as lint-fix: edit in place, bash exists to RUN the
|
|
@@ -151,16 +151,7 @@ export function buildFinalFixPrompt(failReason) {
|
|
|
151
151
|
* early-out on a self-declared BLOCKED.
|
|
152
152
|
*/
|
|
153
153
|
export function parseFinalFixMarker(text) {
|
|
154
|
-
|
|
155
|
-
let last = null;
|
|
156
|
-
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
157
|
-
last = m;
|
|
158
|
-
if (!last)
|
|
159
|
-
return { blocked: false };
|
|
160
|
-
if (last[1].toUpperCase() === 'BLOCKED') {
|
|
161
|
-
return { blocked: true, note: last[2].trim() || 'no reason given' };
|
|
162
|
-
}
|
|
163
|
-
return { blocked: false, note: last[2].trim() || undefined };
|
|
154
|
+
return parseFixMarker('FINAL-GATE-FIX', text);
|
|
164
155
|
}
|
|
165
156
|
/**
|
|
166
157
|
* STRANDED SUB-FIXES (mx5 run 13, PROMPT 4 item 3).
|
|
@@ -219,16 +210,16 @@ export async function runFinalGateAutofix(deps) {
|
|
|
219
210
|
// ignored files are untracked, so git alone cannot tell a file this pass wrote
|
|
220
211
|
// from one that was already sitting in the worktree.
|
|
221
212
|
const ignoredBefore = deps.ignoredSnapshot ? await deps.ignoredSnapshot() : null;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
213
|
+
// The four-rung ladder is task/fix-child.ts, shared with the per-task pass.
|
|
214
|
+
const end = await runFixChild({
|
|
215
|
+
runChild: deps.runChild,
|
|
216
|
+
tools: FINAL_FIX_TOOLS,
|
|
217
|
+
prompt: buildFinalFixPrompt(deps.failReason),
|
|
218
|
+
signal: deps.signal,
|
|
219
|
+
marker: 'FINAL-GATE-FIX'
|
|
220
|
+
});
|
|
221
|
+
if (end.kind === 'error')
|
|
222
|
+
return { ok: false, reason: `fix child failed: ${end.msg}` };
|
|
232
223
|
// What the child wrote to gitignored paths. Recorded on the trail IMMEDIATELY —
|
|
233
224
|
// before any guard can reject the attempt — because `discard` reverts tracked
|
|
234
225
|
// edits only: an ignored file the pass wrote survives a rejection, and the trail
|
|
@@ -327,20 +318,13 @@ export async function runFinalGateAutofix(deps) {
|
|
|
327
318
|
return r;
|
|
328
319
|
}
|
|
329
320
|
}
|
|
330
|
-
|
|
331
|
-
if (marker.blocked) {
|
|
321
|
+
if (end.kind === 'blocked') {
|
|
332
322
|
// Self-declared blocked: skip the (expensive) gate re-run; nothing converged.
|
|
333
|
-
return withIgnored({ ok: false, reason: `fix child blocked: ${
|
|
323
|
+
return withIgnored({ ok: false, reason: `fix child blocked: ${end.note}` });
|
|
334
324
|
}
|
|
335
325
|
const fin = await deps.gate(deps.cwd);
|
|
336
326
|
if (!fin.ok) {
|
|
337
|
-
return withIgnored({
|
|
338
|
-
ok: false,
|
|
339
|
-
reason: `did not converge: ${fin.reason}`,
|
|
340
|
-
gateReason: fin.reason,
|
|
341
|
-
gateFailures: fin.failures,
|
|
342
|
-
...(fin.observedFailures ? { gateObservedFailures: fin.observedFailures } : {})
|
|
343
|
-
});
|
|
327
|
+
return withIgnored({ ok: false, reason: `did not converge: ${fin.reason}`, gate: fin });
|
|
344
328
|
}
|
|
345
329
|
// IGNORED-DEPENDENCY DOWNGRADE (mx5 run 19). The gate says PASS; the question
|
|
346
330
|
// this answers is whether that PASS belongs to the REPOSITORY or only to this
|
|
@@ -368,6 +352,7 @@ export async function runFinalGateAutofix(deps) {
|
|
|
368
352
|
return withIgnored({
|
|
369
353
|
ok: true,
|
|
370
354
|
reason: fin.reason,
|
|
355
|
+
gate: fin,
|
|
371
356
|
...(notes.length > 0 ? { unobserved: notes.join(' ') } : {}),
|
|
372
357
|
...(ignoredDependent !== undefined ? { ignoredDependent } : {})
|
|
373
358
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type HealthCommand } from './repo-health-check.js';
|
|
2
2
|
import { deriveOpenDebts, rerunDebtVerifyCommand, type AcceptDebt } from './accept-debt.js';
|
|
3
|
-
import { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners, type BootDeps } from './boot-probe.js';
|
|
3
|
+
import { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners, type BootDeps } from './boot-probe.js';
|
|
4
4
|
import { type CommandRunner } from './command-run.js';
|
|
5
5
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
6
6
|
import { type EnvClosure } from './env-template-closure.js';
|
|
@@ -133,8 +133,9 @@ export declare function discoverGateCommandBodies(cwd: string): Record<string, s
|
|
|
133
133
|
export { runVerifyCommandLine, type VerifyRerunOutcome } from './command-run.js';
|
|
134
134
|
export { observabilityGapFailure, unobservedVerdict };
|
|
135
135
|
export { taskThatIntroduced };
|
|
136
|
-
export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
136
|
+
export { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
137
137
|
export type { BootDeps };
|
|
138
|
+
export type { BootSectionVerdict } from './boot-probe.js';
|
|
138
139
|
export { deriveOpenDebts, rerunDebtVerifyCommand };
|
|
139
140
|
/**
|
|
140
141
|
* Where in the gate a closure scan runs. The two stages are NOT interchangeable
|
|
@@ -259,5 +260,12 @@ export interface FinalGateOptions {
|
|
|
259
260
|
envClosure?: (cwd: string) => EnvClosure;
|
|
260
261
|
/** The repo's tracked file list, or null when it cannot be determined. */
|
|
261
262
|
trackedFiles?: (cwd: string) => string[] | null;
|
|
263
|
+
/**
|
|
264
|
+
* The run's cancel. Reaches every command the gate spawns — repo-health, the
|
|
265
|
+
* lockfile/integration/launch sections and the ACCEPT-debt re-runs. Nothing
|
|
266
|
+
* could be cancelled while `CommandRunner` was synchronous: the event loop
|
|
267
|
+
* never got a turn in which to notice.
|
|
268
|
+
*/
|
|
269
|
+
signal?: AbortSignal;
|
|
262
270
|
}
|
|
263
271
|
export declare function runFinalIntegrationGate(cwd: string, opts?: FinalGateOptions): Promise<FinalGateOutcome>;
|
package/dist/task/final-gate.js
CHANGED
|
@@ -51,10 +51,8 @@ import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.
|
|
|
51
51
|
import { deriveOpenDebts, rerunDebtVerifyCommand } from './accept-debt.js';
|
|
52
52
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
53
53
|
import { readLaunchManifest, inertLaunchContractNote, packageScripts, makeHasTarget } from './launch-manifest.js';
|
|
54
|
-
import { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners
|
|
54
|
+
import { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners } from './boot-probe.js';
|
|
55
55
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
56
|
-
import { runRenderCheck } from './render-check.js';
|
|
57
|
-
import { runDeepRenderCheck } from './deep-render-check.js';
|
|
58
56
|
import { resolveRunner, runnerEnv } from './runner-resolve.js';
|
|
59
57
|
import { classifyCommandRun, spawnCommand, INFRA_GAP_OUTPUT_RE } from './command-run.js';
|
|
60
58
|
import { findLaunchConfigGap, probeEnv, configGapUnobservedNote } from './launch-config-gap.js';
|
|
@@ -64,6 +62,7 @@ import { findMissingEnvDeclarations, envGateFailureText, scanEnvTemplateClosure,
|
|
|
64
62
|
import { findMissingServeEntry, serveEntryGateFailureText } from './serve-entry.js';
|
|
65
63
|
import { makefileRecipe } from './command-shrink.js';
|
|
66
64
|
import { GateTally, observabilityGapFailure, unobservedVerdict } from './gate-tally.js';
|
|
65
|
+
import { VERIFY_FAIL_PREFIX } from './verify-work.js';
|
|
67
66
|
/**
|
|
68
67
|
* The project's OWN whole-repo integration commands (test, then build — test
|
|
69
68
|
* first because it is the richer signal and the more common script). First
|
|
@@ -255,19 +254,26 @@ function resolveCommandBody(bin, args, scripts, makefile) {
|
|
|
255
254
|
* passes `extraGapRe` (launch scripts), missing external infrastructure. Only a
|
|
256
255
|
* command that actually ran and exited non-zero for a real reason fails.
|
|
257
256
|
*/
|
|
258
|
-
function runGateCommand(cwd, [bin, args], timeoutMs, extraGapRe,
|
|
257
|
+
async function runGateCommand(cwd, [bin, args], timeoutMs, extraGapRe,
|
|
259
258
|
/** Replaces the child's environment wholesale (config-gap probe re-run only —
|
|
260
259
|
* see launch-config-gap.ts). Absent ⇒ `runnerEnv(runner)`, i.e. unchanged. */
|
|
261
260
|
envOverride,
|
|
262
261
|
/** The spawner. Injected so the gate's own tests can script a verdict. */
|
|
263
|
-
run = spawnCommand) {
|
|
262
|
+
run = spawnCommand, signal) {
|
|
264
263
|
// Runner resolution (mx5 run 16): a login-shell-stripped PATH left `bun`
|
|
265
264
|
// unspawnable, so every dynamic check skipped and the gate went blind. The
|
|
266
265
|
// resolved binary is spawned, and its directory rides on the child's PATH so
|
|
267
266
|
// the SCRIPT CHAIN can re-invoke the runner (`bun run test` runs `bun test`
|
|
268
267
|
// inside — a bare 127 there is the same blindness one level down).
|
|
269
268
|
const runner = resolveRunner(bin);
|
|
270
|
-
const verdict = classifyCommandRun(run({
|
|
269
|
+
const verdict = classifyCommandRun(await run({
|
|
270
|
+
cwd,
|
|
271
|
+
bin: runner.bin,
|
|
272
|
+
args,
|
|
273
|
+
timeoutMs,
|
|
274
|
+
env: envOverride ?? runnerEnv(runner),
|
|
275
|
+
...(signal === undefined ? {} : { signal })
|
|
276
|
+
}), extraGapRe ? [extraGapRe] : []);
|
|
271
277
|
if (verdict.outcome === 'gap') {
|
|
272
278
|
return { outcome: 'skip', spawnFailed: verdict.gap === 'spawn-failed' };
|
|
273
279
|
}
|
|
@@ -288,7 +294,7 @@ export { taskThatIntroduced };
|
|
|
288
294
|
// src/). Re-exported so the seven validation harnesses under scripts/ — which have
|
|
289
295
|
// always imported exactly this surface and nothing else from the gate — keep
|
|
290
296
|
// working unchanged. Same pattern as taskThatIntroduced above.
|
|
291
|
-
export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
297
|
+
export { discoverBootCommand, detectsServedApp, runBootCheck, runBootSection, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
292
298
|
// The ACCEPT-debt re-check (`deriveOpenDebts`, `rerunDebtVerifyCommand`) lives in
|
|
293
299
|
// accept-debt.ts with the ledger it reads and writes; re-exported so the
|
|
294
300
|
// orchestrator and the harnesses under scripts/ keep working unchanged.
|
|
@@ -394,18 +400,28 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
394
400
|
return inertClosure();
|
|
395
401
|
}
|
|
396
402
|
}, trackedFiles: trackedFilesFn = trackedFiles } = opts;
|
|
397
|
-
|
|
403
|
+
// ASYNC: the run-end gate no longer blocks the event loop for the project's own
|
|
404
|
+
// lint (measured 15s mx5 / 69s aiz-client with zero timer ticks), so a loader can
|
|
405
|
+
// paint and a cancel can reach the child.
|
|
406
|
+
const stat = await runRepoHealthCheck(cwd, {
|
|
407
|
+
run: runCmd,
|
|
408
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal })
|
|
409
|
+
});
|
|
398
410
|
// Debts are derived once, before any section runs, and ride on every verdict
|
|
399
411
|
// shape (GateTally.verdict): `reason` stays the mechanical failure because it
|
|
400
412
|
// seeds the autofix child's prompt — run 11's fix child executed a recorded
|
|
401
413
|
// claim as an instruction.
|
|
402
|
-
const debts = await deriveOpenDebts(cwd, stat.ok);
|
|
414
|
+
const debts = await deriveOpenDebts(cwd, stat.ok, runCmd, opts.signal);
|
|
403
415
|
// Every section below RECORDS into the tally (failures ranked, the four
|
|
404
416
|
// dynamic counters, the notes) and the verdict is assembled ONCE at the end —
|
|
405
417
|
// see gate-tally.ts for what each method means.
|
|
406
418
|
const tally = new GateTally();
|
|
419
|
+
// The prefix comes from VERIFY_FAIL_PREFIX so this run-level mint and the
|
|
420
|
+
// task-level `repo health:` one stay linked: both are the deterministic
|
|
421
|
+
// whole-repo static check, and `isStaticClassDebt` must recognise a debt that
|
|
422
|
+
// entered the ledger through EITHER altitude.
|
|
407
423
|
if (!stat.ok)
|
|
408
|
-
tally.fail(
|
|
424
|
+
tally.fail(`${VERIFY_FAIL_PREFIX['static-checks']} ${stat.reason}`);
|
|
409
425
|
// Launch-contract diff (mx5 run 10 item 4): the design declared `migrate`/`seed`
|
|
410
426
|
// scripts that fell through decompose and shipped missing, unchecked. Diff the
|
|
411
427
|
// plan-time-extracted declared scripts against the manifest; a missing one is a
|
|
@@ -446,7 +462,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
446
462
|
for (const cmd of list) {
|
|
447
463
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
448
464
|
tally.attempted(cmd[0]);
|
|
449
|
-
const r = runGateCommand(cwd, cmd, timeoutMs, undefined, undefined, runCmd);
|
|
465
|
+
const r = await runGateCommand(cwd, cmd, timeoutMs, undefined, undefined, runCmd, opts.signal);
|
|
450
466
|
if (r.outcome === 'skip') {
|
|
451
467
|
if (r.spawnFailed)
|
|
452
468
|
tally.spawnFailure(cmd[0]);
|
|
@@ -496,7 +512,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
496
512
|
const cmd = ['bun', ['run', name]];
|
|
497
513
|
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
498
514
|
tally.attempted(cmd[0]);
|
|
499
|
-
const r = runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, undefined, runCmd);
|
|
515
|
+
const r = await runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, undefined, runCmd, opts.signal);
|
|
500
516
|
if (r.outcome === 'skip') {
|
|
501
517
|
if (r.spawnFailed)
|
|
502
518
|
tally.spawnFailure(cmd[0]);
|
|
@@ -525,7 +541,7 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
525
541
|
env: process.env
|
|
526
542
|
});
|
|
527
543
|
if (gap) {
|
|
528
|
-
const probe = runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, probeEnv(runnerEnv(resolveRunner(cmd[0])), gap), runCmd);
|
|
544
|
+
const probe = await runGateCommand(cwd, cmd, launchTimeout, INFRA_GAP_OUTPUT_RE, probeEnv(runnerEnv(resolveRunner(cmd[0])), gap), runCmd, opts.signal);
|
|
529
545
|
if (probe.outcome === 'pass') {
|
|
530
546
|
// Nothing about this script was OBSERVED: the real run could
|
|
531
547
|
// not reach it and the probe run is a diagnostic, never an
|
|
@@ -585,83 +601,33 @@ export async function runFinalIntegrationGate(cwd, opts = {}) {
|
|
|
585
601
|
// statement about when they are meaningful.
|
|
586
602
|
if (!boot && tally.silent())
|
|
587
603
|
return tally.verdict(debts);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
};
|
|
611
|
-
let b = await runBootCheck(cwd, boot, bootGraceMs, {
|
|
612
|
-
expectServer,
|
|
613
|
-
deps: bootDepsWithRender
|
|
614
|
-
});
|
|
615
|
-
if (b.outcome === 'orphan-port') {
|
|
616
|
-
b = await recoverOrphanPort(cwd, boot, b, bootGraceMs, bootDepsWithRender, expectServer);
|
|
617
|
-
}
|
|
618
|
-
if (b.outcome !== 'skip')
|
|
619
|
-
tally.observed();
|
|
620
|
-
else if (b.spawnFailed)
|
|
621
|
-
tally.spawnFailure(boot[0]);
|
|
622
|
-
tally.bootUnobserved(bootSkipVerdict({
|
|
623
|
-
label,
|
|
624
|
-
skipped: b.outcome === 'skip',
|
|
625
|
-
expectServer
|
|
626
|
-
}));
|
|
627
|
-
if (b.outcome === 'fail') {
|
|
628
|
-
// OBSERVED (nexttask 19A). Every path that produces `fail` here is a
|
|
629
|
-
// probe that looked: the render judge saw an empty body, the deep
|
|
630
|
-
// session saw the authenticated half dead, the enumerator saw no
|
|
631
|
-
// listener, or the launch command itself exited non-zero. The one
|
|
632
|
-
// condition that means "we could not look" — no ss/netstat/lsof, mx5
|
|
633
|
-
// run 14 — returns PASS stamped UNOBSERVED and never reaches here
|
|
634
|
-
// (`b0f90a7`, final-gate.ts `if (!canEnumerate) return passAndKill(…)`).
|
|
635
|
-
tally.failObserved(`boot check: \`${label}\` ${b.detail}`, 0);
|
|
636
|
-
}
|
|
637
|
-
else if (b.outcome === 'orphan-port') {
|
|
638
|
-
// Could not clear the port. Distinct HARNESS diagnosis, never a bare app
|
|
639
|
-
// FAIL: name the port and (when known) the process squatting on it.
|
|
640
|
-
const holder = b.port !== null ? (bootDeps.findPortHolder ?? defaultFindPortHolder)(b.port) : null;
|
|
641
|
-
const who = holder ? ` — held by an orphaned process (pid ${holder.pid}: ${holder.command})`
|
|
642
|
-
: b.port !== null ? ` — port ${b.port} is held by another process`
|
|
643
|
-
: '';
|
|
644
|
-
tally.fail(`boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`, 0);
|
|
645
|
-
}
|
|
646
|
-
else if (b.outcome === 'pass') {
|
|
647
|
-
tally.ran(label);
|
|
648
|
-
// A listener that served, but whose page could not be OBSERVED to render
|
|
649
|
-
// (no browser, undeterminable port) → UNOBSERVED warning, not a silent pass.
|
|
650
|
-
if (b.renderNote)
|
|
651
|
-
tally.warn(b.renderNote);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
// Nothing to boot — but if the reason is that the project's only launch
|
|
656
|
-
// script was REJECTED as not-a-launch (2A), that is not the same thing as a
|
|
657
|
-
// project with no launch surface, and it must not degrade into silence.
|
|
658
|
-
const rejected = rejectedLaunchScript(cwd);
|
|
659
|
-
if (rejected && detectsServedApp(cwd, planText)) {
|
|
660
|
-
tally.bootUnobserved(`boot check: this project's only launch script (\`${rejected.name}\`) is not a `
|
|
661
|
-
+ `launch — ${rejected.reason} — so nothing was started and the app was never `
|
|
662
|
-
+ 'observed to run.');
|
|
663
|
-
}
|
|
604
|
+
// The boot CONCEPT lives in boot-probe.ts (runBootSection): discovery,
|
|
605
|
+
// served-app detection, the probe defaults, the boot check, orphan-port
|
|
606
|
+
// recovery, the port-holder diagnosis, the skip verdict and the
|
|
607
|
+
// rejected-launch-script branch. This is the record, and nothing else.
|
|
608
|
+
const bootSection = await runBootSection(cwd, {
|
|
609
|
+
...(planText === undefined ? {} : { planText }),
|
|
610
|
+
...(bootGraceMs === undefined ? {} : { graceMs: bootGraceMs }),
|
|
611
|
+
deps: bootDeps
|
|
612
|
+
});
|
|
613
|
+
if (bootSection.attempted)
|
|
614
|
+
tally.attempted(bootSection.attempted);
|
|
615
|
+
if (bootSection.observed)
|
|
616
|
+
tally.observed();
|
|
617
|
+
else if (bootSection.spawnFailedBin)
|
|
618
|
+
tally.spawnFailure(bootSection.spawnFailedBin);
|
|
619
|
+
tally.bootUnobserved(bootSection.unobservedNote ?? null);
|
|
620
|
+
if (bootSection.failure) {
|
|
621
|
+
const { detail, rank, observed } = bootSection.failure;
|
|
622
|
+
if (observed)
|
|
623
|
+
tally.failObserved(detail, rank);
|
|
624
|
+
else
|
|
625
|
+
tally.fail(detail, rank);
|
|
664
626
|
}
|
|
627
|
+
if (bootSection.ranLabel)
|
|
628
|
+
tally.ran(bootSection.ranLabel);
|
|
629
|
+
for (const w of bootSection.warnings)
|
|
630
|
+
tally.warn(w);
|
|
665
631
|
// Full-skip blindness guard (mx5 run 16): commands were discovered but every
|
|
666
632
|
// one skipped → rank-0 failure, never a static-only PASS. Runner resolvability
|
|
667
633
|
// is checked through resolveRunner so the failure text can name the missing
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running ONE bounded fix child and deciding what its ending MEANS.
|
|
3
|
+
*
|
|
4
|
+
* Two graduated-resolution passes drive a fix child through the same four rungs —
|
|
5
|
+
* cancel propagates, a thrown child is an error, a self-declared BLOCKED ends the
|
|
6
|
+
* attempt, anything else is DONE — and `final-gate-fix.ts:12` says so out loud:
|
|
7
|
+
* it "mirrors the per-task graduated-resolution shape (lint-fix.ts)". This is that
|
|
8
|
+
* ladder, once.
|
|
9
|
+
*
|
|
10
|
+
* NOT the rejected sharing. CONTEXT.md's "the two resolution loops stay two" is
|
|
11
|
+
* about `runGatesForTask` vs `runFinalGateStage` — the LOOPS, at two altitudes.
|
|
12
|
+
* This is one altitude down: the single child invocation inside each, where the
|
|
13
|
+
* two had actually drifted.
|
|
14
|
+
*
|
|
15
|
+
* - **The lint-fix marker was a DEAD protocol.** `buildLintFixPrompt` instructs
|
|
16
|
+
* the child to end with `LINT-FIX: DONE` or `LINT-FIX: BLOCKED <why>`, and the
|
|
17
|
+
* call site was `await deps.runChild(...)` with the return value DISCARDED —
|
|
18
|
+
* nothing in `src/` or `scripts/` parsed it. The twin parsed its own marker and
|
|
19
|
+
* used it to skip the expensive gate re-run. So a lint-fix child that reported
|
|
20
|
+
* BLOCKED still paid the full guard stack plus a whole repo-health run (15–69s
|
|
21
|
+
* measured), and the user was told `did not converge: <health.reason>` instead
|
|
22
|
+
* of the child's own stated reason. The suite fed `'LINT-FIX: DONE'` as fake
|
|
23
|
+
* output, so it stayed green whether the marker was parsed or deleted.
|
|
24
|
+
* - **The cancel rung re-typed its constant.** `lint-fix.ts` compared against a
|
|
25
|
+
* literal `'__user_cancelled__'`; it was the only production site in `src/` not
|
|
26
|
+
* importing `USER_CANCELLED`.
|
|
27
|
+
*
|
|
28
|
+
* What stays per-site: the arbiter (a gate re-run vs a repo-health re-run), the
|
|
29
|
+
* result shape, and the guard sets. Only the child call is shared.
|
|
30
|
+
*/
|
|
31
|
+
/** How one fix child ended. A CANCEL is not a member: it throws, so the caller's
|
|
32
|
+
* own `USER_CANCELLED` path runs unchanged. */
|
|
33
|
+
export type FixChildEnd = {
|
|
34
|
+
kind: 'done';
|
|
35
|
+
text: string;
|
|
36
|
+
note?: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'blocked';
|
|
39
|
+
text: string;
|
|
40
|
+
note: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'error';
|
|
43
|
+
msg: string;
|
|
44
|
+
};
|
|
45
|
+
export interface FixChildInput {
|
|
46
|
+
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
47
|
+
tools: string;
|
|
48
|
+
prompt: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
/** The marker word this pass's prompt instructs — `LINT-FIX`, `FINAL-GATE-FIX`. */
|
|
51
|
+
marker: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse a fix child's final marker. Last match wins: the model reasons before
|
|
55
|
+
* concluding, and bash output can echo the words. No marker → DONE, because the
|
|
56
|
+
* pass's own arbiter re-runs either way; a missing marker only forfeits the
|
|
57
|
+
* early-out on a self-declared BLOCKED.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseFixMarker(marker: string, text: string): {
|
|
60
|
+
blocked: boolean;
|
|
61
|
+
note?: string;
|
|
62
|
+
};
|
|
63
|
+
/** Run one fix child through the four-rung ladder. Cancel THROWS; nothing else does. */
|
|
64
|
+
export declare function runFixChild(input: FixChildInput): Promise<FixChildEnd>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running ONE bounded fix child and deciding what its ending MEANS.
|
|
3
|
+
*
|
|
4
|
+
* Two graduated-resolution passes drive a fix child through the same four rungs —
|
|
5
|
+
* cancel propagates, a thrown child is an error, a self-declared BLOCKED ends the
|
|
6
|
+
* attempt, anything else is DONE — and `final-gate-fix.ts:12` says so out loud:
|
|
7
|
+
* it "mirrors the per-task graduated-resolution shape (lint-fix.ts)". This is that
|
|
8
|
+
* ladder, once.
|
|
9
|
+
*
|
|
10
|
+
* NOT the rejected sharing. CONTEXT.md's "the two resolution loops stay two" is
|
|
11
|
+
* about `runGatesForTask` vs `runFinalGateStage` — the LOOPS, at two altitudes.
|
|
12
|
+
* This is one altitude down: the single child invocation inside each, where the
|
|
13
|
+
* two had actually drifted.
|
|
14
|
+
*
|
|
15
|
+
* - **The lint-fix marker was a DEAD protocol.** `buildLintFixPrompt` instructs
|
|
16
|
+
* the child to end with `LINT-FIX: DONE` or `LINT-FIX: BLOCKED <why>`, and the
|
|
17
|
+
* call site was `await deps.runChild(...)` with the return value DISCARDED —
|
|
18
|
+
* nothing in `src/` or `scripts/` parsed it. The twin parsed its own marker and
|
|
19
|
+
* used it to skip the expensive gate re-run. So a lint-fix child that reported
|
|
20
|
+
* BLOCKED still paid the full guard stack plus a whole repo-health run (15–69s
|
|
21
|
+
* measured), and the user was told `did not converge: <health.reason>` instead
|
|
22
|
+
* of the child's own stated reason. The suite fed `'LINT-FIX: DONE'` as fake
|
|
23
|
+
* output, so it stayed green whether the marker was parsed or deleted.
|
|
24
|
+
* - **The cancel rung re-typed its constant.** `lint-fix.ts` compared against a
|
|
25
|
+
* literal `'__user_cancelled__'`; it was the only production site in `src/` not
|
|
26
|
+
* importing `USER_CANCELLED`.
|
|
27
|
+
*
|
|
28
|
+
* What stays per-site: the arbiter (a gate re-run vs a repo-health re-run), the
|
|
29
|
+
* result shape, and the guard sets. Only the child call is shared.
|
|
30
|
+
*/
|
|
31
|
+
import { USER_CANCELLED } from './child-runner.js';
|
|
32
|
+
/**
|
|
33
|
+
* Parse a fix child's final marker. Last match wins: the model reasons before
|
|
34
|
+
* concluding, and bash output can echo the words. No marker → DONE, because the
|
|
35
|
+
* pass's own arbiter re-runs either way; a missing marker only forfeits the
|
|
36
|
+
* early-out on a self-declared BLOCKED.
|
|
37
|
+
*/
|
|
38
|
+
export function parseFixMarker(marker, text) {
|
|
39
|
+
const re = new RegExp(`${marker}:\\s*(DONE|BLOCKED)\\b[ \\t]*(.*)`, 'gi');
|
|
40
|
+
let last = null;
|
|
41
|
+
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
42
|
+
last = m;
|
|
43
|
+
if (!last)
|
|
44
|
+
return { blocked: false };
|
|
45
|
+
if (last[1].toUpperCase() === 'BLOCKED') {
|
|
46
|
+
return { blocked: true, note: last[2].trim() || 'no reason given' };
|
|
47
|
+
}
|
|
48
|
+
return { blocked: false, note: last[2].trim() || undefined };
|
|
49
|
+
}
|
|
50
|
+
/** Run one fix child through the four-rung ladder. Cancel THROWS; nothing else does. */
|
|
51
|
+
export async function runFixChild(input) {
|
|
52
|
+
let text;
|
|
53
|
+
try {
|
|
54
|
+
text = await input.runChild(input.tools, input.prompt, input.signal);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
if (msg === USER_CANCELLED)
|
|
59
|
+
throw err;
|
|
60
|
+
return { kind: 'error', msg };
|
|
61
|
+
}
|
|
62
|
+
const parsed = parseFixMarker(input.marker, text);
|
|
63
|
+
if (parsed.blocked)
|
|
64
|
+
return { kind: 'blocked', text, note: parsed.note ?? 'no reason given' };
|
|
65
|
+
return parsed.note ? { kind: 'done', text, note: parsed.note } : { kind: 'done', text };
|
|
66
|
+
}
|