@mjasnikovs/pi-task 0.21.3 → 0.21.5

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.
@@ -29,6 +29,11 @@
29
29
  * `lsof` and the sandbox had neither) and no further attempt can move it. The
30
30
  * run is allowed to converge on the REMAINING checks, carrying this one here
31
31
  * so the next run's gate re-checks and re-surfaces it rather than losing it.
32
+ * Also recorded for the ZERO-OBSERVATION verdict (final-gate.ts
33
+ * unobservedVerdict): the whole gate observed nothing dynamic — usually because
34
+ * the project's ecosystem exposes no discoverable command at all — so the run
35
+ * completed on statics alone. Same treatment for the same reason: unprovable
36
+ * here, so carry it forward rather than let a silent PASS bury it.
32
37
  * - 'root-cause' — the task's verify FAILed because of a PRE-EXISTING defect in a
33
38
  * file a DIFFERENT task created, which this task's own work never touched (mx5
34
39
  * run 14: TASK_0007's `test/teardown.ts` TRUNCATE bug FAILed TASK_0013 and
@@ -36,6 +36,8 @@ export interface AutoDeps extends GateDeps {
36
36
  failures?: string[];
37
37
  debtNote?: string;
38
38
  openDebts?: AcceptDebt[];
39
+ /** Set ⇒ the gate observed nothing dynamic: UNOBSERVED, not PASS. */
40
+ unobserved?: string;
39
41
  }>;
40
42
  /**
41
43
  * Bounded model-driven fix pass for a final-gate FAIL (see final-gate-fix.ts),
@@ -1063,6 +1063,11 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1063
1063
  announceDone(active, `${id} cancelled before the final integration gate — resume with /task-auto-resume.`, 'warning');
1064
1064
  return;
1065
1065
  }
1066
+ // Set when the gate finished having observed NOTHING dynamic. Declared
1067
+ // out here so the run-completion announcement below can say so: a run
1068
+ // that completes on statics alone must not be announced the same way as
1069
+ // one whose product was actually exercised.
1070
+ let unobservedNote = null;
1066
1071
  if (deps.finalGate) {
1067
1072
  active.ui.notify(`${id}: running final integration gate…`, 'info');
1068
1073
  // Run-level gate trail on the parent task file — same durable
@@ -1096,7 +1101,21 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1096
1101
  // Record the outcome symmetrically (mx5 run 10 item 7): only FAIL was
1097
1102
  // ever trailed, so a PASSing gate was indistinguishable from a gate
1098
1103
  // that never ran. The PASS reason names the commands that were run.
1099
- if (fin.ok) {
1104
+ // THREE verdicts, not two (final-gate.ts unobservedVerdict): a gate
1105
+ // that observed nothing dynamic is UNOBSERVED, never PASS — IAR1
1106
+ // shipped `PASS — no integration command found` twice while carrying
1107
+ // open verify-FAIL debt. It does not block (justified there), but it
1108
+ // is labelled here, warned about, and recorded as durable debt so the
1109
+ // next run's gate re-surfaces it.
1110
+ if (fin.ok && fin.unobserved) {
1111
+ unobservedNote = fin.unobserved;
1112
+ await recGate(`final-gate: UNOBSERVED — ${fin.reason.slice(0, 300)}`);
1113
+ await recordFinalGateUnobservedDebt(cwd, id, fin.unobserved);
1114
+ active.ui.notify(`${id}: the final integration gate observed NOTHING dynamic — `
1115
+ + 'the run completed on static checks alone. Nothing verified '
1116
+ + 'that the assembled product builds, boots or works.', 'warning');
1117
+ }
1118
+ else if (fin.ok) {
1100
1119
  await recGate(`final-gate: PASS — ${fin.reason.slice(0, 300)}`);
1101
1120
  }
1102
1121
  else {
@@ -1247,8 +1266,15 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1247
1266
  const fix = await deps.finalGateFix(active, cwd, seed);
1248
1267
  if (fix.ok) {
1249
1268
  await deps.commit(cwd, `FINAL GATE AUTOFIX (${id})`);
1250
- await recGate(`final-gate: autofix converged — ${fix.reason.slice(0, 200)}`);
1251
- active.ui.notify(`${id}: final integration gate PASSES after autofix — ${fix.reason.slice(0, 140)}`, 'info');
1269
+ // A converged re-run that observed nothing dynamic is
1270
+ // UNOBSERVED on this door too — never announce it as a
1271
+ // PASS just because it arrived via autofix.
1272
+ if (fix.unobserved) {
1273
+ unobservedNote = fix.unobserved;
1274
+ await recordFinalGateUnobservedDebt(cwd, id, fix.unobserved);
1275
+ }
1276
+ await recGate(`final-gate: autofix ${fix.unobserved ? 'ended UNOBSERVED' : 'converged'} — ${fix.reason.slice(0, 200)}`);
1277
+ active.ui.notify(`${id}: final integration gate ${fix.unobserved ? 'is UNOBSERVED' : 'PASSES'} after autofix — ${fix.reason.slice(0, 140)}`, fix.unobserved ? 'warning' : 'info');
1252
1278
  fin = { ok: true, reason: fix.reason };
1253
1279
  break;
1254
1280
  }
@@ -1367,7 +1393,12 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1367
1393
  }
1368
1394
  }
1369
1395
  await updateTaskFrontMatter(cwd, id, { state: 'completed' });
1370
- announceDone(active, `${id} complete — all ${entries.length} tasks done.`, 'info');
1396
+ announceDone(active, `${id} complete — all ${entries.length} tasks done.`
1397
+ + (unobservedNote ?
1398
+ ' WARNING: the final integration gate was UNOBSERVED — it ran no '
1399
+ + 'dynamic check at all, so "complete" here means the statics '
1400
+ + 'passed and nothing more. Carried as debt for the next run.'
1401
+ : ''), unobservedNote ? 'warning' : 'info');
1371
1402
  return;
1372
1403
  }
1373
1404
  // REFUSE to start on a conflicted tree: an unmerged index dooms every
@@ -103,6 +103,11 @@ export interface FinalFixResult {
103
103
  /** The fresh gate's individual ranked failures (see FinalGateOutcome.failures),
104
104
  * so the caller can trail each entry — never just the first. */
