infinity-harness 2.3.0 → 2.3.1

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/CHANGELOG.md CHANGED
@@ -4,7 +4,10 @@ All notable changes to this project are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [2.3.0] — 2026-08-24
7
+ ## [2.3.1] — 2026-08-24
8
+
9
+ *(2.3.0 was staged at the registry and never completed; 2.3.1 is that release plus the last two
10
+ fixes below, and is the first version of this work anyone can install.)*
8
11
 
9
12
  Five bugs were reported against 2.1.0 by someone actually using the thing. Every one of them was
10
13
  real, none of them could be seen by a test that mocks pi, and finding out why led to the change
@@ -106,6 +109,23 @@ that matters most in this release: **the suite now drives a real `pi` process.**
106
109
 
107
110
  - **The phase picker did not offer RESEARCH** — a feature nobody could find.
108
111
 
112
+ - **A handoff in a one-shot `pi -p` run replaced the session out from under the instance that
113
+ asked for it**, so every handler afterwards touched a torn-down context and pi reported
114
+ "This extension ctx is stale after session replacement" on every turn. A headless run has no
115
+ next turn to hand anything to, so it does not hand off — and the extension now stops touching
116
+ pi the moment its session is shut down, whatever the reason.
117
+
118
+ - **A new phase counted as a stall.** The first failure of a phase was compared against the
119
+ fingerprint taken when the *previous* phase passed — identical, because nothing had happened
120
+ yet — so the run spent `retry` and `reframe` on the opening turn of every single phase, and
121
+ arrived at the rungs that matter with the cheap ones already gone. A stall is the agent
122
+ producing nothing when asked; a fresh brief has not asked yet.
123
+
124
+ - **The gate history counted every pass twice.** `runChecks` recorded the verdict and
125
+ `transitionPhase` recorded it again, so the history read `define:pass → define:pass`, which says
126
+ a phase had to be attempted twice — the opposite of what happened. Repeated *failures* are still
127
+ every one of them: that is the fact a human comes back to read.
128
+
109
129
  ### Changed
110
130
 
111
131
  - The dashboard shows every subtask, not only the active task's. The widget has nine rows and a job
@@ -117,6 +117,17 @@ export default function (pi: ExtensionAPI): void {
117
117
  let remoteDir: string | null = null;
118
118
  let view: WidgetView = defaultView();
119
119
 
120
+ /**
121
+ * Is this instance's session still the live one?
122
+ *
123
+ * After `ctx.newSession()` pi tears the old runtime down and rebinds
124
+ * extensions, but this closure and its registered handlers still exist. Any
125
+ * of them that touches `pi` or a captured `ctx` afterwards gets
126
+ * "This extension ctx is stale after session replacement" — which is what a
127
+ * handoff produced on every single turn until this flag existed.
128
+ */
129
+ let sessionLive = true;
130
+
120
131
  /**
121
132
  * Is a continuous run armed?
122
133
  *
@@ -307,6 +318,10 @@ export default function (pi: ExtensionAPI): void {
307
318
  // this session stops driving and the replacement never arrives. One
308
319
  // attempt, then carry on here — a run that continues in a fat session is
309
320
  // far better than a run that stops.
321
+ // A one-shot `pi -p` run has no next turn to hand anything to: replacing
322
+ // its session mid-flight produces a stale-context error and nothing else.
323
+ if (ctx.mode !== "tui" && ctx.mode !== "rpc") return false;
324
+
310
325
  if (handingOff) {
311
326
  if (hasPendingHandoff(dir)) {
312
327
  notify(ctx, "infinity-harness: the new session never started — continuing here.", "warning");
@@ -531,6 +546,7 @@ export default function (pi: ExtensionAPI): void {
531
546
  * summarised. Anything the run cannot afford to forget belongs here.
532
547
  */
533
548
  pi.on("before_agent_start", async (event, ctx) => {
549
+ if (!sessionLive) return;
534
550
  const dir = projectDir(ctx);
535
551
  if (!isHarnessProject(dir)) return;
536
552
  try {
@@ -544,6 +560,7 @@ export default function (pi: ExtensionAPI): void {
544
560
  });
545
561
 
546
562
  pi.on("session_tree", async (_event, ctx) => {
563
+ if (!sessionLive) return;
547
564
  refreshWidget(ctx);
548
565
  });
549
566
 
@@ -553,6 +570,7 @@ export default function (pi: ExtensionAPI): void {
553
570
  * few calls costs little and keeps the plan honest.
554
571
  */
555
572
  pi.on("context", async (event, ctx) => {
573
+ if (!sessionLive) return;
556
574
  const dir = projectDir(ctx);
557
575
  if (!isHarnessProject(dir)) return;
558
576
 
@@ -630,6 +648,7 @@ export default function (pi: ExtensionAPI): void {
630
648
  });
631
649
 
632
650
  pi.on("session_compact", async (event, ctx) => {
651
+ if (!sessionLive) return;
633
652
  const dir = projectDir(ctx);
634
653
  if (!isHarnessProject(dir)) return;
635
654
  try {
@@ -658,6 +677,7 @@ export default function (pi: ExtensionAPI): void {
658
677
  });
659
678
 
660
679
  pi.on("turn_end", async (_event, ctx) => {
680
+ if (!sessionLive) return;
661
681
  refreshWidget(ctx);
662
682
  });
663
683
 
@@ -670,6 +690,7 @@ export default function (pi: ExtensionAPI): void {
670
690
  * `/reload`, `/resume`, and pi being restarted.
671
691
  */
672
692
  pi.on("agent_settled", async (_event, ctx) => {
693
+ if (!sessionLive) return;
673
694
  const dir = projectDir(ctx);
674
695
  if (!isHarnessProject(dir)) return;
675
696
  if (loopBusy || handingOff) return;
@@ -749,6 +770,7 @@ export default function (pi: ExtensionAPI): void {
749
770
  * touch of the config would stop the harness configuring itself.
750
771
  */
751
772
  pi.on("tool_call", async (event, ctx) => {
773
+ if (!sessionLive) return;
752
774
  const dir = projectDir(ctx);
753
775
  if (!isHarnessProject(dir)) return;
754
776
 
@@ -791,6 +813,7 @@ export default function (pi: ExtensionAPI): void {
791
813
  });
792
814
 
793
815
  pi.on("session_shutdown", async () => {
816
+ sessionLive = false;
794
817
  if (remoteServer) {
795
818
  try {
796
819
  await remoteServer.close();
@@ -1392,7 +1415,12 @@ export default function (pi: ExtensionAPI): void {
1392
1415
  } catch (e) {
1393
1416
  // A handoff that cannot happen must never end the run: fall back to
1394
1417
  // carrying on in this session, which is the pre-2.3 behaviour.
1418
+ //
1419
+ // Unless the session is already gone — `newSession` can fail *after*
1420
+ // replacing the runtime, and reaching for the old `pi` then is the
1421
+ // "stale ctx" error rather than a recovery.
1395
1422
  clearHandoff(dir);
1423
+ if (!sessionLive) return;
1396
1424
  notify(ctx, `infinity-harness: could not start a new session — ${errMsg(e)}`, "warning");
1397
1425
  pi.sendUserMessage(pending.kickoff, { deliverAs: "followUp" });
1398
1426
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infinity-harness",
3
- "version": "2.3.0",
4
- "description": "A pi agent extension that runs a gated build pipeline unattended enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
3
+ "version": "2.3.1",
4
+ "description": "A pi agent extension that runs a gated build pipeline unattended \u2014 enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package",