@mjasnikovs/pi-task 0.38.11 → 0.38.13

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 (59) hide show
  1. package/README.md +8 -5
  2. package/dist/config/config.d.ts +0 -1
  3. package/dist/config/config.js +0 -1
  4. package/dist/config/register.js +0 -2
  5. package/dist/index.js +0 -2
  6. package/dist/shared/child-process.d.ts +8 -0
  7. package/dist/shared/command-watchdog.d.ts +1 -1
  8. package/dist/shared/command-watchdog.js +1 -1
  9. package/dist/task/accept-debt.d.ts +47 -0
  10. package/dist/task/accept-debt.js +127 -28
  11. package/dist/task/auto-orchestrator.js +91 -114
  12. package/dist/task/child-runner.d.ts +39 -25
  13. package/dist/task/child-runner.js +59 -31
  14. package/dist/task/child-status.d.ts +95 -0
  15. package/dist/task/child-status.js +99 -0
  16. package/dist/task/command-run.d.ts +36 -0
  17. package/dist/task/command-run.js +48 -1
  18. package/dist/task/command-watchdog.js +1 -1
  19. package/dist/task/context-usage.d.ts +4 -3
  20. package/dist/task/context-usage.js +4 -3
  21. package/dist/task/contracts.js +18 -35
  22. package/dist/task/deep-render-check.d.ts +47 -0
  23. package/dist/task/deep-render-check.js +110 -65
  24. package/dist/task/env-notes.d.ts +3 -3
  25. package/dist/task/env-notes.js +24 -35
  26. package/dist/task/final-gate-fix.d.ts +1 -1
  27. package/dist/task/final-gate-fix.js +1 -1
  28. package/dist/task/final-gate.d.ts +5 -151
  29. package/dist/task/final-gate.js +81 -379
  30. package/dist/task/gate-child.d.ts +8 -10
  31. package/dist/task/gate-child.js +15 -19
  32. package/dist/task/gate-deps.d.ts +29 -0
  33. package/dist/task/gate-deps.js +192 -206
  34. package/dist/task/gate-tally.d.ts +189 -0
  35. package/dist/task/gate-tally.js +249 -0
  36. package/dist/task/implementation-turn.d.ts +201 -0
  37. package/dist/task/implementation-turn.js +263 -0
  38. package/dist/task/launch-contract.js +27 -43
  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 +67 -294
  45. package/dist/task/plan-orchestrator.js +34 -33
  46. package/dist/task/requirements.d.ts +1 -1
  47. package/dist/task/requirements.js +50 -66
  48. package/dist/task/root-cause-repair.js +20 -32
  49. package/dist/task/run-bracket.d.ts +75 -0
  50. package/dist/task/run-bracket.js +41 -0
  51. package/dist/task/stall-detector.d.ts +110 -0
  52. package/dist/task/stall-detector.js +159 -0
  53. package/dist/task/verify-work.d.ts +53 -67
  54. package/dist/task/verify-work.js +15 -11
  55. package/dist/workers/single-read-extension.d.ts +1 -1
  56. package/dist/workers/single-read-extension.js +5 -4
  57. package/dist/workers/single-read-guard.d.ts +32 -10
  58. package/dist/workers/single-read-guard.js +67 -16
  59. package/package.json +1 -1
@@ -32,9 +32,7 @@
32
32
  * exactly that scrutiny, and forbids treating a grep of a generated artifact as
33
33
  * evidence of absence. Provenance is mechanical; re-validation is prompt-level.
34
34
  */
35
- import * as fsp from 'node:fs/promises';
36
- import * as path from 'node:path';
37
- import { tasksDir } from './task-io.js';
35
+ import { makeLedger } from './ledger.js';
38
36
  const ENV_NOTES_FILE = 'env-notes.md';
39
37
  /** Cap kept notes so a chatty run cannot grow the prompt block unboundedly. */
40
38
  const MAX_NOTES = 40;
@@ -46,18 +44,6 @@ const MAX_NOTE_LENGTH = 240;
46
44
  * stray tab in an emitted fact is normalised to a space before storage.
47
45
  */
48
46
  const ORIGIN_SEP = '\t';