105
105
  gateFailures?: string[];
106
+ /** On a converged outcome: the re-run gate's UNOBSERVED note, if it observed
107
+ * nothing dynamic (see FinalGateOutcome.unobserved). Carried so the caller
108
+ * labels a converge-on-statics-alone the same way it labels a first-pass one —
109
+ * "converged" must never quietly mean "we stopped being able to check". */
110
+ unobserved?: string;
106
111
  /** A write-guard rejected this attempt (deletion / shrink / probe-gaming). */
107
112
  guardTripped?: boolean;
108
113
  /** …and its edits were discarded. When a guard tripped and this is false, the
@@ -126,6 +131,7 @@ export interface FinalFixDeps {
126
131
  ok: boolean;
127
132
  reason: string;
128
133
  failures?: string[];
134
+ unobserved?: string;
129
135
  }>;
130
136
  /** Labels of every currently-discoverable gate command (static + integration),
131
137
  * for the shrink guard. Pure discovery — nothing is executed. */
@@ -295,5 +295,5 @@ export async function runFinalGateAutofix(deps) {
295
295
  gateFailures: fin.failures
296
296
  };
297
297
  }
298
- return { ok: true, reason: fin.reason };
298
+ return { ok: true, reason: fin.reason, ...(fin.unobserved ? { unobserved: fin.unobserved } : {}) };
299
299
  }
@@ -38,6 +38,15 @@ export interface FinalGateOutcome {
38
38
  * run never completes silently carrying an accepted defect. Empty/absent = none.
39
39
  */
40
40
  openDebts?: AcceptDebt[];
41
+ /**
42
+ * Set (with the UNOBSERVED note) when the gate observed NOTHING dynamic — either
43
+ * no command was discoverable at all, or every discovered one was skipped as an
44
+ * environment gap. `ok` is still true (the statics did pass and there is nothing
45
+ * to fix), but this is NOT a PASS: the caller must record it as UNOBSERVED, never
46
+ * as "checked and fine". Absent ⇒ at least one dynamic command actually ran.
47
+ * See unobservedVerdict for the three-way verdict and the blocking decision.
48
+ */
49
+ unobserved?: string;
41
50
  }
