Skip to content
Open
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
1 change: 1 addition & 0 deletions .agents/skills/design-profile/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Do not infer or pin a model because each harness's current authenticated catalog

Scaffold with `bin/fm-brief.sh <id> <repo> --design --mode <mode>` plus any applicable work-item and Herdr flags.
Spawn with `bin/fm-spawn.sh <id> <repo-path> --design --mode <mode> --yolo <on|off> --harness <harness>` plus the selected model and effort when present.
Restart a live design worker with `bin/fm-control.sh relaunch`, which keeps `kind=design` and the dispatch-pinned skill release ([`docs/agent-control.md`](../../../docs/agent-control.md#transactional-relaunch); [`docs/fleet-data-contracts.md`](../../../docs/fleet-data-contracts.md#the-design-tasks-plugin-release)).

## Interview authority

Expand Down
30 changes: 18 additions & 12 deletions bin/fm-control.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
# axis for the replacement. With no explicit axis, a secondmate
# re-resolves its durable config/secondmate-harness pin (harness
# plus its optional model and effort tokens) exactly as any other
# respawn does, while a ship or scout keeps the exact adapter
# already recorded for it.
# respawn does, while a ship, design, or scout keeps the exact
# adapter already recorded for it.
# A prefixed raw-command basename cannot reconstruct its launch
# command, so relaunch requires an explicit --harness for it.
# --note is required for a ship or scout, whose replacement
# inherits the local copy but none of the conversation; a
# secondmate reconciles its own home's records at startup, so its
# --note is required for a ship, design, or scout, whose
# replacement inherits the local copy but none of the conversation;
# a secondmate reconciles its own home's records at startup, so its
# standing charter is never rewritten.
# Records a durable checkpoint and that note, exits the old agent,
# then delegates the launch to its single owner,
Expand Down Expand Up @@ -644,10 +644,10 @@ resolve_relaunch_profile() {
# A secondmate's harness, model, and effort are a durable configured pin
# that every respawn re-resolves (the secondmate-provisioning contract), so
# a relaunch with no explicit harness picks up a newly configured one
# instead of freezing whatever this incarnation happens to run. Crewmates
# and scouts deliberately do NOT resolve config here: their harness comes
# from firstmate's own dispatch-profile judgment at intake, and silently
# re-resolving it would bypass that consultation.
# instead of freezing whatever this incarnation happens to run. Crewmates,
# design workers, and scouts deliberately do NOT resolve config here: their
# harness comes from firstmate's own dispatch-profile judgment at intake,
# and silently re-resolving it would bypass that consultation.
CONFIG_HARNESS=$("$SCRIPT_DIR/fm-harness.sh" secondmate 2>/dev/null || true)
CONFIG_MODEL=$("$SCRIPT_DIR/fm-harness.sh" secondmate-model 2>/dev/null || true)
CONFIG_EFFORT=$("$SCRIPT_DIR/fm-harness.sh" secondmate-effort 2>/dev/null || true)
Expand Down Expand Up @@ -765,8 +765,11 @@ safe_checkpoint() {
fi
}

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

# record_note: put the required progress note somewhere durable, and - for a
# ship or scout, whose only record of the interrupted reasoning is the
# ship, design, or scout, whose only record of the interrupted reasoning is the
# conversation about to be discarded - into the instructions the replacement
# actually reads. A secondmate's charter is a durable standing document and is
# never rewritten: a secondmate reconciles its own home's records at startup,
Expand All @@ -777,7 +780,7 @@ record_note() {
stamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf '%s\n' "$NOTE" > "$NOTE_FILE"
case "$KIND" in
ship|scout)
ship|design|scout)
cp -p "$RELAUNCH_BRIEF" "$BRIEF_PRIOR" \
|| die "could not preserve task $ID's instructions before recording the progress note"
{
Expand Down Expand Up @@ -806,12 +809,15 @@ do_relaunch() {
resolve_relaunch_profile

case "$KIND" in
ship|scout)
ship|design|scout)
RELAUNCH_BRIEF="$DATA/$ID/brief.md"
[ -f "$RELAUNCH_BRIEF" ] \
|| die "task $ID has no instructions at $RELAUNCH_BRIEF; refusing to relaunch a worker with nothing to work from"
[ "$NOTE_SET" = 1 ] && [ -n "$NOTE" ] \
|| die "relaunch of a $KIND task requires --note (or --note-file): the replacement worker inherits the local copy but none of the conversation, so it must be told what happened"
if [ "$KIND" = design ]; then
adopt_relaunch_design_skills "$META" "$ID" || exit 1
fi
;;
secondmate)
# The charter in the secondmate's own home is its instruction source and
Expand Down
78 changes: 78 additions & 0 deletions bin/fm-design-skills-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash

# Matches the durable manifest's free-text cap (FM_OUTCOME_TEXT_MAX in
# bin/fm-outcome-lib.sh), so a value recorded here can always be published.
DESIGN_SKILLS_FIELD_MAX=240
# Collapses to the same single-line, trimmed, capped shape the durable manifest
# applies, so the emptiness check below sees exactly what would be published.
design_skills_field() { # <resolve-json> <field> -> one meta-safe line
printf '%s\n' "$1" \
| jq -r --arg field "$2" '.[$field] // ""' \
| tr -d '\000-\037\177' \
| sed -e 's/ */ /g' -e 's/^ //' -e 's/ $//' \
| cut -c "1-$DESIGN_SKILLS_FIELD_MAX"
}
design_skill_path() { # <resolve-json> <skill-key> -> exact absolute path
printf '%s\n' "$1" \
| jq -er --arg skill "$2" '.skills[$skill] | select(type == "string" and length > 0)'
}
design_skill_path_is_safe() { # <path>
case "$1" in
/*) ;;
*) return 1 ;;
esac
[ "$(printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177')" = "$1" ]
}
# adopt_relaunch_design_skills: reuse the dispatch pin already recorded for
# this design task. Never call fm-design-skills.sh resolve here; a later
# plugin auto-update must not silently rebind the interview.
adopt_relaunch_design_skills() {
local RELAUNCH_META=$1 ID=$2
local recorded_plugin recorded_version recorded_updated tasktmp dispatch_brief
local binding schema
recorded_plugin=$(fm_meta_get "$RELAUNCH_META" design_skills_plugin)
recorded_version=$(fm_meta_get "$RELAUNCH_META" design_skills_version)
recorded_updated=$(fm_meta_get "$RELAUNCH_META" design_skills_updated)
if [ -z "$recorded_plugin" ] || [ -z "$recorded_version" ] \
|| [ -z "$recorded_updated" ]; then
echo "error: task $ID has no recorded design-skill release; refusing to relaunch rather than resolving a different plugin pin" >&2
return 1
fi
tasktmp=$(fm_meta_get "$RELAUNCH_META" tasktmp)
[ -n "$tasktmp" ] || tasktmp="/tmp/fm-$ID"
dispatch_brief="$tasktmp/brief.md"
if [ ! -f "$dispatch_brief" ] || [ -L "$dispatch_brief" ] || [ ! -r "$dispatch_brief" ]; then
echo "error: task $ID has no dispatch-pinned design brief at $dispatch_brief; refusing to relaunch rather than resolving a different plugin release" >&2
return 1
fi
binding=$(sed -n '/^```json$/{n;p;q;}' "$dispatch_brief")
schema=$(printf '%s\n' "$binding" | jq -r '.schema // empty' 2>/dev/null) || schema=
[ "$schema" = fm-design-skills.dispatch.v1 ] || {
echo "error: task $ID's dispatch-pinned design brief is not a usable skill binding; refusing to relaunch rather than resolving a different plugin release" >&2
return 1
}
DESIGN_SKILLS_PLUGIN=$(design_skills_field "$binding" plugin)
DESIGN_SKILLS_VERSION=$(design_skills_field "$binding" version)
DESIGN_SKILLS_UPDATED=$(design_skills_field "$binding" last_updated)
DESIGN_SKILLS_GRILLING=$(design_skill_path "$binding" grilling) || DESIGN_SKILLS_GRILLING=
DESIGN_SKILLS_DOMAIN_MODELING=$(design_skill_path "$binding" domain_modeling) || DESIGN_SKILLS_DOMAIN_MODELING=
if [ "$DESIGN_SKILLS_PLUGIN" != "$recorded_plugin" ] \
|| [ "$DESIGN_SKILLS_VERSION" != "$recorded_version" ] \
|| [ "$DESIGN_SKILLS_UPDATED" != "$recorded_updated" ] \
|| ! design_skill_path_is_safe "$DESIGN_SKILLS_GRILLING" \
|| ! design_skill_path_is_safe "$DESIGN_SKILLS_DOMAIN_MODELING"; then
echo "error: task $ID's dispatch-pinned design skills do not match its recorded release; refusing to relaunch rather than substituting another plugin pin" >&2
return 1
fi
design_skill_files_readable
}

design_skill_files_readable() {
if [ ! -f "$DESIGN_SKILLS_GRILLING" ] || [ -L "$DESIGN_SKILLS_GRILLING" ] \
|| [ ! -r "$DESIGN_SKILLS_GRILLING" ] \
|| [ ! -f "$DESIGN_SKILLS_DOMAIN_MODELING" ] || [ -L "$DESIGN_SKILLS_DOMAIN_MODELING" ] \
|| [ ! -r "$DESIGN_SKILLS_DOMAIN_MODELING" ]; then
echo "error: a dispatch-pinned mattpocock design skill path disappeared or became unreadable after resolution; refusing instead of silently resolving a different plugin release" >&2
return 1
fi
}
86 changes: 34 additions & 52 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
# transaction; call fm-control rather than this flag directly unless you are
# deliberately re-launching an already-stopped task. Every identity axis -
# backend, kind, project or home, worktree, endpoint - comes from the task's
# validated state/<id>.meta, so --backend, --scout, --secondmate, a project
# validated state/<id>.meta, so --backend, --scout, --design, --secondmate, a project
# positional, and batch pairs are all refused alongside it; only harness,
# model, and effort may change, which is what makes a harness switch one
# ordinary relaunch. It refuses unless the recorded endpoint is positively
Expand Down Expand Up @@ -179,9 +179,12 @@
# config reread generations because the new agent reads the converged files.
# --design records kind=design in the task's meta (interactive ADR deliverable;
# see the design-profile skill) and, from the one bin/fm-design-skills.sh
# resolve that gates the dispatch, records design_skills_plugin=,
# resolve that gates a fresh dispatch, records design_skills_plugin=,
# design_skills_version=, and design_skills_updated= so the auto-updating
# plugin release that informed the interview stays readable after cleanup;
# plugin release that informed the interview stays readable after cleanup.
# A --relaunch of that same design task reuses the recorded release and the
# dispatch-pinned skill paths rather than resolving again
# (docs/fleet-data-contracts.md "The design task's plugin release").
# --scout records kind=scout (report deliverable,
# scratch worktree; see AGENTS.md task lifecycle); --secondmate records
# kind=secondmate and launches in a provisioned firstmate home; the default is kind=ship.
Expand Down Expand Up @@ -557,7 +560,7 @@ esac
# refusal rather than a silently-ignored flag.
if [ "$RELAUNCH" -eq 1 ]; then
[ "$BACKEND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded backend; --backend cannot override it" >&2; exit 1; }
[ "$KIND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded kind; --scout/--secondmate cannot override it" >&2; exit 1; }
[ "$KIND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded kind; --scout/--design/--secondmate cannot override it" >&2; exit 1; }
[ "$MODE_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded delivery mode; --mode cannot override it" >&2; exit 1; }
[ "$YOLO_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded yolo posture; --yolo cannot override it" >&2; exit 1; }
else
Expand Down Expand Up @@ -1978,54 +1981,39 @@ SOURCE_BRIEF=$BRIEF
# resolve call serves as both the dispatch gate and the provenance record, so
# what is recorded is exactly what was verified present at dispatch. Reading the
# plugin later - at cleanup, say - could name a version that only arrived after
# the interview ended, which is worse than recording none. A relaunch resolves
# again, so the recorded value always names this task's most recent dispatch.
# the interview ended, which is worse than recording none. A relaunch of the
# same design task reuses that recorded release and the worker-facing pinned
# paths rather than resolving again
# (docs/fleet-data-contracts.md "The design task's plugin release").
# The resolver's own refusal already names the missing install or skill.
DESIGN_SKILLS_PLUGIN=
DESIGN_SKILLS_VERSION=
DESIGN_SKILLS_UPDATED=
DESIGN_SKILLS_GRILLING=
DESIGN_SKILLS_DOMAIN_MODELING=
DESIGN_SKILLS_BINDING=
# Matches the durable manifest's free-text cap (FM_OUTCOME_TEXT_MAX in
# bin/fm-outcome-lib.sh), so a value recorded here can always be published.
DESIGN_SKILLS_FIELD_MAX=240
# Collapses to the same single-line, trimmed, capped shape the durable manifest
# applies, so the emptiness check below sees exactly what would be published.
design_skills_field() { # <resolve-json> <field> -> one meta-safe line
printf '%s\n' "$1" \
| jq -r --arg field "$2" '.[$field] // ""' \
| tr -d '\000-\037\177' \
| sed -e 's/ */ /g' -e 's/^ //' -e 's/ $//' \
| cut -c "1-$DESIGN_SKILLS_FIELD_MAX"
}
design_skill_path() { # <resolve-json> <skill-key> -> exact absolute path
printf '%s\n' "$1" \
| jq -er --arg skill "$2" '.skills[$skill] | select(type == "string" and length > 0)'
}
design_skill_path_is_safe() { # <path>
case "$1" in
/*) ;;
*) return 1 ;;
esac
[ "$(printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177')" = "$1" ]
}
# shellcheck source=bin/fm-design-skills-lib.sh
. "$SCRIPT_DIR/fm-design-skills-lib.sh"
if [ "$KIND" = design ]; then
DESIGN_SKILLS_RECORD=$("$FM_ROOT/bin/fm-design-skills.sh" resolve) || {
echo "error: design spawn requires the captain-installed mattpocock design skills; do not install or copy them from a worker" >&2
exit 1
}
DESIGN_SKILLS_PLUGIN=$(design_skills_field "$DESIGN_SKILLS_RECORD" plugin)
DESIGN_SKILLS_VERSION=$(design_skills_field "$DESIGN_SKILLS_RECORD" version)
DESIGN_SKILLS_UPDATED=$(design_skills_field "$DESIGN_SKILLS_RECORD" last_updated)
DESIGN_SKILLS_GRILLING=$(design_skill_path "$DESIGN_SKILLS_RECORD" grilling) || DESIGN_SKILLS_GRILLING=
DESIGN_SKILLS_DOMAIN_MODELING=$(design_skill_path "$DESIGN_SKILLS_RECORD" domain_modeling) || DESIGN_SKILLS_DOMAIN_MODELING=
if [ -z "$DESIGN_SKILLS_PLUGIN" ] || [ -z "$DESIGN_SKILLS_VERSION" ] \
|| [ -z "$DESIGN_SKILLS_UPDATED" ] \
|| ! design_skill_path_is_safe "$DESIGN_SKILLS_GRILLING" \
|| ! design_skill_path_is_safe "$DESIGN_SKILLS_DOMAIN_MODELING"; then
echo "error: the installed mattpocock plugin resolved without a usable identity, version, update stamp, and absolute skill paths, so this design task's inputs could not be pinned and recorded; refusing rather than dispatching an untraceable design" >&2
exit 1
if [ "$RELAUNCH" -eq 1 ]; then
adopt_relaunch_design_skills "$RELAUNCH_META" "$ID" || exit 1
else
DESIGN_SKILLS_RECORD=$("$FM_ROOT/bin/fm-design-skills.sh" resolve) || {
echo "error: design spawn requires the captain-installed mattpocock design skills; do not install or copy them from a worker" >&2
exit 1
}
DESIGN_SKILLS_PLUGIN=$(design_skills_field "$DESIGN_SKILLS_RECORD" plugin)
DESIGN_SKILLS_VERSION=$(design_skills_field "$DESIGN_SKILLS_RECORD" version)
DESIGN_SKILLS_UPDATED=$(design_skills_field "$DESIGN_SKILLS_RECORD" last_updated)
DESIGN_SKILLS_GRILLING=$(design_skill_path "$DESIGN_SKILLS_RECORD" grilling) || DESIGN_SKILLS_GRILLING=
DESIGN_SKILLS_DOMAIN_MODELING=$(design_skill_path "$DESIGN_SKILLS_RECORD" domain_modeling) || DESIGN_SKILLS_DOMAIN_MODELING=
if [ -z "$DESIGN_SKILLS_PLUGIN" ] || [ -z "$DESIGN_SKILLS_VERSION" ] \
|| [ -z "$DESIGN_SKILLS_UPDATED" ] \
|| ! design_skill_path_is_safe "$DESIGN_SKILLS_GRILLING" \
|| ! design_skill_path_is_safe "$DESIGN_SKILLS_DOMAIN_MODELING"; then
echo "error: the installed mattpocock plugin resolved without a usable identity, version, update stamp, and absolute skill paths, so this design task's inputs could not be pinned and recorded; refusing rather than dispatching an untraceable design" >&2
exit 1
fi
fi
DESIGN_SKILLS_BINDING=$(jq -cn \
--arg plugin "$DESIGN_SKILLS_PLUGIN" \
Expand Down Expand Up @@ -3123,13 +3111,7 @@ TASK_TMP="/tmp/fm-$ID"
mkdir -p "$TASK_TMP/gotmp"

if [ "$KIND" = design ]; then
if [ ! -f "$DESIGN_SKILLS_GRILLING" ] || [ -L "$DESIGN_SKILLS_GRILLING" ] \
|| [ ! -r "$DESIGN_SKILLS_GRILLING" ] \
|| [ ! -f "$DESIGN_SKILLS_DOMAIN_MODELING" ] || [ -L "$DESIGN_SKILLS_DOMAIN_MODELING" ] \
|| [ ! -r "$DESIGN_SKILLS_DOMAIN_MODELING" ]; then
echo "error: a dispatch-pinned mattpocock design skill path disappeared or became unreadable after resolution; refusing instead of silently resolving a different plugin release" >&2
exit 1
fi
design_skill_files_readable || exit 1
DESIGN_DISPATCH_BRIEF="$TASK_TMP/brief.md"
DESIGN_DISPATCH_BRIEF_TMP="$TASK_TMP/.brief.${BASHPID:-$$}"
{
Expand Down Expand Up @@ -3722,7 +3704,7 @@ SPAWN_META_PATH=$SPAWN_META_TMP
preserve_relaunch_meta() {
awk -F= '
BEGIN {
split("window endpoint_task_id worktree project harness kind mode yolo design_skills_plugin design_skills_version design_skills_updated tasktmp model effort busy_gen spawn_gen traceparent backend herdr_session herdr_workspace_id herdr_tab_id herdr_pane_id zellij_session zellij_tab_id zellij_pane_id orca_worktree_id terminal cmux_workspace_id cmux_surface_id home projects control_relaunch_tx", keys, " ")
split("window endpoint_task_id worktree branch project harness kind mode yolo design_skills_plugin design_skills_version design_skills_updated tasktmp model effort busy_gen spawn_gen traceparent backend herdr_session herdr_workspace_id herdr_tab_id herdr_pane_id zellij_session zellij_tab_id zellij_pane_id orca_worktree_id terminal cmux_workspace_id cmux_surface_id home projects control_relaunch_tx", keys, " ")
for (i in keys) owned[keys[i]] = 1
}
!($1 in owned)
Expand Down
Loading
Loading