@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.
Files changed (78) hide show
  1. package/README.md +7 -3
  2. package/dist/shared/child-process.d.ts +8 -0
  3. package/dist/shared/command-watchdog.d.ts +1 -1
  4. package/dist/shared/command-watchdog.js +1 -1
  5. package/dist/task/accept-debt.d.ts +47 -0
  6. package/dist/task/accept-debt.js +127 -28
  7. package/dist/task/auto-orchestrator.js +93 -116
  8. package/dist/task/boot-probe.d.ts +298 -0
  9. package/dist/task/boot-probe.js +806 -0
  10. package/dist/task/child-runner.d.ts +56 -25
  11. package/dist/task/child-runner.js +65 -31
  12. package/dist/task/child-status.d.ts +95 -0
  13. package/dist/task/child-status.js +99 -0
  14. package/dist/task/command-run.d.ts +36 -0
  15. package/dist/task/command-run.js +48 -1
  16. package/dist/task/command-watchdog.js +1 -1
  17. package/dist/task/context-usage.d.ts +4 -3
  18. package/dist/task/context-usage.js +4 -3
  19. package/dist/task/contracts.js +18 -35
  20. package/dist/task/deep-render-check.d.ts +47 -0
  21. package/dist/task/deep-render-check.js +110 -65
  22. package/dist/task/env-notes.d.ts +3 -3
  23. package/dist/task/env-notes.js +24 -35
  24. package/dist/task/final-gate-fix.d.ts +1 -1
  25. package/dist/task/final-gate-fix.js +1 -1
  26. package/dist/task/final-gate.d.ts +38 -432
  27. package/dist/task/final-gate.js +105 -1213
  28. package/dist/task/gate-child.d.ts +8 -10
  29. package/dist/task/gate-child.js +15 -19
  30. package/dist/task/gate-deps.d.ts +29 -0
  31. package/dist/task/gate-deps.js +192 -206
  32. package/dist/task/gate-tally.d.ts +189 -0
  33. package/dist/task/gate-tally.js +249 -0
  34. package/dist/task/implementation-turn.d.ts +201 -0
  35. package/dist/task/implementation-turn.js +263 -0
  36. package/dist/task/launch-contract.js +27 -43
  37. package/dist/task/launch-manifest.d.ts +5 -0
  38. package/dist/task/launch-manifest.js +21 -0
  39. package/dist/task/ledger.d.ts +38 -0
  40. package/dist/task/ledger.js +83 -0
  41. package/dist/task/loop-detector.d.ts +14 -8
  42. package/dist/task/loop-detector.js +36 -12
  43. package/dist/task/orchestrator.d.ts +61 -126
  44. package/dist/task/orchestrator.js +70 -297
  45. package/dist/task/phases.d.ts +18 -0
  46. package/dist/task/phases.js +4 -3
  47. package/dist/task/plan-orchestrator.js +34 -33
  48. package/dist/task/requirements.d.ts +1 -1
  49. package/dist/task/requirements.js +50 -66
  50. package/dist/task/root-cause-repair.js +20 -32
  51. package/dist/task/run-bracket.d.ts +75 -0
  52. package/dist/task/run-bracket.js +41 -0
  53. package/dist/task/stall-detector.d.ts +110 -0
  54. package/dist/task/stall-detector.js +159 -0
  55. package/dist/task/task-gates.d.ts +69 -0
  56. package/dist/task/task-gates.js +114 -90
  57. package/dist/task/verify-work.d.ts +53 -67
  58. package/dist/task/verify-work.js +15 -11
  59. package/dist/workers/docs-core.d.ts +0 -4
  60. package/dist/workers/docs-core.js +10 -34
  61. package/dist/workers/docs-project.js +3 -3
  62. package/dist/workers/docs-resolve.d.ts +18 -0
  63. package/dist/workers/docs-resolve.js +39 -0
  64. package/dist/workers/docs-retrieve.d.ts +13 -0
  65. package/dist/workers/docs-retrieve.js +17 -2
  66. package/dist/workers/fetch-core.d.ts +0 -4
  67. package/dist/workers/fetch-core.js +2 -5
  68. package/dist/workers/phantom-imports.d.ts +3 -3
  69. package/dist/workers/phantom-imports.js +16 -29
  70. package/dist/workers/pi-worker-docs.d.ts +49 -0
  71. package/dist/workers/pi-worker-docs.js +33 -9
  72. package/dist/workers/pi-worker-fetch.d.ts +18 -0
  73. package/dist/workers/pi-worker-fetch.js +19 -4
  74. package/dist/workers/single-read-extension.d.ts +1 -1
  75. package/dist/workers/single-read-extension.js +5 -4
  76. package/dist/workers/single-read-guard.d.ts +32 -10
  77. package/dist/workers/single-read-guard.js +67 -16
  78. package/package.json +1 -1
