Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
59c5ab1
fix: keep Relay public loops open until retire
kunchenguid Aug 21, 2026
d96ef76
no-mistakes(review): Propagate public follow-up registration removal …
kunchenguid Aug 21, 2026
34ded84
no-mistakes(review): Persist retire receipts and align parent resolution
kunchenguid Aug 21, 2026
2b566dd
no-mistakes(review): Make rechain resumable after partial obligation …
kunchenguid Aug 21, 2026
bb07c0c
no-mistakes(review): Repair follow-up state, briefs, and expiry escal…
kunchenguid Aug 21, 2026
e9aa044
no-mistakes(review): Serialize follow-up delivery stamps with retirement
kunchenguid Aug 21, 2026
dfba629
no-mistakes(review): Serialize rechain claims and protect registratio…
kunchenguid Aug 21, 2026
1900286
no-mistakes(review): Avoid reporting retired delivery loops as open
kunchenguid Aug 21, 2026
958ada1
no-mistakes(document): Refresh public-loop documentation and verifica…
kunchenguid Aug 21, 2026
5604b0c
no-mistakes: apply CI fixes
kunchenguid Aug 22, 2026
f2c9678
no-mistakes(review): Preserve delivered follow-up bindings during reg…
kunchenguid Aug 22, 2026
c04eb66
no-mistakes(review): Harden public follow-up retirement and rechain r…
kunchenguid Aug 22, 2026
c62f945
no-mistakes(review): Fail closed on unresolved secondmate retirement
kunchenguid Aug 22, 2026
5dc4cac
no-mistakes(review): Bind secondmate cleanup to its recorded canonica…
kunchenguid Aug 22, 2026
09e9e90
no-mistakes(review): Fix rechain command output and expiry validation
kunchenguid Aug 22, 2026
f40bfa7
no-mistakes(review): Validate brief keys and warn on remote promotion
kunchenguid Aug 22, 2026
9e24e26
no-mistakes(document): Document retained public follow-up loops
kunchenguid Aug 22, 2026
51bdbca
no-mistakes(lint): Remove unused bounded-wait loop variable
kunchenguid Aug 22, 2026
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
24 changes: 19 additions & 5 deletions .agents/skills/fmx-respond/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,31 +231,45 @@ So treat second-mate-routed Relay work as a promised final by construction: the
**When you promise a final (including every Relay request whose work is routed to a second mate):**

1. Create the typed obligation with `tasks-axi public-followup add` and bind the work with `bind-work`, keeping the public-safe summary and the opaque thread binding in the obligation and the full request context where the poll already put it.
When the public ask plainly implies follow-on work ("look into X and fix it"), register the promised-final against the outcome and deliver any interim report as a separate `--purpose milestone` obligation on the same thread.
An ask that genuinely terminates at a report stays `report-ready`; do not invent a ship commitment for work the captain has not authorized.
2. Register it with `bin/fm-public-followup.sh register <obligation-id> --relation <relation-id> --work-home <main|secondmate:<id>> --work-id <task-id> --generation <n>`.
This is what makes the commitment reconcilable without you.
3. Put `bin/fm-public-followup.sh brief <obligation-id>` output straight into the worker's brief.
It prints the exact reporting command for that binding.
When the work is routed to a second mate rather than spawned here, the routed item's own note carries that same output, so it survives the routing and reaches whoever ends up doing the work.
It prints the exact reporting command for that binding, including the obligation's actual required deliverable keys.
When the work is routed to a second mate rather than spawned here, the routed item's own note MUST carry that same `brief` output so it survives the routing and reaches whoever ends up doing the work.
A header-only routed item loses the emit command.
Never ask a worker to find the thread or post the reply: only this home holds the relay consent and the thread binding.

**When work reports back, or on a `public-followup ...` check wake, or when the session-start digest lists a public commitment:**
**When work reports back, or on a `public-followup ...` check wake, or when the session-start digest lists a public commitment or an open public loop:**