42
51
  /**
43
52
  * The project's OWN whole-repo integration commands (test, then build — test
@@ -47,8 +56,9 @@ export interface FinalGateOutcome {
47
56
  *
48
57
  * THE MANIFEST ALLOWLIST BELOW IS NARROW, AND THAT IS A KNOWN, MEASURED GAP: a
49
58
  * C++/CMake project (no package.json) and a package.json whose only script is
50
- * `verify` both discover NOTHING here and fall through to the static-only PASS
51
- * at the `no integration command found` return below.
59
+ * `verify` both discover NOTHING here. That outcome is now reported as UNOBSERVED
60
+ * rather than as a PASS (see unobservedVerdict), which makes the blindness loud and
61
+ * durable — it does not remove it. The gap itself stands.
52
62
  *
53
63
  * The obvious fix — harvest each task's own `## verified tooling` section, which
54
64
  * DOES record the missing commands — was measured on 2026-07-27 and REFUTED. That
@@ -265,6 +275,48 @@ export declare function observabilityGapFailure(args: {
265
275
  /** Is this runner spawnable (bare or via a known install location)? */
266
276
  runnerResolvable: (bin: string) => boolean;
267
277
  }): string | null;
278
+ /**
279
+ * The THIRD verdict. observabilityGapFailure above covers "commands were DISCOVERED
280
+ * but every one failed to spawn" — a rank-0 FAIL. It deliberately returns null for
281
+ * `attempted === 0`, and until now that silence fell straight through to
282
+ * `PASS — no integration command found (statics passed)`: the run-16 blindness class
283
+ * entering through a different door, where "we never checked" reads exactly like "we
284
+ * checked and it was fine". Measured 2026-07-27: IAR1 (C++/CMake, no package.json)
285
+ * shipped that verdict TWICE while carrying 2 and 3 open verify-FAIL debts, and
286
+ * godot-engine (package.json whose only script is `verify`) reproduces it live today.
287
+ *
288
+ * So: observed anything dynamic ⇒ PASS; discovered-but-all-spawn-failed ⇒ the
289
+ * existing FAIL; observed NOTHING ⇒ this note, carried on an `ok: true` outcome.
290
+ *
291
+ * WHY NON-BLOCKING (decided, not deferred — the evidence cuts both ways and this is
292
+ * the resolution):
293
+ * - Blocking's case: both real occurrences also carried open verify-FAIL debt, so
294
+ * the runs with no dynamic evidence were exactly the runs already known to be
295
+ * carrying defects.
296
+ * - Against, and decisive: (1) that debt is ALREADY surfaced unconditionally at the
297
+ * gate moment, on PASS as on FAIL — the IAR1 records literally read "PASS — no
298
+ * integration command found … UNRESOLVED VERIFY-FAIL DEBT still open (2)". The
299
+ * missing signal was never the debt, it was the word PASS endorsing the run, and
300
+ * that is what this fixes. (2) `ok: false` routes into the autofix picker, whose
301
+ * seed is `reason`; "no integration command is discoverable" is not fixable by
302
+ * editing code, so the highest-probability child response is to FABRICATE a
303
+ * runnable command to satisfy the gate — the same fabrication class that refuted
304
+ * the `## verified tooling` harvest (see discoverIntegrationCommands) and that had
305
+ * run 11's fix child `rm` a sibling's deliverable. (3) That harvest being refuted
306
+ * means IAR1 and godot-engine can NEVER discover a command, so blocking would end
307
+ * every non-npm run in `failed` permanently, with no remedy — the task's own I3
308
+ * ("show blocking does not block IAR1/godot post-Task-1") is unsatisfiable, and
309
+ * its stated consequence is to downgrade to a warning and say so. This is that.
310
+ * The teeth are elsewhere and are real: the verdict word changes, the gate trail says
311
+ * UNOBSERVED, and the caller records a durable final-gate debt that the NEXT run's
312
+ * gate re-surfaces (it can never auto-close — it is not static-class).
313
+ */
314
+ export declare function unobservedVerdict(args: {
315
+ /** Dynamic commands the gate discovered and tried to run (0 ⇒ nothing existed). */
316
+ discovered: number;
317
+ /** Of those, how many actually RAN (a real pass or a real fail). */
318
+ observed: number;
319
+ }): string | null;
268
320
  export { taskThatIntroduced };