@@ -0,0 +1,298 @@
1
+ import type { RenderOutcome } from './render-check.js';
2
+ import { type DeepRenderOutcome } from './deep-render-check.js';
3
+ import type { HealthCommand } from './repo-health-check.js';
4
+ /**
5
+ * Why this script is NOT a launch of the shipped app, or null when it plausibly
6
+ * is one (mx5 run 18, validated).
7
+ *
8
+ * Run 18's boot command resolved to `bun run dev`, whose body is
9
+ * `docker compose -f docker-compose.dev.yml up -d && until docker compose … pg_isready
10
+ * … && concurrently "bun run dev:css" "bun run dev:js" "bun run --watch
11
+ * src/server/index.ts"`. The gate sandbox has no docker, so the chain died at 127 and
12
+ * the boot SKIPPED as an environment gap — while the shipped app had no HTTP listener
13
+ * at all. A script whose first act is `docker compose up` cannot distinguish "the app
14
+ * is broken" from "this box has no docker", so it is not evidence either way: better
15
+ * to discover NO boot command — reported as "nothing to boot" — and let the static
16
+ * serve-entry check (serve-entry.ts) carry the signal, than to spend the grace window
17
+ * producing an unfalsifiable skip.
18
+ *
19
+ * CONSERVATIVE AND LEXICAL BY CONSTRUCTION. Only two shapes are rejected, both
20
+ * decidable from the script text alone:
21
+ * 1. the chain OPENS with container orchestration (docker/podman/nerdctl … up|start|run);
22
+ * 2. the whole body is a multiplexer (concurrently/npm-run-all/run-p/run-s/turbo)
23
+ * whose every child is an ASSET watcher in watch mode (tailwind/tsc/esbuild/…),
24
+ * i.e. nothing in it can ever listen.
25
+ * Anything else — `vite`, `next dev`, `node dist/index.js`, `nodemon`, `bun --watch
26
+ * src/index.ts`, and any multiplexer with one non-asset child — is accepted
27
+ * unchanged. Deciding whether a watcher actually SERVES is not attempted here; that
28
+ * is exactly what the static serve-entry check is for.
29
+ */
30
+ export declare function nonLaunchScriptReason(body: string, scripts?: Record<string, string>): string | null;
31
+ /**
32
+ * The project's OWN launch command, if it declares one (package.json `start`,
33
+ * else `dev`; Makefile `run`). null means the project has nothing to boot —
34
+ * the boot check degrades to nothing-to-run.
35
+ *
36
+ * A script that is not a LAUNCH at all (nonLaunchScriptReason — mx5 run 18's
37
+ * `docker compose up` orchestrator) is rejected here and falls through to the
38
+ * next candidate, then to null. Discovering nothing is strictly better than
39
+ * discovering something unfalsifiable: an env-gap skip of an orchestration script
40
+ * says nothing about the app, and null is reported as "nothing to boot".
41
+ */
42
+ export declare function discoverBootCommand(cwd: string): HealthCommand | null;
43
+ /**
44
+ * The launch script that EXISTS but was rejected as not-a-launch, if any. Without
45
+ * this the rejection would trade run 18's unfalsifiable skip for pure silence: no
46
+ * boot command means bootSkipVerdict has no label to name, and a project whose test
47
+ * suite ran still reports `observed > 0`, so unobservedVerdict stays quiet too. A
48
+ * served app whose only declared launch script cannot start it was not observed to
49
+ * run, and must say so.
50
+ */
51
+ export declare function rejectedLaunchScript(cwd: string): {
52
+ name: string;
53
+ reason: string;
54
+ } | null;
55
+ type BootOutcome = {
56
+ outcome: 'skip' | 'pass';
57
+ /** Set when the render check could not OBSERVE the served page (no browser,
58
+ * undeterminable port) or its AUTHENTICATED half (no declared credentials,
59
+ * an undrivable sign-in form, credentials the server rejected) — surfaced
60
+ * by the gate as an UNOBSERVED warning. */
61
+ renderNote?: string;
62
+ /** skip only: the boot command never spawned (ENOENT) — feeds the
63
+ * full-blindness guard (mx5 run 16), unlike a 127 where the runner ran. */
64
+ spawnFailed?: boolean;
65
+ } | {
66
+ outcome: 'fail';
67
+ detail: string;
68
+ } | {
69
+ outcome: 'orphan-port';
70
+ detail: string;
71
+ port: number | null;
72
+ };
73
+ /** Injectable environment probes for the boot check's orphan-port recovery, so the
74
+ * reap-and-retry path is deterministically testable without a real listener. */
75
+ export interface BootDeps {
76
+ /** The pid + command line holding `port` in LISTEN, or null if none/unknown. */
77
+ findPortHolder?: (port: number) => {
78
+ pid: number;
79
+ command: string;
80
+ } | null;
81
+ /** Terminate a pid we attribute to ourselves; returns whether it was signalled. */
82
+ reap?: (pid: number) => boolean;
83
+ /**
84
+ * Does process group `pgid` currently own a LISTENing TCP socket? Drives the
85
+ * served-app boot check (mx5 run 10): a watcher (`dev` = tailwind/bundler
86
+ * --watch) stays alive forever without ever listening, so "still alive after the
87
+ * grace window = PASS" blessed a project that cannot serve a single request.
88
+ * Injected so the listener requirement is deterministically testable without a
89
+ * real socket; the default probes ss/lsof + pgid.
90
+ */
91
+ groupHasListener?: (pgid: number) => boolean;
92
+ /**
93
+ * The (lowest) TCP port a listener owned by process group `pgid` is bound to,
94
+ * or null when it cannot be determined. Feeds the render check's URL; injected
95
+ * for tests, default probes ss/lsof + pgid.
96
+ */
97
+ groupListeningPort?: (pgid: number) => number | null;
98
+ /**
99
+ * Load the served page once in a headless browser and judge the RENDERED DOM
100
+ * (mx5 runs 8/11: curl cannot execute JS, so a blank-mount app passed every
101
+ * gate). Runs only for a served app, against the live listener, before the
102
+ * boot child is killed. Absent → the boot check behaves exactly as before;
103
+ * the gate wires runRenderCheck by default for served apps.
104
+ */
105
+ renderProbe?: (url: string) => RenderOutcome;
106
+ /**
107
+ * SIGN IN on the served page and judge the AUTHENTICATED half of the app (mx5
108
+ * run 17). Runs only after `renderProbe` PASSED — the shallow blank-page rule
109
+ * keeps its own RED/GREEN-proven verdict and is never shadowed by this one.
110
+ * Absent → the boot check behaves exactly as before; the gate wires
111
+ * runDeepRenderCheck by default for served apps. May only FAIL when the SERVER
112
+ * itself authenticated the session (see deep-render-check.judgeDeepSession);
113
+ * anything else — no browser, no declared credentials, an undrivable form,
114
+ * rejected credentials — is an env gap and skips with an UNOBSERVED note.
115
+ */
116
+ deepRenderProbe?: (url: string) => DeepRenderOutcome | Promise<DeepRenderOutcome>;
117
+ /**
118
+ * Can this box enumerate listeners with pids AT ALL (ss/netstat/lsof)? False
119
+ * means the served-app requirement is UNOBSERVABLE here and must degrade to the
120
+ * survival rule rather than fail — see canEnumerateListeners.
121
+ */
122
+ enumerationCapable?: () => boolean;
123
+ /**
124
+ * Reserve a free port to hand the boot child as PORT, so an HTTP answer on it is
125
+ * ownership evidence. null → no port could be reserved (the check then relies on
126
+ * pgid attribution alone). Injected for tests.
127
+ */
128
+ pickPort?: () => Promise<number | null>;
129
+ /**
130
+ * The port the project's own client was BUILT to call, when it declares one and
131
+ * nothing is holding it — preferred over a freshly reserved port so the served
132
+ * origin and the origin the client calls are the same one (see pinnedLocalPort).
133
+ * null → use the reserved private port exactly as before.
134
+ */
135
+ preferredPort?: () => Promise<number | null>;
136
+ /** Does anything answer HTTP on 127.0.0.1:`port`? Injected for tests. */
137
+ httpProbe?: (port: number) => boolean;
138
+ }
139
+ /**
140
+ * Does the finished run stand up a listening HTTP server? Deterministic, from the
141
+ * built manifest (a server-framework dependency is the plan's own artifact) OR, when
142
+ * available, the plan/spec text. Used to decide whether the boot check must observe a
143
+ * LISTENER (served app) or may pass on mere survival / quick exit (CLI project).
144
+ */
145
+ export declare function detectsServedApp(cwd: string, planText?: string): boolean;
146
+ /** `ss -tlnpH` rows → {pid, port}. Column 4 (0-based 3) is the local address; the
147
+ * port is its last `:`-suffixed number ("0.0.0.0:3000", "[::]:3000"). */
148
+ export declare function parseSsListeners(stdout: string): Array<{
149
+ pid: number;
150
+ port: number;
151
+ }>;
152
+ /**
153
+ * `netstat -tlnp` rows → {pid, port} (mx5 run 14, validated: the agent-sandbox
154
+ * image ships NEITHER ss NOR lsof — only ps and netstat — so the served-app boot
155
+ * check could never observe a listener and failed unfalsifiably). The pid rides
156
+ * in the trailing "PID/Program name" column ("1234/bun"); rows the kernel will
157
+ * not attribute to us print "-" there and are skipped.
158
+ */
159
+ export declare function parseNetstatListeners(stdout: string): Array<{
160
+ pid: number;
161
+ port: number;
162
+ }>;
163
+ /** `lsof -iTCP -sTCP:LISTEN -n -P` rows → {pid, port}. */
164
+ export declare function parseLsofListeners(stdout: string): Array<{
165
+ pid: number;
166
+ port: number;
167
+ }>;
168
+ export declare function canEnumerateListeners(): boolean;
169
+ /**
170
+ * A free TCP port on the loopback interface, or null if one cannot be reserved.
171
+ * The boot check hands this to the child as PORT so that a successful HTTP
172
+ * request to it is OWNERSHIP evidence: nobody else knows the number (mx5 runs
173
+ * 8/10/11 — orphaned servers from earlier checks answered curl on the
174
+ * conventional :3000 and passed checks the app had not earned).
175
+ */
176
+ export declare function pickFreePort(): Promise<number | null>;
177
+ /** Can we bind 127.0.0.1:`port` right now? (Free ⇒ the boot child can have it.) */
178
+ export declare function isPortFree(port: number): Promise<boolean>;
179
+ /**
180
+ * The project's own declared local port, but only if nothing is holding it — the
181
+ * default `preferredPort` for the gate. A declared port that is BUSY falls back to
182
+ * a reserved one rather than colliding: a stranger's server on :3000 must never be
183
+ * mistaken for the app we just booted.
184
+ */
185
+ export declare function preferredDeclaredPort(cwd: string): Promise<number | null>;
186
+ /** Default port-holder lookup: `lsof` first, then `ss`/`fuser`. Returns null on any
187
+ * failure (the diagnosis then omits the pid — never blocks). */
188
+ export declare function defaultFindPortHolder(port: number): {
189
+ pid: number;
190
+ command: string;
191
+ } | null;
192
+ /**
193
+ * Exercise the start command ONCE. For a CLI project (`expectServer` false) the
194
+ * command's own fate within the grace window decides:
195
+ *
196
+ * - non-zero exit (or signal death) before the window closes → FAIL, output tail;
197
+ * - exit 0 before the window closes → PASS (a CLI-style "run" that finished);
198
+ * - still alive when the window closes → PASS, then the whole process group is
199
+ * killed (detached spawn = own group; SIGTERM, escalating to SIGKILL).
200
+ *
201
+ * For a SERVED app (`expectServer` true — the spec/plan promised an HTTP server) mere
202
+ * survival is not enough: a watcher (`dev` = tailwind/bundler --watch) stays alive
203
+ * forever without ever listening, and a type-only entrypoint exits 0 in <1s having
204
+ * served nothing (mx5 run 10 — both were blessed by the survival rule). The boot then
205
+ * PASSes only once a LISTENing socket owned by our process group is observed; if the
206
+ * command exits, or the grace window closes, with no listener ever seen → FAIL naming
207
+ * that a listening server was expected.
208
+ *
209
+ * OBSERVABILITY is a precondition of that FAIL (mx5 run 14, validated). The listener
210
+ * requirement needs pgid-attributed socket enumeration; win32 has none, and neither
211
+ * does a Linux image shipping no ss/netstat/lsof — run 14's sandbox was exactly that,
212
+ * so the check emitted "never opened a listening socket" against an app that
213
+ * demonstrably served, three autofix passes could not falsify it, and the run was
214
+ * recorded failed. Two defences, in order:
215
+ *
216
+ * - the child is spawned with a freshly reserved, otherwise-unused PORT, and an
217
+ * HTTP answer on THAT port proves a listener regardless of tooling. The private
218
+ * port is what makes the HTTP probe trustworthy: an orphaned server from an
219
+ * earlier check answers on :3000, but nobody else knows this number.
220
+ * - if nothing can enumerate listeners AND the assigned port never answered, the
221
+ * served-app requirement is unobservable here, so `expectServer` collapses to
222
+ * the survival rule and the PASS is stamped UNOBSERVED. An app that ignores PORT
223
+ * is indistinguishable from one that never listened — an observer limitation,
224
+ * not an app defect, and it may not be reported as one.
225
+ *
226
+ * A child that EXITS non-zero still FAILs in every environment: "the process died"
227
+ * needs no socket probe, so run 14's original true positive (a `--hot` runtime
228
+ * pinning a crashed app) stays reportable wherever the tooling exists.
229
+ *
230
+ * Env-gap contract as everywhere: spawn error (ENOENT) or a command-not-found
231
+ * inside the chain (exit 127, or the runner's own wording where the platform
232
+ * reports it that way — see isCommandNotFound) → skip.
233
+ */
234
+ export declare function runBootCheck(cwd: string, [bin, args]: HealthCommand, graceMs?: number, opts?: {
235
+ expectServer?: boolean;
236
+ deps?: BootDeps;
237
+ }): Promise<BootOutcome>;
238
+ /**
239
+ * The SAME third verdict, at the door unobservedVerdict cannot reach: the boot
240
+ * check specifically (mx5 run 18, validated).
241
+ *
242
+ * Run 18 shipped an app with no HTTP server behind a converged final gate. Its
243
+ * `src/server/index.ts` ends at `export {app}` — no `Bun.serve`, no
244
+ * `export default app`, no `start` script — so `bun run src/server/index.ts` exits
245
+ * 0 immediately and the product cannot be started at all. The gate's boot command
246
+ * resolved to `bun run dev`, whose body begins `docker compose … up -d`; the gate
247
+ * sandbox had no docker, so the boot SKIPPED as an environment gap. Skips
248
+ * contribute nothing to `dynObserved`, and `bun run test`, `test:ct`, `build`,
249
+ * `lint`, `seed` and `migrate` all ran — so `dynObserved > 0`, the full-skip
250
+ * blindness guard (observabilityGapFailure) stayed correctly quiet, and the trail
251
+ * read `final-gate: autofix converged — statics + … passed` with 24/24 tasks green.
252
+ *
253
+ * The defect is that "the app was never observed to boot" and "the app booted
254
+ * fine" produced BYTE-IDENTICAL gate output. That is the class scripts/ab-verdict.ts
255
+ * exists to kill one layer up: absence of evidence rendered in the shape of
256
+ * evidence. So a discovered-but-skipped boot now names itself, and — unlike every
257
+ * other skip — it CANNOT be cancelled by observations from other commands.
258
+ * Component tests are the trap here, not the alibi: run 18 had 51 green Playwright
259
+ * CT tests, and CT mounts components in a browser without ever assembling or
260
+ * starting the server.
261
+ *
262
+ * DECIDED, do not silently re-open:
263
+ * - NOT a FAIL. A boot skip on a docker-less box is a genuine environment gap, and
264
+ * failing it re-creates run 16's unfalsifiable-FAIL mistake pointing the other
265
+ * way. UNOBSERVED blocks nothing while being loud and durable (the caller records
266
+ * it as final-gate debt the next run re-surfaces), and it keeps "boot never ran"
267
+ * out of the autofix child's seed — a child cannot fix a missing docker, so the
268
+ * highest-probability response would be to FABRICATE a bootable command, the
269
+ * class that refuted the `## verified tooling` harvest.
270
+ * - BOTH skip flavours count. Run 18's skip carried `spawnFailed: false` (127 inside
271
+ * the script chain, not an ENOENT on the runner), so keying off spawnFailed would
272
+ * have missed the actual defect.
273
+ * - SERVED APPS ONLY. `expectServer === false` (a CLI/library project) is fenced off
274
+ * deliberately: a CLI whose `dev` script needs an absent tool has no server to be
275
+ * unobserved, and widening the lever there buys warnings nobody can act on.
276
+ */
277
+ export declare function bootSkipVerdict(args: {
278
+ /** `bin args…` of the DISCOVERED boot command; null ⇒ nothing to boot, which is
279
+ * not the same thing as a boot that was not observed. */
280
+ label: string | null;
281
+ /** Did the boot check end in `skip` (either flavour)? */
282
+ skipped: boolean;
283
+ /** Does this project stand up an HTTP server (detectsServedApp)? */
284
+ expectServer: boolean;
285
+ }): string | null;
286
+ /**
287
+ * Boot check hit an address-in-use bind failure. If the port is held by one of OUR
288
+ * own orphaned gate children (a `dev`/`start` run), reap it and retry the boot once
289
+ * so the app gets a fair launch; otherwise leave the (foreign) holder alone and let
290
+ * the caller emit the harness diagnosis. Never reaps a process we cannot attribute
291
+ * to ourselves.
292
+ */
293
+ export declare function recoverOrphanPort(cwd: string, boot: HealthCommand, first: {
294
+ outcome: 'orphan-port';
295
+ detail: string;
296
+ port: number | null;
297
+ }, bootGraceMs: number, deps: BootDeps, expectServer: boolean): Promise<BootOutcome>;
298
+ export {};