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
118 changes: 118 additions & 0 deletions bin/fm-herdr-outcome-publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Publish a real work outcome firstmate has just learned (a PR merged, a
# validation failed, a task landed) into herdr, so the fleet sidebar can show
# it. Herdr cannot derive outcomes itself - every event it sees on its own is
# mechanical (a pane appeared, a process exited); only firstmate talks to
# GitHub with identity and intent, so only firstmate knows a PR merged or CI
# went red. See data/herdr-event-channel-research/report.md sections C and D
# for the design this follows; nothing here re-derives that reasoning. That
# path is this captain's own private fleet record under data/ (gitignored
# per AGENTS.md section 2, never committed to this repo's tracked tree), so
# it will not appear in `git log` or a checkout of this repo - the citations
# below summarize its conclusions rather than pointing at trackable history.
#
# Two herdr CLI calls, matching report.md section D's "two calls, not one
# fused method" recommendation so the momentary and durable channels keep
# their deliberately different retention rules decoupled:
# 1. `herdr workspace report-signal` - momentary, fire-and-forget, exactly
# the four existing WorkspaceSignalKind values (transfer/completed/
# failed/idle). Never widen this vocabulary here - map the caller's own
# outcome onto one of the four; widening the herdr-side enum is a
# separate, out-of-scope design decision (report.md section C.1).
# 2. `herdr workspace report-metadata` - durable, per-workspace token
# ledger, written as outcome=<outcome> and (when given) summary=<summary>.
#
# The workspace targeted is resolved ONLY from the task's own
# state/<task-id>.meta (herdr_session=, herdr_workspace_id=, written by
# fm-spawn.sh when backend=herdr) - never a second identity scheme.
#
# This is a decoration, never a blocker: every unresolvable target (no task
# meta, a non-herdr task, no recorded herdr session/workspace, the herdr or
# jq tools missing, the CLI call itself failing) is a silent no-op that exits
# 0. A dropped report costs nothing - herdr's own report-signal and
# report-metadata already answer success on an unknown workspace or a stale
# sequence (report.md section C.1 live evidence). Callers may still append
# `|| true` for defense in depth, but this script never needs it to stay
# non-blocking on its own.
#
# A malformed call (wrong argument count, an outcome kind outside the four
# WorkspaceSignalKind values) is the one case treated as a caller bug: it
# prints a usage error and exits 2, so a broken call site is caught in
# testing rather than silently swallowed forever.
#
# Usage: fm-herdr-outcome-publish.sh <task-id> <signal-kind> <outcome> [summary]
# <signal-kind> one of: transfer completed failed idle
# <outcome> short durable token value, e.g. pr_merged, landed, failed
# <summary> optional short human-readable context
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
# FM_HOME resolution, including the refusal on an ambiently inherited home,
# has one owner: bin/fm-home-anchor-lib.sh.
# shellcheck source=bin/fm-home-anchor-lib.sh
. "$SCRIPT_DIR/fm-home-anchor-lib.sh"
fm_home_anchor_resolve "$FM_ROOT" >/dev/null 2>&1 || exit 0
STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"

# shellcheck source=bin/fm-pr-lib.sh
. "$SCRIPT_DIR/fm-pr-lib.sh"

if [ "$#" -lt 3 ] || [ "$#" -gt 4 ]; then
echo "usage: fm-herdr-outcome-publish.sh <task-id> <transfer|completed|failed|idle> <outcome> [summary]" >&2
exit 2
fi
ID=$1
KIND=$2
OUTCOME=$3
SUMMARY=${4:-}

case "$KIND" in
transfer|completed|failed|idle) ;;
*)
echo "usage: fm-herdr-outcome-publish.sh <task-id> <transfer|completed|failed|idle> <outcome> [summary]" >&2
exit 2
;;
esac
if ! fm_task_id_path_safe "$ID" || [ -z "$OUTCOME" ]; then
echo "usage: fm-herdr-outcome-publish.sh <task-id> <transfer|completed|failed|idle> <outcome> [summary]" >&2
exit 2
fi

# Everything below is target resolution and the CLI calls themselves: any
# failure here is a decoration dropped, never a caller-visible error.
META="$STATE/$ID.meta"
[ -f "$META" ] && [ ! -L "$META" ] || exit 0

