Skip to content
Closed
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
104 changes: 104 additions & 0 deletions .agents/skills/linear-ticket-intake/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
name: linear-ticket-intake
description: >-
Agent-only procedure for query-only Linear poll events and agent-owned ticket
updates. Use before arming the Linear poller and on any
`procevent linear <source-id> <sequence>` wake. Owns Linear re-fetch,
duplicate prevention, one-writer assignment, Sol and Luna role separation,
comment routing, writer transfer, and process-event acknowledgement.
user-invocable: false
metadata:
internal: true
---

# Linear ticket intake

Use this procedure before arming `bin/fm-procevent-linear.sh` and whenever a `check:` wake carries `procevent linear <source-id> <sequence>`.
Load `process-event-sources` for the shared capture, read, and handled-acknowledgement contract.

The poller is a detector, never a Linear writer.
It may query mapped projects, compare its private observation snapshot, and emit immutable issue ids, identifiers, project names, event types, comment ids, and the API-provided canonical URL.
Never add a mutation to it, give it a ticket-writer lease, or use it as a fallback when an assigned worker cannot reach Linear MCP.

## Arm

Review the private config, then arm the source through its adapter:

```sh
bin/fm-procevent-linear.sh arm config/linear-poll.json
```

The registered command blocks outside the conversational turn and checks every 30 seconds.
An unchanged snapshot prints nothing, so the process-event runner has no result to capture and no wake or model call to create.

## Handle a detected Todo

Read the exact captured result through the adapter:

```sh
bin/fm-procevent-linear.sh read state/procevent-inbox/<source-id>.<sequence>.result
```

Re-fetch the issue through Linear MCP using its immutable id or identifier.
Treat Linear as the source of truth for current status, blockers, project mapping, and canonical URL; reject a mapping mismatch rather than guessing.
Check Firstmate's backlog, live task metadata, and `bin/fm-linear-ticket-writer.sh show <identifier>` before dispatch.
If the issue is blocked, no longer Todo, already leased, or already represented by a live or retained local task, do not create another task.

For a new eligible issue:

1. Create the normal Firstmate ship task and its local backlog record.
2. Choose one persistent Luna implementation worker as ticket owner and create its lease before spawn:

```sh
bin/fm-linear-ticket-writer.sh assign <immutable-issue-id> <identifier> <canonical-url> <task-id> <luna-writer-id>
bin/fm-linear-ticket-writer.sh owner-brief <identifier> <task-id> <luna-writer-id>
```

3. Create a separate Sol planning or review task when needed and apply its no-write brief before spawn:

```sh
bin/fm-linear-ticket-writer.sh planner-brief <identifier> <sol-task-id>
```

4. Spawn through the normal Firstmate harness procedure.

The owner brief is the authority boundary.
Luna confirms the exact ticket, checks its lease before every Linear mutation, moves Todo to In Progress, creates or updates exactly one `## Firstmate Workpad`, and records the accepted plan, progress, blockers, PR URL, review results, fixes, and completion state.
Luna may change only its assigned ticket and must report a blocker when Linear MCP is unavailable.
Luna moves the ticket to Human Review only after the project delivery gates pass, and marks it Done only after independently verifying the PR merged.

Sol can plan and review but cannot mutate Linear.
Sol reports findings through Firstmate, and Firstmate steers those findings to Luna so the sole writer records them on the ticket.
Firstmate owns its local backlog and fleet records and must not ask Luna to edit them.

## Handle a detected comment

Re-fetch the comment and issue through Linear MCP.
If the issue has a current writer lease, steer the comment to that Luna task through the durable task inbox instead of creating another task or replying as Firstmate.
If no valid lease exists, reconcile the local task state before deciding whether this is missed intake or stale external activity.
The poll event itself never authorizes a Linear reply.

## Transfer a writer

Two live workers never share write authority.
Stop or revoke the old worker's Linear work first, then transfer explicitly:

