diff --git a/pstack/.cursor-plugin/plugin.json b/pstack/.cursor-plugin/plugin.json index e9935daa7..78486703d 100644 --- a/pstack/.cursor-plugin/plugin.json +++ b/pstack/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack", - "version": "0.14.2", + "version": "0.14.3", "description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence.", "author": { "name": "Lauren Tan" diff --git a/pstack/skills/poteto-mode/playbooks/multi-phase-plan.md b/pstack/skills/poteto-mode/playbooks/multi-phase-plan.md index 956eee235..76affeb2d 100644 --- a/pstack/skills/poteto-mode/playbooks/multi-phase-plan.md +++ b/pstack/skills/poteto-mode/playbooks/multi-phase-plan.md @@ -1,3 +1,155 @@ ### Multi-phase or multi-PR plan -Follow [../references/plan.md](../references/plan.md). +**You own the plan, not the code. The plan is a checklist an owner runs box by box and the operator audits from the evidence.** For work that spans phases or stacked PRs. The plan is the deliverable. Do not implement. + +1. When the change is one or two files with an obvious approach, skip the plan. Say so and stop. +2. Settle open questions by prototype before you write. For a question about layout, timing, behavior, or whether an API works, run `playbooks/prototype.md`. Keep the branch, the SHA, and the screenshots for Appendix A. Ask the operator only about a product or preference call that no run can settle. Give options (the **never-block-on-the-human** principle skill). +3. Explore in subagents with `subagent_type: "poteto-agent"` and an explicit model per the Subagents section (the **guard-the-context-window** principle skill). Each returns file pointers, conventions, test commands, and entry points. No inlined dumps. +4. Copy the skeleton below into the plan file and fill every placeholder. Unless the operator names a path, write the file under the agent store's `docs/`. Keep every heading and every sub-block in the order shown. One section per PR. One PR is one change with its own evidence (the **sequence-verifiable-units** principle skill). Name the execution playbook in **How to read this**. Pick between `playbooks/autopilot-full.md` and `playbooks/autopilot-stack.md` per the rule at the end of `playbooks/autopilot-stack.md`. A standing program takes `playbooks/orchestrate.md`. +5. Write under `/technical-writing` in full, then `/unslop`. The body is one Diátaxis mode, how-to. Appendices hold explanation and reference. Two rules apply verbatim. "i dont want any abstract metaphors" and "write like hemingway". Each heading states the task or the finding. No long dashes. No mid-sentence colons. +6. Run `node pstack/skills/poteto-mode/scripts/check-plan.mjs ` and fix every line it prints (the **encode-lessons-in-structure** principle skill). It enforces the skeleton's shape, the verification rule in every verification block, and the punctuation rules. +7. Hand back. Post the plan path and the script's output, then stop. Execution starts on the operator's explicit go, under the execution playbook the plan names. + +**Verification.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked (the **prove-it-works** principle skill). That sentence is the verification rule. Every verification block opens with it. The live block is mandatory. Ten lanes on `grok-4.6-fast-xhigh` at the PR head drive the real surface through its control skill, per the **swarm** skill. Each lane is one box with a concrete scenario, the screenshot it saves, and its pass predicate. The perf block names the metric, the probe, the trunk baseline measured first, and the rule with the number that fails. A PR that changes an interaction is review-gated. The operator reviews it in chat with screenshots and a video before merge. A PR that changes no interaction writes `**Review gate.** None. is not review-gated.` and no boxes under it. + +**Control skill.** Pick it by surface. Browser, Electron, and web UIs use `control-ui` from `cursor-team-kit`. CLIs and TUIs use `control-cli` from `cursor-team-kit`. Native mobile uses whatever simulator-driving skill the repo has. A PR that touches two surfaces gets lanes on both. A surface with no control skill is a risk in Appendix C, and its live block still names how each lane drives it. + +````markdown +# plan + + + +## How to read this + +One box is one unit of work. Every box names the evidence that checks it. A nested box is a sub-step of the box above it. Check a box only when its evidence exists, a file, a log line, a screenshot, a test run, or a SHA. The body is a how-to. The appendices explain and record. + +The program runs `pstack/skills/poteto-mode/playbooks/.md`. + +Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +## Program checklist + +### Arm the program + +- [ ] State the protocol and this plan to the operator, then stop. Start execution only on her explicit go. +- [ ] On her go, arm a `/goal` with this exact text. "" +- [ ] Read these from trunk at program start. Re-read them at every tick. + - [ ] `git show origin/main:pstack/skills/poteto-mode/playbooks/.md` + - [ ] `git show origin/main:pstack/skills/swarm/SKILL.md` + - [ ] `git show origin/main:` + - [ ] `git show origin/main:pstack/skills/poteto-mode/playbooks/opening-a-pr.md` + - [ ] `git show origin/main:pstack/skills/` +- [ ] Arm the 30-minute audit tick. In a local session, a real terminal `/loop`. In a cloud root, a cloud-sleeper wake chain. Never leave the cadence to memory. +- [ ] Use this tick prompt, verbatim. "Re-read the execution playbook from trunk and the armed /goal. Audit the operation against both and fix drift in this tick. Probe every active lane and judge progress by side effects only. Stand down a stuck lane and dispatch its replacement now. Then send the operator a status message, whether or not anything changed, with the queue table of PR, owner, state, and head SHA, the verdicts since the last tick, what merged, open operator gates, and blockers." +- [ ] On the operator's hold or stand-down, send every owner a zero-writes order at once. + +### Spawn owners + +- [ ] Spawn one owner per PR with the full lifecycle the execution playbook names. +- [ ] Follow this dependency graph. Start dependent work only after its parent merges, or base it on the parent branch when the execution playbook stacks. + - [ ] and are independent and first. Both branch from `main`. + - [ ] after . +- [ ] Hold the file boundaries. touches only ``. +- [ ] Hold the review gate. change an interaction. They wait for the operator's review in chat with screenshots and a video before merge. + +### PR mechanics, for every PR + +- [ ] Open the PR ready, never draft, with `gh pr create` and `draft: false`, or with Graphite `gt` for a stack. +- [ ] Run the repo's lint and typecheck once before the PR-facing push. Push with hooks on. +- [ ] Run `/deslop` before each commit and `/no-comments` before review. +- [ ] Triage every Bugbot and security-reviewer comment per `../references/bugbot-triage.md`. +- [ ] Rebase onto current trunk before babysit and again before the merge-ready report. + +### Verdict and merge, for every PR + +- [ ] At the merge-ready head SHA, run the swarm per `pstack/skills/swarm/SKILL.md`. One gates lane. The ten live lanes from the PR's **Verify, live** block. The perf lane from its **Verify, perf** block. One audit lane that reads the diff and the receipts and distrusts the PR body. +- [ ] Clean only when every lane is `PASS`. Findings go back to the owner. A new head gets a fresh swarm and a fresh verdict. +- [ ] + +### Boot recipe, for every live lane + +Each live lane runs on its own cloud VM at the PR head. Drive through `control-ui` or `control-cli` from `cursor-team-kit`. + +- [ ] `git fetch origin && git checkout `. +- [ ] +- [ ] +- [ ] Save every screenshot to `/tmp/swarm-/worker-/.png` and return the paths with the report. + +## () + +**Depends on.** + +**Files.** + +- [ ] Edit ``. +- [ ] Create ``. +- [ ] Delete ``. + +**Build.** + +- [ ] + +**You see.** + +- [ ] + +**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Run ``. + +**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `grok-4.6-fast-xhigh` at the PR head, per the boot recipe. + +- [ ] Lane 1. Save `.png`. Pass when . +- [ ] Lane 2. Save `.png`. Pass when . +- [ ] Lane 3. Save `.png`. Pass when . +- [ ] Lane 4. Save `.png`. Pass when . +- [ ] Lane 5. Save `.png`. Pass when . +- [ ] Lane 6. Save `.png`. Pass when . +- [ ] Lane 7. Save `.png`. Pass when . +- [ ] Lane 8. Save `.png`. Pass when . +- [ ] Lane 9. Save `.png`. Pass when . +- [ ] Lane 10. Save `.png`. Pass when . + +**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Metric. +- [ ] Probe. +- [ ] Baseline. Record the trunk first. +- [ ] Rule. + +**Review gate.** The operator reviews before merge. + +- [ ] Copy lane screenshots into `/-review-.png`. +- [ ] Record a 30 to 60 second video of the change on a lane VM. Save it as `/-review.mp4`. +- [ ] Post the screenshots and the video in chat. Stop at merge-ready. Wait for the operator's click. + +**Merge.** + +- [ ] Root's clean verdict at the exact head SHA. +- [ ] Bugbot triage done. +- [ ] Rebased onto current trunk after the verdict, patch-id unchanged. +- [ ] + +## Close the program + +- [ ] Every box above is checked with its evidence. +- [ ] Reply to the operator with the report the execution playbook names. + +## Appendix A. Prototype evidence + + + +## Appendix B. Alternatives rejected + + + +## Appendix C. Risks + + + +## Appendix D. Links and reading list + + +```` + +**Reply:** the plan path, the PR ids with their dependencies and the review-gated set, what the prototypes proved and what stays unproven, and the check script's output. diff --git a/pstack/skills/poteto-mode/references/plan.md b/pstack/skills/poteto-mode/references/plan.md deleted file mode 100644 index dd067d1b2..000000000 --- a/pstack/skills/poteto-mode/references/plan.md +++ /dev/null @@ -1,105 +0,0 @@ -# Plan - -Produce a phased implementation plan grounded in the **Principles** section of the `poteto-mode` skill. The plan is the deliverable. Do not implement. - -Open a todolist with one item per step below. - -## 0. Triage - -Skip the plan when the change is one or two files with an obvious approach. Say so and stop. - -Plan when the change spans three or more files, introduces architecture, has competing approaches or unclear scope, or the user asked for one. - -## 1. Re-read principles - -Read the **Principles** section of the `poteto-mode` skill end to end, and the leaf `principle-*` skills it indexes. The principles govern every plan decision; cross-link them. - -## 2. Scope and constraints - -State your read of scope and constraints in one paragraph. Use `AskQuestion` only for genuinely ambiguous intent (the **never-block-on-the-human** principle skill); give concrete options with each open question. - -Resolve what is in scope vs explicitly out, technical or platform constraints, patterns to preserve, and the definition of done. - -## 3. Explore in subagents - -Delegate codebase exploration (the **guard-the-context-window** principle skill). - -- Prefer `subagent_type: "poteto-agent"`. `generalPurpose` is the fallback. Never use the built-in `plan` subagent_type; it ignores this skill. -- Pass `model:` explicitly per the configured roles (defaults `grok-4.6-fast-xhigh` for code, `claude-fable-5-thinking-max` for judgment). - -Each explorer returns file pointers, conventions, dependencies, test infrastructure, and entry points. No inlined dumps. - -## 4. Write the plan - -The user specifies where the plan lives. - -Single file `NN-slug.md` for small plans. For three or more phases, a directory with `overview.md` plus phase files: - -``` -NN-slug/ -├── overview.md -├── phase-1-scaffold.md -├── phase-2-...md -└── testing.md -``` - -### Phase sizing - -- One function or type plus tests, or one bug fix. Not "one file". -- Two to three files touched, max. -- Prefer eight to ten small phases over three to four large ones to preserve option value (the **foundational-thinking** principle skill). -- Split if a phase has more than five test cases or three functions. - -### Overview file - -- **Context.** Problem and why now. -- **Scope.** Included; explicitly excluded. -- **Constraints.** Technical, platform, dependency, pattern. -- **Alternatives.** Two or three approaches sketched, choice and rationale (the **exhaust-the-design-space** principle skill). Skip when constraints dictate one. -- **Applicable skills.** Domain skills the implementer should invoke, by name. -- **Phases.** Ordered standard-markdown links to phase files. -- **Verification.** Project-level commands. -- **Implementation guidance.** Per section 6. - -### Phase files - -- Back-link to overview. -- **Goal.** What the phase accomplishes. -- **Changes.** Files affected and the change at a high level. What and why, not how. No code snippets. -- **Data structures.** Name the key types or schemas. One-line sketch only (the **foundational-thinking** principle skill). -- **Verification.** Per section 6. - -Order phases so infrastructure and shared types land first (the **foundational-thinking** principle skill). Each phase should be independently shippable. - -For changes touching existing code, apply the **redesign-from-first-principles** principle skill: if we'd built this with the new requirement on day one, what would it look like? Redesign holistically; deliver incrementally. - -If a phase creates or edits a skill, the phase instructs the implementer to use the **create-skill** skill (Cursor's built-in for authoring SKILL.md files). - -## 5. Verification per phase - -Each phase needs both: - -**Static.** Type check, lint, project tests pass. - -**Runtime.** Exercise the feature on the matching surface via the relevant control skill: - -- Browser / Electron / Web UIs: the `control-ui` skill from the `cursor-team-kit` plugin. -- CLIs and TUIs: the `control-cli` skill from the `cursor-team-kit` plugin. -- Native mobile: whatever simulator-driving skill your team has. -- No control skill for the touched surface: flag it in the plan. - -For bug fixes, the loop is reproduce on the surface, fix, verify on the same surface. Unit tests show a branch behaves a certain way; they do not prove the bug is gone (the **prove-it-works** principle skill). - -## 6. Implementation guidance - -In the overview, name which poteto-mode non-negotiables the implementer must apply, by name: - -- the **how** skill over each unfamiliar subsystem before changing it. -- the **interrogate** skill for adversarial review on contested designs before shipping. -- `/deslop` over each diff before commit. the **unslop** skill over any prose surface. -- the **show-me-your-work** skill to keep a decision trail when the plan is large enough to need an auditable record. -- Cursor's built-in **babysit** skill after opening the PR. - -## 7. Hand back - -Summarize phases, scope boundaries, applicable skills, and verification. Stop. The user decides when implementation starts. diff --git a/pstack/skills/poteto-mode/scripts/check-plan.mjs b/pstack/skills/poteto-mode/scripts/check-plan.mjs new file mode 100755 index 000000000..21d350ac1 --- /dev/null +++ b/pstack/skills/poteto-mode/scripts/check-plan.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import process from "node:process"; + +const RULE = + "Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked."; +const LANES = "Ten lanes on `grok-4.6-fast-xhigh` at the PR head"; +const SUB_BLOCKS = [ + "Depends on.", + "Files.", + "Build.", + "You see.", + "Verify, unit.", + "Verify, live.", + "Verify, perf.", + "Review gate.", + "Merge.", +]; +const PROGRAM_H3 = ["Arm the program", "Spawn owners", "PR mechanics", "Verdict and merge", "Boot recipe"]; +const PROGRAM_MARKERS = ["/goal", "git show origin/main:", /30[- ]minute/, "status message"]; +const HOW_TO_READ_MARKERS = [ + "One box is one unit of work", + "names the evidence", + "Check a box only when its evidence exists", + "playbooks/", + RULE, +]; +const PERF_ITEMS = ["Metric.", "Probe.", "Baseline.", "Rule."]; +const BOX = /^\s*- \[[ x]\] (.*)$/; + +const file = process.argv[2]; +if (!file) { + console.error("Usage: node check-plan.mjs "); + process.exit(2); +} + +const raw = fs.readFileSync(file, "utf8").split(/\r?\n/); +const problems = []; +const fail = (line, message) => problems.push(`${file}:${line}: ${message}`); + +let start = 0; +if (raw[0] === "---") { + start = raw.indexOf("---", 1) + 1; +} + +const lines = []; +let fence = false; +for (let i = start; i < raw.length; i++) { + const text = raw[i]; + const n = i + 1; + if (/^```/.test(text)) fence = !fence; + lines.push({ n, text, code: fence }); + if (fence) continue; + const prose = text + .replace(/`[^`]*`/g, "`") + .replace(/!\[[^\]]*\]\([^)]*\)/g, "") + .replace(/\]\([^)]*\)/g, "]"); + if (/[\u2013\u2014]/.test(prose)) fail(n, "long dash"); + if (/[\u2018\u2019\u201c\u201d]/.test(prose)) fail(n, "curly quote"); + if (/: \S/.test(prose)) fail(n, "mid-sentence colon"); +} + +const h2 = (l) => (!l.code && l.text.startsWith("## ") ? l.text.slice(3).trim() : null); +const sections = []; +for (const l of lines) { + const title = h2(l); + if (title !== null) sections.push({ title, n: l.n, body: [] }); + else if (sections.length) sections.at(-1).body.push(l); +} +const find = (title) => sections.find((s) => s.title === title); +const bodyText = (s) => s.body.map((l) => l.text).join("\n"); +const boxes = (ls) => ls.filter((l) => !l.code && BOX.test(l.text)).map((l) => ({ n: l.n, text: l.text.match(BOX)[1] })); + +const h1 = lines.findIndex((l) => !l.code && l.text.startsWith("# ")); +if (h1 === -1) fail(1, "no H1 title"); +const howToRead = find("How to read this"); +if (!howToRead) fail(1, 'no "## How to read this" section'); +if (h1 !== -1 && howToRead) { + const intro = lines.slice(h1 + 1).filter((l) => l.n < howToRead.n && l.text.trim() !== ""); + if (intro.length >= 10) fail(lines[h1].n, `intro is ${intro.length} lines, under ten required`); + for (const marker of HOW_TO_READ_MARKERS) { + if (!bodyText(howToRead).includes(marker)) fail(howToRead.n, `How to read this lacks "${marker}"`); + } +} + +const program = find("Program checklist"); +if (!program) fail(1, 'no "## Program checklist" section'); +else { + const h3s = program.body.filter((l) => !l.code && l.text.startsWith("### ")).map((l) => l.text.slice(4).trim()); + let cursor = 0; + for (const name of PROGRAM_H3) { + const at = h3s.findIndex((t, i) => i >= cursor && t.startsWith(name)); + if (at === -1) fail(program.n, `Program checklist lacks "### ${name}" in order`); + else cursor = at + 1; + } + for (const marker of PROGRAM_MARKERS) { + const ok = marker instanceof RegExp ? marker.test(bodyText(program)) : bodyText(program).includes(marker); + if (!ok) fail(program.n, `Program checklist lacks "${marker}"`); + } +} + +const close = find("Close the program"); +if (!close) fail(1, 'no "## Close the program" section'); +const programIndex = sections.indexOf(program); +const closeIndex = sections.indexOf(close); +const prSections = programIndex === -1 || closeIndex === -1 ? [] : sections.slice(programIndex + 1, closeIndex); +if (prSections.length === 0) fail(1, "no PR sections between Program checklist and Close the program"); + +const report = []; +for (const pr of prSections) { + const heads = []; + for (const l of pr.body) { + if (l.code) continue; + const m = l.text.match(/^\*\*([^*]+)\*\*(.*)$/); + if (m && SUB_BLOCKS.includes(m[1])) heads.push({ name: m[1], n: l.n, rest: m[2].trim(), lines: [] }); + else if (heads.length) heads.at(-1).lines.push(l); + } + const names = heads.map((h) => h.name); + if (names.join("|") !== SUB_BLOCKS.join("|")) { + fail(pr.n, `${pr.title}: sub-blocks are [${names.join(", ")}], expected [${SUB_BLOCKS.join(", ")}]`); + } + const block = (name) => heads.find((h) => h.name === name); + const counts = {}; + for (const h of heads) counts[h.name] = boxes(h.lines).length; + + const depends = block("Depends on."); + if (depends && depends.rest === "") fail(depends.n, `${pr.title}: Depends on names nothing`); + for (const name of ["Files.", "Build.", "You see.", "Verify, unit.", "Merge."]) { + const b = block(name); + if (b && boxes(b.lines).length === 0) fail(b.n, `${pr.title}: ${name} has no box`); + } + for (const name of ["Verify, unit.", "Verify, live.", "Verify, perf."]) { + const b = block(name); + if (b && !b.rest.startsWith(RULE)) fail(b.n, `${pr.title}: ${name} does not open with the rule`); + } + + const live = block("Verify, live."); + if (live) { + if (!live.rest.includes(LANES)) fail(live.n, `${pr.title}: Verify, live lacks "${LANES}"`); + const lanes = boxes(live.lines).map((b) => ({ ...b, m: b.text.match(/^Lane (\d+)\. /) })); + const numbers = lanes.filter((b) => b.m).map((b) => Number(b.m[1])).sort((a, b) => a - b); + if (numbers.join(",") !== "1,2,3,4,5,6,7,8,9,10") fail(live.n, `${pr.title}: lanes are [${numbers.join(",")}], expected 1 to 10`); + for (const lane of lanes) { + if (!lane.m) fail(lane.n, `${pr.title}: live box is not a lane`); + else if (!/Save `[^`]+`/.test(lane.text)) fail(lane.n, `${pr.title}: lane ${lane.m[1]} names no screenshot`); + else if (!lane.text.includes("Pass when")) fail(lane.n, `${pr.title}: lane ${lane.m[1]} has no pass predicate`); + } + } + + const perf = block("Verify, perf."); + if (perf) { + const items = boxes(perf.lines).map((b) => b.text.split(" ")[0]); + if (items.join("|") !== PERF_ITEMS.join("|")) fail(perf.n, `${pr.title}: perf boxes are [${items.join(", ")}], expected [${PERF_ITEMS.join(", ")}]`); + } + + const gate = block("Review gate."); + if (gate) { + const gateBoxes = boxes(gate.lines); + if (gate.rest.startsWith("None.")) { + if (gateBoxes.length) fail(gate.n, `${pr.title}: Review gate says None but has boxes`); + } else { + const text = gate.lines.map((l) => l.text).join("\n"); + if (gateBoxes.length === 0) fail(gate.n, `${pr.title}: Review gate has no box`); + for (const word of ["screenshot", "video", "operator"]) { + if (!text.includes(word)) fail(gate.n, `${pr.title}: Review gate lacks "${word}"`); + } + } + } + + const total = boxes(pr.body).length; + const cells = SUB_BLOCKS.filter((s) => s !== "Depends on.").map((s) => `${s.replace(/[ ,.]+/g, "-").replace(/-$/, "").toLowerCase()}=${counts[s] ?? 0}`); + report.push(`${pr.title} boxes=${total} ${cells.join(" ")}`); +} + +if (closeIndex !== -1) { + const tail = sections.slice(closeIndex + 1); + for (const s of tail) { + if (!s.title.startsWith("Appendix")) fail(s.n, `"## ${s.title}" after Close the program is not an appendix`); + } + if (!tail.some((s) => s.title.includes("Prototype evidence"))) fail(close.n, 'no "## Appendix ... Prototype evidence" section'); +} + +for (const line of report) console.log(line); +console.log(`${prSections.length} PR sections, ${problems.length} problems`); +for (const p of problems) console.error(p); +process.exit(problems.length ? 1 : 0);