diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41fd0d1..b24aa9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: - name: Run pi agent-kit offline verification run: bash pi-coding-agent/scripts/verify + - name: Run first-mate skill-kit offline verification + run: bash first-mate-skill/scripts/verify + - name: Set up Python uses: actions/setup-python@v5 with: @@ -56,7 +59,7 @@ jobs: ref: a290d5d80737fc739e7ec139da0eeaf438631631 path: .patterns - - name: Validate pi agent-kit spec with the patterns validator (when present) + - name: Validate agent-kit and skill-kit specs with the patterns validator (when present) shell: bash run: | set -euo pipefail @@ -68,13 +71,18 @@ jobs: exit 0 fi work="$(mktemp -d)" - kit="$work/integrations/isolation/acq-kits/pi-coding-agent" - mkdir -p "$work/schemas" "$kit" + mkdir -p "$work/schemas" "$work/integrations/isolation/acq-kits" cp "$schema" "$work/schemas/" - cp pi-coding-agent/spec.yaml pi-coding-agent/README.md "$kit/" - cp -R pi-coding-agent/files "$kit/files" - printf 'schemaVersion: "acq-kits-registry/v1"\nkits:\n pi-coding-agent:\n backends: [sbx, msb]\n parity: CI validation entry.\n' \ + printf 'schemaVersion: "acq-kits-registry/v1"\nkits:\n' \ > "$work/integrations/isolation/acq-kits/kits.yaml" + for name in pi-coding-agent first-mate-skill; do + kit="$work/integrations/isolation/acq-kits/$name" + mkdir -p "$kit" + cp "$name/spec.yaml" "$name/README.md" "$kit/" + cp -R "$name/files" "$kit/files" + printf ' %s:\n backends: [sbx, msb]\n parity: CI validation entry.\n' "$name" \ + >> "$work/integrations/isolation/acq-kits/kits.yaml" + done python "$validator" --root "$work" --strict - name: Validate provider input with a patched credential binding diff --git a/README.md b/README.md index ea1ed88..1276430 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Agentic Coding Kits -Provider kits and agent kits for +Provider kits, agent kits, and skill kits for [`acq`](https://github.com/GSA-TTS/agentic-coding-quickstart), the sandbox wrapper used by the GSA-TTS [agentic-coding-quickstart](https://github.com/GSA-TTS/agentic-coding-quickstart) @@ -9,13 +9,16 @@ project. - A **provider kit** describes a model provider once, in a harness-neutral form: protocol, host, base URL, models URL, credential variable name, and a neutral model catalog. It names no coding agent and writes no agent configuration. +- A **skill kit** drops an Agent Skill into the guest's skills root. It installs + nothing, runs nothing, and needs no network access. - An **agent kit** installs a coding agent and renders the selected provider generation into that agent's own configuration. It owns the agent config and nothing else. The two compose at sandbox creation: a provider that speaks the provider's protocol can be consumed by any agent kit that implements the neutral → harness -mapping. +mapping. A skill kit is independent of both and composes with any +of them. This repository is separate from [agentic-coding-patterns](https://github.com/GSA-TTS/agentic-coding-patterns), @@ -59,6 +62,7 @@ An agent kit is a `hybrid/v1` mixin that: | --- | --- | --- | --- | | [`opencode-go-provider/`](opencode-go-provider/) | Provider | OpenCode Go (`openai-compatible`) facts and a 19-model neutral catalog | **Preview** — blocked on the acq credential-binding addition | | [`pi-coding-agent/`](pi-coding-agent/) | Agent | Pinned pi CLI + `~/.pi/agent/models.json` renderer | **Preview** — live verification pending acq PR 611 | +| [`first-mate-skill/`](first-mate-skill/) | Skill | The `paseo-first-mate` Agent Skill and two helper scripts, dropped into `~/.agents/skills` | Offline-verified; no live sandbox run | **Preview status.** @@ -100,6 +104,11 @@ pi-coding-agent/ agent kit scripts/verify offline gate + optional live acq/msb check tests/ node:test suite, fixtures, vendored schemas docs/decisions/ design records +first-mate-skill/ skill kit + spec.yaml hybrid/v1 mixin definition (file drops only) + files/home/.agents/skills/ the paseo-first-mate skill and its assets + scripts/verify offline gate + docs/decisions/ design records ``` New kits follow the same directory contract and get a row in the table above. diff --git a/first-mate-skill/README.md b/first-mate-skill/README.md new file mode 100644 index 0000000..ed46ffc --- /dev/null +++ b/first-mate-skill/README.md @@ -0,0 +1,105 @@ +# first-mate-skill (acq mixin kit, `hybrid/v1`) + +An [`acq`](https://github.com/GSA-TTS/agentic-coding-quickstart) **skill kit**. +It drops the `paseo-first-mate` Agent Skill into the sandbox so a coding agent +can coordinate a multi-issue engineering effort through isolated Paseo +workspaces and GitHub: delegate work, track dependent pull requests, wait on +changes cheaply, and keep a durable handoff. + +It is an opt-in `--kit` mixin. It installs no agent, writes no provider +configuration, runs no commands, publishes no ports, and allow-lists no +network hosts. + +## What it ships + +```text +~/.agents/skills/paseo-first-mate/ + SKILL.md the skill (version 1.2.0 at authorship) + assets/first-mate-check.py change detector (prints nothing when nothing changed) + assets/first-mate-wait.sh background wait loop around the detector +``` + +`~/.agents/skills` is the cross-agent skills root that the supported agents scan, +so no per-agent linking is needed. + +The payload is written at its final path by the kit's `files[]` records, which +is why the spec has no `commands`. Nothing can fail at startup beyond the file +drop itself. + +## Using the kit + +Pin the ref to a full commit SHA and select the subdirectory with `&dir=`. +Never use `#ref=main`: a branch ref lets the host fetch different code at a +different time. + +```bash +export ACQ_EXTRA_KIT_SOURCES="github.com/mogul/" + +acq run opencode /path/to/project \ + --kit "git+https://github.com/mogul/agentic-coding-kits.git#ref=&dir=first-mate-skill" +``` + +To apply it on every run, put the same ref in `ACQ_EXTRA_KITS`. Extra kits apply +after the built-ins, and acq resolves overlapping file paths last-wins; that +rule is confirmed in the sbx translator and not separately checked on msb. + +Then ask the agent to coordinate an effort ("act as first mate for these +issues"). The skill tells it to copy `assets/` into the coordinator worktree's +git-ignored `tmp/` before using the scripts. + +## What the helper scripts need + +The detector is a Python 3 script and the wait loop is bash. They call the `gh` +and `paseo` CLIs when the agent runs them. This kit installs none of `python3`, +`gh`, or `paseo` and does not allow-list `api.github.com`; kits cannot declare +dependencies yet, so those must come from the sandbox image, its other kits, and +secret bindings. A missing tool is reported rather than hidden: the detector +prints an `ERROR` line naming the missing CLI, and the shell reports a missing +`python3`. The detector only reads GitHub and Paseo state; its only writes are +the state and watch files, and the directory that holds them, `FIRST_MATE_DIR` +(default `./tmp`, relative to the current directory). + +## Updating the skill + +The skill is a **one-time snapshot** of the author's working copy, not a synced +copy; see [`docs/decisions/0001`](docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md). +To update it: + +1. Replace `SKILL.md` and the two `assets/` files under + `files/home/.agents/skills/paseo-first-mate/`. +2. Keep each file's mode in `spec.yaml` (`0644` for `SKILL.md`, `0755` for the + scripts). +3. If you add or remove a file, add or remove its `files[]` record. msb + materializes only the listed records. +4. Run `scripts/verify`. +5. Recreate the sandbox. `acq kit apply` re-applies the spec to an existing + sandbox, but a new kit commit is a new ref, so a fresh `acq run` is the + reliable path. + +## Verifying the kit + +```bash +scripts/verify +``` + +The offline gate checks that the spec and `files/` agree, the declared modes and +the scripts' executable bits, the skill's frontmatter and asset references, that +both helper scripts parse, that the detector runs against an empty state +directory, that no credential-shaped string is shipped, and `acq kit validate` +when available. `shellcheck` runs when installed. There is no live sandbox +check: the kit only drops files, so a live run would exercise acq's file drop +rather than this kit. + +## Security notes + +- **No secrets.** The skill and scripts reference credentials by variable name + only, in prose. `scripts/verify` scans the payload for token and key shapes. +- **Instructions are executable influence.** A skill steers an agent that can + run commands and push code. The skill is plain text you can read in this + directory; review it before pinning a new SHA. +- **Agent-writable.** The files land under `/home/agent`, which the agent owns. + They are a convenience payload, not a trust anchor. + +## License + +MIT — see the repository [`LICENSE`](../LICENSE). diff --git a/first-mate-skill/TROUBLESHOOTING.md b/first-mate-skill/TROUBLESHOOTING.md new file mode 100644 index 0000000..1667183 --- /dev/null +++ b/first-mate-skill/TROUBLESHOOTING.md @@ -0,0 +1,50 @@ +# Troubleshooting — first-mate-skill kit + +## The agent does not list or load `paseo-first-mate` + +- **Check the file landed.** + + ```bash + acq exec -- ls -l /home/agent/.agents/skills/paseo-first-mate + ``` + + Expect `SKILL.md` and `assets/`. If it is missing, the kit was not applied: + confirm `ACQ_EXTRA_KIT_SOURCES` includes `github.com/mogul/`, the ref is a + full SHA, and `&dir=first-mate-skill` is present. +- **Start a new session.** Agents usually discover skills at startup. + +## `first-mate-wait.sh: Permission denied` + +The scripts are declared `0755`. On sbx, every `files/` payload is first +materialized at `0644` and a combined chmod step restores the declared modes; +if that step did not run, run the script as `bash first-mate-wait.sh` or +`chmod +x` the two files. + +## `python3`, `gh`, or `paseo`: command not found + +The detector needs `python3` and shells out to `gh` and `paseo`. This kit +installs none of them. +Use an image or kit that provides them, and make sure GitHub access is bound +through acq's secret flow. The detector treats a transient API error as "keep +the previous value" and reports it, so a missing `gh` or `paseo` shows up as an +`ERROR` line on every run, which makes the wait loop return immediately each +time. A missing `python3` also makes the wait loop return at once, with the +shell's "command not found" as its output. + +## The detector prints nothing and the wait loop never returns + +That is the intended quiet path: once a baseline exists, it prints nothing when +nothing changed, and the wait loop exits 3 at its cap. The very first run prints +`BASELINE saved` (even with an empty watch list, because agents and permission +prompts are always collected), so the wait loop returns once at first start; +restart it after that. If `paseo` is missing the baseline never settles (see the +previous section), so the loop returns every time. Run the script with `--show` +to see the full current snapshot, and `list` to see what is being watched. State +and watch files live in `FIRST_MATE_DIR` (default `./tmp`), relative to where +you run the script. + +## An edit to the skill does not show up in a running sandbox + +The kit is applied from a pinned SHA. Push the change, pin the new SHA, and +create a new sandbox. Editing the files inside a running sandbox changes that +sandbox only. diff --git a/first-mate-skill/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md b/first-mate-skill/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md new file mode 100644 index 0000000..5379c89 --- /dev/null +++ b/first-mate-skill/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md @@ -0,0 +1,62 @@ +# Decision: ship the First Mate skill as a snapshot kit at its final path + +**Status:** accepted (2026-10-29) + +## Context + +The `paseo-first-mate` skill is a single `SKILL.md` plus two helper scripts +(a change detector and a wait loop). The author wants it available inside every +acq sandbox, as a personal skill, without editing the playbook kit or the image. +acq's extension point for that is an opt-in `hybrid/v1` mixin applied with +`--kit` or `ACQ_EXTRA_KITS`. + +The skill is still being developed in a separate working copy. + +## Decision + +1. **A mixin kit that only drops files.** The spec has `files[]` records and + nothing else: no `commands`, no `caps.network`, no `agent:`, no ports. The + skill needs no install step, and the scripts' network use (`gh`, `paseo`) + belongs to the kits and image that provide those CLIs. + +2. **Write the payload at its final path, `~/.agents/skills/paseo-first-mate/`.** + This is the cross-agent skills root that the supported agents scan. The + playbook kit instead extracts a tree elsewhere and symlinks entries into + several roots, with integrity and symlink guards. + That machinery protects against a tampered tarball; this kit has no + download, so it has nothing to guard and no reason to add a link step that + could fail. The playbook kit links its own skill names into the same + directory; none of them is `paseo-first-mate`, and both write as the agent + user, so the two do not collide. + +3. **A one-time snapshot, not a synced copy.** The files were copied once from + the author's working copy at skill version 1.2.0, and later edits are applied + by replacing the files by hand. Considered and rejected for now: a sync + script that copies from the working copy into the kit. A snapshot keeps this + repository self-contained and reviewable by SHA, and avoids a script that + reaches into a path that exists on one machine. If the skill starts changing + often, add the sync script then. + +4. **Declared modes.** `SKILL.md` is `0644`; the two scripts are `0755`. The + skill tells the agent to copy the scripts into a git-ignored worktree + directory before use. They stay executable so the copy keeps working + and so `bash` and direct invocation both work. + +5. **Verification is offline.** `scripts/verify` checks the spec against the + tree, modes, frontmatter, script syntax, and a credential-pattern scan, and + runs `acq kit validate` when `acq` is present. A live sandbox run would + mostly exercise acq's file drop, not anything this kit decides. + +6. **Two edits to the snapshot.** Relative to the working copy, the skill's + `owners` is `@mogul` (this repository's owner), and the dangling + `pattern-router` line in "Related Patterns" is removed because that pattern + does not exist here. + +## Consequences + +- The kit adds no egress, no startup work, and no failure modes beyond the file + drop. +- Updating the skill is a manual replace plus `scripts/verify` plus a new pinned + SHA. +- The spec's `files[]` list and the `files/` tree must be kept equal by hand; + `scripts/verify` enforces it, because msb materializes only listed records. diff --git a/first-mate-skill/files/home/.agents/skills/paseo-first-mate/SKILL.md b/first-mate-skill/files/home/.agents/skills/paseo-first-mate/SKILL.md new file mode 100644 index 0000000..d79927f --- /dev/null +++ b/first-mate-skill/files/home/.agents/skills/paseo-first-mate/SKILL.md @@ -0,0 +1,621 @@ +--- +name: paseo-first-mate +id: paseo-first-mate +version: "1.2.0" +title: "Paseo First Mate" +description: "Coordinate a multi-issue engineering effort through isolated Paseo workspaces, structured GitHub metadata, dependency-aware delivery, change-detecting follow-up, and a durable handoff." +type: skill +status: experimental +owners: + - "@mogul" +primary_personas: + - developers + - agents +requires: + anchors: [] + skills: [] +output: + format: markdown + contract: + required_sections: + - "Summary" + - "Critical Path" + - "Actions" + - "Verification" + prohibited_content: + - "Secrets" + - "Real PII" + - "Real CUI" + - "Internal URLs" +quality_gates: + readability_max_grade: 10 + citations_required: false +collection: engineering +categories: + - development + - documentation +triggers: + - "first mate" + - "coordinate agents" + - "multi-issue epic" + - "manage several pull requests" + - "delegate with Paseo" +tags: + - "paseo" + - "orchestration" + - "github" + - "delivery" +routing: + task_types: + - orchestrate + - plan + - review + input_artifacts: + - pull-request-diff + - documentation + output_artifacts: + - documentation + aliases: + - "coordinating agent" + - "delivery coordinator" + - "epic coordinator" + prefer_when: + - "Work spans several tracked issues, branches, repositories, or dependent pull requests" + - "The user wants one agent to delegate implementation while continuously managing delivery state" + avoid_when: + - "A single agent can complete one bounded change without coordination" +portability: + opencode: true + cursor: false + claude_projects: false + chatgpt: false + generic_llm: false +changelog: + - version: "1.2.0" + changes: "Add a change-detecting check and background wait loop (bundled assets) as the token-cheap default, alongside the Paseo heartbeat; add the end-the-turn rule for quiet wake-ups, standing ready-when-green grant, agent self-review and reuse rules, filing rule, host-run script hygiene, reviewer reply guidance, and new troubleshooting rows." + - version: "1.1.0" + changes: "Add delivery levels (including user-granted ready-push), adoption procedure, audit-before-implement, private handling of security findings, permission and agent-cap guidance, compact handoff with clearable log, and trims." + - version: "1.0.0" + changes: "Initial release." +--- + +# Paseo First Mate + +## Summary + +Act as the coordinating agent for a multi-issue engineering effort. Maintain the +critical path, delegate bounded issue ownership into isolated Paseo workspaces, +keep GitHub relationships authoritative, respond to CI and review events, and +preserve enough durable context that another coordinator can resume safely. + +The First Mate coordinates delivery. It does not replace code owners, approve +its own work, invent live validation results, or merge changes without explicit +authorization. + +## When to Use + +- An epic spans several issues or repositories. +- Multiple pull requests have strict merge or release dependencies. +- Implementation should be delegated while one agent manages the overall goal. +- The effort needs periodic CI/review follow-up over a long session. +- Context compaction or coordinator handoff is likely. + +## When Not to Use + +- Repository or classified-data rules prohibit the required tools. + +Single-change and read-only work are covered by `avoid_when` above. + +## Prerequisites + +- Paseo projects registered for every repository in scope. +- Authenticated GitHub CLI with the minimum required permissions. +- Repository instructions loaded from each target checkout, including its + one-command verification. +- A durable tracker containing the epic and implementation issues. +- Explicit approval before network, dependency, CI, release, merge, or other + privileged operations when policy requires it. + +## Core Invariants + +1. GitHub issue, sub-issue, development branch, PR, review, and check metadata is + authoritative. PR prose and the handoff are explanatory context, not the + relationship database. +2. One implementation owner controls one issue or one explicitly combined atomic + PR. Do not create duplicate agents on the same branch. Never open a + standalone PR for intentionally folded work. +3. Enforce the user-provided active-agent cap. Review agents count while active. + Keep one slot free for independent review when the cap allows. The user may + change the cap; record each change. +4. Same-repository linear dependencies use real Git ancestry and `gh stack`. + Cross-repository dependencies use draft guards, structured issue metadata, + and documented merge order. Never create a fictional cross-repository stack. +5. Use focused tests and blocker-only adversarial review before pushing. Let + required CI run the full suite once. Fix only observed failures. The + `ready-push` level (see Delivery Levels) is the only exception to waiting for + CI before a PR is marked ready. +6. Never present mocked, offline, or simulated evidence as live end-to-end proof. +7. Label every claim as verified, hypothesis, or unverified. Fix the live + acceptance criterion before implementation starts. +8. Never archive dirty, unpushed, unrecoverable, or intentionally retained work. +9. Never include token, key, credential, or secret values in prompts, logs, API + calls, comments, or handoff documents. +10. Hold security-relevant findings (guest-integrity, downgrade, trust-boundary) + privately and ask the user how to proceed. Do not file them as public issues. +11. Never merge, and never change PR draft/ready state, beyond what the user has + explicitly granted. + +## Delivery Levels + +State the level in every delegation prompt. The user sets it; the coordinator +never raises it. + +| Level | Agent may | Typical use | +|-------|-----------|-------------| +| `report-only` | Analyze and report | Audits, investigations | +| `uncommitted` | Edit the worktree; no commit, push, or PR | Work awaiting plan or commit approval | +| `draft-push` | Push and open a draft PR; CI runs | Default for normal work | +| `ready-push` | Push and mark the PR ready immediately, without waiting for CI | Simple or urgent changes, only when the user says so | + +Separately, the user may grant a standing **ready-when-green** rule: the +coordinator may mark a draft PR ready for review on its own initiative when all +checks pass and no further commits are expected without external review. It is a +recorded grant, never a default. It never covers merge, release, un-readying, or +a PR whose merge guard depends on other work. Record the rule, date, and scope in +the handoff. + +`ready-push` rules: + +- It is granted per named PR, never as a default. Record the PR, date, and the + user's stated reason in the handoff. +- Focused tests and static checks still run before the push. Blocker-only + adversarial review is the default; the user may waive it for an urgent fix, and + the waiver is recorded. +- CI still runs after the PR is marked ready. Watch it and report a failure + promptly; do not stay silent on that PR. +- A PR whose merge guard depends on another PR, release, or pin stays draft + unless the user explicitly overrides that. +- The grant covers draft to ready only. It never covers merge. + +## Procedure + +### 1. Adopt or Establish the Role + +When resuming an existing effort: + +1. Read the handoff. +2. Refresh authoritative remote state before reporting anything. +3. Verify that every control the handoff names actually exists on the accessible + daemon: wait loop, schedules, workspaces, agents. Record any control that is + missing or invisible; do not assume its state, resume it, or recreate it + without authorization. Reconcile workspaces against `paseo workspace ls`; + retained-workspace notes may be stale in a fresh sandbox. +4. Start the wait loop (section 8) if none is running. + +### 2. Establish the Mission + +Read the epic, all structured sub-issues, relevant open PRs, architecture +decisions, repository instructions, and current code boundaries. Report issues +that are in the epic's narrative scope but missing from its formal child list. +State one measurable milestone rather than treating the entire backlog as one +deliverable. + +Create a dependency graph: + +```text +contract or prerequisite + -> producer implementation + -> consumer implementation + -> release or pin + -> live validation + -> milestone +``` + +Classify every related issue as one of `critical-path`, `parallel`, `deferred`, +`superseded`, `completed`, or `out-of-scope`. Do not close or supersede an issue +until the replacement relationship and delivered scope are unambiguous. + +### 3. Reconcile GitHub Metadata + +For every implementation unit, propose these changes and apply them with the +user's approval: + +- Make it a real epic sub-issue where supported. +- Create the development branch through the issue relationship. +- Create the PR from that linked branch. +- Verify structured closing references before merge. +- Add retrospective closing references for older issues only when the merged PR + clearly delivered their acceptance criteria. +- Close superseded issues as not planned and name the replacement in a comment. +- Make sure each PR body mentions any folded work. + +Fully qualify tracker references in durable GitHub artifacts: + +```text +OWNER/REPOSITORY#NUMBER +``` + +Do not put tracker references in durable repository docs or code comments. In +user-facing messages, link every PR and issue with a full Markdown link. + +### 4. Audit Before Implementing Broad Issues + +For a broad or partly-stale issue, delegate a read-only audit pinned to an exact +head first. It returns a table of decision, current authority, remaining change, +and test, plus draft child issues. An audit can correct a wrong premise about +what is already done. + +1. Copy the audit report into the coordinator worktree before archiving the + auditor's workspace. +2. Propose slices ranked by size and risk. Get approval before launching, and + start with the smallest. +3. Treat security-relevant findings under invariant 10. +4. Label each finding verified, hypothesis, or unverified. + +### 5. Allocate Workspaces and Agents + +Use one Paseo workspace per issue branch (unless a single atomic PR is better), +in the project configured for its repository. + +Models: + +- Always pass the model and reasoning level explicitly. The provider default may + be expensive and the CLI may not expose profiles; confirm what a new agent + resolved to. +- Use a low-cost model for bounded tests, docs, or small fixes. Use a balanced + model at medium reasoning for feature work and for the coordinator. Reserve high + reasoning for architecture, security boundaries, complex rebases, or repeatedly + blocked work, and delegate that depth rather than raising the coordinator's cost. +- Prefer a different model family for adversarial review. + +Reuse and self-review: + +- Prefer reusing an existing research or audit agent, via `paseo send`, for the + implementation that follows. Its workspace already has the user's auto-accept + setting, so it avoids permission prompts. Keep such a workspace and its idle + agent while implementation is likely to follow; archive once no follow-up is + expected or the PR is merged or closed. `paseo send` can exceed two minutes, so + use a longer timeout or run it in the background. +- Do not launch adversarial reviewers from the coordinator. Tell the implementing + agent to launch its own read-only reviewer subagent before reporting, so the + reviewer inherits the workspace's permissions. Coordinator-launched reviewers + repeatedly blocked on permission prompts. +- An agent can go idle with uncommitted edits after its prompt is approved. Nudge + it to commit, self-review, and report. +- Identify agents and workspaces you did not create, leave them alone, do not + count them against the cap, and mention them once. + +Permissions: auto-accept is usually a per-workspace UI toggle the CLI does not +expose, so ask the user to set it after creating a workspace. A provider "build" +mode may not suppress prompts; check pending permissions and the daemon log. +Never approve a request unless the user instructs it. + +Branching from an in-flight PR: branch from its exact head, expect a rebase after +it lands, and do not mark the new work ready first. If an SSH fetch fails +host-key verification, fetch over authenticated HTTPS without altering remotes. +Never bypass the check. + +An implementation prompt states: issue, branch, and delivery level; in- and +out-of-scope behavior; base; acceptance criteria including the live criterion; +required focused tests and agent-launched self-review; the draft/ready/merge guard; the PR standard +(push levels only); and "if the result contradicts the premise, report it and +do not fix it." + +Use the completion clause that matches the delivery level: + +```text +report-only / uncommitted: + Do not return a progress-only response. Return only with the finished result and + evidence, or one exact external blocker and its evidence. Leave changes + uncommitted and prepare, but do not run, any live validation procedure. + +draft-push / ready-push: + Do not return a progress-only response. Return only after the reviewed commit is + pushed and fresh CI starts, or with one exact external blocker and its evidence. +``` + +When an agent repeatedly narrates without execution, preserve its worktree, +archive the stalled session, and replace it in the same workspace with a smaller +prompt. Do not discard or duplicate its work. + +### 6. Manage Stacks Truthfully + +Use `gh stack` only when branches are in the same repository and actual Git +ancestry represents the dependency. + +```text +main <- prerequisite <- dependent <- adoption +``` + +After changing a lower layer: + +1. Preserve uncommitted upper-layer work. +2. Rebase or restack upward. +3. Verify ancestry. +4. Restore upper-layer work. +5. Push with lease protection. + +When the base branch moves and the lower PR conflicts, prefer merging the base +into the lower branch (a plain merge, no force-push). The old head stays an +ancestor, so the stack above stays valid; only the upper PR needs a sync. Any +force-push still needs the user's approval. + +For cross-repository dependencies: + +- Keep the dependent PR draft. +- Put the dependency in a prominent merge guard. +- Mark it ready only after the upstream merge, release, and immutable pin. +- Never use `gh stack` to imply cross-repository ancestry. + +### 7. Apply the Focused Delivery Loop + +```text +focused implementation tests + -> static checks + -> blocker-only adversarial review + -> push or refresh PR (draft, or ready under ready-push) + -> required CI full suite + -> reproduce only observed failures + -> focused fix and review + -> push again +``` + +Do not run the full local suite during every implementation or review cycle. +Run it locally only when CI is unavailable or the change is so cross-cutting +that focused coverage cannot provide useful confidence. State the reason. + +Before significant pushes, ask the reviewer to inspect only the changed risk +surface and named prior blockers. Broad reviews that keep inventing unrelated +scope make delivery non-terminating. + +PR bodies use plain language and stay concise: + +```markdown +## Summary +## Issues +## Before / After +## Verification +## Merge Guard +## Rollback +## Security Impact +``` + +Keep Issues to five bullets of `OWNER/REPOSITORY#NUMBER` (including folded work), +report live checks as PASS, FAIL, or BLOCKED, and list every ready-gate in Merge +Guard. + +Keep preparatory and dependent PRs draft. Mark ready only when all merge guards +are satisfied and current-head CI/review is green, unless the user has granted +`ready-push` for that PR. + +### 8. Follow Up With a Change-Detecting Check + +The coordinator needs something to wake it. Two mechanisms are acceptable; use the +one the user prefers and record the choice and any later change in the handoff: + +- **Background wait loop (default, cheapest).** A shell loop that runs the check + on an interval and exits only when something changed. The harness notifies the + coordinator when it finishes, so waiting costs no model calls. +- **Paseo heartbeat.** A scheduled prompt that runs the check each time. Use + `paseo heartbeat update --cron ...` to change its cadence (for example, + hourly while nothing is moving, every 15 minutes once something changes). + +Either way, a quiet wake-up must not turn into filler. Empty calls (`true`, +`date > /dev/null`) cost tokens, change nothing, and can run away into hundreds +of calls. Run the check once, then end the turn. + +Bundled assets: + +- `assets/first-mate-check.py` queries the watched PRs (head, draft state, reviews, + inline comments, check results, merge state), issues, releases, other agents, + and pending permission prompts in parallel, compares them with a saved + snapshot, and prints **nothing** when nothing changed. A transient API error + keeps the previous value and is reported. Manage the watch list with `add`, + `remove`, `prune`, `repo`, and `list`; `--show` prints the snapshot without + saving, and `--reset` forgets state. State and watch files live in + `FIRST_MATE_DIR` (default `./tmp`, which must be ignored by git). +- `assets/first-mate-wait.sh INTERVAL_MIN MAX_MIN` runs the check on an interval + and exits as soon as it reports a change (exit 0, report printed) or at the cap + (exit 3, nothing printed). + +Procedure: + +1. Copy both assets next to each other into the coordinator worktree's ignored + `tmp/`, register the repositories, and add the watch list. +2. For the wait loop, start the wait script as a **background** shell command. + For a heartbeat, have its prompt run `first-mate-check.py` once. +3. Use a 5-minute interval while work is in flight (an agent running, CI pending, + or a review or live run awaited) and 15 minutes when quiet. Use a cap of about + two hours on the wait loop, and restart it after handling each notification. +4. **Quiet wake-up rule:** if the check reports nothing, make no further tool + calls and end the turn. Whether to say "no change" follows the user's current + preference; record it in the handoff. Say it only when it is true: the check + ran and reported nothing, never as a guess. +5. On a change, read the report, act only on concrete changes, update the handoff, + report meaningful changes or blockers to the user, and start the next wait. +6. Prune merged and closed items from the watch list. + +The check resolves exact-head state, so stale cached PR check fields cannot +mislead it. It cannot see GitHub notifications or review requests on PRs outside +the watch list. Under `ready-push`, a CI failure is always reported. Quiet +wake-ups never delay a direct answer: respond to the user when asked. + +### 9. Maintain the Durable Handoff + +Store the handoff in the coordinator worktree, not another repository checkout. +Use a repository-local ignored path such as `tmp/FIRST-MATE-HANDOFF.md`. If +`tmp/` is not ignored, add it to the repository-local `.git/info/exclude` rather +than changing project ignore policy solely for the coordinator. + +Template: + +```markdown +# First Mate Handoff + +Updated: YYYY-MM-DD HH:MM UTC + +## Goal +## Operating Rules +## Controls +## Current Coordination State +## Completed Work +## Critical Path +## Parallel Work +## Authoritative PR and Branch Heads +## Issue and Metadata State +## Workspaces and Active Agents +## Verification Evidence +## Live Results +## Merge and Release Order +## Immediate Next Actions +## Workspace Safety +## Fast-Track Grants +## Wait Log +``` + +- **Controls:** wait mechanism (loop or heartbeat), interval, and cap, schedule status (or "not visible"), + agent cap, coordinator and delegated models, permission setup per workspace. +- **Current Coordination State:** the first section read after each wake-up. Keep the + whole file near 200 lines. +- **Wait Log:** short dated entries only. Clear it periodically: fold + durable facts into the state sections, save a gitignored backup under + `tmp/archive/`, then empty the log. +- **Standing rules:** record every user process rule with its date: delivery + grants, ready-when-green, filing rule, self-review rule, agent reuse. +- **Prune stale detail** once superseded: old head SHAs, retention notes for + workspaces that no longer exist, and debugging narratives. + +Update it after every meaningful event before sending the user-facing report. +Do not include secrets, sensitive internal topology, or credentials. + +### 10. Retire Workspaces Safely + +Archive a workspace only when all conditions are true: + +- Worktree is clean, or its valuable artifacts are already copied out and + byte-compared. +- Required commits are pushed or otherwise durably recoverable. +- No agent is running in it, and no dependent work or PR awaiting feedback is + retained there. +- Archival is authorized: the coordinator confirms it, or the user has granted + standing permission. Record any standing permission in the handoff. +- PR checks are green or the workspace is definitively obsolete. + +Dirty files, untracked files, staged changes, unpushed commits, unavailable +owners, or named retention are hard stops. + +### 11. Close the Loop + +After upstream merges: + +- Rebase or restack dependent branches. +- Resolve immutable release SHAs. +- Update every dependent pin and assertion. +- Re-run focused host validation. +- Execute real live validation at the defined cadence. +- Record `PASS`, `FAIL`, or `BLOCKED`, with transcript paths. +- File real, actionable, untracked defects immediately, except security findings + (invariant 10). Do not file an issue for work you are about to finish in an + existing open PR; file only deferred or out-of-scope work. +- Close delivered issues through structured metadata. +- Update the epic status and handoff. +- Surface work that the merge unblocked. + +Do not declare the milestone complete until production wiring, downstream +consumers, current-head CI, review, and live evidence agree. + +## Working With the User and Reviewers + +- **Check existing evidence before asking the user to repeat work.** Search the + PR, its docs, and results folders. If earlier runs do not cover the new check, + say exactly what was logged, what it did not cover, and why the new run is + needed. Results must be tied to the tested revision. +- **Host-run scripts.** When the user must run something on their machine, give + one line starting with a `cd` to the right worktree, or a script plus a one-line + invocation. Scripts must: + - use `timeout --foreground`; plain `timeout` backgrounds the child's process + group, and job control freezes it (state `T`) when it touches the terminal; + - show live output (`tee`) and set non-interactive flags for the tool under test; + - give resources names valid for the tool (for example, lowercase); + - have a per-step timeout and an interrupt trap that cleans up; + - print PASS, FAIL, or BLOCKED with the tested revision. + + State that the script was only syntax-checked if it was not run live. +- **Diagnose a stalled run before blaming the code.** Check process state and the + last log write time. Separate "my script's fault" from "possible regression". +- **Reviewer feedback.** A `CHANGES_REQUESTED` review stays on the PR until the + reviewer re-reviews, so a push does not clear it. Reproduce a reported bug + before fixing it, add a regression test, and confirm the old form fails it. + Reply on the PR after the push lands, citing commits. Do not invent authority a + reviewer implies. A proposed decision record has no authority until accepted, + so statements about it are conditional. +- **Release PRs created by the workflow token may not trigger required checks.** + Raise it early; the exemption or manual trigger is the user's decision. +- **State only what you have verified.** If you have not looked since the last + report, say so. Correct a mistaken claim as soon as you notice it. +- **Lead reports with what changed and what needs a decision.** List decisions + once under "What I need from you". + +## Critical Path + +Report a compact dependency view. Arrows mean prerequisite to dependent, not Git +ancestry: + +```text +completed prerequisite -> active PR -> review or release gate -> dependent PR + -> live validation -> milestone +``` + +Status labels: `COMPLETE`, `ACTIVE`, `WAITING`, `BLOCKED`, `FAILED`. When the +user asks for a map, use a mermaid flowchart with an emoji legend (for example +✅ merged, 🟢 approved and green, 🟡 awaiting review, 🔄 in progress, 🔵 not +started, ⛔ blocked, 🔒 needs a user decision, 📝 draft). Solid arrows mean "must +land before"; dotted arrows mean "informs". Link every issue and PR. + +## Actions + +Name only actions performed or newly actionable state. Avoid repetitive "no +change" reports. Example: "Upstream merged; restacked the dependent PR and +refreshed CI." + +## Verification + +Before declaring coordination complete, verify: + +- All critical-path issues have truthful structured state. +- Every active PR has a current-head CI and review status. +- Draft guards match actual dependencies. Every ready PR either has green + current-head CI or a recorded `ready-push` grant. +- Same-repository stacks match Git ancestry. +- Cross-repository order is documented without fake stacking. +- Deferred work is tracked. +- Current release pins are immutable and verified. +- Live results are observed, not simulated. +- Required workspaces are retained; safe obsolete workspaces are archived. +- The handoff reflects the final state and its log is cleared. +- The wait loop or heartbeat is stopped and its state file cleared when coordination ends. + +## Troubleshooting + +| Problem | Response | +|---------|----------| +| A control named in the handoff (schedule, wait loop) is not visible | Record the gap, do not recreate it silently, and ask the user. | +| New agent resolved to an unexpected or expensive model | Stop and archive it, then relaunch in the same workspace with an explicit model and reasoning level. | +| Agent prompts for permission despite "build" mode | Check pending permissions and the daemon log; ask the user to set the workspace toggle. Do not approve it yourself. | +| SSH fetch fails host-key verification | Fetch needed refs over authenticated HTTPS without changing remotes. | +| Audit contradicts the issue's premise | Update the handoff and issue plan with the corrected scope before delegating. | +| Branches diverge across worktrees | Compare commits and content; replay only missing reviewed commits with lease protection. | +| Dependent PR conflicts after upstream merge | Preserve work, restack on current base, rerun focused checks, and refresh draft CI. | +| Disk cleanup threatens blocked work | Retain dirty or dependency-blocked workspaces and record them in the handoff. | +| A quiet turn fills with empty tool calls | Stop. Run the check once and end the turn (section 8). | +| Host-run script stalls with processes in state `T` | Job control froze the child. Use `timeout --foreground` and show live output. | +| Reviewer's change request remains after the fix | Reply with the commits and wait for re-review; a push does not clear it. | +| Release PR has no checks | Workflow-token PRs may not trigger them. Ask the user: exempt or trigger manually. | +| Wait script reports an API error | One transient error is noise (the previous value is kept); repeated errors are a blocker. | +| Coordinator context is compacted | Read the handoff, GitHub metadata, and the active Paseo inventory before acting. | + +## Related Patterns + +- Repository code-review procedures for final human review and PR quality. +- Paseo product documentation for workspace, agent, and schedule + operations. diff --git a/first-mate-skill/files/home/.agents/skills/paseo-first-mate/assets/first-mate-check.py b/first-mate-skill/files/home/.agents/skills/paseo-first-mate/assets/first-mate-check.py new file mode 100755 index 0000000..15f38f2 --- /dev/null +++ b/first-mate-skill/files/home/.agents/skills/paseo-first-mate/assets/first-mate-check.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +"""First Mate change detector. + +Prints NOTHING when nothing meaningful changed since the last run, so a +wait loop that finds no change costs no model tokens. Prints one block per +change otherwise. Read-only against GitHub and Paseo; the only writes are the +local state and watch files in FIRST_MATE_DIR (default: ./tmp, which should be +ignored by git). + +What to watch lives in first-mate-watch.json, not in this script: + {"repos": {"qs": "OWNER/repo", ...}, + "prs": ["qs#611", ...], "issues": ["qs#503", ...], + "releases": ["qs", ...], "ignore_agents": ["First Mate"]} + +Usage: + first-mate-check.py compare to saved state, print changes, save + first-mate-check.py --show print the full current snapshot, save nothing + first-mate-check.py --reset forget saved state (next run is a baseline) + first-mate-check.py list print the watch list + first-mate-check.py add pr qs#637 start watching (also: issue; also OWNER/repo#N) + first-mate-check.py remove pr qs#611 stop watching and forget its state + first-mate-check.py repo qs OWNER/repo register a repo alias + first-mate-check.py prune stop watching merged/closed PRs and closed issues +""" +import concurrent.futures as cf +import json +import os +import re +import subprocess +import sys + +HERE = os.path.abspath(os.environ.get("FIRST_MATE_DIR", "tmp")) +os.makedirs(HERE, exist_ok=True) +STATE = os.path.join(HERE, ".first-mate-state.json") +WATCH = os.path.join(HERE, "first-mate-watch.json") +TIMEOUT = 45 + +DEFAULT_WATCH = { + "repos": {}, + "prs": [], + "issues": [], + "releases": [], + "ignore_agents": ["First Mate"], +} + +BAD_CONCLUSIONS = {"FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"} +BAD_STATES = {"FAILURE", "ERROR"} +REF = re.compile(r"^(?:(?P[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_-]+)#)(?P\d+)$") + + +def load_watch(): + if not os.path.exists(WATCH): + save_watch(DEFAULT_WATCH) + return json.loads(json.dumps(DEFAULT_WATCH)) + with open(WATCH) as fh: + w = json.load(fh) + for k, v in DEFAULT_WATCH.items(): + w.setdefault(k, type(v)()) + return w + + +def save_watch(w): + with open(WATCH, "w") as fh: + json.dump(w, fh, indent=1) + fh.write("\n") + + +def resolve(w, ref): + m = REF.match(ref) + if not m: + raise SystemExit(f"bad reference '{ref}' (want alias#N or OWNER/repo#N)") + alias = m.group("alias") + repo = w["repos"].get(alias, alias if "/" in alias else None) + if not repo: + raise SystemExit(f"unknown repo alias '{alias}'; register it: repo {alias} OWNER/repo") + return repo, int(m.group("num")) + + +def short(w, repo): + for a, r in w["repos"].items(): + if r == repo: + return a + return repo.split("/")[-1] + + +def run(cmd): + out = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT) + if out.returncode != 0: + raise RuntimeError((out.stderr or out.stdout).strip().splitlines()[-1][:160]) + return out.stdout + + +def gh_json(args): + return json.loads(run(["gh"] + args)) + + +def pr_snapshot(repo, num): + d = gh_json([ + "pr", "view", str(num), "--repo", repo, "--json", + "headRefOid,isDraft,state,reviewDecision,mergeStateStatus,reviews,comments,statusCheckRollup", + ]) + inline = sum(int(x) for x in run([ + "gh", "api", f"repos/{repo}/pulls/{num}/comments?per_page=100", "--paginate", + "--jq", "length"]).split()) + ok = bad = pend = 0 + bad_names = [] + for c in d.get("statusCheckRollup") or []: + concl, state, status = c.get("conclusion"), c.get("state"), c.get("status") + name = c.get("name") or c.get("context") or "?" + if concl in BAD_CONCLUSIONS or state in BAD_STATES: + bad += 1 + bad_names.append(name) + elif status not in (None, "COMPLETED") or state == "PENDING": + pend += 1 + else: + ok += 1 + reviews = sorted( + f"{r['author']['login']}:{r['state']}@{(r.get('commit') or {}).get('oid', '')[:8]}" + for r in d.get("reviews") or []) + return ( + f"head={d['headRefOid'][:8]} {'draft' if d['isDraft'] else 'ready'} {d['state']} " + f"decision={d.get('reviewDecision') or '-'} merge={d.get('mergeStateStatus')} " + f"checks ok={ok} fail={bad} pending={pend}" + + (f" failing={sorted(bad_names)}" if bad_names else "") + + f" reviews={reviews} comments={len(d.get('comments') or [])} inline={inline}") + + +def issue_snapshot(repo, num): + d = gh_json(["issue", "view", str(num), "--repo", repo, "--json", "state,comments"]) + return f"{d['state']} comments={len(d.get('comments') or [])}" + + +def release_snapshot(repo): + return gh_json(["release", "view", "--repo", repo, "--json", "tagName"])["tagName"] + + +def agents_snapshot(ignore): + rows = json.loads(run(["paseo", "ls", "--json"])) + items = sorted( + f"{r['shortId']}:{r['status']}:{r.get('name', '')[:36]}" + for r in rows if r.get("name") not in ignore) + return "; ".join(items) or "none" + + +def permits_snapshot(): + rows = json.loads(run(["paseo", "permit", "ls", "--json"])) + return "; ".join(sorted(f"{r['agentShortId']}:{r['id']}" for r in rows)) or "none" + + +def collect(w): + jobs = {} + for ref in w["prs"]: + repo, n = resolve(w, ref) + jobs[f"pr {short(w, repo)}#{n}"] = (pr_snapshot, repo, n) + for ref in w["issues"]: + repo, n = resolve(w, ref) + jobs[f"issue {short(w, repo)}#{n}"] = (issue_snapshot, repo, n) + for alias in w["releases"]: + repo = w["repos"].get(alias, alias) + jobs[f"release {short(w, repo)}"] = (release_snapshot, repo) + jobs["agents"] = (agents_snapshot, set(w["ignore_agents"])) + jobs["permission prompts"] = (permits_snapshot,) + results, errors = {}, {} + with cf.ThreadPoolExecutor(max_workers=8) as ex: + futs = {ex.submit(*j): k for k, j in jobs.items()} + for f in cf.as_completed(futs): + k = futs[f] + try: + results[k] = f.result() + except Exception as e: # noqa: BLE001 - report, keep old value + errors[k] = str(e) or e.__class__.__name__ + return results, errors + + +def load_state(): + if os.path.exists(STATE): + with open(STATE) as fh: + return json.load(fh) + return {} + + +def save_state(s): + with open(STATE, "w") as fh: + json.dump(s, fh, indent=1, sort_keys=True) + + +def manage(w, argv): + cmd = argv[0] + if cmd == "list": + print(json.dumps(w, indent=1)) + elif cmd in ("add", "remove") and len(argv) == 3 and argv[1] in ("pr", "issue"): + key = "prs" if argv[1] == "pr" else "issues" + repo, n = resolve(w, argv[2]) + alias = short(w, repo) + item = f"{alias}#{n}" + if cmd == "add": + if item not in w[key]: + w[key].append(item) + save_watch(w) + print(f"watching {argv[1]} {item}") + else: + w[key] = [x for x in w[key] if x != item] + save_watch(w) + st = load_state() + st.pop(f"{argv[1]} {item}", None) + save_state(st) + print(f"stopped watching {argv[1]} {item}") + elif cmd == "repo" and len(argv) == 3: + w["repos"][argv[1]] = argv[2] + save_watch(w) + print(f"alias {argv[1]} -> {argv[2]}") + elif cmd == "prune": + st = load_state() + dropped = [] + for kind, key in (("pr", "prs"), ("issue", "issues")): + keep = [] + for item in w[key]: + snap = st.get(f"{kind} {item}", "") + done = (" MERGED " in f" {snap} " or " CLOSED " in f" {snap} ") if kind == "pr" \ + else snap.startswith("CLOSED") + if done: + dropped.append(f"{kind} {item}") + st.pop(f"{kind} {item}", None) + else: + keep.append(item) + w[key] = keep + save_watch(w) + save_state(st) + print("pruned: " + (", ".join(dropped) or "nothing")) + else: + raise SystemExit(__doc__) + + +def main(): + argv = sys.argv[1:] + if argv and not argv[0].startswith("--"): + manage(load_watch(), argv) + return + arg = argv[0] if argv else "" + if arg == "--reset": + if os.path.exists(STATE): + os.remove(STATE) + print("state reset") + return + w = load_watch() + new, errors = collect(w) + if arg == "--show": + for k in sorted(new): + print(f"{k}: {new[k]}") + for k in sorted(errors): + print(f"ERROR {k}: {errors[k]}") + return + old = load_state() + lines = [] + if not old: + lines.append(f"BASELINE saved ({len(new)} items)") + else: + for k in sorted(new): + if k not in old: + lines.append(f"NEW {k}: {new[k]}") + elif old[k] != new[k]: + lines.append(f"CHANGED {k}\n was: {old[k]}\n now: {new[k]}") + for k in sorted(errors): + lines.append(f"ERROR {k}: {errors[k]} (kept previous value)") + save_state({**old, **new}) + if lines: + print("\n".join(lines)) + + +if __name__ == "__main__": + main() diff --git a/first-mate-skill/files/home/.agents/skills/paseo-first-mate/assets/first-mate-wait.sh b/first-mate-skill/files/home/.agents/skills/paseo-first-mate/assets/first-mate-wait.sh new file mode 100755 index 0000000..8f15388 --- /dev/null +++ b/first-mate-skill/files/home/.agents/skills/paseo-first-mate/assets/first-mate-wait.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# First Mate wait loop. Runs first-mate-check.py (same directory) every INTERVAL minutes and +# returns as soon as it reports a change, so waiting costs no model tokens. +# +# Usage: first-mate-wait.sh INTERVAL_MIN MAX_MIN +# exit 0 -> change found; the change report is printed (state already saved) +# exit 3 -> nothing changed within MAX_MIN (print nothing) +# Run it as a BACKGROUND shell command so the harness notifies on completion. +# Use INTERVAL 5 while work is actively in flight, 15 when it is quiet. +set -uo pipefail +INTERVAL=${1:-15} +MAX=${2:-60} +HERE=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +# State and watch files live in FIRST_MATE_DIR (default ./tmp); see first-mate-check.py. +deadline=$(( $(date +%s) + MAX * 60 )) +while :; do + out=$(python3 "$HERE/first-mate-check.py" 2>&1) + if [ -n "$out" ]; then + printf '%s\n' "$out" + exit 0 + fi + [ "$(date +%s)" -ge "$deadline" ] && exit 3 + sleep $(( INTERVAL * 60 )) +done diff --git a/first-mate-skill/scripts/verify b/first-mate-skill/scripts/verify new file mode 100755 index 0000000..7f4e8bc --- /dev/null +++ b/first-mate-skill/scripts/verify @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# +# verify — host-side offline gate for the first-mate-skill acq mixin kit. +# +# Checks that the spec and the payload under files/ agree, that the skill is +# well-formed, that the helper scripts parse, and that nothing credential-shaped +# is shipped. If `acq kit validate` is available, the neutral hybrid/v1 spec is +# validated too. There is no live sandbox check: the kit only drops files. +# +# Usage (from anywhere): path/to/first-mate-skill/scripts/verify + +set -uo pipefail + +KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SPEC="$KIT_DIR/spec.yaml" +SKILL_NAME="paseo-first-mate" +SKILL_DIR="$KIT_DIR/files/home/.agents/skills/$SKILL_NAME" +SKILL_MD="$SKILL_DIR/SKILL.md" +CHECK_PY="$SKILL_DIR/assets/first-mate-check.py" +WAIT_SH="$SKILL_DIR/assets/first-mate-wait.sh" +PYTHON_BIN="${PYTHON_BIN:-python3}" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/first-mate-skill-verify.XXXXXX")" || { echo "mktemp failed" >&2; exit 1; } + +pass=0 +fail=0 +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass+1)); } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); } +skip() { printf ' \033[33mSKIP\033[0m %s\n' "$1"; } +info() { printf '\n\033[1m%s\033[0m\n' "$1"; } + +trap 'rm -rf "$WORK" >/dev/null 2>&1 || true' EXIT INT TERM + +info "0. Preconditions" +for f in "$SPEC" "$SKILL_MD" "$CHECK_PY" "$WAIT_SH"; do + if [ -f "$f" ]; then + ok "present: ${f#"$KIT_DIR"/}" + else + bad "missing: ${f#"$KIT_DIR"/}" + fi +done +if [ "$fail" -ne 0 ]; then + printf '\n %d passed, %d failed\n' "$pass" "$fail" + exit 1 +fi + +info "1. Spec and payload agree" +# Every files[].source must exist, and every file under files/ must be listed: +# msb materializes only the listed records, so an unlisted file is silently +# missing in one backend and present in the other. +listed="$WORK/listed.txt" +actual="$WORK/actual.txt" +awk '/^files:/ {f=1; next} /^[A-Za-z]/ {f=0} f && /^[[:space:]]+source:[[:space:]]/ {sub(/^[[:space:]]+source:[[:space:]]+/, ""); print}' "$SPEC" | LC_ALL=C sort > "$listed" +(cd "$KIT_DIR" && find files -type f | LC_ALL=C sort) > "$actual" +if diff -u "$listed" "$actual" >"$WORK/diff.log" 2>&1; then + ok "files[].source entries match every file under files/ ($(wc -l < "$listed" | tr -d ' ') files)" +else + bad "spec files[] and files/ tree differ (- listed, + on disk)" + sed 's/^/ | /' "$WORK/diff.log" >&2 +fi +while IFS= read -r src; do + [ -f "$KIT_DIR/$src" ] || bad "source does not exist: $src" +done < "$listed" + +info "2. Declared modes are right and the scripts are executable on disk" +for pair in "SKILL.md:0644" "assets/first-mate-check.py:0755" "assets/first-mate-wait.sh:0755"; do + rel="${pair%%:*}" + want="${pair##*:}" + guest="/home/agent/.agents/skills/$SKILL_NAME/$rel" + got="$(awk -v p="$guest" '$0 == " - path: " p {f=1; next} f && /mode:/ {gsub(/"/,"",$2); print $2; exit}' "$SPEC")" + if [ "$got" = "$want" ]; then + ok "$rel mode $want" + else + bad "$rel: spec mode '${got:-}', expected $want" + fi +done +for script in "$CHECK_PY" "$WAIT_SH"; do + if [ -x "$script" ]; then + ok "${script#"$KIT_DIR"/} is executable on disk" + else + bad "${script#"$KIT_DIR"/} lost its executable bit" + fi +done + +info "3. Skill frontmatter" +fm_name="$(awk 'NR==1 && $0=="---" {f=1; next} f && $0=="---" {exit} f && /^name:/ {sub(/^name:[[:space:]]*/,""); gsub(/"/,""); print; exit}' "$SKILL_MD")" +if [ "$fm_name" = "$SKILL_NAME" ]; then + ok "SKILL.md name matches its directory ($SKILL_NAME)" +else + bad "SKILL.md name '${fm_name:-}' does not match directory '$SKILL_NAME'" +fi +if [ "$(head -n 1 "$SKILL_MD")" = "---" ] && awk 'NR>1 && $0=="---" {found=1} END {exit !found}' "$SKILL_MD"; then + ok "SKILL.md has a closed frontmatter block" +else + bad "SKILL.md frontmatter is not delimited by ---" +fi +for asset in first-mate-check.py first-mate-wait.sh; do + if grep -q "assets/$asset" "$SKILL_MD"; then + ok "SKILL.md references assets/$asset" + else + bad "SKILL.md does not reference assets/$asset" + fi +done + +info "4. Helper scripts parse" +if command -v "$PYTHON_BIN" >/dev/null 2>&1; then + if "$PYTHON_BIN" -c 'import ast,sys; ast.parse(open(sys.argv[1], encoding="utf-8").read(), sys.argv[1])' "$CHECK_PY" 2>"$WORK/py.log"; then + ok "first-mate-check.py parses as Python 3" + else + bad "first-mate-check.py has a syntax error" + sed 's/^/ | /' "$WORK/py.log" >&2 + fi +else + skip "$PYTHON_BIN not installed" +fi +if bash -n "$WAIT_SH" 2>"$WORK/sh.log"; then + ok "first-mate-wait.sh parses as bash" +else + bad "first-mate-wait.sh has a syntax error" + sed 's/^/ | /' "$WORK/sh.log" >&2 +fi +if command -v shellcheck >/dev/null 2>&1; then + if shellcheck --severity=warning "$WAIT_SH" "$0" >"$WORK/shellcheck.log" 2>&1; then + ok "shellcheck --severity=warning passes" + else + bad "shellcheck --severity=warning failed" + sed 's/^/ | /' "$WORK/shellcheck.log" >&2 + fi +else + skip "shellcheck not installed (install it to run this gate)" +fi + +info "5. Shipped detector has no baked-in watch targets" +# `list` prints the watch list without calling GitHub or Paseo. It must come +# back with no PRs, issues, or releases, so nothing is hard-coded to a repo. +if command -v "$PYTHON_BIN" >/dev/null 2>&1; then + mkdir -p "$WORK/state" + if FIRST_MATE_DIR="$WORK/state" "$PYTHON_BIN" "$CHECK_PY" list >"$WORK/list.out" 2>"$WORK/list.err" \ + && "$PYTHON_BIN" -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if not (d.get("repos") or d.get("prs") or d.get("issues") or d.get("releases")) else 1)' "$WORK/list.out"; then + ok "first-mate-check.py ships with an empty watch list" + else + bad "first-mate-check.py list failed or returned a non-empty watch list" + sed 's/^/ | /' "$WORK/list.err" >&2 + fi +else + skip "$PYTHON_BIN not installed" +fi + +info "6. No credential-shaped content" +# Names of variables may appear in prose; values must not. +scan_rc=0 +grep -rEn \ + -e 'gh[pousr]_[A-Za-z0-9]{20,}' \ + -e 'github_pat_[A-Za-z0-9_]{20,}' \ + -e 'sk-[A-Za-z0-9]{20,}' \ + -e 'AKIA[0-9A-Z]{16}' \ + -e 'BEGIN [A-Z ]*PRIVATE KEY' \ + "$KIT_DIR" --exclude=verify >"$WORK/secrets.log" 2>&1 || scan_rc=$? +case "$scan_rc" in + 1) ok "no token, key, or private-key patterns in the kit (payload and docs)" ;; + 0) bad "credential-shaped string found"; sed 's/^/ | /' "$WORK/secrets.log" >&2 ;; + *) bad "credential scan could not run (grep exit $scan_rc)"; sed 's/^/ | /' "$WORK/secrets.log" >&2 ;; +esac + +info "7. acq neutral-spec validation (optional)" +if command -v acq >/dev/null 2>&1; then + if acq kit validate "$KIT_DIR" >"$WORK/kit-validate.log" 2>&1; then + ok "acq kit validate accepts the hybrid/v1 spec" + else + bad "acq kit validate rejected the spec" + sed 's/^/ | /' "$WORK/kit-validate.log" >&2 + fi +else + skip "acq kit validate not available" +fi + +info "Summary" +printf ' %d passed, %d failed\n' "$pass" "$fail" +if [ "$fail" -eq 0 ]; then + printf ' \033[32mOffline gate passed.\033[0m\n' + exit 0 +fi +printf ' \033[31mOffline gate failed — see above.\033[0m\n' +exit 1 diff --git a/first-mate-skill/spec.yaml b/first-mate-skill/spec.yaml new file mode 100644 index 0000000..5b10a8f --- /dev/null +++ b/first-mate-skill/spec.yaml @@ -0,0 +1,53 @@ +# spec.yaml — first-mate-skill (a neutral acq mixin kit, hybrid/v1) +# +# SKILL KIT: drops the paseo-first-mate Agent Skill (SKILL.md plus two bundled +# helper scripts) into the cross-agent skills root, ~/.agents/skills, which +# OpenCode, Codex, Docker Agent, and Copilot all scan. It carries no agent, no +# provider, and no configuration. +# +# NO COMMANDS: the payload is dropped at its final path, so there is nothing to +# link, install, or render. Nothing here can fail a sandbox create beyond the +# file drop itself. +# +# NO EGRESS: the skill is instructions plus two local scripts. The scripts call +# the `gh` and `paseo` CLIs when the agent runs them, and those CLIs' network +# needs belong to the sandbox's other kits, not to this one. +# +# NO `agent:` BLOCK and NO published ports: this is an opt-in --kit mixin. +# +# The skill content is a one-time snapshot of the author's working copy; see +# docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md for how to +# update it. +schemaVersion: "hybrid/v1" +kind: mixin +name: first-mate-skill +displayName: Paseo First Mate skill +description: > + Drops the paseo-first-mate Agent Skill and its two helper scripts into + ~/.agents/skills so a coding agent in the sandbox can coordinate a + multi-issue effort through Paseo workspaces and GitHub. Opt-in mixin; no + egress, commands, or ports. + +files: + - path: /home/agent/.agents/skills/paseo-first-mate/SKILL.md + mode: "0644" + source: files/home/.agents/skills/paseo-first-mate/SKILL.md + description: The paseo-first-mate skill instructions + - path: /home/agent/.agents/skills/paseo-first-mate/assets/first-mate-check.py + mode: "0755" + source: files/home/.agents/skills/paseo-first-mate/assets/first-mate-check.py + description: Change detector for watched PRs, issues, releases, agents, and permissions + - path: /home/agent/.agents/skills/paseo-first-mate/assets/first-mate-wait.sh + mode: "0755" + source: files/home/.agents/skills/paseo-first-mate/assets/first-mate-wait.sh + description: Background wait loop that runs the change detector until it reports a change + +agentContext: | + ## Paseo First Mate skill + + The `paseo-first-mate` skill is installed at + `~/.agents/skills/paseo-first-mate/`. Load it when the user asks you to + coordinate several agents, issues, or dependent pull requests. Its helper + scripts are in the skill's `assets/` directory; copy them into the + coordinator worktree's git-ignored `tmp/` as the skill describes rather than + running them in place.