```sh
bin/fm-linear-ticket-writer.sh transfer <identifier> <expected-old-writer-id> <replacement-writer-id>
```

The current lease generation and append-only transfer history are the durable authority.
Apply an owner brief for the replacement only after the transfer succeeds.
A stale worker fails `assert-writer` and `assert-target` after transfer.

## Reconcile and acknowledge

Firstmate may read Linear to reconcile status but does not duplicate Luna's comments, Workpad edits, status changes, or completion update.
After the Todo or comment event is fully routed, acknowledge that exact captured sequence:

```sh
bin/fm-procevent.sh handled <source-id> <sequence>
```

Repeated wakes for an already represented issue are a dedupe check, not permission to create another task, lease, or Workpad.
Never merge a project PR without the captain's explicit authority unless the project's separately configured standing merge posture already grants it.
5 changes: 4 additions & 1 deletion .agents/skills/process-event-sources/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Eligibility is a firstmate judgment made BEFORE arming, because the scripts cann
Never bind an action that is destructive, irreversible, or security-sensitive, an action needing captain approval or any gate decision, or an action whose right form depends on what the condition finds - those keep the existing check-fires-then-firstmate-decides flow, for which a plain custom check or another adapter stays correct.
When in doubt, arm only the condition half as an ordinary check and keep the action as a wake-time decision.

`bin/fm-procevent.sh --help`, `bin/fm-procevent-lavish.sh --help`, `bin/fm-procevent-when.sh --help`, `bin/fm-procevent-quota.sh --help`, and `bin/fm-procevent-remote-reply.sh --help` own the exact commands and flags.
`bin/fm-procevent.sh --help`, `bin/fm-procevent-lavish.sh --help`, `bin/fm-procevent-linear.sh --help`, `bin/fm-procevent-when.sh --help`, `bin/fm-procevent-quota.sh --help`, and `bin/fm-procevent-remote-reply.sh --help` own the exact commands and flags.

