pi-baton 0.7.2 → 0.7.4

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
@@ -6,14 +6,26 @@ This project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- - Add Buy Me a Coffee sponsor button to README and native GitHub funding link via `.github/FUNDING.yml`.
9
+ ### Changed
10
+
11
+ - Bump package version to `0.7.3` for the next patch release.
12
+ - Bumped Pi dev toolchain packages to clear the transitive `protobufjs` audit advisory; the remaining `brace-expansion` finding is documented as upstream-blocked by `@earendil-works/pi-coding-agent`'s published shrinkwrap.
13
+ - Removed unused `clearActiveRunPointer` export from `lib/run-store.ts` (dead code with no callers; `loadActiveRun` / `loadMostRecentTerminalRun` already treat a missing `runId` as no active pointer).
14
+
15
+ ## [0.7.2] - 2026-07-04
16
+
17
+ ### Added
18
+
19
+ - `/baton:status` now shows the most recent finished run summary (state, last step, iteration, run directory) after a run reaches `completed` or `failed`, with clear finished-run framing instead of the no-active-run message.
20
+
21
+ - Buy Me a Coffee sponsor button in README and native GitHub funding link via `.github/FUNDING.yml`.
10
22
 
11
23
  ### Changed
12
24
 
13
25
  - Merged Dependabot PR #14: bump `@earendil-works/pi-ai` devDependency from 0.78.1 to 0.80.2 (type-check clean, 57/57 tests passing).
14
26
  - Removed leftover template scaffolding (`prompts/`, `themes/`, `skills/`).
15
27
  - Deleted stale template bootstrap docs (`docs/examples.md`, `docs/github-template.md`, `docs/repository-settings.md`, `docs/typescript.md`, `docs/template-checklist.md`) that shipped to npm consumers describing non-existent commands.
16
- - Updated ROADMAP.md to reflect current version (0.7.1), test count (57), and seed completion status for S-101, S-102, S-103, S-105.
28
+ - Updated ROADMAP.md to reflect current version (0.7.2), test count (57), and seed completion status for S-101, S-102, S-103, S-105.
17
29
 
18
30
  ## [0.7.1] - 2026-06-27
19
31
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Pi Baton
2
2
 