# shellcheck source=bin/fm-backend.sh
. "$SCRIPT_DIR/fm-backend.sh"

[ "$(grep -c '^backend=' "$META" 2>/dev/null || true)" = 1 ] || exit 0
BACKEND=$(fm_backend_meta_exact_value "$META" backend) || exit 0
[ "$BACKEND" = herdr ] || exit 0

SESSION=$(fm_backend_meta_exact_value "$META" herdr_session) || exit 0
WORKSPACE=$(fm_backend_meta_exact_value "$META" herdr_workspace_id) || exit 0

fm_backend_source herdr >/dev/null 2>&1 || exit 0
fm_backend_herdr_tool_check >/dev/null 2>&1 || exit 0

# transfer lands on the receiver (--to); completed/failed/idle leave from the
# reporter (--from) - report.md section C.1.
DIRECTION_FLAG=--from
[ "$KIND" != transfer ] || DIRECTION_FLAG=--to

fm_backend_herdr_cli "$SESSION" workspace report-signal \
--source firstmate --kind "$KIND" "$DIRECTION_FLAG" "$WORKSPACE" \
>/dev/null 2>&1 || true

TOKENS=(--token "outcome=$OUTCOME")
[ -z "$SUMMARY" ] || TOKENS+=(--token "summary=$SUMMARY")
# The WORKSPACE_ID positional must come first: the installed herdr CLI
# (0.8.0) does not accept it after --source/--token options - verified
# empirically (`unknown option: firstmate` when the positional trails).
fm_backend_herdr_cli "$SESSION" workspace report-metadata \
"$WORKSPACE" --source firstmate "${TOKENS[@]}" \
>/dev/null 2>&1 || true

exit 0
5 changes: 5 additions & 0 deletions bin/fm-pr-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ if ! caller_has_merge_method "$@"; then
fi

gh-axi pr merge "$PR_NUMBER" --repo "$PR_OWNER/$PR_REPO" "${merge_args[@]+"${merge_args[@]}"}" "$@"

# Decoration only, never a blocker: publishes the merge into herdr as a
# signal and durable metadata when the task's own meta names a herdr
# workspace target. See bin/fm-herdr-outcome-publish.sh's header.
"$SCRIPT_DIR/fm-herdr-outcome-publish.sh" "$ID" completed pr_merged "PR #$PR_NUMBER merged" || true
9 changes: 9 additions & 0 deletions bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,15 @@ fm_backend_clear_transition "$BACKEND" "$STATE" "$T" || true
# Read before the state-file rm below; empty (pre-fix tasks without tasktmp=) is a no-op.
[ -n "$TASK_TMP" ] && rm -rf "$TASK_TMP"
remove_pr_poll_artifacts "$STATE" "$ID" || exit 1
if [ "$FORCE" != "--force" ]; then
# Decoration only, never a blocker: publishes the landed teardown into
# herdr as a signal and durable metadata when the task's own meta names a
# herdr workspace target. Read before the meta removal below, since that
# target lives only in this task's own state/<id>.meta. See
# bin/fm-herdr-outcome-publish.sh's header. --force means an explicitly
# authorized discard, not landed work, so it publishes nothing.
"$SCRIPT_DIR/fm-herdr-outcome-publish.sh" "$ID" completed landed "task $ID landed" || true
fi
rm -f "$STATE/$ID.status" "$STATE/$ID.turn-ended" "$STATE/$ID.meta" \
"$STATE/$ID.pi-ext.ts" "$STATE/$ID.grok-turnend-token" \
"$STATE/$ID.kimi-turnend-token"
Expand Down
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-pr-merge.sh` | Record PR metadata, then merge a task's canonical full GitHub URL |
| `fm-promote.sh` | Promote a scout task in place to a protected ship task |
| `fm-teardown.sh` | Fail-closed teardown: return landed ship worktrees, require completed scout deliverables, retire secondmate homes |
| `fm-herdr-outcome-publish.sh` | Decoration-only: publish a real learned work outcome (PR merged, task landed) into herdr as a signal plus durable metadata, when the task's own meta names a herdr workspace target |
| `fm-harness.sh` | Detect the running harness and resolve crew or secondmate harness, model, and effort |
| `fm-lock.sh` | Per-home firstmate session lock |
| `fm-x-lib.sh` | Shared X-mode config, relay, and reply-threading helpers |
Expand Down
Loading
Loading