49
- export function envNotesFile(cwd) {
50
- return path.join(tasksDir(cwd), ENV_NOTES_FILE);
51
- }
52
- /** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
53
- export async function readEnvNotes(cwd) {
54
- try {
55
- return (await fsp.readFile(envNotesFile(cwd), 'utf8')).trim();
56
- }
57
- catch {
58
- return '';
59
- }
60
- }
61
47
  /**
62
48
  * Parse the stored file into fact+origin records. Legacy lines written before
63
49
  * provenance (no separator) parse with an empty origin, so old caches still read.
@@ -79,6 +65,22 @@ export function parseEnvNotes(raw) {
79
65
  function serializeNote(n) {
80
66
  return n.origin ? `${n.fact}${ORIGIN_SEP}${n.origin}` : n.fact;
81
67
  }
68
+ /** Keyed on the fact alone (case-insensitive): a fact already present keeps its
69
+ * ORIGINAL origin — provenance traces to who first established it. */
70
+ const ledger = makeLedger({
71
+ file: ENV_NOTES_FILE,
72
+ max: MAX_NOTES,
73
+ key: n => n.fact.toLowerCase(),
74
+ serialize: serializeNote,
75
+ parse: parseEnvNotes
76
+ });
77
+ export function envNotesFile(cwd) {
78
+ return ledger.path(cwd);
79
+ }
80
+ /** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
81
+ export async function readEnvNotes(cwd) {
82
+ return ledger.readRaw(cwd);
83
+ }
82
84
  /**
83
85
  * Pull `ENV-NOTE: <fact>` lines out of a child's answer text. Deduplicated,
84
86
  * length-capped; verdict markers can never match (different prefix).
@@ -134,27 +136,14 @@ export function isExcuseNote(fact) {
134
136
  * blocker.
135
137
  */
