pi-baton 0.7.4 → 0.8.0
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 +25 -1
- package/README.md +17 -7
- package/docs/release.md +2 -2
- package/docs/workflows.md +154 -0
- package/extensions/index.ts +14 -1
- package/lib/run-store.ts +102 -1
- package/lib/status.ts +43 -0
- package/package.json +4 -4
- package/workflows/two-stage-review-gauntlet.yaml +75 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ This project follows semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-09-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `/baton:history` lists recent completed or failed Baton runs from persisted `.pi/baton/runs/` manifests, with bounded scan/output limits, skip diagnostics for unreadable manifests, and tests for empty, single, multi-run, exclusion, scan-cap ordering, and command registration cases.
|
|
14
|
+
- `/baton:status` and `formatStatusSummary` now include the stored `failure` reason for terminal failed runs, with status tests covering iteration-cap exhaustion and other failure paths.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- README quick start and `docs/workflows.md` now document when to use `/baton:history` versus `/baton:status`, including the terminal-run fallback behavior for status.
|
|
19
|
+
- README quick start and `docs/workflows.md` now document that `/baton:status` falls back to the most recent finished run when no active run exists.
|
|
20
|
+
- Refreshed ROADMAP.md: marked S-109 done, updated release direction for 0.8.0/0.9.0, and added maintenance seed candidates S-110 through S-113 for the weekly seed planner.
|
|
21
|
+
- Updated ROADMAP.md to mark S-108 done and refresh the security status table after the two-stage review gauntlet shipped in 0.7.5.
|
|
22
|
+
- Aligned ROADMAP S-109 status with the shipped `docs/workflows.md` authoring guide.
|
|
23
|
+
|
|
24
|
+
## [0.7.5] - 2026-08-22
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Merge the 2026-08-22 managed OSS dependency and maintenance PR batch.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Added the builtin `Two-Stage Review Gauntlet` workflow to demonstrate a non-default two-stage review graph.
|
|
33
|
+
|
|
9
34
|
### Changed
|
|
10
35
|
|
|
11
36
|
- Bump package version to `0.7.3` for the next patch release.
|
|
@@ -151,4 +176,3 @@ This project follows semantic versioning.
|
|
|
151
176
|
- Initial Pi package template.
|
|
152
177
|
- Example extension, Agent Skill, prompt, and theme.
|
|
153
178
|
- CI and npm Trusted Publishing workflow.
|
|
154
|
-
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install npm:pi-baton
|
|
|
39
39
|
Pin a specific version when you want reproducible installs:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
pi install npm:pi-baton@0.
|
|
42
|
+
pi install npm:pi-baton@0.8.0
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Install into the current project instead of your user Pi settings:
|
|
@@ -74,10 +74,16 @@ Then run:
|
|
|
74
74
|
/baton:new create a workflow scaffold
|
|
75
75
|
/baton:start choose workflow + task brief → idle run
|
|
76
76
|
/baton:run execute run to terminal state (with live widget)
|
|
77
|
-
/baton:status show the active run summary
|
|
77
|
+
/baton:status show the active run summary, or the most recent finished run
|
|
78
|
+
/baton:history list recent completed or failed runs from persisted history
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
Use `/baton:status` for the active run or the single most recent finished run. Use `/baton:history` when you need a short list of recent completed or failed runs and their `.pi/baton/runs/<id>` directories.
|
|
82
|
+
|
|
83
|
+
Builtin workflows work out of the box — no agent setup required:
|
|
84
|
+
|
|
85
|
+
- `Default Review Loop` (`implement → review → fix`)
|
|
86
|
+
- `Two-Stage Review Gauntlet` (`draft → technical_review → editorial_review`, with rejects routed through `fix`)
|
|
81
87
|
|
|
82
88
|
## Prerequisites
|
|
83
89
|
|
|
@@ -94,12 +100,16 @@ Discovery order: project `.pi/agents/` → user `~/.pi/agent/agents/` → pi-bat
|
|
|
94
100
|
|
|
95
101
|
## Workflow authoring
|
|
96
102
|
|
|
103
|
+
See [`docs/workflows.md`](docs/workflows.md) for an end-to-end walkthrough: step kinds, transitions, the review contract, `iteration_cap`, model overrides, and agent discovery order.
|
|
104
|
+
|
|
97
105
|
```txt
|
|
98
106
|
/baton:new
|
|
99
107
|
```
|
|
100
108
|
|
|
101
109
|
Pick a name and a scaffold from `default-review-loop` is written to `.pi/baton/workflows/` and opened in editor. The scaffold includes `<your-fast-model>` / `<your-strong-model>` placeholders for step-level model overrides.
|
|
102
110
|
|
|
111
|
+
The shipped `workflows/` directory also includes `two-stage-review-gauntlet.yaml`, a second builtin graph that demonstrates chaining two review gates before completion.
|
|
112
|
+
|
|
103
113
|
### Workflow YAML reference
|
|
104
114
|
|
|
105
115
|
```yaml
|
|
@@ -132,12 +142,12 @@ steps:
|
|
|
132
142
|
|
|
133
143
|
| Path | Purpose |
|
|
134
144
|
|---|---|
|
|
135
|
-
| `extensions/` | Slash-command entrypoints (`/baton:new`, `/baton:start`, `/baton:run`, `/baton:status`) |
|
|
145
|
+
| `extensions/` | Slash-command entrypoints (`/baton:new`, `/baton:start`, `/baton:run`, `/baton:status`, `/baton:history`) |
|
|
136
146
|
| `lib/` | Workflow parser, run engine, subagent runner, review contract, UI widget |
|
|
137
147
|
| `agents/` | Builtin `worker` and `reviewer` subagent definitions |
|
|
138
|
-
| `workflows/` | Builtin `default-review-loop.yaml` |
|
|
148
|
+
| `workflows/` | Builtin workflows (`default-review-loop.yaml`, `two-stage-review-gauntlet.yaml`) |
|
|
139
149
|
| `assets/` | README / package branding assets |
|
|
140
|
-
| `docs/` |
|
|
150
|
+
| `docs/` | Workflow authoring guide, release and maintainer documentation |
|
|
141
151
|
|
|
142
152
|
## Development
|
|
143
153
|
|
|
@@ -171,4 +181,4 @@ For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
|
|
|
171
181
|
|
|
172
182
|
## License
|
|
173
183
|
|
|
174
|
-
MIT
|
|
184
|
+
MIT
|
package/docs/release.md
CHANGED
|
@@ -19,9 +19,9 @@ npm version patch
|
|
|
19
19
|
git push
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
On `main`, `.github/workflows/auto-release.yml` checks `package.json` version. If `v<version>` does not exist yet, it creates the tag, creates the GitHub Release, then explicitly dispatches `.github/workflows/publish.yml` for that tag.
|
|
22
|
+
On `main`, `.github/workflows/auto-release.yml` checks `package.json` version. If `v<version>` does not exist yet, it creates the tag, creates the GitHub Release, then explicitly dispatches `.github/workflows/publish.yml` for that tag. Version bumps on `main` do not trigger `publish.yml` directly; auto-release is the single handoff path for those commits.
|
|
23
23
|
|
|
24
|
-
The `v*.*.*` tag also triggers `.github/workflows/publish.yml`, which runs CI and publishes to npm when tags are pushed manually.
|
|
24
|
+
The `v*.*.*` tag push also triggers `.github/workflows/publish.yml`, which runs CI and publishes to npm when tags are pushed manually.
|
|
25
25
|
Publishing also runs when a GitHub Release is published, and can be run manually from GitHub Actions with `workflow_dispatch`.
|
|
26
26
|
|
|
27
27
|
The workflow skips `name@version` if that exact package version already exists on npm.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Custom workflow authoring
|
|
2
|
+
|
|
3
|
+
This guide walks through authoring a custom Pi Baton workflow YAML. For a compact field reference, see the [Workflow YAML reference](../README.md#workflow-yaml-reference) in the README.
|
|
4
|
+
|
|
5
|
+
## Where workflows live
|
|
6
|
+
|
|
7
|
+
| Location | Source | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `.pi/baton/workflows/*.yaml` | user | Your custom workflows; listed first in `/baton:start` |
|
|
10
|
+
| `workflows/*.yaml` (package) | builtin | Shipped with pi-baton (`default-review-loop`, `two-stage-review-gauntlet`) |
|
|
11
|
+
|
|
12
|
+
Run `/baton:new` to scaffold a new file under `.pi/baton/workflows/`. The scaffold copies the default review loop and adds `<your-fast-model>` / `<your-strong-model>` placeholders on worker and reviewer steps.
|
|
13
|
+
|
|
14
|
+
The **first step key** in `steps:` is the entry step when a run starts.
|
|
15
|
+
|
|
16
|
+
## Step kinds
|
|
17
|
+
|
|
18
|
+
Every step requires `agent` and `prompt`. Pi Baton infers the step kind from its transition fields.
|
|
19
|
+
|
|
20
|
+
### Linear steps
|
|
21
|
+
|
|
22
|
+
Use `next` to move unconditionally to the next step. Linear steps are for implement, fix, draft, or any work that always proceeds forward.
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
implement:
|
|
26
|
+
agent: worker
|
|
27
|
+
prompt: |
|
|
28
|
+
Complete the task brief. End with a JSON summary block.
|
|
29
|
+
next: review
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Review steps
|
|
33
|
+
|
|
34
|
+
Use `on_accept` and `on_reject` instead of `next`. Review steps gate progress on a structured judgment.
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
review:
|
|
38
|
+
agent: reviewer
|
|
39
|
+
prompt: |
|
|
40
|
+
Review the work. Return accept or reject using the JSON contract.
|
|
41
|
+
on_accept: _complete # or another step name
|
|
42
|
+
on_reject: fix
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A step **cannot** mix `next` with `on_accept` / `on_reject`. Each step is either linear or review.
|
|
46
|
+
|
|
47
|
+
## Transitions
|
|
48
|
+
|
|
49
|
+
| Field | Used by | Target |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `next` | linear | Another step name |
|
|
52
|
+
| `on_accept` | review | `_complete` or another step name |
|
|
53
|
+
| `on_reject` | review | Another step name (typically a fix step) |
|
|
54
|
+
|
|
55
|
+
`_complete` is the only special token. It marks a successful terminal state — the run completes when a review step accepts into `_complete`.
|
|
56
|
+
|
|
57
|
+
Transition targets must reference steps defined in the same `steps:` map (except `_complete`).
|
|
58
|
+
|
|
59
|
+
### Choosing `on_accept` vs `next`
|
|
60
|
+
|
|
61
|
+
- **`next`** — the step always hands off to the same successor. Use for deterministic pipelines (`draft → technical_review`).
|
|
62
|
+
- **`on_accept` / `on_reject`** — the successor depends on reviewer judgment. Use when a gate can block or redirect work.
|
|
63
|
+
|
|
64
|
+
### Chaining review gates
|
|
65
|
+
|
|
66
|
+
The builtin `two-stage-review-gauntlet` demonstrates two review steps before completion:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
draft → technical_review ──accept──→ editorial_review ──accept──→ _complete
|
|
70
|
+
│ │
|
|
71
|
+
└──────── reject ──→ fix ──────┘
|
|
72
|
+
│
|
|
73
|
+
└── next → technical_review
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
After a reject, route through a fix step and re-enter the earliest gate that must re-validate the changes.
|
|
77
|
+
|
|
78
|
+
## Review contract
|
|
79
|
+
|
|
80
|
+
Review steps require agents to end with a fenced JSON block. Pi Baton parses `judgment`, and validates the payload before choosing a branch.
|
|
81
|
+
|
|
82
|
+
**Accept** — `judgment` must be `"accept"` and `acceptanceNote` must be non-empty:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{"summary":"Short review summary","judgment":"accept","acceptanceNote":"Why this passes"}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Reject** — `judgment` must be `"reject"` and `findings` must be a non-empty string array:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{"summary":"Short review summary","judgment":"reject","findings":["Actionable issue 1"]}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Non-review steps use `summary` when present and fall back to the output text when it is absent. If parsing fails on a review step, the run fails with a `ReviewContractError`.
|
|
95
|
+
|
|
96
|
+
Builtin `reviewer` agent prompts include the contract. Custom review agents should instruct the model to follow the same shape.
|
|
97
|
+
|
|
98
|
+
## `iteration_cap`
|
|
99
|
+
|
|
100
|
+
`iteration_cap` is a required positive integer. It limits how many times a review step can **reject** before the run fails.
|
|
101
|
+
|
|
102
|
+
- The counter starts at `0` when a run begins.
|
|
103
|
+
- Each review **reject** increments the counter by one.
|
|
104
|
+
- Before executing a review step, if `iteration >= iteration_cap`, the run fails with `Iteration cap (N) reached`.
|
|
105
|
+
- Accepts and linear steps do not increment the counter.
|
|
106
|
+
|
|
107
|
+
Example: `iteration_cap: 5` allows up to five review reject cycles (reviews at iterations 0–4). After the fifth reject raises the counter to 5, the next review attempt is blocked.
|
|
108
|
+
|
|
109
|
+
Set the cap high enough for your loop depth but low enough to prevent runaway reject cycles.
|
|
110
|
+
|
|
111
|
+
## Model overrides
|
|
112
|
+
|
|
113
|
+
Each step may set an optional `model` field (`provider/model-id`, e.g. `openai/gpt-5.4`).
|
|
114
|
+
|
|
115
|
+
| Situation | Model used |
|
|
116
|
+
|---|---|
|
|
117
|
+
| Step defines a concrete `model` | That model |
|
|
118
|
+
| Step omits `model` | Current Pi session model |
|
|
119
|
+
| Scaffold placeholder (`<your-fast-model>`) | Current Pi session model |
|
|
120
|
+
|
|
121
|
+
Convention: use a faster model on worker/linear steps and a stronger model on review steps. `/baton:new` inserts placeholders as a reminder — replace them with real model IDs or remove them to inherit the session model.
|
|
122
|
+
|
|
123
|
+
Step-level overrides apply per subagent invocation; other steps in the same run can use different models.
|
|
124
|
+
|
|
125
|
+
## Agent discovery order
|
|
126
|
+
|
|
127
|
+
Each step's `agent` value must match a Pi subagent `name` from frontmatter. Pi Baton merges agents from three locations; when names collide, **project overrides user overrides builtin**:
|
|
128
|
+
|
|
129
|
+
1. **Project** — nearest `.pi/agents/*.md` walking up from the run's target directory
|
|
130
|
+
2. **User** — `~/.pi/agent/agents/*.md`
|
|
131
|
+
3. **pi-baton builtin** — `agents/` in the package (`worker`, `reviewer`)
|
|
132
|
+
|
|
133
|
+
If a workflow references an agent name that cannot be resolved, validation fails before the run starts.
|
|
134
|
+
|
|
135
|
+
To customize behavior, add `.pi/agents/worker.md` or `.pi/agents/reviewer.md` in your project. Use the same `name` as the workflow references so your file overrides the builtin definition.
|
|
136
|
+
|
|
137
|
+
## End-to-end checklist
|
|
138
|
+
|
|
139
|
+
1. Run `/baton:new` (or copy a builtin workflow into `.pi/baton/workflows/`).
|
|
140
|
+
2. Set `name`, `iteration_cap`, and define `steps:` with the first step as entry.
|
|
141
|
+
3. Assign `agent` values that exist in your agent discovery path.
|
|
142
|
+
4. Add optional `model` overrides on steps that need a different model.
|
|
143
|
+
5. Write prompts that end with the JSON contract (review steps must include judgment rules).
|
|
144
|
+
6. Wire transitions: linear steps use `next`; review steps use `on_accept` / `on_reject`.
|
|
145
|
+
7. Run `/baton:start` to pick the workflow, then `/baton:run` to execute.
|
|
146
|
+
8. Run `/baton:status` to inspect the active run, or the most recent finished run after `/baton:run` completes.
|
|
147
|
+
9. Run `/baton:history` to list recent completed or failed runs when you need older terminal outcomes or run directories. `/baton:status` stays focused on the active run or the latest finished run only.
|
|
148
|
+
|
|
149
|
+
## Examples
|
|
150
|
+
|
|
151
|
+
Study the shipped workflows for complete, working graphs:
|
|
152
|
+
|
|
153
|
+
- [`workflows/default-review-loop.yaml`](../workflows/default-review-loop.yaml) — classic `implement → review → fix` loop
|
|
154
|
+
- [`workflows/two-stage-review-gauntlet.yaml`](../workflows/two-stage-review-gauntlet.yaml) — chained review gates with a shared fix step
|
package/extensions/index.ts
CHANGED
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
createIdleRun,
|
|
11
11
|
loadActiveRun,
|
|
12
12
|
loadMostRecentTerminalRun,
|
|
13
|
+
loadTerminalRunHistory,
|
|
13
14
|
} from "../lib/run-store.ts";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
NO_ACTIVE_RUN_MESSAGE,
|
|
17
|
+
formatHistorySummary,
|
|
18
|
+
formatStatusSummary,
|
|
19
|
+
} from "../lib/status.ts";
|
|
15
20
|
import { createSubagentRunner } from "../lib/subagent-runner.ts";
|
|
16
21
|
import { WorkflowNameCollisionError, createWorkflowScaffold } from "../lib/workflow-scaffold.ts";
|
|
17
22
|
import { WorkflowValidationError } from "../lib/workflow-schema.ts";
|
|
@@ -199,4 +204,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
204
|
ctx.ui.notify(formatStatusSummary(manifest), "info");
|
|
200
205
|
},
|
|
201
206
|
});
|
|
207
|
+
|
|
208
|
+
pi.registerCommand("baton:history", {
|
|
209
|
+
description: "List recent completed or failed Baton runs from persisted history",
|
|
210
|
+
handler: async (_args, ctx) => {
|
|
211
|
+
const history = await loadTerminalRunHistory(ctx.cwd);
|
|
212
|
+
ctx.ui.notify(formatHistorySummary(history), "info");
|
|
213
|
+
},
|
|
214
|
+
});
|
|
202
215
|
}
|
package/lib/run-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
ensureBatonScaffolding,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getActiveRunPointerPath,
|
|
7
7
|
getRunManifestPath,
|
|
8
8
|
getRunStepsDir,
|
|
9
|
+
getRunsDir,
|
|
9
10
|
} from "./paths.ts";
|
|
10
11
|
import type { ActiveRunPointer, RunManifest, RunState, StepRecord } from "./types.ts";
|
|
11
12
|
|
|
@@ -60,6 +61,106 @@ export async function loadActiveRun(cwd: string): Promise<RunManifest | null> {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
export const TERMINAL_HISTORY_DEFAULT_LIMIT = 10;
|
|
65
|
+
export const TERMINAL_HISTORY_HARD_CAP = 20;
|
|
66
|
+
export const TERMINAL_HISTORY_MAX_SCAN = 200;
|
|
67
|
+
|
|
68
|
+
export interface TerminalRunHistoryResult {
|
|
69
|
+
runs: RunManifest[];
|
|
70
|
+
skippedCount: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isCompleteManifest(value: unknown): value is RunManifest {
|
|
74
|
+
if (!value || typeof value !== "object") return false;
|
|
75
|
+
|
|
76
|
+
const manifest = value as Partial<RunManifest>;
|
|
77
|
+
return (
|
|
78
|
+
typeof manifest.id === "string" &&
|
|
79
|
+
typeof manifest.state === "string" &&
|
|
80
|
+
typeof manifest.workflowName === "string" &&
|
|
81
|
+
typeof manifest.updatedAt === "string" &&
|
|
82
|
+
typeof manifest.createdAt === "string" &&
|
|
83
|
+
typeof manifest.iteration === "number" &&
|
|
84
|
+
(manifest.lastStep === null || typeof manifest.lastStep === "string")
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runIdTimestamp(runId: string): string {
|
|
89
|
+
const prefix = runId.slice(0, 14);
|
|
90
|
+
return /^\d{14}$/.test(prefix) ? prefix : "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function compareRunIdsNewestFirst(a: string, b: string): number {
|
|
94
|
+
const aTimestamp = runIdTimestamp(a);
|
|
95
|
+
const bTimestamp = runIdTimestamp(b);
|
|
96
|
+
if (aTimestamp && bTimestamp && aTimestamp !== bTimestamp) {
|
|
97
|
+
return bTimestamp.localeCompare(aTimestamp);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return b.localeCompare(a);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareRunsNewestFirst(a: RunManifest, b: RunManifest): number {
|
|
104
|
+
const updatedCompare = b.updatedAt.localeCompare(a.updatedAt);
|
|
105
|
+
if (updatedCompare !== 0) return updatedCompare;
|
|
106
|
+
|
|
107
|
+
const createdCompare = b.createdAt.localeCompare(a.createdAt);
|
|
108
|
+
if (createdCompare !== 0) return createdCompare;
|
|
109
|
+
|
|
110
|
+
return compareRunIdsNewestFirst(a.id, b.id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function loadTerminalRunHistory(
|
|
114
|
+
cwd: string,
|
|
115
|
+
options: { limit?: number } = {},
|
|
116
|
+
): Promise<TerminalRunHistoryResult> {
|
|
117
|
+
await ensureBatonScaffolding(cwd);
|
|
118
|
+
|
|
119
|
+
const limit = Math.min(
|
|
120
|
+
Math.max(1, options.limit ?? TERMINAL_HISTORY_DEFAULT_LIMIT),
|
|
121
|
+
TERMINAL_HISTORY_HARD_CAP,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let entries;
|
|
125
|
+
try {
|
|
126
|
+
entries = await readdir(getRunsDir(cwd), { withFileTypes: true });
|
|
127
|
+
} catch {
|
|
128
|
+
return { runs: [], skippedCount: 0 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const runIds = entries
|
|
132
|
+
.filter((entry) => entry.isDirectory())
|
|
133
|
+
.map((entry) => entry.name)
|
|
134
|
+
.sort(compareRunIdsNewestFirst)
|
|
135
|
+
.slice(0, TERMINAL_HISTORY_MAX_SCAN);
|
|
136
|
+
|
|
137
|
+
const terminalRuns: RunManifest[] = [];
|
|
138
|
+
let skippedCount = 0;
|
|
139
|
+
|
|
140
|
+
for (const runId of runIds) {
|
|
141
|
+
try {
|
|
142
|
+
const manifest = await readJson<unknown>(getRunManifestPath(cwd, runId));
|
|
143
|
+
if (!isCompleteManifest(manifest)) {
|
|
144
|
+
skippedCount++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!isTerminalRunState(manifest.state)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
terminalRuns.push(manifest);
|
|
151
|
+
} catch {
|
|
152
|
+
skippedCount++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
terminalRuns.sort(compareRunsNewestFirst);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
runs: terminalRuns.slice(0, limit),
|
|
160
|
+
skippedCount,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
63
164
|
export async function loadMostRecentTerminalRun(cwd: string): Promise<RunManifest | null> {
|
|
64
165
|
const pointer = await readActiveRunPointer(cwd);
|
|
65
166
|
if (!pointer?.runId) return null;
|
package/lib/status.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TerminalRunHistoryResult } from "./run-store.ts";
|
|
1
2
|
import type { RunManifest } from "./types.ts";
|
|
2
3
|
|
|
3
4
|
function isTerminalRunState(state: RunManifest["state"]): boolean {
|
|
@@ -21,8 +22,50 @@ export function formatStatusSummary(manifest: RunManifest): string {
|
|
|
21
22
|
`run directory: .pi/baton/runs/${manifest.id}`,
|
|
22
23
|
);
|
|
23
24
|
|
|
25
|
+
if (manifest.failureReason) {
|
|
26
|
+
lines.push(`failure: ${manifest.failureReason}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
return lines.join("\n");
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export const NO_ACTIVE_RUN_MESSAGE =
|
|
28
33
|
"No active Baton run. Start one with /baton:start after choosing a workflow and task brief.";
|
|
34
|
+
|
|
35
|
+
export const NO_TERMINAL_HISTORY_MESSAGE =
|
|
36
|
+
"No completed or failed Baton runs yet. Start one with /baton:start, then /baton:run.";
|
|
37
|
+
|
|
38
|
+
function formatRelativeRunDirectory(runId: string): string {
|
|
39
|
+
return `.pi/baton/runs/${runId}`.replace(/\\/g, "/");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatHistoryRow(manifest: RunManifest): string {
|
|
43
|
+
const lastStep = manifest.lastStep ?? "(none)";
|
|
44
|
+
return [
|
|
45
|
+
manifest.state,
|
|
46
|
+
manifest.id,
|
|
47
|
+
manifest.workflowName,
|
|
48
|
+
`last: ${lastStep}`,
|
|
49
|
+
`iter: ${manifest.iteration}`,
|
|
50
|
+
`updated: ${manifest.updatedAt}`,
|
|
51
|
+
formatRelativeRunDirectory(manifest.id),
|
|
52
|
+
].join(" | ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatHistorySummary(result: TerminalRunHistoryResult): string {
|
|
56
|
+
const lines: string[] = [];
|
|
57
|
+
|
|
58
|
+
if (result.runs.length === 0) {
|
|
59
|
+
lines.push(NO_TERMINAL_HISTORY_MESSAGE);
|
|
60
|
+
} else {
|
|
61
|
+
for (const manifest of result.runs) {
|
|
62
|
+
lines.push(formatHistoryRow(manifest));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (result.skippedCount > 0) {
|
|
67
|
+
lines.push(`(${result.skippedCount} run(s) skipped: unreadable or incomplete manifest)`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-baton",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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,10 +58,10 @@
|
|
|
58
58
|
"typebox": "*"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@earendil-works/pi-ai": "^0.
|
|
62
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
61
|
+
"@earendil-works/pi-ai": "^0.84.1",
|
|
62
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
63
63
|
"typebox": "latest",
|
|
64
64
|
"@types/node": "^26.0.1",
|
|
65
|
-
"typescript": "^
|
|
65
|
+
"typescript": "^7.0.2"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Two-Stage Review Gauntlet
|
|
2
|
+
iteration_cap: 4
|
|
3
|
+
steps:
|
|
4
|
+
draft:
|
|
5
|
+
agent: worker
|
|
6
|
+
prompt: |
|
|
7
|
+
You are the draft step in a Pi Baton two-stage review gauntlet.
|
|
8
|
+
|
|
9
|
+
Produce a complete first-pass solution for the task brief. Keep the work focused
|
|
10
|
+
and prepare it for technical and editorial review gates.
|
|
11
|
+
|
|
12
|
+
End your response with a fenced JSON block:
|
|
13
|
+
```json
|
|
14
|
+
{"summary":"One paragraph summary of the draft you produced"}
|
|
15
|
+
```
|
|
16
|
+
next: technical_review
|
|
17
|
+
technical_review:
|
|
18
|
+
agent: reviewer
|
|
19
|
+
prompt: |
|
|
20
|
+
You are the technical review gate in a Pi Baton two-stage review gauntlet.
|
|
21
|
+
|
|
22
|
+
Review the draft for correctness, completeness, tests, regressions, and implementation risk.
|
|
23
|
+
Accept only when the work is technically ready to proceed to editorial review.
|
|
24
|
+
|
|
25
|
+
End your response with a fenced JSON block:
|
|
26
|
+
```json
|
|
27
|
+
{"summary":"Short technical review summary","judgment":"accept","acceptanceNote":"Why this is technically ready"}
|
|
28
|
+
```
|
|
29
|
+
or
|
|
30
|
+
```json
|
|
31
|
+
{"summary":"Short technical review summary","judgment":"reject","findings":["Actionable technical issue 1"]}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- judgment must be "accept" or "reject"
|
|
36
|
+
- reject requires non-empty findings
|
|
37
|
+
- accept requires a non-empty acceptanceNote
|
|
38
|
+
on_accept: editorial_review
|
|
39
|
+
on_reject: fix
|
|
40
|
+
editorial_review:
|
|
41
|
+
agent: reviewer
|
|
42
|
+
prompt: |
|
|
43
|
+
You are the editorial review gate in a Pi Baton two-stage review gauntlet.
|
|
44
|
+
|
|
45
|
+
Review the technically accepted work for clarity, maintainability, user-facing wording,
|
|
46
|
+
and handoff quality. Accept only when the final result is ready to deliver.
|
|
47
|
+
|
|
48
|
+
End your response with a fenced JSON block:
|
|
49
|
+
```json
|
|
50
|
+
{"summary":"Short editorial review summary","judgment":"accept","acceptanceNote":"Why this is ready to deliver"}
|
|
51
|
+
```
|
|
52
|
+
or
|
|
53
|
+
```json
|
|
54
|
+
{"summary":"Short editorial review summary","judgment":"reject","findings":["Actionable editorial issue 1"]}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Rules:
|
|
58
|
+
- judgment must be "accept" or "reject"
|
|
59
|
+
- reject requires non-empty findings
|
|
60
|
+
- accept requires a non-empty acceptanceNote
|
|
61
|
+
on_accept: _complete
|
|
62
|
+
on_reject: fix
|
|
63
|
+
fix:
|
|
64
|
+
agent: worker
|
|
65
|
+
prompt: |
|
|
66
|
+
You are the fix step in a Pi Baton two-stage review gauntlet.
|
|
67
|
+
|
|
68
|
+
Address the latest review findings while preserving good work from earlier steps.
|
|
69
|
+
After fixing, the work returns through the technical gate before editorial review.
|
|
70
|
+
|
|
71
|
+
End your response with a fenced JSON block:
|
|
72
|
+
```json
|
|
73
|
+
{"summary":"One paragraph summary of fixes applied"}
|
|
74
|
+
```
|
|
75
|
+
next: technical_review
|