Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion first-mate-skill/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ network hosts.

```text
~/.agents/skills/paseo-first-mate/
SKILL.md the skill (version 1.3.0)
SKILL.md the skill (version 1.4.0)
assets/first-mate-check.py change detector (prints nothing when nothing changed)
assets/first-mate-wait.sh background wait loop around the detector; one loop per
FIRST_MATE_DIR, owned by the starting agent (flock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ The snapshot moved to skill version 1.3.0. `first-mate-wait.sh` now holds an
(run from `scripts/verify`) covers it offline with a stub detector. This changes
the shipped script, not the kit's shape: still files only, no commands, no
egress. The script now also requires `flock` from util-linux on the sandbox image.

## Update 2026-11-03: skill 1.4.0

The snapshot moved to skill version 1.4.0. `first-mate-check.py` now scopes its
agent watch by the caller's `paseo.parent-agent-id` label instead of the agent's
name, and agent/permission watching is opt-in and off by default (`agents on` /
`permits on`). The kit's shape is unchanged: still files only, no commands, no
egress.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: paseo-first-mate
id: paseo-first-mate
version: "1.3.0"
version: "1.4.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
Expand Down Expand Up @@ -71,6 +71,8 @@ portability:
chatgpt: false
generic_llm: false
changelog:
- version: "1.4.0"
changes: "Agent watching reworked: scoped by `paseo.parent-agent-id` label instead of agent name, and agent/permission watching is now opt-in and off by default (`agents on` / `permits on`). The harness already sends finish notices and surfaces pending permits, so watching them by default made the loop wake on the coordinator's own create/archive/finish actions."
- version: "1.3.0"
changes: "One coordinator and one wait loop per effort (owner-keyed flock in the wait script with status, stop, and take-over; forks and resumed sessions are non-coordinators; never report a loop as running without a status result from this turn). Detector watches branches and workflows. Fresh-base rule, verified agent claims, and reading finish notices against agent status. Runtime acceptance signal, mutation proof, and post-merge proof. Repository PR standards override the template. Pending Decisions and Held Items handoff section, recorded permission policies, shape-only host reads, drafts for human threads, cleanup on merge, and new troubleshooting rows."
- version: "1.2.0"
Expand Down Expand Up @@ -466,10 +468,14 @@ Bundled assets:
- `assets/first-mate-check.py` queries the watched PRs (head, draft state, reviews,
inline comments, check results, merge state), issues, releases, **branches**
(each new commit), **workflows** (latest run on a branch: head, status,
conclusion), 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` (for example `add branch kits@main`,
conclusion) in parallel, compares them with a saved snapshot, and prints
**nothing** when nothing changed. Your own delegated agents and pending
permission prompts are watched only when you run `agents on` / `permits on`
(off by default: the harness already sends finish notices and surfaces permits,
so watching them here would wake the loop on the coordinator's own actions). A
transient API error keeps the previous value and is reported. Manage the watch
list with `add`, `remove`, `prune`, `repo`, `agents`, `permits`, and `list`
(for example `add branch kits@main`,
`add workflow kits:deploy.yml@main`); `--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).
Expand Down Expand Up @@ -734,6 +740,7 @@ Before declaring coordination complete, verify:
| 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. |
| Several wait loops, or duplicate wake-ups | Run `first-mate-wait.sh status`. With the lock there is at most one loop; duplicates come from an older copy of the script or a different `FIRST_MATE_DIR`. Stop the extras, and check for a forked session acting as a second coordinator. |
| The loop wakes on the coordinator's own actions (archiving or finishing a delegated agent) | Agent and permit watching is off by default for exactly this reason. Run `first-mate-check.py agents off` and `permits off`, then re-baseline. Only enable them for a heartbeat flow with no harness finish notices. |
| A forked session inherits "wait loop running" from the handoff | Treat it as unverified. The fork is a non-coordinator: no loop, read-only `--show`, and ask the user before taking over. |
| Start exits 5 (another owner holds the loop) | Do not force it. Ask the user who the coordinator is; use `take-over` only on their word. |
| A quiet turn fills with empty tool calls | Stop. Run the check once and end the turn (section 8). |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"prs": ["qs#611", ...], "issues": ["qs#503", ...],
"releases": ["qs", ...],
"branches": ["qs@main", ...],
"workflows": ["qs:deploy-dev.yml@main", ...],
"ignore_agents": ["First Mate"]}
"workflows": ["qs:deploy-dev.yml@main", ...]}

A watched branch reports each new commit on it (so work that lands from other
sessions is seen). A watched workflow reports the latest run on a branch: its
Expand All @@ -31,6 +30,8 @@
first-mate-check.py remove pr qs#611 stop watching and forget its state (also: issue, branch, workflow)
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
first-mate-check.py agents on|off watch your own delegated agents (off by default)
first-mate-check.py permits on|off watch pending permission prompts (off by default)
"""
import concurrent.futures as cf
import json
Expand All @@ -53,7 +54,13 @@
"releases": [],
"branches": [],
"workflows": [],
"ignore_agents": ["First Mate"],
# Local orchestration watches are OFF by default: the harness already sends
# "[Agent finished]" notices and surfaces pending permission prompts, so
# watching the coordinator's own delegated agents (and its own permits) here
# would make the loop wake on its own actions. Enable with `agents on` /
# `permits on` for heartbeat flows that receive no harness finish notices.
"watch_agents": False,
"watch_permits": False,
}

BRANCH_REF = re.compile(r"^(?P<alias>[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)?)@(?P<branch>.+)$")
Expand Down Expand Up @@ -195,11 +202,14 @@ def workflow_snapshot(repo, workflow, branch):
return f"run {r['databaseId']} head={r['headSha'][:8]} {r['status']} {r.get('conclusion') or '-'}"


def agents_snapshot(ignore):
rows = json.loads(run(["paseo", "ls", "--json"]))
def agents_snapshot(self_id):
args = ["paseo", "ls", "--json"]
if self_id:
args += ["--label", f"paseo.parent-agent-id={self_id}"]
rows = json.loads(run(args))
items = sorted(
f"{r['shortId']}:{r['status']}:{r.get('name', '')[:36]}"
for r in rows if r.get("name") not in ignore)
for r in rows)
return "; ".join(items) or "none"


Expand All @@ -225,8 +235,10 @@ def collect(w):
for ref in w["workflows"]:
repo, wf, br, label = parse_workflow(w, ref)
jobs[f"workflow {label}"] = (workflow_snapshot, repo, wf, br)
jobs["agents"] = (agents_snapshot, set(w["ignore_agents"]))
jobs["permission prompts"] = (permits_snapshot,)
if w.get("watch_agents"):
jobs["agents"] = (agents_snapshot, os.environ.get("PASEO_AGENT_ID", ""))
if w.get("watch_permits"):
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()}
Expand Down Expand Up @@ -312,6 +324,11 @@ def manage(w, argv):
save_watch(w)
save_state(st)
print("pruned: " + (", ".join(dropped) or "nothing"))
elif cmd in ("agents", "permits") and len(argv) == 2 and argv[1] in ("on", "off"):
key = "watch_agents" if cmd == "agents" else "watch_permits"
w[key] = (argv[1] == "on")
save_watch(w)
print(f"{cmd} watching {'on' if w[key] else 'off'}")
else:
raise SystemExit(__doc__)

Expand Down
Loading