diff --git a/first-mate-skill/README.md b/first-mate-skill/README.md index b8f6549..174b7d4 100644 --- a/first-mate-skill/README.md +++ b/first-mate-skill/README.md @@ -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) 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 index 3f5181d..d25f5c5 100644 --- 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 @@ -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. 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 index 85c1a1d..3da64da 100644 --- 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 @@ -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 @@ -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" @@ -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). @@ -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). | 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 index 437afc3..ea14347 100755 --- 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 @@ -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 @@ -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 @@ -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[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)?)@(?P.+)$") @@ -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" @@ -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()} @@ -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__)