1. Run `bin/fm-public-followup.sh consume`.
It reconciles every typed terminal result from disk and prints `ready <obligation-id> <request-id> <platform>` for each commitment that became deliverable.
A refusal prints `rejected <event-id>: <reason>` and quarantines that event; read the reason rather than re-emitting blindly.
2. For each ready commitment, run `bin/fm-public-followup.sh deliver <obligation-id>`.
With no `--text-file` it reuses the accepted terminal outcome exactly, which is the preferred path for a landed result.
Only pass `--text-file` when the outcome genuinely needs composing, and hold it to the same public-safety bar as every other reply here.
Delivery clears the bound task's legacy Relay link at the validated receipt boundary; if it reports a cleanup failure, use its reconciliation message and do not post a legacy final.
Delivery clears the bound task's legacy Relay link at the validated receipt boundary and stamps the registration `state=delivered`; it does **not** close the public loop.
If it reports a cleanup failure, use its reconciliation message and do not post a legacy final.
3. Read the outcome and stop guessing at anything it refuses:
- "still waiting on its bound work" means the work has not reported a typed terminal result yet - do not post.
- "recorded as retryable" means nothing was posted; retry on a later wake.
- "held" means the thread's platform or budget is unresolvable right now; retry once it is recoverable.
- "mid-delivery" means a previous post started and its outcome was never recorded. Do NOT deliver again. Establish whether that post landed, then either close it with `record-posted <id> --attempt <n> --chunks <exact-count>` or escalate. Posting again would put a second reply in a public thread.
- "mid-delivery" means a previous post started and its outcome was never recorded.
Do NOT deliver again.
Establish whether that post landed, then either record its receipt with `record-posted <id> --attempt <n> --chunks <exact-count>` or escalate.
Posting again would put a second reply in a public thread.
- "the relay no longer accepts a follow-up" is a captain decision, not a retry.
4. After a successful deliver (or when the digest lists an `open-loop` line), decide the disposition in that same turn:
- Follow-on work authorized from the same public thread: `bin/fm-public-followup.sh rechain <new-id> --from <delivered-id> --work-home <main|secondmate:<id>> --work-id <task-id> --expected <pr-merged|report-ready|local-main>`, then put the printed `brief` into that follow-on's instructions (and into the routed item's own note when the work is routed).
If rechain reports an interrupted bind or source-retirement failure, resume the same destination with the same command; the retained source claim forbids choosing another destination.
- The public loop is finished: `bin/fm-public-followup.sh retire <id> --reason "<why the loop is done>"`.
Delivering a final is not closure.
Silence after delivery is an open loop, not a kept promise for later work.

Cleanup refuses while a commitment is still owed for that exact work, so never reach for `--force` to get past it.
Treat a commitment as kept only after a validated posted receipt or an explicit captain waiver.
Treat a public loop as closed only after `retire`.

