diff --git a/.pi/extensions/mypi-lib/ask.ts b/.pi/extensions/mypi-lib/ask.ts new file mode 100644 index 0000000..88b4e05 --- /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; record no answer to it.', + 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..65d3f61 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,17 @@ 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 in Pi from the plan to the merge and the release: /flow […], or /flow to resume', + handler: async args => { + pi.sendUserMessage(('/skill:flow ' + args.trim()).trim(), { expandPromptTemplates: true }); + }, + }); + pi.registerCommand('epic', { + description: 'Lead an epic in Pi from research to its subtasks: /epic ', + handler: async args => { + pi.sendUserMessage(('/skill:epic ' + args.trim()).trim(), { expandPromptTemplates: true }); + }, + }); + registerAsk(pi); } 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/CHANGELOG.md b/CHANGELOG.md index 4ee9bba..b9b4026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ 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. +- 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 ` 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 `/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 @@ -33,6 +37,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 diff --git a/README.md b/README.md index dd4fe0f..5e8f929 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 ` | @@ -466,8 +467,10 @@ 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 │ ├── extensions/mypi.ts # MCP registration and warmup per session │ └── skills -> ../integrations/skills ├── .claude/ # tracked Claude Code configuration diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md index e1a8a5f..5cf559d 100644 --- a/docs/PIPELINE.md +++ b/docs/PIPELINE.md @@ -4,7 +4,10 @@ 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»), 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. @@ -68,8 +71,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` @@ -101,7 +108,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/` also `mp-task project P` and reading the flow score. The texts (``, the typed +`/ship flow/` also `mp-task project P` and reading the flow score. The same holds for the Pi `/flow ` typed without keys, which resumes the +keys of that score. The texts (``, the typed arguments and `` substituted): - `/board` with another project's code: `/board is not allowed in session scope ; this chat shows the board of

