From 1452f1310cad3cd877e4584ec226738227d4ede4 Mon Sep 17 00:00:00 2001 From: Josh Mabry Date: Fri, 25 Sep 2026 15:03:28 -0700 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20the=20Engineer=20archetype=20?= =?UTF-8?q?=E2=80=94=20a=20hands-on=20pair-programming=20navigator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle manifest (engineer + craft + friction + delegates builtins, terminal-plugin v0.4.1 and github-plugin v0.8.0 as floors), recommended config (filesystem with per-command approval and read-only git auto-approved, code pane, zed editor, onboarding under ~/code from any GitHub repo, github read-only, friction), two optional Configure inputs (a starting checkout -> terminal.cwd + managed project; github.write), the archetype block (soul_preset: engineer), the verify/bump pin-lifecycle scripts + workflow, and a README covering the navigator stance (with the research behind it), setup, the core floor, Zed Agent Panel wiring, the code pane, Allow-for-session / run_auto_approve, GitHub and friction. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/verify-bundle.yml | 114 +++++++++++++ README.md | 249 +++++++++++++++++++++++++++- protoagent.bundle.yaml | 146 ++++++++++++++++ scripts/bump_pins_pr.sh | 139 ++++++++++++++++ scripts/check_bundle_updates.py | 102 ++++++++++++ scripts/verify_bundle.py | 150 +++++++++++++++++ 6 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/verify-bundle.yml create mode 100644 protoagent.bundle.yaml create mode 100755 scripts/bump_pins_pr.sh create mode 100644 scripts/check_bundle_updates.py create mode 100644 scripts/verify_bundle.py diff --git a/.github/workflows/verify-bundle.yml b/.github/workflows/verify-bundle.yml new file mode 100644 index 0000000..e89b8c4 --- /dev/null +++ b/.github/workflows/verify-bundle.yml @@ -0,0 +1,114 @@ +# Bundle verify-and-bump loop (ADR 0049). Copy into your bundle repo's +# .github/workflows/ — under examples/ in protoAgent this file is inert. +# +# Two jobs: +# verify — installs THIS manifest's pin set into a scratch agent on a fresh +# protoAgent checkout and probes every declared console view. Runs on +# every PR (so a pin-bump must prove itself), on demand, and weekly +# (so a rotted pin set turns the schedule red instead of rotting silently). +# bump — (every trigger except pull_request: the schedule, manual dispatch, +# and the `member-released` repository_dispatch member repos send on +# release, #2960) checks each tag-pinned member for a newer +# release tag and opens/updates a PR with the bump; that PR then runs +# `verify`. But a PR opened with the repository GITHUB_TOKEN never +# auto-starts a `pull_request` run — GitHub holds it `action_required` +# until a maintainer approves it (see README.md "Pin-bump PR lifecycle", +# #2645). So this job (a) reuses ONE branch/PR per bundle repo instead of +# piling up duplicates every week, and (b) fails loudly — PR comment + +# label + a red job — if the run it just pushed doesn't show up started +# within a bounded wait, so an unapproved candidate reads as a +# maintenance failure instead of quietly rotting. + +name: verify-bundle + +on: + pull_request: + workflow_dispatch: + # Sent by a member repo's release.yml the moment it publishes a release (the + # member-side step lives in protoAgent's examples/bundles/member-release-notify.yml, + # #2960), so pins bump within minutes of a member release instead of at the + # next cron. The dispatch is a hint; the schedule below stays the backstop. + repository_dispatch: + types: [member-released] + schedule: + - cron: "17 6 * * 1" # weekly, Monday 06:17 UTC + +permissions: + contents: write + pull-requests: write + issues: write # labeling the pin-bump PR (`gh pr edit --add-label`) rides the issues API + actions: read # `gh run list`, to detect the approval-required stall below + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The core to verify against — keep in sync with the manifest's + # `verified_against:` (a release tag), or use `main` to verify ahead. + - uses: actions/checkout@v4 + with: + repository: protoLabsAI/protoAgent + ref: main + path: protoagent + + - uses: astral-sh/setup-uv@v5 + - name: Sync protoAgent deps + working-directory: protoagent + run: uv sync + + - name: Verify pin set (install + load + probe declared views) + working-directory: protoagent + run: uv run --no-sync python "$GITHUB_WORKSPACE/scripts/verify_bundle.py" "$GITHUB_WORKSPACE" + + bump: + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check tag-pinned members for newer releases + id: check + run: | + python3 scripts/check_bundle_updates.py protoagent.bundle.yaml | tee "${BUMP_PINS_SCRATCH_DIR:-/tmp}/bumps.txt" + echo "changed=$(git diff --quiet && echo false || echo true)" >> "$GITHUB_OUTPUT" + + # ONE candidate per bundle repo (#2645): a stable branch name means a later + # scheduled run force-pushes and updates the existing open PR instead of + # opening a duplicate. One manifest per bundle repo, so "per repo" here is + # just "per repo" — see README.md "Pin-bump PR lifecycle" for the full + # contract. Treat `bump-pins` as bot-owned: it is rewritten wholesale every + # run, so hand edits to it don't survive the next bump. Logic lives in + # scripts/bump_pins_pr.sh (#2669) — a real, unit-tested script rather than + # inline YAML bash, so a change to this contract has automated coverage + # instead of relying on a manual dry-run against a live repo. + - name: Open or update the pin-bump PR + if: steps.check.outputs.changed == 'true' + id: pr + env: + GH_TOKEN: ${{ github.token }} + run: bash "$GITHUB_WORKSPACE/scripts/bump_pins_pr.sh" open-or-update + + # Make the approval-required stall visible instead of silent (#2645): poll + # for the `verify` run this push should have queued. If it comes back + # `action_required` (GitHub's recursion guard for GITHUB_TOKEN-authored + # PRs) — or never shows up at all within the bounded wait, which is worse — + # comment on the PR, label it, and fail this job so an unapproved candidate + # turns the weekly schedule red instead of rotting quietly. + # + # `bump-pins` is a permanently-reused branch (one candidate per bundle repo, + # see the step above), so on a repeat run the branch name alone can + # match a STALE prior run (already-approved from last week, or a + # leftover from a since-closed PR) before GitHub has even registered a + # run for the commit just pushed — run creation is async. Filter on the + # exact commit SHA (`gh run list -c/--commit`) so the poll can only ever + # see the run for THIS push, not whatever last happened to be on top of + # the branch's run list. + - name: Flag the approval-required verify run + if: steps.check.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: > + bash "$GITHUB_WORKSPACE/scripts/bump_pins_pr.sh" flag-approval + "${{ steps.pr.outputs.number }}" "${{ steps.pr.outputs.branch }}" "${{ steps.pr.outputs.sha }}" diff --git a/README.md b/README.md index 890d59e..cad1979 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,250 @@ # engineer-archetype -The **Engineer** archetype bundle for [protoAgent](https://github.com/protoLabsAI/protoAgent). Contents land in the first PR. +The **Engineer** archetype bundle for [protoAgent](https://github.com/protoLabsAI/protoAgent): +a hands-on pair-programming **navigator** for your own machine. Point it at a repo (a git +URL or a folder) and it clones or registers it, proves the toolchain, writes a short repo +card, and gives you a guided tour. Point it at a bug and it walks you through it **one +checkpoint per turn**: it reproduces, narrows, asks for *your* hypothesis first, puts the +evidence in front of you, and lets **you** write the fix. Then it reviews your diff, runs +the checks, and you commit. + +## Navigator, not solver, and why + +Most coding agents are solvers: you describe the problem and watch the diff appear. That's +fast, and it costs you the understanding. Two recent studies measured it: + +- **Balepur et al., "(Im)Paired Programming: Coding Agents Improve Productivity but Harm + Understanding"** ([arXiv 2607.26375](https://arxiv.org/abs/2607.26375)). Coding agents + raised task speed but lowered comprehension and the ability to extend the code afterwards. + Low-effort interaction (copy-paste prompts, auto-accepted edits) went with the lowest + comprehension. +- **Anthropic, "How AI assistance impacts the formation of coding skills"** + ([anthropic.com/research](https://www.anthropic.com/research/AI-assistance-coding-skills)). + Developers using AI scored about 17% lower on comprehension. Those who asked conceptual + questions, requested explanations, or debugged themselves with AI as a sounding board kept + most of the learning; those who delegated generation and debugging did not. + +So this persona is built around the patterns that preserve understanding: + +| Rule | What it means in practice | +|---|---| +| **You author the change** | It never edits, writes, deletes, or commits unless your *latest* message says so ("apply it", "commit it"). It describes the fix in words, not replacement code, unless you ask. | +| **One checkpoint per turn** | One step (map, reproduce, narrow, locate, verify), the evidence, then a question. It doesn't chain steps on its own momentum. Repo setup is the exception: that's plumbing. | +| **Your hypothesis first** | Until you've offered one, it shows evidence, never the conclusion: no "there it is". Say "just tell me" and it will. | +| **Evidence with `path:line`** | Every claim cites a location or a command it actually ran and its real output. | +| **Point, don't paste** | It opens the exact lines in the console's code pane with `show_code` and a one-line note (a fact or a question, not the diagnosis). When you're about to type, `open_in_editor` puts your cursor on the line. | +| **GitHub is yours to post to** | It reads issues, PRs, diffs and CI runs freely, but never opens, comments on, labels, closes or merges anything, and never pushes, unless you ask in your latest message. By default it drafts the text for you to post. | +| **Teach the why** | A sentence or two on any non-obvious contract or quirk, so you can explain the bug yourself afterwards. | + +The flow (the `debug-loop` skill): orient → reproduce → narrow → *your* hypothesis → locate +→ **you** fix → it reviews and verifies → **you** commit, with a message that explains why. +It ends with a three-bullet recap (cause, fix, how it was verified) you could say out loud. + +## What's inside + +| plugin | source | role | +|---|---|---| +| `engineer` | builtin (protoAgent ≥ 0.180.0) | the navigator skills: `repo-onboard` (setup + guided tour) and `debug-loop` (seven guided steps, plus an appendix on why LLM/agent apps "stop without answering"). Off for every other agent. | +| `craft` | builtin | `/code-review`, `/grill`, `/due-diligence` and friends, rituals you type | +| `friction` | builtin | harness-friction ledger: a rough edge leaves evidence instead of vanishing | +| `delegates` | builtin | `delegate_to`, for an optional coding agent, used only when you say so | +| `terminal` | [terminal-plugin](https://github.com/protoLabsAI/terminal-plugin) | **your** terminal: a real PTY shell in a console view beside chat | +| `github` | [github-plugin](https://github.com/protoLabsAI/github-plugin) | GitHub over the `gh` CLI, **read-only by default**: issues, PRs, diffs, CI status, repo files | + +The persona is protoAgent's `engineer` soul preset (`config/soul-presets/engineer.md` in +core). The skills are core's in-tree `engineer` plugin. Both ship with protoAgent, and this +bundle names them, so there's one copy of each to keep correct. + +### The terminal is yours + +The terminal plugin gives **you** a real shell (xterm.js on a PTY) in the console, next to +the chat and the code pane. It's the driver's seat: run the app, poke at it, `git commit`. +It isn't an agent tool. The agent runs its own commands through `run_command`, which you +approve. The terminal's WebSocket is gated by the operator bearer. On Windows it needs +`pywinpty` (declared with a `sys_platform == 'win32'` marker; the install dialog offers it). +macOS and Linux use the stdlib PTY and need nothing. + +### GitHub: read by default, write when you decide + +The github plugin gives the agent **read** tools over the `gh` CLI (issues, PRs and their +merge readiness, comments, diffs, CI status, repo files and contents). That's how it gets up +to speed on a codebase's history and reads the CI failure you're debugging. It also adds a +read-only **GitHub** view (Issues/PRs tabs over your repos), a **New issue** form in the +utility bar and ⌘K, and the user-only `/issue` command, which you type. + +**Before you start:** `gh` must be installed and authenticated on the host: run +`gh auth login`, or paste a personal access token into Settings ▸ GitHub. It must also be on +the PATH the agent process sees; the desktop app passes your login-shell PATH. Without it, +the GitHub tools answer with a classified "gh missing / not authenticated" error and the +view shows a setup card; nothing else breaks. Public-repo reads at low volume work +unauthenticated. + +**Writes are off** (`github.write: false`): the write tools (create/edit/close/merge issues +and PRs, comments, labels, assignees) aren't even bound. To let the agent post *when you ask +it to*, flip the Configure step's GitHub toggle at create time, or later set: + +```yaml +github: + write: true +``` + +(Settings ▸ GitHub ▸ "Allow write tools (this agent)"). Even then, the persona only writes +on your explicit instruction; merges are `confirm`-guarded by the plugin. There's no +`default_repo` pinned: the plugin derives its repos from the projects you onboard. + +## Install + +Pick **Engineer** in the new-agent picker (Fleet ▸ New agent, or the setup wizard), or: + +``` +python -m server plugin install https://github.com/protoLabsAI/engineer-archetype +``` + +**Core floor: protoAgent ≥ 0.180.0.** Bundles can't enforce a minimum version, so here's +what an older core misses: + +- the `engineer` skill pack is missing (it ships in core); +- before 0.180.0 (#3618) the plugin dependency check ignored PEP 508 markers, so the + terminal's Windows-only `pywinpty` showed as a **missing dependency** on macOS/Linux; +- the terminal and github plugins need 0.27.0 (console views); +- `show_code` / the code pane needs 0.179.0, `run_auto_approve` 0.177.0, `open_in_editor` + 0.176.0. + +## First run + +**The Configure step asks two optional questions.** First, *a local repo to start in* (an +absolute path): it's registered as a managed project the agent can reach, and the terminal +opens there. Second, whether the *GitHub write tools* bind (off by default; see above). +Leave the repo blank and onboard from chat instead: + +> onboard github.com/owner/repo and give me the repo card + +Everything else arrives as a **recommended default** you can change in Settings. A bundle's +create-time form may not prompt for the core `filesystem` and `onboarding` sections, so these +aren't asked: + +| Setting | Default | Change it in | +|---|---|---| +| `onboarding.root` | `~/code`: clones land in `~/code/`; `register_local_project` accepts folders under it | Settings ▸ Capabilities ▸ Project onboarding | +| `onboarding.allow` | `["github.com/*"]`: any GitHub repo, private ones through your git credentials. Choosing this archetype is that consent; narrow it to `github.com/your-org/*` if you want less | same | +| `onboarding.write_default` | `true`, so "apply it" works when you ask | same | +| `filesystem.allow_run` / `run_requires_approval` | on / on: every command asks, except the list below | Settings ▸ Tools ▸ Filesystem | +| `filesystem.run_auto_approve` | read-only git: `git status`, `git diff`, `git log`, `git show`, `git branch --show-current` | same | +| `filesystem.code_pane` | `true`: binds `show_code` and the console's Code pane | same (Tools ▸ Filesystem ▸ Shell & filesystem tools) | +| `filesystem.editor_command` | `zed`. Use `code -g` for VS Code, `cursor -g` for Cursor, or empty to unbind `open_in_editor` | same | +| `github.write` | `false`: read tools only | Settings ▸ GitHub, or the Configure toggle | +| `friction.issue_repo` | empty (copy to clipboard). Point it at `protoLabsAI/protoAgent` if you file harness friction upstream, **never** at the repo you're working in | Settings ▸ Plugins (Friction) | +| `friction.escape_hatch_exempt` | `[run_command]`, the persona's main instrument, not an escape hatch | same | +| model | **unset**: uses your host's model connection | Settings ▸ Host / the agent's model picker | + +### Auto-approve and "Allow for this session" + +Tests and builds **execute the repo's code**, so they still ask by default. Once you trust a +repo, add the commands you run all the time: + +```yaml +filesystem: + run_auto_approve: + - git status + - git diff + - git log + - git show + - git branch --show-current + - npm test + - npx tsc --noEmit + - uv run pytest + - mise exec -- npm test # mise-managed repos: the agent prefixes toolchain commands +``` + +Entries are argv prefixes, matched token by token, exec'd without a shell; entries that are +too broad or contain shell metacharacters are dropped with a warning. For a one-off run of +commands, pick **Allow for this session** on the first approval (in Zed), or `/bypass` a +turn in the console. Permanent deletes always ask. + +### A coding delegate (optional) + +The navigator doesn't need one. If you want "you do it" to hand a well-specified fix to a +coding agent, register an ACP delegate in **Settings ▸ Delegates**, e.g. Claude Code via +`claude-agent-acp`. Use the **absolute** path to the binary: the agent process may not see +your shell's PATH. Set its `workdir` to your onboarding root, and name the project path in +the brief. The persona uses `delegate_to` only on your explicit instruction. + +## The code pane and `show_code` + +With `filesystem.code_pane: true` the console shows a read-only **Code** pane beside chat. +The agent calls `show_code(project, path, line, end_line, note)` to open an exact range +there, with a one-line note on why it matters, and leaves a chip in the transcript you can +click back to. The pane's **Diff** tab shows your working-tree changes, which is what the +review step reads. Click a file path in chat to open it in the pane; ⌘-click to open it in +your external editor. + +## Zed: the Agent Panel + +You can talk to this agent from Zed's Agent Panel through `protoagent-acp`, a small ACP +server that is an A2A client of the running agent. Nothing else needs installing; `uvx` +fetches it. Add to Zed's `settings.json`: + +```jsonc +{ + "agent_servers": { + "Engineer": { + "type": "custom", + // Desktop app: strip the frozen server's certificate env so the shim's HTTPS works + // (Zed launched by the agent can inherit a path that no longer exists). + "command": "/usr/bin/env", + "args": [ + "-u", "SSL_CERT_FILE", "-u", "SSL_CERT_DIR", + "/ABSOLUTE/PATH/TO/uvx", + "--from", "git+https://github.com/protoLabsAI/protoAgent@vX.Y.Z#subdirectory=integrations/zed-acp", + "protoagent-acp", + "--url", "http://127.0.0.1:", + "--token-file", "/workspaces/.fleet-token" + ] + } + } +} +``` + +- **`vX.Y.Z`**: pin to your installed protoAgent release (the shim ships in the same repo). +- **`/ABSOLUTE/PATH/TO/uvx`** (`which uvx`): Zed doesn't inherit your shell's PATH. +- **``**: the agent's port (Fleet shows it). **``**: the desktop app's data + directory. On macOS that's `~/Library/Application Support/studio.protolabs.protoagent`; + write it out in full, since `~` isn't expanded. On a server (not desktop), drop the + `/usr/bin/env -u …` wrapper and pass `--token-file` or `PROTOAGENT_TOKEN` for the + instance's bearer, if it has one. + +Then pick **Engineer** in the Agent Panel's new-thread menu. What you get: + +- **Follow the agent**: tool cards with file locations jump into your files; `show_code` + and `open_in_editor` cards are followable. +- **Allow once / Allow for this session / Deny** on each `run_command` approval. +- **Send Now** on a queued message steers the running turn. +- **Thread history** lists every chat the agent has, console chats included, and reopening + one continues the same session. +- **Continue in Zed**: press it in the console (or let the agent run `open_in_editor`), then + start a thread in Zed within 2 minutes. The new thread *is* that console chat, with its + history replayed. It never talks over a turn that's still running in the console. + +Details and limits: protoAgent's `integrations/zed-acp/README.md` and ADR 0111. + +## Friction + +`friction` records the harness's rough edges (a missing tool, a confusing error) in the +agent's ledger, and open friction shows up in its working state so it stops re-reporting +the same thing. Browse it with `/friction` or the Friction view. Leave `issue_repo` empty +unless you file harness issues upstream; the ledger is about protoAgent, not about the code +you're debugging. + +## Pin lifecycle (ADR 0049) + +The one external member (`terminal`) is pinned to a release tag, which is a **floor**: +installs take its newest *compatible* release (caret semantics; for 0.x the minor is the +boundary). `scripts/verify_bundle.py`, run by `.github/workflows/verify-bundle.yml` on +every PR and weekly, installs this manifest into a scratch agent on a fresh protoAgent +checkout, loads every member with the recommended config, and probes each declared console +view. `scripts/check_bundle_updates.py` opens a bump PR only for an out-of-range release. +Run the verify locally from a protoAgent checkout: + +``` +uv run --no-sync python /path/to/engineer-archetype/scripts/verify_bundle.py /path/to/engineer-archetype +``` diff --git a/protoagent.bundle.yaml b/protoagent.bundle.yaml new file mode 100644 index 0000000..405fefc --- /dev/null +++ b/protoagent.bundle.yaml @@ -0,0 +1,146 @@ +# A protoAgent plugin BUNDLE (ADR 0040) — the Engineer archetype: a hands-on +# pair-programming NAVIGATOR for the operator's own machine. This is the dedicated +# bundle repo protoAgent's archetype-catalog.json `engineer` row installs; the persona +# ships as protoAgent's `engineer` soul preset, and its two skills (repo-onboard, +# debug-loop) ship as protoAgent's in-tree `engineer` plugin, which this bundle turns on. +# +# Install (onto any host): python -m server plugin install +# — or pick "Engineer" in the new-agent picker. +# +# NAVIGATOR, NOT SOLVER. The operator drives and authors every change; the agent +# orients, reproduces, points at evidence with `show_code`, asks for the operator's +# hypothesis first, reviews their diff, and runs the checks. It edits or commits only +# when the operator's latest message says so. See README.md for why. +# +# CORE FLOOR — protoAgent >= 0.180.0 (bundles carry no min-version, so it is stated +# here and in the README, not enforced): +# * the in-tree `engineer` skill pack ships with the core (protoAgent PR that added +# this archetype's catalog row); on an older core that `builtin` member is missing +# and the agent boots without its skills; +# * the terminal member declares `pywinpty; sys_platform == 'win32'`, and cores before +# 0.180.0 (#3618) ignored the marker and reported that Windows-only dep MISSING on +# macOS/Linux; +# * `filesystem.code_pane` (show_code) is 0.179.0+, run_auto_approve 0.177.0+, +# open_in_editor 0.176.0+. +# +# FIRST-RUN SETUP — two optional Configure answers (core >= 0.144.0 renders +# `config_inputs:`): a local checkout to start in — written to `terminal.cwd` (the +# terminal opens there) and, because it is flagged `project: true`, registered as a +# managed project (ADR 0095) the agent's filesystem tools can reach; leave it blank and +# onboard repos from chat instead ("onboard github.com/owner/repo") — and whether the +# GitHub write tools bind (off by default). +# +# BEFORE YOU START — `gh` (GitHub CLI) on the PATH the agent sees, logged in +# (`gh auth login`) or a token in Settings ▸ GitHub. Without it the GitHub tools answer +# with a classified "gh missing / not authenticated" error and the view shows a setup +# card; nothing else breaks. +# +# Everything else is a RECOMMENDED DEFAULT below, editable in Settings afterwards. The +# create-time form can't ask for these: `filesystem.*` and `onboarding.*` are core +# sections a bundle may not prompt for (graph/plugins/installer.py +# CONFIG_INPUT_RESERVED_SECTIONS), so they arrive as defaults the operator adjusts: +# * Settings ▸ Capabilities ▸ Project onboarding — where clones land (`~/code`) and +# which sources may be cloned (any GitHub repo). +# * Settings ▸ Tools ▸ Filesystem — the editor (`zed`), the code pane, the commands +# that run without asking. +# +# MODEL — deliberately unset: the agent uses the host's model connection (Settings ▸ +# Host). A navigator turn reads a lot of code; a strong model with prompt caching is +# the comfortable choice. +# +# PIN LIFECYCLE (ADR 0049): the external member is pinned to a release TAG; on core +# >= 0.146 that pin is a FLOOR (installs take the newest COMPATIBLE tag — caret +# semantics, so for 0.x the minor is the boundary). Bump it deliberately only to adopt +# an out-of-range release; the verify workflow proves each pin set. Keep member +# entries on ONE line — scripts/check_bundle_updates.py rewrites `ref:` in place. + +id: engineer-archetype +name: Engineer +description: >- + A hands-on pair-programming navigator for the operator's own machine. Point it at a + repo (a git URL or a folder) and it clones or registers it, proves the toolchain, + writes a short repo card, and gives a guided tour; point it at a bug and it walks + you through it one checkpoint per turn — reproduce, narrow, your hypothesis first, + you write the fix, it reviews and runs the checks, you commit. It points at code in + the console's code pane, opens your editor on the line when you're about to type, + gives you a real terminal beside chat, and reads GitHub (issues, PRs, CI) to get up to + speed. It never edits, commits, or posts to GitHub unless you ask. + +# The core version this pin set was last verified against (ADR 0049) — metadata, not +# enforced. See CORE FLOOR above. +verified_against: 0.180.0 + +plugins: + - { id: engineer, builtin: true } # the navigator skills: repo-onboard + debug-loop (off for every other agent) + - { id: craft, builtin: true } # /code-review, /grill, /due-diligence — rituals the operator can type + - { id: friction, builtin: true } # harness-friction ledger — a rough edge leaves evidence + - { id: delegates, builtin: true } # delegate_to — an optional coding agent, only on the operator's say-so + - { id: terminal, url: https://github.com/protoLabsAI/terminal-plugin, ref: v0.4.1 } # the OPERATOR's shell beside chat (not an agent tool) + - { id: github, url: https://github.com/protoLabsAI/github-plugin, ref: v0.8.0 } # READ tools over `gh` (issues, PRs, diffs, CI, files); writes off by default + +enabled: [engineer, craft, friction, delegates, terminal, github] # suggested turn-on list + +# Create-time setup prompts (#2934, core >= 0.144.0) — the Configure step in the Setup +# Wizard and the New Agent panel renders these and writes each answer at its key. +config_inputs: + - { key: terminal.cwd, label: "A local repo to start in (absolute path; optional — it is registered as a project and the terminal opens there)", type: path, project: true } + - { key: github.write, label: "Let the agent write to GitHub when you ask (open/comment/label/merge issues and PRs). Off = read-only", type: boolean, default: false } + +config: # recommended defaults (you apply them) + # Hands-on coding on the operator's machine: read and run freely, with a per-command + # approval prompt for anything not on the auto-approve list. + filesystem: + enabled: true + allow_run: true + run_requires_approval: true + # Read-only git runs without asking (core >= 0.177.0). Tests and builds EXECUTE the + # repo's code, so they still ask by default — add your own (`npm test`, + # `npx tsc --noEmit`, `uv run pytest`, `mise exec -- npm test`, …) once you trust the + # repo, or pick "Allow for this session" on the first prompt. + run_auto_approve: + - git status + - git diff + - git log + - git show + - git branch --show-current + # The console code pane (core >= 0.179.0): binds `show_code`, the persona's way of + # pointing at exact lines beside chat. + code_pane: true + # `open_in_editor` (core >= 0.176.0) — the operator's editor at path:line. zed is the + # house default (and the Zed Agent Panel pairs with it, see README); `code -g` or + # `cursor -g` for VS Code / Cursor, or "" to unbind the tool. + editor_command: zed + # Project onboarding (#2555): where `onboard_project` clones and which sources it may + # clone. Opting into this archetype IS the consent for "any GitHub repo, cloned under + # ~/code"; narrow `allow` to your org (github.com/your-org/*) if you want less. + # Registered projects are read-write so "apply it" works when the operator asks. + onboarding: + enabled: true + root: ~/code + allow: ["github.com/*"] + write_default: true + # GitHub over the `gh` CLI (needs `gh` installed + authenticated on the host, or a token + # in Settings ▸ GitHub). READ-ONLY by default: issues, PRs, diffs, CI status and repo + # files — for getting up to speed and reading the CI failure you're debugging. The + # navigator never posts, pushes, labels or merges on its own; `write: true` (the + # Configure toggle above, or Settings ▸ GitHub) binds the write tools for when you ask. + # No `default_repo`: the plugin derives repos from the projects you onboard. + github: + write: false + friction: + # Friction belongs to the harness, not to the repo being worked on: leave + # issue_repo empty (copy-to-clipboard) or point it at protoLabsAI/protoAgent — never + # let it default to the operator's project. + issue_repo: "" + # run_command is this persona's main instrument, not an escape hatch (cores before + # 0.180.0 logged every call as friction). + escape_hatch_exempt: [run_command] + +# Agent archetype (ADR 0042 / 0100) — presents this bundle as the "Engineer" agent type +# in the new-agent picker. `soul_preset` gives a DIRECT `plugin install ` the +# Engineer persona even on a host whose catalog has no engineer row. +archetype: + label: Engineer + icon: Compass + soul_preset: engineer + blurb: A hands-on pair-programming navigator — onboards any repo, reproduces the bug, and points at the evidence; you write the fix, it reviews. A code pane beside chat, a real terminal, and your editor one click away. diff --git a/scripts/bump_pins_pr.sh b/scripts/bump_pins_pr.sh new file mode 100755 index 0000000..7712240 --- /dev/null +++ b/scripts/bump_pins_pr.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# The `bump` job's PR-lifecycle logic (ADR 0049, #2645), extracted out of the +# verify-bundle.yml workflow into a real, unit-testable script — see #2669's +# "commit the stubbed git/gh dry-run test harness" work item. Previously this +# logic only lived inline in YAML `run:` blocks, so verifying a change to it +# meant a manual dry-run against a real GitHub repo. tests/test_bump_pins_pr.py +# drives this file directly, with `gh` stubbed on PATH and a real throwaway git +# repo — see that file's docstring for the harness shape. +# +# Two subcommands, mirroring the workflow's two `bump`-job steps: +# open-or-update — reuse the ONE `bump-pins` branch/PR per bundle repo (dedup; +# #2645) instead of piling up a dated branch every run. +# flag-approval +# — GitHub never auto-starts a `pull_request` run for a +# PR opened with GITHUB_TOKEN (recursion-prevention); +# make that stall visible (label + comment + red job) +# instead of letting the candidate rot silently. +# +# Every output this prints as `key=value` on stdout ALSO goes to $GITHUB_OUTPUT when +# that's set (real CI) — callers (CI or a test) only ever need to read stdout. +set -euo pipefail + +# Scratch dir for the two hand-off files below — /tmp in real CI (one runner per job, no +# collision risk); overridable so parallel test processes don't share a literal /tmp path. +SCRATCH="${BUMP_PINS_SCRATCH_DIR:-/tmp}" + +emit() { + # emit KEY VALUE — stdout always; $GITHUB_OUTPUT too, when the caller set one. + echo "$1=$2" + if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "$1=$2" >>"$GITHUB_OUTPUT" + fi +} + +cmd_open_or_update() { + local branch="bump-pins" + git config user.name "bundle-bot" + git config user.email "bundle-bot@users.noreply.github.com" + git checkout -b "$branch" + git commit -am "chore: bump plugin pins to latest release tags" + git push --force origin "$branch" + emit sha "$(git rev-parse HEAD)" + + # (A heredoc nested inside a `$(...)` command substitution mis-parses any + # apostrophe in its body under bash — write straight to a file instead.) + cat >"$SCRATCH/pr_body.md" </dev/null || true) +\`\`\` + +Merging requires the verify job green — the pin only moves through a passing verification. + +This PR was opened with the repository GITHUB_TOKEN, so GitHub holds its \`verify\` run for approval (Actions tab -> **Approve and run workflows**) instead of starting it automatically — the documented explicit-approval contract, see README.md's "Pin-bump PR lifecycle" section (#2645). A later scheduled run updates THIS PR in place rather than opening a new one. +BODY + + local number + number="$(gh pr list --state open --head "$branch" --json number --jq '.[0].number // empty')" + if [ -n "$number" ]; then + echo "updating existing pin-bump PR #$number" + gh pr edit "$number" --body-file "$SCRATCH/pr_body.md" + else + gh pr create --title "chore: bump plugin pins to latest release tags" --body-file "$SCRATCH/pr_body.md" + number="$(gh pr list --state open --head "$branch" --json number --jq '.[0].number // empty')" + if [ -z "$number" ]; then + # gh pr list is eventually consistent — a create can return before a + # follow-up list sees the new PR. Fail loudly rather than emit + # number=null: flag-approval would run `gh pr edit null`/`gh pr comment + # null` on it, corrupting the approval-stall signal for a PR that DID + # get created, just not read back successfully. + echo "::error::opened a pin-bump PR for $branch but could not read its number back" >&2 + exit 1 + fi + echo "opened pin-bump PR #$number" + fi + emit number "$number" + emit branch "$branch" +} + +cmd_flag_approval() { + local number="$1" branch="$2" sha="$3" + local status="" conclusion="" run_url="" + + local workflow_file="${BUMP_PINS_WORKFLOW_FILE:-verify-bundle.yml}" + for _ in 1 2 3 4 5 6; do + run_json="$(gh run list --branch "$branch" --commit "$sha" --event pull_request \ + --workflow "$workflow_file" --limit 1 \ + --json status,conclusion,url 2>/dev/null || echo '[]')" + status="$(echo "$run_json" | jq -r '.[0].status // empty')" + conclusion="$(echo "$run_json" | jq -r '.[0].conclusion // empty')" + run_url="$(echo "$run_json" | jq -r '.[0].url // empty')" + if [ -n "$status" ]; then + break + fi + sleep "${BUMP_PINS_POLL_INTERVAL:-10}" + done + + if [ "$conclusion" = "action_required" ] || [ -z "$status" ]; then + local why + if [ -z "$status" ]; then + why="its verify run never showed up in the run list at all (even more silent than the known action_required stall)" + else + why="its verify run concluded action_required with no jobs started" + fi + local msg="Pin-bump PR #$number needs a maintainer to approve its verify run — $why. GITHUB_TOKEN-authored PRs don't run \`pull_request\` workflows automatically; click **Approve and run workflows** on the Actions tab${run_url:+ ($run_url)}. This is the documented explicit-approval step (README.md \"Pin-bump PR lifecycle\", #2645) — approve it (or close the PR) so the pin doesn't sit unverified." + echo "::error::$msg" + + gh label create "needs-approval" --color FBCA04 \ + --description "A workflow run is stuck action_required and needs a maintainer to approve it" \ + 2>/dev/null || true + gh pr edit "$number" --add-label "needs-approval" || true + + local already + already="$(gh pr view "$number" --json comments --jq \ + '[.comments[].body | select(contains("Approve and run workflows"))] | length' \ + 2>/dev/null || echo 0)" + if [ "${already:-0}" -eq 0 ]; then + gh pr comment "$number" --body "$msg" || true + fi + + exit 1 + fi + + echo "verify run started without manual approval (status=$status)" +} + +case "${1:-}" in +open-or-update) + cmd_open_or_update + ;; +flag-approval) + cmd_flag_approval "$2" "$3" "$4" + ;; +*) + echo "usage: $0 open-or-update | flag-approval " >&2 + exit 2 + ;; +esac diff --git a/scripts/check_bundle_updates.py b/scripts/check_bundle_updates.py new file mode 100644 index 0000000..1133b53 --- /dev/null +++ b/scripts/check_bundle_updates.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Pin-bump checker for a bundle manifest (ADR 0049, as amended 2026-08-31). + +A member pin is a bounded FLOOR, not an exact pin: protoAgent's installer already +ls-remotes each release-tag-pinned member and adopts the newest COMPATIBLE release +(caret semantics — the boundary is the leftmost non-zero component, so for the 0.x +versions this fleet uses, the MINOR). + +So this script deliberately does NOT chase compatible releases any more. Rewriting +`ref: v0.7.0` to `v0.7.1` produced a PR, an approval click and a merge to write down +a value the installer computes on its own — churn with no effect on what installs. + +It now reports only what actually needs a human: a member whose newest release is +OUTSIDE the compatible range. Adopting that is a real decision (it may be breaking), +so the ref is rewritten and the verify job gates the result. + + python3 scripts/check_bundle_updates.py protoagent.bundle.yaml + +Prints `bump: -> ` per out-of-range member (workflow turns the dirty +tree into a PR), and `compatible: …` for releases the installer picks up by +itself. Raw-SHA pins and `builtin:` members are left alone by design. +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +_MEMBER = re.compile(r"^\s*-\s*\{\s*id:\s*(?P[\w-]+)\s*,\s*url:\s*(?P\S+?)\s*,\s*ref:\s*(?P\S+?)\s*\}") +_SEMVER_TAG = re.compile(r"^v?\d+\.\d+\.\d+$") + + +def _semver_key(tag: str) -> tuple[int, ...]: + return tuple(int(x) for x in tag.lstrip("v").split(".")) + + +def latest_tag(url: str) -> str | None: + """Newest semver tag at ``url`` (peeled lines preferred-equivalent: tag NAMES only).""" + out = subprocess.run( + ["git", "ls-remote", "--tags", url], + check=True, + capture_output=True, + text=True, + timeout=30, + ).stdout + tags = set() + for line in out.splitlines(): + parts = line.split("\t") + if len(parts) != 2: + continue + name = parts[1].removeprefix("refs/tags/").removesuffix("^{}") + if _SEMVER_TAG.match(name): + tags.add(name) + return max(tags, key=_semver_key) if tags else None + + +def is_compatible(pinned: str, candidate: str) -> bool: + """Caret semantics — mirrors ``graph.plugins.installer.is_compatible_upgrade``. + + The boundary is the leftmost NON-ZERO component. That distinction is the whole point + for this fleet: every plugin is 0.x, where a minor bump is where breaking changes land, + so bounding on the major alone would be no bound at all. + """ + a, b = _semver_key(pinned), _semver_key(candidate) + if b < a: + return False + if a[0] != 0: + return a[0] == b[0] + if a[1] != 0: + return b[0] == 0 and a[1] == b[1] + return b[0] == 0 and b[1] == 0 + + +def main(manifest_path: str) -> int: + path = Path(manifest_path) + lines = path.read_text().splitlines(keepends=True) + changed = False + for i, line in enumerate(lines): + m = _MEMBER.match(line) + if not m or not _SEMVER_TAG.match(m["ref"]): + continue # builtin, raw-SHA pin, or not a member line — leave alone + newest = latest_tag(m["url"]) + if not newest or _semver_key(newest) <= _semver_key(m["ref"]): + continue + if is_compatible(m["ref"], newest): + # The installer adopts this by itself — bumping the manifest changes nothing. + print(f"compatible: {m['id']} {m['ref']} -> {newest} (adopted at install; no bump needed)") + continue + lines[i] = line.replace(f"ref: {m['ref']}", f"ref: {newest}") + print(f"bump: {m['id']} {m['ref']} -> {newest} [OUT OF RANGE — review before merging]") + changed = True + if changed: + path.write_text("".join(lines)) + else: + print("no out-of-range member releases — nothing to decide") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "protoagent.bundle.yaml")) diff --git a/scripts/verify_bundle.py b/scripts/verify_bundle.py new file mode 100644 index 0000000..ce691f0 --- /dev/null +++ b/scripts/verify_bundle.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Verify a bundle's pin set against a protoAgent checkout (ADR 0049). + +Installs the bundle (at its pinned refs) into a SCRATCH agent, enables every member, +loads them through the real plugin loader, and probes every declared console-view +path over HTTP — the check that catches "the pin predates the view fix" before an +operator spawns a broken archetype. + +Run from inside a protoAgent checkout with deps synced (the verify workflow does +exactly this): + + uv run --no-sync python /path/to/bundle/scripts/verify_bundle.py /path/to/bundle + +The bundle path must be a git repo (a CI checkout is); the installer clones from it. +Exit code 0 = every member installed, loaded with the bundle's recommended config, every +declared view answered 200, and every tool the archetype's capability contract names +actually registered. +""" + +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + + +def main(bundle_src: str) -> int: + # The cwd is the protoAgent checkout (`python