3
+ [![Join dotfield.xyz on Discord](https://img.shields.io/badge/Join%20dotfield.xyz%20on%20Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/4945dXZVW5)
4
+
3
5
  <p align="center">
4
6
  <img src="./assets/pi-baton-icon-512.png" alt="Pi Baton icon" width="192" height="192" />
5
7
  </p>
@@ -37,7 +39,7 @@ pi install npm:pi-baton
37
39
  Pin a specific version when you want reproducible installs:
38
40
 
39
41
  ```bash
40
- pi install npm:pi-baton@0.7.1
42
+ pi install npm:pi-baton@0.7.4
41
43
  ```
42
44
 
43
45
  Install into the current project instead of your user Pi settings:
@@ -169,4 +171,4 @@ For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
169
171
 
170
172
  ## License
171
173
 
172
- MIT
174
+ MIT
@@ -0,0 +1,249 @@
1
+ # Incident — 2026-07-04 `Publish to npm` failed with duplicate-version E403
2
+
3
+ > Status: **investigation report only.** No release workflow, package version,
4
+ > CHANGELOG, npm registry state, or release was changed to produce this document.
5
+ > Correction options below are proposals for a separate follow-up issue.
6
+
7
+ ## Summary
8
+
9
+ The `Publish to npm` run
10
+ [28704529442](https://github.com/eiei114/pi-baton/actions/runs/28704529442)
11
+ (`v0.7.2`, dispatched 2026-07-04) failed at the final `npm publish` step with:
12
+
13
+ ```text
14
+ npm error code E403
15
+ npm error 403 403 Forbidden - PUT https://registry.npmjs.org/pi-baton
16
+ npm error 403 You cannot publish over the previously published versions: 0.7.2.
17
+ ```
18
+
19
+ **Root cause (classification: trigger/configuration):** a version-bump push to
20
+ `main` fans out into **two concurrent `publish.yml` runs** for the same version,
21
+ keyed by **different concurrency groups** so they are not serialized. The
22
+ "Skip already published version" guard is a non-atomic check-then-publish
23
+ (TOCTOU) and was defeated by ~11s of npm registry propagation latency, so the
24
+ losing run reached `npm publish` after the winning run had already published
25
+ `0.7.2`.
26
+
27
+ This is **not** an authentication / Trusted Publishing failure (OIDC provenance
28
+ was signed successfully) and **not** a duplicate-version-at-dispatch (the version
29
+ was unpublished when the run started).
30
+
31
+ **Impact:** none on the registry. `pi-baton@0.7.2` is correctly published and is
32
+ `dist-tags.latest`. The failed run is a harmless duplicate that lost the race.
33
+
34
+ ## Evidence
35
+
36
+ ### Failed run
37
+
38
+ | Field | Value |
39
+ | --- | --- |
40
+ | Run | [`Publish to npm` · 28704529442](https://github.com/eiei114/pi-baton/actions/runs/28704529442) |
41
+ | Workflow | `.github/workflows/publish.yml` |
42
+ | Event / trigger | `workflow_dispatch` |
43
+ | Ref checked out | `v0.7.2` (tag), via `actions/checkout@v7` (`ref: v0.7.2`) |
44
+ | Package | `pi-baton@0.7.2` |
45
+ | Started / failed | 2026-07-04T11:18:38Z / 2026-07-04T11:19:11Z |
46
+ | Failed step | `Publish to npm` (`npm publish --access public`) |
47
+ | Conclusion | failure, exit code 1 |
48
+
49
+ Step-by-step status (from the run): `Set up job`, `Checkout`, `Setup Node.js`,
50
+ `Ensure npm supports trusted publishing`, `Install dependencies`, `Validate
51
+ package`, `Skip already published version` all **passed**; `Publish to npm`
52
+ **failed**.
53
+
54
+ Failure output excerpt (from the failed step log):
55
+
56
+ ```text
57
+ npm notice 📦 pi-baton@0.7.2
58
+ ...
59
+ npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
60
+ npm notice publish Signed provenance statement with source and build information from GitHub Actions
61
+ npm notice publish Provenance statement published to transparency log: https://search.sigstore.dev/?logIndex=2069634820
62
+ npm error code E403
63
+ npm error 403 403 Forbidden - PUT https://registry.npmjs.org/pi-baton - You cannot publish over the previously published versions: 0.7.2.
64
+ npm error A complete log of this run can be found in: /home/runner/.npm/_logs/2026-07-04T11_19_08_227Z-debug-0.log
65
+ ##[error]Process completed with exit code 1.
66
+ ```
67
+
68
+ Note: provenance **was** signed (Trusted Publishing / OIDC worked); the failure is
69
+ the publish PUT, not authentication.
70
+
71
+ ### Who dispatched it (trigger chain)
72
+
73
+ The `workflow_dispatch` run was dispatched by the `Auto Release` workflow, not by
74
+ a human clicking "Run workflow" in the UI:
75
+
76
+ - PR #25 (`chore/sponsor-funding-patch-20260704`) merged to `main` at
77
+ **11:18:31Z**, bumping `package.json` to `0.7.2`.
78
+ - The merge push fired two workflows on `main`:
79
+ - [`Publish to npm` · 28704526901](https://github.com/eiei114/pi-baton/actions/runs/28704526901)
80
+ (event `push`, ref `refs/heads/main`) — **success**.
81
+ - [`Auto Release` · 28704526903](https://github.com/eiei114/pi-baton/actions/runs/28704526903)
82
+ (event `push`, ref `refs/heads/main`) — **success**.
83
+ - `Auto Release` detected the version bump, created tag `v0.7.2` and the GitHub
84
+ Release (11:18:35–37Z), then ran its final step:
85
+ `gh workflow run publish.yml --ref v0.7.2 -f ref=v0.7.2`
86
+ (log: `Run TAG="v0.7.2"` → `gh workflow run publish.yml --ref "$TAG" -f ref="$TAG"`).
87
+ - That dispatch produced the failed run **28704529442** at **11:18:38Z**.
88
+
89
+ So a single version-bump merge produced **two** `publish.yml` runs for `0.7.2`:
90
+ one from the `push` trigger (main) and one from `Auto Release`'s explicit
91
+ `workflow_dispatch` (v0.7.2). This double-trigger is consistent with the design
92
+ documented in [`docs/release.md`](../release.md) (the push trigger and the
93
+ auto-release handoff both fire; the tag/`release.published` triggers are
94
+ unreliable for `GITHUB_TOKEN`-created tags, which is why auto-release dispatches
95
+ explicitly).
96
+
97
+ ### Race timeline (all times UTC)
98
+
99
+ | Time | Run | Event |
100
+ | --- | --- | --- |
101
+ | 11:18:31 | 28704526901 / 28704526903 | merge push to `main` fires `publish.yml` (push) + `auto-release.yml` |
102
+ | 11:18:35–37 | 28704526903 | `Auto Release` creates tag `v0.7.2`, release, dispatches `publish.yml` |
103
+ | 11:18:38 | 28704529442 | dispatched `publish.yml` (v0.7.2) starts — the failed run |
104
+ | 11:18:52 | 28704526901 | skip guard says `Publishing pi-baton@0.7.2.` (0.7.2 not yet on npm) |
105
+ | **11:18:56** | **28704526901** | **`+ pi-baton@0.7.2` — push run publishes 0.7.2** |
106
+ | 11:19:07 | 28704529442 | skip guard runs `npm view pi-baton@0.7.2 version` → **404** (skip=false) |
107
+ | 11:19:08 | 28704529442 | prints `Publishing pi-baton@0.7.2.`, starts `npm publish` |
108
+ | 11:19:11 | 28704529442 | `E403` — cannot publish over `0.7.2` (already published 11s earlier) |
109
+
110
+ The decisive detail: the failed run's `npm view pi-baton@0.7.2 version` returned
111
+ **404 at 11:19:07**, ~11 seconds **after** the push run actually published
112
+ `0.7.2` at 11:18:56. The skip guard therefore proceeded, and `npm publish` hit
113
+ the real-time conflict.
114
+
115
+ ### npm public state (read-only)
116
+
117
+ ```console
118
+ $ npm view pi-baton version dist-tags.latest
119
+ 0.7.2
120
+ { latest: '0.7.2' }
121
+
122
+ $ npm view pi-baton time --json
123
+ {
124
+ "0.7.2": "2026-07-04T11:18:56.171Z",
125
+ ...
126
+ }
127
+ ```
128
+
129
+ The `0.7.2` timestamp was published by the winning push run (28704526901).
130
+
131
+ `pi-baton@0.7.2` is present, is `dist-tags.latest`, and was published at
132
+ 11:18:56Z — i.e. by the parallel push run (28704526901), not by the failed run.
133
+ Published versions: `0.2.2, 0.2.3, 0.3.0, 0.4.0, 0.5.0, 0.6.0, 0.7.0, 0.7.1,
134
+ 0.7.2`. No registry correction is needed.
135
+
136
+ ## Cause classification
137
+
138
+ **Trigger / configuration** — specifically a concurrent-duplicate-publish race:
139
+
140
+ 1. **Double trigger for one version bump.** A `package.json` version bump on
141
+ `main` fires `publish.yml` twice: once via the `push` (branches: `main`,
142
+ paths: `package.json`) trigger, and once via `auto-release.yml`'s explicit
143
+ `gh workflow run publish.yml --ref "$TAG" -f ref="$TAG"`.
144
+ 2. **Concurrency groups do not collide.** `publish.yml` uses
145
+ `group: npm-publish-${{ github.event.inputs.ref || github.ref }}`:
146
+ - push run → `npm-publish-refs/heads/main`
147
+ - dispatch run → `npm-publish-v0.7.2`
148
+
149
+ Different keys ⇒ `concurrency` does not serialize the two runs; they execute
150
+ in parallel for the same version.
151
+ 3. **Non-atomic idempotency guard (TOCTOU).** "Skip already published version"
152
+ runs `npm view <name>@<version>` and, on 404, proceeds to `npm publish` in a
153
+ later step. There is no lock between check and publish, and `npm view` reads
154
+ registry metadata that lags the actual publish (here by ~11s). The losing run
155
+ therefore sees "not published", then loses the race to `npm publish`.
156
+
157
+ Explicitly **not** the cause:
158
+
159
+ - **Duplicate-version at dispatch.** No — `0.7.2` was not on npm when the run was
160
+ dispatched (11:18:38) or when its skip guard ran (the guard returned 404).
161
+ - **Trusted Publishing / authentication.** No — provenance was signed
162
+ successfully, `permissions.id-token: write` is set, `registry-url` is correct,
163
+ and no `NPM_TOKEN` is used (Trusted Publishing). Auth was healthy.
164
+
165
+ ## Reproducible non-publish check
166
+
167
+ These commands are **read-only** — they do not publish, rerun the workflow, or
168
+ touch the registry/versions. They reproduce the diagnosis from a clean checkout.
169
+
170
+ ```bash
171
+ # 1) Confirm 0.7.2 is published and is latest (read-only registry query).
172
+ npm view pi-baton version # -> 0.7.2
173
+ npm view pi-baton dist-tags.latest # -> 0.7.2
174
+ npm view pi-baton@0.7.2 time --json # -> "0.7.2": "2026-07-04T11:18:56.171Z"
175
+
176
+ # 2) Show the two concurrent publish runs produced by one version-bump merge.
177
+ # (--created takes a date, not a full timestamp.)
178
+ gh run list --repo eiei114/pi-baton --workflow "Publish to npm" \
179
+ --created 2026-07-04 --limit 10 \
180
+ --json databaseId,event,headBranch,conclusion,createdAt
181
+ # Expect both:
182
+ # 28704526901 push main success (published 0.7.2)
183
+ # 28704529442 workflow_dispatch v0.7.2 failure (E403 duplicate)
184
+
185
+ # 3) Show the dispatch origin (Auto Release triggered the failed run).
186
+ gh run view 28704526903 --repo eiei114/pi-baton --log \
187
+ | grep -E 'gh workflow run publish.yml|released=true|TAG="v0.7.2"'
188
+
189
+ # 4) Prove the two runs had DIFFERENT concurrency groups (so they raced).
190
+ # publish.yml: group: npm-publish-${{ github.event.inputs.ref || github.ref }}
191
+ # push run -> github.event.inputs.ref is unset, github.ref = refs/heads/main
192
+ # => "npm-publish-refs/heads/main"
193
+ # dispatch run -> github.event.inputs.ref = "v0.7.2" (-f ref=v0.7.2)
194
+ # => "npm-publish-v0.7.2"
195
+ node -e '
196
+ const g = (ref, inputRef) => `npm-publish-${inputRef || ref}`;
197
+ console.log("push :", g("refs/heads/main", undefined));
198
+ console.log("dispatch:", g("refs/tags/v0.7.2", "v0.7.2"));
199
+ console.log("collide :", g("refs/heads/main", undefined) === g("refs/tags/v0.7.2", "v0.7.2"));
200
+ '
201
+ # => push : npm-publish-refs/heads/main
202
+ # => dispatch: npm-publish-v0.7.2
203
+ # => collide : false
204
+ ```
205
+
206
+ ## Minimal safe correction options
207
+
208
+ None of these are applied here — they are candidates for a **separate
209
+ correction issue** (release/publish workflows remain human-owned). Listed from
210
+ smallest/safest to largest behavior change.
211
+
212
+ - **Option A — make `npm publish` idempotent (recommended, smallest).** In the
213
+ `Publish to npm` step, treat the specific "cannot publish over the previously
214
+ published versions" `E403` as success instead of failure. This makes the
215
+ workflow resilient to the double-trigger race regardless of concurrency or
216
+ propagation lag. Scope the match to that exact message so genuine permission
217
+ errors still fail. Sketch (not applied):
218
+
219
+ ```bash
220
+ set +e
221
+ npm publish --access public 2>&1 | tee /tmp/publish.log
222
+ status=${PIPESTATUS[0]}
223
+ set -e
224
+ if [ "$status" -ne 0 ] && grep -Eq 'cannot publish over the previously published versions' /tmp/publish.log; then
225
+ echo "Version already published; treating publish as success."
226
+ exit 0
227
+ fi
228
+ exit "$status"
229
+ ```
230
+
231
+ - **Option B — serialize the two runs via one concurrency group.** Replace the
232
+ ref-keyed group with a single constant group (e.g. `group: npm-publish`) and
233
+ keep `cancel-in-progress: false`, so the push run and the auto-release dispatch
234
+ queue instead of racing. Best combined with Option A, since the skip guard is
235
+ still non-atomic. Avoid `cancel-in-progress: true` here — cancelling mid-publish
236
+ risks a partial publish.
237
+
238
+ - **Option C — collapse to a single handoff path.** Remove the `push`
239
+ (`branches: [main]`, `paths: [package.json]`) trigger from `publish.yml` so a
240
+ version bump publishes only via `auto-release.yml`'s explicit dispatch (the path
241
+ `docs/release.md` already describes as the reliable one). Largest behavior
242
+ change; needs validation that the manual tag / `release.published` /
243
+ `workflow_dispatch` entry points still cover intended cases.
244
+
245
+ ## Out of scope for this issue (enforced)
246
+
247
+ Per the issue brief, this slice intentionally does **not**: edit release/publish
248
+ workflows, publish a package, rerun the workflow, change the package version, or
249
+ update the CHANGELOG. A safe correction should be tracked in a follow-up issue.
@@ -5,7 +5,12 @@ import { MissingAgentsError, validateWorkflowAgents } from "../lib/agents.ts";
5
5
  import { ensureBatonScaffolding } from "../lib/paths.ts";
6
6
  import { formatRunResultSummary, runContinuous } from "../lib/run-engine.ts";
7
7
  import { createRunUiController } from "../lib/run-ui.ts";
8
- import { ActiveRunGuardError, createIdleRun, loadActiveRun } from "../lib/run-store.ts";
8
+ import {
9
+ ActiveRunGuardError,
10
+ createIdleRun,
11
+ loadActiveRun,
12
+ loadMostRecentTerminalRun,
13
+ } from "../lib/run-store.ts";
9
14
  import { NO_ACTIVE_RUN_MESSAGE, formatStatusSummary } from "../lib/status.ts";
10
15
  import { createSubagentRunner } from "../lib/subagent-runner.ts";
11
16
  import { WorkflowNameCollisionError, createWorkflowScaffold } from "../lib/workflow-scaffold.ts";
@@ -185,7 +190,7 @@ export default function (pi: ExtensionAPI) {
185
190
  pi.registerCommand("baton:status", {
186
191
  description: "Show the active Baton run summary",
187
192
  handler: async (_args, ctx) => {
188
- const manifest = await loadActiveRun(ctx.cwd);
193
+ const manifest = (await loadActiveRun(ctx.cwd)) ?? (await loadMostRecentTerminalRun(ctx.cwd));
189
194
  if (!manifest) {
190
195
  ctx.ui.notify(NO_ACTIVE_RUN_MESSAGE, "info");
191
196
  return;
package/lib/run-store.ts CHANGED
@@ -41,13 +41,32 @@ export async function readRunManifest(cwd: string, runId: string): Promise<RunMa
41
41
  return readJson<RunManifest>(getRunManifestPath(cwd, runId));
42
42
  }
43
43
 
44
+ function isTerminalRunState(state: RunState): boolean {
45
+ return state === "completed" || state === "failed";
46
+ }
47
+
44
48
  export async function loadActiveRun(cwd: string): Promise<RunManifest | null> {
45
49
  const pointer = await readActiveRunPointer(cwd);
46
- if (!pointer) return null;
50
+ if (!pointer?.runId) return null;
51
+
52
+ try {
53
+ const manifest = await readRunManifest(cwd, pointer.runId);
54
+ if (isTerminalRunState(manifest.state)) {
55
+ return null;
56
+ }
57
+ return manifest;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export async function loadMostRecentTerminalRun(cwd: string): Promise<RunManifest | null> {
64
+ const pointer = await readActiveRunPointer(cwd);
65
+ if (!pointer?.runId) return null;
47
66
 
48
67
  try {
49
68
  const manifest = await readRunManifest(cwd, pointer.runId);
50
- if (manifest.state === "completed" || manifest.state === "failed") {
69
+ if (!isTerminalRunState(manifest.state)) {
51
70
  return null;
52
71
  }
53
72
  return manifest;
@@ -65,10 +84,6 @@ export async function setActiveRunPointer(cwd: string, runId: string): Promise<v
65
84
  await writeJson(getActiveRunPointerPath(cwd), { runId } satisfies ActiveRunPointer);
66
85
  }
67
86
 
68
- export async function clearActiveRunPointer(cwd: string): Promise<void> {
69
- await writeJson(getActiveRunPointerPath(cwd), { runId: null });
70
- }
71
-
72
87
  export interface CreateRunInput {
73
88
  workflowId: string;
74
89
  workflowName: string;
package/lib/status.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import type { RunManifest } from "./types.ts";
2
2
 
3
+ function isTerminalRunState(state: RunManifest["state"]): boolean {
4
+ return state === "completed" || state === "failed";
5
+ }
6
+
3
7
  export function formatStatusSummary(manifest: RunManifest): string {
4
- return [
8
+ const lines: string[] = [];
9
+
10
+ if (isTerminalRunState(manifest.state)) {
11
+ lines.push(`This Baton run has finished (${manifest.state}).`);
12
+ }
13
+
14
+ lines.push(
5
15
  `workflow: ${manifest.workflowName}`,
6
16
  `task brief: ${manifest.taskBrief}`,
7
17
  `last step: ${manifest.lastStep ?? "(none)"}`,
@@ -9,7 +19,9 @@ export function formatStatusSummary(manifest: RunManifest): string {
9
19
  `run state: ${manifest.state}`,
10
20
  `iteration count: ${manifest.iteration}`,
11
21
  `run directory: .pi/baton/runs/${manifest.id}`,
12
- ].join("\n");
22
+ );
23
+
24
+ return lines.join("\n");
13
25
  }
14
26
 
15
27
  export const NO_ACTIVE_RUN_MESSAGE =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-baton",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Pi-native workflow baton runner with per-step model switching and isolated step context.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -58,8 +58,8 @@
58
58
  "typebox": "*"
59
59
  },
60
60
  "devDependencies": {
61
- "@earendil-works/pi-ai": "latest",
62
- "@earendil-works/pi-coding-agent": "latest",
61
+ "@earendil-works/pi-ai": "^0.82.1",
62
+ "@earendil-works/pi-coding-agent": "^0.82.1",
63
63
  "typebox": "latest",
64
64
  "@types/node": "^26.0.1",
65
65
  "typescript": "^6.0.3"