@mjasnikovs/pi-task 0.38.10 → 0.38.12
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/README.md +7 -3
- package/dist/shared/child-process.d.ts +8 -0
- package/dist/shared/command-watchdog.d.ts +1 -1
- package/dist/shared/command-watchdog.js +1 -1
- package/dist/task/accept-debt.d.ts +47 -0
- package/dist/task/accept-debt.js +127 -28
- package/dist/task/auto-orchestrator.js +93 -116
- package/dist/task/boot-probe.d.ts +298 -0
- package/dist/task/boot-probe.js +806 -0
- package/dist/task/child-runner.d.ts +56 -25
- package/dist/task/child-runner.js +65 -31
- package/dist/task/child-status.d.ts +95 -0
- package/dist/task/child-status.js +99 -0
- package/dist/task/command-run.d.ts +36 -0
- package/dist/task/command-run.js +48 -1
- package/dist/task/command-watchdog.js +1 -1
- package/dist/task/context-usage.d.ts +4 -3
- package/dist/task/context-usage.js +4 -3
- package/dist/task/contracts.js +18 -35
- package/dist/task/deep-render-check.d.ts +47 -0
- package/dist/task/deep-render-check.js +110 -65
- package/dist/task/env-notes.d.ts +3 -3
- package/dist/task/env-notes.js +24 -35
- package/dist/task/final-gate-fix.d.ts +1 -1
- package/dist/task/final-gate-fix.js +1 -1
- package/dist/task/final-gate.d.ts +38 -432
- package/dist/task/final-gate.js +105 -1213
- package/dist/task/gate-child.d.ts +8 -10
- package/dist/task/gate-child.js +15 -19
- package/dist/task/gate-deps.d.ts +29 -0
- package/dist/task/gate-deps.js +192 -206
- package/dist/task/gate-tally.d.ts +189 -0
- package/dist/task/gate-tally.js +249 -0
- package/dist/task/implementation-turn.d.ts +201 -0
- package/dist/task/implementation-turn.js +263 -0
- package/dist/task/launch-contract.js +27 -43
- package/dist/task/launch-manifest.d.ts +5 -0
- package/dist/task/launch-manifest.js +21 -0
- package/dist/task/ledger.d.ts +38 -0
- package/dist/task/ledger.js +83 -0
- package/dist/task/loop-detector.d.ts +14 -8
- package/dist/task/loop-detector.js +36 -12
- package/dist/task/orchestrator.d.ts +61 -126
- package/dist/task/orchestrator.js +70 -297
- package/dist/task/phases.d.ts +18 -0
- package/dist/task/phases.js +4 -3
- package/dist/task/plan-orchestrator.js +34 -33
- package/dist/task/requirements.d.ts +1 -1
- package/dist/task/requirements.js +50 -66
- package/dist/task/root-cause-repair.js +20 -32
- package/dist/task/run-bracket.d.ts +75 -0
- package/dist/task/run-bracket.js +41 -0
- package/dist/task/stall-detector.d.ts +110 -0
- package/dist/task/stall-detector.js +159 -0
- package/dist/task/task-gates.d.ts +69 -0
- package/dist/task/task-gates.js +114 -90
- package/dist/task/verify-work.d.ts +53 -67
- package/dist/task/verify-work.js +15 -11
- package/dist/workers/docs-core.d.ts +0 -4
- package/dist/workers/docs-core.js +10 -34
- package/dist/workers/docs-project.js +3 -3
- package/dist/workers/docs-resolve.d.ts +18 -0
- package/dist/workers/docs-resolve.js +39 -0
- package/dist/workers/docs-retrieve.d.ts +13 -0
- package/dist/workers/docs-retrieve.js +17 -2
- package/dist/workers/fetch-core.d.ts +0 -4
- package/dist/workers/fetch-core.js +2 -5
- package/dist/workers/phantom-imports.d.ts +3 -3
- package/dist/workers/phantom-imports.js +16 -29
- package/dist/workers/pi-worker-docs.d.ts +49 -0
- package/dist/workers/pi-worker-docs.js +33 -9
- package/dist/workers/pi-worker-fetch.d.ts +18 -0
- package/dist/workers/pi-worker-fetch.js +19 -4
- package/dist/workers/single-read-extension.d.ts +1 -1
- package/dist/workers/single-read-extension.js +5 -4
- package/dist/workers/single-read-guard.d.ts +32 -10
- package/dist/workers/single-read-guard.js +67 -16
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type HealthCommand } from './repo-health-check.js';
|
|
2
|
-
import {
|
|
3
|
-
import { type
|
|
4
|
-
import { type DeepRenderOutcome } from './deep-render-check.js';
|
|
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';
|
|
5
4
|
import { type CommandRunner } from './command-run.js';
|
|
6
5
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
6
|
+
import { type EnvClosure } from './env-template-closure.js';
|
|
7
|
+
import { observabilityGapFailure, unobservedVerdict } from './gate-tally.js';
|
|
7
8
|
export interface FinalGateOutcome {
|
|
8
9
|
/** true → statics and every runnable integration command passed (or nothing to run). */
|
|
9
10
|
ok: boolean;
|
|
@@ -106,236 +107,6 @@ export declare function discoverIntegrationCommands(cwd: string): {
|
|
|
106
107
|
};
|
|
107
108
|
/** Every lockfile consistency check that applies to this tree (possibly none). */
|
|
108
109
|
export declare function discoverLockfileChecks(cwd: string): HealthCommand[];
|
|
109
|
-
/**
|
|
110
|
-
* Why this script is NOT a launch of the shipped app, or null when it plausibly
|
|
111
|
-
* is one (mx5 run 18, validated).
|
|
112
|
-
*
|
|
113
|
-
* Run 18's boot command resolved to `bun run dev`, whose body is
|
|
114
|
-
* `docker compose -f docker-compose.dev.yml up -d && until docker compose … pg_isready
|
|
115
|
-
* … && concurrently "bun run dev:css" "bun run dev:js" "bun run --watch
|
|
116
|
-
* src/server/index.ts"`. The gate sandbox has no docker, so the chain died at 127 and
|
|
117
|
-
* the boot SKIPPED as an environment gap — while the shipped app had no HTTP listener
|
|
118
|
-
* at all. A script whose first act is `docker compose up` cannot distinguish "the app
|
|
119
|
-
* is broken" from "this box has no docker", so it is not evidence either way: better
|
|
120
|
-
* to discover NO boot command — reported as "nothing to boot" — and let the static
|
|
121
|
-
* serve-entry check (serve-entry.ts) carry the signal, than to spend the grace window
|
|
122
|
-
* producing an unfalsifiable skip.
|
|
123
|
-
*
|
|
124
|
-
* CONSERVATIVE AND LEXICAL BY CONSTRUCTION. Only two shapes are rejected, both
|
|
125
|
-
* decidable from the script text alone:
|
|
126
|
-
* 1. the chain OPENS with container orchestration (docker/podman/nerdctl … up|start|run);
|
|
127
|
-
* 2. the whole body is a multiplexer (concurrently/npm-run-all/run-p/run-s/turbo)
|
|
128
|
-
* whose every child is an ASSET watcher in watch mode (tailwind/tsc/esbuild/…),
|
|
129
|
-
* i.e. nothing in it can ever listen.
|
|
130
|
-
* Anything else — `vite`, `next dev`, `node dist/index.js`, `nodemon`, `bun --watch
|
|
131
|
-
* src/index.ts`, and any multiplexer with one non-asset child — is accepted
|
|
132
|
-
* unchanged. Deciding whether a watcher actually SERVES is not attempted here; that
|
|
133
|
-
* is exactly what the static serve-entry check is for.
|
|
134
|
-
*/
|
|
135
|
-
export declare function nonLaunchScriptReason(body: string, scripts?: Record<string, string>): string | null;
|
|
136
|
-
/**
|
|
137
|
-
* The project's OWN launch command, if it declares one (package.json `start`,
|
|
138
|
-
* else `dev`; Makefile `run`). null means the project has nothing to boot —
|
|
139
|
-
* the boot check degrades to nothing-to-run.
|
|
140
|
-
*
|
|
141
|
-
* A script that is not a LAUNCH at all (nonLaunchScriptReason — mx5 run 18's
|
|
142
|
-
* `docker compose up` orchestrator) is rejected here and falls through to the
|
|
143
|
-
* next candidate, then to null. Discovering nothing is strictly better than
|
|
144
|
-
* discovering something unfalsifiable: an env-gap skip of an orchestration script
|
|
145
|
-
* says nothing about the app, and null is reported as "nothing to boot".
|
|
146
|
-
*/
|
|
147
|
-
export declare function discoverBootCommand(cwd: string): HealthCommand | null;
|
|
148
|
-
/**
|
|
149
|
-
* The launch script that EXISTS but was rejected as not-a-launch, if any. Without
|
|
150
|
-
* this the rejection would trade run 18's unfalsifiable skip for pure silence: no
|
|
151
|
-
* boot command means bootSkipVerdict has no label to name, and a project whose test
|
|
152
|
-
* suite ran still reports `observed > 0`, so unobservedVerdict stays quiet too. A
|
|
153
|
-
* served app whose only declared launch script cannot start it was not observed to
|
|
154
|
-
* run, and must say so.
|
|
155
|
-
*/
|
|
156
|
-
export declare function rejectedLaunchScript(cwd: string): {
|
|
157
|
-
name: string;
|
|
158
|
-
reason: string;
|
|
159
|
-
} | null;
|
|
160
|
-
type BootOutcome = {
|
|
161
|
-
outcome: 'skip' | 'pass';
|
|
162
|
-
/** Set when the render check could not OBSERVE the served page (no browser,
|
|
163
|
-
* undeterminable port) or its AUTHENTICATED half (no declared credentials,
|
|
164
|
-
* an undrivable sign-in form, credentials the server rejected) — surfaced
|
|
165
|
-
* by the gate as an UNOBSERVED warning. */
|
|
166
|
-
renderNote?: string;
|
|
167
|
-
/** skip only: the boot command never spawned (ENOENT) — feeds the
|
|
168
|
-
* full-blindness guard (mx5 run 16), unlike a 127 where the runner ran. */
|
|
169
|
-
spawnFailed?: boolean;
|
|
170
|
-
} | {
|
|
171
|
-
outcome: 'fail';
|
|
172
|
-
detail: string;
|
|
173
|
-
} | {
|
|
174
|
-
outcome: 'orphan-port';
|
|
175
|
-
detail: string;
|
|
176
|
-
port: number | null;
|
|
177
|
-
};
|
|
178
|
-
/** Injectable environment probes for the boot check's orphan-port recovery, so the
|
|
179
|
-
* reap-and-retry path is deterministically testable without a real listener. */
|
|
180
|
-
export interface BootDeps {
|
|
181
|
-
/** The pid + command line holding `port` in LISTEN, or null if none/unknown. */
|
|
182
|
-
findPortHolder?: (port: number) => {
|
|
183
|
-
pid: number;
|
|
184
|
-
command: string;
|
|
185
|
-
} | null;
|
|
186
|
-
/** Terminate a pid we attribute to ourselves; returns whether it was signalled. */
|
|
187
|
-
reap?: (pid: number) => boolean;
|
|
188
|
-
/**
|
|
189
|
-
* Does process group `pgid` currently own a LISTENing TCP socket? Drives the
|
|
190
|
-
* served-app boot check (mx5 run 10): a watcher (`dev` = tailwind/bundler
|
|
191
|
-
* --watch) stays alive forever without ever listening, so "still alive after the
|
|
192
|
-
* grace window = PASS" blessed a project that cannot serve a single request.
|
|
193
|
-
* Injected so the listener requirement is deterministically testable without a
|
|
194
|
-
* real socket; the default probes ss/lsof + pgid.
|
|
195
|
-
*/
|
|
196
|
-
groupHasListener?: (pgid: number) => boolean;
|
|
197
|
-
/**
|
|
198
|
-
* The (lowest) TCP port a listener owned by process group `pgid` is bound to,
|
|
199
|
-
* or null when it cannot be determined. Feeds the render check's URL; injected
|
|
200
|
-
* for tests, default probes ss/lsof + pgid.
|
|
201
|
-
*/
|
|
202
|
-
groupListeningPort?: (pgid: number) => number | null;
|
|
203
|
-
/**
|
|
204
|
-
* Load the served page once in a headless browser and judge the RENDERED DOM
|
|
205
|
-
* (mx5 runs 8/11: curl cannot execute JS, so a blank-mount app passed every
|
|
206
|
-
* gate). Runs only for a served app, against the live listener, before the
|
|
207
|
-
* boot child is killed. Absent → the boot check behaves exactly as before;
|
|
208
|
-
* the gate wires runRenderCheck by default for served apps.
|
|
209
|
-
*/
|
|
210
|
-
renderProbe?: (url: string) => RenderOutcome;
|
|
211
|
-
/**
|
|
212
|
-
* SIGN IN on the served page and judge the AUTHENTICATED half of the app (mx5
|
|
213
|
-
* run 17). Runs only after `renderProbe` PASSED — the shallow blank-page rule
|
|
214
|
-
* keeps its own RED/GREEN-proven verdict and is never shadowed by this one.
|
|
215
|
-
* Absent → the boot check behaves exactly as before; the gate wires
|
|
216
|
-
* runDeepRenderCheck by default for served apps. May only FAIL when the SERVER
|
|
217
|
-
* itself authenticated the session (see deep-render-check.judgeDeepSession);
|
|
218
|
-
* anything else — no browser, no declared credentials, an undrivable form,
|
|
219
|
-
* rejected credentials — is an env gap and skips with an UNOBSERVED note.
|
|
220
|
-
*/
|
|
221
|
-
deepRenderProbe?: (url: string) => DeepRenderOutcome | Promise<DeepRenderOutcome>;
|
|
222
|
-
/**
|
|
223
|
-
* Can this box enumerate listeners with pids AT ALL (ss/netstat/lsof)? False
|
|
224
|
-
* means the served-app requirement is UNOBSERVABLE here and must degrade to the
|
|
225
|
-
* survival rule rather than fail — see canEnumerateListeners.
|
|
226
|
-
*/
|
|
227
|
-
enumerationCapable?: () => boolean;
|
|
228
|
-
/**
|
|
229
|
-
* Reserve a free port to hand the boot child as PORT, so an HTTP answer on it is
|
|
230
|
-
* ownership evidence. null → no port could be reserved (the check then relies on
|
|
231
|
-
* pgid attribution alone). Injected for tests.
|
|
232
|
-
*/
|
|
233
|
-
pickPort?: () => Promise<number | null>;
|
|
234
|
-
/**
|
|
235
|
-
* The port the project's own client was BUILT to call, when it declares one and
|
|
236
|
-
* nothing is holding it — preferred over a freshly reserved port so the served
|
|
237
|
-
* origin and the origin the client calls are the same one (see pinnedLocalPort).
|
|
238
|
-
* null → use the reserved private port exactly as before.
|
|
239
|
-
*/
|
|
240
|
-
preferredPort?: () => Promise<number | null>;
|
|
241
|
-
/** Does anything answer HTTP on 127.0.0.1:`port`? Injected for tests. */
|
|
242
|
-
httpProbe?: (port: number) => boolean;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Does the finished run stand up a listening HTTP server? Deterministic, from the
|
|
246
|
-
* built manifest (a server-framework dependency is the plan's own artifact) OR, when
|
|
247
|
-
* available, the plan/spec text. Used to decide whether the boot check must observe a
|
|
248
|
-
* LISTENER (served app) or may pass on mere survival / quick exit (CLI project).
|
|
249
|
-
*/
|
|
250
|
-
export declare function detectsServedApp(cwd: string, planText?: string): boolean;
|
|
251
|
-
/** `ss -tlnpH` rows → {pid, port}. Column 4 (0-based 3) is the local address; the
|
|
252
|
-
* port is its last `:`-suffixed number ("0.0.0.0:3000", "[::]:3000"). */
|
|
253
|
-
export declare function parseSsListeners(stdout: string): Array<{
|
|
254
|
-
pid: number;
|
|
255
|
-
port: number;
|
|
256
|
-
}>;
|
|
257
|
-
/**
|
|
258
|
-
* `netstat -tlnp` rows → {pid, port} (mx5 run 14, validated: the agent-sandbox
|
|
259
|
-
* image ships NEITHER ss NOR lsof — only ps and netstat — so the served-app boot
|
|
260
|
-
* check could never observe a listener and failed unfalsifiably). The pid rides
|
|
261
|
-
* in the trailing "PID/Program name" column ("1234/bun"); rows the kernel will
|
|
262
|
-
* not attribute to us print "-" there and are skipped.
|
|
263
|
-
*/
|
|
264
|
-
export declare function parseNetstatListeners(stdout: string): Array<{
|
|
265
|
-
pid: number;
|
|
266
|
-
port: number;
|
|
267
|
-
}>;
|
|
268
|
-
/** `lsof -iTCP -sTCP:LISTEN -n -P` rows → {pid, port}. */
|
|
269
|
-
export declare function parseLsofListeners(stdout: string): Array<{
|
|
270
|
-
pid: number;
|
|
271
|
-
port: number;
|
|
272
|
-
}>;
|
|
273
|
-
export declare function canEnumerateListeners(): boolean;
|
|
274
|
-
/** Test seam: forget the memoised capability answer. */
|
|
275
|
-
export declare function resetListenerToolCapability(): void;
|
|
276
|
-
/**
|
|
277
|
-
* A free TCP port on the loopback interface, or null if one cannot be reserved.
|
|
278
|
-
* The boot check hands this to the child as PORT so that a successful HTTP
|
|
279
|
-
* request to it is OWNERSHIP evidence: nobody else knows the number (mx5 runs
|
|
280
|
-
* 8/10/11 — orphaned servers from earlier checks answered curl on the
|
|
281
|
-
* conventional :3000 and passed checks the app had not earned).
|
|
282
|
-
*/
|
|
283
|
-
export declare function pickFreePort(): Promise<number | null>;
|
|
284
|
-
/** Can we bind 127.0.0.1:`port` right now? (Free ⇒ the boot child can have it.) */
|
|
285
|
-
export declare function isPortFree(port: number): Promise<boolean>;
|
|
286
|
-
/**
|
|
287
|
-
* The project's own declared local port, but only if nothing is holding it — the
|
|
288
|
-
* default `preferredPort` for the gate. A declared port that is BUSY falls back to
|
|
289
|
-
* a reserved one rather than colliding: a stranger's server on :3000 must never be
|
|
290
|
-
* mistaken for the app we just booted.
|
|
291
|
-
*/
|
|
292
|
-
export declare function preferredDeclaredPort(cwd: string): Promise<number | null>;
|
|
293
|
-
/**
|
|
294
|
-
* Exercise the start command ONCE. For a CLI project (`expectServer` false) the
|
|
295
|
-
* command's own fate within the grace window decides:
|
|
296
|
-
*
|
|
297
|
-
* - non-zero exit (or signal death) before the window closes → FAIL, output tail;
|
|
298
|
-
* - exit 0 before the window closes → PASS (a CLI-style "run" that finished);
|
|
299
|
-
* - still alive when the window closes → PASS, then the whole process group is
|
|
300
|
-
* killed (detached spawn = own group; SIGTERM, escalating to SIGKILL).
|
|
301
|
-
*
|
|
302
|
-
* For a SERVED app (`expectServer` true — the spec/plan promised an HTTP server) mere
|
|
303
|
-
* survival is not enough: a watcher (`dev` = tailwind/bundler --watch) stays alive
|
|
304
|
-
* forever without ever listening, and a type-only entrypoint exits 0 in <1s having
|
|
305
|
-
* served nothing (mx5 run 10 — both were blessed by the survival rule). The boot then
|
|
306
|
-
* PASSes only once a LISTENing socket owned by our process group is observed; if the
|
|
307
|
-
* command exits, or the grace window closes, with no listener ever seen → FAIL naming
|
|
308
|
-
* that a listening server was expected.
|
|
309
|
-
*
|
|
310
|
-
* OBSERVABILITY is a precondition of that FAIL (mx5 run 14, validated). The listener
|
|
311
|
-
* requirement needs pgid-attributed socket enumeration; win32 has none, and neither
|
|
312
|
-
* does a Linux image shipping no ss/netstat/lsof — run 14's sandbox was exactly that,
|
|
313
|
-
* so the check emitted "never opened a listening socket" against an app that
|
|
314
|
-
* demonstrably served, three autofix passes could not falsify it, and the run was
|
|
315
|
-
* recorded failed. Two defences, in order:
|
|
316
|
-
*
|
|
317
|
-
* - the child is spawned with a freshly reserved, otherwise-unused PORT, and an
|
|
318
|
-
* HTTP answer on THAT port proves a listener regardless of tooling. The private
|
|
319
|
-
* port is what makes the HTTP probe trustworthy: an orphaned server from an
|
|
320
|
-
* earlier check answers on :3000, but nobody else knows this number.
|
|
321
|
-
* - if nothing can enumerate listeners AND the assigned port never answered, the
|
|
322
|
-
* served-app requirement is unobservable here, so `expectServer` collapses to
|
|
323
|
-
* the survival rule and the PASS is stamped UNOBSERVED. An app that ignores PORT
|
|
324
|
-
* is indistinguishable from one that never listened — an observer limitation,
|
|
325
|
-
* not an app defect, and it may not be reported as one.
|
|
326
|
-
*
|
|
327
|
-
* A child that EXITS non-zero still FAILs in every environment: "the process died"
|
|
328
|
-
* needs no socket probe, so run 14's original true positive (a `--hot` runtime
|
|
329
|
-
* pinning a crashed app) stays reportable wherever the tooling exists.
|
|
330
|
-
*
|
|
331
|
-
* Env-gap contract as everywhere: spawn error (ENOENT) or a command-not-found
|
|
332
|
-
* inside the chain (exit 127, or the runner's own wording where the platform
|
|
333
|
-
* reports it that way — see isCommandNotFound) → skip.
|
|
334
|
-
*/
|
|
335
|
-
export declare function runBootCheck(cwd: string, [bin, args]: HealthCommand, graceMs?: number, opts?: {
|
|
336
|
-
expectServer?: boolean;
|
|
337
|
-
deps?: BootDeps;
|
|
338
|
-
}): Promise<BootOutcome>;
|
|
339
110
|
/**
|
|
340
111
|
* Labels (`bin args…`) of every command the gate CAN currently discover — the
|
|
341
112
|
* static half (repo-health) plus the integration half. Pure discovery, nothing
|
|
@@ -359,205 +130,12 @@ export declare function discoverGateCommandLabels(cwd: string): string[];
|
|
|
359
130
|
* guard already owns.
|
|
360
131
|
*/
|
|
361
132
|
export declare function discoverGateCommandBodies(cwd: string): Record<string, string>;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
* pass — it ran and exited 0. The ONLY outcome that may close a debt.
|
|
365
|
-
* fail — it ran and exited non-zero for a real reason. Debt stays open.
|
|
366
|
-
* gap — nothing was observed: the shell/runner never spawned, 127 inside the
|
|
367
|
-
* chain, a timeout, a missing browser, or absent external infrastructure.
|
|
368
|
-
* INCONCLUSIVE, so the debt stays open (surface, never re-hide).
|
|
369
|
-
*/
|
|
370
|
-
export type VerifyRerunOutcome = {
|
|
371
|
-
outcome: 'pass';
|
|
372
|
-
} | {
|
|
373
|
-
outcome: 'fail';
|
|
374
|
-
status: number;
|
|
375
|
-
tail: string;
|
|
376
|
-
} | {
|
|
377
|
-
outcome: 'gap';
|
|
378
|
-
detail: string;
|
|
379
|
-
};
|
|
380
|
-
/**
|
|
381
|
-
* Re-run one VERIFY-block command line (nexttask 5) under the gate's existing
|
|
382
|
-
* env-gap contract, so a debt whose reason NAMES that command can be closed by the
|
|
383
|
-
* command itself rather than by a judgement about it.
|
|
384
|
-
*
|
|
385
|
-
* Runs through `sh -c` because a VERIFY line is a shell line, not an argv: run 19's
|
|
386
|
-
* is `AGENT=1 bun test test/listings.test.ts`, and env prefixes, `&&` and redirects
|
|
387
|
-
* are all ordinary there. The leading command word is still resolved through
|
|
388
|
-
* runner-resolve so a login-shell-stripped PATH cannot make every re-run look like a
|
|
389
|
-
* gap (mx5 run 16's blindness, one level down).
|
|
390
|
-
*
|
|
391
|
-
* The asymmetry is the point: only exit 0 is conclusive. Every other ending — real
|
|
392
|
-
* failure, missing tool, unreachable database, timeout, no POSIX shell — leaves the
|
|
393
|
-
* debt exactly as open as it was.
|
|
394
|
-
*/
|
|
395
|
-
export declare function runVerifyCommandLine(cwd: string, line: string, timeoutMs: number, extraGapRe?: RegExp,
|
|
396
|
-
/** The spawner. Injected so a re-run's outcome can be tested without one. */
|
|
397
|
-
run?: CommandRunner): VerifyRerunOutcome;
|
|
398
|
-
/**
|
|
399
|
-
* The full-skip blindness guard (mx5 run 16, validated): dynamic commands were
|
|
400
|
-
* DISCOVERED but every single one skipped as an environment gap, so the gate
|
|
401
|
-
* decided on statics alone and stamped a permanently blank app green. Per-command
|
|
402
|
-
* env-gap skips stay legitimate (a missing browser must not fail a suite); what
|
|
403
|
-
* may never happen again is ALL of them skipping while the gate still reports
|
|
404
|
-
* PASS — a gate that observed nothing dynamic has no basis to vouch for the
|
|
405
|
-
* assembled app. Pure so the semantics are unit-tested; the caller feeds it the
|
|
406
|
-
* attempt/observation counters and runner resolvability.
|
|
407
|
-
*/
|
|
408
|
-
export declare function observabilityGapFailure(args: {
|
|
409
|
-
/** Dynamic commands the gate discovered and tried to run. */
|
|
410
|
-
attempted: number;
|
|
411
|
-
/** Of those, how many it actually OBSERVED (a real pass OR a real fail —
|
|
412
|
-
* either proves the command ran; only skips observe nothing). */
|
|
413
|
-
observed: number;
|
|
414
|
-
/** Of the skips, how many were SPAWN failures (runner never ran, ENOENT).
|
|
415
|
-
* Tool-level gaps (missing browser, 127 inside the chain, timeout) prove
|
|
416
|
-
* the runner itself works and keep the classic env-gap contract — the
|
|
417
|
-
* blindness class fires only when EVERY attempt failed to even spawn. */
|
|
418
|
-
spawnFailures: number;
|
|
419
|
-
/** Distinct runner bins across the attempted commands. */
|
|
420
|
-
runnerBins: string[];
|
|
421
|
-
/** Is this runner spawnable (bare or via a known install location)? */
|
|
422
|
-
runnerResolvable: (bin: string) => boolean;
|
|
423
|
-
}): string | null;
|
|
424
|
-
/**
|
|
425
|
-
* The THIRD verdict. observabilityGapFailure above covers "commands were DISCOVERED
|
|
426
|
-
* but every one failed to spawn" — a rank-0 FAIL. It deliberately returns null for
|
|
427
|
-
* `attempted === 0`, and until now that silence fell straight through to
|
|
428
|
-
* `PASS — no integration command found (statics passed)`: the run-16 blindness class
|
|
429
|
-
* entering through a different door, where "we never checked" reads exactly like "we
|
|
430
|
-
* checked and it was fine". Measured 2026-07-27: IAR1 (C++/CMake, no package.json)
|
|
431
|
-
* shipped that verdict TWICE while carrying 2 and 3 open verify-FAIL debts, and
|
|
432
|
-
* godot-engine (package.json whose only script is `verify`) reproduces it live today.
|
|
433
|
-
*
|
|
434
|
-
* So: observed anything dynamic ⇒ PASS; discovered-but-all-spawn-failed ⇒ the
|
|
435
|
-
* existing FAIL; observed NOTHING ⇒ this note, carried on an `ok: true` outcome.
|
|
436
|
-
*
|
|
437
|
-
* WHY NON-BLOCKING (decided, not deferred — the evidence cuts both ways and this is
|
|
438
|
-
* the resolution):
|
|
439
|
-
* - Blocking's case: both real occurrences also carried open verify-FAIL debt, so
|
|
440
|
-
* the runs with no dynamic evidence were exactly the runs already known to be
|
|
441
|
-
* carrying defects.
|
|
442
|
-
* - Against, and decisive: (1) that debt is ALREADY surfaced unconditionally at the
|
|
443
|
-
* gate moment, on PASS as on FAIL — the IAR1 records literally read "PASS — no
|
|
444
|
-
* integration command found … UNRESOLVED VERIFY-FAIL DEBT still open (2)". The
|
|
445
|
-
* missing signal was never the debt, it was the word PASS endorsing the run, and
|
|
446
|
-
* that is what this fixes. (2) `ok: false` routes into the autofix picker, whose
|
|
447
|
-
* seed is `reason`; "no integration command is discoverable" is not fixable by
|
|
448
|
-
* editing code, so the highest-probability child response is to FABRICATE a
|
|
449
|
-
* runnable command to satisfy the gate — the same fabrication class that refuted
|
|
450
|
-
* the `## verified tooling` harvest (see discoverIntegrationCommands) and that had
|
|
451
|
-
* run 11's fix child `rm` a sibling's deliverable. (3) That harvest being refuted
|
|
452
|
-
* means IAR1 and godot-engine can NEVER discover a command, so blocking would end
|
|
453
|
-
* every non-npm run in `failed` permanently, with no remedy — the task's own I3
|
|
454
|
-
* ("show blocking does not block IAR1/godot post-Task-1") is unsatisfiable, and
|
|
455
|
-
* its stated consequence is to downgrade to a warning and say so. This is that.
|
|
456
|
-
* The teeth are elsewhere and are real: the verdict word changes, the gate trail says
|
|
457
|
-
* UNOBSERVED, and the caller records a durable final-gate debt that the NEXT run's
|
|
458
|
-
* gate re-surfaces (it can never auto-close — it is not static-class).
|
|
459
|
-
*/
|
|
460
|
-
export declare function unobservedVerdict(args: {
|
|
461
|
-
/** Dynamic commands the gate discovered and tried to run (0 ⇒ nothing existed). */
|
|
462
|
-
discovered: number;
|
|
463
|
-
/** Of those, how many actually RAN (a real pass or a real fail). */
|
|
464
|
-
observed: number;
|
|
465
|
-
}): string | null;
|
|
466
|
-
/**
|
|
467
|
-
* The SAME third verdict, at the door unobservedVerdict cannot reach: the boot
|
|
468
|
-
* check specifically (mx5 run 18, validated).
|
|
469
|
-
*
|
|
470
|
-
* Run 18 shipped an app with no HTTP server behind a converged final gate. Its
|
|
471
|
-
* `src/server/index.ts` ends at `export {app}` — no `Bun.serve`, no
|
|
472
|
-
* `export default app`, no `start` script — so `bun run src/server/index.ts` exits
|
|
473
|
-
* 0 immediately and the product cannot be started at all. The gate's boot command
|
|
474
|
-
* resolved to `bun run dev`, whose body begins `docker compose … up -d`; the gate
|
|
475
|
-
* sandbox had no docker, so the boot SKIPPED as an environment gap. Skips
|
|
476
|
-
* contribute nothing to `dynObserved`, and `bun run test`, `test:ct`, `build`,
|
|
477
|
-
* `lint`, `seed` and `migrate` all ran — so `dynObserved > 0`, the full-skip
|
|
478
|
-
* blindness guard (observabilityGapFailure) stayed correctly quiet, and the trail
|
|
479
|
-
* read `final-gate: autofix converged — statics + … passed` with 24/24 tasks green.
|
|
480
|
-
*
|
|
481
|
-
* The defect is that "the app was never observed to boot" and "the app booted
|
|
482
|
-
* fine" produced BYTE-IDENTICAL gate output. That is the class scripts/ab-verdict.ts
|
|
483
|
-
* exists to kill one layer up: absence of evidence rendered in the shape of
|
|
484
|
-
* evidence. So a discovered-but-skipped boot now names itself, and — unlike every
|
|
485
|
-
* other skip — it CANNOT be cancelled by observations from other commands.
|
|
486
|
-
* Component tests are the trap here, not the alibi: run 18 had 51 green Playwright
|
|
487
|
-
* CT tests, and CT mounts components in a browser without ever assembling or
|
|
488
|
-
* starting the server.
|
|
489
|
-
*
|
|
490
|
-
* DECIDED, do not silently re-open:
|
|
491
|
-
* - NOT a FAIL. A boot skip on a docker-less box is a genuine environment gap, and
|
|
492
|
-
* failing it re-creates run 16's unfalsifiable-FAIL mistake pointing the other
|
|
493
|
-
* way. UNOBSERVED blocks nothing while being loud and durable (the caller records
|
|
494
|
-
* it as final-gate debt the next run re-surfaces), and it keeps "boot never ran"
|
|
495
|
-
* out of the autofix child's seed — a child cannot fix a missing docker, so the
|
|
496
|
-
* highest-probability response would be to FABRICATE a bootable command, the
|
|
497
|
-
* class that refuted the `## verified tooling` harvest.
|
|
498
|
-
* - BOTH skip flavours count. Run 18's skip carried `spawnFailed: false` (127 inside
|
|
499
|
-
* the script chain, not an ENOENT on the runner), so keying off spawnFailed would
|
|
500
|
-
* have missed the actual defect.
|
|
501
|
-
* - SERVED APPS ONLY. `expectServer === false` (a CLI/library project) is fenced off
|
|
502
|
-
* deliberately: a CLI whose `dev` script needs an absent tool has no server to be
|
|
503
|
-
* unobserved, and widening the lever there buys warnings nobody can act on.
|
|
504
|
-
*/
|
|
505
|
-
export declare function bootSkipVerdict(args: {
|
|
506
|
-
/** `bin args…` of the DISCOVERED boot command; null ⇒ nothing to boot, which is
|
|
507
|
-
* not the same thing as a boot that was not observed. */
|
|
508
|
-
label: string | null;
|
|
509
|
-
/** Did the boot check end in `skip` (either flavour)? */
|
|
510
|
-
skipped: boolean;
|
|
511
|
-
/** Does this project stand up an HTTP server (detectsServedApp)? */
|
|
512
|
-
expectServer: boolean;
|
|
513
|
-
}): string | null;
|
|
133
|
+
export { runVerifyCommandLine, type VerifyRerunOutcome } from './command-run.js';
|
|
134
|
+
export { observabilityGapFailure, unobservedVerdict };
|
|
514
135
|
export { taskThatIntroduced };
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
* tree. A static-class debt whose statics now pass is provably RESOLVED (a later
|
|
519
|
-
* task fixed it) and pruned from the ledger; every other debt cannot be proven
|
|
520
|
-
* resolved deterministically, so it stays OPEN and is surfaced — a run may not
|
|
521
|
-
* complete silently carrying an accepted defect. FP-safe by construction (see
|
|
522
|
-
* accept-debt.ts). Best-effort: a ledger read/write failure must never break the
|
|
523
|
-
* caller.
|
|
524
|
-
*
|
|
525
|
-
* FACTORED OUT of runFinalIntegrationGate (nexttask 6): the derivation has to be
|
|
526
|
-
* runnable at a SECOND moment — after a converged final-gate autofix, where the
|
|
527
|
-
* orchestrator used to rebuild its gate outcome as a bare `{ok, reason}` and drop
|
|
528
|
-
* `openDebts` entirely. The report a run ends on has to be derived from the tree
|
|
529
|
-
* the run ends with, not from the tree as it was before the fix pass.
|
|
530
|
-
*
|
|
531
|
-
* `staticOk` is the caller's claim about the CURRENT statics, and it is the only
|
|
532
|
-
* thing that can auto-close a static-class debt — so a caller that does not know
|
|
533
|
-
* must pass `false` (unprovable ⇒ stays open), never a guess.
|
|
534
|
-
*/
|
|
535
|
-
export declare function deriveOpenDebts(cwd: string, staticOk: boolean): Promise<{
|
|
536
|
-
openDebts: AcceptDebt[];
|
|
537
|
-
debtNote?: string;
|
|
538
|
-
trail?: string[];
|
|
539
|
-
}>;
|
|
540
|
-
/**
|
|
541
|
-
* Re-run ONE debt's stored VERIFY command for the re-check, with the no-write guard
|
|
542
|
-
* (`inv-no-write`) wrapped around it.
|
|
543
|
-
*
|
|
544
|
-
* A VERIFY command is the project's own command and may legitimately write (a build
|
|
545
|
-
* emits `dist/`, a suite writes a snapshot). What it may NOT do is turn the tree into
|
|
546
|
-
* a passing tree and have that count as the debt being fixed — the run would then be
|
|
547
|
-
* certifying its own side effect. So tracked state is captured before and after, and
|
|
548
|
-
* a pass that came with a tracked change is downgraded to INCONCLUSIVE with the
|
|
549
|
-
* change named. Untracked output is left alone: it is what a build legitimately
|
|
550
|
-
* produces, and `git status --porcelain` in a repo with the usual ignores does not
|
|
551
|
-
* see it.
|
|
552
|
-
*
|
|
553
|
-
* A repository the guard cannot read (no git, git absent) is not a licence to skip
|
|
554
|
-
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
555
|
-
* assumption rather than an observation.
|
|
556
|
-
*/
|
|
557
|
-
export declare function rerunDebtVerifyCommand(cwd: string, command: string,
|
|
558
|
-
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
559
|
-
* the guard's four outcomes are testable without a repo or a real command. */
|
|
560
|
-
run?: CommandRunner): VerifyRerunResult;
|
|
136
|
+
export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
|
|
137
|
+
export type { BootDeps };
|
|
138
|
+
export { deriveOpenDebts, rerunDebtVerifyCommand };
|
|
561
139
|
/**
|
|
562
140
|
* Where in the gate a closure scan runs. The two stages are NOT interchangeable
|
|
563
141
|
* and neither is a scheduling preference:
|
|
@@ -654,4 +232,32 @@ export type { ClosureScan, ClosureScanInput, ClosureScanStage };
|
|
|
654
232
|
* it, and autofix converges only when the whole list is empty. Per-section
|
|
655
233
|
* env-gap/INFRA_GAP skip semantics and orphan-port recovery are unchanged.
|
|
656
234
|
*/
|
|
657
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Everything the run-end gate needs beyond the tree it is judging.
|
|
237
|
+
*
|
|
238
|
+
* An options object rather than a positional tail: the production call site read
|
|
239
|
+
* `runFinalIntegrationGate(cwd, undefined, undefined, undefined, planText)`, and
|
|
240
|
+
* `bootGraceMs`/`timeoutMs` are adjacent numbers that swap without a type error.
|
|
241
|
+
*
|
|
242
|
+
* `run`, `envClosure` and `trackedFiles` are SEAMS, by the same test GateDeps
|
|
243
|
+
* states: a scenario needs to substitute them. `runGateCommand`,
|
|
244
|
+
* `runVerifyCommandLine` and `rerunDebtVerifyCommand` each already take a
|
|
245
|
+
* `CommandRunner`; this is the fourth and last driver in the file, and without it
|
|
246
|
+
* the config-gap branch below is unreachable in test — not by oversight, but
|
|
247
|
+
* because reaching it needs a git-tracked env template, so every launch-contract
|
|
248
|
+
* test (bare `makeDir`, no `git init`) misses it by construction.
|
|
249
|
+
*/
|
|
250
|
+
export interface FinalGateOptions {
|
|
251
|
+
timeoutMs?: number;
|
|
252
|
+
bootGraceMs?: number;
|
|
253
|
+
bootDeps?: BootDeps;
|
|
254
|
+
planText?: string;
|
|
255
|
+
/** Spawner for the lockfile / integration / launch-script sections. Boot
|
|
256
|
+
* spawns through `bootDeps`, which has its own probes. */
|
|
257
|
+
run?: CommandRunner;
|
|
258
|
+
/** The tracked env-template closure. Default reads git, degrading to inert. */
|
|
259
|
+
envClosure?: (cwd: string) => EnvClosure;
|
|
260
|
+
/** The repo's tracked file list, or null when it cannot be determined. */
|
|
261
|
+
trackedFiles?: (cwd: string) => string[] | null;
|
|
262
|
+
}
|
|
263
|
+
export declare function runFinalIntegrationGate(cwd: string, opts?: FinalGateOptions): Promise<FinalGateOutcome>;
|