136
138
  export async function appendEnvNotes(cwd, notes, origin = '') {
137
- if (notes.length === 0)
138
- return;
139
- try {
140
- const existing = parseEnvNotes(await readEnvNotes(cwd));
141
- const seen = new Set(existing.map(n => n.fact.toLowerCase()));
142
- const merged = [...existing];
143
- for (const note of notes) {
144
- const fact = note.trim().replace(/\t/g, ' ');
145
- const key = fact.toLowerCase();
146
- if (key.length === 0 || seen.has(key))
147
- continue;
148
- seen.add(key);
149
- merged.push({ fact, origin: origin.trim() });
150
- }
151
- const kept = merged.slice(-MAX_NOTES);
152
- await fsp.mkdir(tasksDir(cwd), { recursive: true });
153
- await fsp.writeFile(envNotesFile(cwd), kept.map(serializeNote).join('\n') + '\n', 'utf8');
154
- }
155
- catch {
156
- // best-effort cache
139
+ const fresh = [];
140
+ for (const note of notes) {
141
+ const fact = note.trim().replace(/\t/g, ' ');
142
+ if (fact.length === 0)
143
+ continue;
144
+ fresh.push({ fact, origin: origin.trim() });
157
145
  }
146
+ await ledger.append(cwd, fresh);
158
147
  }
159
148
  /**
160
149
  * The prompt block a gate child receives when notes exist. Two things are
@@ -38,7 +38,7 @@ export declare function classifyFinalGateAnswer(answer: string | undefined): Fin
38
38
  * Extract it for reporting; the shrink guard itself compares the FULL
39
39
  * discovered-command sets, so a reason this cannot parse still guards.
40
40
  */
41
- export declare function extractFailingCommand(reason: string): string | null;
41
+ export declare function exitedCommandFromReason(reason: string): string | null;
42
42
  /**
43
43
  * Build the fix child's prompt. Generic by construction: the only project facts
44
44
  * in it are the gate's own failure text — the command comes from the project's
@@ -90,7 +90,7 @@ export function classifyFinalGateAnswer(answer) {
90
90
  * Extract it for reporting; the shrink guard itself compares the FULL
91
91
  * discovered-command sets, so a reason this cannot parse still guards.
92
92
  */
93
- export function extractFailingCommand(reason) {
93
+ export function exitedCommandFromReason(reason) {
94
94
  const m = /`([^`]+)`\s+exited\b/.exec(reason);
95
95
  return m ? m[1] : null;
96
96
  }
@@ -1,9 +1,10 @@
1
1
  import { type HealthCommand } from './repo-health-check.js';
2
- import { type AcceptDebt, type VerifyRerunResult } from './accept-debt.js';
2
+ import { deriveOpenDebts, rerunDebtVerifyCommand, type AcceptDebt } from './accept-debt.js';
3
3
  import { discoverBootCommand, detectsServedApp, runBootCheck, 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';
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;
@@ -129,159 +130,12 @@ export declare function discoverGateCommandLabels(cwd: string): string[];
129
130
  * guard already owns.
130
131
  */
131
132
  export declare function discoverGateCommandBodies(cwd: string): Record<string, string>;
132
- /**
133
- * How a re-run of ONE recorded VERIFY command line ended.
134
- * pass — it ran and exited 0. The ONLY outcome that may close a debt.
135
- * fail — it ran and exited non-zero for a real reason. Debt stays open.
136
- * gap — nothing was observed: the shell/runner never spawned, 127 inside the
137
- * chain, a timeout, a missing browser, or absent external infrastructure.
138
- * INCONCLUSIVE, so the debt stays open (surface, never re-hide).
139
- */
140
- export type VerifyRerunOutcome = {
141
- outcome: 'pass';
142
- } | {
143
- outcome: 'fail';
144
- status: number;
145
- tail: string;
146
- } | {
147
- outcome: 'gap';
148
- detail: string;
149
- };
150
- /**
151
- * Re-run one VERIFY-block command line (nexttask 5) under the gate's existing
152
- * env-gap contract, so a debt whose reason NAMES that command can be closed by the
153
- * command itself rather than by a judgement about it.
154
- *
155
- * Runs through `sh -c` because a VERIFY line is a shell line, not an argv: run 19's
156
- * is `AGENT=1 bun test test/listings.test.ts`, and env prefixes, `&&` and redirects
157
- * are all ordinary there. The leading command word is still resolved through
158
- * runner-resolve so a login-shell-stripped PATH cannot make every re-run look like a
159
- * gap (mx5 run 16's blindness, one level down).
160
- *
161
- * The asymmetry is the point: only exit 0 is conclusive. Every other ending — real
162
- * failure, missing tool, unreachable database, timeout, no POSIX shell — leaves the
163
- * debt exactly as open as it was.
164
- */
165
- export declare function runVerifyCommandLine(cwd: string, line: string, timeoutMs: number, extraGapRe?: RegExp,
166
- /** The spawner. Injected so a re-run's outcome can be tested without one. */
167
- run?: CommandRunner): VerifyRerunOutcome;
168
- /**
169
- * The full-skip blindness guard (mx5 run 16, validated): dynamic commands were
170
- * DISCOVERED but every single one skipped as an environment gap, so the gate
171
- * decided on statics alone and stamped a permanently blank app green. Per-command
172
- * env-gap skips stay legitimate (a missing browser must not fail a suite); what
173
- * may never happen again is ALL of them skipping while the gate still reports
174
- * PASS — a gate that observed nothing dynamic has no basis to vouch for the
175
- * assembled app. Pure so the semantics are unit-tested; the caller feeds it the
176
- * attempt/observation counters and runner resolvability.
177
- */
178
- export declare function observabilityGapFailure(args: {
179
- /** Dynamic commands the gate discovered and tried to run. */
180
- attempted: number;
181
- /** Of those, how many it actually OBSERVED (a real pass OR a real fail —
182
- * either proves the command ran; only skips observe nothing). */
183
- observed: number;
184
- /** Of the skips, how many were SPAWN failures (runner never ran, ENOENT).
185
- * Tool-level gaps (missing browser, 127 inside the chain, timeout) prove
186
- * the runner itself works and keep the classic env-gap contract — the
187
- * blindness class fires only when EVERY attempt failed to even spawn. */
188
- spawnFailures: number;
189
- /** Distinct runner bins across the attempted commands. */
190
- runnerBins: string[];
191
- /** Is this runner spawnable (bare or via a known install location)? */
192
- runnerResolvable: (bin: string) => boolean;
193
- }): string | null;
194
- /**
195
- * The THIRD verdict. observabilityGapFailure above covers "commands were DISCOVERED
196
- * but every one failed to spawn" — a rank-0 FAIL. It deliberately returns null for
197
- * `attempted === 0`, and until now that silence fell straight through to
198
- * `PASS — no integration command found (statics passed)`: the run-16 blindness class
199
- * entering through a different door, where "we never checked" reads exactly like "we
200
- * checked and it was fine". Measured 2026-07-27: IAR1 (C++/CMake, no package.json)
201
- * shipped that verdict TWICE while carrying 2 and 3 open verify-FAIL debts, and
202
- * godot-engine (package.json whose only script is `verify`) reproduces it live today.
203
- *
204
- * So: observed anything dynamic ⇒ PASS; discovered-but-all-spawn-failed ⇒ the
205
- * existing FAIL; observed NOTHING ⇒ this note, carried on an `ok: true` outcome.
206
- *
207
- * WHY NON-BLOCKING (decided, not deferred — the evidence cuts both ways and this is
208
- * the resolution):
209
- * - Blocking's case: both real occurrences also carried open verify-FAIL debt, so
210
- * the runs with no dynamic evidence were exactly the runs already known to be
211
- * carrying defects.
212
- * - Against, and decisive: (1) that debt is ALREADY surfaced unconditionally at the
213
- * gate moment, on PASS as on FAIL — the IAR1 records literally read "PASS — no
214
- * integration command found … UNRESOLVED VERIFY-FAIL DEBT still open (2)". The
215
- * missing signal was never the debt, it was the word PASS endorsing the run, and
216
- * that is what this fixes. (2) `ok: false` routes into the autofix picker, whose
217
- * seed is `reason`; "no integration command is discoverable" is not fixable by
218
- * editing code, so the highest-probability child response is to FABRICATE a
219
- * runnable command to satisfy the gate — the same fabrication class that refuted
220
- * the `## verified tooling` harvest (see discoverIntegrationCommands) and that had
221
- * run 11's fix child `rm` a sibling's deliverable. (3) That harvest being refuted
222
- * means IAR1 and godot-engine can NEVER discover a command, so blocking would end
223
- * every non-npm run in `failed` permanently, with no remedy — the task's own I3
224
- * ("show blocking does not block IAR1/godot post-Task-1") is unsatisfiable, and
225
- * its stated consequence is to downgrade to a warning and say so. This is that.
226
- * The teeth are elsewhere and are real: the verdict word changes, the gate trail says
227
- * UNOBSERVED, and the caller records a durable final-gate debt that the NEXT run's
228
- * gate re-surfaces (it can never auto-close — it is not static-class).
229
- */
230
- export declare function unobservedVerdict(args: {
231
- /** Dynamic commands the gate discovered and tried to run (0 ⇒ nothing existed). */
232
- discovered: number;
233
- /** Of those, how many actually RAN (a real pass or a real fail). */
234
- observed: number;
235
- }): string | null;
133
+ export { runVerifyCommandLine, type VerifyRerunOutcome } from './command-run.js';
134
+ export { observabilityGapFailure, unobservedVerdict };
236
135
  export { taskThatIntroduced };
237
136
  export { discoverBootCommand, detectsServedApp, runBootCheck, bootSkipVerdict, nonLaunchScriptReason, rejectedLaunchScript, parseSsListeners, parseNetstatListeners, parseLsofListeners, pickFreePort, preferredDeclaredPort, canEnumerateListeners };
238
137
  export type { BootDeps };
239
- /**
240
- * ACCEPT-debt re-check (mx5 run 4 B3 / run 8 TASK_0012): read the ledger of tasks
241
- * the user accepted despite a verify-FAIL and re-check each against the CURRENT
242
- * tree. A static-class debt whose statics now pass is provably RESOLVED (a later
243
- * task fixed it) and pruned from the ledger; every other debt cannot be proven
244
- * resolved deterministically, so it stays OPEN and is surfaced — a run may not
245
- * complete silently carrying an accepted defect. FP-safe by construction (see
246
- * accept-debt.ts). Best-effort: a ledger read/write failure must never break the
247
- * caller.
248
- *
249
- * FACTORED OUT of runFinalIntegrationGate (nexttask 6): the derivation has to be
250
- * runnable at a SECOND moment — after a converged final-gate autofix, where the
251
- * orchestrator used to rebuild its gate outcome as a bare `{ok, reason}` and drop
252
- * `openDebts` entirely. The report a run ends on has to be derived from the tree
253
- * the run ends with, not from the tree as it was before the fix pass.
254
- *
255
- * `staticOk` is the caller's claim about the CURRENT statics, and it is the only
256
- * thing that can auto-close a static-class debt — so a caller that does not know
257
- * must pass `false` (unprovable ⇒ stays open), never a guess.
258
- */
259
- export declare function deriveOpenDebts(cwd: string, staticOk: boolean): Promise<{
260
- openDebts: AcceptDebt[];
261
- debtNote?: string;
262
- trail?: string[];
263
- }>;
264
- /**
265
- * Re-run ONE debt's stored VERIFY command for the re-check, with the no-write guard
266
- * (`inv-no-write`) wrapped around it.
267
- *
268
- * A VERIFY command is the project's own command and may legitimately write (a build
269
- * emits `dist/`, a suite writes a snapshot). What it may NOT do is turn the tree into
270
- * a passing tree and have that count as the debt being fixed — the run would then be
271
- * certifying its own side effect. So tracked state is captured before and after, and
272
- * a pass that came with a tracked change is downgraded to INCONCLUSIVE with the
273
- * change named. Untracked output is left alone: it is what a build legitimately
274
- * produces, and `git status --porcelain` in a repo with the usual ignores does not
275
- * see it.
276
- *
277
- * A repository the guard cannot read (no git, git absent) is not a licence to skip
278
- * the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
279
- * assumption rather than an observation.
280
- */
281
- export declare function rerunDebtVerifyCommand(cwd: string, command: string,
282
- /** The spawner, for BOTH the command and the tracked-state reads. Injected so
283
- * the guard's four outcomes are testable without a repo or a real command. */
284
- run?: CommandRunner): VerifyRerunResult;
138
+ export { deriveOpenDebts, rerunDebtVerifyCommand };
285
139
  /**
286
140
  * Where in the gate a closure scan runs. The two stages are NOT interchangeable
287
141
  * and neither is a scheduling preference: