@stacknyu/pi-flow 0.0.0-stage → 1.5.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/setup.mjs ADDED
@@ -0,0 +1,310 @@
1
+ // pi-flow setup: detect the repo's stack, verify check commands by running them once,
2
+ // apply your saved plugin profile, and keep a short managed block in AGENTS.md.
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, appendFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join, relative } from "node:path";
5
+ import { LOGIN_SHELL, cleanEnv, git, loadJson, resolvePi, run, tail } from "./runner.mjs";
6
+
7
+ export const FLOW_DIR = ".pi-flow";
8
+ export const TASKS_DIR = ".tasks";
9
+ const AGENT_DIR = process.env.PI_CODING_AGENT_DIR || join(process.env.HOME || "", ".pi", "agent");
10
+ export const PROFILE = join(AGENT_DIR, "pi-flow", "profile.json");
11
+ const SKIP = new Set(["node_modules", ".git", ".pi-queue", ".pi-flow", "dist", "build", ".next", ".turbo", "vendor", "target"]);
12
+
13
+ /** Keep a path out of git without touching .gitignore (uses .git/info/exclude). */
14
+ export async function exclude(repo, path) {
15
+ const rel = (await git(repo, "rev-parse", "--git-path", "info/exclude")).out.trim();
16
+ if (!rel) return;
17
+ const file = isAbsolute(rel) ? rel : join(repo, rel);
18
+ mkdirSync(dirname(file), { recursive: true });
19
+ const cur = existsSync(file) ? readFileSync(file, "utf8") : "";
20
+ if (!cur.split("\n").includes(path)) appendFileSync(file, `${cur && !cur.endsWith("\n") ? "\n" : ""}${path}\n`);
21
+ }
22
+
23
+ // ---------- stack detection ----------
24
+
25
+ function readJson(p) {
26
+ try {
27
+ return JSON.parse(readFileSync(p, "utf8"));
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ function dirsMatching(repo, pattern) {
34
+ const clean = pattern.replace(/^["']|["']$/g, "").replace(/^\.\//, "").replace(/\/$/, "");
35
+ if (!clean || clean.startsWith("!")) return [];
36
+ const parts = clean.split("/");
37
+ const out = [];
38
+ const walk = (base, i) => {
39
+ if (i === parts.length) return out.push(base);
40
+ const part = parts[i];
41
+ const abs = join(repo, base);
42
+ if (part === "**") {
43
+ walk(base, i + 1);
44
+ for (const d of subdirs(abs)) walk(join(base, d), i);
45
+ return;
46
+ }
47
+ if (part.includes("*")) {
48
+ const re = new RegExp(`^${part.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
49
+ for (const d of subdirs(abs)) if (re.test(d)) walk(join(base, d), i + 1);
50
+ return;
51
+ }
52
+ if (existsSync(join(abs, part))) walk(join(base, part), i + 1);
53
+ };
54
+ walk("", 0);
55
+ return out.filter((d) => existsSync(join(repo, d, "package.json")));
56
+ }
57
+
58
+ function subdirs(abs) {
59
+ try {
60
+ return readdirSync(abs).filter((d) => !SKIP.has(d) && !d.startsWith(".") && statSync(join(abs, d)).isDirectory());
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ function findFiles(repo, name, depth = 3, base = "") {
67
+ const out = [];
68
+ if (existsSync(join(repo, base, name))) out.push(base || ".");
69
+ if (depth === 0) return out;
70
+ for (const d of subdirs(join(repo, base))) out.push(...findFiles(repo, name, depth - 1, join(base, d)));
71
+ return out;
72
+ }
73
+
74
+ export function detect(repo) {
75
+ const has = (f) => existsSync(join(repo, f));
76
+ const pm = has("pnpm-lock.yaml") ? "pnpm" : has("bun.lock") || has("bun.lockb") ? "bun" : has("yarn.lock") ? "yarn" : has("package-lock.json") ? "npm" : has("package.json") ? "npm" : "";
77
+ const install = {
78
+ pnpm: "pnpm install --frozen-lockfile --prefer-offline",
79
+ bun: "bun install --frozen-lockfile",
80
+ yarn: "yarn install --frozen-lockfile",
81
+ npm: "npm ci --prefer-offline --no-audit --no-fund",
82
+ }[pm] ?? "";
83
+
84
+ // workspace packages
85
+ let patterns = [];
86
+ if (has("pnpm-workspace.yaml")) {
87
+ let inPackages = false;
88
+ for (const line of readFileSync(join(repo, "pnpm-workspace.yaml"), "utf8").split("\n")) {
89
+ if (/^packages\s*:/.test(line)) inPackages = true;
90
+ else if (/^\S/.test(line)) inPackages = false;
91
+ else if (inPackages) {
92
+ const m = line.match(/^\s*-\s*(.+?)\s*$/);
93
+ if (m) patterns.push(m[1]);
94
+ }
95
+ }
96
+ }
97
+ const rootPkg = readJson(join(repo, "package.json"));
98
+ const ws = rootPkg?.workspaces;
99
+ if (Array.isArray(ws)) patterns.push(...ws);
100
+ else if (Array.isArray(ws?.packages)) patterns.push(...ws.packages);
101
+ const dirs = [...new Set(patterns.flatMap((p) => dirsMatching(repo, p)))].sort();
102
+ if (dirs.length === 0 && rootPkg) dirs.push(".");
103
+
104
+ const packages = dirs.map((dir) => {
105
+ const pkg = readJson(join(repo, dir, "package.json")) ?? {};
106
+ return { dir, name: pkg.name ?? dir, scripts: pkg.scripts ?? {}, tsconfig: existsSync(join(repo, dir, "tsconfig.json")) };
107
+ });
108
+ const go = findFiles(repo, "go.mod");
109
+ const cargo = findFiles(repo, "Cargo.toml", 2).filter((d) => d === "." || !existsSync(join(repo, "Cargo.toml")));
110
+ return { pm, install, packages, go, cargo };
111
+ }
112
+
113
+ /** Check commands worth trying, per package. Tests are listed but not run (they can be slow or need services). */
114
+ export function candidates(repo, det) {
115
+ const runS = { pnpm: "pnpm run", bun: "bun run", yarn: "yarn run", npm: "npm run" }[det.pm] ?? "npm run";
116
+ const execTsc = { pnpm: "pnpm exec tsc --noEmit", bun: "bunx tsc --noEmit", yarn: "yarn tsc --noEmit", npm: "npx --no-install tsc --noEmit" }[det.pm] ?? "npx --no-install tsc --noEmit";
117
+ const at = (dir, cmd) => (dir === "." ? cmd : `cd ${dir} && ${cmd}`);
118
+ const out = [];
119
+ const tscAvailable = (dir) => existsSync(join(repo, dir, "node_modules", ".bin", "tsc")) || existsSync(join(repo, "node_modules", ".bin", "tsc"));
120
+ for (const p of det.packages) {
121
+ const s = p.scripts;
122
+ const tc = ["typecheck", "type-check", "check-types", "types", "tsc"].find((k) => s[k]);
123
+ if (tc) out.push({ dir: p.dir, kind: "typecheck", cmd: at(p.dir, `${runS} ${tc}`) });
124
+ else if (p.tsconfig && tscAvailable(p.dir)) out.push({ dir: p.dir, kind: "typecheck", cmd: at(p.dir, execTsc) });
125
+ if (s.lint) out.push({ dir: p.dir, kind: "lint", cmd: at(p.dir, `${runS} lint`) });
126
+ if (s.check && !tc) out.push({ dir: p.dir, kind: "check", cmd: at(p.dir, `${runS} check`) });
127
+ if (s.test) out.push({ dir: p.dir, kind: "test", cmd: at(p.dir, `${runS} test`), notRun: true });
128
+ }
129
+ for (const d of det.go) {
130
+ out.push({ dir: d, kind: "build", cmd: at(d, "go build ./...") });
131
+ out.push({ dir: d, kind: "vet", cmd: at(d, "go vet ./...") });
132
+ out.push({ dir: d, kind: "test", cmd: at(d, "go test ./..."), notRun: true });
133
+ }
134
+ for (const d of det.cargo) out.push({ dir: d, kind: "build", cmd: at(d, "cargo check") });
135
+ return out;
136
+ }
137
+
138
+ /** Run each candidate once on the current code. Only commands that pass can become a task's checks. */
139
+ export async function verify(repo, cands, onProgress = () => {}, minutes = 10) {
140
+ const results = [];
141
+ let i = 0;
142
+ for (const c of cands) {
143
+ i++;
144
+ if (c.notRun) {
145
+ results.push({ ...c, ok: null });
146
+ continue;
147
+ }
148
+ onProgress(`checking ${i}/${cands.length}: ${c.cmd}`);
149
+ const t0 = Date.now();
150
+ const r = await run(repo, LOGIN_SHELL, ["-lc", c.cmd], { timeoutMs: minutes * 60_000, env: cleanEnv() });
151
+ results.push({ ...c, ok: r.code === 0, seconds: Math.round((Date.now() - t0) / 1000), ...(r.code === 0 ? {} : { output: tail(r.out, 15) }) });
152
+ }
153
+ return results;
154
+ }
155
+
156
+ // ---------- AGENTS.md ----------
157
+
158
+ const START = "<!-- pi-flow:start -->";
159
+ const END = "<!-- pi-flow:end -->";
160
+ export const AGENTS_BLOCK = `${START}
161
+ ## How large work runs here (pi-flow)
162
+
163
+ - Large tasks start with \`/task <request>\`: a planning session researches and writes \`${TASKS_DIR}/<task>/\` (brief, inventory, maps, rules, questions, items), asks the user its open questions, then hands the item list to the queue.
164
+ - Queue items run unattended, each in a fresh session: never ask the user questions; stay inside the item and the task's allowed paths; do not commit; run the task's checks before stopping. Reply \`NEEDS: <files> files, <lines> lines\` if the item needs more than its size limits, or \`BLOCKED: <reason>\` if it cannot be done without a decision.
165
+ - Subagents may read, search and research; only the main agent edits files.
166
+ - A task's \`brief.md\`, \`rules.md\` and answered \`questions.md\` narrow the scope for that task; they never loosen the rules in this file.
167
+ ${END}`;
168
+
169
+ /** Insert or refresh the managed block; drop stale harness blocks from earlier setups. Returns notes. */
170
+ export function updateAgents(repo) {
171
+ const file = join(repo, "AGENTS.md");
172
+ const notes = [];
173
+ let text = existsSync(file) ? readFileSync(file, "utf8") : "# Agent rules\n";
174
+ const stale = /<!-- harness:long-running:start -->[\s\S]*?<!-- harness:long-running:end -->\n?/g;
175
+ if (stale.test(text)) {
176
+ text = text.replace(stale, "");
177
+ notes.push("removed the old long-running harness block (GLLA) from AGENTS.md");
178
+ }
179
+ if (text.includes(START) && text.includes(END)) {
180
+ const before = text;
181
+ text = text.replace(new RegExp(`${START}[\\s\\S]*?${END}`), AGENTS_BLOCK);
182
+ if (before !== text) notes.push("refreshed the pi-flow block in AGENTS.md");
183
+ } else {
184
+ text = `${text.trimEnd()}\n\n${AGENTS_BLOCK}\n`;
185
+ notes.push(existsSync(file) ? "added the pi-flow block to AGENTS.md" : "created AGENTS.md with the pi-flow block");
186
+ }
187
+ const leftovers = text.split("\n").filter((l) => /\bGLLA\b|\/goal\b|pi-goal/i.test(l) && !l.includes("pi-flow"));
188
+ if (leftovers.length) notes.push(`AGENTS.md still mentions the old loop in ${leftovers.length} line(s), e.g. "${leftovers[0].trim().slice(0, 80)}" — review it`);
189
+ writeFileSync(file, text);
190
+ return notes;
191
+ }
192
+
193
+ // ---------- plugin profile ----------
194
+
195
+ function packagesOf(settingsFile) {
196
+ return (readJson(settingsFile)?.packages ?? []).map((p) => (typeof p === "string" ? p : p?.source)).filter(Boolean);
197
+ }
198
+
199
+ /** Save this repo's project plugins (.pi/settings.json) as your profile for other repos. */
200
+ export function saveProfile(repo) {
201
+ const settings = readJson(join(repo, ".pi", "settings.json")) ?? {};
202
+ const packages = (settings.packages ?? []).filter((p) => {
203
+ const src = typeof p === "string" ? p : p?.source;
204
+ return src && !/^\.{1,2}\//.test(src); // repo-relative local packages cannot move to another repo
205
+ });
206
+ const { packages: _p, ...rest } = settings;
207
+ mkdirSync(dirname(PROFILE), { recursive: true });
208
+ writeFileSync(PROFILE, `${JSON.stringify({ savedFrom: repo, savedAt: new Date().toISOString(), packages, settings: rest }, null, 2)}\n`);
209
+ return { file: PROFILE, count: packages.length };
210
+ }
211
+
212
+ /** Install the profile's plugins into this repo (pi install -l) and copy its other project settings where unset. */
213
+ export async function applyProfile(repo, onProgress = () => {}) {
214
+ const profile = loadJson(PROFILE, null);
215
+ if (!profile) return { notes: ["no plugin profile yet: run /task save-profile in a repo whose plugins you like"], installed: [] };
216
+ const settingsFile = join(repo, ".pi", "settings.json");
217
+ const have = new Set([...packagesOf(settingsFile), ...packagesOf(join(AGENT_DIR, "settings.json"))]);
218
+ const pi = await resolvePi({});
219
+ const installed = [];
220
+ const notes = [];
221
+ for (const p of profile.packages ?? []) {
222
+ const src = typeof p === "string" ? p : p.source;
223
+ if (have.has(src)) continue;
224
+ onProgress(`installing ${src}`);
225
+ const r = await run(repo, LOGIN_SHELL, ["-lc", `"${pi}" install -l "$0"`, src], { timeoutMs: 10 * 60_000, env: cleanEnv() });
226
+ if (r.code === 0) installed.push(src);
227
+ else notes.push(`could not install ${src}: ${tail(r.out, 2).replace(/\n/g, " ")}`);
228
+ }
229
+ // object-form package filters and other project settings: merge where this repo has nothing
230
+ const cur = readJson(settingsFile) ?? {};
231
+ let changed = false;
232
+ for (const [k, v] of Object.entries(profile.settings ?? {})) {
233
+ if (cur[k] === undefined) {
234
+ cur[k] = v;
235
+ changed = true;
236
+ }
237
+ }
238
+ const objs = (profile.packages ?? []).filter((p) => typeof p === "object");
239
+ if (objs.length) {
240
+ cur.packages = (cur.packages ?? []).map((p) => {
241
+ const src = typeof p === "string" ? p : p.source;
242
+ return objs.find((o) => o.source === src) ?? p;
243
+ });
244
+ changed = true;
245
+ }
246
+ if (changed) {
247
+ mkdirSync(dirname(settingsFile), { recursive: true });
248
+ writeFileSync(settingsFile, `${JSON.stringify(cur, null, 2)}\n`);
249
+ }
250
+ return { notes, installed };
251
+ }
252
+
253
+ // ---------- the whole setup ----------
254
+
255
+ export async function setupRepo(repo, onProgress = () => {}) {
256
+ const notes = [];
257
+ if ((await git(repo, "rev-parse", "--is-inside-work-tree")).out.trim() !== "true") throw new Error("not a git repository");
258
+ mkdirSync(join(repo, FLOW_DIR), { recursive: true });
259
+ await exclude(repo, `${FLOW_DIR}/`);
260
+ await exclude(repo, ".pi-queue/");
261
+
262
+ const dirtyBefore = (await git(repo, "status", "--porcelain", "--", "AGENTS.md", ".pi/settings.json")).out.trim();
263
+
264
+ onProgress("plugins");
265
+ const prof = await applyProfile(repo, onProgress);
266
+ notes.push(...prof.notes);
267
+ if (prof.installed.length) notes.push(`installed ${prof.installed.length} plugin(s): ${prof.installed.join(", ")}`);
268
+
269
+ notes.push(...updateAgents(repo));
270
+
271
+ onProgress("detecting the stack");
272
+ const det = detect(repo);
273
+ const cands = candidates(repo, det);
274
+ const checks = await verify(repo, cands, onProgress);
275
+ const info = { updatedAt: new Date().toISOString(), pm: det.pm, install: det.install, packages: det.packages.map(({ dir, name }) => ({ dir, name })), go: det.go, cargo: det.cargo, checks };
276
+ writeFileSync(join(repo, FLOW_DIR, "repo.json"), `${JSON.stringify(info, null, 2)}\n`);
277
+
278
+ // commit setup's own files, only if they were clean before (never sweep up your edits)
279
+ if (!dirtyBefore) {
280
+ const changed = (await git(repo, "status", "--porcelain", "--", "AGENTS.md", ".pi/settings.json")).out.trim();
281
+ if (changed) {
282
+ await git(repo, "add", "--", "AGENTS.md", ...(existsSync(join(repo, ".pi", "settings.json")) ? [".pi/settings.json"] : []));
283
+ const c = await git(repo, "commit", "-m", "pi-flow setup", "--", "AGENTS.md", ...(existsSync(join(repo, ".pi", "settings.json")) ? [".pi/settings.json"] : []));
284
+ notes.push(c.code === 0 ? "committed AGENTS.md / .pi/settings.json as \"pi-flow setup\"" : `could not commit setup changes: ${tail(c.out, 2)}`);
285
+ }
286
+ } else notes.push("AGENTS.md or .pi/settings.json had your own uncommitted edits, so setup did not commit; commit them yourself before /task");
287
+
288
+ const ok = checks.filter((c) => c.ok === true);
289
+ const bad = checks.filter((c) => c.ok === false);
290
+ const report = [
291
+ `# pi-flow setup — ${info.updatedAt}`,
292
+ "",
293
+ `Package manager: ${det.pm || "none"}${det.install ? ` (worktree install: \`${det.install}\`)` : ""}`,
294
+ `Packages: ${det.packages.map((p) => p.dir).join(", ") || "none"}${det.go.length ? ` · Go: ${det.go.join(", ")}` : ""}${det.cargo.length ? ` · Rust: ${det.cargo.join(", ")}` : ""}`,
295
+ "",
296
+ "## Checks that pass today (usable as task checks)",
297
+ ...ok.map((c) => `- \`${c.cmd}\` (${c.kind}, ${c.seconds}s)`),
298
+ "",
299
+ "## Checks that fail today (not used until fixed)",
300
+ ...bad.map((c) => `- \`${c.cmd}\` (${c.kind})\n\n \`\`\`\n${(c.output ?? "").split("\n").map((l) => ` ${l}`).join("\n")}\n \`\`\``),
301
+ "",
302
+ "## Available but not run (tests)",
303
+ ...checks.filter((c) => c.ok === null).map((c) => `- \`${c.cmd}\``),
304
+ "",
305
+ "## Notes",
306
+ ...notes.map((n) => `- ${n}`),
307
+ ].join("\n");
308
+ writeFileSync(join(repo, FLOW_DIR, "setup-report.md"), `${report}\n`);
309
+ return { info, notes, ok: ok.length, bad: bad.length, report };
310
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: pi-flow
3
+ description: How pi-flow plans and runs large tasks with a queue of fresh Pi sessions (/task, /task manage, /queue, items.md format, [check]/[areas]/[after] tags, limits, re-queuing, cleanup, troubleshooting). Use when planning a /task, managing or explaining the queue, answering how pi-flow works, or diagnosing a queue problem.
4
+ ---
5
+
6
+ # pi-flow
7
+
8
+ pi-flow turns a large request into a planned work list that a queue of fresh, unattended Pi sessions implements item by item. A script judges every item; each passing item becomes one commit.
9
+
10
+ Read the reference that fits the question:
11
+
12
+ - `references/guide.md`: the whole workflow (setup → plan → questions → handoff → queue → cleanup), the terminal app versus Pi Web, and the files involved. Start here when explaining pi-flow.
13
+ - `references/items.md`: the items.md line format, every tag (`[areas]`, `[after]`, `[check]`, `[big]`, size tags), the size limits and how they rise, splitting, gap fixing, deferral, rechecks. Read it before writing or changing a plan.
14
+ - `references/manager.md`: what the manager chat does, its tools and actions (retry, recheck, rerun, unpark, next, add, parallel, stop, start), how to diagnose a run, and the rules it follows. Read it when acting as the manager.
15
+ - `references/troubleshooting.md`: known problems and their fixes (failed baseline, dirty tree, items blocked by missing prerequisites, parked items running one by one, stray Pi Web projects, and more). Read it when something went wrong.
16
+
17
+ Rules that always hold:
18
+ - The runner, not an agent, decides whether an item passes: scope (allowed paths), size limits, checks.
19
+ - Queue items never ask questions, never commit, and never use the queue tools.
20
+ - The manual manager acts only when asked. The isolated automatic manager may propose bounded recovery of approved existing work; see references/manager.md. Neither manager edits code, reverts commits or deletes branches.
@@ -0,0 +1,80 @@
1
+ # pi-flow guide
2
+
3
+ ## The idea
4
+
5
+ Large work (hours) fails in one long agent session: context fills up, the agent drifts, and nothing checks its claims. pi-flow splits the work into a written plan and a list of small items. A queue gives every item a fresh session and has a script judge the result. You answer the plan's questions once, then watch or walk away.
6
+
7
+ ## One-time install
8
+
9
+ - `pi install npm:@stacknyu/pi-flow` installs pi-flow personally for every repo. Optionally install the independent check plugin with `pi install npm:@stacknyu/pi-check`. Reload Pi / Pi Web.
10
+ - In a repo whose plugins you like: `/task save-profile`. That set becomes your profile.
11
+
12
+ ## Per repo, once: `/task setup`
13
+
14
+ - Installs your profile's plugins into the repo (`pi install -l`).
15
+ - Detects the stack: pnpm/npm/yarn/bun workspaces, Go modules, Rust.
16
+ - Runs every typecheck, lint and build command **once on today's code**. Only commands that pass can become a task's checks. Failing ones are listed with their output in `.pi-flow/setup-report.md`.
17
+ - Adds a short managed block to `AGENTS.md` and removes stale harness blocks (old GLLA). It commits only `AGENTS.md` and `.pi/settings.json`, and only if you had no edits there.
18
+
19
+ ## Per task
20
+
21
+ 1. **`/task <request>`.** This chat becomes the planning session. It researches the code (subagents may search in parallel, read-only) and writes `.tasks/<slug>/`:
22
+
23
+ | File | Content |
24
+ |---|---|
25
+ | `brief.md` | goal, what "done" means, out of scope, risks |
26
+ | `inventory.md` | facts from the code, with paths |
27
+ | `map/<area>.md` | what changes per area, and what must not |
28
+ | `rules.md` | task-specific rules |
29
+ | `questions.md` | decisions only you can make |
30
+ | `items.md` | the work list (see items.md) |
31
+ | `task.json` | `allowedPaths`, `checks`, suggested `parallel` |
32
+
33
+ 2. **Questions.** It asks them all in one message, each with a recommended answer. Reply in plain words.
34
+ 3. **Handoff.** The planner calls `task_handoff`. It refuses until:
35
+ - every question is answered
36
+ - every item has an id, a "Done when" and `[areas]`
37
+ - every `[after]` id exists and dependencies are acyclic
38
+ - the checks pass on today's code
39
+ - the tree is clean apart from the plan
40
+
41
+ Then it commits `.tasks/<slug>/` and starts the queue. In Pi Web it pins this chat as the control room.
42
+ 4. **The queue runs.** Up to `parallel` items at once, each in its own git worktree (`.pi-queue/worktrees/wN`) and its own fresh session. Each passing item is rebased onto the branch and fast-forwarded: one commit `ID: title`.
43
+ 5. **Automatic supervision.** Once the baseline passes, a detached supervisor creates a chat-only manager. Healthy 60-second ticks make no model calls. Bounded recovery and human-only blockers are defined in manager.md. `/task manage` and `/queue …` remain available for manual intervention.
44
+ 6. **Clean up.** Finished item sessions are removed when the run ends. `/task cleanup` removes failed items' sessions and the planning chat. `--all` also removes worktrees and `queue-failed/*` branches.
45
+
46
+ ## Terminal app versus Pi Web
47
+
48
+ Everything works in the plain terminal app. Pi Web is optional and found automatically.
49
+
50
+ | | Terminal | Pi Web |
51
+ |---|---|---|
52
+ | Items run as | background `pi --mode json` processes | real Pi Web sessions, live in the sidebar |
53
+ | Watch an item | `/queue log <id>`, the live panel | open `queue <id>: …` in the sidebar |
54
+ | Open a finished item | `/queue open <id>` (switches this chat to it) | sidebar |
55
+ | Sessions stored | `.pi-queue/sessions/` | Pi Web's store |
56
+ | Cleanup | deletes the files | Pi Web API (delete or archive) |
57
+ | Planning chat | — | pinned while the queue runs |
58
+
59
+ ## Files
60
+
61
+ | Path | In git | What |
62
+ |---|---|---|
63
+ | `.tasks/<slug>/` | yes | the plan, which is what every item reads |
64
+ | `.pi-flow/` | no | `repo.json` (verified checks), `setup-report.md`, `status.json` |
65
+ | `.pi-queue/config.json` | no | queue settings (your edits survive across tasks) |
66
+ | `.pi-queue/log.md` | no | one line per event: PASS, FAIL, SPLIT, GAPS, DEFERRED, RECHECK, REDO, WIDTH, CONTROL, END |
67
+ | `.pi-queue/state.json` | no | done, failed, parked, splits, fixes, sessions; a restart continues from here |
68
+ | `.pi-queue/manager.json` | no | manager session, approval identity, incident and recovery budgets |
69
+ | `.pi-queue/control/` | no | atomic control inbox; one file per request |
70
+ | `.pi-queue/notes/<id>.md` | no | each item's result and the agent's last reply |
71
+ | `.pi-queue/live/<id>.log` | no | each item's activity |
72
+ | `.pi-queue/archive/` | no | earlier runs' records |
73
+
74
+ ## While it runs
75
+
76
+ - `/queue pause` stops new dispatch and lets running items finish normally. `/queue start` resumes. `/queue stop` still aborts running items and preserves their work; use pause for upgrades.
77
+ - Agent parallelism and pi-check's CPU/job budgets are independent. Planning and manager controls accept more than eight agents; choose based on independent work and available memory/provider capacity, not test-process count.
78
+ - Don't commit to the task's branch or edit files in the repo folder. The queue fast-forwards that branch.
79
+ - Keep the machine awake (`caffeinate -dims` on a Mac). In Pi Web mode, keep the Pi Web server running.
80
+ - One queue per repo at a time. You can discuss the next task meanwhile, but keep draft files outside the active checkout. Its handoff must wait until the queue is free.
@@ -0,0 +1,68 @@
1
+ # Work items
2
+
3
+ ## Line format
4
+
5
+ ```
6
+ - [ ] A01 — <what to do, naming files/functions>. Done when: <observable result>. [areas: <folders/files it changes>] [after: <ids>]
7
+ ```
8
+
9
+ - **Id:** capital letters plus a number (`A01`, `P03`, `V04`), unique. File order breaks ties, not dependencies. Ids made by the queue: `A01a…` (split parts), `V01-fix1…` (gap fixes).
10
+ - **Done when:** what a reviewer can verify. Every item needs one.
11
+ - **Context:** each item runs in a fresh session that has never seen the planning chat. It reads `AGENTS.md`, `brief.md`, `rules.md`, the answers in `questions.md`, and the map and inventory parts it names. Anything it needs must be there or in its line.
12
+
13
+ ## Tags
14
+
15
+ | Tag | Meaning |
16
+ |---|---|
17
+ | `[areas: a, b]` | folders/files the item changes. Items with overlapping areas never run at the same time; disjoint ones run in parallel. Required. |
18
+ | `[after: A01, A02]` | wait until these pass. If one fails, this item fails without running. `[after: *]` waits for everything else. |
19
+ | `[check]` | a verification item (see below) |
20
+ | `[big]` | start at the `big` limits (15 files / 600 lines / 90 min) |
21
+ | `[files=12 lines=400 min=60]` | start at these limits |
22
+ | `[model: provider/model-id]` | exact model selected during planning; otherwise queue default |
23
+ | `[thinking: medium]` | supported thinking level selected during planning; otherwise queue default |
24
+
25
+ ## Plan session choices upfront
26
+
27
+ The planner receives the authenticated model catalog, supported thinking levels and advertised costs. Choose a sufficient model/effort for each item according to complexity and risk, honoring user preferences; explain the choices briefly in `rules.md`. Routine localized edits need not use the strongest model. Security, migrations, cross-cutting changes and final verification deserve stronger reasoning. Price is not a speed benchmark.
28
+
29
+ `task.json` may set default `model` and `thinking`; item tags override those defaults. Handoff rejects unavailable models and unsupported thinking levels before running checks or committing. Legacy untagged items retain queue defaults. Both CLI and Pi Web session creation use the resolved pair; automatically generated split/fix items inherit their parent's selection. No mid-run model guessing or automatic fallback.
30
+
31
+ ## What the runner judges
32
+
33
+ An item passes only if:
34
+ - it changed files only under `allowedPaths`
35
+ - it stays within its size limits: net added lines (`maxNetLines`, 150) and files gaining lines (`maxFilesGrowing`, 5)
36
+ - every check command passes
37
+
38
+ If a check fails, the agent gets `fixRounds` (1) chances in the same session. Failed work is kept on `queue-failed/<id>`.
39
+
40
+ ## Sizes adjust themselves
41
+
42
+ 1. An agent that sees the item is bigger replies `NEEDS: 20 files, 900 lines`. The runner jumps to the first `escalate` level that fits (15/600 → 40/1500 → 200/5000) and the same agent carries on.
43
+ 2. An agent that stops on size, or whose change overflows, gets the next level.
44
+ 3. Typing "bypass limits" in a running item's session lifts size limits for that item.
45
+ 4. Past the last level, the same agent **splits** the item into 3–8 parts (`A01a…`), with their own areas and `[after]` order. They run next. Parts are not split again.
46
+
47
+ ## Re-queuing
48
+
49
+ - **Deferred:** an item that says `BLOCKED` while other work is still pending is retried after that work. This catches the most common case, a prerequisite that hasn't run yet. A second BLOCKED with nothing left that could help is a failure for you (a missing decision, contract or environment).
50
+ - **Conflict:** an item whose change no longer fits the moved branch is redone at once on the latest code, in the same session (`REDO`). If it conflicts again it is parked and retried at the end.
51
+ - **Parked items** (conflicts, deferred) run only when nothing else is running, one at a time. `unpark` or `next` lets them run normally (see manager.md).
52
+
53
+ ## [check] items
54
+
55
+ - Verify instead of build. Give them `[after: <items they verify>]`.
56
+ - Pass **without changes** when everything holds. The log reads `PASS V01 · checked, nothing to change`.
57
+ - May fix tests, fixtures and docs inside the allowed paths.
58
+ - Report production gaps as lines: `GAP: <what is wrong and what to change>. Done when: … [areas: …]`. The queue adds fix items `V01-fix1…` before the check and checks again after them, up to `gapRounds` (2) rounds.
59
+ - **Recheck:** a passed check runs again automatically when an item it covers changes later, meaning its `[after]` names that item or their `[areas]` overlap (`RECHECK` in the log).
60
+ - `BLOCKED` when it cannot check at all, for example with no test environment.
61
+
62
+ ## Good plans
63
+
64
+ - Use `[after]` only for real prerequisites, not blanket stage barriers. Ready prerequisites on longer chains get preference; independent work stays parallel. Cycles and duplicate tags are rejected.
65
+ - Narrow, accurate `[areas]`. They decide parallelism and prevent conflicts.
66
+ - One coherent change per item. Splitting artificially is worse than one `[big]` item.
67
+ - No items that only investigate or review: every item builds something or is a `[check]`.
68
+ - No item may depend on an unanswered question.
@@ -0,0 +1,59 @@
1
+ # The manager
2
+
3
+ ## Automatic manager
4
+
5
+ After an approved handoff passes its baseline, a detached supervisor creates a dedicated manager session. It survives closing the planning terminal/chat. In Pi Web the manager is pinned; in terminal mode its session is in `.pi-queue/sessions/`, with its ID/reply in `.pi-queue/manager.json`.
6
+
7
+ - A 60-second filesystem/process health tick makes **no model calls** while work is healthy. Runner readiness/exit also wakes the supervisor.
8
+ - After the initial manager turn, a new terminal failure can trigger a diagnostic turn; subsequent interventions are spaced at least 10 minutes apart. An unchanged incident is not prompted repeatedly.
9
+ - The manager is chat-only, with no tools or extension capabilities. It proposes exact existing-item retries with repair guidance, or a clean runner restart. Code validates proposals. Workers make repairs under the original scope, checks and model selection.
10
+ - At most two automatic retries per item and two clean runner restarts per task, persisted across supervisor restarts. Manual pause/stop, changed approval/configuration, missing decisions/credentials and uncertain worker ownership prevent automatic recovery. Exhausted or rejected interventions stop supervision and leave evidence for the user.
11
+ - A crash with recorded workers quarantines their worktrees: no reset, cleanup or automatic orphan reclamation. Quiet tests/provider retries are not treated as hangs.
12
+ - Set `manager: false` in `.pi-queue/config.json` to disable automatic management. A direct `node runner.mjs` invocation is unsupervised; normal handoff and `/queue start` launch the supervisor.
13
+ - Models requiring a custom provider extension cannot run in the isolated chat-only manager; configure an available built-in/configured provider. No model fallback is guessed.
14
+
15
+ ## Manual manager
16
+
17
+ `/task manage` makes the current chat a separate interactive manager. In Pi Web it is pinned. This chat acts **only on what the user asks in each message**; the automatic supervisor does not prompt it.
18
+
19
+ ## Tools
20
+
21
+ | Tool | What |
22
+ |---|---|
23
+ | `queue_overview` | counts, running items, failed items with reasons, parked and open items, recent log |
24
+ | `queue_item` | one item: its line, status, commit (files changed), last result, activity |
25
+ | `queue_control` | `stop`, `start`, `retry`, `recheck`, `rerun`, `unpark`, `next`, `add`, `parallel` |
26
+
27
+ `ids` are item ids or prefixes: `V04` matches `V04a…V04h`. Empty ids means all.
28
+
29
+ | Action | Effect |
30
+ |---|---|
31
+ | `retry` | failed or parked items run again |
32
+ | `recheck` | passed `[check]` items run again |
33
+ | `rerun` | passed items run again (their commits stay; the new run builds on top) |
34
+ | `unpark` | parked/deferred items run normally, in parallel, without waiting for the end |
35
+ | `next` | these items run next: un-parked and moved to the front; dependencies and overlapping areas still apply |
36
+ | `add` | new work-list lines, only lines the user agreed to (kept in state; the plan file is not edited) |
37
+ | `parallel` | how many run at once. Works while running; the number becomes the ceiling and is saved for the next start |
38
+ | `stop` / `start` | stop the run (running items' work kept on branches) / start it |
39
+
40
+ Controls go through atomic files in `.pi-queue/control/`. The runner alone applies them, within seconds while running or on the next start otherwise; applied actions are logged. `/queue pause` lets current workers finish; `/queue stop` aborts them and preserves work. Only an explicit `/queue start` clears those markers. Wait for the old runner/supervisor to exit before starting.
41
+
42
+ The same actions exist as commands: `/queue retry [id]`, `/queue recheck [id]`, `/queue rerun <id>`, `/queue unpark`, `/queue next <ids>`, `/queue parallel <n>`.
43
+
44
+ ## Diagnosing
45
+
46
+ 1. `queue_overview` first. Group failures by cause; many failures often share one.
47
+ 2. Typical causes:
48
+ - **A check or consumer ran before what it depends on.** The reasons say "not built yet", "absent", "still uses the old …". Fix: `retry` once the prerequisites pass, or `next` the prerequisites. Plans should tag `[after]`.
49
+ - **A real gap the plan missed.** Propose items, and `add` them after the user agrees.
50
+ - **A decision or contract question.** Tell the user plainly what must be decided.
51
+ - **Environment** (no test endpoint, credentials, a broken test suite). The user must provide it, or the item should be dropped.
52
+ - **The size limit.** That handles itself. If it keeps failing, suggest splitting the item.
53
+ 3. Explain in a few lines: what is wrong, why, and what you would do. Act only when asked.
54
+
55
+ ## Rules
56
+
57
+ - Never edit code, revert commits or delete branches. If that is needed, say so.
58
+ - Never add items the user did not agree to.
59
+ - Queue items cannot use these tools (they refuse inside `.pi-queue/worktrees/` and in sessions named `queue …`).
@@ -0,0 +1,39 @@
1
+ # Troubleshooting
2
+
3
+ Each of these was hit in real use.
4
+
5
+ **The queue stops right away: "checks fail before any change".**
6
+ The checks fail on today's code, so every item would fail. `/queue problem` shows the output and a diagnosis.
7
+ - **"Missing script" / "pnpm recursive exec failed":** the check names a script that doesn't exist. `/task setup` only offers commands that ran successfully; re-run it after scripts change.
8
+ - **Passes in your folder but fails in a fresh worktree:** git-ignored files are missing (`.env`, generated types, another package's build output). List env files in `copyFromRepo`, or add the build to `setup`.
9
+ - **Fails everywhere:** the code itself is broken. Fix that first, or drop the command from `checks`.
10
+
11
+ **Checks behave differently when started from Pi Web.** Pi Web is a Node/pnpm process and leaks `npm_*`/`PNPM_*`/`NODE_ENV` variables. pi-flow strips them before setup and checks; if a tool needs one of them, set it inside the check command.
12
+
13
+ **"Working tree is not clean".** The queue fast-forwards your branch, so it needs a clean tree. Commit or stash your edits. The handoff commits only the plan folder.
14
+
15
+ **Interrupted workers / quarantined worktrees.** The runner refuses to reset dirty or unintegrated worktrees and retains worker ownership after an uncertain exit. Stop/confirm termination of those exact CLI/Web workers, preserve their changes/commits, and reconcile the recorded slots before restarting. Do not delete `state.running` or force-clean a worktree merely to bypass this protection. The automatic manager will not do that for you.
16
+
17
+ **Many items are BLOCKED with "not built yet / absent / still uses the old …".** They ran before their prerequisites. pi-flow defers such items and retries them after the rest. If the run ended anyway, ask the manager: "retry everything that was blocked by missing prerequisites". For future plans, tag `[after: …]`.
18
+
19
+ **A check fails with "no changes were made".** Only in old plans without `[check]`. Tag verification items `[check]` and they pass when nothing needs changing.
20
+
21
+ **Parked items run one at a time at the end.** Parked items (after a conflict or a deferral) wait for quiet so they don't conflict again. To speed up: `unpark`, or `next <ids>`.
22
+
23
+ **The run stopped with "5 failures in a row".** That's the safety stop. The automatic manager may diagnose it and propose bounded retries within the approved task; unresolved blockers or exhausted budgets remain stopped. Read the reasons, address the cause, then explicitly `retry` and `start`. Manual stops do not count as real failures.
24
+
25
+ **You continued a failed item by hand and another item clashed with it.** When an item fails, its worktree goes to the next item. Don't keep working in a failed item's session; `retry` it, or raise its limits through the queue.
26
+
27
+ **Size limits keep blocking an item.** Limits rise by themselves (`NEEDS`, escalation, split). "bypass limits" in its session lifts them for that item, and `[big]` or `[files= lines= min=]` in its line starts it higher.
28
+
29
+ **`/queue` exists twice, or behaves oddly.** The old pi-queue package is still installed next to pi-flow. Remove it (`pi remove -l ./.pi/pi-queue` or the personal entry) and reload.
30
+
31
+ **Pi Web shows a stray "w0" project.** Sessions of a removed worktree lose their grouping. `/task cleanup --all` keeps worktrees while item sessions exist. Delete those sessions (`/task cleanup`) before removing worktrees.
32
+
33
+ **Pi Web asks for a password / "401".** Start Pi Web with `PI_WEB_PASSWORD` set in the environment the queue sees, or put `piWebPassword` in `.pi-queue/config.json`.
34
+
35
+ **Pi Web isn't used although it runs.** The runner looks for Pi Web's run records and port 30141. Set `piWebUrl` in `.pi-queue/config.json` if yours differs. `runIn: "web"` refuses to start without it, and `"cli"` never uses it.
36
+
37
+ **Nothing happens overnight.** Keep the machine awake (`caffeinate -dims`) and Pi Web running for Web workers. The detached supervisor survives closing the planning chat, not a machine shutdown or killed supervisor. Explicit start resumes saved state only when no interrupted worker ownership remains. Grant project trust before starting unattended CLI workers; saved trust denials are not overridden.
38
+
39
+ **Parallel items rerun the same tests and the machine crawls.** Install pi-check (a separate plugin). The queue then sends every check through one shared broker per repo. Results are cached by file content and shared by all worktrees, identical runs in progress are joined, and heavy runs are throttled. Go failures that already fail on the base branch stop blocking items. See `/check status` for what runs and how much time was saved.