only. Use /board here or /open global for another project.` @@ -129,7 +137,8 @@ tree of any depth. `mp-task subtasks ` 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; @@ -137,7 +146,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 planning "в потоке : включена инженером"` and goes on as `planning`; - a typed key in `review` with subtasks and no `pr.md` of this flow is the epic: @@ -158,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 ` (`.claude/skills/epic/SKILL.md`) leads steps 1–5 in the epic's chat: +`/epic ` (`.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 ` 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/` merges. The step comes from the epic's status +`/epic` or typed; `/ship flow/` 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 `; `N` — @@ -179,7 +190,7 @@ The first row from the top whose sign holds is the step: | `ROWS` < `N` | 3 | `/epic `: a question on each remaining finding | | `ROWS` = `N`, `V` = 1 | 4 | `/epic `: version 2 | | `V` ≥ 2, `mp-task subtasks ` empty | 5 | `/epic `: the «Подзадачи» question | -| `mp-task subtasks ` not empty | 6–8 | `/flow `, then `/ship flow/` | +| `mp-task subtasks ` not empty | 6–8 | `/flow `, then `/ship flow/` (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 @@ -213,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 @@ -248,7 +260,82 @@ 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 +answer schema is its `outputSchema`, beside the workflow templates `standard.js` +(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). `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 +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`. + +## Pi + +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 ` | +| the skill `.claude/skills/epic` | the extension command `/epic`, which sends `/skill:epic ` | +| `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 --base flow/` 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 `/stand.md` | +| the manual check, then a typed `/ship flow/` | 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 ` ` | +| 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 ` 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 согласован` 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: the cancelled answer is not +recorded, and only `mp-home-commit` commits the answers already written. +After «Согласовать» the key executes by its plan's «Execution»: the base check, +the task worktree from `mp-worktree --base flow/`, 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. + +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 @@ -260,7 +347,8 @@ final message is exactly one JSON object per its definition. Under `/epic` the e - `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. @@ -307,7 +395,8 @@ One writer per fact; nothing is written twice. writes one `note` in the project scope (`note` kind `journal`, scope `{"type":"project","key":""}`) 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»). @@ -320,11 +409,21 @@ One writer per fact; nothing is written twice. - An approved plan is the engineer typing `/do ` 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 ` authorizes the research, its publication on derive and, after the engineer's answer to the «Подзадачи» question, `mypi task create --parent ` for the agreed subtasks. The answer «Завести и запустить /flow» in the epic's chat equals typing `/flow `; `/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/` (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/` 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 @@ -332,13 +431,21 @@ 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 «Правки»; +- 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; @@ -402,7 +509,7 @@ else `/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 @@ -411,6 +518,8 @@ else `/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 diff --git a/docs/PLAN-FORMAT.md b/docs/PLAN-FORMAT.md index b280dc8..4352afd 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,16 @@ 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 document (`/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 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 +50,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 diff --git a/docs/TESTING.md b/docs/TESTING.md index 86a2cc7..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 | 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, /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 | @@ -129,6 +129,8 @@ 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 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 | | 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/agent-integration.md b/docs/agents/agent-integration.md index 1fa3629..1b57fb5 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 @@ -22,7 +22,15 @@ 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, 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"}`, + `{"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 ` with a 5-second limit and ignores @@ -34,6 +42,36 @@ 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; 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: … })`, + not with bare `node --check`, which rejects the top-level `await` and `return`. ## Claude Code @@ -65,7 +103,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/`. The development pipeline's skills and agents in + `.claude/skills/`, 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. ## Personal customization @@ -74,3 +113,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. 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. 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/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/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/parallel.js b/integrations/pi-flow/parallel.js new file mode 100644 index 0000000..3096612 --- /dev/null +++ b/integrations/pi-flow/parallel.js @@ -0,0 +1,62 @@ +/* + * 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. + * 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: [{ round, range, status, findings, outsidePlan }] }] in the order of args.parts. + */ +const reviewerRole = args.reviewer || "reviewer"; +function rounds(part, context, state, round) { + return runs + .run(`${part.key}-review-${round}`, { + agent: reviewerRole, + 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, 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}`, { + 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); + }); + }); +} +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)); 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..4283e49 --- /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}},"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 `..`. 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. 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. +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`. `outsidePlan` — the changed files no step of the plan names, an empty list when there are none. diff --git a/integrations/pi-flow/scout.md b/integrations/pi-flow/scout.md new file mode 100644 index 0000000..76d7c53 --- /dev/null +++ b/integrations/pi-flow/scout.md @@ -0,0 +1,26 @@ +--- +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`. + +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. + +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/standard.js b/integrations/pi-flow/standard.js new file mode 100644 index 0000000..2ff513d --- /dev/null +++ b/integrations/pi-flow/standard.js @@ -0,0 +1,47 @@ +/* + * 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 + * 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, range, status, findings, outsidePlan }] }. + */ +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, 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, + 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/integrations/pi-flow/tester.md b/integrations/pi-flow/tester.md new file mode 100644 index 0000000..37b68f9 --- /dev/null +++ b/integrations/pi-flow/tester.md @@ -0,0 +1,21 @@ +--- +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"}},"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 document `/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 /bin/env.sh && herdr session attach `) 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 — 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; `handover` — `{ attach, teardown }` or null; `openQuestions`. 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 ".' +--- + +# /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: "", 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 `` 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 `: `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 ` and stop. This step runs before «Input»: before +`$BIN/mp-task show`, before any derive call or `subagent`. + +## Input + +`$BIN/mp-task show `: + +- `new` or `planning` → «State»; +- `review` and `$BIN/mp-task subtasks ` is not empty → print + `: подзадачи заведены (<ключи>); следующий шаг, если поток ещё не запущен, — /flow ; в Pi /ship — ответ «Одобрить» потока` + (`` is the output of `$BIN/mp-task slug `) and stop; +- any other status (`review` without subtasks included) → name the status and stop. + +`DIR=$($BIN/mp-task dir )`. + +## 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 ` 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 ""` («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 ` gives `project`, `checkoutPath` and + `home`; when `checkoutPath` is `null`, say the project has no registered clone + and stop. `$BIN/mp-task flow ` gives `integration`; a non-zero exit → print + its error and stop. `git -C fetch -q origin`; then, only when + `git -C branch --show-current` prints `` and + `git -C status --porcelain` prints nothing, + `git -C merge -q --ff-only origin/`; otherwise read + `origin/` and say so. `SHA` is + `git -C rev-parse origin/`. +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 `), the project, the clone path and `SHA` + (read-only; `git -C show :` 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=" — + исследование: " `; take `short_id`, `url` and the + version number from the answer. + 3. `tools.mcp__derive__organize({ short_ids: [], set: ["", + "", "research"] })` (`` 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 planning "исследование v1 опубликовано: "`; + `$BIN/mp-home-commit ": 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 ": 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` — + +``` +Находка (). +Что сейчас: +Предложение: +Что выигрываем / что теряем: +Обоснование: +Принять — ; Отклонить — . +``` + +with the fields verbatim; `options` — `Принять` and `Отклонить`, the lead's +recommendation first. Right after the answer append to the end of the table the row +`| | <находка> | | <принято|отклонено|комментарий: «<дословно>»> | |` +(`<находка>` is `proposal` as one short line without `|`; `Принять` → `принято`, +`Отклонить` → `отклонено`, an own word → `комментарий: «»`) and +run `$BIN/mp-home-commit ": finding "`; 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, »; 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: независимая проверка — находок, решения инженера в разделе «Решения по независимой проверке»"`; +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 ": research v2"`. + +## Step 5 — subtasks + +Row 5 (`V` ≥ 2, no subtasks). `` is the output of `$BIN/mp-task slug `. + +1. One `mypi_ask`: `question` — + + ``` + Подзадачи : + . — <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/integrations/skills/flow/SKILL.md b/integrations/skills/flow/SKILL.md new file mode 100644 index 0000000..89f5c68 --- /dev/null +++ b/integrations/skills/flow/SKILL.md @@ -0,0 +1,609 @@ +--- +name: flow +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 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 +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 «Согласовать», 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). + +«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. 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»). +- 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 + +`$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 <name>` without keys resumes: only when the scope's `project` is not `null` +(a `P` or `P-N` chat), `$BIN/mp-task project <project>` gives `home`, and the flow +score `<home>/flows/<name>.md` exists; its `- Command:` line gives the keys, and the +rule above applies to them. Otherwise print `usage: /flow <name> <KEY> [<KEY>…]; +/flow <name> 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 `<name>`, not a task key; at least one key + follows, or the score gives them (Step 0). Otherwise print + `usage: /flow <name> <KEY> [<KEY>…]` 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 <KEY>` is not empty and that has no + `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`; + a non-zero exit → print its error and stop before any write. + +## Subtasks + +Before the flow score is written, run `$BIN/mp-task subtasks <KEY>` 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 +`Подзадача <KEY> «<title>» в статусе <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 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»; + - `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 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 + 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 + +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 — 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. + +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 + 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 + «Согласовать» 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`, 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 + 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**, 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, + 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 — `- Приёмка эпика:`, `- 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; `- Приёмка эпика:` → 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. + +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>"` — 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 + 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` — 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 + <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 «Одобрить»: + +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 + `--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; 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 + 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"`, 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 + 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 + +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. + +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. + It is committed with the next `mp-home-commit` of the run. +- `{ 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. + +`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. 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. 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"}`. diff --git a/test/fast/pi-extension.test.ts b/test/fast/pi-extension.test.ts index 3addab8..95a121c 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<string, { description: string; handler: (args: string, ctx: any) => Promise<void>; getArgumentCompletions?: (prefix: string) => Promise<unknown> }>(); + const tools = new Map<string, any>(); 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<void>; getArgumentCompletions?: (prefix: string) => Promise<unknown> }) { 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<string, string>; toolGuidelines?: Record<string, string[]> }; }; - 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 }; } @@ -65,7 +67,8 @@ 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', '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: '' }; await pi.start('startup'); @@ -383,6 +386,69 @@ 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 /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); + 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; @@ -411,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']); + assert.deepEqual([...pi.commands.keys()], ['open', 'inbox', 'flow', 'epic']); assert.equal(pi.commands.get('inbox')!.getArgumentCompletions, undefined); }); diff --git a/test/fast/pi-flow.test.ts b/test/fast/pi-flow.test.ts new file mode 100644 index 0000000..2839832 --- /dev/null +++ b/test/fast/pi-flow.test.ts @@ -0,0 +1,147 @@ +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'); + +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'] }, + }); +}); + +const ROLES: Record<string, { tools: string; acceptanceRole: string }> = { + 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<string, string> { + const match = /^---\n([\s\S]*?)\n---\n/.exec(text); + assert(match !== null, 'frontmatter'); + const fields = new Map<string, string>(); + 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<string, unknown>[] { + if (Array.isArray(schema)) return schema.flatMap(objects); + if (schema === null || typeof schema !== 'object') return []; + const node = schema as Record<string, unknown>; + 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']); + 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', () => { + 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); + } +}); + +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 = { + 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'); + + 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']); + 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'); +}); diff --git a/test/fast/skills.test.ts b/test/fast/skills.test.ts new file mode 100644 index 0000000..71e2568 --- /dev/null +++ b/test/fast/skills.test.ts @@ -0,0 +1,26 @@ +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')); + 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); + 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); + } +});