From 3f7d9a6d5cec67f72ac1ed04aabfacdc18884ec1 Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Sun, 4 Oct 2026 20:35:59 -0400 Subject: [PATCH 1/8] Integrate Matt Pocock engineering skills --- .../skills/matt-pocock-integration/SKILL.md | 66 +++++++++++++++++++ AGENTS.md | 2 + README.md | 2 + bin/fm-brief.sh | 41 ++++++++++-- bin/fm-dod-lib.sh | 51 ++++++++++++-- bin/fm-promote.sh | 24 ++++++- bin/fm-spawn.sh | 16 ++++- docs/architecture.md | 10 +++ docs/documentation-audiences.json | 13 +++- docs/matt-pocock-skills.md | 50 ++++++++++++++ docs/scripts.md | 4 +- tests/fixtures.sh | 7 ++ tests/fm-brief.test.sh | 14 ++++ .../fm-cmux-claude-composer-live-e2e.test.sh | 10 ++- tests/fm-control-relaunch.test.sh | 2 +- tests/fm-omp-harness.test.sh | 4 +- tests/fm-spawn-dispatch-profile.test.sh | 2 + tests/fm-task-delivery.test.sh | 52 +++++++++++++++ 18 files changed, 349 insertions(+), 21 deletions(-) create mode 100644 .agents/skills/matt-pocock-integration/SKILL.md create mode 100644 docs/matt-pocock-skills.md diff --git a/.agents/skills/matt-pocock-integration/SKILL.md b/.agents/skills/matt-pocock-integration/SKILL.md new file mode 100644 index 00000000000..2c3b7b089a2 --- /dev/null +++ b/.agents/skills/matt-pocock-integration/SKILL.md @@ -0,0 +1,66 @@ +--- +name: matt-pocock-integration +description: >- + Agent-only routing for combining FirstMate with Matt Pocock's engineering skills. + Use when the captain invokes one of those workflows, before translating it into FirstMate work, and before choosing a worker's required skills or agreed testing seams. +user-invocable: false +metadata: + internal: true +--- + +# Matt Pocock skills integration + +FirstMate owns the operating system of the work: captain interaction, authority, task classification, worker allocation, isolation, tracker mutation, supervision, delivery, and merge decisions. +Matt Pocock skills supply engineering methods inside that system. +When the two prescribe the same concern, keep FirstMate's lifecycle and use the upstream skill only for method. +For a captain-facing upstream workflow, load its installed skill and apply its interviewing, modeling, or writing method subject to the translations below. + +## Translate workflows + +| Upstream workflow | FirstMate route | +|---|---| +| `grill-me`, `grill-with-docs` | Keep the interview and decisions in the firstmate chat while delegating project investigation to scouts and approved repository changes to ships. | +| `wayfinder` | Keep the captain-facing decision map with firstmate while using scouts for research or prototypes, publishing planning artifacts through the project's approved path, and requiring separate implementation authorization. | +| `to-spec`, `to-tickets` | Preserve their planning output and dependency semantics because publishing a spec or tickets does not authorize implementation, while authorized tickets become ordinary FirstMate tasks. | +| `research`, `prototype`, `diagnosing-bugs` | Dispatch a scout when a separate evidence artifact is needed, retain the scout's report as durable evidence, and promote it only after implementation is authorized. | +| `implement` | Convert the accepted spec or tickets into ship tasks instead of invoking the upstream workflow inside a worker, where its delegation and final review steps would create a second lifecycle. | +| `code-review` | Use only when the captain explicitly requests a standalone review deliverable, dispatching it as dedicated review work rather than stacking it on a no-mistakes delivery. | +| `setup-matt-pocock-skills` | Treat setup as an explicit per-project change that inspects first, obtains the required project decisions, and ships the approved configuration through that project's delivery path. | + +If another upstream skill creates subagents, mutates the issue tracker, opens a PR, or owns final review, route those actions through the corresponding FirstMate scout, ship, tracker, or delivery contract instead of running a nested workflow. + +## Choose worker methods + +Name upstream skills by exact ID in the worker's `## Required skills` subsection. +Reference the installed skill rather than copying its procedure into the instructions. + +Use this starting map, then narrow it to what the task actually needs: + +| Work shape | Method skills | +|---|---| +| Bug or regression | `diagnosing-bugs`, then `tdd` when an executable regression seam exists | +| Feature behavior | `tdd` | +| New or changed interface | `codebase-design`, plus `tdd` when executable behavior changes | +| Domain terminology or decisions | `domain-modeling` | +| Agent-consumed instructions | `writing-for-agents` | + +Orchestration skills such as `wayfinder`, `research`, and `implement`, and final-review skills such as `code-review`, do not belong in a worker's required-method list. +The selected delivery contract remains the only final review owner, including no-mistakes when that mode is selected. + +## Record the engineering contract + +Before dispatch, fill every current ship or scout instruction scaffold as follows: + +1. `## Required skills` lists each exact method skill ID as a bullet, or contains only `None - .` when no upstream method applies. +2. `## Agreed testing seams` lists the public interfaces already made explicit or agreed with the captain, or `None - .` when no executable test is appropriate. +3. The Firstmate specification names relevant `CONTEXT.md` files and ADRs when they exist, without copying their contents. + +Do not silently replace a missing required skill with generic advice. +If no eligible worker can load it, report that blocker and ask whether to install the skill pack or revise the method. +Load `harness-adapters` before sending any harness-specific skill invocation to a live worker. + +## Completion + +The worker uses required skills during investigation and implementation, then follows only the existing Definition of done. +Planning and investigation findings are evidence, not implementation authority. +No upstream skill changes FirstMate's communication, approval, merge, or cleanup rules. diff --git a/AGENTS.md b/AGENTS.md index 65a3197944d..3b3b5e16a4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -543,6 +543,7 @@ Preserve durable structured identifiers, dependencies, and completion artifact l Use its scaffold as the contract, then fill `## Captain's intent` (`{TASK}`) with the captain's own ask and any boundary the captain stated, plus the context needed to read it, including the substance of any report, decision, or PR the ask refers to; never widen the ask there into a general goal or an enumerated coverage list, because the reviewer treats that subsection as acceptance criteria. Fill `## Firstmate spec` (`{FIRSTMATE_SPEC}`) with only the build instructions that ask requires, naming what stays out of scope when the ask is narrow; a generalization, consistency sweep, or extra hardening the captain did not ask for is follow-up work to note, not scope to add. `bin/fm-dod-lib.sh` owns intent authoring without added speaker labels or direct address, its provenance markers, what a no-mistakes worker may pass as `--intent`, and the string's self-sufficiency rule. +`matt-pocock-integration` owns the task-specific required-skill and agreed-testing-seam fields plus the precedence between external engineering methods and FirstMate's lifecycle. Keep additions task-specific rather than repeating lifecycle instructions, and alter generated sections only when the task genuinely differs from the standard shape. Every ship brief must retain the worktree-isolation assertion and stop if launched in the primary checkout. @@ -581,6 +582,7 @@ These skills are not captain-invocable; load them only at their precise triggers - `fmx-respond` - load on an `x-mention ` `check:` wake to handle the mention, on an `x-mode-error ...` `check:` wake to report the Relay configuration blocker, on a `public-followup ...` `check:` wake or a startup-surfaced public commitment, and on any milestone or terminal wake for a Relay-linked task before posting its completion follow-up; relevant only when Relay is on. - `firstmate-codexapp` - load before coordinating a visible Codex Desktop thread, evaluating a Codex App backend request, or reconciling Codex Desktop host-tool smoke evidence for Firstmate work. - `firstmate-coding-guidelines` - load before changing firstmate's shared, tracked material, as defined by section 1's list, whether editing directly or briefing a crewmate for a firstmate-repo task. +- `matt-pocock-integration` - load when the captain invokes a Matt Pocock workflow, before translating one into FirstMate work, and before selecting a ship or scout's required skills or agreed testing seams. ## 14. Relay diff --git a/README.md b/README.md index 89dc9a4fbaf..3d112826282 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Launching a supported harness inside it for your primary session instantiates yo - **A visible crew** - every crewmate works in its own tmux window, Herdr tab, or experimental zellij tab, cmux workspace, or Orca terminal you can watch or type into; the first mate reconciles. - **Disposable worktrees** - each task runs in a clean [treehouse](https://github.com/kunchenguid/treehouse) git worktree, or an Orca-managed worktree when `backend=orca`, so parallel work on one repo never collides. - **Two task shapes** - ship tasks deliver authorized changes; scout tasks leave standalone investigation reports when the intake contract warrants separate research. +- **Composable engineering methods** - optional Matt Pocock skills can supply diagnosis, TDD, module-design, and domain-modeling methods while FirstMate keeps orchestration and delivery ownership. - **Explicit project modes** - each project ships via `no-mistakes`, `direct-PR`, or `local-only`, with an optional `+yolo` merge-autonomy flag. - **Optional secondmates** - opt in to persistent second mates that run from isolated firstmate homes with their own `FM_HOME`, state, projects, and session lock, either locally or as a whole home on an SSH-reachable host, with guarded updates and recovery that never turns an unavailable remote route into a local replacement. - **Event-driven, zero-token supervision** - a bash watcher sleeps on the fleet and wakes the first mate only when something needs you; verified primary harnesses also get a turn-end backstop that blocks or follows up on a blind stop when work is under way and supervision is not live. @@ -214,6 +215,7 @@ Firstmate's skills live in two separate places with different audiences: - [docs/architecture.md](docs/architecture.md) - maintainer architecture for the crew, supervision, worktrees, secondmates, and project modes. - [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend selection, optional Relay and its X and Discord setup steps, trusted external process-event adapter setup, the files you set, and harness support. +- [docs/matt-pocock-skills.md](docs/matt-pocock-skills.md) - install and use Matt Pocock's engineering skills without creating a second orchestration or review lifecycle. - [docs/extension-bindings.md](docs/extension-bindings.md) - maintainer architecture for the narrow trusted external `process-event-adapter/1` package, binding, handshake, and evidence boundary. - [docs/remote-secondmates.md](docs/remote-secondmates.md) - current setup, routing, transfer, recovery, and safety behavior for whole-home remote second mates. - [docs/calm.md](docs/calm.md) - current `/calm` behavior on Pi and Claude Code and its supported presentation limits. diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 7536546264e..f78e1b07612 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -8,8 +8,11 @@ # report, decision, or PR the ask refers to, without added speaker labels or # direct address) and `{FIRSTMATE_SPEC}` # under `## Firstmate spec` (build instructions, which are never the captain's -# intent). bin/fm-dod-lib.sh owns the no-mistakes `--intent` contract those -# subsections feed; bin/fm-spawn.sh refuses leftover placeholders and a +# intent). Their `# Engineering method` section has two more task-specific fill +# sites: `{REQUIRED_SKILLS}` names the method skills the worker must load, and +# `{TESTING_SEAMS}` names the agreed public test seams or gives the reason none +# applies. bin/fm-dod-lib.sh owns the no-mistakes `--intent` contract those +# Task subsections feed; bin/fm-spawn.sh refuses leftover placeholders and a # `## Captain's intent` line opening with a Captain label or address. Secondmate # charters still use a single `{TASK}` charter fill. Firstmate may adjust other # sections when the task genuinely deviates (e.g. working an existing external @@ -358,6 +361,32 @@ IFS= read -r -d '' TASK_SECTION <<'EOF' || true EOF TASK_SECTION=${TASK_SECTION%$'\n'} +IFS= read -r -d '' ENGINEERING_SECTION <<'EOF' || true +# Engineering method +Fill each task-specific subsection with one bullet per item, or write `None - .` + +## Required skills +{REQUIRED_SKILLS} + +## Agreed testing seams +{TESTING_SEAMS} + +## Execution +Each required-skill bullet is an exact skill ID unless the subsection contains only `None - .` +Load every named skill before using its method. +If a named skill is unavailable, report the missing skill as a blocker and stop. +Testing seams are the agreed public interfaces where behavior will be tested. +Write tests only at these seams. +For a bug, reproduce the end-user path first and observe the regression test fail before the fix. +When tests apply, work in vertical red-green slices rather than writing every test or every implementation change in one horizontal batch. + +## Ownership +FirstMate's selected delivery path is the only final review owner. +Use the required skills for engineering method while keeping orchestration, worker allocation, tracker mutation, review, PR, CI, and merge authority with FirstMate and this brief's Definition of done. +Do not invoke `code-review`, `implement`, `research`, `wayfinder`, or another workflow that delegates work or starts a second delivery path. +EOF +ENGINEERING_SECTION=${ENGINEERING_SECTION%$'\n'} + if [ "$KIND" = scout ]; then if "$SCRIPT_DIR/fm-bootstrap.sh" lavish-compatible >/dev/null 2>&1; then LAVISH_LINE='If your deliverable is a visual artifact the captain will review and iterate on, you may host the Lavish review loop yourself (poll, revise, re-serve, staying alive) instead of handing it back to firstmate.' @@ -369,6 +398,8 @@ You are a crewmate: an autonomous worker agent managed by firstmate. Work on you $TASK_SECTION +$ENGINEERING_SECTION + $HERDR_SECTION # Setup @@ -424,7 +455,7 @@ Before reporting done, read and follow \`$FM_ROOT/.agents/skills/captain-hold-li When the report is complete, append \`done: {one-line conclusion}\` to the status file and stop. If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report; firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up message. EOF -echo "scaffolded: $BRIEF (scout; replace {TASK} and {FIRSTMATE_SPEC})" +echo "scaffolded: $BRIEF (scout; replace {TASK}, {FIRSTMATE_SPEC}, {REQUIRED_SKILLS}, and {TESTING_SEAMS})" exit 0 fi @@ -453,6 +484,8 @@ You are a crewmate: an autonomous worker agent managed by firstmate. Work on you $TASK_SECTION +$ENGINEERING_SECTION + $HERDR_SECTION # Setup @@ -514,4 +547,4 @@ Keep it proportionate: skip \`AGENTS.md\` edits for trivial tasks that produced $DOD EOF -echo "scaffolded: $BRIEF (ship, mode=$MODE; replace {TASK} and {FIRSTMATE_SPEC})" +echo "scaffolded: $BRIEF (ship, mode=$MODE; replace {TASK}, {FIRSTMATE_SPEC}, {REQUIRED_SKILLS}, and {TESTING_SEAMS})" diff --git a/bin/fm-dod-lib.sh b/bin/fm-dod-lib.sh index 5dba2534734..4503927d491 100755 --- a/bin/fm-dod-lib.sh +++ b/bin/fm-dod-lib.sh @@ -23,10 +23,10 @@ # roughly the same specification - so a report, decision, or PR the intent # refers to is written into it as substance, never left as a pointer. # bin/fm-brief.sh scaffolds those two `# Task` subsections; bin/fm-spawn.sh and -# bin/fm-promote.sh refuse leftover `{TASK}` / `{FIRSTMATE_SPEC}` placeholders -# and a `## Captain's intent` line opening with a Captain label or address -# through the helpers below. Other mentions of `--intent` point here rather than -# restating the rule. +# bin/fm-promote.sh refuse leftover `{TASK}` / `{FIRSTMATE_SPEC}` placeholders, +# incomplete current Engineering method sections, and a `## Captain's intent` +# line opening with a Captain label or address through the helpers below. +# Other mentions of `--intent` point here rather than restating the rule. # Every heredoc here stays outside a command substitution: `VAR=$(cat < printf '%s\n' "$task" | fm_brief_heading_parse - "$2" present >/dev/null } +fm_brief_engineering_contract_present() { # + fm_brief_heading_present "$1" "# Engineering method" +} + +fm_brief_engineering_heading_body() { # + local method + method=$(fm_brief_heading_body "$1" "# Engineering method") + printf '%s\n' "$method" | fm_brief_heading_parse - "$2" body +} + +fm_brief_engineering_heading_present() { # + local method + method=$(fm_brief_heading_body "$1" "# Engineering method") + printf '%s\n' "$method" | fm_brief_heading_parse - "$2" present >/dev/null +} + +# Return 0 only when a current Engineering method subsection still consists of +# its scaffold placeholder. Literal examples elsewhere remain ordinary content. +fm_brief_engineering_placeholders_present() { # + local file=$1 skills seams + [ -f "$file" ] || return 1 + skills=$(fm_brief_engineering_heading_body "$file" "## Required skills") + seams=$(fm_brief_engineering_heading_body "$file" "## Agreed testing seams") + [ "$(printf '%s' "$skills" | tr -d '[:space:]')" = '{REQUIRED_SKILLS}' ] && return 0 + [ "$(printf '%s' "$seams" | tr -d '[:space:]')" = '{TESTING_SEAMS}' ] && return 0 + return 1 +} + +# A current Engineering method contract is complete only when both task-specific +# bodies have content. A brief without the top-level section predates the +# contract and remains launchable for recovery compatibility. +fm_brief_engineering_content_valid() { # + local file=$1 skills seams + [ -f "$file" ] && [ -r "$file" ] || return 1 + fm_brief_engineering_contract_present "$file" || return 1 + fm_brief_engineering_heading_present "$file" "## Required skills" || return 1 + fm_brief_engineering_heading_present "$file" "## Agreed testing seams" || return 1 + skills=$(fm_brief_engineering_heading_body "$file" "## Required skills") + seams=$(fm_brief_engineering_heading_body "$file" "## Agreed testing seams") + [ -n "$(printf '%s' "$skills" | tr -d '[:space:]')" ] || return 1 + [ -n "$(printf '%s' "$seams" | tr -d '[:space:]')" ] || return 1 +} + fm_brief_marked_captain_words() { # printf '%s\n' "$1" | awk ' match($0, /^[[:space:]]*(\[captain\]|Captain('\''s (words|ask|intent))?:)[[:space:]]*/) { diff --git a/bin/fm-promote.sh b/bin/fm-promote.sh index df6d6e4a3a3..709f59c5563 100755 --- a/bin/fm-promote.sh +++ b/bin/fm-promote.sh @@ -11,7 +11,9 @@ # bin/fm-dod-lib.sh, the single owner an ordinary ship brief also uses - the # mode-specific Definition of done, so a promoted worker receives exactly the same # delivery contract as a briefed one, including the no-mistakes mode's ask-user -# escalation rule and --yes ban. The instructions also carry `# Task` with +# escalation rule and --yes ban. Current briefs also contribute their validated +# Engineering method section, so an updated promotion method reaches the live +# worker as well as any later replacement. The instructions also carry `# Task` with # `## Captain's intent` preserved from the scout brief and promotion's ship-time # instructions under `## Firstmate spec`; the scout-time spec remains context but # is not relabeled as the ship spec. Promotion refuses leftover `{TASK}` / @@ -153,6 +155,16 @@ if ! fm_brief_task_content_valid "$SCOUT_BRIEF"; then echo "error: $SCOUT_BRIEF must contain nonempty ## Captain's intent and ## Firstmate spec subsections (or a nonempty legacy # Task body) before promotion" >&2 exit 1 fi +if fm_brief_engineering_contract_present "$SCOUT_BRIEF"; then + if fm_brief_engineering_placeholders_present "$SCOUT_BRIEF"; then + echo "error: $SCOUT_BRIEF still contains {REQUIRED_SKILLS} or {TESTING_SEAMS}; fill ## Required skills and ## Agreed testing seams before promotion" >&2 + exit 1 + fi + if ! fm_brief_engineering_content_valid "$SCOUT_BRIEF"; then + echo "error: $SCOUT_BRIEF must contain nonempty ## Required skills and ## Agreed testing seams under # Engineering method before promotion" >&2 + exit 1 + fi +fi if ADDRESS_LINE=$(fm_brief_intent_address_line "$SCOUT_BRIEF"); then echo "error: $SCOUT_BRIEF ## Captain's intent has an operator-address line: $ADDRESS_LINE; write the captain's actual words without a Captain label or address before promotion, since the heading already records provenance" >&2 exit 1 @@ -167,6 +179,15 @@ if [ -z "$(printf '%s' "$INTENT_BODY" | tr -d '[:space:]')" ]; then echo "error: $SCOUT_BRIEF has no provenance-marked Captain's intent; add the captain's actual words before promotion" >&2 exit 1 fi +ENGINEERING_SECTION= +if fm_brief_engineering_contract_present "$SCOUT_BRIEF"; then + ENGINEERING_BODY=$(fm_brief_heading_body "$SCOUT_BRIEF" "# Engineering method") + IFS= read -r -d '' ENGINEERING_SECTION < "$TMP" || { echo "error: could not render ship instructions for mode=$MODE" >&2; exit 1; } diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index d9867cca406..71082d5c45c 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -12,9 +12,9 @@ # "Delivery contract: mode=" line and REFUSES a mismatch, so the worker's # instructions and the recorded task delivery cannot drift apart; a brief # scaffolded before that line existed warns once and launches on the flag. A -# ship or scout spawn also refuses leftover `{TASK}` / `{FIRSTMATE_SPEC}` -# placeholders, an empty Task, an incomplete pair of Task subsections, or a -# `## Captain's intent` line opening with a Captain label or address. +# ship or scout spawn also refuses leftover Task or Engineering method +# placeholders, empty current task-specific subsections, or a `## Captain's +# intent` line opening with a Captain label or address. # Every ship or scout spawn renders `launch-brief.md`; for a no-mistakes ship # it also carries the current `--intent` contract and the extracted captain # intent. A legacy mixed Task is accepted there only under bin/fm-dod-lib.sh's @@ -2551,6 +2551,16 @@ if [ "$KIND" = ship ] || [ "$KIND" = scout ]; then echo "error: $BRIEF must contain nonempty ## Captain's intent and ## Firstmate spec subsections (or a nonempty legacy # Task body) before spawn" >&2 exit 1 fi + if fm_brief_engineering_contract_present "$BRIEF"; then + if fm_brief_engineering_placeholders_present "$BRIEF"; then + echo "error: $BRIEF still contains {REQUIRED_SKILLS} or {TESTING_SEAMS}; fill ## Required skills and ## Agreed testing seams before spawn" >&2 + exit 1 + fi + if ! fm_brief_engineering_content_valid "$BRIEF"; then + echo "error: $BRIEF must contain nonempty ## Required skills and ## Agreed testing seams under # Engineering method before spawn" >&2 + exit 1 + fi + fi if ADDRESS_LINE=$(fm_brief_intent_address_line "$BRIEF"); then echo "error: $BRIEF ## Captain's intent has an operator-address line: $ADDRESS_LINE; write the captain's actual words without a Captain label or address before spawn, since the heading already records provenance" >&2 exit 1 diff --git a/docs/architecture.md b/docs/architecture.md index 9e13ff818b7..351879cb8fa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -313,6 +313,16 @@ The [`secondmate-provisioning` skill](../.agents/skills/secondmate-provisioning/ The `data/secondmates.md` line contract is owned by the [`secondmate-provisioning` skill](../.agents/skills/secondmate-provisioning/SKILL.md#routing-table), and the secondmate environment variables are documented in [configuration.md](configuration.md). +## Engineering methods stay inside the task lifecycle + +FirstMate owns captain interaction, task classification, worker allocation, isolation, supervision, delivery, and merge authority, while optional Matt Pocock skills contribute engineering methods inside that lifecycle. +The [`matt-pocock-integration` skill](../.agents/skills/matt-pocock-integration/SKILL.md) translates upstream planning and implementation workflows into FirstMate scouts, ships, and planning artifacts rather than allowing a nested agent fleet or second delivery path. +Every newly scaffolded ship or scout instruction file has task-specific `Required skills` and `Agreed testing seams` subsections, and `bin/fm-spawn.sh` refuses a current scaffold while either is unfilled or empty. +Saved instructions that predate the section remain launchable for recovery compatibility. +`bin/fm-promote.sh` validates the same contract and sends the current engineering method to the promoted worker, while preserving it in the instructions used by any later replacement. +The selected task delivery mode remains the sole final-review owner, so a no-mistakes task never stacks an upstream `code-review` or `implement` completion flow on top of its pipeline. +See [Matt Pocock engineering skills](matt-pocock-skills.md) for installation and operator-facing behavior. + ## Delivery modes are explicit per task `no-mistakes` tasks run the full validation pipeline, `direct-PR` tasks open PRs without that pipeline, and `local-only` tasks stay local until firstmate performs an approved fast-forward merge. diff --git a/docs/documentation-audiences.json b/docs/documentation-audiences.json index e459e95006a..41637b75987 100644 --- a/docs/documentation-audiences.json +++ b/docs/documentation-audiences.json @@ -24,8 +24,9 @@ "operator-example" ], "readmeSetupTargets": [ - "docs/configuration.md", - "docs/wedge-alarm.md", + "docs/configuration.md", + "docs/matt-pocock-skills.md", + "docs/wedge-alarm.md", "docs/tmux-backend.md", "docs/herdr-backend.md", "docs/zellij-backend.md", @@ -152,6 +153,10 @@ "path": ".agents/skills/firstmate-orca/SKILL.md", "audience": "agent-runtime" }, + { + "path": ".agents/skills/matt-pocock-integration/SKILL.md", + "audience": "agent-runtime" + }, { "path": ".agents/skills/fmx-respond/SKILL.md", "audience": "agent-runtime" @@ -368,6 +373,10 @@ "path": "docs/herdr-backend.md", "audience": "operator-current" }, + { + "path": "docs/matt-pocock-skills.md", + "audience": "operator-current" + }, { "path": "docs/orca-backend.md", "audience": "operator-current" diff --git a/docs/matt-pocock-skills.md b/docs/matt-pocock-skills.md new file mode 100644 index 00000000000..bf6bebcda34 --- /dev/null +++ b/docs/matt-pocock-skills.md @@ -0,0 +1,50 @@ +# Matt Pocock engineering skills + +FirstMate can use the [mattpocock/skills](https://github.com/mattpocock/skills) pack as an engineering-method library while retaining its own orchestration and delivery lifecycle. +FirstMate remains responsible for user decisions, worker allocation, isolation, supervision, pull requests, CI, and merge authority. +The external skills contribute methods such as diagnosis, test-driven development, module design, and domain modeling. + +## Install + +Install the complete pack globally for every agent harness that may run either the firstmate or its workers. +The complete pack matters because user-facing workflow skills delegate to smaller method skills by ID. + +```sh +npx --yes skills@latest add mattpocock/skills \ + --skill '*' \ + --global \ + --agent claude-code \ + --agent codex \ + --agent opencode \ + --agent pi \ + --yes +``` + +Adjust the repeated `--agent` flags to the worker harnesses you actually use. +Do not install a second copy through another plugin system when that would expose duplicate skill definitions to the same harness. + +## How routing works + +Captain-facing workflows such as `grill-me` and `wayfinder` remain coordinated by FirstMate. +Project investigation, prototypes, implementation, and standalone reviews are dispatched through normal scout or ship work rather than through a nested agent fleet. +Publishing a plan, specification, ticket map, or investigation report never grants implementation authority. + +Every newly scaffolded ship or scout instruction file records two task-specific inputs: + +- Required skill IDs, or a reason no external method applies. +- Agreed public testing seams, or a reason no executable test applies. + +The launch is refused while either input is still a scaffold placeholder or has an empty body. +If a named skill is unavailable to the worker, the worker reports the missing skill instead of silently falling back. +Saved instructions created before this contract remain launchable for recovery compatibility. + +## Review ownership + +The task's selected FirstMate delivery path remains the only final review owner. +In particular, a worker does not invoke `implement` or `code-review` as an extra completion flow when no-mistakes already owns review, fixes, tests, documentation, the pull request, and CI. +A standalone code review remains available when explicitly requested as its own deliverable. + +## Project setup + +This integration does not modify individual projects. +Run `setup-matt-pocock-skills` separately for a project only when you want its issue-tracker and domain-document conventions configured, and ship those changes through the project's existing FirstMate delivery policy. diff --git a/docs/scripts.md b/docs/scripts.md index 5b8ceb56d3e..fbb5e02a579 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -32,8 +32,8 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize | `fm-backlog-receive.sh` | Idempotently ingest one confined remote handoff outbox through tasks-axi | | `fm-captain-hold.sh` | Hold tasks for the captain, record the captain's answers, gate investigation completion, and report record divergence between the status log and the backlog | | `fm-decision-hold.sh` | One-release compatibility shim mapping the retired decision commands onto fm-captain-hold.sh | -| `fm-brief.sh` | Scaffold ship (explicit `--mode`), scout, secondmate-charter, and Herdr-lab briefs, with Captain's intent and Firstmate spec subsections on ship/scout | -| [`fm-dod-lib.sh`](../bin/fm-dod-lib.sh) | Own ship/scout worker role scope, ship definitions of done, and the no-mistakes `--intent` contract | +| `fm-brief.sh` | Scaffold ship, scout, secondmate-charter, and Herdr-lab instructions, including task-specific engineering methods for ships and scouts | +| [`fm-dod-lib.sh`](../bin/fm-dod-lib.sh) | Own worker role scope, instruction-section parsing, ship definitions of done, and the no-mistakes `--intent` contract | | `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session | | `fm-herdr-lab-viewer.py` | The pty engine behind `fm-herdr-lab.sh viewer`: one real foreground Herdr client on a non-zero window grid | | `fm-install-herdr.sh` | Install CI's exact-version Herdr pin with official asset URL, SHA-256, and protocol checks | diff --git a/tests/fixtures.sh b/tests/fixtures.sh index 043d350012e..f138016c411 100755 --- a/tests/fixtures.sh +++ b/tests/fixtures.sh @@ -247,6 +247,13 @@ $intent ## Firstmate spec Exercise the spawn behavior under test. + +# Engineering method +## Required skills +- tdd + +## Agreed testing seams +- The task launch interface. EOF } diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 88f4e566ff0..8da9ba450dc 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -213,6 +213,15 @@ test_ship_modes_generate_clean_briefs() { assert_grep "{FIRSTMATE_SPEC}" "$brief" "$id: brief missing the {FIRSTMATE_SPEC} placeholder" assert_grep "## Captain's intent" "$brief" "$id: brief missing Captain's intent subsection" assert_grep "## Firstmate spec" "$brief" "$id: brief missing Firstmate spec subsection" + assert_grep "# Engineering method" "$brief" "$id: brief missing Engineering method section" + assert_grep "## Required skills" "$brief" "$id: brief missing Required skills subsection" + assert_grep "{REQUIRED_SKILLS}" "$brief" "$id: brief missing the {REQUIRED_SKILLS} placeholder" + assert_grep "## Agreed testing seams" "$brief" "$id: brief missing Agreed testing seams subsection" + assert_grep "{TESTING_SEAMS}" "$brief" "$id: brief missing the {TESTING_SEAMS} placeholder" + assert_grep "FirstMate's selected delivery path is the only final review owner." "$brief" \ + "$id: brief did not keep final review with FirstMate's delivery path" + assert_grep 'Do not invoke `code-review`, `implement`, `research`, `wayfinder`, or another workflow that delegates work or starts a second delivery path.' "$brief" \ + "$id: brief did not prevent nested orchestration and duplicate review" assert_grep 'never a bare number such as "PR 108"' "$brief" "$id: brief missing the full-PR-URL rule" assert_grep "mid-task \`working:\` line (including setup complete) is nonterminal" "$brief" \ "$id: brief missing nonterminal working:/setup-complete gate protection" @@ -887,6 +896,9 @@ test_scout_and_secondmate_scaffold() { assert_grep "## Captain's intent" "$brief" "scout brief missing Captain's intent subsection" assert_grep "## Firstmate spec" "$brief" "scout brief missing Firstmate spec subsection" assert_grep "{FIRSTMATE_SPEC}" "$brief" "scout brief missing the spec placeholder" + assert_grep "# Engineering method" "$brief" "scout brief missing Engineering method section" + assert_grep "{REQUIRED_SKILLS}" "$brief" "scout brief missing the Required skills placeholder" + assert_grep "{TESTING_SEAMS}" "$brief" "scout brief missing the testing seams placeholder" FM_SECONDMATE_CHARTER='Supervise the alpha domain.' \ FM_HOME="$BRIEF_HOME" "$ROOT/bin/fm-brief.sh" brief-sm-q6 --secondmate alpha >/dev/null 2>&1 \ @@ -899,6 +911,8 @@ test_scout_and_secondmate_scaffold() { "secondmate charter must not grow ship/scout Task subsections" assert_no_grep "{FIRSTMATE_SPEC}" "$brief" \ "secondmate charter must not carry the Firstmate spec placeholder" + assert_no_grep "# Engineering method" "$brief" \ + "secondmate charter must not carry a task engineering method" pass "fm-brief: scout and secondmate code paths still scaffold well-formed briefs" } diff --git a/tests/fm-cmux-claude-composer-live-e2e.test.sh b/tests/fm-cmux-claude-composer-live-e2e.test.sh index 439d9335e97..d84d839d6bc 100755 --- a/tests/fm-cmux-claude-composer-live-e2e.test.sh +++ b/tests/fm-cmux-claude-composer-live-e2e.test.sh @@ -53,12 +53,18 @@ import sys brief = Path(sys.argv[1]) status = sys.argv[2] -brief.write_text(brief.read_text().replace("{TASK}", f'''Run a cmux communication probe. +brief.write_text( + brief.read_text() + .replace("{TASK}", f'''Run a cmux communication probe. Immediately append `working: cmux composer probe ready` to `{status}`. Then append exactly `needs-decision [key=probe-decision]: awaiting codeword` to that file and stop to wait for a firstmate message. When you receive a firstmate message containing `ALBATROSS`, append `done: received ALBATROSS` to that status file and stop. -Do not change project files or make a commit.''')) +Do not change project files or make a commit.''') + .replace("{FIRSTMATE_SPEC}", "Exercise only the supervised communication path.") + .replace("{REQUIRED_SKILLS}", "- None - this is a communication probe.") + .replace("{TESTING_SEAMS}", "- The cmux worker messaging interface.") +) PY FM_HOME="$LAB" "$ROOT/bin/fm-spawn.sh" "$TASK" "$LAB/projects/comms" --scout --harness claude --model haiku --backend cmux \ diff --git a/tests/fm-control-relaunch.test.sh b/tests/fm-control-relaunch.test.sh index 0367ff56749..286b97752ec 100755 --- a/tests/fm-control-relaunch.test.sh +++ b/tests/fm-control-relaunch.test.sh @@ -980,7 +980,7 @@ test_promoted_scout_relaunch_receives_the_current_delivery_contract() { FM_HOME="$home" "$BRIEF" "$id" firstmate --scout >/dev/null \ || fail "$mode: could not scaffold the scout brief" brief="$home/data/$id/brief.md" - sed 's/{TASK}/Fix the promotion relaunch contract./; s/{FIRSTMATE_SPEC}/Preserve the current delivery mode./' \ + sed 's/{TASK}/Fix the promotion relaunch contract./; s/{FIRSTMATE_SPEC}/Preserve the current delivery mode./; s/{REQUIRED_SKILLS}/- tdd/; s/{TESTING_SEAMS}/- The promoted worker relaunch interface./' \ "$brief" > "$brief.filled" mv "$brief.filled" "$brief" { diff --git a/tests/fm-omp-harness.test.sh b/tests/fm-omp-harness.test.sh index 7c25848eedd..3d4fb0d9dfb 100755 --- a/tests/fm-omp-harness.test.sh +++ b/tests/fm-omp-harness.test.sh @@ -55,7 +55,7 @@ export NODE_NO_WARNINGS=1 make_named_shells() { # -> echoes local dir=$1 name mkdir -p "$dir" - for name in omp ompd comp; do + for name in omp ompd comp claude; do ln -sf /bin/bash "$dir/$name" done printf '%s' "$dir" @@ -84,7 +84,7 @@ test_detection_anchored_name_and_marker_precedence() { # ...and is inert when it leaks into a worker with no omp ancestor. # shellcheck disable=SC2016 # the quoted body expands inside the named shell out=$(env -u PI_CODING_AGENT -u CURSOR_AGENT -u CURSOR_INVOKED_AS CLAUDECODE=1 FM_OMP_HARNESS=omp \ - bash -c '"$1"; :' _ "$HARNESS") + "$bin/claude" -c '"$1"; :' _ "$HARNESS") [ "$out" = claude ] || fail "a leaked FM_OMP_HARNESS without an omp ancestor must not relabel a claude worker, got '$out'" pass "fm-harness: omp detects by its anchored name; the marker is a precedence override that needs real omp ancestry" } diff --git a/tests/fm-spawn-dispatch-profile.test.sh b/tests/fm-spawn-dispatch-profile.test.sh index f7274693ae7..0fe77732592 100755 --- a/tests/fm-spawn-dispatch-profile.test.sh +++ b/tests/fm-spawn-dispatch-profile.test.sh @@ -1266,6 +1266,8 @@ test_worker_launch_delivers_role_scope() { content=$(cat "$brief") content=${content//'{TASK}'/brief for $id} content=${content//'{FIRSTMATE_SPEC}'/Exercise the spawn behavior under test.} + content=${content//'{REQUIRED_SKILLS}'/'- None - this test exercises worker launch behavior.'} + content=${content//'{TESTING_SEAMS}'/'- The generated worker launch prompt.'} printf '%s\n' "$content" > "$brief" fi cp "$HOME_DIR/data/$id/brief.md" "$CASE_DIR/brief-before" diff --git a/tests/fm-task-delivery.test.sh b/tests/fm-task-delivery.test.sh index f3232dec761..096097c2ca9 100755 --- a/tests/fm-task-delivery.test.sh +++ b/tests/fm-task-delivery.test.sh @@ -56,6 +56,8 @@ fill_brief_subsections() { # content=$(cat "$file") content=${content//'{TASK}'/$intent} content=${content//'{FIRSTMATE_SPEC}'/$spec} + content=${content//'{REQUIRED_SKILLS}'/'- tdd'} + content=${content//'{TESTING_SEAMS}'/'- The command-line task launch interface.'} printf '%s\n' "$content" > "$file" } @@ -354,6 +356,16 @@ STUB "$mode: promoted worker did not receive the Captain's intent subsection" assert_grep "## Firstmate spec" "$payload" \ "$mode: promoted worker did not receive the Firstmate spec subsection" + assert_grep "# Engineering method" "$payload" \ + "$mode: promoted worker did not receive the current engineering method" + assert_grep "## Required skills" "$payload" \ + "$mode: promoted worker did not receive its required skill list" + assert_grep "- tdd" "$payload" \ + "$mode: promoted worker did not receive its task-specific required skill" + assert_grep "## Agreed testing seams" "$payload" \ + "$mode: promoted worker did not receive its agreed testing seams" + assert_grep "The command-line task launch interface" "$payload" \ + "$mode: promoted worker did not receive its task-specific testing seam" # Compare the public outputs of both real generation paths. The promoted # payload ends at its Definition of done, as does an ordinary generated @@ -751,6 +763,45 @@ EOF pass "fm-spawn/fm-promote: leftover Task placeholders are refused until both subsections are filled" } +# Current scaffolds require task-specific engineering skills and testing seams. +# Spawn validates only the actual subsection bodies, so literal placeholder +# examples elsewhere in a completed brief remain ordinary task content. +test_spawn_requires_filled_engineering_method() { + local rec home proj fakebin id brief content out status + rec=$(make_home engineering-method) + IFS='|' read -r home proj fakebin </dev/null 2>&1 \ + || fail "engineering-method brief should scaffold" + brief="$home/data/$id/brief.md" + content=$(cat "$brief") + content=${content//'{TASK}'/'Implement the launch contract.'} + content=${content//'{FIRSTMATE_SPEC}'/'Keep delivery ownership unchanged.'} + printf '%s\n' "$content" > "$brief" + + out=$(run_spawn "$home" "$fakebin" "$id" "$proj" claude --mode direct-PR --yolo off) + status=$? + [ "$status" -ne 0 ] || fail "spawn with an unfilled engineering method should exit non-zero" + assert_contains "$out" 'still contains {REQUIRED_SKILLS} or {TESTING_SEAMS}' \ + "spawn did not name the unfilled engineering method" + assert_contains "$out" 'fill ## Required skills and ## Agreed testing seams' \ + "spawn did not identify the engineering subsections to fill" + assert_absent "$home/state/$id.meta" "unfilled engineering method wrote task metadata" + + content=${content//'{REQUIRED_SKILLS}'/'- tdd'} + content=${content//'{TESTING_SEAMS}'/'- The literal {REQUIRED_SKILLS} and {TESTING_SEAMS} examples in this seam.'} + printf '%s\n' "$content" > "$brief" + out=$(run_spawn "$home" "$fakebin" "$id" "$proj" claude --mode direct-PR --yolo off) + assert_not_contains "$out" 'still contains {REQUIRED_SKILLS} or {TESTING_SEAMS}' \ + "completed engineering instructions containing literal examples were refused" + assert_not_contains "$out" 'must contain nonempty ## Required skills and ## Agreed testing seams' \ + "completed engineering instructions failed semantic validation" + pass "fm-spawn: current briefs require task-specific engineering skills and testing seams" +} + # Exercise the serialized input a worker is told to pass to no-mistakes, not # just the presence of words somewhere in its much larger launch brief. # No live model or pipeline is needed: spawn publishes this exact input before @@ -892,4 +943,5 @@ test_promote_refuses_a_symlinked_task_record test_promotion_delivers_the_real_definition_of_done test_project_mode_maps_the_conditional_policy test_spawn_and_promote_require_filled_task_subsections +test_spawn_requires_filled_engineering_method echo "# all fm-task-delivery tests passed" From d3a55a7139318aa9f12877818b24c696d29b8d09 Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Mon, 5 Oct 2026 00:28:38 -0400 Subject: [PATCH 2/8] no-mistakes(document): Fix promotion header enumeration and setup-target indentation --- bin/fm-promote.sh | 5 +++-- docs/documentation-audiences.json | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bin/fm-promote.sh b/bin/fm-promote.sh index 709f59c5563..a1fc6b1e4a7 100755 --- a/bin/fm-promote.sh +++ b/bin/fm-promote.sh @@ -17,8 +17,9 @@ # `## Captain's intent` preserved from the scout brief and promotion's ship-time # instructions under `## Firstmate spec`; the scout-time spec remains context but # is not relabeled as the ship spec. Promotion refuses leftover `{TASK}` / -# `{FIRSTMATE_SPEC}` placeholders and a `## Captain's intent` line opening with -# a Captain label or address (bin/fm-dod-lib.sh). A pre-subsection scout +# `{FIRSTMATE_SPEC}`/`{REQUIRED_SKILLS}`/`{TESTING_SEAMS}` placeholders, +# an empty current Engineering method subsection, and a `## Captain's intent` +# line opening with a Captain label or address (bin/fm-dod-lib.sh). A pre-subsection scout # brief contributes only Task lines explicitly marked as captain words to intent. # A scout records no delivery posture, so promotion is where this task's delivery # contract is decided: --mode and --yolo are REQUIRED and written into the meta diff --git a/docs/documentation-audiences.json b/docs/documentation-audiences.json index 41637b75987..aadbcebb7c1 100644 --- a/docs/documentation-audiences.json +++ b/docs/documentation-audiences.json @@ -24,9 +24,9 @@ "operator-example" ], "readmeSetupTargets": [ - "docs/configuration.md", - "docs/matt-pocock-skills.md", - "docs/wedge-alarm.md", + "docs/configuration.md", + "docs/matt-pocock-skills.md", + "docs/wedge-alarm.md", "docs/tmux-backend.md", "docs/herdr-backend.md", "docs/zellij-backend.md", From 68b2d4ed39fb5b0400d5aca1cb4be0e53ba472b9 Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Mon, 5 Oct 2026 00:29:41 -0400 Subject: [PATCH 3/8] no-mistakes(lint): Fix SC2016 false positive in fm-brief test assertion --- tests/fm-brief.test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 8da9ba450dc..0d08f622f2a 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -220,7 +220,7 @@ test_ship_modes_generate_clean_briefs() { assert_grep "{TESTING_SEAMS}" "$brief" "$id: brief missing the {TESTING_SEAMS} placeholder" assert_grep "FirstMate's selected delivery path is the only final review owner." "$brief" \ "$id: brief did not keep final review with FirstMate's delivery path" - assert_grep 'Do not invoke `code-review`, `implement`, `research`, `wayfinder`, or another workflow that delegates work or starts a second delivery path.' "$brief" \ + assert_grep "Do not invoke \`code-review\`, \`implement\`, \`research\`, \`wayfinder\`, or another workflow that delegates work or starts a second delivery path." "$brief" \ "$id: brief did not prevent nested orchestration and duplicate review" assert_grep 'never a bare number such as "PR 108"' "$brief" "$id: brief missing the full-PR-URL rule" assert_grep "mid-task \`working:\` line (including setup complete) is nonterminal" "$brief" \ From 5e85296f88c738dcef6373598e7135635bbc0035 Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Mon, 5 Oct 2026 03:25:43 -0400 Subject: [PATCH 4/8] =?UTF-8?q?no-mistakes(ci):=20Fixed=20the=20single=20f?= =?UTF-8?q?ailing=20test=20in=20'Behavior=20portable=20serial=203':=20test?= =?UTF-8?q?s/fm-pi-branch-extension.test.sh=20case=20'Pi=20outcomes=20rend?= =?UTF-8?q?ering=20consumers=20must=20preserve=20stock=20behavior'=20(Calm?= =?UTF-8?q?-off=20ToolExecutionComponent=20rendering=20differs=20from=20Pi?= =?UTF-8?q?=20stock).=20Root=20cause:=20upstream=20version=20drift,=20not?= =?UTF-8?q?=20this=20PR's=20diff=20=E2=80=94=20CI=20does=20npm=20install?= =?UTF-8?q?=20-g=20@earendil-works/pi-coding-agent=20(latest),=20which=20n?= =?UTF-8?q?ow=20resolves=20to=201.0.2,=20and=20since=20Pi=200.99.0=20the?= =?UTF-8?q?=20stock=20call-fallback=20renders=20the=20tool's=20args=20besi?= =?UTF-8?q?de=20the=20bold=20title=20(collapsed=20muted=20preview,=20e.g.?= =?UTF-8?q?=20'fm=5Fbranch=5Foutcomes=20recent=3D2')=20or=20as=20an=20inde?= =?UTF-8?q?nted=20per-arg=20block=20when=20expanded.=20The=20tracked=20ext?= =?UTF-8?q?ension=20renderers=20(fm=5Fbranch=5Foutcomes,=20fm=5Fbranch=5Fp?= =?UTF-8?q?rocessed)=20in=20.pi/extensions/fm-branch-supervision.ts=20stil?= =?UTF-8?q?l=20emitted=20only=20the=20bare=20bold=20title,=20so=20calm-off?= =?UTF-8?q?=20rendering=20diverged=20from=20stock=20and=20the=20stock-comp?= =?UTF-8?q?arison=20case=20failed.=20Reproduced=20locally=20with=20an=20is?= =?UTF-8?q?olated=20npm=20prefix=20of=20pi-coding-agent@1.0.2=20(FM=5FPI?= =?UTF-8?q?=5FPACKAGE=5FDIR):=20the=20case=20failed=20before=20the=20fix?= =?UTF-8?q?=20with=20the=20exact=20CI=20error.=20Fix=20(smallest=20root-ca?= =?UTF-8?q?use=20change,=20matching=20the=20file's=20existing=20version-fl?= =?UTF-8?q?oor=20precedent):=20(1)=20added=20a=20stockToolCallComponent=20?= =?UTF-8?q?helper=20in=20the=20extension=20that=20byte-mirrors=20Pi=200.99?= =?UTF-8?q?.0+'s=20formatToolCallWithArgs=20fallback=20and=20made=20both?= =?UTF-8?q?=20tools'=20renderCall=20use=20it;=20(2)=20raised=20the=20test'?= =?UTF-8?q?s=20PI=5FSTOCK=5FRENDER=5FFLOOR=20from=200.84.4=20to=200.99.0?= =?UTF-8?q?=20so=20the=20comparison=20skips=20(named=20version=20skip)=20o?= =?UTF-8?q?n=20hosts=20with=20a=20Pi=20that=20predates=20the=20args-in-cal?= =?UTF-8?q?l=20contract,=20per=20the=20existing=20version-skew=20pattern.?= =?UTF-8?q?=20Verified:=20bin/fm-test-run.sh=20tests/fm-pi-branch-extensio?= =?UTF-8?q?n.test.sh=20now=20exits=200=20against=20Pi=201.0.2=20with=20the?= =?UTF-8?q?=20previously=20failing=20case=20passing;=20bin/fm-test-run.sh?= =?UTF-8?q?=20tests/fm-pi-primary-types.test.sh=20passes=20strict=20no-emi?= =?UTF-8?q?t=20typecheck=20against=20Pi=201.0.2;=20bin/fm-lint.sh=20passes?= =?UTF-8?q?=20with=20the=20pinned=20ShellCheck=200.11.0=20and=20actionlint?= =?UTF-8?q?=201.7.12.=20Intentionally=20not=20attempted:=20pinning=20the?= =?UTF-8?q?=20Pi=20version=20in=20CI=20=E2=80=94=20the=20repo's=20establis?= =?UTF-8?q?hed=20contract=20is=20that=20the=20extension=20tracks=20Pi's=20?= =?UTF-8?q?current=20stock=20rendering=20(runtime=20probes=20+=20named=20v?= =?UTF-8?q?ersion=20floors),=20so=20updating=20the=20renderers=20and=20the?= =?UTF-8?q?=20floor=20is=20the=20root-cause=20fix=20consistent=20with=20ex?= =?UTF-8?q?isting=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pi/extensions/fm-branch-supervision.ts | 41 ++++++++++++++++++++++--- tests/fm-pi-branch-extension.test.sh | 23 ++++++++------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.pi/extensions/fm-branch-supervision.ts b/.pi/extensions/fm-branch-supervision.ts index 682f0a087ab..d4fc8447437 100644 --- a/.pi/extensions/fm-branch-supervision.ts +++ b/.pi/extensions/fm-branch-supervision.ts @@ -2061,6 +2061,39 @@ ${context.command} return shell; }; + // Pi 0.99.0's stock call fallback now renders the tool's args beside the + // bold title (a muted collapsed preview) or as an indented per-arg block + // (expanded). The package does not export that formatter, so mirror its + // exact output here: calm-off rendering must be byte-identical to stock. + const STOCK_COLLAPSED_ARGS_CHARS = 100; + const stockToolCallComponent = ( + name: string, + args: unknown, + theme: Parameters>[1], + expanded: boolean, + ): Text => { + const header = theme.fg("toolTitle", theme.bold(name)); + if (args === null || args === undefined) return new Text(header, 0, 0); + const entries: [string, unknown][] = + typeof args === "object" && !Array.isArray(args) + ? Object.entries(args as Record) + : [["args", args]]; + if (entries.length === 0) return new Text(header, 0, 0); + if (expanded) { + const lines = entries.map(([key, value]) => { + const text = typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? String(value); + return ` ${key}: ${text.replace(/\t/g, " ").replace(/\r/g, "").split("\n").join("\n ")}`; + }); + return new Text(`${header}\n${theme.fg("muted", lines.join("\n"))}`, 0, 0); + } + const pairs = entries.map(([key, value]) => `${key}=${JSON.stringify(value) ?? String(value)}`).join(" "); + const preview = + pairs.length > STOCK_COLLAPSED_ARGS_CHARS + ? `${pairs.slice(0, STOCK_COLLAPSED_ARGS_CHARS - 3)}...` + : pairs; + return new Text(`${header} ${theme.fg("muted", preview)}`, 0, 0); + }; + registerFirstmateTool(pi, { name: "fm_branch_outcomes", label: "Read supervision branch outcomes", @@ -2071,11 +2104,11 @@ ${context.command} recent: Type.Optional(Type.Number({ description: "How many most-recent outcomes to read (default 20)" })), }), renderShell: "self", - renderCall: (_args, theme, context) => { + renderCall: (args, theme, context) => { if (calmPresentation.stockExportRendering) throw new Error("Use Pi stock export rendering"); if (calmHides("assistant-tool-call")) return new Container(); const shellState = context.state as OutcomesToolShellState; - shellState.call = new Text(theme.fg("toolTitle", theme.bold("fm_branch_outcomes")), 0, 0); + shellState.call = stockToolCallComponent("fm_branch_outcomes", args, theme, context.expanded); return refreshOutcomesToolShell(shellState, theme, context); }, renderResult: (result, options, theme, context) => { @@ -2133,11 +2166,11 @@ ${context.command} through: Type.Number({ description: "The highest outcome sequence number this conversation has processed" }), }), renderShell: "self", - renderCall: (_args, theme, context) => { + renderCall: (args, theme, context) => { if (calmPresentation.stockExportRendering) throw new Error("Use Pi stock export rendering"); if (calmHides("assistant-tool-call")) return new Container(); const shellState = context.state as OutcomesToolShellState; - shellState.call = new Text(theme.fg("toolTitle", theme.bold("fm_branch_processed")), 0, 0); + shellState.call = stockToolCallComponent("fm_branch_processed", args, theme, context.expanded); return refreshOutcomesToolShell(shellState, theme, context); }, renderResult: (result, _options, theme, context) => { diff --git a/tests/fm-pi-branch-extension.test.sh b/tests/fm-pi-branch-extension.test.sh index 812a55eade3..163f59595c7 100644 --- a/tests/fm-pi-branch-extension.test.sh +++ b/tests/fm-pi-branch-extension.test.sh @@ -15,10 +15,12 @@ set -u TMP_ROOT=$(fm_test_tmproot fm-pi-branch-extension) EXT="$ROOT/.pi/extensions/fm-branch-supervision.ts" export NODE_NO_WARNINGS=1 -# The Pi release whose stock renderer stopped supplying an implicit reset at -# multiline boundaries, which is the contract this file's renderer cases -# compare against. -PI_STOCK_RENDER_FLOOR=0.84.4 +# The Pi release whose stock renderer contract the extension's own renderers +# now target: since 0.99.0 the stock call fallback also renders the tool's +# args (a muted collapsed preview beside the bold title, or an indented +# per-arg block when expanded), and the extension emits them itself. This is +# also the contract this file's renderer cases compare against. +PI_STOCK_RENDER_FLOOR=0.99.0 # Semantic-version floor for a version string this file already holds (Pi's # package.json field). bin/fm-bootstrap.sh's tool_version_at_least is the same @@ -4218,12 +4220,13 @@ test_outcomes_tool_uses_stock_execution_and_export_consumers() { fi # This case compares the extension's own renderers against Pi's stock # rendering, so its verdict is only meaningful against the vendor contract - # those renderers target: since Pi 0.84.4 the stock renderer no longer - # supplies an implicit reset at multiline boundaries, and the extension - # emits that reset itself. An older installed Pi still supplies it, so the - # two legitimately differ there and a comparison would report a defect that - # is really a version skew. Name the version and skip rather than degrade - # quietly; a package whose version cannot be read at all is still a failure. + # those renderers target: since Pi 0.99.0 the stock call fallback renders + # the tool's args beside (collapsed) or below (expanded) the bold title, + # and the extension mirrors that itself. An older installed Pi renders the + # bare title, so the two legitimately differ there and a comparison would + # report a defect that is really a version skew. Name the version and skip + # rather than degrade quietly; a package whose version cannot be read at + # all is still a failure. package_version=$(node -p 'require(process.argv[1]).version || ""' "$package_dir/package.json" 2>/dev/null || printf '') [ -n "$package_version" ] \ || fail "installed @earendil-works/pi-coding-agent has no readable version at $package_dir" From 0c2b9c88725292bd255efce525d4953edf2cb953 Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Mon, 5 Oct 2026 04:26:07 -0400 Subject: [PATCH 5/8] =?UTF-8?q?no-mistakes(ci):=20Fixed=20the=20single=20f?= =?UTF-8?q?ailing=20case=20in=20check=20'Behavior=20portable=20serial=201'?= =?UTF-8?q?:=20tests/fm-calm-pi-extension.test.sh=20'Pi=20calm=20renderer?= =?UTF-8?q?=20and=20lifecycle=20contract'=20('grep=20disappeared=20from=20?= =?UTF-8?q?/export=20calm.html=20HTML=20while=20calm=20mode=20was=20on').?= =?UTF-8?q?=20Root=20cause:=20upstream=20Pi=20version=20drift,=20not=20thi?= =?UTF-8?q?s=20PR's=20diff=20=E2=80=94=20CI=20installs=20@earendil-works/p?= =?UTF-8?q?i-coding-agent=20latest=20(1.0.2),=20and=20Pi=201.0.1=20renamed?= =?UTF-8?q?=20the=20createToolHtmlRenderer=20lookup=20dependency=20from=20?= =?UTF-8?q?getToolDefinition=20to=20getToolRenderers=20(agent-session's=20?= =?UTF-8?q?exportToHtml=20now=20resolves=20renderers=20via=20the=20extensi?= =?UTF-8?q?on=20runner).=20The=20fixtures=20still=20passed=20getToolDefini?= =?UTF-8?q?tion,=20so=20on=20Pi=20>=3D=201.0.1=20the=20renderer=20resolved?= =?UTF-8?q?=20no=20definition=20and=20renderCall=20returned=20undefined.?= =?UTF-8?q?=20Reproduced=20locally=20with=20an=20isolated=20npm=20prefix?= =?UTF-8?q?=20of=20pi-coding-agent@1.0.2=20(FM=5FPI=5FPACKAGE=5FDIR):=20th?= =?UTF-8?q?e=20case=20failed=20before=20the=20fix=20with=20the=20exact=20C?= =?UTF-8?q?I=20error.=20Fix=20(smallest=20root-cause=20change):=20pass=20b?= =?UTF-8?q?oth=20lookup=20spellings=20in=20the=20affected=20fixtures=20?= =?UTF-8?q?=E2=80=94=20older=20Pi=20destructures=20getToolDefinition=20and?= =?UTF-8?q?=20ignores=20the=20extra=20key,=20newer=20Pi=20uses=20getToolRe?= =?UTF-8?q?nderers,=20with=20identical=20lookup=20semantics,=20so=20no=20v?= =?UTF-8?q?ersion=20check=20is=20needed.=20Two=20files:=20tests/fm-calm-pi?= =?UTF-8?q?-extension.test.sh=20(new=20exportToolHtmlDeps=20helper=20used?= =?UTF-8?q?=20by=20both=20createToolHtmlRenderer=20sites=20in=20the=20rend?= =?UTF-8?q?erer/lifecycle=20case)=20and=20tests/fm-pi-branch-extension.tes?= =?UTF-8?q?t.sh=20(the=20structured-fallback=20export=20assertion,=20which?= =?UTF-8?q?=20under=20Pi=20>=3D=201.0.1=20was=20passing=20only=20vacuously?= =?UTF-8?q?=20from=20the=20unrecognized=20key=20=E2=80=94=20adding=20getTo?= =?UTF-8?q?olRenderers=20restores=20real=20exercise=20of=20the=20tool's=20?= =?UTF-8?q?throw-to-fallback=20contract).=20Verified:=20bin/fm-test-run.sh?= =?UTF-8?q?=20tests/fm-calm-pi-extension.test.sh=20exits=200=20on=20both?= =?UTF-8?q?=20Pi=201.0.2=20and=20the=20locally=20installed=20Pi=200.85.1;?= =?UTF-8?q?=20bin/fm-test-run.sh=20tests/fm-pi-branch-extension.test.sh=20?= =?UTF-8?q?exits=200=20on=20Pi=201.0.2;=20bin/fm-lint.sh=20passes=20with?= =?UTF-8?q?=20pinned=20ShellCheck=200.11.0=20and=20actionlint=201.7.12.=20?= =?UTF-8?q?Only=20test=20fixtures=20changed;=20no=20production=20code=20co?= =?UTF-8?q?nsumes=20createToolHtmlRenderer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fm-calm-pi-extension.test.sh | 20 ++++++++++---------- tests/fm-pi-branch-extension.test.sh | 7 +++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/fm-calm-pi-extension.test.sh b/tests/fm-calm-pi-extension.test.sh index 156136b01fe..39c88e2c3a2 100755 --- a/tests/fm-calm-pi-extension.test.sh +++ b/tests/fm-calm-pi-extension.test.sh @@ -1299,14 +1299,18 @@ for (const { name, actual } of rows) { throw new Error(`${name} was not hidden before export rendering`); } } +// Pi 1.0.1 renamed the createToolHtmlRenderer lookup dep from getToolDefinition +// to getToolRenderers (exportToHtml now resolves renderers through the extension +// runner). Older Pi destructures the old name and ignores the extra key, so the +// fixture passes both spellings of the same lookup to cover either era. +const exportToolHtmlDeps = () => { + const lookup = (name) => tools.find((tool) => tool.name === name); + return { getToolDefinition: lookup, getToolRenderers: lookup, theme, cwd: process.cwd() }; +}; async function assertStockHtmlRendering(command, submitData) { editorText = command; terminalInputHandler(submitData); - const htmlRenderer = createToolHtmlRenderer({ - getToolDefinition: (name) => tools.find((tool) => tool.name === name), - theme, - cwd: process.cwd(), - }); + const htmlRenderer = createToolHtmlRenderer(exportToolHtmlDeps()); const exportCases = [ ...cases.filter(([toolName]) => toolName === "grep" || toolName === "find"), ["fm_watch_arm_pi", watchArgs, watchResult], @@ -1333,11 +1337,7 @@ await assertStockHtmlRendering("/export calm.html", "\r"); getKeybindings().setUserBindings({ "tui.input.submit": "alt+s" }); editorText = "/export remapped.html"; terminalInputHandler("\r"); -const unmatchedRenderer = createToolHtmlRenderer({ - getToolDefinition: (name) => tools.find((tool) => tool.name === name), - theme, - cwd: process.cwd(), -}); +const unmatchedRenderer = createToolHtmlRenderer(exportToolHtmlDeps()); if (unmatchedRenderer.renderCall("unmatched-submit", "grep", { pattern: "alpha", path: "." })) { throw new Error("ordinary non-submit input activated HTML export rendering"); } diff --git a/tests/fm-pi-branch-extension.test.sh b/tests/fm-pi-branch-extension.test.sh index 163f59595c7..57db76e7596 100644 --- a/tests/fm-pi-branch-extension.test.sh +++ b/tests/fm-pi-branch-extension.test.sh @@ -4347,8 +4347,11 @@ if (JSON.stringify(actualRow.render(100)) !== JSON.stringify(stockRow.render(100 } pi.events.emit("firstmate:calm-presentation", { active: true, stockExportRendering: true }); -const stockHtml = createToolHtmlRenderer({ getToolDefinition: () => stockDefinition, theme, cwd: process.cwd() }); -const actualHtml = createToolHtmlRenderer({ getToolDefinition: () => actualDefinition, theme, cwd: process.cwd() }); +// Pi 1.0.1 renamed the lookup dep from getToolDefinition to getToolRenderers; +// pass both spellings so the structured-fallback assertion exercises the tool +// on either era of Pi rather than passing vacuously from an unrecognized key. +const stockHtml = createToolHtmlRenderer({ getToolDefinition: () => stockDefinition, getToolRenderers: () => stockDefinition, theme, cwd: process.cwd() }); +const actualHtml = createToolHtmlRenderer({ getToolDefinition: () => actualDefinition, getToolRenderers: () => actualDefinition, theme, cwd: process.cwd() }); const stockCall = stockHtml.renderCall("stock-html", "fm_branch_outcomes", args); const actualCall = actualHtml.renderCall("actual-html", "fm_branch_outcomes", args); const stockResult = stockHtml.renderResult("stock-html", "fm_branch_outcomes", result.content, result.details, false); From 397e6de198d603d808f51610148b2f7d2a2b89ba Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Mon, 5 Oct 2026 06:11:34 -0400 Subject: [PATCH 6/8] test(calm-pi): give E2E tmux panes 400 rows so transcript rows stay visible Since Pi 1.0's fullscreen TUI, restored transcript rows live in Pi's virtualized scrollback, so a 44-row tmux capture no longer sees the E2E-appended rows (CI failure: missing CALM_E2E_OUTPUT on Pi 1.0.2). Raise the session/resize height to 400 at the three affected sites so the viewport holds the whole restored transcript plus appended rows. --- tests/fm-calm-pi-extension.test.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/fm-calm-pi-extension.test.sh b/tests/fm-calm-pi-extension.test.sh index 39c88e2c3a2..62c1daa9f46 100755 --- a/tests/fm-calm-pi-extension.test.sh +++ b/tests/fm-calm-pi-extension.test.sh @@ -3577,7 +3577,11 @@ TS {"type":"message","id":"a0000016","parentId":"a0000015","timestamp":"$now","message":{"role":"assistant","content":[{"type":"text","text":"The deterministic tool example is complete."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":2,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":3,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":16}} JSON - tmux -L "$TMUX_SOCKET" new-session -d -s "$TMUX_SESSION" -x 180 -y 44 \ + # The pane must be tall enough to hold the whole restored transcript plus every + # row this E2E appends later: since Pi 1.0's fullscreen TUI the transcript lives + # in Pi's own virtualized scrollback, so the tmux capture below (viewport plus + # recent history) only ever sees rows currently on screen. + tmux -L "$TMUX_SOCKET" new-session -d -s "$TMUX_SESSION" -x 180 -y 400 \ "cd '$project' && env FM_HOME='$home' PI_CODING_AGENT_DIR='$config' FM_OPERATIONAL_INPUT_SCRIPT='$OPERATIONAL_INPUT' PI_OFFLINE=1 pi --approve --no-skills --no-prompt-templates --no-context-files --session '$session_file'; rc=\$?; printf '\nPI_EXIT=%s\n' \"\$rc\"; sleep 30" wait_for_text "$default_snapshot" "The deterministic tool example is complete." \ || fail "Pi calm E2E did not reach the restored session transcript" @@ -4182,7 +4186,7 @@ JS active_screen_wait=$((active_screen_wait + 1)) done [ "$(cat "$home/config/calm")" = on ] || fail "Calm was not restored before the persistence restart" - tmux -L "$TMUX_SOCKET" resize-window -t "$TMUX_SESSION" -x 180 -y 44 + tmux -L "$TMUX_SOCKET" resize-window -t "$TMUX_SESSION" -x 180 -y 400 tmux -L "$TMUX_SOCKET" send-keys -t "$TMUX_SESSION" -l "/quit" tmux -L "$TMUX_SOCKET" send-keys -t "$TMUX_SESSION" M-s @@ -4190,7 +4194,7 @@ JS || fail "Pi did not exit cleanly before the Calm persistence restart" tmux -L "$TMUX_SOCKET" kill-session -t "$TMUX_SESSION" 2>/dev/null || true - tmux -L "$TMUX_SOCKET" new-session -d -s "$TMUX_SESSION" -x 180 -y 44 \ + tmux -L "$TMUX_SOCKET" new-session -d -s "$TMUX_SESSION" -x 180 -y 400 \ "cd '$project' && env FM_HOME='$home' PI_CODING_AGENT_DIR='$config' FM_OPERATIONAL_INPUT_SCRIPT='$OPERATIONAL_INPUT' PI_OFFLINE=1 pi --approve --no-skills --no-prompt-templates --no-context-files --session '$session_file'; rc=\$?; printf '\nPI_EXIT=%s\n' \"\$rc\"; sleep 30" wait_for_text "$restarted_snapshot" "CALM_WORKING_E2E_RESPONSE" \ || fail "Pi did not restore the persisted session after restart" From 894f4de2fcad574d0e1a30dc423a9d1a4caf338d Mon Sep 17 00:00:00 2001 From: Matt Leus Date: Mon, 5 Oct 2026 08:25:12 -0400 Subject: [PATCH 7/8] test(calm-pi): scope the export-DOM conversation boundary to visible hook rows CI Pi 1.0.3 failed the 'rendered export DOM violated the Calm conversation boundary' assertion because newer Pi exports display:false custom messages as provenance rows with class 'hook-message hook-message-hidden', kept out of the rendered conversation by the export's stock CSS unless the body carries show-hidden-messages; Pi <= 1.0.2 omits those rows entirely. The old flat includes() assertions matched the hidden provenance row's label text in the messages region and failed. Scope the assertions to the visible conversation: every hook row must carry hook-message-hidden, the body must not activate show-hidden-messages, and the [firstmate-synthetic-input] label must not survive once hidden hook rows are stripped. Genuine user/assistant entries, the five current operational user-role inputs, and tree provenance assertions are unchanged, so the Calm boundary semantics are preserved and older Pi (no hidden rows emitted) still passes. Verified headlessly against the prior worker's existing /tmp/fm-dom-repro Pi 1.0.3 rendered DOM (now passes; it previously failed exactly on the synthetic-label predicate), plus fixtures: old-Pi markup without hidden rows passes, a visible hook row fails, and an active show-hidden-messages body fails. bash -n and bin/fm-lint.sh (pinned ShellCheck 0.11.0, actionlint 1.7.12) pass. The tmux/Chrome E2E verdict belongs to GitHub's exact check. --- tests/fm-calm-pi-extension.test.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/fm-calm-pi-extension.test.sh b/tests/fm-calm-pi-extension.test.sh index 62c1daa9f46..53e286eaed5 100755 --- a/tests/fm-calm-pi-extension.test.sh +++ b/tests/fm-calm-pi-extension.test.sh @@ -3796,8 +3796,19 @@ const tree = dom.match(/]*id="tree-container"[^>]*>([\s\S]*?)]*id= if (!messages || !tree) process.exit(1); if (!/
]*>[\s\S]*Show a deterministic tool example\./.test(messages)) process.exit(1); if (!/
]*>[\s\S]*The deterministic tool example is complete\./.test(messages)) process.exit(1); -if (messages.includes('