An explicitly enabled external adapter registers through `bin/fm-procevent.sh register-extension`, never through a package-discovered script or package-supplied argv.
[`docs/configuration.md`](../../../docs/configuration.md#trusted-external-process-event-adapters-configextensionsd) owns setup and [`docs/extension-bindings.md`](../../../docs/extension-bindings.md) owns the narrow trusted-code and untrusted-evidence boundary.
Expand Down Expand Up @@ -97,6 +97,9 @@ Two rules the commands cannot enforce for you:
: Ask the adapter what the result means rather than parsing it yourself.
`bin/fm-procevent.sh classify <result-file>` routes through the immutable built-in or extension identity captured with that result; for Lavish, its existing direct command returns `feedback`, `ended`, `waiting`, `missing`, or `unknown`.
Consume a Lavish capture with `bin/fm-procevent-lavish.sh read <result-file>` rather than grepping the raw file: that command reports declared and presented item counts plus a completeness verdict, enumerates every captured queued item while retaining supplied element identity, and surfaces a `tag=message` session-ending message as its own field.
: A `linear` wake is intake evidence, not write authority.
Load `linear-ticket-intake`, consume the capture with `bin/fm-procevent-linear.sh read <result-file>`, and follow its re-fetch, dedupe, one-writer, comment-routing, and acknowledgement procedure.
Never let the poller comment, change status, assign a worker, or become the fallback writer.
`answers` remains the keyed-choice extractor and never treats freeform prose as a decision key.
A `feedback` result can still be the last one a review ever produces, so never assume another wake is coming just because the state is not `ended`.
: A routine no-op an adapter positively identifies never becomes a wake at all - it is recorded as handled and stays silent, so you never see it. For Lavish that is exactly an ended session carrying nothing: a board the captain closed without saying anything. A board close carrying a real answer, and every other result, still wakes you unchanged. Never read the absence of a wake as proof a review is still open; ask the source, not the queue.
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ config/turnend-churn-absorb optional presence flag opting this home into the de
config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup")
config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/watched-tools.json optional list of the tools this home depends on, read by the update check armed with bin/fm-tool-update-check.sh; LOCAL, gitignored, firstmate-maintained but human-editable, and NOT inherited by secondmate homes; see docs/configuration.md "Watched tool updates"
config/linear-poll.json optional mapped-project configuration for the read-only Linear GraphQL detector; LOCAL, gitignored, and not inherited; see docs/configuration.md "Linear Todo and comment polling"
config/x-mode.env generated Relay watcher cadence; LOCAL, gitignored; source before arming watcher when present
data/ personal fleet records; LOCAL, gitignored as a whole
backlog.md task queue, dependencies, history
Expand Down Expand Up @@ -117,6 +118,8 @@ state/ runtime records and signals; gitignored
pending-replies/ parent-owned secondmate pending-reply records (correlation id, delivery vs reply, recovery, escalation); fm-pending-reply-lib.sh
procevent/ registered process-to-event sources, one private record per canonical source id; written only by bin/fm-procevent.sh, and their presence alone keeps supervision required (section 13)
procevent-inbox/ private captured results and their durable handled-acknowledgement markers; source output lives here and never in an event line
linear-poll/ private read-only detector snapshots keyed by Linear process-event source id; written only by bin/fm-procevent-linear.sh
linear-ticket-writers/ private one-writer leases and append-only transfer histories keyed by Linear issue identifier; written only by bin/fm-linear-ticket-writer.sh
decision-bindings/ private records marking a captured-answer source as feeding the keyed-answer intake, with a legacy origin on pre-collapse records; written only by bin/fm-captain-hold.sh bind, dropped by unbind and by source retirement (section 13; docs/captain-hold-lifecycle.md)
when/ private condition->action watch specs, their trust bindings, and single-fire markers; written only by bin/fm-procevent-when.sh (section 13's process-event-sources trigger)
inbox/ captain notes captured out of band by bin/fm-inbox.sh, including the voice handover's queued requests; each note appends one `check` wake and stays pending until acknowledged with `bin/fm-inbox.sh drain --ack <id>`, which moves it to inbox/handled/ (docs/voice-relay.md)
Expand Down Expand Up @@ -549,6 +552,7 @@ These skills are not captain-invocable; load them only at their precise triggers
- `captain-hold-lifecycle` - load before treating an investigation or visual review as complete, before ending a visual review that exposed a captain decision, when recording or routing the captain's answer, and on any `RECORD DIVERGENCE` line from the wake drain.
- `process-event-sources` - load before arming a long-polling source, before registering a deterministic condition->action watch (do X as soon as Y is true), and on any `procevent <adapter> <source-id> <sequence>` check wake.
Never run a registered source's blocking command yourself in a conversational turn.
- `linear-ticket-intake` - load before arming the Linear poller and on any `procevent linear <source-id> <sequence>` check wake; it owns read-only re-fetch, duplicate prevention, one-writer assignment, Sol/Luna role separation, comment routing, writer transfer, and acknowledgement.
- `fmx-respond` - load on an `x-mention <request_id>` `check:` wake to handle the mention, on an `x-mode-error ...` `check:` wake to report the Relay configuration blocker, on a `public-followup ...` `check:` wake or a startup-surfaced public commitment, and on any milestone or terminal wake for a Relay-linked task before posting its completion follow-up; relevant only when Relay is on.
- `firstmate-codexapp` - load before coordinating a visible Codex Desktop thread, evaluating a Codex App backend request, or reconciling Codex Desktop host-tool smoke evidence for Firstmate work.
- `firstmate-coding-guidelines` - load before changing firstmate's shared, tracked material, as defined by section 1's list, whether editing directly or briefing a crewmate for a firstmate-repo task.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ Firstmate's skills live in two separate places with different audiences:

- [docs/architecture.md](docs/architecture.md) - maintainer architecture for the crew, supervision, worktrees, secondmates, and project modes.
- [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend selection, optional Relay and its X and Discord setup steps, trusted external process-event adapter setup, the files you set, and harness support.
- [docs/configuration.md#linear-todo-and-comment-polling](docs/configuration.md#linear-todo-and-comment-polling) - optional query-only Linear Todo and comment polling with exact ticket links and one assigned ticket writer.
- [docs/extension-bindings.md](docs/extension-bindings.md) - maintainer architecture for the narrow trusted external `process-event-adapter/1` package, binding, handshake, and evidence boundary.
- [docs/remote-secondmates.md](docs/remote-secondmates.md) - current setup, routing, transfer, recovery, and safety behavior for whole-home remote second mates.
- [docs/calm.md](docs/calm.md) - current Pi `/calm` behavior and supported presentation limits.
Expand Down
49 changes: 48 additions & 1 deletion bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,53 @@ nm_run_head_matches_worktree() {
fm_nm_head_matches_worktree "$WT" "$run_head"
}

# A terminal no-mistakes `outcome: passed` is only a local pipeline result until
# the linked GitHub PR is checked live. Never turn an unverified or still-open
# PR into a merged/closed claim. A missing PR remains a genuine local-only
# completion, while non-GitHub PRs and unavailable GitHub reads stay explicit
# without making a forge claim this helper cannot prove.
nm_passed_run_detail() {
local pr_url identity repo_path number pr_out state
pr_url=$(strip_quotes "$(nm_field pr)")
if [ -z "$pr_url" ]; then
printf 'run passed: local work complete'
return
fi

# Only GitHub URLs can be checked by the required gh-axi GitHub surface.
# Strip ordinary URL decorations before applying the exact owner/repo/pull
# shape, so malformed or foreign-forge URLs cannot become a merge claim.
pr_url=${pr_url%%\?*}
pr_url=${pr_url%%\#*}
pr_url=${pr_url%/}
identity=$(printf '%s\n' "$pr_url" \
| sed -nE 's#^https://github\.com/([^/]+/[^/]+)/pull/([0-9]+)$#\1 \2#p')
if [ -z "$identity" ]; then
printf 'run passed: PR state unverified'
return
fi
repo_path=${identity% *}
number=${identity##* }
if ! command -v gh-axi >/dev/null 2>&1; then
printf 'run passed: PR state unverified'
return
fi
# Address the exact linked repository explicitly via gh-axi's --repo flag
# (as bin/fm-pr-merge.sh already does), so a stale or foreign-fork link
# cannot silently resolve against the crew worktree's own checkout instead.
pr_out=$(cd "$WT" && gh-axi pr view "$number" --repo "$repo_path" 2>/dev/null) || {
printf 'run passed: PR state unverified'
return
}
state=$(fm_nm_strip_quotes "$(fm_nm_field "$pr_out" state)")
case "$state" in
merged) printf 'run passed: PR merged/closed' ;;
closed) printf 'run passed: PR closed (not merged)' ;;
open) printf 'run passed: PR open (not merged/closed)' ;;
*) printf 'run passed: PR state unverified' ;;
esac
}

# Coarse runs-list rows are "<status> <branch> <short-sha> ...". 0 if the short
# sha for this branch row matches the worktree head under the same rules as
# nm_run_head_matches_worktree (equal, or local is ancestor of run tip).
Expand Down Expand Up @@ -499,7 +546,7 @@ if [ "$HAVE_RUN" = 1 ]; then

if [ -n "$outcome" ]; then
case "$outcome" in
passed) RUN_STATE="done"; RUN_DETAIL="run passed: PR merged/closed" ;;
passed) RUN_STATE="done"; RUN_DETAIL=$(nm_passed_run_detail) ;;
checks-passed) RUN_STATE="done"; RUN_DETAIL="checks green: PR ready for review" ;;
failed) RUN_STATE=failed; RUN_DETAIL="run failed" ;;
cancelled) RUN_STATE=failed; RUN_DETAIL="run cancelled" ;;
Expand Down
Loading
Loading