269
321
  /**
270
322
  * Run the final gate: static analysis first, then the lockfile consistency
@@ -76,8 +76,9 @@ function makeHasTarget(cwd, target) {
76
76
  *
77
77
  * THE MANIFEST ALLOWLIST BELOW IS NARROW, AND THAT IS A KNOWN, MEASURED GAP: a
78
78
  * C++/CMake project (no package.json) and a package.json whose only script is
79
- * `verify` both discover NOTHING here and fall through to the static-only PASS
80
- * at the `no integration command found` return below.
79
+ * `verify` both discover NOTHING here. That outcome is now reported as UNOBSERVED
80
+ * rather than as a PASS (see unobservedVerdict), which makes the blindness loud and
81
+ * durable — it does not remove it. The gap itself stands.
81
82
  *
82
83
  * The obvious fix — harvest each task's own `## verified tooling` section, which
83
84
  * DOES record the missing commands — was measured on 2026-07-27 and REFUTED. That
@@ -789,6 +790,55 @@ export function observabilityGapFailure(args) {
789
790
  + `could even spawn in this environment${runnerNote}; `
790
791
  + `the gate observed nothing dynamic and cannot vouch for the assembled app`);
791
792
  }
793
+ /**
794
+ * The THIRD verdict. observabilityGapFailure above covers "commands were DISCOVERED
795
+ * but every one failed to spawn" — a rank-0 FAIL. It deliberately returns null for
796
+ * `attempted === 0`, and until now that silence fell straight through to
797
+ * `PASS — no integration command found (statics passed)`: the run-16 blindness class
798
+ * entering through a different door, where "we never checked" reads exactly like "we
799
+ * checked and it was fine". Measured 2026-07-27: IAR1 (C++/CMake, no package.json)
800
+ * shipped that verdict TWICE while carrying 2 and 3 open verify-FAIL debts, and
801
+ * godot-engine (package.json whose only script is `verify`) reproduces it live today.
802
+ *
803
+ * So: observed anything dynamic ⇒ PASS; discovered-but-all-spawn-failed ⇒ the
804
+ * existing FAIL; observed NOTHING ⇒ this note, carried on an `ok: true` outcome.
805
+ *
806
+ * WHY NON-BLOCKING (decided, not deferred — the evidence cuts both ways and this is
807
+ * the resolution):
808
+ * - Blocking's case: both real occurrences also carried open verify-FAIL debt, so
809
+ * the runs with no dynamic evidence were exactly the runs already known to be
810
+ * carrying defects.
811
+ * - Against, and decisive: (1) that debt is ALREADY surfaced unconditionally at the
812
+ * gate moment, on PASS as on FAIL — the IAR1 records literally read "PASS — no
813
+ * integration command found … UNRESOLVED VERIFY-FAIL DEBT still open (2)". The
814
+ * missing signal was never the debt, it was the word PASS endorsing the run, and
815
+ * that is what this fixes. (2) `ok: false` routes into the autofix picker, whose
816
+ * seed is `reason`; "no integration command is discoverable" is not fixable by
817
+ * editing code, so the highest-probability child response is to FABRICATE a
818
+ * runnable command to satisfy the gate — the same fabrication class that refuted
819
+ * the `## verified tooling` harvest (see discoverIntegrationCommands) and that had
820
+ * run 11's fix child `rm` a sibling's deliverable. (3) That harvest being refuted
821
+ * means IAR1 and godot-engine can NEVER discover a command, so blocking would end
822
+ * every non-npm run in `failed` permanently, with no remedy — the task's own I3
823
+ * ("show blocking does not block IAR1/godot post-Task-1") is unsatisfiable, and
824
+ * its stated consequence is to downgrade to a warning and say so. This is that.
825
+ * The teeth are elsewhere and are real: the verdict word changes, the gate trail says
826
+ * UNOBSERVED, and the caller records a durable final-gate debt that the NEXT run's
827
+ * gate re-surfaces (it can never auto-close — it is not static-class).
828
+ */
829
+ export function unobservedVerdict(args) {
830
+ if (args.observed > 0)
831
+ return null;
832
+ // Kept short ON PURPOSE: the run-level trail line slices the reason at 300 chars,
833
+ // and the whole point of this verdict is that the durable record carries it.
834
+ const why = args.discovered === 0 ?
835
+ 'no integration, lockfile or boot command was discoverable here, so the gate ran '
836
+ + 'nothing at all'
837
+ : `all ${args.discovered} discovered command(s) skipped as environment gaps, so the `
838
+ + 'gate ran nothing observable';
839
+ return (`UNOBSERVED — NOT a pass: ${why}; statics passed, but this run produced NO evidence `
840
+ + 'that the assembled product builds, boots or works.');
841
+ }
792
842
  /**
793
843
  * Boot check hit an address-in-use bind failure. If the port is held by one of OUR
794
844
  * own orphaned gate children (a `dev`/`start` run), reap it and retry the boot once
@@ -885,15 +935,19 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
885
935
  const lockCmds = discoverLockfileChecks(cwd);
886
936
  const { cmds } = discoverIntegrationCommands(cwd);
887
937
  const boot = discoverBootCommand(cwd);
888
- // ZERO-DISCOVERY STILL READS AS A PASS HERE, AND THAT IS THE OPEN DEFECT. Nothing was
889
- // discovered, so nothing ran, so observabilityGapFailure (attempted === 0 → null) never
890
- // fires: "we never checked" is reported identically to "we checked and it was fine". IAR1
891
- // shipped this verdict TWICE while carrying open verify-FAIL debt (its .pi-tasks/
892
- // TASK_AUTO_0001.md:31 and TASK_AUTO_0002.md:37). The fix is to make this outcome
893
- // UNOBSERVED rather than PASS — it needs no new command source and so cannot inject a
894
- // fabricated failure, unlike the harvest lever refuted at discoverIntegrationCommands above.
938
+ // ZERO DISCOVERY IS UNOBSERVED, NEVER A PASS (see unobservedVerdict). Nothing was
939
+ // discovered, so nothing ran, so observabilityGapFailure (attempted === 0 → null) does
940
+ // not fire — and this outcome used to be reported as `PASS — no integration command
941
+ // found (statics passed)`, i.e. "we never checked" reading identically to "we checked
942
+ // and it was fine". IAR1 shipped that verdict TWICE while carrying open verify-FAIL
943
+ // debt (its .pi-tasks/TASK_AUTO_0001.md:31 and TASK_AUTO_0002.md:37). The outcome stays
944
+ // `ok: true` (non-blocking, justified at unobservedVerdict) but is now labelled, trailed
945
+ // and carried as debt by the caller. It needs no new command source, so unlike the
946
+ // harvest lever refuted at discoverIntegrationCommands it cannot inject a fabricated
947
+ // failure.
895
948
  if (lockCmds.length === 0 && cmds.length === 0 && !boot && failures.length === 0) {
896
- return withDebts({ ok: true, reason: 'no integration command found (statics passed)' });
949
+ const note = unobservedVerdict({ discovered: 0, observed: 0 }) ?? '';
950
+ return withDebts({ ok: true, unobserved: note, reason: note });
897
951
  }
898
952
  const ran = [];
899
953
  // Full-skip blindness counters (mx5 run 16): every dynamic spawn counts an
@@ -1071,10 +1125,20 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
1071
1125
  });
1072
1126
  }
1073
1127
  const warningNote = warnings.length > 0 ? ` — WARNING: ${warnings.join('; WARNING: ')}` : '';
1128
+ // The same three-way verdict at the other zero-observation door: commands WERE
1129
+ // discovered, none spawn-failed (so the run-16 guard correctly stayed silent — every
1130
+ // skip was a tool-level env gap), and yet nothing ran. That was `statics passed
1131
+ // (integration commands not runnable here)`, which is the identical "we never checked"
1132
+ // silence wearing different words. Unchanged when anything at all was observed, so a
1133
+ // project with runnable commands is byte-for-byte unaffected.
1134
+ const unobserved = unobservedVerdict({ discovered: dynAttempted, observed: dynObserved });
1074
1135
  return withDebts({
1075
1136
  ok: true,
1076
- reason: (ran.length > 0 ?
1077
- `statics + ${ran.map(c => `\`${c}\``).join(', ')} passed`
1078
- : 'statics passed (integration commands not runnable here)') + warningNote
1137
+ ...(unobserved ? { unobserved } : {}),
1138
+ reason: (unobserved ? `${unobserved} — ` : '')
1139
+ + (ran.length > 0 ?
1140
+ `statics + ${ran.map(c => `\`${c}\``).join(', ')} passed`
1141
+ : 'statics passed (integration commands not runnable here)')
1142
+ + warningNote
1079
1143
  });
1080
1144
  }
@@ -1343,6 +1343,50 @@ export const PHASES = [
1343
1343
  }
1344
1344
  }
1345
1345
  ];
1346
+ // INTEGRATION-DEPTH APPEND (2026-07-27): the lever proposed for this exact site —
1347
+ // deterministically append a known-runnable integration command to the VERIFY block
1348
+ // whenever a task's ACCEPTANCE claims runtime behaviour — is REFUTED at STEP 0 and was
1349
+ // NOT built. Nothing below is wired; this is the record.
1350
+ //
1351
+ // The defect is real and reproduces on four unrelated stacks. Measured with
1352
+ // scripts/verify-integration-depth-step0.ts (published metric regex in that file;
1353
+ // re-runnable, no model time): VERIFY blocks that boot or hit the real integrated
1354
+ // product — mx5 7/41, IAR1 3/10, godot-engine 2/20, runner 0/2, total 12/73 (16.4%).
1355
+ // That total OVERSTATES the truth: godot's two hits only match because the block greps
1356
+ // a URL out of CLAUDE.md, and IAR1's TASK_0006 curls a GitHub tarball to check a hash,
1357
+ // so genuine product integration is ~9/73 (~12%). The addressable class — runtime claim
1358
+ // in ACCEPTANCE, a boot command available with provenance, static-only VERIFY anyway —
1359
+ // is 29/73 (39.7%), clearing the task's 25% kill condition, but 27 of those 29 are mx5.
1360
+ //
1361
+ // What kills the lever is its input, not its premise. It needs a command that is
1362
+ // (1) provenance-bearing, (2) proven runnable, (3) terminating, and (4) integration-
1363
+ // shaped. Measured on scratch clones with scripts/integration-command-provenance.ts,
1364
+ // that intersection is EMPTY on every stack available here:
1365
+ // mx5 `bun run dev` is the only command matching the metric and it does not
1366
+ // terminate (probe killed it at 60s) — appending it hangs the verify child
1367
+ // until the 15-minute command watchdog and then FAILs. Every command that DOES
1368
+ // terminate (`bun run build` exit 0) fails the metric.
1369
+ // godot `godot --headless --quit` (0.8s, exit 0) and the real GUT runner (1.7s,
1370
+ // exit 0) are both appendable — and neither matches the metric, which is
1371
+ // web-shaped (curl/localhost/PORT=/run dev/http://). Nothing appendable can
1372
+ // move the registered metric on a non-web stack, so the two-stack A/B the
1373
+ // task requires is unsatisfiable by construction, not by sample size.
1374
+ // IAR1 no candidate at all: cmake is absent from this box (the N5 finding).
1375
+ // R4 forbids swapping the metric after seeing results, so a broadened "boots the real
1376
+ // product" metric is a separate pre-registered experiment, not a rescue of this one.
1377
+ //
1378
+ // The deeper finding: on the stack holding 93% of the addressable mass, integration is
1379
+ // not a COMMAND. mx5's 7 integrated blocks are whole task-specific procedures — pick a
1380
+ // free port (3911/41234/42421/3001), export DATABASE_URL, boot, seed, log in, assert N
1381
+ // endpoints, tear down. A host-side append cannot synthesize that from provenance
1382
+ // (launch-contract.md records script NAMES only — no port, no health URL), and
1383
+ // synthesizing it from the task's own `## verified tooling` is the N5 fabrication road.
1384
+ // That machinery already exists in ONE place that owns ports, seeding and teardown: the
1385
+ // final gate's render check. The mx5-class defect belongs there (nexttask TASK 6), not
1386
+ // in per-task VERIFY blocks.
1387
+ //
1388
+ // Durable assets kept: both rigs above and their unit tests. Do NOT wire an append here
1389
+ // without a command source that satisfies all four properties at once.
1346
1390
  export async function postCommitPhase(phase, deps, pc, out) {
1347
1391
  if (phase.name !== 'refine')
1348
1392
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.21.3",
3
+ "version": "0.21.5",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",