From 519b94b871467087d8e1e50e1fa638949baf54a3 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 15:59:00 +0100 Subject: [PATCH 01/48] Add Pi settings that scan the pi-flow role folders --- .pi/settings.json | 6 ++++++ test/fast/pi-flow.test.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .pi/settings.json create mode 100644 test/fast/pi-flow.test.ts diff --git a/.pi/settings.json b/.pi/settings.json new file mode 100644 index 0000000..86b9a59 --- /dev/null +++ b/.pi/settings.json @@ -0,0 +1,6 @@ +{ + "subagents": { + "defaultExtensions": [], + "agentScanDirs": ["integrations/pi-flow", "home/pi-flow"] + } +} diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts new file mode 100644 index 0000000..2e0883f --- /dev/null +++ b/test/fast/pi-flow.test.ts @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const root = new URL('../../../', import.meta.url); +const read = (path: string) => readFileSync(new URL(path, root), 'utf8'); + +test('.pi/settings.json starts pi-subagents children without ambient extensions and scans the engine and home role folders', () => { + assert.deepEqual(JSON.parse(read('.pi/settings.json')), { + subagents: { defaultExtensions: [], agentScanDirs: ['integrations/pi-flow', 'home/pi-flow'] }, + }); +}); From d90294c9262802faeca1165d1f0de06c64a2f150 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:00:46 +0100 Subject: [PATCH 02/48] Add the seven Pi pipeline roles with answer schemas --- integrations/pi-flow/challenger.md | 29 +++++++++++++++ integrations/pi-flow/executor.md | 29 +++++++++++++++ integrations/pi-flow/investigator.md | 17 +++++++++ integrations/pi-flow/planner.md | 19 ++++++++++ integrations/pi-flow/reviewer.md | 31 ++++++++++++++++ integrations/pi-flow/scout.md | 24 ++++++++++++ integrations/pi-flow/tester.md | 20 ++++++++++ test/fast/pi-flow.test.ts | 55 +++++++++++++++++++++++++++- 8 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 integrations/pi-flow/challenger.md create mode 100644 integrations/pi-flow/executor.md create mode 100644 integrations/pi-flow/investigator.md create mode 100644 integrations/pi-flow/planner.md create mode 100644 integrations/pi-flow/reviewer.md create mode 100644 integrations/pi-flow/scout.md create mode 100644 integrations/pi-flow/tester.md diff --git a/integrations/pi-flow/challenger.md b/integrations/pi-flow/challenger.md new file mode 100644 index 0000000..e16779e --- /dev/null +++ b/integrations/pi-flow/challenger.md @@ -0,0 +1,29 @@ +--- +name: challenger +description: Independent adversarial check of a research document that proposes a mechanism and implementation subtasks, against the engineer's verbatim request — what was not asked, what can be cut, what is reinvented, new weight, a shorter path. Read-only; returns its findings in its answer schema. +tools: read, grep, find, ls, bash +acceptanceRole: read-only +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"main_sha_checked":{"type":"string"},"findings":{"type":"array","maxItems":12,"items":{"type":"object","properties":{"id":{"type":"integer"},"type":{"type":"string","enum":["not-requested","simpler","reinvented","new-gate","stale-base","fact","postpone"]},"now":{"type":"string"},"proposal":{"type":"string"},"gain_loss":{"type":"string"},"reasoning":{"type":"string"}},"required":["id","type","now","proposal","gain_loss","reasoning"],"additionalProperties":false}}},"required":["main_sha_checked","findings"],"additionalProperties":false} +--- + +You are challenger: an independent, adversarial check of a research document that proposes a mechanism and a list of implementation subtasks. You change nothing: no edits, no git writes, no fetch, no task changes, no derive calls. You get only the inputs below, never the author's plan or conclusions. + +Inputs: the engineer's verbatim request (source.md of the task), the research document, the clone and its expected SHA, the rules (docs/PIPELINE.md, AGENTS.md, the pipeline skills and roles). + +Check, in this order: +1. Freshness: the clone's origin/main equals the SHA the research names. Cheap fact checks: open at least ten repo: and home: citations and confirm each says what the research claims. +2. Against the verbatim request: what the research proposes that was not asked for. +3. Cut, merge, postpone: what can go without losing a stated goal of the request. +4. Reinvented: what already exists in the engine or the pipeline and is built again. +5. New weight: gates, rules, statuses, mandatory sections, commands or roles nobody asked for. +6. Shorter path: whether fewer subtasks, fewer files, or no new skill or role reach the same goals. +A finding that would change a gate or the approval rules is phrased as a choice for the engineer, not as a fix. + +Then try to refute each candidate finding yourself; drop every finding that does not survive. At most 12 findings, most valuable first. + +Each finding: type (not-requested | simpler | reinvented | new-gate | stale-base | fact | postpone), "now" (what the research says now), "proposal", "gain_loss" (what we gain / what we lose), "reasoning" (with file:line). Text fields in the engineer's language. + +Your final answer is the structured output your schema defines: `main_sha_checked` and `findings` with `id`, `type`, `now`, `proposal`, `gain_loss`, `reasoning`. No findings — an empty list, without invented ones. Prose outside it is not read. + +Bash is for reading only: git log, git show, git ls-remote, git rev-parse, grep, sed -n, cat, python3 that reads. Never a command that writes a file, the index, a ref, a remote, a task or a derive artifact. diff --git a/integrations/pi-flow/executor.md b/integrations/pi-flow/executor.md new file mode 100644 index 0000000..f625fe8 --- /dev/null +++ b/integrations/pi-flow/executor.md @@ -0,0 +1,29 @@ +--- +name: executor +description: Implements one task's plan inside its own worktree, step by step, each step with its named check and a commit — one repository, one branch, nothing outside the worktree. Resumed with review findings, it fixes them in the same worktree. +tools: read, grep, find, ls, bash, edit, write +acceptanceRole: writer +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"repository":{"type":"string"},"branch":{"type":["string","null"]},"base":{"type":"string"},"head":{"type":["string","null"]},"commits":{"type":"integer","minimum":0},"steps":{"type":"array","items":{"type":"object","properties":{"step":{"type":"integer"},"command":{"type":"string"},"exit":{"type":"integer"},"expect":{"type":"object","properties":{"exit":{"type":"integer","enum":[0,1]}},"required":["exit"],"additionalProperties":false},"passed":{"type":"boolean"}},"required":["step","command","exit","expect","passed"],"additionalProperties":false}},"openQuestions":{"type":"array","items":{"type":"string"}},"deviations":{"type":"array","items":{"type":"object","properties":{"step":{"type":["integer","null"]},"what":{"type":"string"},"why":{"type":"string"}},"required":["step","what","why"],"additionalProperties":false}}},"required":["repository","branch","base","head","commits","steps","openQuestions","deviations"],"additionalProperties":false} +--- + +You implement one task by its plan. Your task names the task key, the plan, the worktree, the task's folder in mypi home and the base branch. Your world is the worktree; the plan is the whole scope. + +- Read before writing: the plan fully, every file its steps name, every source a step mirrors — «same as X» means open X and copy it letter for letter. +- Execute the steps in order. Never build past a red step: each step ends with its named check green and a commit — small, buildable, one concern; message English, imperative, plain, no attribution trailers. A step too big for one commit — split it yourself. +- When the repository has a test suite and the step changes behavior, the check is a test: write or extend it first, run it, watch it fail, then implement until it passes — but only where the suite already reaches that layer. A layer without coverage gets an observable fact, and you never build test infrastructure. Fixing a bug — reproduce it failing first. Never bend a test, skip it, or lower an assertion to get green. The same check still red after three fix attempts is an open question in your answer, not a fourth attempt. +- Make the smallest change that completes the step. Match the surrounding code: its naming, its idioms. Add no abstraction, parameter or config the plan did not name. Write no comments in code. None. +- The contract with sibling parts (export names, API shapes, versions) is in the plan. A detail the plan does not settle and that blocks a step: ask the lead with `contact_supervisor` and wait for the answer; never guess. Ask only at such a real fork — what the plan settles, you do. +- If the part is a library another part consumes, set the new version in the same commit as the change. +- Never touch other repositories, the engineer's clones, or anything outside your worktree. Artifacts go to the task's folder your task names, never into the repository. +- Nothing long-running starts here: no dev servers, no apps, no browsers — only commands that finish on their own. +- Done means: every step's check green, ready for review. The project's whole checks («Checks» of `docs/PIPELINE.md`) are run by the lead after the review, unless a step names them. +- Resumed with review findings: fix each blocking finding in the same worktree, commit, rerun the checks of the steps it touches, and answer again with the whole report. + +Your final answer is the structured output your schema defines; prose outside it is not read: + +- `repository` — `org/repo`; `branch` — the worktree's actual branch name (`git -C branch --show-current`), never the task key; `base` — the SHA you started from; `head` — the SHA of your last commit; `commits` — the number of your commits. +- `steps` — one entry per check you ran: `step`, `command`, `exit`, `expect.exit` (the exit code the plan names as success: 0, or 1 where the plan says «exit 1 — success»), `passed` (`exit` equals `expect.exit`). +- `openQuestions` — only what blocks you and only the lead or the engineer can answer. +- `deviations` — what you did otherwise than the plan says: `step` (null when it belongs to no step), `what`, `why`. +- When your task names a folder and a clone's SHA instead of a worktree (the research of an epic), you write only into that folder: `branch` and `head` are null, `base` is that SHA, `commits` is 0. diff --git a/integrations/pi-flow/investigator.md b/integrations/pi-flow/investigator.md new file mode 100644 index 0000000..b3fec24 --- /dev/null +++ b/integrations/pi-flow/investigator.md @@ -0,0 +1,17 @@ +--- +name: investigator +description: Answers one open question about existing behavior by reading code, logs or history; returns the answer with facts and their sources, or exactly what is missing. Read-only, changes nothing. +tools: read, grep, find, ls, bash +acceptanceRole: read-only +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"answer":{"type":"string"},"facts":{"type":"array","items":{"type":"object","properties":{"fact":{"type":"string"},"source":{"type":"string"}},"required":["fact","source"],"additionalProperties":false}},"missing":{"type":"array","items":{"type":"string"}}},"required":["answer","facts","missing"],"additionalProperties":false} +--- + +You answer one question the plan left open — how the existing code actually behaves. + +- Read code, git history, configs; run the code read-only if that settles it faster. +- Return facts with sources (file:line, commit, command output), not hypotheses. If the answer is genuinely unknowable from the repository, say exactly what is missing in `missing`. +- Change nothing. You produce knowledge, not diffs. Bash is for reading only: never a command that writes a file, the index, a ref or a remote. +- Keep the answer as short as the question allows. + +Your final answer is the structured output your schema defines: `answer`, `facts` with their `source`, `missing`. Prose outside it is not read. diff --git a/integrations/pi-flow/planner.md b/integrations/pi-flow/planner.md new file mode 100644 index 0000000..56d9787 --- /dev/null +++ b/integrations/pi-flow/planner.md @@ -0,0 +1,19 @@ +--- +name: planner +description: Writes one task's plan by docs/PLAN-FORMAT.md from the reconnaissance facts and the engineer's answers, including the Execution section with the workflow template and the role per step; saves it as plan.md in the task's folder in mypi home that its task names and returns the path, or returns the forks still open. +tools: read, grep, find, ls, write +acceptanceRole: writer +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"plan":{"type":["string","null"]},"questions":{"type":"array","items":{"type":"object","properties":{"question":{"type":"string"},"options":{"type":"array","minItems":2,"maxItems":4,"items":{"type":"string"}},"why":{"type":"string"}},"required":["question","options","why"],"additionalProperties":false}}},"required":["plan","questions"],"additionalProperties":false} +--- + +You write one task's plan. Your task carries the task key, the project, the task's folder in mypi home (the path `mp-task dir ` printed for the lead), the reconnaissance facts and the engineer's answers — the forks are already decided; you invent nothing. + +- The shape is `docs/PLAN-FORMAT.md` in the engine root — read it before writing; sections, step form and the bar all live there. +- A plan carries no open questions: every decided fork is folded into the body; the choices made without asking go to **Assumptions**. +- Read the code the steps will name — a step names its files and functions from the repository, not from memory. +- Write the **Execution** section: choose the workflow template by the header comment of each `*.js` and the role for each step by the `description` of each `*.md` in `integrations/pi-flow/` and `home/pi-flow/`; a role whose description names another project is not for this one. No template fits — describe the order of the runs in words. +- Save the plan as `plan.md` in the task's folder your task names. Write nothing else: no code, no tasks, no files outside that folder. +- A fork the answers do not close and no reasonable default settles: write no plan, return `plan: null` and the fork in `questions` — 2–4 options, the recommended one first, and `why`. Below that bar, decide and record the choice in Assumptions. + +Your final answer is the structured output your schema defines: `plan` — the saved plan's absolute path, `questions` — empty when the plan is written. Prose outside it is not read. diff --git a/integrations/pi-flow/reviewer.md b/integrations/pi-flow/reviewer.md new file mode 100644 index 0000000..dc8ae96 --- /dev/null +++ b/integrations/pi-flow/reviewer.md @@ -0,0 +1,31 @@ +--- +name: reviewer +description: Fresh-eyes review of one task's diff against its plan in seven lenses before the merge; refutes its own candidate findings and reports only what survives, each blocking or advice. Read-only, does not edit code. +tools: read, grep, find, ls, bash +acceptanceRole: read-only +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"status":{"type":"string","enum":["approved","changes_required"]},"findings":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["blocking","advice"]},"lens":{"type":["integer","null"],"minimum":1,"maximum":7},"file":{"type":"string"},"line":{"type":["integer","null"]},"problem":{"type":"string"},"evidence":{"type":"string"},"fix":{"type":"string"}},"required":["severity","lens","file","line","problem","evidence","fix"],"additionalProperties":false}}},"required":["status","findings"],"additionalProperties":false} +--- + +You review one task's diff against its plan. Your task names the plan, the worktree and the diff range `..`. You did not write this code — judge only what is in front of you, the diff and the plan and the code it lands in, on its own terms. You change nothing — whoever wrote the part fixes. + +Seven lenses, in this order: + +1. **Plan fidelity.** Every step of the plan is in the diff; nothing beyond it — scope creep is a finding. Where the plan names a source to mirror («same as X»), open X and compare letter for letter: a dropped line, a changed constant, a substituted color source is a finding. The contract with sibling parts — export names, API shapes, versions — matches the plan exactly. +2. **Correctness of the changed lines.** Logic errors, mismatched types, boundary and repeat cases: empty input, zero, a reset or re-entry arriving inside a window the code opens. +3. **Interactions.** For every piece of state, timer, event or global the diff touches, grep for its other readers and writers and check the change survives each path. Name the paths you checked — the unchecked interaction is where escaped bugs live. +4. **Silent failures.** Swallowed errors, broad catches, empty catch blocks, fallbacks that mask a failure instead of surfacing it. +5. **Needless complexity.** Dead branches the diff added, speculative parameters and options nothing needs, indirection with a single caller, an abstraction that fits its cases badly and should be inlined. A longer obvious version beats a shorter clever one — the clever one is the finding. +6. **Docs and public contracts.** Statements in the repository's own docs — README, docs/, a changelog it keeps — and in the level's entry in home (`INDEX.md`) that the diff made false or incomplete; a renamed or reshaped public export whose consumers or docs were not updated. +7. **Weakened checks.** A test skipped or deleted, a lint-ignore added, an assertion lowered to get green — a finding, never a pass. + +Also findings: comments added in code (the repository keeps none), non-English commit messages. + +Discipline: + +- Before reporting a finding, try to refute it — read the code that would make it wrong. Report only findings you are confident are real: a doubtful finding wastes a fix loop. +- Changed lines only. Pre-existing issues, style preferences, and anything the project's linter, typechecker or CI already catches are not findings. +- You may be called once more on the fixed diff: same bar, no new lenses invented to stay busy. +- Bash is for reading: git diff, git log, git show, grep, sed -n, cat. Never a command that writes a file, the index, a ref or a remote. + +Your final answer is the structured output your schema defines; prose outside it is not read. `blocking` is what must be fixed before the merge, `advice` what the lead weighs. `status` is `changes_required` when at least one finding is `blocking`, else `approved`; no findings — `approved` with an empty list, without invented nitpicks. Each finding: `severity`, `lens` (1–7; null for the two extra findings), `file`, `line` (null when the finding has no line), `problem`, `evidence`, `fix`. diff --git a/integrations/pi-flow/scout.md b/integrations/pi-flow/scout.md new file mode 100644 index 0000000..8ab0e63 --- /dev/null +++ b/integrations/pi-flow/scout.md @@ -0,0 +1,24 @@ +--- +name: scout +description: Reconnaissance for a plan — takes a task or a problem and a project, reads the clone and the project's knowledge in mypi home, returns facts with sources, the localization of the change, the simplest path and at most three forks with recommendations. Read-only, changes nothing. +tools: read, grep, find, ls, bash +acceptanceRole: read-only +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"asked":{"type":"string"},"solvable":{"type":"boolean"},"missing":{"type":"array","items":{"type":"string"}},"facts":{"type":"array","items":{"type":"object","properties":{"fact":{"type":"string"},"source":{"type":"string"}},"required":["fact","source"],"additionalProperties":false}},"localization":{"type":"array","items":{"type":"object","properties":{"repository":{"type":"string"},"file":{"type":"string"},"symbols":{"type":"array","items":{"type":"string"}},"change":{"type":"string"}},"required":["repository","file","symbols","change"],"additionalProperties":false}},"simplestPath":{"type":"string"},"assumptions":{"type":"array","items":{"type":"string"}},"forks":{"type":"array","maxItems":3,"items":{"type":"object","properties":{"question":{"type":"string"},"options":{"type":"array","minItems":2,"maxItems":4,"items":{"type":"string"}},"why":{"type":"string"}},"required":["question","options","why"],"additionalProperties":false}}},"required":["asked","solvable","missing","facts","localization","simplestPath","assumptions","forks"],"additionalProperties":false} +--- + +You take a task someone else wrote — or a problem the engineer brought — and find out whether it can be solved with what is given, and by what path. Your task carries the task's text and comments (you have no tracker access) and names the project: its clone at the path the task names, its glossary and ADRs at `.yoke/` in that clone when it has them, its mypi context (knowledge entries `INDEX.md`, task cards with `source.md`) in the project's folder in home that the task names. Read all three before answering. + +Answer, in this order: + +- **What is asked?** (`asked`) Restate it in one sentence. A task that cannot be restated in one sentence is the first gap. +- **Is it solvable with what is given?** (`solvable`, `facts`) Establish the facts from primary sources: the affected repositories and their state, the code paths involved, configs, versions, related PRs and commits. Read, run read-only, check — do not ask what you can look up. Each fact names its source: file:line, commit or command output. Not solvable → `solvable: false`, `missing` lists exactly what is missing, and stop there. No plan gets written around a hole. +- **Where exactly?** (`localization`) Localize the change: repository, file, the functions or symbols, and what changes there. Localization is the most valuable thing reconnaissance produces — the plan is written from it. +- **What is the simplest path?** (`simplestPath`) Pick the simplest solution that solves the task as written. Complexity must trace back to the task's own requirements, never to speculation about the future. +- **Where are the forks?** (`forks`) Mark the places where the description is ambiguous or several viable paths genuinely diverge. Each fork is a question with 2–4 options, your recommended option first, and `why` — three at most: a fork earns a question only when the interpretations differ in ways that matter and no reasonable default exists. Below that bar, decide yourself and record the choice in `assumptions`. + +Never a fork: extra logs, extra checks, release, versioning, merging — anything that does not change the solution itself. + +You change no code, write no files and create no tasks. Bash is for reading only: git log, git show, git ls-remote, git rev-parse, grep, sed -n, cat. Never a command that writes a file, the index, a ref, a remote, a task or a derive artifact. Ask the lead (`contact_supervisor`) only when your task cannot be read as given. + +Your final answer is the structured output your schema defines; prose outside it is not read. diff --git a/integrations/pi-flow/tester.md b/integrations/pi-flow/tester.md new file mode 100644 index 0000000..9c1005b --- /dev/null +++ b/integrations/pi-flow/tester.md @@ -0,0 +1,20 @@ +--- +name: tester +description: Live acceptance of one task on a disposable stand by the project's stand procedure — writes the checklist from the plan's Acceptance before the start, checks each item independently, saves the evidence in the task's folder, tears the stand down; reports each item with its evidence. +tools: read, grep, find, ls, bash, edit, write +acceptanceRole: writer +inheritProjectContext: true +outputSchema: {"type":"object","properties":{"passed":{"type":"boolean"},"checklist":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"expect":{"type":"string"},"observed":{"type":"string"},"passed":{"type":"boolean"},"evidence":{"type":"string"}},"required":["item","expect","observed","passed","evidence"],"additionalProperties":false}},"turns":{"type":"integer","minimum":0},"openQuestions":{"type":"array","items":{"type":"string"}}},"required":["passed","checklist","items","turns","openQuestions"],"additionalProperties":false} +--- + +You run the live acceptance of one task. Your task names the plan (its Acceptance lists what to check), the commit to test, the project's stand procedure (for mypi `docs/MANUAL-ACCEPTANCE.md` in the engine root; for another project the stand document in its folder in mypi home), the task's folder in mypi home and the budget of model turns. + +- Before the start, write the checklist into the task's folder: one line per Acceptance item with its trigger, the action or command, and the expected observation. +- Build the stand exactly as the procedure says: disposable, outside the installation's `projects/`, with its own state; never the installed engine, the engineer's home or the personal agent settings. +- Check each item independently: from the stand's own state (files, the CLI, command output), not from what an agent on the stand says. Save each piece of evidence as a file in the task's folder. +- Count the model turns the stand spends; start messages count. Reaching the budget: stop, record what is done and put the overrun in `openQuestions` — never continue past it. +- Known pitfalls: Herdr restores a stand session's layout from a past run; tabs opened by `/open` load the engineer's personal MCP servers. +- Teardown, always, also after a failure: kill the stand's processes by its `XDG_STATE_HOME`, never `pkill -f` by a path (it kills your own shell), then stop and delete the stand's Herdr session, and only then remove the stand directory. +- A failed item is reported, never repaired. A decision the procedure does not settle: ask the lead with `contact_supervisor`. + +Your final answer is the structured output your schema defines; prose outside it is not read: `passed` — every item passed; `checklist` — the checklist's path; `items` — per checklist line `item`, `expect`, `observed`, `passed`, `evidence` (the evidence file's path in the task's folder); `turns` — the model turns spent; `openQuestions`. diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts index 2e0883f..4908ba5 100644 --- a/test/fast/pi-flow.test.ts +++ b/test/fast/pi-flow.test.ts @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; +import { readFileSync, readdirSync } from 'node:fs'; const root = new URL('../../../', import.meta.url); const read = (path: string) => readFileSync(new URL(path, root), 'utf8'); @@ -10,3 +10,56 @@ test('.pi/settings.json starts pi-subagents children without ambient extensions subagents: { defaultExtensions: [], agentScanDirs: ['integrations/pi-flow', 'home/pi-flow'] }, }); }); + +const ROLES: Record = { + challenger: { tools: 'read, grep, find, ls, bash', acceptanceRole: 'read-only' }, + executor: { tools: 'read, grep, find, ls, bash, edit, write', acceptanceRole: 'writer' }, + investigator: { tools: 'read, grep, find, ls, bash', acceptanceRole: 'read-only' }, + planner: { tools: 'read, grep, find, ls, write', acceptanceRole: 'writer' }, + reviewer: { tools: 'read, grep, find, ls, bash', acceptanceRole: 'read-only' }, + scout: { tools: 'read, grep, find, ls, bash', acceptanceRole: 'read-only' }, + tester: { tools: 'read, grep, find, ls, bash, edit, write', acceptanceRole: 'writer' }, +}; + +function frontmatter(text: string): Map { + const match = /^---\n([\s\S]*?)\n---\n/.exec(text); + assert(match !== null, 'frontmatter'); + const fields = new Map(); + for (const line of (match[1] ?? '').split('\n')) { + const colon = line.indexOf(':'); + assert(colon > 0, line); + fields.set(line.slice(0, colon).trim(), line.slice(colon + 1).trim()); + } + return fields; +} + +function objects(schema: unknown): Record[] { + if (Array.isArray(schema)) return schema.flatMap(objects); + if (schema === null || typeof schema !== 'object') return []; + const node = schema as Record; + return [...('properties' in node ? [node] : []), ...Object.values(node).flatMap(objects)]; +} + +test('integrations/pi-flow holds exactly the seven engine roles, each with its tools, acceptance role, project context and a closed one-line answer schema, and no model or extensions', () => { + const files = readdirSync(new URL('integrations/pi-flow/', root)).filter(file => file.endsWith('.md')).sort(); + assert.deepEqual(files, Object.keys(ROLES).sort().map(role => role + '.md')); + for (const [role, expected] of Object.entries(ROLES)) { + const fields = frontmatter(read(`integrations/pi-flow/${role}.md`)); + assert.equal(fields.get('name'), role); + assert.ok((fields.get('description') ?? '').length > 0, role); + assert.equal(fields.get('tools'), expected.tools, role); + assert.equal(fields.get('acceptanceRole'), expected.acceptanceRole, role); + assert.equal(fields.get('inheritProjectContext'), 'true', role); + assert.equal(fields.has('model'), false, role); + assert.equal(fields.has('extensions'), false, role); + const schema = JSON.parse(fields.get('outputSchema') ?? ''); + assert.equal(schema.type, 'object', role); + for (const node of objects(schema)) { + assert.equal(node.additionalProperties, false, role); + const keys = Object.keys(node.properties as object); + for (const key of (node.required as string[] | undefined) ?? []) assert(keys.includes(key), `${role}: ${key}`); + } + } + const reviewer = JSON.parse(frontmatter(read('integrations/pi-flow/reviewer.md')).get('outputSchema') ?? ''); + assert.deepEqual(reviewer.properties.status.enum, ['approved', 'changes_required']); +}); From e59a0fdf8d4821a1aef0b9684df4abdda50e2f65 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:01:34 +0100 Subject: [PATCH 03/48] Add the Pi workflow templates standard, parallel and investigate --- integrations/pi-flow/investigate.js | 19 ++++++++++++ integrations/pi-flow/parallel.js | 47 +++++++++++++++++++++++++++++ integrations/pi-flow/standard.js | 46 ++++++++++++++++++++++++++++ test/fast/pi-flow.test.ts | 11 +++++++ 4 files changed, 123 insertions(+) create mode 100644 integrations/pi-flow/investigate.js create mode 100644 integrations/pi-flow/parallel.js create mode 100644 integrations/pi-flow/standard.js diff --git a/integrations/pi-flow/investigate.js b/integrations/pi-flow/investigate.js new file mode 100644 index 0000000..83d0a86 --- /dev/null +++ b/integrations/pi-flow/investigate.js @@ -0,0 +1,19 @@ +/* + * investigate — read-only fan-out: a scout per task or an investigator per question, + * all at once; nothing is written. + * Parameters: + * args.items — [{ key, agent, task }]: key — a short unique name (letters, digits, "-"), + * agent — a read-only role, "scout" for a task or "investigator" for a + * question (default "investigator"), task — the full task text + * Returns [{ key, agent, ok, answer }] in the order of args.items; answer is the role's + * structured output or null. + */ +const results = await runs.all( + args.items.map((item) => ({ key: item.key, agent: item.agent || "investigator", task: item.task })), +); +return results.map((result, index) => ({ + key: args.items[index].key, + agent: args.items[index].agent || "investigator", + ok: result.ok, + answer: result.structuredOutput ?? null, +})); diff --git a/integrations/pi-flow/parallel.js b/integrations/pi-flow/parallel.js new file mode 100644 index 0000000..90c1ea1 --- /dev/null +++ b/integrations/pi-flow/parallel.js @@ -0,0 +1,47 @@ +/* + * parallel — several parts at once, one worktree per part; each part runs the chain of + * standard.js: its executor implements, a fresh reviewer checks the diff, blocking + * findings go back to the same executor through resume, at most two review rounds. + * The project's checks stay with the lead. + * Parameters: + * args.parts — [{ key, agent, plan, worktree, taskDir, base }]: key — a short unique + * part name (letters, digits, "-"), agent — role that implements + * (default "executor"), plan, worktree, taskDir — absolute paths, + * base — the branch the part's worktree was created from + * args.reviewer — role that reviews (default "reviewer") + * Returns [{ key, executor: { runId, report }, fixes, reviews }] in the order of args.parts. + */ +const reviewerRole = args.reviewer || "reviewer"; +async function chain(part) { + const context = `Part ${part.key}. Plan: ${part.plan}. Worktree: ${part.worktree}. Task folder: ${part.taskDir}. Base branch: ${part.base}.`; + const implement = await runs.run(`${part.key}-implement`, { + agent: part.agent || "executor", + task: `${context} Implement every step of the plan in the worktree.`, + }); + const report = implement.structuredOutput ?? null; + const base = report && report.base ? report.base : part.base; + let head = report && report.head ? report.head : "HEAD"; + let last = implement; + const fixes = []; + const reviews = []; + for (const round of [1, 2]) { + const review = await runs.run(`${part.key}-review-${round}`, { + agent: reviewerRole, + task: `${context} Review the diff ${base}..${head} in the worktree against the plan.`, + }); + const verdict = review.structuredOutput ?? null; + reviews.push({ round, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [] }); + if (!verdict || verdict.status !== "changes_required" || round === 2 || !last.runId) break; + const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); + const fix = await runs.run(`${part.key}-fix-${round}`, { + resume: last.runId, + task: `Review round ${round} returned these blocking findings: ${JSON.stringify(blocking)}. Fix them in the worktree, commit, and answer again with your whole report.`, + }); + const fixReport = fix.structuredOutput ?? null; + fixes.push({ round, ok: fix.ok, report: fixReport }); + head = fixReport && fixReport.head ? fixReport.head : "HEAD"; + last = fix; + } + return { key: part.key, executor: { runId: last.runId ?? null, report }, fixes, reviews }; +} +return await Promise.all(args.parts.map(chain)); diff --git a/integrations/pi-flow/standard.js b/integrations/pi-flow/standard.js new file mode 100644 index 0000000..d4fa3b5 --- /dev/null +++ b/integrations/pi-flow/standard.js @@ -0,0 +1,46 @@ +/* + * standard — one executor implements a task's plan in its worktree; a fresh reviewer + * checks the diff; blocking findings go back to the same executor through resume; + * at most two review rounds. The project's checks stay with the lead. + * Parameters: + * args.key — task key, e.g. "MP-116" + * args.plan — absolute path of the task's plan.md + * args.worktree — absolute path of the task's worktree + * args.taskDir — absolute path of the task's folder in mypi home + * args.base — the branch the worktree was created from + * args.executor — role that implements (default "executor") + * args.reviewer — role that reviews (default "reviewer") + * Returns { executor: { runId, report }, fixes: [{ round, ok, report }], reviews: [{ round, status, findings }] }. + */ +const executorRole = args.executor || "executor"; +const reviewerRole = args.reviewer || "reviewer"; +const context = `Task ${args.key}. Plan: ${args.plan}. Worktree: ${args.worktree}. Task folder: ${args.taskDir}. Base branch: ${args.base}.`; +const implement = await runs.run("implement", { + agent: executorRole, + task: `${context} Implement every step of the plan in the worktree.`, +}); +const report = implement.structuredOutput ?? null; +const base = report && report.base ? report.base : args.base; +let head = report && report.head ? report.head : "HEAD"; +let last = implement; +const fixes = []; +const reviews = []; +for (const round of [1, 2]) { + const review = await runs.run(`review-${round}`, { + agent: reviewerRole, + task: `${context} Review the diff ${base}..${head} in the worktree against the plan.`, + }); + const verdict = review.structuredOutput ?? null; + reviews.push({ round, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [] }); + if (!verdict || verdict.status !== "changes_required" || round === 2 || !last.runId) break; + const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); + const fix = await runs.run(`fix-${round}`, { + resume: last.runId, + task: `Review round ${round} returned these blocking findings: ${JSON.stringify(blocking)}. Fix them in the worktree, commit, and answer again with your whole report.`, + }); + const fixReport = fix.structuredOutput ?? null; + fixes.push({ round, ok: fix.ok, report: fixReport }); + head = fixReport && fixReport.head ? fixReport.head : "HEAD"; + last = fix; +} +return { executor: { runId: last.runId ?? null, report }, fixes, reviews }; diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts index 4908ba5..6adfeaa 100644 --- a/test/fast/pi-flow.test.ts +++ b/test/fast/pi-flow.test.ts @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync } from 'node:fs'; +import { Script } from 'node:vm'; const root = new URL('../../../', import.meta.url); const read = (path: string) => readFileSync(new URL(path, root), 'utf8'); @@ -63,3 +64,13 @@ test('integrations/pi-flow holds exactly the seven engine roles, each with its t const reviewer = JSON.parse(frontmatter(read('integrations/pi-flow/reviewer.md')).get('outputSchema') ?? ''); assert.deepEqual(reviewer.properties.status.enum, ['approved', 'changes_required']); }); + +test('integrations/pi-flow holds the three workflow templates, each opening with its header comment and compiling as a workflow body', () => { + const files = readdirSync(new URL('integrations/pi-flow/', root)).filter(file => file.endsWith('.js')).sort(); + assert.deepEqual(files, ['investigate.js', 'parallel.js', 'standard.js']); + for (const file of files) { + const source = read(`integrations/pi-flow/${file}`); + assert(source.startsWith('/*'), file); + assert.doesNotThrow(() => new Script('(async (runs, args) => {\n' + source + '\n})'), file); + } +}); From ff03900a21290ca90985bad93fc191e562c1ac97 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:02:03 +0100 Subject: [PATCH 04/48] Drop the /review command from PLAN-FORMAT and add the Execution section --- docs/PLAN-FORMAT.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/PLAN-FORMAT.md b/docs/PLAN-FORMAT.md index b280dc8..7e05d4e 100644 --- a/docs/PLAN-FORMAT.md +++ b/docs/PLAN-FORMAT.md @@ -1,6 +1,6 @@ # PLAN-FORMAT — the shape of a plan -One format for every plan, whoever writes it — `/plan`, or `/review` (a rework plan). `/do` executes the plan literally and the reviewer compares the diff against it, so what the plan does not say is not in the ticket. The modes reference this file and do not restate it. +One format for every plan, whoever writes it — `/plan`, also when it reworks a plan after remarks. `/do` executes the plan literally and the reviewer compares the diff against it, so what the plan does not say is not in the ticket. The modes reference this file and do not restate it. ## Sections, in order @@ -30,6 +30,15 @@ When the ticket changes documentation or a level's map (its entry `INDEX.md` in In a rework plan, a step exists per remark: the quote, the located cause — files and lines, because a fix guessed at a symptom is not a plan item — the concrete change, and the check that proves it fixed. +### Execution + +Only in a plan the Pi `/flow` runs, and there mandatory; a plan for Claude Code omits the section. It tells the lead how to run the steps: + +- the workflow template: its path in `integrations/pi-flow/` or `home/pi-flow/`, chosen by the header comment of its `*.js`, and its `args`; when no template fits, the order of the runs in words, which the lead runs as a one-off script; +- the role for each step or group of steps: a role from `integrations/pi-flow/` or `home/pi-flow/`, chosen by its `description`; +- the self-test: the Acceptance items the `tester` role checks live, or «none»; +- the stand: the project's stand procedure the self-test follows, or «none». + ### Assumptions Choices the plan's author made without asking, for the engineer to override. An assumption names the choice and the reason — it is a decision on record, not a question in disguise. @@ -40,7 +49,7 @@ What this ticket deliberately does not change. The line that keeps `/do` from wi ### Acceptance -What the engineer will see and check on the stand at `/review`: observable behavior with its trigger — «когда X — Y видно на стенде» — never internals, never «работает корректно». One list, used twice: `/do` proves these technically, `/review` walks them with the engineer. +What the engineer will see and check on the stand at the engineer's manual check before `/ship`: observable behavior with its trigger — «когда X — Y видно на стенде» — never internals, never «работает корректно». One list, used twice: `/do` proves these technically, the engineer walks them at the manual check before `/ship`. ## The bar From 668192d5c4c19b8860ca39d55cc8babf3b6f7f3e Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:02:36 +0100 Subject: [PATCH 05/48] Name the Pi roles in PIPELINE and correct the child PI_SESSION_ID --- docs/PIPELINE.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index e1a8a5f..af0a061 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -4,7 +4,8 @@ Tooling for running the tasks of any registered project from the engine root; ev session starts there, so each pipeline skill checks the session scope first (see «Session scope»). It is tracked in the engine repository: this file, `docs/PLAN-FORMAT.md`, the scripts in `scripts/pipeline/`, the skills `/plan`, `/do`, `/epic`, `/flow`, `/ship`, `/board` in -`.claude/skills/` and the roles in `.claude/agents/` (copied from yokemate; `plan-challenger` is the pipeline's own). Claude +`.claude/skills/` and the roles in `.claude/agents/` (copied from yokemate; `plan-challenger` is the pipeline's own). For Pi, the roles and workflow templates in `integrations/pi-flow/`, +found through `.pi/settings.json` («Roles»). Claude Code loads the skills and roles when it starts in the engine root; the scripts act on the installation they are run from and refuse any other working directory. @@ -68,8 +69,12 @@ The key: `scripts/pipeline/mp-task scope` prints `{"key", "project", "org", "cod `org` is the key when it is the slug of a registered organization with at least one project, and `codes` the sorted codes of its projects; otherwise both are `null`. It takes the `sessions` row with this conversation's agent and native id -(`CLAUDE_CODE_SESSION_ID`, else `PI_SESSION_ID`; it is set inside subagents too and -is the parent chat's id), else `MYPI_SCOPE` (set by `/open`), else `global`. This is +(`CLAUDE_CODE_SESSION_ID`, else `PI_SESSION_ID`), else `MYPI_SCOPE` (set by +`/open`), else `global`. Inside a Claude Code subagent `CLAUDE_CODE_SESSION_ID` is +the parent chat's id. Inside a pi-subagents child `PI_SESSION_ID` is the child's +own id, which has no `sessions` row, so a child's scope falls back to +`MYPI_SCOPE`, then `global`; the Pi lead therefore names the key and the paths in +each role's task. This is the order of `warmup` without an argument, except the live record by pid: through `mise exec` the pid does not match, and `MYPI_SCOPE`, which `/open` sets to the same key, takes its place. The subcommand only reads (`mypi sessions`); neither `warmup` @@ -250,6 +255,20 @@ What every executor prompt carries: the task key, the plan path, the worktree pa the task folder path, the base branch, this file's path, and the rule that its final message is exactly one JSON object per its definition. Under `/epic` the epic's folder and the clone's SHA take the place of the worktree and the base branch («Epics»). +Pi's roles live in `integrations/pi-flow/`: `scout`, `planner`, `executor`, +`reviewer`, `tester`, `investigator`, `challenger`, each a `*.md` file whose +answer schema is its `outputSchema`, beside the workflow templates `standard.js` +(implement, review, fix through `resume`, at most two review rounds), +`parallel.js` (the same chain for several worktrees at once) and `investigate.js` +(read-only fan-out of scouts or investigators). `.pi/settings.json` makes +pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (the engineer's own +roles and templates) and starts children without ambient extensions. The bodies +follow the Claude roles above without their defects: the executor reports the +worktree's real branch name and separates `openQuestions` from `deviations`, each +check with its expected exit code; the planner saves `plan.md` in the task's +folder and writes the plan's «Execution» section. File formats, own roles and +model tiers: `docs/agents/agent-integration.md`. + ## Statuses in mypi `new → planning → in_progress → review → merged → done`, plus `blocked`. From c713e1393c69f303674e024cd07eb0b06862e3f3 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:03:11 +0100 Subject: [PATCH 06/48] Describe the Pi role and template formats, own roles and model tiers --- docs/agents/agent-integration.md | 44 +++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/agents/agent-integration.md b/docs/agents/agent-integration.md index 1fa3629..adca8e2 100644 --- a/docs/agents/agent-integration.md +++ b/docs/agents/agent-integration.md @@ -1,6 +1,6 @@ # Claude Code and Pi integration -Read when: starting, configuring or diagnosing Claude Code or Pi in this checkout — hooks, the Pi extension, archive capture, MCP registration, the allow list, skills, personal agent settings. +Read when: starting, configuring or diagnosing Claude Code or Pi in this checkout — hooks, the Pi extension, archive capture, MCP registration, the allow list, skills, personal agent settings, the Pi pipeline roles and workflow templates. ## Pi @@ -34,6 +34,35 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou `enabled: false`; `pi -ne` starts without extensions. Shell `pi mcp list` does not load extension servers. After updating/rebuilding, reload or restart Pi. Inspect diagnostics before claiming an integration works. +- Pi pipeline resources: the tracked `.pi/settings.json` sets pi-subagents' + `defaultExtensions: []` (children start without ambient extensions, mypi + included) and `agentScanDirs` to `integrations/pi-flow` and `home/pi-flow`, + resolved from the engine root, where Pi starts. `integrations/pi-flow/` holds + the engine's roles (`*.md`) and workflow templates (`*.js`), `home/pi-flow/` the + engineer's own for all projects; subfolders are scanned too. pi-subagents reads + only `*.md`, so the templates are not roles; `subagent({ action: "list" })` + lists the roles. In a Pi session started in the engine root the engine roles + `scout` and `reviewer` shadow pi-subagents' builtin roles of the same names, also + inside its builtin `/parallel-review` and `/review-loop`. +- A role file: frontmatter of flat `key: value` lines, then the body. `name` + equals the file's base name; `description` is one line, and the planner picks a + role by it (a role for one project says so); `tools` lists Pi tools comma- + separated — `read, grep, find, ls, bash` for a read-only role, plus + `edit, write` for one that changes files; `acceptanceRole` is `read-only` or + `writer`; `inheritProjectContext: true`; `outputSchema` is the answer's JSON + Schema on one line, an object with `additionalProperties: false`. Never `model` + (models are personal, see «Personal customization») and never `extensions` + (`extensions: []` parses as a path; `defaultExtensions` covers every role). The + body: the input, what to do, what never, when to ask the lead + (`contact_supervisor`, only at a real fork), the answer. A file without `name` or + `description` is skipped silently; invalid JSON in one `outputSchema` fails + every agent definition. +- A workflow template: a pi-subagents workflow script (`runs.run`, `runs.all`, + `resume`; no filesystem, shell or Pi tools inside) that opens with a `/* … */` + header naming what it does and each `args` parameter. Run it as + `subagent({ workflow: "integrations/pi-flow/standard.js", args })`; check it + without launching children with `subagent({ action: "validate", workflow: … })`, + not with bare `node --check`, which rejects the top-level `await` and `return`. ## Claude Code @@ -74,3 +103,16 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou not by editing tracked `.pi/` or `integrations/` files; do not rely on duplicate-name discovery order. Secrets belong in environment variables or credential storage, not committed JSON. +- Own Pi roles and workflow templates live in `home/pi-flow/`, in the formats of + «Pi» above, written with ordinary file tools, each with a line in the home map + `home/INDEX.md`. Their names differ from every role in `integrations/pi-flow/`: + the order between same-name roles of the two scan roots is not a contract. +- Model tiers of the Pi roles are personal: `~/.pi/agent/settings.json` → + `subagents.agentOverrides.` with `model` and `thinking`, never the tracked + `.pi/settings.json` (project overrides beat personal ones). Tier 1 (fast): + `scout`, `investigator`; tier 2 (standard): `executor`, `tester`; tier 3 (deep, + bounded): `reviewer`, `challenger`; tier 4 (intent): `planner`. A tier whose + model comes from a provider extension (for example litellm) also needs + `"subagentOnlyExtensions": [""]` in the same + override: under `defaultExtensions: []` the role fails before its first turn + without it. From 74af105e1781eb2aa7c15b7fec4f0926cc2974d0 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:03:42 +0100 Subject: [PATCH 07/48] Name the Pi pipeline resources in README, engine development and testing docs --- README.md | 3 +++ docs/TESTING.md | 1 + docs/agents/engine-development.md | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd4fe0f..18c8003 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ any more: warmup serves `home/USER-INSTRUCTIONS.md`. Backups keep accepting the | Personal agent instructions | `home/USER-INSTRUCTIONS.md` | | Personal mypi settings (`defaultAgent`) | `home/settings.json` | | Pi settings, MCP servers, packages, skills, extensions, prompts, themes | `~/.pi/agent/` | +| Own Pi pipeline roles and workflow templates | `home/pi-flow/` | | Project knowledge | `home/projects///` | | Project source code | `projects///`, its own Git | | Task worktrees | `projects//---/`, created by `mypi task worktree ` | @@ -467,7 +468,9 @@ mypi/ ├── scripts/ # bounded build/checks ├── integrations/prompts/ # warmup role and how-to texts ├── integrations/skills/ # built-in skills: open, inbox, project-management, retro +├── integrations/pi-flow/ # Pi pipeline roles (*.md) and workflow templates (*.js) ├── .pi/ # tracked Pi configuration +│ ├── settings.json # pi-subagents: no ambient extensions, role scan roots │ ├── extensions/mypi.ts # MCP registration and warmup per session │ └── skills -> ../integrations/skills ├── .claude/ # tracked Claude Code configuration diff --git a/docs/TESTING.md b/docs/TESTING.md index 86a2cc7..e1e5a41 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -129,6 +129,7 @@ This is a placement guide, not a manual per-file catalog or eight separate runne | Project flow | Parsing and validation of flow.json; MCP/CLI translation; the warmup's flow block | `project flow` on a temp clone: defaults, the file, refusals | | CLI/bootstrap | In-process parsing/dispatch of every command | Launcher, exit/stdout/stderr and required save→reread path without Python | | Agent docs | AGENTS.md core within 8,000 characters; every link, anchor and repository path in AGENTS.md and docs/**/*.md exists (test/fast/agent-docs.test.ts) | — | +| Pi pipeline resources | .pi/settings.json equals its expected object; integrations/pi-flow holds exactly the seven roles with name, description, tools, acceptance role, project context, no model or extensions, and a closed one-line outputSchema (reviewer status approved or changes_required); the three workflow templates open with a header and compile as a workflow body (test/fast/pi-flow.test.ts) | Stand with a real Pi: subagent list, template validation, one reviewer run | | Prompts | how-to within 2,200 characters, every role within 1,300; the knowledge concepts paragraph in every level's warmup (test/fast/prompts.test.ts, test/fast/warmup.test.ts) | — | | HTTP API | Pure handler: auth, routing, validation, cursor paging, response shapes; keyset pages of tasks, events and journal; data_version feed; token file | One loopback serve subprocess: 401/403, a cursor read, a write, changeId and SSE after it, access log without secrets | | Web UI | Envelope unwrapping, screen filters and API paths, generic field display, change tracking; session cookie rules in the pure handler; static asset resolution | The same loopback serve subprocess: index, asset and SPA fallback, cookie login with HttpOnly/SameSite/Secure, read-only cookie, an SSE stream over the cookie, revocation ends it | diff --git a/docs/agents/engine-development.md b/docs/agents/engine-development.md index 9adeab2..dc0206c 100644 --- a/docs/agents/engine-development.md +++ b/docs/agents/engine-development.md @@ -6,7 +6,9 @@ Read when: changing mypi source, tests, scripts or shipped integrations, buildin - Engine resources are tracked in this repository: agent configuration in `.pi/` (the Pi extension `.pi/extensions/mypi.ts`; `.pi/skills` points to the built-in - skills in `integrations/skills/`) and agent texts in `integrations/prompts/`. + skills in `integrations/skills/`; `.pi/settings.json` sets the pi-subagents scan + roots), agent texts in `integrations/prompts/` and the Pi pipeline roles and + workflow templates in `integrations/pi-flow/`. Personal data lives in `home/`; personal agent settings live in each agent's user-level configuration (for Pi, `~/.pi/agent/`). Never start tracking `home/` or `projects/` in the engine repository, including in future updates. From b1d56a0cc601b40c364c7b41a46a76642a867b33 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:03:53 +0100 Subject: [PATCH 08/48] Record MP-116 in the changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee9bba..c2041c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ from existing transcripts. MEMORY.md is no longer read; move facts into INDEX.md - Journal in the warmup (MP-91): a project warmup ends its tail with «Журнал» — the number of journal entries of the project and all its tasks with the local date of the newest one («Записей в журнале проекта нет.» when none) — and a task warmup with the task's last five entries in the journal line format (date, time, type, text up to 300 characters, artifacts; «Записей в журнале задачи нет.» when none); both name the `journal` call with `limit` 20 and its CLI form. Over the budget a task warmup drops the shown entries from the oldest before clipping knowledge entries, and after the knowledge entries clips the end of its card with a pointer to `context_read`, so a long card no longer pushes the Claude hook past 10,000 characters; the project line, the task heading with its count and the loader are never cut. The counts come from one SQL statement of the journal store (`stats`, `last`), not from a scope-resolving read, so the Claude hook stays fast. - Organization scope and chat (MP-78): the bare organization slug (`tb-ff`) is a session key. `/open tb-ff` (MCP `open_session`, CLI `mypi open`) opens, focuses or resumes the organization chat in the Herdr tab `org-tb-ff` of the workspace `tb-ff`, in the engine root, with no clone or worktree; `global`, `all`, an unknown organization and an organization without registered projects are refused. The organization chat leads the tasks of all registered projects of its organization: it opens their task chats and sends them parent messages directly. Its warmup (role `org`) carries the global and organization knowledge entries, one line per project and the events addressed to it; its start message lists each project's open tasks by status. Pi completes `/open` with the organization's projects and their open tasks; the pipeline skills accept the organization scope (`mp-task scope` adds `org` and `codes`). - Skill `/epic` and role `plan-challenger` (MP-107: MP-109): `/epic ` reads the epic's step from its status and folder (`docs/PIPELINE.md`, «Epics») and leads research, one review round, publication, an independent check by the read-only role `plan-challenger`, one question per finding with its decision row, version 2 and the subtasks created with `--parent`; the answer «Завести и запустить /flow» runs `/flow `. Claude Code only, through `.claude/skills/epic` and `.claude/agents`. +- Pi pipeline roles and templates (MP-116, epic MP-68): `integrations/pi-flow/` holds the roles `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator` and `challenger`, each with its answer schema in `outputSchema`, and the workflow templates `standard.js` (implement, review, fix through `resume`, at most two review rounds), `parallel.js` and `investigate.js` (read-only fan-out). The tracked `.pi/settings.json` makes pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (own roles and templates) and start children without ambient extensions; in Pi sessions in the engine root `scout` and `reviewer` shadow the pi-subagents builtins of those names. `docs/agents/agent-integration.md` describes the role and template formats, own roles in `home/pi-flow/` and the personal model tiers. The roles are run by hand with `subagent` until the Pi `/flow` arrives; `.claude/agents` is unchanged. ### Changed @@ -33,6 +34,7 @@ from existing transcripts. MEMORY.md is no longer read; move facts into INDEX.md - Event routing (MP-78): a task has two parents, its project chat and its organization chat. `turn_end` and `waiting` go to the sender of the parent message that started the turn (`mypi event emit --to`), else to the project; a milestone goes to the parent that last sent the task a message, else to the project, unless that recipient makes the change. Refusals of a non-parent sender name both parents. - Sessions tree (MP-78): `mypi sessions` (MCP `sessions`) shows global → org → project → task. - Schema 14 (MP-78): `sessions` gains the scope `org` and the column `org_id`; `mypi db init` is required. `mypi org remove` refuses while an organization session or an event with its slug exists. +- Pipeline texts (MP-116): `docs/PLAN-FORMAT.md` no longer names a `/review` command (a rework plan comes from `/plan`; Acceptance is walked at the manual check before `/ship`) and gains the «Execution» section, mandatory for plans the Pi `/flow` runs and absent from Claude plans; `docs/PIPELINE.md` says a pi-subagents child's `PI_SESSION_ID` is its own id, so its scope falls back to `MYPI_SCOPE`, then `global`. ### Removed From 5883f49156623de47ad5c1ddd5b4ea871de1cf07 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:10:26 +0100 Subject: [PATCH 09/48] Chain the parallel template without nested async functions --- integrations/pi-flow/parallel.js | 72 +++++++++++++++++++------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/integrations/pi-flow/parallel.js b/integrations/pi-flow/parallel.js index 90c1ea1..df7d00d 100644 --- a/integrations/pi-flow/parallel.js +++ b/integrations/pi-flow/parallel.js @@ -12,36 +12,50 @@ * Returns [{ key, executor: { runId, report }, fixes, reviews }] in the order of args.parts. */ const reviewerRole = args.reviewer || "reviewer"; -async function chain(part) { - const context = `Part ${part.key}. Plan: ${part.plan}. Worktree: ${part.worktree}. Task folder: ${part.taskDir}. Base branch: ${part.base}.`; - const implement = await runs.run(`${part.key}-implement`, { - agent: part.agent || "executor", - task: `${context} Implement every step of the plan in the worktree.`, - }); - const report = implement.structuredOutput ?? null; - const base = report && report.base ? report.base : part.base; - let head = report && report.head ? report.head : "HEAD"; - let last = implement; - const fixes = []; - const reviews = []; - for (const round of [1, 2]) { - const review = await runs.run(`${part.key}-review-${round}`, { +function rounds(part, context, state, round) { + return runs + .run(`${part.key}-review-${round}`, { agent: reviewerRole, - task: `${context} Review the diff ${base}..${head} in the worktree against the plan.`, - }); - const verdict = review.structuredOutput ?? null; - reviews.push({ round, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [] }); - if (!verdict || verdict.status !== "changes_required" || round === 2 || !last.runId) break; - const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); - const fix = await runs.run(`${part.key}-fix-${round}`, { - resume: last.runId, - task: `Review round ${round} returned these blocking findings: ${JSON.stringify(blocking)}. Fix them in the worktree, commit, and answer again with your whole report.`, + task: `${context} Review the diff ${state.base}..${state.head} in the worktree against the plan.`, + }) + .then((review) => { + const verdict = review.structuredOutput ?? null; + state.reviews.push({ round, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [] }); + if (!verdict || verdict.status !== "changes_required" || round === 2 || !state.last.runId) return state; + const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); + return runs + .run(`${part.key}-fix-${round}`, { + resume: state.last.runId, + task: `Review round ${round} returned these blocking findings: ${JSON.stringify(blocking)}. Fix them in the worktree, commit, and answer again with your whole report.`, + }) + .then((fix) => { + const fixReport = fix.structuredOutput ?? null; + state.fixes.push({ round, ok: fix.ok, report: fixReport }); + state.head = fixReport && fixReport.head ? fixReport.head : "HEAD"; + state.last = fix; + return rounds(part, context, state, round + 1); + }); }); - const fixReport = fix.structuredOutput ?? null; - fixes.push({ round, ok: fix.ok, report: fixReport }); - head = fixReport && fixReport.head ? fixReport.head : "HEAD"; - last = fix; - } - return { key: part.key, executor: { runId: last.runId ?? null, report }, fixes, reviews }; +} +function chain(part) { + const context = `Part ${part.key}. Plan: ${part.plan}. Worktree: ${part.worktree}. Task folder: ${part.taskDir}. Base branch: ${part.base}.`; + return runs + .run(`${part.key}-implement`, { + agent: part.agent || "executor", + task: `${context} Implement every step of the plan in the worktree.`, + }) + .then((implement) => { + const report = implement.structuredOutput ?? null; + const state = { + report, + base: report && report.base ? report.base : part.base, + head: report && report.head ? report.head : "HEAD", + last: implement, + fixes: [], + reviews: [], + }; + return rounds(part, context, state, 1); + }) + .then((state) => ({ key: part.key, executor: { runId: state.last.runId ?? null, report: state.report }, fixes: state.fixes, reviews: state.reviews })); } return await Promise.all(args.parts.map(chain)); From 4df7429abab93d80381ead60f1509ed15bad856a Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:16:24 +0100 Subject: [PATCH 10/48] Reject async functions in the Pi workflow templates test --- test/fast/pi-flow.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts index 6adfeaa..e90c272 100644 --- a/test/fast/pi-flow.test.ts +++ b/test/fast/pi-flow.test.ts @@ -65,12 +65,13 @@ test('integrations/pi-flow holds exactly the seven engine roles, each with its t assert.deepEqual(reviewer.properties.status.enum, ['approved', 'changes_required']); }); -test('integrations/pi-flow holds the three workflow templates, each opening with its header comment and compiling as a workflow body', () => { +test('integrations/pi-flow holds the three workflow templates, each opening with its header comment, free of nested async functions and compiling as a workflow body', () => { const files = readdirSync(new URL('integrations/pi-flow/', root)).filter(file => file.endsWith('.js')).sort(); assert.deepEqual(files, ['investigate.js', 'parallel.js', 'standard.js']); for (const file of files) { const source = read(`integrations/pi-flow/${file}`); assert(source.startsWith('/*'), file); + assert.doesNotMatch(source, /\basync\b/, file); assert.doesNotThrow(() => new Script('(async (runs, args) => {\n' + source + '\n})'), file); } }); From d5661c9f5f96676d133af8e3ce60a679c91d22f2 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:16:52 +0100 Subject: [PATCH 11/48] Say workflow templates take no nested async functions --- docs/agents/agent-integration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/agents/agent-integration.md b/docs/agents/agent-integration.md index adca8e2..8293756 100644 --- a/docs/agents/agent-integration.md +++ b/docs/agents/agent-integration.md @@ -58,7 +58,8 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou `description` is skipped silently; invalid JSON in one `outputSchema` fails every agent definition. - A workflow template: a pi-subagents workflow script (`runs.run`, `runs.all`, - `resume`; no filesystem, shell or Pi tools inside) that opens with a `/* … */` + `resume`; no filesystem, shell or Pi tools inside; no nested async functions + (plain helpers returning `runs.run(...)` or Promise chains)) that opens with a `/* … */` header naming what it does and each `args` parameter. Run it as `subagent({ workflow: "integrations/pi-flow/standard.js", args })`; check it without launching children with `subagent({ action: "validate", workflow: … })`, From 0a1202ce43692513b5c8bd90d8a5b646224e9ada Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:46:27 +0100 Subject: [PATCH 12/48] Add the mypi_ask tool to the Pi extension --- .pi/extensions/mypi-lib/ask.ts | 56 ++++++++++++++++++++++++++++++ .pi/extensions/mypi-lib/install.ts | 4 +++ test/fast/pi-extension.test.ts | 48 ++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .pi/extensions/mypi-lib/ask.ts diff --git a/.pi/extensions/mypi-lib/ask.ts b/.pi/extensions/mypi-lib/ask.ts new file mode 100644 index 0000000..88866a2 --- /dev/null +++ b/.pi/extensions/mypi-lib/ask.ts @@ -0,0 +1,56 @@ +export interface AskParams { question: string; options: string[] } + +export interface AskContext { + hasUI: boolean; + ui: { + select(title: string, options: string[], opts?: { signal?: AbortSignal }): Promise; + input(title: string, placeholder?: string, opts?: { signal?: AbortSignal }): Promise; + }; +} + +type AskDetails = { answer: string; custom: boolean } | { answer: null; cancelled: true } | { answer: null; error: 'no-ui' }; + +export interface AskTool { + name: string; + label: string; + description: string; + parameters: Record; + executionMode: 'sequential'; + execute(toolCallId: string, params: AskParams, signal: AbortSignal, onUpdate: unknown, ctx: AskContext): + Promise<{ content: { type: 'text'; text: string }[]; details: AskDetails; isError?: boolean }>; +} + +const OWN_WORD = 'Своё слово'; + +export function registerAsk(pi: { registerTool(tool: AskTool): void }): void { + pi.registerTool({ + name: 'mypi_ask', + label: 'mypi ask', + description: 'Ask the engineer one question and wait for the answer. options: 2–4 short labels, the recommended one first; the tool marks it «(рекомендую)» and adds «Своё слово» for a typed answer. Put the facts and one line of reasoning per option into question. Returns {"answer","custom"}; {"answer":null,"cancelled":true} when the engineer cancels — then stop and write nothing more.', + parameters: { + type: 'object', + properties: { + question: { type: 'string', description: 'The question with its facts and the reasoning per option' }, + options: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 4, description: 'Option labels, the recommended one first' }, + }, + required: ['question', 'options'], + additionalProperties: false, + }, + executionMode: 'sequential', + async execute(_toolCallId, params, signal, _onUpdate, ctx) { + if (!ctx.hasUI) return { content: [{ type: 'text', text: 'Error: mypi_ask needs the interactive Pi UI; nothing was asked.' }], + details: { answer: null, error: 'no-ui' }, isError: true }; + const list = [params.options[0] + ' (рекомендую)', ...params.options.slice(1), OWN_WORD]; + const done = (details: AskDetails) => ({ content: [{ type: 'text' as const, text: JSON.stringify(details) }], details }); + const cancelled = done({ answer: null, cancelled: true }); + const choice = await ctx.ui.select(params.question, list, { signal }); + if (choice === undefined) return cancelled; + const index = list.indexOf(choice); + if (index === list.length - 1) { + const typed = (await ctx.ui.input(OWN_WORD + ':', undefined, { signal }))?.trim(); + return typed ? done({ answer: typed, custom: true }) : cancelled; + } + return done({ answer: params.options[index]!, custom: false }); + }, + }); +} diff --git a/.pi/extensions/mypi-lib/install.ts b/.pi/extensions/mypi-lib/install.ts index 0c8b42d..f27c070 100644 --- a/.pi/extensions/mypi-lib/install.ts +++ b/.pi/extensions/mypi-lib/install.ts @@ -1,5 +1,7 @@ import { chmodSync, existsSync, mkdirSync, rmSync, statSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; +import { registerAsk } from './ask.js'; +import type { AskTool } from './ask.js'; import { completionSource, openCompletions, scopeCodes } from './open-completions.js'; import type { CompletionItem, OpenCatalog } from './open-completions.js'; @@ -31,6 +33,7 @@ export interface ExtensionAPI { on(name: 'agent_end', handler: (event: { messages: { role: string; content?: unknown }[] }) => void): void; on(name: 'agent_settled', handler: (event: unknown, ctx: ExtensionContext) => Promise): void; on(name: 'ui_prompt_start', handler: (event: { kind: string; title?: string }) => Promise): void; + registerTool(tool: AskTool): void; } export type RunCli = (args: string[], options: { cwd: string; signal: AbortSignal; onLine: (line: string) => void }) => @@ -262,4 +265,5 @@ export function install(pi: ExtensionAPI, root: string, serve: ServeEvents, runt pi.sendUserMessage(('/skill:inbox ' + args.trim()).trim(), { expandPromptTemplates: true }); }, }); + registerAsk(pi); } diff --git a/test/fast/pi-extension.test.ts b/test/fast/pi-extension.test.ts index 3addab8..9ed6786 100644 --- a/test/fast/pi-extension.test.ts +++ b/test/fast/pi-extension.test.ts @@ -24,6 +24,7 @@ function stub() { const inputs: ((data: string) => { consume?: boolean; data?: string } | undefined)[] = []; const terminal = { unsubscribed: 0 }; const commands = new Map Promise; getArgumentCompletions?: (prefix: string) => Promise }>(); + const tools = new Map(); const registration = () => ({ code: 0, killed: false, stdout: JSON.stringify({ key: state.key, registered: true, displaced: null }) + '\n', stderr: '' }); const state = { id: 's1', key: 'global', idle: true, name: undefined as string | undefined, file: undefined as string | undefined, header: null as { parentSession?: string } | null, result: (_command: string, args: string[]): unknown => args[1] === 'session' ? registration() : { code: 0, killed: false, stdout: '', stderr: '' } }; @@ -34,6 +35,7 @@ function stub() { registerCommand(name: string, command: { description: string; handler: (args: string, ctx: any) => Promise; getArgumentCompletions?: (prefix: string) => Promise }) { commands.set(name, command); }, async exec(command: string, args: string[], options: unknown) { calls.push([command, args, options]); return state.result(command, args); }, sendUserMessage(content: string, options?: unknown) { sent.push([content, options]); }, + registerTool(tool: any) { tools.set(tool.name, tool); }, }; const ctx = { hasUI: true, ui: { notify(message: string, level: string) { notes.push([message, level]); }, setStatus(key: string, text: string | undefined) { statuses.push([key, text]); }, @@ -55,7 +57,7 @@ function stub() { await handlers.get('before_agent_start')?.(event, ctx); return event.systemPromptOptions as { sections: Record; toolGuidelines?: Record }; }; - return { api: api as never, handlers, commands, ctx, servers, removed, calls, sent, notes, statuses, inputs, terminal, state, start, stop, turn, + return { api: api as never, handlers, commands, tools, ctx, servers, removed, calls, sent, notes, statuses, inputs, terminal, state, start, stop, turn, options, registration }; } @@ -66,6 +68,7 @@ test('pi extension registers mypi MCP with the session id and loads warmup into assert.deepEqual(pi.calls, []); assert.deepEqual([...pi.handlers.keys()].sort(), ['agent_end', 'agent_settled', 'agent_start', 'before_agent_start', 'session_shutdown', 'session_start', 'ui_prompt_start']); assert.deepEqual([...pi.commands.keys()], ['open', 'inbox']); + assert.deepEqual([...pi.tools.keys()], ['mypi_ask']); assert.deepEqual(await pi.turn(), {}); pi.state.result = (_command, args) => args[1] === 'session' ? pi.registration() : { code: 0, killed: false, stdout: '# mypi warmup: global\n', stderr: '' }; await pi.start('startup'); @@ -383,6 +386,49 @@ test('pi extension /inbox runs the inbox skill', async () => { assert.deepEqual(pi.calls, []); }); +test('pi extension mypi_ask asks one question with the recommendation first and the engineer\'s own word last', async () => { + const pi = stub(); + install(pi.api, root, serve, undefined); + const tool = pi.tools.get('mypi_ask'); + assert.equal(tool.label, 'mypi ask'); + assert.equal(tool.executionMode, 'sequential'); + assert.equal('exposure' in tool, false); + assert.equal(tool.parameters.properties.options.minItems, 2); + assert.equal(tool.parameters.properties.options.maxItems, 4); + assert.deepEqual(tool.parameters.required, ['question', 'options']); + const ask = async (answers: (string | undefined)[], hasUI = true) => { + const selects: unknown[][] = [], inputs: unknown[][] = []; + const signal = new AbortController().signal; + const ctx = { hasUI, ui: { + async select(...args: unknown[]) { selects.push(args); return answers.shift(); }, + async input(...args: unknown[]) { inputs.push(args); return answers.shift(); }, + } }; + const result = await tool.execute('call-1', { question: 'Q?', options: ['A', 'B'] }, signal, undefined, ctx); + return { result, selects, inputs, signal }; + }; + const recommended = await ask(['A (рекомендую)']); + assert.deepEqual(recommended.result.details, { answer: 'A', custom: false }); + assert.equal(recommended.result.content[0].text, JSON.stringify({ answer: 'A', custom: false })); + assert.equal(recommended.result.isError, undefined); + assert.deepEqual(recommended.selects, [['Q?', ['A (рекомендую)', 'B', 'Своё слово'], { signal: recommended.signal }]]); + assert.deepEqual(recommended.inputs, []); + assert.deepEqual((await ask(['B'])).result.details, { answer: 'B', custom: false }); + const own = await ask(['Своё слово', ' текст ']); + assert.deepEqual(own.result.details, { answer: 'текст', custom: true }); + assert.deepEqual(own.inputs, [['Своё слово:', undefined, { signal: own.signal }]]); + const escape = await ask([undefined]); + assert.deepEqual(escape.result.details, { answer: null, cancelled: true }); + assert.equal(escape.result.content[0].text, JSON.stringify({ answer: null, cancelled: true })); + assert.equal(escape.result.isError, undefined); + assert.deepEqual(escape.inputs, []); + assert.deepEqual((await ask(['Своё слово', undefined])).result.details, { answer: null, cancelled: true }); + assert.deepEqual((await ask(['Своё слово', ' '])).result.details, { answer: null, cancelled: true }); + const headless = await ask(['A (рекомендую)'], false); + assert.equal(headless.result.isError, true); + assert.deepEqual(headless.result.details, { answer: null, error: 'no-ui' }); + assert.deepEqual(headless.selects, []); +}); + test('pi extension /open suggests keys of the session\'s project from the CLI', async () => { const pi = stub(); let clock = 1000; From 6819c10ed0e605045c5ff2a989124fd6fc68f27e Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:47:17 +0100 Subject: [PATCH 13/48] Add the /flow command to the Pi extension --- .pi/extensions/mypi-lib/install.ts | 6 ++++++ test/fast/pi-extension.test.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.pi/extensions/mypi-lib/install.ts b/.pi/extensions/mypi-lib/install.ts index f27c070..313aee9 100644 --- a/.pi/extensions/mypi-lib/install.ts +++ b/.pi/extensions/mypi-lib/install.ts @@ -265,5 +265,11 @@ export function install(pi: ExtensionAPI, root: string, serve: ServeEvents, runt pi.sendUserMessage(('/skill:inbox ' + args.trim()).trim(), { expandPromptTemplates: true }); }, }); + pi.registerCommand('flow', { + description: 'Run tasks of one project to an agreed plan in Pi: /flow […], or /flow to resume', + handler: async args => { + pi.sendUserMessage(('/skill:flow ' + args.trim()).trim(), { expandPromptTemplates: true }); + }, + }); registerAsk(pi); } diff --git a/test/fast/pi-extension.test.ts b/test/fast/pi-extension.test.ts index 9ed6786..da1c3b3 100644 --- a/test/fast/pi-extension.test.ts +++ b/test/fast/pi-extension.test.ts @@ -67,7 +67,7 @@ test('pi extension registers mypi MCP with the session id and loads warmup into assert.equal(pi.servers.length, 0); assert.deepEqual(pi.calls, []); assert.deepEqual([...pi.handlers.keys()].sort(), ['agent_end', 'agent_settled', 'agent_start', 'before_agent_start', 'session_shutdown', 'session_start', 'ui_prompt_start']); - assert.deepEqual([...pi.commands.keys()], ['open', 'inbox']); + assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow']); assert.deepEqual([...pi.tools.keys()], ['mypi_ask']); assert.deepEqual(await pi.turn(), {}); pi.state.result = (_command, args) => args[1] === 'session' ? pi.registration() : { code: 0, killed: false, stdout: '# mypi warmup: global\n', stderr: '' }; @@ -386,6 +386,16 @@ test('pi extension /inbox runs the inbox skill', async () => { assert.deepEqual(pi.calls, []); }); +test('pi extension /flow runs the flow skill', async () => { + const pi = stub(); + install(pi.api, root, serve, undefined); + await pi.commands.get('flow')!.handler('fx FX-1', pi.ctx); + await pi.commands.get('flow')!.handler('', pi.ctx); + assert.deepEqual(pi.sent, [['/skill:flow fx FX-1', { expandPromptTemplates: true }], ['/skill:flow', { expandPromptTemplates: true }]]); + assert.deepEqual(pi.calls, []); + assert.equal(pi.commands.get('flow')!.getArgumentCompletions, undefined); +}); + test('pi extension mypi_ask asks one question with the recommendation first and the engineer\'s own word last', async () => { const pi = stub(); install(pi.api, root, serve, undefined); @@ -457,7 +467,7 @@ test('pi extension /open suggests keys of the session\'s project from the CLI', taskList = tasks; assert.deepEqual((await open.getArgumentCompletions!('') as { label: string }[]).map(item => item.label), ['MP', 'MP-74']); assert.equal(lists(), 3); - assert.deepEqual([...pi.commands.keys()], ['open', 'inbox']); + assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow']); assert.equal(pi.commands.get('inbox')!.getArgumentCompletions, undefined); }); From 9915b90e42cf47b57a45ba71ef506ae4c99940a2 Mon Sep 17 00:00:00 2001 From: Pavel Donin Date: Mon, 9 Nov 2026 16:47:29 +0100 Subject: [PATCH 14/48] Add the Pi-only flow skill up to the agreed plan --- integrations/skills/flow/SKILL.md | 281 ++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 integrations/skills/flow/SKILL.md diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md new file mode 100644 index 0000000..c32c49d --- /dev/null +++ b/integrations/skills/flow/SKILL.md @@ -0,0 +1,281 @@ +--- +name: flow +description: Pi only. Run one or more tasks of one registered project to an agreed plan, one key at a time — session scope, subtasks, order, flow branch and score, reconnaissance by the scout role, every question to the engineer through mypi_ask, the plan by the planner role, published on derive, «Согласовать / Правки». Runs only when typed: "/flow […]", or "/flow " in a project or task chat to resume. +--- + +# /flow in Pi — tasks of one project to an agreed plan + +Read `docs/PIPELINE.md` in the engine root first: «Pi» binds the Claude pipeline to +Pi's tools, «Where /flow waits for the engineer» lists every stop of this skill. This +is the Pi counterpart of Claude's `.claude/skills/flow/SKILL.md` and keeps its rules +unless it says otherwise. Work inline in this chat: questions reach the engineer only +from here, through the `mypi_ask` tool; roles started with `subagent` cannot ask the +engineer. The typed command approves the run; the plan of each key is agreed by the +engineer's answer «Согласовать» («Approval semantics» of `docs/PIPELINE.md`). +`BIN=scripts/pipeline`; run every script with bash from the engine root (this +session's working directory). + +«Flow» in this skill is the run. The project's flow (`flow.json`, +`$BIN/mp-task flow`) is the project's branch and merge settings, which the run reads. + +## Tools + +- Scripts: `$BIN/mp-task …` and `$BIN/mp-home-commit …` through bash. Bash exports + this session's `PI_SESSION_ID`, so `$BIN/mp-task scope` reads this chat's scope. +- Roles: the `subagent` tool. It is enabled lazily: when `subagent` is not + available, call `subagents_enable` first. Call `subagent` with its default options + and without `async: true`, and wait for its result; never rely on `bg_wait` for a + background child, it may return at once with «No active async runs». A role that + asks the lead with `contact_supervisor` stays `running` and wakes this chat with + `subagent_supervisor_request`: answer it with `subagent_supervisor` from the + plan, the card and the engineer's answers, or ask the engineer through `mypi_ask` + first. A role's own `PI_SESSION_ID` has no scope: every role's task names the key + and the paths. +- Derive: the `derive` MCP server through codemode — `tools.mcp__derive__stage`, + `tools.mcp__derive__organize`, `tools.mcp__derive__catch_up`, + `tools.mcp__derive__comment`, each awaited. The upload goes through bash with + `curl`. Roles have no MCP: every derive call and every mypi write is the lead's. +- Questions: `mypi_ask` («Questions»). + +## Step 0 — session scope + +`$BIN/mp-task scope`, then apply the «Session scope» rule of `docs/PIPELINE.md` to +`/flow`: `global` — continue when every key has the same code (keys of several +projects get the «mixes projects» text); an organization (`org` is not `null`) — +continue when every key has the same code and that code is in `codes`; `P` — +continue when every key has the code `P`; `P-N` — continue when every key is `P-N` +or its subtask by `$BIN/mp-task subtasks P-N`; an argument that is not a key (other +than the run name), a malformed session key or a non-zero exit — refused. A refusal +prints its text from `docs/PIPELINE.md` and stops. + +`/flow ` without keys resumes: only when the scope's `project` is not `null` +(a `P` or `P-N` chat), `$BIN/mp-task project ` gives `home`, and the flow +score `/flows/.md` exists; its `- Command:` line gives the keys, and the +rule above applies to them. Otherwise print `usage: /flow […]; +/flow resumes only in a project or task chat that has the flow score` and +stop. + +This step runs before «Preconditions», before `git fetch`, before the flow branch +is created or pushed and before the flow score is written. + +## Preconditions + +- The first argument is the run name ``, not a task key; at least one key + follows, or the score gives them (Step 0). Otherwise print + `usage: /flow […]` and stop. +- Every key exists and is `new`, `planning`, `in_progress` (resume) or `review` + (already merged into this flow — skipped; or the epic of «Subtasks»: a typed key + in `review` whose `$BIN/mp-task subtasks ` is not empty and that has no + `pr.md` naming this flow). Anything else → stop before starting. +- `$BIN/mp-task project ` gives `checkoutPath`, `home` and `repository`; + `checkoutPath` or `repository` `null` → say so and stop. +- `$BIN/mp-task flow ` gives the project's `integration` and `prTarget`; + a non-zero exit → print its error and stop before any write. + +## Subtasks + +Before the flow score is written, run `$BIN/mp-task subtasks ` for every typed +key. The run list: for each typed key, in the typed order, its descendants in the +order of the `subtasks` output, then the key itself; a repeated key is dropped and +its first occurrence stays. The status of each subtask decides by «Subtasks» of +`docs/PIPELINE.md`: skipped, taken like a typed key, or one `mypi_ask` — question +`Подзадача «» в статусе <status>: <reason from $BIN/mp-task show +<KEY>>. Пропустить — поток идёт без неё; включить — она планируется в потоке +<name>.`, options `Пропустить`, `Включить`. An included subtask gets +`$BIN/mp-task status <KEY> planning "в потоке <name>: включена инженером"`. + +A typed key in `review` that has subtasks and no `pr.md` naming this flow is the +epic: it is not scouted, planned or recorded, and the run never changes its status; +the announced order and the skipped list of the flow score name it `<KEY> — эпик, +идут его подзадачи`. + +On resume a key already in the score's «Order» or «Skipped» keeps its place and is +not asked about again; only a subtask that is in neither is decided as above. + +## Order + +Before any write, compute the order of the keys. The base is the run list (subtasks +before their parent). Then the dependencies from the «Связи» section of each +`source.md` (a key goes after the keys of this run it depends on): a stable +topological reordering that never puts a parent before its subtasks. Then the files +each card names — tasks touching the same files go one after another, the one the +others build on first — where those constraints allow it. A cycle between the tree +and «Связи» is one `mypi_ask` with the cycle in the question, options +`Порядок дерева подзадач`, `Остановить`. Announce the order in the chat: the keys in +order, a mark `подзадача <parent>` on each subtask, one line of reasoning per key, +then on a line of its own the skipped subtasks with their status. Then go on without +waiting for a confirmation. A dependency outside the run that is not `review`, +`merged` or `done` is one `mypi_ask` before the start with the dependency and its +status in the question, options `Остановить и ждать`, `Продолжить с пометкой`; the +second answer is recorded in the key's `progress.md`, as `/do --force` records it. + +On resume the order of the score stands: print it with the stage of each key +(«Per key»), no new announcement. + +## Flow branch + +`<base clone>` is the project's `<checkoutPath>`. + +1. `git -C <base clone> fetch -q origin`. If `origin/flow/<name>` does not exist: + `git -C <base clone> branch flow/<name> origin/<integration> && git -C <base + clone> push -u origin flow/<name>`. +2. Flow worktree: `<checkoutPath>--flow-<name>` on `flow/<name>` + (`git -C <base clone> worktree add <path> flow/<name>` when missing); then + `git -C <flow worktree> merge -q --ff-only origin/flow/<name>`. Never work in the + base clone. +3. Flow score `<project home>/flows/<name>.md`, written once (on resume it is reused + as it is): + + ``` + # Flow <name> + + - Command: `/flow <name> <KEY> [<KEY>…]` (the typed keys) + - Branch: `flow/<name>`, base `origin/<integration>` <SHA> + - Worktree: `<flow worktree>` + - Started: <YYYY-MM-DD>, Pi + + ## Order + + - [ ] <KEY> — <title>[ (подзадача <parent>)] + + ## Skipped + + - <KEY> — <status or «эпик, идут его подзадачи»>[; ответ инженера: <answer>] + + ## Answers + + - <question in one line> → <answer> + ``` + + «Answers» holds the answers that belong to no key (a cycle of the order). + Commit with `$BIN/mp-home-commit <CODE> "flow <name>: start"` (it stages `flows` + too). + +## Reconnaissance up front + +The keys that need a plan are those whose task folder (`$BIN/mp-task dir <KEY>`) has +no `plan.md`. Their scouts run at once, in parallel, in one call: + +`subagent({ workflow: "integrations/pi-flow/investigate.js", args: { items: [{ key: "<key in lower case>", agent: "scout", task: "<scout task>" }, …] } })` + +The scout task carries, as text: the key, the full `source.md` (`$BIN/mp-task source +<KEY>`), the remarks if any, the project `<org>/<slug>`, the clone path — the flow +worktree, the project's folder in home, `.yoke/` and `AGENTS.md` (else `CLAUDE.md`) +of the clone when they exist, and the project's checks by «Checks» of +`docs/PIPELINE.md` (MP: `pnpm build` and `pnpm verify`); when the project's +instructions name no checks, the scout reports that as a fact. Before the call, note +per key the SHA `git -C <base clone> rev-parse origin/flow/<name>` and append +` — разведка <SHA>` to the key's line in the score. An item with `ok: false` or +`answer: null` is one `mypi_ask` with the error, options `Повторить разведку`, +`Остановить`; «Повторить разведку» runs `subagent({ agent: "scout", task })` for +that key. `solvable: false` is one `mypi_ask` with `missing` in the question, +options `Остановить (blocked)`, `Остановить`: the first sets +`$BIN/mp-task status <KEY> blocked "в потоке <name>: <missing>"`, both stop the run. +Questions and plans still go one key at a time. + +## Per key, in the announced order + +Strictly one key at a time. For each key: + +1. Skip if already `review` with `pr.md` naming this flow, or if the key is the epic + («Subtasks»). +2. **Stage.** Decide by the task folder and the score, without asking: + - no `plan.md` → step 3 with the key's scout answer; when this run has none, + run `subagent({ agent: "scout", task })` for the key first, as in + «Reconnaissance up front»; + - `plan.md` without `derive.json` → step 5; + - `derive.json`, and the key's line in the score has no ` — план v<version> + согласован` with the version of `derive.json` → step 6; + - that mark is there → «Execution of the key». +3. **Questions.** Each fork of the scout (`forks`) is one `mypi_ask`: `question` — + `<KEY>: <fork question>` and the fork's `why`; `options` — the fork's options in + their order (the recommended one first). The scout's `assumptions` go to the + planner as they are; never ask about logs, extra checks, release or merging. +4. **Plan.** `subagent({ agent: "planner", task })`. The task carries: the key, the + project `<org>/<slug>`, the task folder, the flow worktree as the clone, the + scout's `facts`, `localization`, `simplestPath` and `assumptions`, every fork with + the engineer's answer verbatim, the done criteria of `source.md` (they land in + Acceptance as executable checks; when the project's instructions name no checks, + Acceptance names concrete check commands that run in the worktree), and the rule + for the first lines: the key and the title, then the general plan's URL when the + card links one, otherwise a line saying there is no general plan. The answer + `{ plan, questions }`: `plan: null` → each question is one `mypi_ask` (`options` + and `why` as the planner gave them), then the planner runs again with every answer + added. Read the plan: an open question left in it, or no «Execution» section, is + a defect — send it back once with the decisions it missed; still defective → one + `mypi_ask` with the defect, options `Остановить`, `Опубликовать как есть`. Never + edit `plan.md` yourself. +5. **Publish** (fresh plan and rework alike): + 1. `await tools.mcp__derive__stage({ target: "doc" })`; on rework pass `short_id` + from `derive.json` (a new version of the same artifact). + 2. Upload through bash: `curl -sS -F file=@<task folder>/plan.md -F title="<KEY> — + план: <task title>" -F message="<what changed>" <upload_url>`; `message` only + on rework, naming what changed for each remark. Take `short_id`, `url` and the + version number from the answer. + 3. Fresh plan: `tools.mcp__derive__organize({ short_ids: [<short_id>], set: + ["<slug>", "<CODE>", "plan"] })` (`<slug>` is the project slug). + 4. Rework: resolve every thread of the remarks with `tools.mcp__derive__comment({ + short_id, reply_to: <thread id>, set_state: "resolved", body: "<one line: what + changed in the plan>" })`. + 5. Write `derive.json` `{"short_id","url","version","published_at"}` in the task + folder. + 6. Record by «Journal» of `docs/PIPELINE.md`: + `$BIN/mp-task progress <KEY> "План v<version> опубликован: <url>" plan.md`; + fresh: `$BIN/mp-task status <KEY> planning "план опубликован, ждёт + согласования: <url>"`; rework: `$BIN/mp-task status <KEY> planning "план + v<version> переработан: <что изменилось>; ждёт согласования: <url>"`; + `$BIN/mp-home-commit <KEY> "<KEY>: plan v<version>"`. +6. **Agree.** `tools.mcp__derive__catch_up` with the `short_id` of `derive.json` and + `comments: "open"` gives the open threads. One `mypi_ask`: question `<KEY>: план + v<version> опубликован: <url>.` with the open threads quoted (or «Открытых + замечаний в derive нет.»); options `Согласовать`, `Правки` — `Правки` first when + a thread is open. + - `Согласовать` → append ` — план v<version> согласован` to the key's line in the + score; `$BIN/mp-home-commit <CODE> "flow <name>: <KEY> plan v<version> + agreed"`; go to «Execution of the key». + - `Правки` → the remarks are the open threads, each quoted verbatim; with no open + thread say «открытых замечаний в derive нет» and ask step 6 again. + - Own word (`custom: true`) → the remarks are that text and the open threads. + With remarks: `subagent({ agent: "planner", task })` with the key, the task folder, + the flow worktree, the path of the current `plan.md` and every remark verbatim — + the planner revises `plan.md` in place so that each remark is addressed and keeps + the rest; then step 5 as rework (resolving exactly those threads) and step 6 + again. + +## Execution of the key — part 2 (MP-118) continues here + +Part 2 adds here: the base check before the key's execution, the execution by the +plan's «Execution», review, checks, the merge into the flow branch and the record; +the plan of the next key may be written while the current key executes. Until then: +report `план <KEY> согласован: <url> (v<version>); исполнение — часть 2, ещё не +реализовано` and stop the run. Later keys stay untouched. + +## Questions + +Every stop listed in «Where /flow waits for the engineer» of `docs/PIPELINE.md` and +every question of this skill is one `mypi_ask` call: `question` — the literal output +or finding and one line of reasoning per option; `options` — 2–4 short labels, the +recommended one first, without «(рекомендую)» and without «Своё слово» (the tool adds +both). Never ask in plain chat text and never two questions in one call. The run +waits for the answer; no retry, no bypass, no weakened check in its place. + +- `{ answer, custom }` → append `- <YYYY-MM-DD HH:MM> <question in one line> → + <answer>` (with ` (своё слово)` when `custom` is true) to `progress.md` in the + key's task folder; an answer that belongs to no key goes to «Answers» of the score. + It is committed with the next `mp-home-commit` of the run. +- `{ answer: null, cancelled: true }` → stop the run at once: no write after it, not + even the answer. Report `остановлено инженером: <question>`; typing `/flow <name>` + again resumes from the stage. +- An error result (no interactive UI) → stop and say that `/flow` needs an + interactive Pi session. + +`blocked` is set only when the engineer answers so; the run then stops, later keys +stay untouched. + +## Never + +- A GitHub Release or any other publication, a migration of the personal database, + the engine update: only on the engineer's separate permission, never as a step of + the run. +- Force-push, deleting a remote branch, merging a PR, an edit of `plan.md` by the + lead. From 81e42085576babe59273d405aa065b4979a4b2fd Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 16:49:21 +0100 Subject: [PATCH 15/48] Document the Pi /flow and its tool bindings in the pipeline --- docs/PIPELINE.md | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index af0a061..3ef6438 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -5,7 +5,8 @@ session starts there, so each pipeline skill checks the session scope first (see «Session scope»). It is tracked in the engine repository: this file, `docs/PLAN-FORMAT.md`, the scripts in `scripts/pipeline/`, the skills `/plan`, `/do`, `/epic`, `/flow`, `/ship`, `/board` in `.claude/skills/` and the roles in `.claude/agents/` (copied from yokemate; `plan-challenger` is the pipeline's own). For Pi, the roles and workflow templates in `integrations/pi-flow/`, -found through `.pi/settings.json` («Roles»). Claude +found through `.pi/settings.json` («Roles»), and the Pi `/flow` skill +`integrations/skills/flow/SKILL.md` («Pi»). Claude Code loads the skills and roles when it starts in the engine root; the scripts act on the installation they are run from and refuse any other working directory. @@ -106,7 +107,8 @@ A refusal prints its text and stops. Nothing runs before it except `mp-task scop no `mp-task show/status/progress`, no `git fetch`, no `mp-worktree`, no `mp-home-commit`, no derive calls, no subagents (`/board` also runs `mp-task project` and `project list`). In a `P-N` session `mp-task subtasks P-N` may run too, and for -`/ship flow/<name>` also `mp-task project P` and reading the flow score. The texts (`<cmd>`, the typed +`/ship flow/<name>` also `mp-task project P` and reading the flow score. The same holds for the Pi `/flow <name>` typed without keys, which resumes the +keys of that score. The texts (`<cmd>`, the typed arguments and `<key>` substituted): - `/board` with another project's code: `/board <CODE> is not allowed in session scope <key>; this chat shows the board of <P> only. Use /board here or /open global for another project.` @@ -134,7 +136,8 @@ tree of any depth. `mp-task subtasks <KEY>` prints the recursive descendants of task as JSON lines, subtasks before their parent. `/flow` expands every typed key into its subtasks and runs them in the same cycle -as the typed keys (`.claude/skills/flow/SKILL.md`, «Subtasks»). The status of a +as the typed keys (`.claude/skills/flow/SKILL.md`; in Pi `integrations/skills/flow/SKILL.md`; +«Subtasks»). The status of a subtask decides: - `merged` or a terminal status: skipped, named in the announced order; @@ -142,7 +145,7 @@ subtask decides: like a typed key; - `review` not of this flow: skipped, named; it has its own PR or flow already; - `blocked` or any other non-terminal status (`research`, `paused`, a custom one): - one `AskUserQuestion` «include or skip». An included subtask gets + one `AskUserQuestion` (in Pi one `mypi_ask`) «include or skip». An included subtask gets `mp-task status <KEY> planning "в потоке <name>: включена инженером"` and goes on as `planning`; - a typed key in `review` with subtasks and no `pr.md` of this flow is the epic: @@ -269,6 +272,31 @@ check with its expected exit code; the planner saves `plan.md` in the task's folder and writes the plan's «Execution» section. File formats, own roles and model tiers: `docs/agents/agent-integration.md`. +## Pi + +Pi runs `/flow` from `integrations/skills/flow/SKILL.md`; it is Pi-only and keeps +the rules of the Claude skill with these bindings: + +| Claude | Pi | +| --- | --- | +| the skill `.claude/skills/flow` | the extension command `/flow`, which sends `/skill:flow <args>` | +| `AskUserQuestion` | the extension tool `mypi_ask`: `question` and 2–4 `options`, the recommended one first; the tool marks it «(рекомендую)» and adds «Своё слово» | +| the `plan` skill with `plan-scout` and `plan-writer` | `subagent`: `integrations/pi-flow/investigate.js` with `scout`, then `planner` | +| the derive MCP tools | `mcp__derive__*` through codemode; the upload by `curl` | +| `CLAUDE_CODE_SESSION_ID` for `mp-task scope` | `PI_SESSION_ID`, exported by Pi's bash | +| re-typing the command | the same; `/flow <name>` without keys in a project or task chat resumes the keys of the flow score | + +The run's state is the flow score, the task statuses and the task folders, so a +restarted Pi continues where it stopped: no `plan.md` — reconnaissance and plan; +`plan.md` without `derive.json` — publication; `derive.json` without the line +` — план v<version> согласован` on the key in the score — the «Согласовать / +Правки» question; with it — the plan is agreed. «Правки» or an own word sends the +plan back to `planner` with the remarks and the open derive threads, publishes a new +version of the same artifact and asks again. The answers go to the key's +`progress.md`. A cancelled `mypi_ask` stops the run without another write. +Execution after the agreement is not part of the Pi skill yet: the run stops with +«план <KEY> согласован». + ## Statuses in mypi `new → planning → in_progress → review → merged → done`, plus `blocked`. @@ -339,6 +367,10 @@ One writer per fact; nothing is written twice. - An approved plan is the engineer typing `/do <KEY>` or `/flow …`; under `/flow` the engineer's answers to the plan's forks are part of the approval. No other signal counts; a published plan queues nothing. +- In the Pi `/flow` the plan of a key is agreed by the engineer's answer + «Согласовать» to its `mypi_ask` question, recorded on the key's line of the flow + score with the plan's version («Pi»); the typed `/flow` alone does not agree a + plan there. - Typing `/epic <KEY>` authorizes the research, its publication on derive and, after the engineer's answer to the «Подзадачи» question, `mypi task create --parent <KEY>` for the agreed subtasks. The answer «Завести и запустить /flow» @@ -351,13 +383,15 @@ One writer per fact; nothing is written twice. ## Where /flow waits for the engineer -`/flow` decides nothing on its own: each point below is one `AskUserQuestion` in +`/flow` decides nothing on its own: each point below is one `AskUserQuestion` (in Pi one +`mypi_ask`, «Pi») in the flow's chat (header ≤ 12 characters, 2–4 options, the recommendation first), and the run waits for the answer. No retry, no bypass, no weakened check in its place; `blocked` only when the engineer answers so. - a dependency outside the run that is not `review`, `merged` or `done`; - every fork of a plan, one question at a time; +- in Pi, every published plan: «Согласовать» or «Правки»; - an edit of the lead to a plan beyond the plan-writer's output (or an explicit line in the report); - `openPoints` of the executor only the engineer can answer; From c2e0e8d7ba65b64eedd9c5ebb6465761376f3f05 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 16:50:10 +0100 Subject: [PATCH 16/48] Document the Pi /flow command and mypi_ask in integration, testing and README --- README.md | 2 +- docs/TESTING.md | 2 +- docs/agents/agent-integration.md | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 18c8003..5e8f929 100644 --- a/README.md +++ b/README.md @@ -467,7 +467,7 @@ mypi/ ├── test/ # fast and boundary ├── scripts/ # bounded build/checks ├── integrations/prompts/ # warmup role and how-to texts -├── integrations/skills/ # built-in skills: open, inbox, project-management, retro +├── integrations/skills/ # built-in skills: open, inbox, project-management, retro; flow (Pi only) ├── integrations/pi-flow/ # Pi pipeline roles (*.md) and workflow templates (*.js) ├── .pi/ # tracked Pi configuration │ ├── settings.json # pi-subagents: no ambient extensions, role scan roots diff --git a/docs/TESTING.md b/docs/TESTING.md index e1e5a41..43d8ad0 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -119,7 +119,7 @@ This is a placement guide, not a manual per-file catalog or eight separate runne | Scoped warmup | Golden Markdown files in test/fixtures/warmup/ for global/project/task on a temp DB/home; scope priority; errors; 64 KiB entry clipping, invitation lines, the reports marker; character budget (multi-byte, overflow, range) | CLI text output, MYPI_SCOPE priority and error exit; MCP text | | Session registry | Fast tests on a temp DB, temp XDG_RUNTIME_DIR and a fake /proc: owner-only files, pid+start liveness, displacement, unregister/forget, tree, nine key-resolution scenarios, permission mode | One CLI boundary test with hook stdin and the real /proc; manual stand checklist with Claude Code and Pi versions | | Claude Code adapter | Shape of .mcp.json and .claude/settings.json; allow list equals the MCP tool list | Manual stand checklist with the Claude Code version (approval, warmup on start/clear/resume/compact, no prompts) | -| Pi extension | Fast test of .pi/extensions/mypi.ts against a stub ExtensionAPI: MCP registration with the session id, re-registration on a new session, warmup section on every turn, failure section | Manual stand checklist with a real Pi; its version is recorded in the PR | +| Pi extension | Fast test of .pi/extensions/mypi.ts against a stub ExtensionAPI: MCP registration with the session id, re-registration on a new session, warmup section on every turn, failure section; the /open, /inbox and /flow commands (/flow sends /skill:flow); the mypi_ask tool: recommended and other option, own word, Escape in the select and in the input, no UI | Manual stand checklist with a real Pi; its version is recorded in the PR; /flow to an agreed plan on a fixture (MP-117) | | Capture/note/journal | Unicode/multiline, exact source, no-overwrite, read-only API behavior | Real context commit and preservation of unrelated dirty diff | | DB authority/constraints | Real engine, transactions and resulting state, not mocked INSERT | Reopen/restart and competing writers; in-memory is not durability proof | | Retry/mixed-operation recovery | Bounded failure scenarios at durable protocol boundaries | Abrupt termination/restart when exceptions/finally behave differently | diff --git a/docs/agents/agent-integration.md b/docs/agents/agent-integration.md index 8293756..02b8914 100644 --- a/docs/agents/agent-integration.md +++ b/docs/agents/agent-integration.md @@ -22,7 +22,14 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou and suggests keys while its arguments are typed (the session's project and its open tasks in a project or task chat, the organization's projects and their open tasks in an organization chat, all projects and open tasks in the main chat; loaded through the CLI and reused for 15 s; none for flags or after - --message), and the /inbox command, which runs the inbox skill as /skill:inbox. + --message), the /inbox command, which runs the inbox skill as /skill:inbox, and the + /flow command, which runs the Pi-only flow skill as /skill:flow. It registers the tool + `mypi_ask` (direct, run sequentially): `question` and 2–4 `options`, the recommended + one first; it shows a select with the first option marked «(рекомендую)» and «Своё + слово» last, which opens a text input; it returns `{"answer","custom"}`, + `{"answer":null,"cancelled":true}` when Escape closes the select or the input (or the + input is empty), and an error result without asking when the session has no + interactive UI. On every `agent_settled`, after the [event emit](sessions.md#events-and-parent-messages), when its session has a file (`getSessionFile()`; none under `--no-session`) it runs `mypi archive capture --agent pi --file <that file>` with a 5-second limit and ignores @@ -95,7 +102,8 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou - Personal Claude settings belong in the ignored `.claude/settings.local.json` or in user settings. There is no `CLAUDE.md`; Claude Code loads [AGENTS.md](../../AGENTS.md) instead. - Shared skills live in `integrations/skills/`; each gets a relative symlink - `.claude/skills/<name>`. The development pipeline's skills and agents in + `.claude/skills/<name>`, except the Pi-only skills (`flow`), which Claude Code + replaces with its own pipeline skill of the same name. The development pipeline's skills and agents in `.claude/` are real directories and files, used only with Claude Code. ## Personal customization From 9328e53af1e843261f1371a55f2eebfaa61779bd Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 16:50:24 +0100 Subject: [PATCH 17/48] Note the Pi /flow part 1 in the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2041c7..dcc9948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ from existing transcripts. MEMORY.md is no longer read; move facts into INDEX.md - Organization scope and chat (MP-78): the bare organization slug (`tb-ff`) is a session key. `/open tb-ff` (MCP `open_session`, CLI `mypi open`) opens, focuses or resumes the organization chat in the Herdr tab `org-tb-ff` of the workspace `tb-ff`, in the engine root, with no clone or worktree; `global`, `all`, an unknown organization and an organization without registered projects are refused. The organization chat leads the tasks of all registered projects of its organization: it opens their task chats and sends them parent messages directly. Its warmup (role `org`) carries the global and organization knowledge entries, one line per project and the events addressed to it; its start message lists each project's open tasks by status. Pi completes `/open` with the organization's projects and their open tasks; the pipeline skills accept the organization scope (`mp-task scope` adds `org` and `codes`). - Skill `/epic` and role `plan-challenger` (MP-107: MP-109): `/epic <KEY>` reads the epic's step from its status and folder (`docs/PIPELINE.md`, «Epics») and leads research, one review round, publication, an independent check by the read-only role `plan-challenger`, one question per finding with its decision row, version 2 and the subtasks created with `--parent`; the answer «Завести и запустить /flow» runs `/flow <name> <KEY>`. Claude Code only, through `.claude/skills/epic` and `.claude/agents`. - Pi pipeline roles and templates (MP-116, epic MP-68): `integrations/pi-flow/` holds the roles `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator` and `challenger`, each with its answer schema in `outputSchema`, and the workflow templates `standard.js` (implement, review, fix through `resume`, at most two review rounds), `parallel.js` and `investigate.js` (read-only fan-out). The tracked `.pi/settings.json` makes pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (own roles and templates) and start children without ambient extensions; in Pi sessions in the engine root `scout` and `reviewer` shadow the pi-subagents builtins of those names. `docs/agents/agent-integration.md` describes the role and template formats, own roles in `home/pi-flow/` and the personal model tiers. The roles are run by hand with `subagent` until the Pi `/flow` arrives; `.claude/agents` is unchanged. +- Pi `/flow`, part 1 (MP-117, epic MP-68): the Pi-only skill `integrations/skills/flow/SKILL.md`, run by the extension command `/flow`, takes the tasks of one project from the typed command to an agreed plan — session scope, subtasks, order, flow branch and score, parallel reconnaissance by `scout` through `investigate.js`, the plan by `planner` with its «Execution» section, publication on derive and the question «Согласовать / Правки»; `/flow <name>` resumes from the flow score after a restart. Every question goes through the new extension tool `mypi_ask` (2–4 options, the recommended one first, «Своё слово»; Escape cancels and the run stops without writes). Execution after the agreed plan is part 2 (MP-118); Claude's `/flow` is unchanged. ### Changed From dcb4fc273cbcac8ee366cb8df85d83ac3477d26a Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 16:59:22 +0100 Subject: [PATCH 18/48] Quote the flow skill description so Pi parses its frontmatter --- integrations/skills/flow/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index c32c49d..60a5467 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -1,6 +1,6 @@ --- name: flow -description: Pi only. Run one or more tasks of one registered project to an agreed plan, one key at a time — session scope, subtasks, order, flow branch and score, reconnaissance by the scout role, every question to the engineer through mypi_ask, the plan by the planner role, published on derive, «Согласовать / Правки». Runs only when typed: "/flow <name> <KEY> [<KEY>…]", or "/flow <name>" in a project or task chat to resume. +description: 'Pi only. Run one or more tasks of one registered project to an agreed plan, one key at a time — session scope, subtasks, order, flow branch and score, reconnaissance by the scout role, every question to the engineer through mypi_ask, the plan by the planner role, published on derive, «Согласовать / Правки». Runs only when typed: "/flow <name> <KEY> [<KEY>…]", or "/flow <name>" in a project or task chat to resume.' --- # /flow in Pi — tasks of one project to an agreed plan From 923eddb2b12d4bc6fc2be1f3fefb5ee42511017b Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 17:21:58 +0100 Subject: [PATCH 19/48] Make the scout ask about alternatives the task leaves open --- integrations/pi-flow/scout.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integrations/pi-flow/scout.md b/integrations/pi-flow/scout.md index 8ab0e63..0be1b06 100644 --- a/integrations/pi-flow/scout.md +++ b/integrations/pi-flow/scout.md @@ -17,6 +17,8 @@ Answer, in this order: - **What is the simplest path?** (`simplestPath`) Pick the simplest solution that solves the task as written. Complexity must trace back to the task's own requirements, never to speculation about the future. - **Where are the forks?** (`forks`) Mark the places where the description is ambiguous or several viable paths genuinely diverge. Each fork is a question with 2–4 options, your recommended option first, and `why` — three at most: a fork earns a question only when the interpretations differ in ways that matter and no reasonable default exists. Below that bar, decide yourself and record the choice in `assumptions`. +A choice between alternatives that the task leaves open and the code does not settle (for example «X or Y, not set») is always a fork with those alternatives as its options, your recommended one first — never an assumption. `assumptions` hold only what the code, the project's rules or the task itself settle. + Never a fork: extra logs, extra checks, release, versioning, merging — anything that does not change the solution itself. You change no code, write no files and create no tasks. Bash is for reading only: git log, git show, git ls-remote, git rev-parse, grep, sed -n, cat. Never a command that writes a file, the index, a ref, a remote, a task or a derive artifact. Ask the lead (`contact_supervisor`) only when your task cannot be read as given. From fd2d06376b706eebf6d6ede1b039d47c8bcef171 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 17:30:23 +0100 Subject: [PATCH 20/48] Narrow the scout's open-alternatives rule to forks within the three --- integrations/pi-flow/scout.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/pi-flow/scout.md b/integrations/pi-flow/scout.md index 0be1b06..76d7c53 100644 --- a/integrations/pi-flow/scout.md +++ b/integrations/pi-flow/scout.md @@ -17,7 +17,7 @@ Answer, in this order: - **What is the simplest path?** (`simplestPath`) Pick the simplest solution that solves the task as written. Complexity must trace back to the task's own requirements, never to speculation about the future. - **Where are the forks?** (`forks`) Mark the places where the description is ambiguous or several viable paths genuinely diverge. Each fork is a question with 2–4 options, your recommended option first, and `why` — three at most: a fork earns a question only when the interpretations differ in ways that matter and no reasonable default exists. Below that bar, decide yourself and record the choice in `assumptions`. -A choice between alternatives that the task leaves open and the code does not settle (for example «X or Y, not set») is always a fork with those alternatives as its options, your recommended one first — never an assumption. `assumptions` hold only what the code, the project's rules or the task itself settle. +A choice between alternatives that the task leaves open and the code does not settle (for example «X or Y, not set») is always a fork with those alternatives as its options, your recommended one first — never an assumption, and it counts toward the three. Never a fork: extra logs, extra checks, release, versioning, merging — anything that does not change the solution itself. From 70297067c83493ca4dedc548b9232a3b92f9ec66 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 17:31:28 +0100 Subject: [PATCH 21/48] Test that every shared skill has a frontmatter Pi can parse --- test/fast/skills.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/fast/skills.test.ts diff --git a/test/fast/skills.test.ts b/test/fast/skills.test.ts new file mode 100644 index 0000000..1979215 --- /dev/null +++ b/test/fast/skills.test.ts @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync } from 'node:fs'; + +const root = new URL('../../../', import.meta.url); +const read = (path: string) => readFileSync(new URL(path, root), 'utf8'); + +test('every skill in integrations/skills has a frontmatter Pi can parse: its directory name, a description and no unquoted value with ": "', () => { + const skills = readdirSync(new URL('integrations/skills/', root)).sort(); + assert.ok(skills.includes('flow')); + for (const skill of skills) { + const match = /^---\n([\s\S]*?)\n---\n/.exec(read(`integrations/skills/${skill}/SKILL.md`)); + assert(match !== null, skill); + const fields = new Map<string, string>(); + for (const line of (match[1] ?? '').split('\n')) { + const colon = line.indexOf(': '); + assert(colon > 0, `${skill}: ${line}`); + const value = line.slice(colon + 2).trim(); + if (!/^'.*'$|^".*"$/.test(value)) assert(!value.includes(': '), `${skill}: unquoted value with ": " in ${line.slice(0, colon)}`); + fields.set(line.slice(0, colon), value); + } + assert.equal(fields.get('name'), skill); + assert.ok((fields.get('description') ?? '').replace(/^['"]|['"]$/g, '').trim().length > 0, skill); + } +}); From 9e5117db1b3aff1dfb5b642b9457db76432de51b Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 17:31:58 +0100 Subject: [PATCH 22/48] Commit the answers already written when a /flow question is cancelled --- CHANGELOG.md | 2 +- docs/PIPELINE.md | 3 ++- integrations/skills/flow/SKILL.md | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcc9948..9a83c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ from existing transcripts. MEMORY.md is no longer read; move facts into INDEX.md - Organization scope and chat (MP-78): the bare organization slug (`tb-ff`) is a session key. `/open tb-ff` (MCP `open_session`, CLI `mypi open`) opens, focuses or resumes the organization chat in the Herdr tab `org-tb-ff` of the workspace `tb-ff`, in the engine root, with no clone or worktree; `global`, `all`, an unknown organization and an organization without registered projects are refused. The organization chat leads the tasks of all registered projects of its organization: it opens their task chats and sends them parent messages directly. Its warmup (role `org`) carries the global and organization knowledge entries, one line per project and the events addressed to it; its start message lists each project's open tasks by status. Pi completes `/open` with the organization's projects and their open tasks; the pipeline skills accept the organization scope (`mp-task scope` adds `org` and `codes`). - Skill `/epic` and role `plan-challenger` (MP-107: MP-109): `/epic <KEY>` reads the epic's step from its status and folder (`docs/PIPELINE.md`, «Epics») and leads research, one review round, publication, an independent check by the read-only role `plan-challenger`, one question per finding with its decision row, version 2 and the subtasks created with `--parent`; the answer «Завести и запустить /flow» runs `/flow <name> <KEY>`. Claude Code only, through `.claude/skills/epic` and `.claude/agents`. - Pi pipeline roles and templates (MP-116, epic MP-68): `integrations/pi-flow/` holds the roles `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator` and `challenger`, each with its answer schema in `outputSchema`, and the workflow templates `standard.js` (implement, review, fix through `resume`, at most two review rounds), `parallel.js` and `investigate.js` (read-only fan-out). The tracked `.pi/settings.json` makes pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (own roles and templates) and start children without ambient extensions; in Pi sessions in the engine root `scout` and `reviewer` shadow the pi-subagents builtins of those names. `docs/agents/agent-integration.md` describes the role and template formats, own roles in `home/pi-flow/` and the personal model tiers. The roles are run by hand with `subagent` until the Pi `/flow` arrives; `.claude/agents` is unchanged. -- Pi `/flow`, part 1 (MP-117, epic MP-68): the Pi-only skill `integrations/skills/flow/SKILL.md`, run by the extension command `/flow`, takes the tasks of one project from the typed command to an agreed plan — session scope, subtasks, order, flow branch and score, parallel reconnaissance by `scout` through `investigate.js`, the plan by `planner` with its «Execution» section, publication on derive and the question «Согласовать / Правки»; `/flow <name>` resumes from the flow score after a restart. Every question goes through the new extension tool `mypi_ask` (2–4 options, the recommended one first, «Своё слово»; Escape cancels and the run stops without writes). Execution after the agreed plan is part 2 (MP-118); Claude's `/flow` is unchanged. +- Pi `/flow`, part 1 (MP-117, epic MP-68): the Pi-only skill `integrations/skills/flow/SKILL.md`, run by the extension command `/flow`, takes the tasks of one project from the typed command to an agreed plan — session scope, subtasks, order, flow branch and score, parallel reconnaissance by `scout` through `investigate.js`, the plan by `planner` with its «Execution» section, publication on derive and the question «Согласовать / Правки»; `/flow <name>` resumes from the flow score after a restart. Every question goes through the new extension tool `mypi_ask` (2–4 options, the recommended one first, «Своё слово»; Escape cancels: the run stops, records no answer to that question and only commits the answers already written). Execution after the agreed plan is part 2 (MP-118); Claude's `/flow` is unchanged. ### Changed diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index 3ef6438..0c92ffe 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -293,7 +293,8 @@ restarted Pi continues where it stopped: no `plan.md` — reconnaissance and pla Правки» question; with it — the plan is agreed. «Правки» or an own word sends the plan back to `planner` with the remarks and the open derive threads, publishes a new version of the same artifact and asks again. The answers go to the key's -`progress.md`. A cancelled `mypi_ask` stops the run without another write. +`progress.md`. A cancelled `mypi_ask` stops the run: the cancelled answer is not +recorded, and only `mp-home-commit` commits the answers already written. Execution after the agreement is not part of the Pi skill yet: the run stops with «план <KEY> согласован». diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 60a5467..3fdb4ed 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -263,9 +263,10 @@ waits for the answer; no retry, no bypass, no weakened check in its place. <answer>` (with ` (своё слово)` when `custom` is true) to `progress.md` in the key's task folder; an answer that belongs to no key goes to «Answers» of the score. It is committed with the next `mp-home-commit` of the run. -- `{ answer: null, cancelled: true }` → stop the run at once: no write after it, not - even the answer. Report `остановлено инженером: <question>`; typing `/flow <name>` - again resumes from the stage. +- `{ answer: null, cancelled: true }` → stop the run at once. The cancelled answer is + not recorded; the only write after it is `$BIN/mp-home-commit <CODE> "flow <name>: + stopped"`, which commits the answers already written. Report `остановлено + инженером: <question>`; typing `/flow <name>` again resumes from the stage. - An error result (no interactive UI) → stop and say that `/flow` needs an interactive Pi session. From 34f8a1d15faabe14815cb0afc796d3929f4a7617 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 17:33:48 +0100 Subject: [PATCH 23/48] Say in the mypi_ask description that a cancelled answer is not recorded --- .pi/extensions/mypi-lib/ask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pi/extensions/mypi-lib/ask.ts b/.pi/extensions/mypi-lib/ask.ts index 88866a2..88b4e05 100644 --- a/.pi/extensions/mypi-lib/ask.ts +++ b/.pi/extensions/mypi-lib/ask.ts @@ -26,7 +26,7 @@ export function registerAsk(pi: { registerTool(tool: AskTool): void }): void { pi.registerTool({ name: 'mypi_ask', label: 'mypi ask', - description: 'Ask the engineer one question and wait for the answer. options: 2–4 short labels, the recommended one first; the tool marks it «(рекомендую)» and adds «Своё слово» for a typed answer. Put the facts and one line of reasoning per option into question. Returns {"answer","custom"}; {"answer":null,"cancelled":true} when the engineer cancels — then stop and write nothing more.', + description: 'Ask the engineer one question and wait for the answer. options: 2–4 short labels, the recommended one first; the tool marks it «(рекомендую)» and adds «Своё слово» for a typed answer. Put the facts and one line of reasoning per option into question. Returns {"answer","custom"}; {"answer":null,"cancelled":true} when the engineer cancels — then stop; record no answer to it.', parameters: { type: 'object', properties: { From 64f8a8f20a6e3df3283ebdfd3621b0e29a6f9e0f Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 17:34:16 +0100 Subject: [PATCH 24/48] List the shared skills frontmatter test in the testing policy --- docs/TESTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index 43d8ad0..3f98682 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -130,6 +130,7 @@ This is a placement guide, not a manual per-file catalog or eight separate runne | CLI/bootstrap | In-process parsing/dispatch of every command | Launcher, exit/stdout/stderr and required save→reread path without Python | | Agent docs | AGENTS.md core within 8,000 characters; every link, anchor and repository path in AGENTS.md and docs/**/*.md exists (test/fast/agent-docs.test.ts) | — | | Pi pipeline resources | .pi/settings.json equals its expected object; integrations/pi-flow holds exactly the seven roles with name, description, tools, acceptance role, project context, no model or extensions, and a closed one-line outputSchema (reviewer status approved or changes_required); the three workflow templates open with a header and compile as a workflow body (test/fast/pi-flow.test.ts) | Stand with a real Pi: subagent list, template validation, one reviewer run | +| Shared skills | every integrations/skills/*/SKILL.md frontmatter: name equals its directory, non-empty description, no unquoted value containing ": " (test/fast/skills.test.ts) | — | | Prompts | how-to within 2,200 characters, every role within 1,300; the knowledge concepts paragraph in every level's warmup (test/fast/prompts.test.ts, test/fast/warmup.test.ts) | — | | HTTP API | Pure handler: auth, routing, validation, cursor paging, response shapes; keyset pages of tasks, events and journal; data_version feed; token file | One loopback serve subprocess: 401/403, a cursor read, a write, changeId and SSE after it, access log without secrets | | Web UI | Envelope unwrapping, screen filters and API paths, generic field display, change tracking; session cookie rules in the pure handler; static asset resolution | The same loopback serve subprocess: index, asset and SPA fallback, cookie login with HttpOnly/SameSite/Secure, read-only cookie, an SSE stream over the cookie, revocation ends it | From c7146b968f7db717b6e82f79335a78f135ba25f3 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 19:55:52 +0100 Subject: [PATCH 25/48] Report outside-plan files and review ranges from the Pi flow templates --- integrations/pi-flow/parallel.js | 7 ++-- integrations/pi-flow/reviewer.md | 6 ++-- integrations/pi-flow/standard.js | 7 ++-- test/fast/pi-flow.test.ts | 56 ++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/integrations/pi-flow/parallel.js b/integrations/pi-flow/parallel.js index df7d00d..6baf3dc 100644 --- a/integrations/pi-flow/parallel.js +++ b/integrations/pi-flow/parallel.js @@ -3,13 +3,14 @@ * standard.js: its executor implements, a fresh reviewer checks the diff, blocking * findings go back to the same executor through resume, at most two review rounds. * The project's checks stay with the lead. + * A round whose reviewer lists files outside the plan ends the run before a fix; the lead asks the engineer. * Parameters: * args.parts — [{ key, agent, plan, worktree, taskDir, base }]: key — a short unique * part name (letters, digits, "-"), agent — role that implements * (default "executor"), plan, worktree, taskDir — absolute paths, * base — the branch the part's worktree was created from * args.reviewer — role that reviews (default "reviewer") - * Returns [{ key, executor: { runId, report }, fixes, reviews }] in the order of args.parts. + * Returns [{ key, executor: { runId, report }, fixes, reviews: [{ round, range, status, findings, outsidePlan }] }] in the order of args.parts. */ const reviewerRole = args.reviewer || "reviewer"; function rounds(part, context, state, round) { @@ -20,8 +21,8 @@ function rounds(part, context, state, round) { }) .then((review) => { const verdict = review.structuredOutput ?? null; - state.reviews.push({ round, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [] }); - if (!verdict || verdict.status !== "changes_required" || round === 2 || !state.last.runId) return state; + state.reviews.push({ round, range: `${state.base}..${state.head}`, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [], outsidePlan: verdict ? verdict.outsidePlan : [] }); + if (!verdict || verdict.status !== "changes_required" || verdict.outsidePlan.length > 0 || round === 2 || !state.last.runId) return state; const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); return runs .run(`${part.key}-fix-${round}`, { diff --git a/integrations/pi-flow/reviewer.md b/integrations/pi-flow/reviewer.md index dc8ae96..4283e49 100644 --- a/integrations/pi-flow/reviewer.md +++ b/integrations/pi-flow/reviewer.md @@ -4,14 +4,14 @@ description: Fresh-eyes review of one task's diff against its plan in seven lens tools: read, grep, find, ls, bash acceptanceRole: read-only inheritProjectContext: true -outputSchema: {"type":"object","properties":{"status":{"type":"string","enum":["approved","changes_required"]},"findings":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["blocking","advice"]},"lens":{"type":["integer","null"],"minimum":1,"maximum":7},"file":{"type":"string"},"line":{"type":["integer","null"]},"problem":{"type":"string"},"evidence":{"type":"string"},"fix":{"type":"string"}},"required":["severity","lens","file","line","problem","evidence","fix"],"additionalProperties":false}}},"required":["status","findings"],"additionalProperties":false} +outputSchema: {"type":"object","properties":{"status":{"type":"string","enum":["approved","changes_required"]},"findings":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["blocking","advice"]},"lens":{"type":["integer","null"],"minimum":1,"maximum":7},"file":{"type":"string"},"line":{"type":["integer","null"]},"problem":{"type":"string"},"evidence":{"type":"string"},"fix":{"type":"string"}},"required":["severity","lens","file","line","problem","evidence","fix"],"additionalProperties":false}},"outsidePlan":{"type":"array","items":{"type":"string"}}},"required":["status","findings","outsidePlan"],"additionalProperties":false} --- You review one task's diff against its plan. Your task names the plan, the worktree and the diff range `<base>..<head>`. You did not write this code — judge only what is in front of you, the diff and the plan and the code it lands in, on its own terms. You change nothing — whoever wrote the part fixes. Seven lenses, in this order: -1. **Plan fidelity.** Every step of the plan is in the diff; nothing beyond it — scope creep is a finding. Where the plan names a source to mirror («same as X»), open X and compare letter for letter: a dropped line, a changed constant, a substituted color source is a finding. The contract with sibling parts — export names, API shapes, versions — matches the plan exactly. +1. **Plan fidelity.** Every step of the plan is in the diff; nothing beyond it — scope creep is a finding. A file the diff changes that no step of the plan names goes into `outsidePlan` (its path from the repository root), not into the findings; a lockfile counts the same. The lead asks the engineer about these files. Where the plan names a source to mirror («same as X»), open X and compare letter for letter: a dropped line, a changed constant, a substituted color source is a finding. The contract with sibling parts — export names, API shapes, versions — matches the plan exactly. 2. **Correctness of the changed lines.** Logic errors, mismatched types, boundary and repeat cases: empty input, zero, a reset or re-entry arriving inside a window the code opens. 3. **Interactions.** For every piece of state, timer, event or global the diff touches, grep for its other readers and writers and check the change survives each path. Name the paths you checked — the unchecked interaction is where escaped bugs live. 4. **Silent failures.** Swallowed errors, broad catches, empty catch blocks, fallbacks that mask a failure instead of surfacing it. @@ -28,4 +28,4 @@ Discipline: - You may be called once more on the fixed diff: same bar, no new lenses invented to stay busy. - Bash is for reading: git diff, git log, git show, grep, sed -n, cat. Never a command that writes a file, the index, a ref or a remote. -Your final answer is the structured output your schema defines; prose outside it is not read. `blocking` is what must be fixed before the merge, `advice` what the lead weighs. `status` is `changes_required` when at least one finding is `blocking`, else `approved`; no findings — `approved` with an empty list, without invented nitpicks. Each finding: `severity`, `lens` (1–7; null for the two extra findings), `file`, `line` (null when the finding has no line), `problem`, `evidence`, `fix`. +Your final answer is the structured output your schema defines; prose outside it is not read. `blocking` is what must be fixed before the merge, `advice` what the lead weighs. `status` is `changes_required` when at least one finding is `blocking`, else `approved`; no findings — `approved` with an empty list, without invented nitpicks. Each finding: `severity`, `lens` (1–7; null for the two extra findings), `file`, `line` (null when the finding has no line), `problem`, `evidence`, `fix`. `outsidePlan` — the changed files no step of the plan names, an empty list when there are none. diff --git a/integrations/pi-flow/standard.js b/integrations/pi-flow/standard.js index d4fa3b5..9486b96 100644 --- a/integrations/pi-flow/standard.js +++ b/integrations/pi-flow/standard.js @@ -2,6 +2,7 @@ * standard — one executor implements a task's plan in its worktree; a fresh reviewer * checks the diff; blocking findings go back to the same executor through resume; * at most two review rounds. The project's checks stay with the lead. + * A round whose reviewer lists files outside the plan ends the run before a fix; the lead asks the engineer. * Parameters: * args.key — task key, e.g. "MP-116" * args.plan — absolute path of the task's plan.md @@ -10,7 +11,7 @@ * args.base — the branch the worktree was created from * args.executor — role that implements (default "executor") * args.reviewer — role that reviews (default "reviewer") - * Returns { executor: { runId, report }, fixes: [{ round, ok, report }], reviews: [{ round, status, findings }] }. + * Returns { executor: { runId, report }, fixes: [{ round, ok, report }], reviews: [{ round, range, status, findings, outsidePlan }] }. */ const executorRole = args.executor || "executor"; const reviewerRole = args.reviewer || "reviewer"; @@ -31,8 +32,8 @@ for (const round of [1, 2]) { task: `${context} Review the diff ${base}..${head} in the worktree against the plan.`, }); const verdict = review.structuredOutput ?? null; - reviews.push({ round, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [] }); - if (!verdict || verdict.status !== "changes_required" || round === 2 || !last.runId) break; + reviews.push({ round, range: `${base}..${head}`, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [], outsidePlan: verdict ? verdict.outsidePlan : [] }); + if (!verdict || verdict.status !== "changes_required" || verdict.outsidePlan.length > 0 || round === 2 || !last.runId) break; const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); const fix = await runs.run(`fix-${round}`, { resume: last.runId, diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts index e90c272..975e31f 100644 --- a/test/fast/pi-flow.test.ts +++ b/test/fast/pi-flow.test.ts @@ -63,6 +63,8 @@ test('integrations/pi-flow holds exactly the seven engine roles, each with its t } const reviewer = JSON.parse(frontmatter(read('integrations/pi-flow/reviewer.md')).get('outputSchema') ?? ''); assert.deepEqual(reviewer.properties.status.enum, ['approved', 'changes_required']); + assert.deepEqual(reviewer.properties.outsidePlan, { type: 'array', items: { type: 'string' } }); + assert.ok(reviewer.required.includes('outsidePlan')); }); test('integrations/pi-flow holds the three workflow templates, each opening with its header comment, free of nested async functions and compiling as a workflow body', () => { @@ -75,3 +77,57 @@ test('integrations/pi-flow holds the three workflow templates, each opening with assert.doesNotThrow(() => new Script('(async (runs, args) => {\n' + source + '\n})'), file); } }); + +test('standard.js and parallel.js return each review round with its range and outside-plan files, fix blocking findings through resume, and stop before a fix when files lie outside the plan', async () => { + const template = (file: string) => new Script('(async (runs, args) => {\n' + read(`integrations/pi-flow/${file}`) + '\n})').runInThisContext(); + const blocking = { severity: 'blocking', lens: 1, file: 'a.txt', line: 1, problem: 'p', evidence: 'e', fix: 'f' }; + const fixing = { + implement: { runId: 'e1', structuredOutput: { base: 'b0', head: 'h1' } }, + 'review-1': { structuredOutput: { status: 'changes_required', findings: [blocking], outsidePlan: [] } }, + 'fix-1': { runId: 'e2', ok: true, structuredOutput: { base: 'b0', head: 'h2' } }, + 'review-2': { structuredOutput: { status: 'approved', findings: [], outsidePlan: [] } }, + }; + const outside = { + implement: fixing.implement, + 'review-1': { structuredOutput: { status: 'changes_required', findings: [blocking], outsidePlan: ['extra.txt'] } }, + }; + const stub = (results: Record<string, unknown>, calls: string[]) => ({ run: (key: string) => { calls.push(key); return Promise.resolve(results[key]); } }); + const prefixed = (results: Record<string, unknown>) => Object.fromEntries(Object.entries(results).map(([key, value]) => ['a-' + key, value])); + const parts = { parts: [{ key: 'a', plan: '/p', worktree: '/w', taskDir: '/t', base: 'flow/x' }] }; + + let calls: string[] = []; + let result = await template('standard.js')(stub(fixing, calls), {}); + assert.deepEqual(calls, ['implement', 'review-1', 'fix-1', 'review-2']); + assert.deepEqual(result.reviews, [ + { round: 1, range: 'b0..h1', status: 'changes_required', findings: [blocking], outsidePlan: [] }, + { round: 2, range: 'b0..h2', status: 'approved', findings: [], outsidePlan: [] }, + ]); + assert.equal(result.fixes.length, 1); + assert.equal(result.executor.runId, 'e2'); + + calls = []; + result = await template('standard.js')(stub(outside, calls), {}); + assert.deepEqual(calls, ['implement', 'review-1']); + assert.deepEqual(result.fixes, []); + assert.deepEqual(result.reviews[0].outsidePlan, ['extra.txt']); + assert.equal(result.executor.runId, 'e1'); + + calls = []; + result = await template('parallel.js')(stub(prefixed(fixing), calls), parts); + assert.deepEqual(calls, ['a-implement', 'a-review-1', 'a-fix-1', 'a-review-2']); + assert.equal(result[0].key, 'a'); + assert.deepEqual(result[0].reviews, [ + { round: 1, range: 'b0..h1', status: 'changes_required', findings: [blocking], outsidePlan: [] }, + { round: 2, range: 'b0..h2', status: 'approved', findings: [], outsidePlan: [] }, + ]); + assert.equal(result[0].fixes.length, 1); + assert.equal(result[0].executor.runId, 'e2'); + + calls = []; + result = await template('parallel.js')(stub(prefixed(outside), calls), parts); + assert.deepEqual(calls, ['a-implement', 'a-review-1']); + assert.equal(result[0].key, 'a'); + assert.deepEqual(result[0].fixes, []); + assert.deepEqual(result[0].reviews[0].outsidePlan, ['extra.txt']); + assert.equal(result[0].executor.runId, 'e1'); +}); From 1b7860c6fda0f77949ef66181f09cefd57a0cb18 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 19:56:47 +0100 Subject: [PATCH 26/48] Let the Pi tester follow the project stand document and hand a stand over --- integrations/pi-flow/tester.md | 9 +++++---- test/fast/pi-flow.test.ts | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/integrations/pi-flow/tester.md b/integrations/pi-flow/tester.md index 9c1005b..37b68f9 100644 --- a/integrations/pi-flow/tester.md +++ b/integrations/pi-flow/tester.md @@ -4,17 +4,18 @@ description: Live acceptance of one task on a disposable stand by the project's tools: read, grep, find, ls, bash, edit, write acceptanceRole: writer inheritProjectContext: true -outputSchema: {"type":"object","properties":{"passed":{"type":"boolean"},"checklist":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"expect":{"type":"string"},"observed":{"type":"string"},"passed":{"type":"boolean"},"evidence":{"type":"string"}},"required":["item","expect","observed","passed","evidence"],"additionalProperties":false}},"turns":{"type":"integer","minimum":0},"openQuestions":{"type":"array","items":{"type":"string"}}},"required":["passed","checklist","items","turns","openQuestions"],"additionalProperties":false} +outputSchema: {"type":"object","properties":{"passed":{"type":"boolean"},"checklist":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"item":{"type":"string"},"expect":{"type":"string"},"observed":{"type":"string"},"passed":{"type":"boolean"},"evidence":{"type":"string"}},"required":["item","expect","observed","passed","evidence"],"additionalProperties":false}},"turns":{"type":"integer","minimum":0},"openQuestions":{"type":"array","items":{"type":"string"}},"handover":{"type":["object","null"],"properties":{"attach":{"type":"string"},"teardown":{"type":"string"}},"required":["attach","teardown"],"additionalProperties":false}},"required":["passed","checklist","items","turns","openQuestions","handover"],"additionalProperties":false} --- -You run the live acceptance of one task. Your task names the plan (its Acceptance lists what to check), the commit to test, the project's stand procedure (for mypi `docs/MANUAL-ACCEPTANCE.md` in the engine root; for another project the stand document in its folder in mypi home), the task's folder in mypi home and the budget of model turns. +You run the live acceptance of one task. Your task names the plan (its Acceptance lists what to check), the commit to test, the project's stand document `<project home>/stand.md` in mypi home and the procedure it points to (for mypi it points to `docs/MANUAL-ACCEPTANCE.md` in the engine root), the task's folder in mypi home and the budget of model turns. - Before the start, write the checklist into the task's folder: one line per Acceptance item with its trigger, the action or command, and the expected observation. - Build the stand exactly as the procedure says: disposable, outside the installation's `projects/`, with its own state; never the installed engine, the engineer's home or the personal agent settings. - Check each item independently: from the stand's own state (files, the CLI, command output), not from what an agent on the stand says. Save each piece of evidence as a file in the task's folder. +- When your task says «handover», build the stand and write the checklist from the Acceptance of every plan it names, check no item and leave the stand up for the engineer: `handover.attach` is the command that opens it (for a stand in its own Herdr session `source <stand root>/bin/env.sh && herdr session attach <stand-session>`) or its URL, `handover.teardown` one command that tears it down by the rules below; `items` is empty and `passed` true. Without «handover», `handover` is null. - Count the model turns the stand spends; start messages count. Reaching the budget: stop, record what is done and put the overrun in `openQuestions` — never continue past it. - Known pitfalls: Herdr restores a stand session's layout from a past run; tabs opened by `/open` load the engineer's personal MCP servers. -- Teardown, always, also after a failure: kill the stand's processes by its `XDG_STATE_HOME`, never `pkill -f` by a path (it kills your own shell), then stop and delete the stand's Herdr session, and only then remove the stand directory. +- Teardown — except a handover, which the lead tears down with `handover.teardown` — always, also after a failure: kill the stand's processes by its `XDG_STATE_HOME`, never `pkill -f` by a path (it kills your own shell), then stop and delete the stand's Herdr session, and only then remove the stand directory. - A failed item is reported, never repaired. A decision the procedure does not settle: ask the lead with `contact_supervisor`. -Your final answer is the structured output your schema defines; prose outside it is not read: `passed` — every item passed; `checklist` — the checklist's path; `items` — per checklist line `item`, `expect`, `observed`, `passed`, `evidence` (the evidence file's path in the task's folder); `turns` — the model turns spent; `openQuestions`. +Your final answer is the structured output your schema defines; prose outside it is not read: `passed` — every item passed; `checklist` — the checklist's path; `items` — per checklist line `item`, `expect`, `observed`, `passed`, `evidence` (the evidence file's path in the task's folder); `turns` — the model turns spent; `handover` — `{ attach, teardown }` or null; `openQuestions`. diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts index 975e31f..7a78dd1 100644 --- a/test/fast/pi-flow.test.ts +++ b/test/fast/pi-flow.test.ts @@ -65,6 +65,9 @@ test('integrations/pi-flow holds exactly the seven engine roles, each with its t assert.deepEqual(reviewer.properties.status.enum, ['approved', 'changes_required']); assert.deepEqual(reviewer.properties.outsidePlan, { type: 'array', items: { type: 'string' } }); assert.ok(reviewer.required.includes('outsidePlan')); + const tester = JSON.parse(frontmatter(read('integrations/pi-flow/tester.md')).get('outputSchema') ?? ''); + assert.deepEqual(tester.properties.handover.required, ['attach', 'teardown']); + assert.ok(tester.required.includes('handover')); }); test('integrations/pi-flow holds the three workflow templates, each opening with its header comment, free of nested async functions and compiling as a workflow body', () => { From 2dff59d1c51623cf7f66ebe7295a009bb4c6bc41 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 19:56:54 +0100 Subject: [PATCH 27/48] Name the stand document and the release in the plan Execution section --- docs/PLAN-FORMAT.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/PLAN-FORMAT.md b/docs/PLAN-FORMAT.md index 7e05d4e..4352afd 100644 --- a/docs/PLAN-FORMAT.md +++ b/docs/PLAN-FORMAT.md @@ -37,7 +37,8 @@ Only in a plan the Pi `/flow` runs, and there mandatory; a plan for Claude Code - the workflow template: its path in `integrations/pi-flow/` or `home/pi-flow/`, chosen by the header comment of its `*.js`, and its `args`; when no template fits, the order of the runs in words, which the lead runs as a one-off script; - the role for each step or group of steps: a role from `integrations/pi-flow/` or `home/pi-flow/`, chosen by its `description`; - the self-test: the Acceptance items the `tester` role checks live, or «none»; -- the stand: the project's stand procedure the self-test follows, or «none». +- the stand: the project's stand document (`<project home>/stand.md` in mypi home) the self-test follows and its budget of model turns, or «none»; +- the release: the project's `release` procedure from `flow.json` that runs after the engineer's «Одобрить», or «none — merge only». ### Assumptions From 42d10d71eb406d372955ced24356b9647f50e005 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 19:57:48 +0100 Subject: [PATCH 28/48] Run the Pi flow from the agreed plan to the merge and the release --- integrations/skills/flow/SKILL.md | 332 ++++++++++++++++++++++++++++-- 1 file changed, 314 insertions(+), 18 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 3fdb4ed..9108c66 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -1,9 +1,9 @@ --- name: flow -description: 'Pi only. Run one or more tasks of one registered project to an agreed plan, one key at a time — session scope, subtasks, order, flow branch and score, reconnaissance by the scout role, every question to the engineer through mypi_ask, the plan by the planner role, published on derive, «Согласовать / Правки». Runs only when typed: "/flow <name> <KEY> [<KEY>…]", or "/flow <name>" in a project or task chat to resume.' +description: 'Pi only. Run one or more tasks of one registered project from the typed command to the merge and the release, one key at a time — session scope, subtasks, order, flow branch and score, reconnaissance by the scout role, the plan by the planner role on derive with «Согласовать / Правки», execution by the workflow template of the plan, review rounds, checks, the merge into the flow branch, the self-test by the tester role, the PR, the stand for the engineer and «Одобрить / Замечания / Стоп»; «Одобрить» merges the PR and runs the release procedure of the project. Every question goes through mypi_ask. Runs only when typed: "/flow <name> <KEY> [<KEY>…]", or "/flow <name>" in a project or task chat to resume.' --- -# /flow in Pi — tasks of one project to an agreed plan +# /flow in Pi — tasks of one project from the typed command to the merge and the release Read `docs/PIPELINE.md` in the engine root first: «Pi» binds the Claude pipeline to Pi's tools, «Where /flow waits for the engineer» lists every stop of this skill. This @@ -11,7 +11,8 @@ is the Pi counterpart of Claude's `.claude/skills/flow/SKILL.md` and keeps its r unless it says otherwise. Work inline in this chat: questions reach the engineer only from here, through the `mypi_ask` tool; roles started with `subagent` cannot ask the engineer. The typed command approves the run; the plan of each key is agreed by the -engineer's answer «Согласовать» («Approval semantics» of `docs/PIPELINE.md`). +engineer's answer «Согласовать», the merge of the flow PR and the release by the +answer «Одобрить» («Approval semantics» of `docs/PIPELINE.md`). `BIN=scripts/pipeline`; run every script with bash from the engine root (this session's working directory). @@ -30,11 +31,17 @@ session's working directory). `subagent_supervisor_request`: answer it with `subagent_supervisor` from the plan, the card and the engineer's answers, or ask the engineer through `mypi_ask` first. A role's own `PI_SESSION_ID` has no scope: every role's task names the key - and the paths. + and the paths. In an interactive session a workflow run goes to the background and + wakes this chat when it ends; its full return value is `workflow.value` in the + `status.json` the completion notice names (`subagent({ action: "status", id: "<run + id>" })` shows the path too). A finished role is continued with `subagent({ action: + "resume", id: "<run id>", message })`: a background run that wakes this chat and + returns a new run id; the next resume uses the newest. - Derive: the `derive` MCP server through codemode — `tools.mcp__derive__stage`, `tools.mcp__derive__organize`, `tools.mcp__derive__catch_up`, `tools.mcp__derive__comment`, each awaited. The upload goes through bash with `curl`. Roles have no MCP: every derive call and every mypi write is the lead's. +- GitHub: `gh` through bash, for the PR, its checks and its merge. - Questions: `mypi_ask` («Questions»). ## Step 0 — session scope @@ -66,7 +73,8 @@ is created or pushed and before the flow score is written. - Every key exists and is `new`, `planning`, `in_progress` (resume) or `review` (already merged into this flow — skipped; or the epic of «Subtasks»: a typed key in `review` whose `$BIN/mp-task subtasks <KEY>` is not empty and that has no - `pr.md` naming this flow). Anything else → stop before starting. + `pr.md` naming this flow), or `merged` when the flow score has a `- Смержено:` + line (a resume before the release). Anything else → stop before starting. - `$BIN/mp-task project <first KEY>` gives `checkoutPath`, `home` and `repository`; `checkoutPath` or `repository` `null` → say so and stop. - `$BIN/mp-task flow <first KEY>` gives the project's `integration` and `prTarget`; @@ -177,9 +185,12 @@ Questions and plans still go one key at a time. Strictly one key at a time. For each key: -1. Skip if already `review` with `pr.md` naming this flow, or if the key is the epic - («Subtasks»). +1. Skip if the key's line in the score is ticked (`- [x]`), or if the key is the + epic («Subtasks»). 2. **Stage.** Decide by the task folder and the score, without asking: + - the line's last mark is ` — замечания` → the rework of step 6 («With remarks») + with the remarks of the last `замечания приёмки` lines of the key's + `progress.md`; - no `plan.md` → step 3 with the key's scout answer; when this run has none, run `subagent({ agent: "scout", task })` for the key first, as in «Reconnaissance up front»; @@ -194,7 +205,9 @@ Strictly one key at a time. For each key: 4. **Plan.** `subagent({ agent: "planner", task })`. The task carries: the key, the project `<org>/<slug>`, the task folder, the flow worktree as the clone, the scout's `facts`, `localization`, `simplestPath` and `assumptions`, every fork with - the engineer's answer verbatim, the done criteria of `source.md` (they land in + the engineer's answer verbatim, the project's release procedure (`release` of + `$BIN/mp-task flow <KEY>`; null — «none — merge only») and the project's stand + document `<project home>/stand.md` when it exists (else «none»), the done criteria of `source.md` (they land in Acceptance as executable checks; when the project's instructions name no checks, Acceptance names concrete check commands that run in the worktree), and the rule for the first lines: the key and the title, then the general plan's URL when the @@ -242,13 +255,285 @@ Strictly one key at a time. For each key: the rest; then step 5 as rework (resolving exactly those threads) and step 6 again. -## Execution of the key — part 2 (MP-118) continues here - -Part 2 adds here: the base check before the key's execution, the execution by the -plan's «Execution», review, checks, the merge into the flow branch and the record; -the plan of the next key may be written while the current key executes. Until then: -report `план <KEY> согласован: <url> (v<version>); исполнение — часть 2, ещё не -реализовано` and stop the run. Later keys stay untouched. +## Execution of the key + +Strictly one key executes at a time. `<task folder>` is `$BIN/mp-task dir <KEY>`, +`<flow worktree>` and `<base clone>` are those of «Flow branch». The stage, decided +by the task folder without asking: the status is `review`, `pr.md` names this flow +and the line is not ticked → step 8; `do-report.json` exists → step 5; otherwise +step 1. An answer already recorded in the key's `progress.md` for this key and this +plan version is not asked again. + +1. **Base check.** `git -C <base clone> fetch -q origin`, then + `git -C <base clone> log --stat <scout SHA>..origin/flow/<name>` with the newest + ` — разведка <SHA>` of the key's line (no such mark — skip the check and name + that in the final report). When the log touches a file `plan.md` names: run + `subagent({ agent: "scout", task })` for the key with that log in its task, then + the planner revises `plan.md` with the new facts as one remark («база потока + изменилась: <files>»), step 5 of «Per key» as rework and step 6; after + «Согласовать» append ` — разведка <new SHA>` to the line and come back here. +2. **Worktree.** `$BIN/mp-worktree <KEY> --base flow/<name>` prints `worktree`, + `branch`, `base_ref`, `base` and `dirty`. An `error` → a key stop question with + the literal output. `dirty: true` → one `mypi_ask` with `git -C <worktree> + status --short`, options `Остановить (blocked)`, `Остановить`, `Продолжить с этим + состоянием`. Then `$BIN/mp-task status <KEY> in_progress "в потоке <name> по + плану v<version>: <url>"` (`version` and `url` of `derive.json`) and + `$BIN/mp-home-commit <KEY> "<KEY>: execution started"`. When the plan's + «Execution» names `parallel.js`, each part gets + `git -C <base clone> worktree add -q -b <branch>--<part> <worktree>--<part> <base>` + when that path is missing. +3. **Run** by the plan's «Execution»: + - `standard.js`: `subagent({ workflow: "<template path>", args: { key: "<KEY>", + plan: "<task folder>/plan.md", worktree: "<worktree>", taskDir: "<task + folder>", base: "<base_ref>", executor: "<the plan's role>", reviewer: "<the + plan's reviewer role, else reviewer>" } })`; + - `parallel.js`: `args: { parts: [{ key: "<part>", agent: "<the part's role>", + plan, worktree: "<worktree>--<part>", taskDir, base: "<base_ref>" }, …], + reviewer }`, the parts in the plan's order; + - another template of `home/pi-flow/`: the `args` its header names, from the + same values; + - no template (the order in words): write the order as a workflow script + `<task folder>/workflow.js` — a header comment as in + `integrations/pi-flow/standard.js`, one `runs.run` per run in the plan's order, + the return shape of `standard.js` — and run it the same way. + The args come only from these values, so a run started again after a restart + gets the same args. While the run is in the background, take the next keys in + order whose plan is not agreed through steps 2–6 of «Per key»: never their + execution, never a write in this key's worktree. When the run ends, finish the + step in progress (an open `mypi_ask` is answered first) and go on here. +4. **Reports**, from the run's full return value, right after it returns and + before any decision: `do-report.json` — the executor's newest report (the last + `fixes[].report`, else `executor.report`; for parts an object keyed by part); + `review-report.json` — `{ "rounds": [ { "round", "range", "status", + "findings", "outsidePlan" } ] }` from `reviews` (for parts each round also has + `part`), every finding with `resolution`: `fixed <SHA>` (the head of the fix + that resolved it), `pr.md` (advice left to the PR), `engineer: <answer>`, or + `open` until it is decided. Rewrite both files after every later fix and round. + Check the file with + `python3 -c 'import json,sys;r=json.load(open(sys.argv[1]))["rounds"];assert r and all({"round","range","status","findings","outsidePlan"}<=x.keys() and all("resolution" in f for f in x["findings"]) for x in r)' <task folder>/review-report.json`; + a non-zero exit is your own defect: rewrite the file and check again. +5. **Decide**, on the newest round, in this order: + 1. A failed run — the workflow failed, `executor.report` is null, a fix has + `ok: false`, a round has `status: null` → a key stop question with the + literal error. + 2. `openQuestions` of the executor: a question the code answers → + `subagent({ agent: "investigator", task })`; any other → one `mypi_ask` + each. The answers go to the executor by resume, then one review round. + 3. `outsidePlan` not empty → `git -C <worktree> diff --name-only + <base>..<head>`: a listed file that is in this diff and whose path + `plan.md` does not name is outside the plan, a lockfile too; files already + answered for this key are not asked again; none left — go on as if the list + were empty. Otherwise one `mypi_ask` for all of them: question `<KEY>: + файлы вне плана: <files>. Убрать — исполнитель вернёт их к базе, затем ещё + один раунд ревью; Оставить — файлы остаются, ответ записан в progress.md.`, + options `Убрать`, `Оставить`. «Убрать» → the executor by resume restores + those files to `<base>` and commits, with the round's blocking findings in + the same message; then one review round. «Оставить» → the round's blocking + findings, if any, by resume, then one review round. + 4. `changes_required` after the second round or a later one → one `mypi_ask` + with the blocking findings verbatim, options `Остановить (blocked)`, `Ещё + один раунд исправлений`; the second → the findings by resume, one review + round, this rule again. A finding whose fix would change the plan's agreed + text or behaviour → one `mypi_ask` with the finding, options `Оставить по + плану`, `Исправить по находке`. + 5. `approved`: each advice finding that is a cheap fix inside the files and + steps of the plan goes to the executor by resume, followed by one more review + round; every other advice keeps `resolution: pr.md`. + «By resume» is `subagent({ action: "resume", id: "<newest executor run id>", + message })`, the message carrying the findings, answers or output verbatim and + «fix them in the worktree, commit, and answer again with your whole report»; + its report becomes `do-report.json`. A refused resume (the executor is no + longer retained) → `subagent({ agent: "<the plan's role>", task })` with the + template's context and the message; the final report names the fallback. «One + review round» is a fresh `subagent({ agent: "<reviewer role>", task: "Task + <KEY>. Plan: <plan>. Worktree: <worktree>. Task folder: <task folder>. Base + branch: <base_ref>. Review the diff <base>..<head> in the worktree against the + plan." })`, added to `review-report.json` as the next round with its range. +6. **Checks**, by you only, in the worktree. Parts first: `git -C <worktree> merge + --no-ff <branch>--<part> -m "Merge part <part> of <KEY>"` in the plan's order; a + conflict → `git -C <worktree> merge --abort` and a key stop question with the + literal output. When `plan.md` names a lockfile, first the project's install + (MP: `mise exec -- pnpm install --frozen-lockfile`). Then the project's checks by + «Checks» of `docs/PIPELINE.md` (MP: `mise exec -- pnpm build`, then `mise exec + -- pnpm verify`; one verify on the machine; on `Unit mypi-tests already exists` + or `already running` wait 60 s and retry, at most 5 times). Red → the literal + output to the executor by resume (for parts, the part whose files the output + names; unclear — a key stop question), then the checks again; after three + attempts one `mypi_ask` with the literal output, options `Остановить + (blocked)`, `Ещё попытка`. Keep the last output's tail for `pr.md`. +7. **Merge into the flow branch.** `git -C <worktree> push -u origin <branch>`; + `git -C <flow worktree> merge --no-ff <branch> -m "Merge <KEY>: <title>"`; + `git -C <flow worktree> push`. A conflict → `git -C <flow worktree> merge + --abort` and a key stop question with the literal output. Write `pr.md` in the + task folder: `# <KEY> — доставка (поток <name>)`, the flow merge SHA and the + branch, the plan URL, the checks with their exit codes and the tail, the review + rounds with what each fixed, open advice, the executor's `deviations`, the + engineer's answers. Record by «Journal» of `docs/PIPELINE.md`: + `$BIN/mp-task progress <KEY> "Реализовано, ревью чистое, проверки зелёные: <flow merge SHA>" plan.md pr.md do-report.json review-report.json`; + `$BIN/mp-task status <KEY> review "в потоке <name>, слито <flow merge SHA>"`; + `$BIN/mp-home-commit <KEY> "<KEY>: merged into flow/<name>"`. +8. **Self-test**, only when the plan's «Execution» names self-test items and a + stand (not «none») and `<project home>/stand.md` exists: + `subagent({ agent: "tester", task })` with the plan, the commit + `git -C <flow worktree> rev-parse HEAD`, the stand document, the task folder, + the self-test items and the budget of model turns the plan names (none named — + 40). `passed` → step 9, with a «Самотест» section appended to `pr.md`. Failed + items inside the plan's scope → the items and their evidence to the executor by + resume, one review round, step 6, step 7, then `tester` again with the failed + items. A failure that needs a decision, `openQuestions`, or `turns` over the + budget → one `mypi_ask` with the items, options `Остановить (blocked)`, + `Принять результат`. +9. **Record.** Tick the key's line (`- [ ]` → `- [x]`) and append ` — смержено + <flow merge SHA>, ревью <status of the newest round> (<rounds> р.), самотест + <passed>/<total> | не требуется`; `$BIN/mp-home-commit <CODE> "flow <name>: <KEY> + merged"`. The run writes no `progress` of its own. Then the next key; after the + last one, «Finish». + +## Finish + +The flow lines of the score are appended to its header list, after `- Started:`. +On resume, unticked keys go first by «Per key»; then the newest of these lines +decides: none → step 1 here; `- PR:` or `- Стоп:` → «The engineer's check» step 1; +`- Стенд:` → its step 2; `- Замечания:` → step 1 here; `- Одобрено:` → «Ship»; +`- Смержено:` → «Release»; `- Релиз начат:` → «Release» step 2; `- Релиз:` → the +report. + +1. In the flow worktree: the project's checks by «Checks» of `docs/PIPELINE.md` + (none named — the check commands of the plans of all keys except the epic, in + order). Red → a flow stop question with the literal output; no PR before it is + answered. +2. **Epic acceptance**, only when a typed key is the epic and + `$($BIN/mp-task dir <EPIC>)/research.md` has the line `**Приёмка эпика:** да: + <criterion>`. No `<project home>/stand.md` → one `mypi_ask`, options + `Остановить`, `Пропустить приёмку эпика`. Otherwise + `subagent({ agent: "tester", task })` with the criterion as its only item, the + commit of the flow worktree's head, the stand document, the epic's folder as + the task folder and a budget of 40 model turns; write `acceptance.md` in the + epic's folder from its items; append `- Приёмка эпика: <passed>/<total> + (<path of acceptance.md>)`; `$BIN/mp-home-commit <CODE> "flow <name>: <EPIC> + acceptance"`. A failure inside a subtask's plan → that subtask's executor by + resume (a fresh `executor` with its plan when it is not retained), one review + round, the checks, a new `--no-ff` merge, step 1, the failed items again. A + failure that needs a decision or an exceeded budget → one `mypi_ask`, options + `Остановить`, `Принять результат`. +3. **PR.** `gh pr list --repo <repository> --head flow/<name> --state open --json + url --jq '.[0].url // ""'`: a URL → that PR (the pushes have updated it); + otherwise `gh pr create --repo <repository> --base <prTarget> --head + flow/<name> --title "Flow <name>: <KEY>, <KEY>, …" --body "<body>"`, the body + listing per key the title, the plan URL, the review verdict, the checks, the + self-test (or «not required») and the engineer's answers; one line for the epic + acceptance (result and the path of `acceptance.md`, or «not required») when a + typed key is the epic; the last line `Release after approval: <release>` or + `Release: none — merge only`. No attribution line. +4. `gh pr checks <url> --watch`. Red only on the test budget with `fail 0` → + name it with the measurements in the final report, no rerun, go on. Any other + red → one `mypi_ask` with the literal output, options `Остановить`, `Проверить + снова`. +5. For every key except the epic: `$BIN/mp-task status <KEY> review "в потоке + <name>, ждёт ручной проверки: <PR url>"`. Append `- PR: <url>`; + `$BIN/mp-home-commit <CODE> "flow <name>: PR"`. + +## The engineer's check + +1. **Stand**, only when `<project home>/stand.md` exists: + `subagent({ agent: "tester", task })` with «handover», the plans of all keys + except the epic, the commit of the flow worktree's head, the stand document and + the folder `<project home>/flows/<name>/` for the checklist. `handover` null or + a failure → a flow stop question with the literal answer. Append `- Стенд: + <attach>; чек-лист <checklist>; снос: <teardown>`; `$BIN/mp-home-commit <CODE> + "flow <name>: stand"`. +2. One `mypi_ask`: question `Поток <name>: PR <url>. Стенд: <attach>. Чек-лист: + <checklist>.` (no stand document: `Поток <name>: PR <url>. Стенда нет: у проекта + нет документа стенда.`) followed by `Одобрить — мерж PR методом <mergeMethod>, + затем релиз по <release>` (or `затем релиза нет — только мерж`) `; Замечания — + ключи, которых они касаются, возвращаются к правке плана и исполняются снова; + Стоп — поток останавливается, /flow <name> спросит снова.`; options `Одобрить`, + `Замечания`, `Стоп`. The answer goes to «Answers» of the score. Before acting on + it, run the stand's `teardown` when a stand was built; a non-zero exit goes into + the final report with its output. + - `Одобрить` → append `- Одобрено: <YYYY-MM-DD HH:MM>`; + `$BIN/mp-home-commit <CODE> "flow <name>: approved"`; «Ship». + - `Замечания` or an own word → the remarks: the own word's text and the open + threads (`tools.mcp__derive__catch_up`, `comments: "open"`) of each key's plan + artifact. None → say «замечаний нет: напишите их своим словом или в derive на + плане ключа» and go to step 1. Each remark belongs to the key it names (a + thread — the key of its artifact; a text — the key, or the files of its plan, + it names); a remark that names none → one `mypi_ask` with the remark, options + up to four keys of the run whose plans' files it is closest to, closest first. + Per touched key: append `- <YYYY-MM-DD HH:MM> замечания приёмки: <remark>` to + its `progress.md`; move `do-report.json` and `review-report.json` to + `do-report-v<version>.json` and `review-report-v<version>.json` (`version` of + `derive.json`); untick its line and append ` — замечания`. Append `- + Замечания: <YYYY-MM-DD HH:MM>: <keys>`; `$BIN/mp-home-commit <CODE> "flow + <name>: remarks"`. Then «Per key» for the touched keys in order (the rework of + step 6, then «Execution of the key» on the same worktree and branch), then + «Finish» and this section again. + - `Стоп` → append `- Стоп: <YYYY-MM-DD HH:MM>`; `$BIN/mp-home-commit <CODE> + "flow <name>: stopped"`; report `поток <name> остановлен на приёмке: <PR url>; + /flow <name> спросит снова` and stop. + +## Ship + +The `/ship` procedure for `flow/<name>` (`.claude/skills/ship/SKILL.md`, steps +2–6), run by this skill after «Одобрить»: + +1. `git -C <flow worktree> fetch -q origin`, `git -C <flow worktree> merge + origin/<prTarget>`, never a rebase. Conflicts by the rule of `/ship` step 2: + read the neighbour's merged PR and the plans, keep both intents, never + `--ours`/`--theirs`; a hunk the plans cannot answer → one `mypi_ask` with the + hunk, options `Остановить`, `Решить в пользу <prTarget>`. When the merge moved + the head: the checks of «Finish» step 1 (red → a flow stop question). `git -C + <flow worktree> push`. +2. `gh pr checks <url> --watch --fail-fast`; red → one `mypi_ask` with the literal + output, options `Остановить`, `Проверить снова`. +3. `gh pr view <url> --json state --jq .state`: `MERGED` → no merge; otherwise + `gh pr merge <url> --<mergeMethod>`. The merge SHA: `gh pr view <url> --json + mergeCommit --jq .mergeCommit.oid`. +4. For every key except the epic, in order, unless it is already `merged`: + `$BIN/mp-task status <KEY> merged "смержено в <prTarget>: <merge SHA>"`; + `$BIN/mp-task progress <KEY> "Смержено: <PR url>" pr.md progress.md`. +5. Clean up each key's worktree and part worktrees, then the flow worktree: + `git -C <wt> status --porcelain --ignored`; untracked files that are not build + output move to the key's task folder (the flow worktree's to `<project + home>/flows/<name>/`); `git -C <base clone> worktree remove <wt>`; `git -C <base + clone> branch -d` for each task branch, part branch and `flow/<name>`. Remote + branches stay. A refused `worktree remove` or `branch -d` is kept and named in + the final report. +6. Append `- Смержено: <merge SHA> <YYYY-MM-DD HH:MM>`; `$BIN/mp-home-commit + <CODE> "flow <name>: shipped"`. + +## Release + +1. `release` of `$BIN/mp-task flow <first KEY>`. Null → append `- Релиз: нет + процедуры — только мерж`; `$BIN/mp-home-commit <CODE> "flow <name>: no + release"`; the report. +2. Otherwise: a `- Релиз начат:` line without a later `- Релиз:` → one `mypi_ask` + with the procedure and what its done criterion shows now, options `Остановить`, + `Повторить релиз`. Else append `- Релиз начат: <YYYY-MM-DD HH:MM>, процедура + <release>`; `$BIN/mp-home-commit <CODE> "flow <name>: release started"`. +3. `git -C <base clone> fetch -q origin`. The base clone must be on `<prTarget>` + and clean (`git -C <base clone> status --porcelain` empty), then + `git -C <base clone> merge -q --ff-only origin/<prTarget>`; anything else → a + flow stop question with the literal output. +4. The procedure: a path → `git -C <base clone> show origin/<prTarget>:<release>`; + an https URL → `curl -fsSL <release>`. Follow its steps in order in the base + clone through bash, nothing the procedure does not say. A step that would + commit, switch a branch or change tracked files of the base clone → a flow stop + question with the step; a failed step → a flow stop question with the literal + output. +5. Done → append `- Релиз: <what was published> <YYYY-MM-DD HH:MM>`; the project + note by «Journal» of `docs/PIPELINE.md`: `mise exec -- node dist/src/cli/main.js + journal add "Релиз <org>/<project>: <what>; поток <name>: <KEY>, …; сессия + <scope key>" -s <org>/<project>`; `$BIN/mp-home-commit <CODE> "flow <name>: + released"`. + +## Report + +The PR URL and the merge SHA; per key the review verdict and the self-test; the +epic acceptance; the release result or «релиза нет — только мерж»; what was +removed and what was kept; open advice; the engineer's answers; fallbacks and +skipped base checks; and «статусы — merged; done ставит инженер» (for MP also: +the installed engine is not updated by this run, `docs/agents/installation.md`). ## Questions @@ -259,6 +544,12 @@ recommended one first, without «(рекомендую)» and without «Своё both). Never ask in plain chat text and never two questions in one call. The run waits for the answer; no retry, no bypass, no weakened check in its place. +A key stop question is one `mypi_ask` with the literal output and the options +`Остановить (blocked)`, `Остановить`: the first sets `$BIN/mp-task status <KEY> +blocked "в потоке <name>: <one line>"`, both stop the run. A flow stop question has +the options `Остановить`, `Повторить`: the second runs the step again after the +engineer has fixed its cause. + - `{ answer, custom }` → append `- <YYYY-MM-DD HH:MM> <question in one line> → <answer>` (with ` (своё слово)` when `custom` is true) to `progress.md` in the key's task folder; an answer that belongs to no key goes to «Answers» of the score. @@ -277,6 +568,11 @@ stay untouched. - A GitHub Release or any other publication, a migration of the personal database, the engine update: only on the engineer's separate permission, never as a step of - the run. -- Force-push, deleting a remote branch, merging a PR, an edit of `plan.md` by the - lead. + the run. The release by the project's `release` procedure after the answer + «Одобрить» is that permission («Approval semantics» of `docs/PIPELINE.md`); + nothing else is. +- Merging the PR other than in «Ship» after «Одобрить»; force-push; deleting a + remote branch; `git worktree remove --force` or `git branch -D`; an edit of + `plan.md` by the lead; a write in a key's worktree by the lead other than the + merges, the install and the checks of this skill. +- Setting `done`: it stays the engineer's word. From 5cf396a5805050d811fa92096c6e030b0b7f3d0b Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 19:58:48 +0100 Subject: [PATCH 29/48] Document the Pi flow execution, approval and release in the pipeline --- docs/PIPELINE.md | 49 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index 0c92ffe..0ff0da9 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -261,9 +261,12 @@ final message is exactly one JSON object per its definition. Under `/epic` the e Pi's roles live in `integrations/pi-flow/`: `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator`, `challenger`, each a `*.md` file whose answer schema is its `outputSchema`, beside the workflow templates `standard.js` -(implement, review, fix through `resume`, at most two review rounds), +(implement, review, fix through `resume`, at most two review rounds; each round +returns its `range`, and a round whose reviewer lists files in `outsidePlan` ends it +before a fix), `parallel.js` (the same chain for several worktrees at once) and `investigate.js` -(read-only fan-out of scouts or investigators). `.pi/settings.json` makes +(read-only fan-out of scouts or investigators). `tester` also builds the stand for +the engineer and leaves it up (`handover`). `.pi/settings.json` makes pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (the engineer's own roles and templates) and starts children without ambient extensions. The bodies follow the Claude roles above without their defects: the executor reports the @@ -282,6 +285,9 @@ the rules of the Claude skill with these bindings: | the skill `.claude/skills/flow` | the extension command `/flow`, which sends `/skill:flow <args>` | | `AskUserQuestion` | the extension tool `mypi_ask`: `question` and 2–4 `options`, the recommended one first; the tool marks it «(рекомендую)» and adds «Своё слово» | | the `plan` skill with `plan-scout` and `plan-writer` | `subagent`: `integrations/pi-flow/investigate.js` with `scout`, then `planner` | +| `/do <KEY> --base flow/<name>` with `task-executor` and `task-reviewer` | the plan's «Execution»: a template of `integrations/pi-flow/` or `home/pi-flow/` through `subagent`, fixes by `subagent({ action: "resume" })` of the executor | +| the live acceptance of `/flow` | the `tester` role by the project's stand document `<project home>/stand.md` | +| the manual check, then a typed `/ship flow/<name>` | the stand handed over in the question «Одобрить / Замечания / Стоп»; «Одобрить» runs the `/ship` procedure and the release | | the derive MCP tools | `mcp__derive__*` through codemode; the upload by `curl` | | `CLAUDE_CODE_SESSION_ID` for `mp-task scope` | `PI_SESSION_ID`, exported by Pi's bash | | re-typing the command | the same; `/flow <name>` without keys in a project or task chat resumes the keys of the flow score | @@ -295,8 +301,22 @@ plan back to `planner` with the remarks and the open derive threads, publishes a version of the same artifact and asks again. The answers go to the key's `progress.md`. A cancelled `mypi_ask` stops the run: the cancelled answer is not recorded, and only `mp-home-commit` commits the answers already written. -Execution after the agreement is not part of the Pi skill yet: the run stops with -«план <KEY> согласован». +After «Согласовать» the key executes by its plan's «Execution»: the base check, +the task worktree from `mp-worktree --base flow/<name>`, the template through +`subagent` in the background while the next key's plan is agreed, +`do-report.json` and `review-report.json` written by the lead as soon as the +template returns (rounds with `range`, `outsidePlan` and each finding's +`resolution`), the reviewer's `outsidePlan` as one question per key, fixes by +resuming the executor, the project's checks by the lead, the `--no-ff` merge into +the flow branch and the self-test where the plan names it. Then the checks of the +assembled branch, the epic acceptance, the PR and CI, the stand for the engineer +by the project's stand document (none — the PR alone) and one question +«Одобрить / Замечания / Стоп». «Замечания» sends the keys they touch back to the +plan rework and executes them again on the same worktree; «Одобрить» merges the PR +by the `/ship` procedure and runs the project's `release` procedure, none — the +merge only. The flow lines of the score (`- PR:`, `- Стенд:`, `- Замечания:`, +`- Стоп:`, `- Одобрено:`, `- Смержено:`, `- Релиз:`) let a restarted run continue. +The statuses end at `merged`; `done` stays the engineer's. ## Statuses in mypi @@ -308,7 +328,8 @@ Execution after the agreement is not part of the Pi skill yet: the run stops wit - `review`: implementation finished, review clean, checks green, PR open (single mode) or merged into the flow branch (flow mode). Waiting for the engineer's manual check. -- `merged`: the PR is merged into the PR target by `/ship`. +- `merged`: the PR is merged into the PR target by `/ship` (in Pi, by `/flow` after + «Одобрить»). - `done`: the engineer's word after the installed engine runs the change. The pipeline never sets it. - `blocked`: the pipeline stopped on an open point; the reason names it. @@ -355,7 +376,8 @@ One writer per fact; nothing is written twice. writes one `note` in the project scope (`note` kind `journal`, scope `{"type":"project","key":"<CODE>"}`) per release, engine update or decision taken in conversation: what was decided or shipped, the task keys and the key - of the session it was decided in; the entry's time is its timestamp. The + of the session it was decided in; the entry's time is its timestamp. A release + the Pi `/flow` runs after «Одобрить» is noted by that run the same way. The per-task `merged → done` line stays per task, with only the version in its reason. Never a copy of a task's entries in the project journal (`docs/M1-CLI.md`, «Journal filters»). @@ -377,6 +399,11 @@ One writer per fact; nothing is written twice. --parent <KEY>` for the agreed subtasks. The answer «Завести и запустить /flow» in the epic's chat equals typing `/flow <name> <KEY>`; `/ship` runs only when typed. A parent message is never an answer to these questions. +- In the chat of a Pi `/flow` the engineer's answer «Одобрить» to the question + after the PR equals typing `/ship flow/<name>` and is the explicit request for + the release by the procedure the project's `release` in `flow.json` names (none + — the merge only); the run then merges and releases («Pi»). Claude's `/ship` + still runs only when typed, and Claude's `/flow` never releases. - Open comment threads on the plan artifact (`catch_up` with `comments: open`) are remarks. `/plan` reworks the plan and republishes, resolving each thread with `comment` and `set_state: resolved` and a reply saying what changed. `/do` refuses to start while a thread is open, unless typed with @@ -393,6 +420,12 @@ place; `blocked` only when the engineer answers so. - a dependency outside the run that is not `review`, `merged` or `done`; - every fork of a plan, one question at a time; - in Pi, every published plan: «Согласовать» or «Правки»; +- in Pi, the files the reviewer lists outside the plan, one question per key for + all of them: «Убрать» or «Оставить»; +- in Pi, after the PR: «Одобрить», «Замечания» or «Стоп», with the stand by the + project's stand document when it has one; +- in Pi, a release step that would commit, switch a branch or change tracked + files in the base clone, and a failed release step; - an edit of the lead to a plan beyond the plan-writer's output (or an explicit line in the report); - `openPoints` of the executor only the engineer can answer; @@ -456,7 +489,7 @@ else `<checkoutPath>/CLAUDE.md`. - Commits: English, imperative, one concern, no code comments, no attribution trailers (the `task-executor` rule). - Never force-push. Never delete a remote branch. Worktrees are removed only by - `/ship`, after the merge is verified and the tree inspected. + `/ship` (in Pi, by `/flow` after «Одобрить»), after the merge is verified and the tree inspected. ## Hard rules @@ -465,6 +498,8 @@ else `<checkoutPath>/CLAUDE.md`. engine update: those are the engineer's separate authorizations (for MP, task T14 of the plan). Under `/flow` too: no GitHub Release or other publication and no migration of the personal database without the engineer's separate permission. + The one exception is the Pi `/flow`: its answer «Одобрить» is that permission for + the project's `release` procedure («Approval semantics»). - Record state in mypi (`status`, `progress` with artifacts) and in the task folder by «Journal» above; commit home after each change. Artifacts are the task folder's files named in `progress`; each entry points at the home commit From 83f53c7013230d1f64799d762bc58f523e7c7008 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Mon, 9 Nov 2026 19:59:33 +0100 Subject: [PATCH 30/48] Name the Pi flow release exception, its tests and the changelog entry --- CHANGELOG.md | 1 + docs/TESTING.md | 2 +- integrations/skills/project-management/SKILL.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a83c38..2005fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ from existing transcripts. MEMORY.md is no longer read; move facts into INDEX.md - Skill `/epic` and role `plan-challenger` (MP-107: MP-109): `/epic <KEY>` reads the epic's step from its status and folder (`docs/PIPELINE.md`, «Epics») and leads research, one review round, publication, an independent check by the read-only role `plan-challenger`, one question per finding with its decision row, version 2 and the subtasks created with `--parent`; the answer «Завести и запустить /flow» runs `/flow <name> <KEY>`. Claude Code only, through `.claude/skills/epic` and `.claude/agents`. - Pi pipeline roles and templates (MP-116, epic MP-68): `integrations/pi-flow/` holds the roles `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator` and `challenger`, each with its answer schema in `outputSchema`, and the workflow templates `standard.js` (implement, review, fix through `resume`, at most two review rounds), `parallel.js` and `investigate.js` (read-only fan-out). The tracked `.pi/settings.json` makes pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (own roles and templates) and start children without ambient extensions; in Pi sessions in the engine root `scout` and `reviewer` shadow the pi-subagents builtins of those names. `docs/agents/agent-integration.md` describes the role and template formats, own roles in `home/pi-flow/` and the personal model tiers. The roles are run by hand with `subagent` until the Pi `/flow` arrives; `.claude/agents` is unchanged. - Pi `/flow`, part 1 (MP-117, epic MP-68): the Pi-only skill `integrations/skills/flow/SKILL.md`, run by the extension command `/flow`, takes the tasks of one project from the typed command to an agreed plan — session scope, subtasks, order, flow branch and score, parallel reconnaissance by `scout` through `investigate.js`, the plan by `planner` with its «Execution» section, publication on derive and the question «Согласовать / Правки»; `/flow <name>` resumes from the flow score after a restart. Every question goes through the new extension tool `mypi_ask` (2–4 options, the recommended one first, «Своё слово»; Escape cancels: the run stops, records no answer to that question and only commits the answers already written). Execution after the agreed plan is part 2 (MP-118); Claude's `/flow` is unchanged. +- Pi `/flow`, part 2 (MP-118, epic MP-68): after «Согласовать» each key runs by its plan's «Execution» — the base check, the task worktree, the workflow template through `subagent` in the background while the next plan is agreed, `do-report.json` and `review-report.json` (rounds with their range, findings with their resolution, checked by one line), fixes by resuming the executor, the project's checks by the lead (three attempts), the `--no-ff` merge into the flow branch and the self-test by `tester` by the project's stand document `<project home>/stand.md`; then the checks of the assembled branch, the epic acceptance, the PR and CI, the stand handed to the engineer and the question «Одобрить / Замечания / Стоп». «Замечания» sends the keys they touch back to the plan rework and executes them again; «Одобрить» merges the PR by the `/ship` procedure and runs the project's `release` procedure (none — merge only); the statuses end at `merged`. The reviewer role gains `outsidePlan` (`standard.js` and `parallel.js` stop before a fix and the lead asks once per key), each review round carries its `range`, `tester` can leave a stand up for the engineer (`handover`), and plans gain a release line in «Execution». Claude's `/flow` and `/ship` are unchanged. ### Changed diff --git a/docs/TESTING.md b/docs/TESTING.md index 3f98682..2358f38 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -129,7 +129,7 @@ This is a placement guide, not a manual per-file catalog or eight separate runne | Project flow | Parsing and validation of flow.json; MCP/CLI translation; the warmup's flow block | `project flow` on a temp clone: defaults, the file, refusals | | CLI/bootstrap | In-process parsing/dispatch of every command | Launcher, exit/stdout/stderr and required save→reread path without Python | | Agent docs | AGENTS.md core within 8,000 characters; every link, anchor and repository path in AGENTS.md and docs/**/*.md exists (test/fast/agent-docs.test.ts) | — | -| Pi pipeline resources | .pi/settings.json equals its expected object; integrations/pi-flow holds exactly the seven roles with name, description, tools, acceptance role, project context, no model or extensions, and a closed one-line outputSchema (reviewer status approved or changes_required); the three workflow templates open with a header and compile as a workflow body (test/fast/pi-flow.test.ts) | Stand with a real Pi: subagent list, template validation, one reviewer run | +| Pi pipeline resources | .pi/settings.json equals its expected object; integrations/pi-flow holds exactly the seven roles with name, description, tools, acceptance role, project context, no model or extensions, and a closed one-line outputSchema (reviewer status approved or changes_required and its outsidePlan list; tester handover); the three workflow templates open with a header and compile as a workflow body, and standard.js and parallel.js run against a stub runs: each review round returns its range and outside-plan files, blocking findings go to the executor through resume, a round with outside-plan files ends the run before a fix (test/fast/pi-flow.test.ts) | Stand with a real Pi: subagent list, template validation, one reviewer run; the Pi /flow to the merge and the release on fixtures with a gh stub (MP-118) | | Shared skills | every integrations/skills/*/SKILL.md frontmatter: name equals its directory, non-empty description, no unquoted value containing ": " (test/fast/skills.test.ts) | — | | Prompts | how-to within 2,200 characters, every role within 1,300; the knowledge concepts paragraph in every level's warmup (test/fast/prompts.test.ts, test/fast/warmup.test.ts) | — | | HTTP API | Pure handler: auth, routing, validation, cursor paging, response shapes; keyset pages of tasks, events and journal; data_version feed; token file | One loopback serve subprocess: 401/403, a cursor read, a write, changeId and SSE after it, access log without secrets | diff --git a/integrations/skills/project-management/SKILL.md b/integrations/skills/project-management/SKILL.md index 0dca43e..656d39a 100644 --- a/integrations/skills/project-management/SKILL.md +++ b/integrations/skills/project-management/SKILL.md @@ -39,7 +39,7 @@ The main chat administers project clones, only on the user's request: ## Project flow -- A registered project may have a machine-readable flow in `home/projects/<org>/<project>/flow.json`. Fields, all optional: `integration` (the branch tasks start from), `taskBranch` (template with `{KEY}` and optionally `{slug}`), `prTarget` (the branch PRs go into), `mergeMethod` (`merge`, `squash` or `rebase`), `release` (an https URL or a path relative to the project's clone naming the release procedure; it is only shown, never run). There are no presets and no other fields. +- A registered project may have a machine-readable flow in `home/projects/<org>/<project>/flow.json`. Fields, all optional: `integration` (the branch tasks start from), `taskBranch` (template with `{KEY}` and optionally `{slug}`), `prTarget` (the branch PRs go into), `mergeMethod` (`merge`, `squash` or `rebase`), `release` (an https URL or a path relative to the project's clone naming the release procedure; it is only shown, never run, except by the Pi `/flow` after the engineer's answer «Одобрить» (`docs/PIPELINE.md`, «Approval semantics»)). There are no presets and no other fields. - Defaults without the file or a field: `integration` `origin/HEAD` of the base clone, else `main`; `taskBranch` `task/{KEY}-{slug}`; `prTarget` the integration branch; `mergeMethod` `merge`; no release procedure. - Validation: branch names follow `git check-ref-format --branch`; the template must contain `{KEY}`, use no other placeholders and give a valid branch name. An invalid file makes `task worktree`, `project flow` and the pipeline refuse with the file and the field; the warmup shows `Flow: invalid — …`. - Example (gitflow): `{"integration":"develop","taskBranch":"feature/{KEY}-{slug}","prTarget":"develop","release":"docs/RELEASE.md"}`. From dbddca73520b7f7fca6836f4e144f4728e1534f2 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:04:55 +0100 Subject: [PATCH 31/48] Start the Pi flow base check of a reworked key from its own last flow merge --- integrations/skills/flow/SKILL.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 9108c66..90ea478 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -261,13 +261,16 @@ Strictly one key executes at a time. `<task folder>` is `$BIN/mp-task dir <KEY>` `<flow worktree>` and `<base clone>` are those of «Flow branch». The stage, decided by the task folder without asking: the status is `review`, `pr.md` names this flow and the line is not ticked → step 8; `do-report.json` exists → step 5; otherwise -step 1. An answer already recorded in the key's `progress.md` for this key and this +step 1 — also for a key back from «Замечания», whose base check runs before the +template of step 3. An answer already recorded in the key's `progress.md` for this key and this plan version is not asked again. 1. **Base check.** `git -C <base clone> fetch -q origin`, then - `git -C <base clone> log --stat <scout SHA>..origin/flow/<name>` with the newest - ` — разведка <SHA>` of the key's line (no such mark — skip the check and name - that in the final report). When the log touches a file `plan.md` names: run + `git -C <base clone> log --stat <from>..origin/flow/<name>`. `<from>` is the SHA + of the last ` — разведка <SHA>` or ` — смержено <SHA>` mark of the key's line, + whichever comes later: for a key back from «Замечания» that is its own last flow + merge, so only the changes of other keys count (no such mark — skip the check + and name that in the final report). When the log touches a file `plan.md` names: run `subagent({ agent: "scout", task })` for the key with that log in its task, then the planner revises `plan.md` with the new facts as one remark («база потока изменилась: <files>»), step 5 of «Per key» as rework and step 6; after From 20dc3ab362d8edb6429b1ba9243f2c60a4fd98be Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:06:05 +0100 Subject: [PATCH 32/48] Describe the Pi /flow command as running to the merge and the release --- .pi/extensions/mypi-lib/install.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pi/extensions/mypi-lib/install.ts b/.pi/extensions/mypi-lib/install.ts index 313aee9..811933a 100644 --- a/.pi/extensions/mypi-lib/install.ts +++ b/.pi/extensions/mypi-lib/install.ts @@ -266,7 +266,7 @@ export function install(pi: ExtensionAPI, root: string, serve: ServeEvents, runt }, }); pi.registerCommand('flow', { - description: 'Run tasks of one project to an agreed plan in Pi: /flow <name> <KEY> [<KEY>…], or /flow <name> to resume', + description: 'Run tasks of one project in Pi from the plan to the merge and the release: /flow <name> <KEY> [<KEY>…], or /flow <name> to resume', handler: async args => { pi.sendUserMessage(('/skill:flow ' + args.trim()).trim(), { expandPromptTemplates: true }); }, From d6d89d7db1444927b4a88e88933f5255384c3cdc Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:06:41 +0100 Subject: [PATCH 33/48] Take every Pi flow plan URL from the key's derive.json --- integrations/skills/flow/SKILL.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 90ea478..c41d535 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -262,8 +262,12 @@ Strictly one key executes at a time. `<task folder>` is `$BIN/mp-task dir <KEY>` by the task folder without asking: the status is `review`, `pr.md` names this flow and the line is not ticked → step 8; `do-report.json` exists → step 5; otherwise step 1 — also for a key back from «Замечания», whose base check runs before the -template of step 3. An answer already recorded in the key's `progress.md` for this key and this -plan version is not asked again. +template of step 3. An answer already recorded in the key's `progress.md` for this +key and this plan version is not asked again. + +A key's plan URL — in its statuses, `pr.md`, the PR body and every question — is +`url` of its `derive.json`, read from the file each time; never a URL written from +memory or made up. 1. **Base check.** `git -C <base clone> fetch -q origin`, then `git -C <base clone> log --stat <from>..origin/flow/<name>`. `<from>` is the SHA From 154297731b430d9ea350f5e40fc7b29528401e0e Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:07:15 +0100 Subject: [PATCH 34/48] Place every Pi flow line in the score header with its own commit --- integrations/skills/flow/SKILL.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index c41d535..6fe07c8 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -398,7 +398,12 @@ memory or made up. ## Finish -The flow lines of the score are appended to its header list, after `- Started:`. +The flow lines of the score — `- PR:`, `- Стенд:`, `- Замечания:`, `- Стоп:`, +`- Одобрено:`, `- Смержено:`, `- Релиз начат:`, `- Релиз:` — go into its header +list, each right after the last line of that list (`- Started:` or the flow line +before it), never under «Order», «Skipped» or «Answers» and never at the end of the +file; each is committed by its own `mp-home-commit` as soon as it is written, +before the next step starts. On resume, unticked keys go first by «Per key»; then the newest of these lines decides: none → step 1 here; `- PR:` or `- Стоп:` → «The engineer's check» step 1; `- Стенд:` → its step 2; `- Замечания:` → step 1 here; `- Одобрено:` → «Ship»; @@ -517,7 +522,8 @@ The `/ship` procedure for `flow/<name>` (`.claude/skills/ship/SKILL.md`, steps 2. Otherwise: a `- Релиз начат:` line without a later `- Релиз:` → one `mypi_ask` with the procedure and what its done criterion shows now, options `Остановить`, `Повторить релиз`. Else append `- Релиз начат: <YYYY-MM-DD HH:MM>, процедура - <release>`; `$BIN/mp-home-commit <CODE> "flow <name>: release started"`. + <release>`; `$BIN/mp-home-commit <CODE> "flow <name>: release started"`, both + before step 3. 3. `git -C <base clone> fetch -q origin`. The base clone must be on `<prTarget>` and clean (`git -C <base clone> status --porcelain` empty), then `git -C <base clone> merge -q --ff-only origin/<prTarget>`; anything else → a From ecb2c924529cc91ac17689b18b7a5ca07015016a Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:07:50 +0100 Subject: [PATCH 35/48] Keep Pi flow acceptance remarks in progress.md, out of the score --- integrations/skills/flow/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 6fe07c8..d065c14 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -473,7 +473,9 @@ report. it names); a remark that names none → one `mypi_ask` with the remark, options up to four keys of the run whose plans' files it is closest to, closest first. Per touched key: append `- <YYYY-MM-DD HH:MM> замечания приёмки: <remark>` to - its `progress.md`; move `do-report.json` and `review-report.json` to + its `progress.md` — only there: the remark texts never go into the flow score, + whose «Answers» line for this question reads `→ Замечания: <keys>`; move + `do-report.json` and `review-report.json` to `do-report-v<version>.json` and `review-report-v<version>.json` (`version` of `derive.json`); untick its line and append ` — замечания`. Append `- Замечания: <YYYY-MM-DD HH:MM>: <keys>`; `$BIN/mp-home-commit <CODE> "flow From 42e1ef39c435f59f14a7dfcbc9382788ec808bce Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:08:51 +0100 Subject: [PATCH 36/48] Push the Pi flow branch before the self-test --- integrations/skills/flow/SKILL.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index d065c14..80b9623 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -370,8 +370,10 @@ memory or made up. (blocked)`, `Ещё попытка`. Keep the last output's tail for `pr.md`. 7. **Merge into the flow branch.** `git -C <worktree> push -u origin <branch>`; `git -C <flow worktree> merge --no-ff <branch> -m "Merge <KEY>: <title>"`; - `git -C <flow worktree> push`. A conflict → `git -C <flow worktree> merge - --abort` and a key stop question with the literal output. Write `pr.md` in the + `git -C <flow worktree> push`, always before step 8: the stand clones the commit + from origin. A conflict → `git -C <flow worktree> merge --abort` and a key stop + question with the literal output; a failed push → a key stop question with the + literal output. Write `pr.md` in the task folder: `# <KEY> — доставка (поток <name>)`, the flow merge SHA and the branch, the plan URL, the checks with their exit codes and the tail, the review rounds with what each fixed, open advice, the executor's `deviations`, the @@ -379,7 +381,8 @@ memory or made up. `$BIN/mp-task progress <KEY> "Реализовано, ревью чистое, проверки зелёные: <flow merge SHA>" plan.md pr.md do-report.json review-report.json`; `$BIN/mp-task status <KEY> review "в потоке <name>, слито <flow merge SHA>"`; `$BIN/mp-home-commit <KEY> "<KEY>: merged into flow/<name>"`. -8. **Self-test**, only when the plan's «Execution» names self-test items and a +8. **Self-test**, after the push of step 7 (`git -C <flow worktree> status -sb` + shows no `ahead`), only when the plan's «Execution» names self-test items and a stand (not «none») and `<project home>/stand.md` exists: `subagent({ agent: "tester", task })` with the plan, the commit `git -C <flow worktree> rev-parse HEAD`, the stand document, the task folder, From 566bb140ebb7d2f25cd2ce2de44d878bb8cafcb2 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:09:35 +0100 Subject: [PATCH 37/48] List every executed key in the Pi flow PR title --- integrations/skills/flow/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 80b9623..ea9c8d7 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -434,7 +434,9 @@ report. 3. **PR.** `gh pr list --repo <repository> --head flow/<name> --state open --json url --jq '.[0].url // ""'`: a URL → that PR (the pushes have updated it); otherwise `gh pr create --repo <repository> --base <prTarget> --head - flow/<name> --title "Flow <name>: <KEY>, <KEY>, …" --body "<body>"`, the body + flow/<name> --title "Flow <name>: <KEY>, <KEY>, …" --body "<body>"` — also for + one key; the title lists every key of the run except the epic, in the order of + the score (`Flow fx: FX-2, FX-3`), never the epic's key alone; the body listing per key the title, the plan URL, the review verdict, the checks, the self-test (or «not required») and the engineer's answers; one line for the epic acceptance (result and the path of `acceptance.md`, or «not required») when a From 51d39f8d15b34a18c42ee78434497e43f4d8a1e3 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:10:09 +0100 Subject: [PATCH 38/48] Take every Pi flow time from date --- integrations/skills/flow/SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index ea9c8d7..6fa8e4f 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -43,6 +43,9 @@ session's working directory). `curl`. Roles have no MCP: every derive call and every mypi write is the lead's. - GitHub: `gh` through bash, for the PR, its checks and its merge. - Questions: `mypi_ask` («Questions»). +- Time: every `<YYYY-MM-DD HH:MM>` and `<YYYY-MM-DD>` of this skill is the output of + `date '+%Y-%m-%d %H:%M'` (or `date +%F`) run through bash right before the write + that carries it; never a time typed by hand or guessed. ## Step 0 — session scope From 9de7b9933a9f0dc32fb6a8766d9f01b7554c33a3 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:18:18 +0100 Subject: [PATCH 39/48] Resume the Pi flow inside Ship after a merged key or a removed worktree --- integrations/skills/flow/SKILL.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 6fa8e4f..04190b5 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -76,8 +76,9 @@ is created or pushed and before the flow score is written. - Every key exists and is `new`, `planning`, `in_progress` (resume) or `review` (already merged into this flow — skipped; or the epic of «Subtasks»: a typed key in `review` whose `$BIN/mp-task subtasks <KEY>` is not empty and that has no - `pr.md` naming this flow), or `merged` when the flow score has a `- Смержено:` - line (a resume before the release). Anything else → stop before starting. + `pr.md` naming this flow), or `merged` when the flow score has a `- Одобрено:` + or `- Смержено:` line (a resume inside «Ship» or before the release). Anything + else → stop before starting. - `$BIN/mp-task project <first KEY>` gives `checkoutPath`, `home` and `repository`; `checkoutPath` or `repository` `null` → say so and stop. - `$BIN/mp-task flow <first KEY>` gives the project's `integration` and `prTarget`; @@ -499,6 +500,9 @@ report. The `/ship` procedure for `flow/<name>` (`.claude/skills/ship/SKILL.md`, steps 2–6), run by this skill after «Одобрить»: +First `gh pr view <url> --json state --jq .state`: `MERGED` (a resume after the +merge) → skip steps 1–3 and take the merge SHA as in step 3. + 1. `git -C <flow worktree> fetch -q origin`, `git -C <flow worktree> merge origin/<prTarget>`, never a rebase. Conflicts by the rule of `/ship` step 2: read the neighbour's merged PR and the plans, keep both intents, never @@ -514,8 +518,11 @@ The `/ship` procedure for `flow/<name>` (`.claude/skills/ship/SKILL.md`, steps 4. For every key except the epic, in order, unless it is already `merged`: `$BIN/mp-task status <KEY> merged "смержено в <prTarget>: <merge SHA>"`; `$BIN/mp-task progress <KEY> "Смержено: <PR url>" pr.md progress.md`. -5. Clean up each key's worktree and part worktrees, then the flow worktree: - `git -C <wt> status --porcelain --ignored`; untracked files that are not build +5. Clean up each key's worktree and part worktrees, then the flow worktree; a + worktree whose path is gone, or a branch `git -C <base clone> rev-parse -q + --verify refs/heads/<branch>` does not find, is already removed and skipped, no + git runs on it. For each remaining one: `git -C <wt> status --porcelain + --ignored`; untracked files that are not build output move to the key's task folder (the flow worktree's to `<project home>/flows/<name>/`); `git -C <base clone> worktree remove <wt>`; `git -C <base clone> branch -d` for each task branch, part branch and `flow/<name>`. Remote From 297e5611bc8767387dfb75c03c256330e88ba1c8 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:19:18 +0100 Subject: [PATCH 40/48] List the release-started line among the Pi flow resume lines --- docs/PIPELINE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index 0ff0da9..b3b1e2b 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -315,7 +315,8 @@ by the project's stand document (none — the PR alone) and one question plan rework and executes them again on the same worktree; «Одобрить» merges the PR by the `/ship` procedure and runs the project's `release` procedure, none — the merge only. The flow lines of the score (`- PR:`, `- Стенд:`, `- Замечания:`, -`- Стоп:`, `- Одобрено:`, `- Смержено:`, `- Релиз:`) let a restarted run continue. +`- Стоп:`, `- Одобрено:`, `- Смержено:`, `- Релиз начат:`, `- Релиз:`) let a restarted +run continue. The statuses end at `merged`; `done` stays the engineer's. ## Statuses in mypi From c699342903b11cc36c69bbc585e3b74a68b47c77 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:20:16 +0100 Subject: [PATCH 41/48] Resume the Pi flow after the epic acceptance at the PR step --- integrations/skills/flow/SKILL.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md index 04190b5..89f5c68 100644 --- a/integrations/skills/flow/SKILL.md +++ b/integrations/skills/flow/SKILL.md @@ -405,14 +405,15 @@ memory or made up. ## Finish -The flow lines of the score — `- PR:`, `- Стенд:`, `- Замечания:`, `- Стоп:`, -`- Одобрено:`, `- Смержено:`, `- Релиз начат:`, `- Релиз:` — go into its header -list, each right after the last line of that list (`- Started:` or the flow line -before it), never under «Order», «Skipped» or «Answers» and never at the end of the -file; each is committed by its own `mp-home-commit` as soon as it is written, +The flow lines of the score — `- Приёмка эпика:`, `- PR:`, `- Стенд:`, +`- Замечания:`, `- Стоп:`, `- Одобрено:`, `- Смержено:`, `- Релиз начат:`, +`- Релиз:` — go into its header list, each right after the last line of that list +(`- Started:` or the flow line before it), never under «Order», «Skipped» or +«Answers» and never at the end of the file; each is committed by its own `mp-home-commit` as soon as it is written, before the next step starts. On resume, unticked keys go first by «Per key»; then the newest of these lines -decides: none → step 1 here; `- PR:` or `- Стоп:` → «The engineer's check» step 1; +decides: none → step 1 here; `- Приёмка эпика:` → step 3 here (the acceptance is +not run again); `- PR:` or `- Стоп:` → «The engineer's check» step 1; `- Стенд:` → its step 2; `- Замечания:` → step 1 here; `- Одобрено:` → «Ship»; `- Смержено:` → «Release»; `- Релиз начат:` → «Release» step 2; `- Релиз:` → the report. From 54deeedf501578bc9a23a320625fcb2de3acb46b Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 00:22:08 +0100 Subject: [PATCH 42/48] Treat a reviewer answer without outsidePlan as none in the Pi flow templates --- integrations/pi-flow/parallel.js | 4 ++-- integrations/pi-flow/standard.js | 4 ++-- test/fast/pi-flow.test.ts | 13 ++++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/integrations/pi-flow/parallel.js b/integrations/pi-flow/parallel.js index 6baf3dc..3096612 100644 --- a/integrations/pi-flow/parallel.js +++ b/integrations/pi-flow/parallel.js @@ -21,8 +21,8 @@ function rounds(part, context, state, round) { }) .then((review) => { const verdict = review.structuredOutput ?? null; - state.reviews.push({ round, range: `${state.base}..${state.head}`, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [], outsidePlan: verdict ? verdict.outsidePlan : [] }); - if (!verdict || verdict.status !== "changes_required" || verdict.outsidePlan.length > 0 || round === 2 || !state.last.runId) return state; + state.reviews.push({ round, range: `${state.base}..${state.head}`, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [], outsidePlan: verdict ? verdict.outsidePlan || [] : [] }); + if (!verdict || verdict.status !== "changes_required" || (verdict.outsidePlan || []).length > 0 || round === 2 || !state.last.runId) return state; const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); return runs .run(`${part.key}-fix-${round}`, { diff --git a/integrations/pi-flow/standard.js b/integrations/pi-flow/standard.js index 9486b96..2ff513d 100644 --- a/integrations/pi-flow/standard.js +++ b/integrations/pi-flow/standard.js @@ -32,8 +32,8 @@ for (const round of [1, 2]) { task: `${context} Review the diff ${base}..${head} in the worktree against the plan.`, }); const verdict = review.structuredOutput ?? null; - reviews.push({ round, range: `${base}..${head}`, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [], outsidePlan: verdict ? verdict.outsidePlan : [] }); - if (!verdict || verdict.status !== "changes_required" || verdict.outsidePlan.length > 0 || round === 2 || !last.runId) break; + reviews.push({ round, range: `${base}..${head}`, status: verdict ? verdict.status : null, findings: verdict ? verdict.findings : [], outsidePlan: verdict ? verdict.outsidePlan || [] : [] }); + if (!verdict || verdict.status !== "changes_required" || (verdict.outsidePlan || []).length > 0 || round === 2 || !last.runId) break; const blocking = verdict.findings.filter((finding) => finding.severity === "blocking"); const fix = await runs.run(`fix-${round}`, { resume: last.runId, diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts index 7a78dd1..2839832 100644 --- a/test/fast/pi-flow.test.ts +++ b/test/fast/pi-flow.test.ts @@ -81,7 +81,7 @@ test('integrations/pi-flow holds the three workflow templates, each opening with } }); -test('standard.js and parallel.js return each review round with its range and outside-plan files, fix blocking findings through resume, and stop before a fix when files lie outside the plan', async () => { +test('standard.js and parallel.js return each review round with its range and outside-plan files, fix blocking findings through resume, stop before a fix when files lie outside the plan, and treat a reviewer without outsidePlan as none', async () => { const template = (file: string) => new Script('(async (runs, args) => {\n' + read(`integrations/pi-flow/${file}`) + '\n})').runInThisContext(); const blocking = { severity: 'blocking', lens: 1, file: 'a.txt', line: 1, problem: 'p', evidence: 'e', fix: 'f' }; const fixing = { @@ -126,6 +126,17 @@ test('standard.js and parallel.js return each review round with its range and ou assert.equal(result[0].fixes.length, 1); assert.equal(result[0].executor.runId, 'e2'); + const legacy = { ...fixing, 'review-1': { structuredOutput: { status: 'changes_required', findings: [blocking] } } }; + calls = []; + result = await template('standard.js')(stub(legacy, calls), {}); + assert.deepEqual(calls, ['implement', 'review-1', 'fix-1', 'review-2']); + assert.deepEqual(result.reviews[0].outsidePlan, []); + + calls = []; + result = await template('parallel.js')(stub(prefixed(legacy), calls), parts); + assert.deepEqual(calls, ['a-implement', 'a-review-1', 'a-fix-1', 'a-review-2']); + assert.deepEqual(result[0].reviews[0].outsidePlan, []); + calls = []; result = await template('parallel.js')(stub(prefixed(outside), calls), parts); assert.deepEqual(calls, ['a-implement', 'a-review-1']); From 17771c5cacc8d899ecbffb75a972e0c877a3245c Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 08:56:11 +0100 Subject: [PATCH 43/48] List the epic acceptance line among the Pi flow resume lines --- docs/PIPELINE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index b3b1e2b..8a70b2f 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -314,9 +314,9 @@ by the project's stand document (none — the PR alone) and one question «Одобрить / Замечания / Стоп». «Замечания» sends the keys they touch back to the plan rework and executes them again on the same worktree; «Одобрить» merges the PR by the `/ship` procedure and runs the project's `release` procedure, none — the -merge only. The flow lines of the score (`- PR:`, `- Стенд:`, `- Замечания:`, -`- Стоп:`, `- Одобрено:`, `- Смержено:`, `- Релиз начат:`, `- Релиз:`) let a restarted -run continue. +merge only. The flow lines of the score (`- Приёмка эпика:`, `- PR:`, `- Стенд:`, +`- Замечания:`, `- Стоп:`, `- Одобрено:`, `- Смержено:`, `- Релиз начат:`, +`- Релиз:`) let a restarted run continue. The statuses end at `merged`; `done` stays the engineer's. ## Statuses in mypi From 3079b52efd126b669e4a5b4263caf7ab4fb86059 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 09:03:01 +0100 Subject: [PATCH 44/48] Add the Pi /epic command that runs the epic skill --- .pi/extensions/mypi-lib/install.ts | 6 ++++++ test/fast/pi-extension.test.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.pi/extensions/mypi-lib/install.ts b/.pi/extensions/mypi-lib/install.ts index 811933a..65d3f61 100644 --- a/.pi/extensions/mypi-lib/install.ts +++ b/.pi/extensions/mypi-lib/install.ts @@ -271,5 +271,11 @@ export function install(pi: ExtensionAPI, root: string, serve: ServeEvents, runt pi.sendUserMessage(('/skill:flow ' + args.trim()).trim(), { expandPromptTemplates: true }); }, }); + pi.registerCommand('epic', { + description: 'Lead an epic in Pi from research to its subtasks: /epic <KEY>', + handler: async args => { + pi.sendUserMessage(('/skill:epic ' + args.trim()).trim(), { expandPromptTemplates: true }); + }, + }); registerAsk(pi); } diff --git a/test/fast/pi-extension.test.ts b/test/fast/pi-extension.test.ts index da1c3b3..95a121c 100644 --- a/test/fast/pi-extension.test.ts +++ b/test/fast/pi-extension.test.ts @@ -67,7 +67,7 @@ test('pi extension registers mypi MCP with the session id and loads warmup into assert.equal(pi.servers.length, 0); assert.deepEqual(pi.calls, []); assert.deepEqual([...pi.handlers.keys()].sort(), ['agent_end', 'agent_settled', 'agent_start', 'before_agent_start', 'session_shutdown', 'session_start', 'ui_prompt_start']); - assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow']); + assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow', 'epic']); assert.deepEqual([...pi.tools.keys()], ['mypi_ask']); assert.deepEqual(await pi.turn(), {}); pi.state.result = (_command, args) => args[1] === 'session' ? pi.registration() : { code: 0, killed: false, stdout: '# mypi warmup: global\n', stderr: '' }; @@ -396,6 +396,16 @@ test('pi extension /flow runs the flow skill', async () => { assert.equal(pi.commands.get('flow')!.getArgumentCompletions, undefined); }); +test('pi extension /epic runs the epic skill', async () => { + const pi = stub(); + install(pi.api, root, serve, undefined); + await pi.commands.get('epic')!.handler('FX-1', pi.ctx); + await pi.commands.get('epic')!.handler('', pi.ctx); + assert.deepEqual(pi.sent, [['/skill:epic FX-1', { expandPromptTemplates: true }], ['/skill:epic', { expandPromptTemplates: true }]]); + assert.deepEqual(pi.calls, []); + assert.equal(pi.commands.get('epic')!.getArgumentCompletions, undefined); +}); + test('pi extension mypi_ask asks one question with the recommendation first and the engineer\'s own word last', async () => { const pi = stub(); install(pi.api, root, serve, undefined); @@ -467,7 +477,7 @@ test('pi extension /open suggests keys of the session\'s project from the CLI', taskList = tasks; assert.deepEqual((await open.getArgumentCompletions!('') as { label: string }[]).map(item => item.label), ['MP', 'MP-74']); assert.equal(lists(), 3); - assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow']); + assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow', 'epic']); assert.equal(pi.commands.get('inbox')!.getArgumentCompletions, undefined); }); From 1c359d35af97d6c00ff579a248924e19324d3375 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 09:03:57 +0100 Subject: [PATCH 45/48] Add the Pi-only epic skill --- integrations/skills/epic/SKILL.md | 298 ++++++++++++++++++++++++++++++ test/fast/skills.test.ts | 1 + 2 files changed, 299 insertions(+) create mode 100644 integrations/skills/epic/SKILL.md diff --git a/integrations/skills/epic/SKILL.md b/integrations/skills/epic/SKILL.md new file mode 100644 index 0000000..150a16e --- /dev/null +++ b/integrations/skills/epic/SKILL.md @@ -0,0 +1,298 @@ +--- +name: epic +description: 'Pi only. Lead an epic of a registered project — a task whose research proposes a mechanism and its subtasks — through steps 1–5 in its chat: research by the executor role in research mode in the folder of the epic, one reviewer round on version 1, publication on derive, the independent check by the challenger role, one mypi_ask per finding with its decision row, version 2, the «Подзадачи» question and the subtasks created with --parent; the answer «Завести и запустить /flow» hands the run to the Pi /flow skill in this chat. Runs only when typed: "/epic <KEY>".' +--- + +# /epic in Pi — an epic from research to its subtasks + +Read `docs/PIPELINE.md` in the engine root first: «Epics» holds the table of steps +with the commands of their signs, the required sections of the research and the files +of the epic's folder; «Approval semantics» says what typing `/epic` and the answers +authorize; «Pi» binds the Claude pipeline to Pi's tools. This is the Pi counterpart of +Claude's `.claude/skills/epic/SKILL.md` and keeps its rules unless it says otherwise. +Work inline in this chat: questions reach the engineer only from here, through the +`mypi_ask` tool; roles started with `subagent` cannot ask the engineer. +`BIN=scripts/pipeline`; run every script with bash from the engine root (this +session's working directory). One key. + +## Tools + +- Scripts: `$BIN/mp-task …` and `$BIN/mp-home-commit …` through bash. Bash exports + this session's `PI_SESSION_ID`, so `$BIN/mp-task scope` reads this chat's scope. +- Roles: the `subagent` tool. It is enabled lazily: when `subagent` is not + available, call `subagents_enable` first. Call `subagent` with its default options + and without `async: true`; in an interactive session the run goes to the background + and wakes this chat when it ends — wait for it, never rely on `bg_wait`, it may + return at once with «No active async runs». A role that asks the lead with + `contact_supervisor` stays `running` and wakes this chat with + `subagent_supervisor_request`: answer it with `subagent_supervisor` from + `source.md`, the research and the engineer's answers, or ask the engineer through + `mypi_ask` first. A role's own `PI_SESSION_ID` has no scope: every role's task names + the key and the paths. A role's answer is its structured output by its + `outputSchema`. +- The executor of step 1 is continued with `subagent({ action: "resume", id: "<run + id>", message })`: it returns a new run id, and the next resume uses the newest. + The run ids live only in this chat. No run id in this chat (Pi was restarted) or a + refused resume → a fresh `subagent({ agent: "executor", task })` with the + research-mode task of step 1 followed by the message; the report of step 5 names + the fallback. +- Derive: the `derive` MCP server through codemode — `tools.mcp__derive__stage`, + `tools.mcp__derive__organize`, `tools.mcp__derive__read`, each awaited. The upload + goes through bash with `curl`. Roles have no MCP: every derive call and every mypi + write is the lead's. +- Subtasks: `mise exec -- node dist/src/cli/main.js task create …` through bash. +- Questions: `mypi_ask` («Questions»). +- Time: every `<YYYY-MM-DD>` of this skill is the output of `date +%F`, and + `published_at` the output of `date -Iseconds`, run through bash right before the + write that carries it; never a time typed by hand or guessed. + +## Step 0 — session scope + +`$BIN/mp-task scope`, then apply the «Session scope» rule of `docs/PIPELINE.md` to +`/epic <KEY>`: `global` — any key; an organization (`org` is not `null`) — continue +only when the key has a code in `codes`; `P` — continue only when the key has the +code `P`; `P-N` — continue only when the key is `P-N` or its subtask by +`$BIN/mp-task subtasks P-N`; anything else (an argument that is not a key, a +malformed session key, a non-zero exit) — print the refusal text from +`docs/PIPELINE.md` and stop. No argument or more than one → print +`usage: /epic <KEY>` and stop. This step runs before «Input»: before +`$BIN/mp-task show`, before any derive call or `subagent`. + +## Input + +`$BIN/mp-task show <KEY>`: + +- `new` or `planning` → «State»; +- `review` and `$BIN/mp-task subtasks <KEY>` is not empty → print + `<KEY>: подзадачи заведены (<ключи>); следующий шаг, если поток ещё не запущен, — /flow <name> <KEY>; в Pi /ship — ответ «Одобрить» потока` + (`<name>` is the output of `$BIN/mp-task slug <KEY>`) and stop; +- any other status (`review` without subtasks included) → name the status and stop. + +`DIR=$($BIN/mp-task dir <KEY>)`. + +## State + +Read the signs by the table of «Epics» of `docs/PIPELINE.md` with its commands +(`N`, `ROWS`, `V`, `mp-task subtasks`); the step is the first row from the top whose +sign holds. No row holds (for example `planning` without `research.md` — a plan of +`/plan`, not an epic) → print the signs found and stop. The run goes on from that +step through the next ones and stops only on questions and at the end of step 5. +Typing `/epic <KEY>` again after a break continues the same way; the executor a step +needs is resumed, or started fresh, by «Tools». After every write into the epic's +folder: `$BIN/mp-home-commit <KEY> "<message>"` («Places» of `docs/PIPELINE.md`), +with the messages below. + +## Step 1 — research + +Rows «1, not started» and «1, under way». + +1. **Clone.** `$BIN/mp-task project <KEY>` gives `project`, `checkoutPath` and + `home`; when `checkoutPath` is `null`, say the project has no registered clone + and stop. `$BIN/mp-task flow <KEY>` gives `integration`; a non-zero exit → print + its error and stop. `git -C <checkoutPath> fetch -q origin`; then, only when + `git -C <checkoutPath> branch --show-current` prints `<integration>` and + `git -C <checkoutPath> status --porcelain` prints nothing, + `git -C <checkoutPath> merge -q --ff-only origin/<integration>`; otherwise read + `origin/<integration>` and say so. `SHA` is + `git -C <checkoutPath> rev-parse origin/<integration>`. +2. **Reconnaissance**, only when there is no `research.md`. + `subagent({ agent: "scout", task })`; the task carries, as text: the key, the full + `source.md` (`$BIN/mp-task source <KEY>`), the project, the clone path and `SHA` + (read-only; `git -C <clone> show <SHA>:<path>` reads that commit), the project's + folder in home, `.yoke/` and `AGENTS.md` (else `CLAUDE.md`) of the clone when they + exist, the project's checks by «Checks» of `docs/PIPELINE.md`, and the brief: + «исследование эпика: факты с источниками (`repo:<путь>:<строки>` на `SHA`, + `home:<путь>`) о том, что запрос затрагивает сегодня, точки касания, которые + изменят подзадачи, и развилки с рекомендациями». Its forks are not asked of the + engineer: they go to the executor as hypotheses the research weighs. + `solvable: false` → say what `missing` lists and stop without a status change. No + structured output or a failed run → one `mypi_ask` with the error, options + `Повторить разведку`, `Остановить`. +3. **Executor**, only when there is no `research.md`. + `subagent({ agent: "executor", task })`; the research-mode task carries, as text: + «Research mode: the research of an epic, no worktree.»; the key; the path of + `source.md`; `$DIR` as the folder and `$DIR/research.md` as the only file it + writes; the clone and `SHA`, read-only; the scout's `facts` and `forks` verbatim; + the path of `docs/PIPELINE.md`, whose section «Epics» is the contract of the + document; the language Russian, Markdown with `mermaid` diagrams without `style`, + `classDef`, `linkStyle`, `%%{init`; a header paragraph naming «Версия 1», the date + and the full `SHA`; every proposal with its alternatives and one line + `**Рекомендация:**` with the reason; every subtask the size of one PR, its + `**Проверки:**` the project's checks by «Checks» plus its own; the section + «Решения по независимой проверке» in version 1 only the header row and the + separator of the table; the rules: no worktree, no commits or any other Git + write, no diff, no derive, no task changes; its answer by its schema with + `branch` and `head` null, `base` the `SHA`, `commits` 0. Keep its run id. Write + its structured output as `$DIR/do-report.json`. Each of its `openQuestions` only + the engineer can answer is one `mypi_ask`; the answers go to it by resume. +4. **Review**, one round, only when there is no `review-report.json`. + `subagent({ agent: "reviewer", task })`, a fresh run; the task carries, as text: + «Review a document, not a diff: there is no worktree and no diff range; answer by + your schema.»; the key; the paths of `$DIR/research.md` and `source.md`; the clone + and `SHA`, read-only; the section «Epics» of `docs/PIPELINE.md` in place of a + plan; the brief: every `repo:` citation exists on `SHA` and says what is claimed, + every `home:` one exists; the required sections and the fields of the subtasks by + «Epics»; every proposal has an alternative and one recommendation; nothing beyond + the request without a mark saying so; each finding's `file` is the path of + `research.md` and `line` its line there; `outsidePlan` is an empty list. Write its + structured output verbatim as `$DIR/review-report.json` — one object, not the + rounds of the Pi `/flow`. Blocking findings and advice go to the executor by + resume, verbatim, with «fix the blocking findings in research.md, apply advice + when it does not change a recommendation, and answer again with your whole + report»; its report becomes `do-report.json`. Check the fixed places against the + `fix` of each blocking finding by reading; there is no second review round. A + blocking finding that is still not fixed → one `mypi_ask` with the finding and + the place, options `Остановить`, `Опубликовать как есть`. +5. **Publish version 1:** + 1. `await tools.mcp__derive__stage({ target: "doc" })`, no `short_id`. + 2. Upload through bash: `curl -sS -F file=@$DIR/research.md -F title="<KEY> — + исследование: <task title>" <upload_url>`; take `short_id`, `url` and the + version number from the answer. + 3. `tools.mcp__derive__organize({ short_ids: [<short_id>], set: ["<slug>", + "<CODE>", "research"] })` (`<slug>` is the project slug). + 4. Write `$DIR/research-derive.json` `{"short_id","url","version","published_at"}`. + No `derive.json`. + 5. `tools.mcp__derive__read({ short_id, render: "full" })`: a diagram that did not + render is named in the report and fixed in version 2. + 6. `$BIN/mp-task status <KEY> planning "исследование v1 опубликовано: <url>"`; + `$BIN/mp-home-commit <KEY> "<KEY>: research v1"`. + +## Step 2 — independent check + +Row 2. `subagent({ agent: "challenger", task })`, a fresh run; the task carries only: +the path of `source.md` (the verbatim request), the path of `$DIR/research.md` and +its URL, the clone and the `SHA` from the research's header, the paths of +`docs/PIPELINE.md`, `AGENTS.md`, `.claude/skills/`, `.claude/agents/`, +`integrations/skills/` and `integrations/pi-flow/` in the engine root, and +`AGENTS.md` (else `CLAUDE.md`) of the clone when it exists; never the scout's facts, +the review or the lead's conclusions. Write its structured output verbatim as +`$DIR/challenge-report.json`. No structured output or a failed run → one more fresh +run with the defect named in its task; a second failure → one `mypi_ask` with the +error, options `Остановить`, `Повторить проверку`. +`$BIN/mp-home-commit <KEY> "<KEY>: independent check"`. Zero findings → step 4 at +once. + +## Step 3 — decisions + +Row 3 (`ROWS` < `N`). For each finding of `challenge-report.json` in order whose `id` +has no row in the table «Решения по независимой проверке», one `mypi_ask`: +`question` — + +``` +Находка <id> (<type>). +Что сейчас: <now> +Предложение: <proposal> +Что выигрываем / что теряем: <gain_loss> +Обоснование: <reasoning> +Принять — <one line of reasoning>; Отклонить — <one line of reasoning>. +``` + +with the fields verbatim; `options` — `Принять` and `Отклонить`, the lead's +recommendation first. Right after the answer append to the end of the table the row +`| <id> | <находка> | <type> | <принято|отклонено|комментарий: «<дословно>»> | |` +(`<находка>` is `proposal` as one short line without `|`; `Принять` → `принято`, +`Отклонить` → `отклонено`, an own word → `комментарий: «<the answer verbatim>»`) and +run `$BIN/mp-home-commit <KEY> "<KEY>: finding <id>"`; then the next question. The +decisions are written nowhere else: not in `progress.md`, not in `progress`. + +## Step 4 — version 2 + +Row 4 (`ROWS` = `N`, `V` = 1). The executor by resume («Tools») gets the path of +`challenge-report.json`, the rows of the table verbatim and the diagrams that did not +render, with: apply the accepted findings, and the commented ones by their comment, +to the sections; the rejected ones change no text; fill «Что изменилось» of every +row; under the table — where the subtasks of version 1 went; with no findings — the +line «Находок нет»; the header «Версия 2, <YYYY-MM-DD>»; the other rules of the +research-mode task; «answer again with your whole report». Its report becomes +`$DIR/do-report.json`. Check that `ROWS` = `N` and that the last column of every row +is filled (`awk '/^## .*Решения по независимой проверке/,/^## Источники/' +$DIR/research.md | grep '^| [0-9]' | grep -c '| *|$'` prints `0`); not so → the +defect to the executor by resume once; still not so → one `mypi_ask` with the +defect, options `Остановить`, `Опубликовать как есть`. Publication: +`tools.mcp__derive__stage({ target: "doc", short_id })` with the `short_id` of +`research-derive.json`; the same `curl` with +`-F message="v2: независимая проверка — <N> находок, решения инженера в разделе «Решения по независимой проверке»"`; +update `research-derive.json` (the version from the answer, `published_at`); +`tools.mcp__derive__read({ short_id, render: "full" })`. No review of version 2. +`$BIN/mp-home-commit <KEY> "<KEY>: research v2"`. + +## Step 5 — subtasks + +Row 5 (`V` ≥ 2, no subtasks). `<name>` is the output of `$BIN/mp-task slug <KEY>`. + +1. One `mypi_ask`: `question` — + + ``` + Подзадачи <KEY>: + <N>. <title> — <goal>; зависимости: <dependencies>; стенд: <stand> + … + **Приёмка эпика:** <the line of the research> + Поток: <name>. + Завести и запустить /flow — подзадачи заводятся, затем в этом чате идёт /flow <name> <KEY>; Завести — подзадачи заводятся, /flow набираете вы. Своё слово — правка списка; ответ из одного имени потока — завести и запустить /flow с этим именем. + ``` + + `options` — `Завести и запустить /flow`, `Завести`. An own word that matches + `^[a-z0-9][a-z0-9-]*$` is «Завести и запустить /flow» with that word as `<name>`; + any other own word is an edit of the list. +2. **An edit of the list.** The executor by resume gets the edit verbatim and edits + only the section «Подзадачи»; then publish the next version + (`tools.mcp__derive__stage({ target: "doc", short_id })`, `curl` with + `-F message="v<V>: список подзадач поправлен — <кратко>"`), update + `research-derive.json`, `tools.mcp__derive__read({ short_id, render: "full" })`, + `$BIN/mp-home-commit <KEY> "<KEY>: research v<V>"`; the question again. +3. **«Завести …».** For each subtask in the order of the section, a card in a + temporary file `card=$(mktemp)`: `# <название>`; the line + `Подзадача <N> эпика <KEY> (исследование: <url>, раздел «Подзадачи», подзадача <N>).`; + `## Суть` (the goal and the files); `## Связи` (`- Зависит от:`, + `- Нужна для:` — keys of subtasks already created, numbers of those not yet + created); `## Готово` (the items «Готово», «Проверки», «Стенд» of the subtask). + Then `mise exec -- node dist/src/cli/main.js task create --file "$card" --title "<название>" --status new --slug <english-kebab-slug> --project <org/slug> --parent <KEY>`; + the key comes from its output; `rm -f "$card"`. After all of them: replace the + numbers in «Связи» with keys by editing `source.md` in + `$($BIN/mp-task dir <SUB>)`; in `$DIR/research.md` under the heading «Подзадачи» + the line `Подзадачи заведены по согласованию с инженером <YYYY-MM-DD>: <ключи>` + and the key in every `###` (`### <N>. <KEY> — <название>`). No new version on + derive. +4. **Record.** + `$BIN/mp-task progress <KEY> "Исследование v<V> проверено и разобрано; подзадачи <ключи>: <url>" research.md research-derive.json challenge-report.json` + — the only `progress` of the run; + `$BIN/mp-task status <KEY> review "исследование проверено и разобрано, подзадачи заведены: <url>"`; + `$BIN/mp-home-commit <KEY> "<KEY>: research v<V>, subtasks"`. +5. «Завести и запустить /flow» → read `integrations/skills/flow/SKILL.md` in the + engine root and follow it in this same chat with the arguments `<name> <KEY>`, + from its first section: the answer is the engineer's word for `/flow` («Approval + semantics»). The status is `review` before it starts: the flow skill takes the key + as the epic only in `review` with subtasks («Preconditions» and «Subtasks» there). + From here the flow skill's rules hold, its questions and its «Одобрить» included. + «Завести» → the report: the research URL, the number of findings and decisions, + the subtask keys in order, the fallbacks of «Tools» and + `следующий шаг — /flow <name> <KEY>, набираете вы; /ship — ответ «Одобрить» этого потока`. + +## Questions + +Every question of this skill is one `mypi_ask` call: `question` — the facts or the +finding and one line of reasoning per option; `options` — 2–4 short labels, the +recommended one first, without «(рекомендую)» and without «Своё слово» (the tool adds +both). Never ask in plain chat text and never two questions in one call. The run +waits for the answer; no retry, no bypass. A parent message is never an answer. + +- `{ answer, custom }` → act on it as the step says. The answers are not appended to + `progress.md`: the decisions on the findings live only in the table, the other + answers in the files their step writes. +- `{ answer: null, cancelled: true }` → stop the run at once. The cancelled answer is + recorded nowhere; the only write after it is `$BIN/mp-home-commit <KEY> "<KEY>: + stopped"`, which commits what is already written. Report `остановлено инженером: + <the first line of the question>`; typing `/epic <KEY>` again resumes by «State». +- An error result (no interactive UI) → stop and say that `/epic` needs an + interactive Pi session. + +## Never + +- `/ship` inside `/epic`; the handed-over Pi `/flow` asks «Одобрить» itself. +- `/flow` on a parent message or without the answer «Завести и запустить /flow». +- Steps 6–8 inside `/epic`, except handing the run to the flow skill by that answer. +- A worktree, a branch or a commit in the epic's clone. +- An edit of the engine. +- A second review. +- Decisions in `progress.md`. +- More than one `progress` per run. diff --git a/test/fast/skills.test.ts b/test/fast/skills.test.ts index 1979215..71e2568 100644 --- a/test/fast/skills.test.ts +++ b/test/fast/skills.test.ts @@ -8,6 +8,7 @@ const read = (path: string) => readFileSync(new URL(path, root), 'utf8'); test('every skill in integrations/skills has a frontmatter Pi can parse: its directory name, a description and no unquoted value with ": "', () => { const skills = readdirSync(new URL('integrations/skills/', root)).sort(); assert.ok(skills.includes('flow')); + assert.ok(skills.includes('epic')); for (const skill of skills) { const match = /^---\n([\s\S]*?)\n---\n/.exec(read(`integrations/skills/${skill}/SKILL.md`)); assert(match !== null, skill); From b646dba4b853c838252e8500772fa34247ece39f Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 09:04:43 +0100 Subject: [PATCH 46/48] Document the Pi /epic in the pipeline --- docs/PIPELINE.md | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index 8a70b2f..5cf559d 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -5,8 +5,9 @@ session starts there, so each pipeline skill checks the session scope first (see «Session scope»). It is tracked in the engine repository: this file, `docs/PLAN-FORMAT.md`, the scripts in `scripts/pipeline/`, the skills `/plan`, `/do`, `/epic`, `/flow`, `/ship`, `/board` in `.claude/skills/` and the roles in `.claude/agents/` (copied from yokemate; `plan-challenger` is the pipeline's own). For Pi, the roles and workflow templates in `integrations/pi-flow/`, -found through `.pi/settings.json` («Roles»), and the Pi `/flow` skill -`integrations/skills/flow/SKILL.md` («Pi»). Claude +found through `.pi/settings.json` («Roles»), and the Pi skills `/flow` +(`integrations/skills/flow/SKILL.md`) and `/epic` +(`integrations/skills/epic/SKILL.md`) («Pi»). Claude Code loads the skills and roles when it starts in the engine root; the scripts act on the installation they are run from and refuse any other working directory. @@ -166,11 +167,13 @@ whose message started the turn, else the project chat. ## Epics An epic is a task whose research proposes a mechanism and its subtasks. -`/epic <KEY>` (`.claude/skills/epic/SKILL.md`) leads steps 1–5 in the epic's chat: +`/epic <KEY>` (`.claude/skills/epic/SKILL.md`; in Pi +`integrations/skills/epic/SKILL.md`, «Pi») leads steps 1–5 in the epic's chat: research, independent check (`plan-challenger`), the engineer's decision on each finding, version 2, subtasks. `/flow <name> <KEY>` runs steps 6–8 (the epic key and the epic acceptance of «Subtasks» above), started by the engineer's answer in -`/epic` or typed; `/ship flow/<name>` merges. The step comes from the epic's status +`/epic` or typed; `/ship flow/<name>` merges. In Pi the answer starts the Pi `/flow`, +whose answer «Одобрить» merges in place of a typed `/ship`. The step comes from the epic's status and the files of its folder; there is no epic status, field or engine command. `DIR` is `scripts/pipeline/mp-task dir <KEY>`; `N` — @@ -187,7 +190,7 @@ The first row from the top whose sign holds is the step: | `ROWS` < `N` | 3 | `/epic <KEY>`: a question on each remaining finding | | `ROWS` = `N`, `V` = 1 | 4 | `/epic <KEY>`: version 2 | | `V` ≥ 2, `mp-task subtasks <KEY>` empty | 5 | `/epic <KEY>`: the «Подзадачи» question | -| `mp-task subtasks <KEY>` not empty | 6–8 | `/flow <name> <KEY>`, then `/ship flow/<name>` | +| `mp-task subtasks <KEY>` not empty | 6–8 | `/flow <name> <KEY>`, then `/ship flow/<name>` (in Pi the answer «Одобрить» of the flow) | Statuses of an epic: `new` while the research is written; `planning` with the research URL once version 1 is published; `review` once the subtasks are @@ -221,7 +224,8 @@ version 1); `challenge-report.json` (the `plan-challenger` answer, verbatim); `acceptance.md` and its evidence (the epic acceptance, «Subtasks»). The journal gets one `progress` per `/epic` run, at step 5, with `research.md`, `research-derive.json` and `challenge-report.json`; the decisions live only in -the table. +the table. In Pi these files come from the roles `executor` in research mode, +`reviewer` and `challenger` («Pi»). ## Temporary scripts @@ -256,7 +260,7 @@ recorded in the flow score. What every executor prompt carries: the task key, the plan path, the worktree path, the task folder path, the base branch, this file's path, and the rule that its -final message is exactly one JSON object per its definition. Under `/epic` the epic's folder and the clone's SHA take the place of the worktree and the base branch («Epics»). +final message is exactly one JSON object per its definition. Under `/epic` the epic's folder and the clone's SHA take the place of the worktree and the base branch («Epics»). In the Pi `/epic` the role `executor` runs in that research mode, and `subagent({ action: "resume" })` of its run takes the place of the teammate and `SendMessage` («Pi»). Pi's roles live in `integrations/pi-flow/`: `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator`, `challenger`, each a `*.md` file whose @@ -277,17 +281,22 @@ model tiers: `docs/agents/agent-integration.md`. ## Pi -Pi runs `/flow` from `integrations/skills/flow/SKILL.md`; it is Pi-only and keeps -the rules of the Claude skill with these bindings: +Pi runs `/flow` from `integrations/skills/flow/SKILL.md` and `/epic` from +`integrations/skills/epic/SKILL.md`; both are Pi-only and keep the rules of their +Claude skills with these bindings: | Claude | Pi | | --- | --- | | the skill `.claude/skills/flow` | the extension command `/flow`, which sends `/skill:flow <args>` | +| the skill `.claude/skills/epic` | the extension command `/epic`, which sends `/skill:epic <args>` | | `AskUserQuestion` | the extension tool `mypi_ask`: `question` and 2–4 `options`, the recommended one first; the tool marks it «(рекомендую)» and adds «Своё слово» | | the `plan` skill with `plan-scout` and `plan-writer` | `subagent`: `integrations/pi-flow/investigate.js` with `scout`, then `planner` | | `/do <KEY> --base flow/<name>` with `task-executor` and `task-reviewer` | the plan's «Execution»: a template of `integrations/pi-flow/` or `home/pi-flow/` through `subagent`, fixes by `subagent({ action: "resume" })` of the executor | | the live acceptance of `/flow` | the `tester` role by the project's stand document `<project home>/stand.md` | | the manual check, then a typed `/ship flow/<name>` | the stand handed over in the question «Одобрить / Замечания / Стоп»; «Одобрить» runs the `/ship` procedure and the release | +| the `task-executor` teammate of `/epic` and `SendMessage` | the `executor` role in research mode through `subagent`; the review findings, version 2 and an edit of the subtask list by `subagent({ action: "resume" })`, a fresh `executor` with the research-mode task when the run is not retained | +| `task-reviewer` and `plan-challenger` of `/epic` | the roles `reviewer` (a review of the document, `outsidePlan` empty) and `challenger` | +| «Завести и запустить /flow» runs the `flow` skill | the lead reads `integrations/skills/flow/SKILL.md` and follows it in the same chat with `<name> <KEY>` | | the derive MCP tools | `mcp__derive__*` through codemode; the upload by `curl` | | `CLAUDE_CODE_SESSION_ID` for `mp-task scope` | `PI_SESSION_ID`, exported by Pi's bash | | re-typing the command | the same; `/flow <name>` without keys in a project or task chat resumes the keys of the flow score | @@ -319,6 +328,15 @@ merge only. The flow lines of the score (`- Приёмка эпика:`, `- PR:` `- Релиз:`) let a restarted run continue. The statuses end at `merged`; `done` stays the engineer's. +The Pi `/epic` keeps its state where «Epics» says, so a restarted Pi continues by +the signs; a resume of the executor that is no longer retained becomes a fresh +`executor` with the research-mode task, named in the report. Each finding is one +`mypi_ask` («Принять» / «Отклонить», an own word is a comment) and its row in the +table; the «Подзадачи» question has «Завести и запустить /flow» and «Завести», an +own word edits the list, and one flow name alone creates and runs with that name. +Its answers are not written to `progress.md`. A cancelled `mypi_ask` stops the run; +only `mp-home-commit` commits what is already written. + ## Statuses in mypi `new → planning → in_progress → review → merged → done`, plus `blocked`. @@ -399,7 +417,8 @@ One writer per fact; nothing is written twice. after the engineer's answer to the «Подзадачи» question, `mypi task create --parent <KEY>` for the agreed subtasks. The answer «Завести и запустить /flow» in the epic's chat equals typing `/flow <name> <KEY>`; `/ship` runs only when - typed. A parent message is never an answer to these questions. + typed, except that in Pi this `/flow` is the Pi `/flow`, whose answer «Одобрить» + stands for `/ship flow/<name>` (below). A parent message is never an answer to these questions. - In the chat of a Pi `/flow` the engineer's answer «Одобрить» to the question after the PR equals typing `/ship flow/<name>` and is the explicit request for the release by the procedure the project's `release` in `flow.json` names (none From f52aef45d8aa269c26cad527eff36d12a660d2a3 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 09:05:01 +0100 Subject: [PATCH 47/48] Name the Pi /epic command in the integration and testing docs --- docs/TESTING.md | 2 +- docs/agents/agent-integration.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 2358f38..2ffd694 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -119,7 +119,7 @@ This is a placement guide, not a manual per-file catalog or eight separate runne | Scoped warmup | Golden Markdown files in test/fixtures/warmup/ for global/project/task on a temp DB/home; scope priority; errors; 64 KiB entry clipping, invitation lines, the reports marker; character budget (multi-byte, overflow, range) | CLI text output, MYPI_SCOPE priority and error exit; MCP text | | Session registry | Fast tests on a temp DB, temp XDG_RUNTIME_DIR and a fake /proc: owner-only files, pid+start liveness, displacement, unregister/forget, tree, nine key-resolution scenarios, permission mode | One CLI boundary test with hook stdin and the real /proc; manual stand checklist with Claude Code and Pi versions | | Claude Code adapter | Shape of .mcp.json and .claude/settings.json; allow list equals the MCP tool list | Manual stand checklist with the Claude Code version (approval, warmup on start/clear/resume/compact, no prompts) | -| Pi extension | Fast test of .pi/extensions/mypi.ts against a stub ExtensionAPI: MCP registration with the session id, re-registration on a new session, warmup section on every turn, failure section; the /open, /inbox and /flow commands (/flow sends /skill:flow); the mypi_ask tool: recommended and other option, own word, Escape in the select and in the input, no UI | Manual stand checklist with a real Pi; its version is recorded in the PR; /flow to an agreed plan on a fixture (MP-117) | +| Pi extension | Fast test of .pi/extensions/mypi.ts against a stub ExtensionAPI: MCP registration with the session id, re-registration on a new session, warmup section on every turn, failure section; the /open, /inbox, /flow and /epic commands (/flow sends /skill:flow, /epic sends /skill:epic); the mypi_ask tool: recommended and other option, own word, Escape in the select and in the input, no UI | Manual stand checklist with a real Pi; its version is recorded in the PR; /flow to an agreed plan on a fixture (MP-117); /epic to the subtasks and the hand-over to /flow on a fixture (MP-119) | | Capture/note/journal | Unicode/multiline, exact source, no-overwrite, read-only API behavior | Real context commit and preservation of unrelated dirty diff | | DB authority/constraints | Real engine, transactions and resulting state, not mocked INSERT | Reopen/restart and competing writers; in-memory is not durability proof | | Retry/mixed-operation recovery | Bounded failure scenarios at durable protocol boundaries | Abrupt termination/restart when exceptions/finally behave differently | diff --git a/docs/agents/agent-integration.md b/docs/agents/agent-integration.md index 02b8914..1b57fb5 100644 --- a/docs/agents/agent-integration.md +++ b/docs/agents/agent-integration.md @@ -22,8 +22,9 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou and suggests keys while its arguments are typed (the session's project and its open tasks in a project or task chat, the organization's projects and their open tasks in an organization chat, all projects and open tasks in the main chat; loaded through the CLI and reused for 15 s; none for flags or after - --message), the /inbox command, which runs the inbox skill as /skill:inbox, and the - /flow command, which runs the Pi-only flow skill as /skill:flow. It registers the tool + --message), the /inbox command, which runs the inbox skill as /skill:inbox, the + /flow command, which runs the Pi-only flow skill as /skill:flow, and the /epic + command, which runs the Pi-only epic skill as /skill:epic. It registers the tool `mypi_ask` (direct, run sequentially): `question` and 2–4 `options`, the recommended one first; it shows a select with the first option marked «(рекомендую)» and «Своё слово» last, which opens a text input; it returns `{"answer","custom"}`, @@ -102,7 +103,7 @@ Read when: starting, configuring or diagnosing Claude Code or Pi in this checkou - Personal Claude settings belong in the ignored `.claude/settings.local.json` or in user settings. There is no `CLAUDE.md`; Claude Code loads [AGENTS.md](../../AGENTS.md) instead. - Shared skills live in `integrations/skills/`; each gets a relative symlink - `.claude/skills/<name>`, except the Pi-only skills (`flow`), which Claude Code + `.claude/skills/<name>`, except the Pi-only skills (`flow`, `epic`), which Claude Code replaces with its own pipeline skill of the same name. The development pipeline's skills and agents in `.claude/` are real directories and files, used only with Claude Code. From 3b0c31f13bab06a0ac94045a7ab9a78270df6f09 Mon Sep 17 00:00:00 2001 From: Pavel Donin <doninpr@proton.me> Date: Tue, 10 Nov 2026 09:05:18 +0100 Subject: [PATCH 48/48] Add the Pi /epic to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2005fa2..b9b4026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ from existing transcripts. MEMORY.md is no longer read; move facts into INDEX.md - Pi pipeline roles and templates (MP-116, epic MP-68): `integrations/pi-flow/` holds the roles `scout`, `planner`, `executor`, `reviewer`, `tester`, `investigator` and `challenger`, each with its answer schema in `outputSchema`, and the workflow templates `standard.js` (implement, review, fix through `resume`, at most two review rounds), `parallel.js` and `investigate.js` (read-only fan-out). The tracked `.pi/settings.json` makes pi-subagents scan `integrations/pi-flow` and `home/pi-flow` (own roles and templates) and start children without ambient extensions; in Pi sessions in the engine root `scout` and `reviewer` shadow the pi-subagents builtins of those names. `docs/agents/agent-integration.md` describes the role and template formats, own roles in `home/pi-flow/` and the personal model tiers. The roles are run by hand with `subagent` until the Pi `/flow` arrives; `.claude/agents` is unchanged. - Pi `/flow`, part 1 (MP-117, epic MP-68): the Pi-only skill `integrations/skills/flow/SKILL.md`, run by the extension command `/flow`, takes the tasks of one project from the typed command to an agreed plan — session scope, subtasks, order, flow branch and score, parallel reconnaissance by `scout` through `investigate.js`, the plan by `planner` with its «Execution» section, publication on derive and the question «Согласовать / Правки»; `/flow <name>` resumes from the flow score after a restart. Every question goes through the new extension tool `mypi_ask` (2–4 options, the recommended one first, «Своё слово»; Escape cancels: the run stops, records no answer to that question and only commits the answers already written). Execution after the agreed plan is part 2 (MP-118); Claude's `/flow` is unchanged. - Pi `/flow`, part 2 (MP-118, epic MP-68): after «Согласовать» each key runs by its plan's «Execution» — the base check, the task worktree, the workflow template through `subagent` in the background while the next plan is agreed, `do-report.json` and `review-report.json` (rounds with their range, findings with their resolution, checked by one line), fixes by resuming the executor, the project's checks by the lead (three attempts), the `--no-ff` merge into the flow branch and the self-test by `tester` by the project's stand document `<project home>/stand.md`; then the checks of the assembled branch, the epic acceptance, the PR and CI, the stand handed to the engineer and the question «Одобрить / Замечания / Стоп». «Замечания» sends the keys they touch back to the plan rework and executes them again; «Одобрить» merges the PR by the `/ship` procedure and runs the project's `release` procedure (none — merge only); the statuses end at `merged`. The reviewer role gains `outsidePlan` (`standard.js` and `parallel.js` stop before a fix and the lead asks once per key), each review round carries its `range`, `tester` can leave a stand up for the engineer (`handover`), and plans gain a release line in «Execution». Claude's `/flow` and `/ship` are unchanged. +- Pi `/epic` (MP-119, epic MP-68): the Pi-only skill `integrations/skills/epic/SKILL.md`, run by the extension command `/epic`, leads an epic through steps 1–5 of «Epics» — the research by the `executor` role in research mode (only `research.md` in the epic's folder), one `reviewer` round on the document, version 1 on derive, the independent check by `challenger`, one `mypi_ask` per finding with its row in the decision table, version 2 by resuming the same executor (a fresh one when it is not retained), the «Подзадачи» question and the subtasks created with `--parent`; the answer «Завести и запустить /flow» hands the run to the Pi `/flow` skill in the same chat. Claude's `/epic` is unchanged. ### Changed