## Notes

Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ state/ runtime records and signals; gitignored
x-inbox/ generated Relay pending mention payloads; fmx-respond drains it (section 14)
x-context/ generated Relay durable per-request reply context and one-wake offer markers, keyed by request_id; survives inbox cleanup and expires within seven days (section 14; bin/fm-x-lib.sh)
x-outbox/ generated Relay dry-run reply and dismiss previews; inspect it when FMX_DRY_RUN is set (section 14)
public-followup/ generated private transport for promised public replies: commitment registrations, typed terminal-result inbox, accepted/rejected ledgers (section 14; bin/fm-public-followup.sh)
public-followup/ generated private transport for promised public replies: retained open-loop registrations, typed terminal-result inbox, accepted/rejected ledgers, and retirement receipts (section 14; bin/fm-public-followup.sh)
x-poll.error x-poll.claim-error generated Relay and offer-claim diagnostic dedupe markers
.startup-network.* status, report, per-step elapsed timings, inline-print claim, and lock for the deferred network stage session start runs off its blocking path; bin/fm-startup-network.sh
.wake-queue durable queued wakes retained until post-handling acknowledgement: epoch<TAB>seq<TAB>kind<TAB>key<TAB>payload
Expand Down Expand Up @@ -555,7 +555,7 @@ On an `x-mention <request_id>` or `x-mode-error ...` check wake, load `fmx-respo
For every Relay-linked terminal outcome, load that owner and use the promised-final reconciliation when a typed public commitment exists, otherwise post the final completion follow-up before teardown.

A promised final public reply is durable state, never conversation memory.
Load `fmx-respond` before promising one, on a `public-followup ...` check wake, and whenever the session-start digest lists a public commitment awaiting delivery.
Load `fmx-respond` before promising one, on a `public-followup ...` check wake, and whenever the session-start digest lists a public commitment awaiting delivery or an open public loop.
Only the home holding the relay consent and thread binding ever posts it, so never ask a secondmate or crewmate to find the thread or send the reply, and never recover a terminal result by reading a `done:` sentence.

## Captain instruction precedence
Expand Down
14 changes: 11 additions & 3 deletions bin/fm-backlog-handoff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
# already present in the secondmate backlog is reported and skipped, and if
# any key matches neither backlog nothing is moved;
# - warning, after a successful move, when a moved key still owes a public
# relay reply bound to main/<key>, because that binding no longer names the
# home that owns the work. The move is not blocked: rebinding the commitment
# to secondmate:<id> is a relay-side decision the caller makes.
# relay reply bound to main/<key>, or when this home has an open public loop
# with nothing owed, because routing work out does not close that loop. The
# move is not blocked: rebinding or rechain is a relay-side decision the
# caller makes.
#
# What `tasks-axi mv <id>... --to <dest>` owns: moving each full item BLOCK
# byte-exact (header, body lines, blank separators, and indented pseudo-headings
Expand Down Expand Up @@ -58,6 +59,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
REG="$DATA/secondmates.md"
MAIN_BACKLOG="$DATA/backlog.md"
# shellcheck source=bin/fm-tasks-axi-lib.sh disable=SC1091
Expand All @@ -66,6 +68,8 @@ MAIN_BACKLOG="$DATA/backlog.md"
. "$SCRIPT_DIR/fm-secondmate-registry-lib.sh"
# shellcheck source=bin/fm-wake-lib.sh
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-public-followup-lib.sh
. "$SCRIPT_DIR/fm-public-followup-lib.sh"

ACTIVE_HANDOFF_LOCK=
ACTIVE_REGISTRY_LOCK=
Expand Down Expand Up @@ -288,6 +292,10 @@ warn_stale_public_commitments() { # <secondmate-id> <moved-key>...
printf 'warning: %s still owes a public reply bound to main/%s; rebind it to secondmate:%s (tasks-axi public-followup bind-work, then bin/fm-public-followup.sh register <obligation-id> --relation <relation-id> --work-home secondmate:%s --work-id %s --generation <n>) or the promised reply will be reconciled against work this home no longer owns.\n' \
"$key" "$key" "$id" "$id" "$key" >&2
done
if fm_pf_relay_active "$FM_HOME" && fm_pf_has_delivered_open_loops "$STATE"; then
printf 'warning: this home has an open public loop with nothing owed; routing work to secondmate:%s does not close it. Hand it on with bin/fm-public-followup.sh rechain or close it with retire --reason.\n' \
"$id" >&2
fi
# Reporting never changes the handoff's own success: the move already landed.
return 0
}
Expand Down
108 changes: 108 additions & 0 deletions bin/fm-promote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
. "$SCRIPT_DIR/fm-pr-lib.sh"
# shellcheck source=bin/fm-wake-lib.sh
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-public-followup-lib.sh
. "$SCRIPT_DIR/fm-public-followup-lib.sh"
# shellcheck source=bin/fm-secondmate-parent-lib.sh
. "$SCRIPT_DIR/fm-secondmate-parent-lib.sh"
# shellcheck source=bin/fm-secondmate-registry-lib.sh
. "$SCRIPT_DIR/fm-secondmate-registry-lib.sh"

MODE=
YOLO=
Expand Down Expand Up @@ -123,3 +129,105 @@ META_LOCK_HELD=0
HOME_Q=$(printf '%q' "$FM_HOME")
echo "promoted $ID to ship mode=$MODE yolo=$YOLO (teardown protection restored)"
echo "next: FM_HOME=$HOME_Q bin/fm-send.sh fm-$ID '<ship instructions for mode=$MODE: review scratch state with git status and git log; reset to a clean default-branch base; carry over only intended fix changes; create branch fm/$ID; implement; report done>'"

promote_print_rechain_hint() {
local consent_home=$1 work_home=$2 task_id=$3 id prefix
prefix=
[ "$consent_home" = "$FM_HOME" ] || prefix="FM_HOME=$(printf '%q' "$consent_home") "
while IFS= read -r id; do
[ -n "$id" ] || continue
[ "$(fm_pf_registry_get "$consent_home/state" "$id" state)" = delivered ] || continue
echo "next: ${prefix}bin/fm-public-followup.sh rechain <new-obligation-id> --from $id --work-home $work_home --work-id $task_id --expected pr-merged"
done <<EOF
$(fm_pf_registry_ids_for_work "$consent_home/state" "$work_home" "$task_id")
EOF
}

promote_canonical_home() {
local home=$1
case "$home" in /*) ;; *) return 1 ;; esac
CDPATH='' cd -- "$home" 2>/dev/null && pwd -P
}

promote_resolve_primary_home() {
local parent=$1 child=$2 mate_id=$3 parent_meta registry meta_home
fm_pf_home_id_valid "secondmate:$mate_id" || return 1
parent=$(promote_canonical_home "$parent") || return 1
child=$(promote_canonical_home "$child") || return 1
[ "$parent" != "$child" ] || return 1
parent_meta="$parent/state/$mate_id.meta"
[ -f "$parent_meta" ] && [ ! -L "$parent_meta" ] || return 1
[ "$(fmx_meta_get "$parent_meta" kind)" = secondmate ] || return 1
meta_home=$(fmx_meta_get "$parent_meta" home)
meta_home=$(CDPATH='' cd -- "$meta_home" 2>/dev/null && pwd -P) || return 1
[ "$meta_home" = "$child" ] || return 1
registry="$parent/data/secondmates.md"
secondmate_registry_validate_bindings "$registry" secondmate_registry_path_key \
"$mate_id" "$child" || return 1
printf '%s\n' "$parent"
}

promote_warn_parent_unresolved() {
echo "warning: could not resolve the consent-holding parent home for secondmate $1; promotion succeeded, but any open public loop must be inspected and rechained from the parent." >&2
}

if [ -f "$FM_HOME/.fm-secondmate-home" ]; then
PROMOTE_MATE_ID=$(sed -n '1p' "$FM_HOME/.fm-secondmate-home" 2>/dev/null || true)
PROMOTE_PARENT_RECORD=absent
PROMOTE_PARENT_ROUTE=
PROMOTE_DURABLE_PARENT=
if [ -e "$FM_HOME/.fm-secondmate-parent" ] || [ -L "$FM_HOME/.fm-secondmate-parent" ]; then
PROMOTE_PARENT_RECORD=invalid
if fm_secondmate_parent_record_parse "$FM_HOME/.fm-secondmate-parent"; then
PROMOTE_PARENT_RECORD=valid
PROMOTE_PARENT_ROUTE=$FM_SECONDMATE_PARENT_ROUTE
PROMOTE_DURABLE_PARENT=$FM_SECONDMATE_PARENT_HOME
fi
fi
if [ "$PROMOTE_PARENT_RECORD" = invalid ]; then
promote_warn_parent_unresolved "$PROMOTE_MATE_ID"
elif [ "$PROMOTE_PARENT_ROUTE" = local ]; then
PROMOTE_PARENT_CANDIDATE=${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-$PROMOTE_DURABLE_PARENT}
PROMOTE_PARENT_BINDINGS_MATCH=1
if [ -n "${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-}" ]; then
PROMOTE_LIVE_PARENT=$(promote_canonical_home "$FM_PUBLIC_FOLLOWUP_PRIMARY_HOME") \
|| PROMOTE_PARENT_BINDINGS_MATCH=0
PROMOTE_RECORDED_PARENT=$(promote_canonical_home "$PROMOTE_DURABLE_PARENT") \
|| PROMOTE_PARENT_BINDINGS_MATCH=0
if [ "$PROMOTE_PARENT_BINDINGS_MATCH" = 1 ] \
&& [ "$PROMOTE_LIVE_PARENT" != "$PROMOTE_RECORDED_PARENT" ]; then
PROMOTE_PARENT_BINDINGS_MATCH=0
fi
fi
if [ "$PROMOTE_PARENT_BINDINGS_MATCH" = 1 ] \
&& PROMOTE_PARENT=$(promote_resolve_primary_home \
"$PROMOTE_PARENT_CANDIDATE" "$FM_HOME" "$PROMOTE_MATE_ID"); then
if fm_pf_relay_active "$PROMOTE_PARENT"; then
promote_print_rechain_hint "$PROMOTE_PARENT" "secondmate:$PROMOTE_MATE_ID" "$ID"
fi
else
promote_warn_parent_unresolved "$PROMOTE_MATE_ID"
fi
elif [ "$PROMOTE_PARENT_ROUTE" = remote ]; then
PROMOTE_HOME_ENV_TOKEN=
if [ -f "$FM_HOME/.env" ]; then
PROMOTE_HOME_ENV_TOKEN=$(fmx_env_get FMX_PAIRING_TOKEN "$FM_HOME/.env")
fi
if [ -n "$PROMOTE_HOME_ENV_TOKEN" ]; then
promote_warn_parent_unresolved "$PROMOTE_MATE_ID"
fi
elif [ -n "${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-}" ]; then
if fm_pf_relay_active "$FM_PUBLIC_FOLLOWUP_PRIMARY_HOME"; then
if PROMOTE_PARENT=$(promote_resolve_primary_home \
"$FM_PUBLIC_FOLLOWUP_PRIMARY_HOME" "$FM_HOME" "$PROMOTE_MATE_ID"); then
promote_print_rechain_hint "$PROMOTE_PARENT" "secondmate:$PROMOTE_MATE_ID" "$ID"
else
promote_warn_parent_unresolved "$PROMOTE_MATE_ID"
fi
fi
elif fm_pf_relay_active "$FM_HOME"; then
promote_warn_parent_unresolved "$PROMOTE_MATE_ID"
fi
elif fm_pf_relay_active "$FM_HOME"; then
promote_print_rechain_hint "$FM_HOME" main "$ID"
fi
Loading
Loading