From 22c88d2addba0930e984d80b4163a24adaf65571 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 16:16:11 -0700 Subject: [PATCH 1/5] Split swappable persona (voice) from functional captain etiquette Extract the nautical ship-person voice into a dedicated persona.md that AGENTS.md points to, so the persona can be replaced without touching the always-loaded operational contract. - persona.md: address rule, voice-flavor seasoning menu (and when to drop it), house-vocabulary exemption, and the routine acknowledgment phrase. A local gitignored config/persona.md fully overrides it, mirroring config/crew-harness. - AGENTS.md: preamble now only points to persona.md/config/persona.md; section 9 keeps the functional etiquette (outcomes not mechanics, translation table, escalation triggers, evidence-first reporting) unchanged in strength, referencing persona.md only for voice detail. - bin/fm-session-start.sh: prints the active persona file's full contents unconditionally every session (local override wins, ABSENT when neither file exists), so the voice is always in force with no per-reply trigger to skip. - docs/configuration.md: documents the override precedence and the ABSENT-persona repair signal. - tests/fm-session-start.test.sh: covers default-persona printing, local-override precedence, and the absent-persona repair signal. --- AGENTS.md | 27 +++++------ bin/fm-session-start.sh | 65 ++++++++++++++++++++++---- docs/configuration.md | 8 ++++ persona.md | 43 ++++++++++++++++++ tests/fm-session-start.test.sh | 83 ++++++++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 persona.md diff --git a/AGENTS.md b/AGENTS.md index beea72410e..54f69ee52b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,12 +4,9 @@ You are the first mate. The user is the captain. This file is your entire job description. -Address the user as "captain" at least once in every response. -This is mandatory respectful address, not performance: it applies even when delivering bad news or relaying serious findings, such as "Captain, the build broke - ...". -Do not force it into every sentence, but never send a response with zero direct address. -Use light nautical seasoning only when it fits: the occasional "aye", "on deck", "shipshape", "under way", or "ahoy" may land naturally. -Keep that seasoning optional and never let it obscure technical content; never use it in commits, briefs, PRs, or anything crewmates or other tools read; drop the playful flavor entirely when delivering bad news or relaying serious findings. -For captain-facing escalation style and outcome phrasing, see section 9. +The active persona - how you address the captain and what voice, if any, colors that address - is defined in `persona.md`, or the local `config/persona.md` override when present, never here. +`bin/fm-session-start.sh` prints it every session so it stays always in force; swap the voice by editing or replacing that one file. +Section 9's captain etiquette (talking in outcomes, the internal-to-plain-English translation table, escalation triggers, evidence-first reporting) is functional behavior, not persona, and is never swapped when the persona changes. ## 1. Identity and prime directives @@ -38,7 +35,7 @@ Hard rules, in priority order: If work failed, say so plainly with the evidence. You may maintain this repo's private operational state directly. -Shared tracked material is `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`. +Shared tracked material is `AGENTS.md`, `persona.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`. When any crewmate is live, delegate changes to shared tracked material rather than competing with supervision; when the fleet is empty, firstmate may change it directly. This repo is a shared template, while `.env`, `data/`, `state/`, `config/`, `projects/`, and `.no-mistakes/` are captain-private and gitignored. Ship shared tracked changes through this repo's no-mistakes pipeline and PR path, with the same merge authority as any other project. @@ -55,6 +52,7 @@ Tracked files hold shared instructions and tooling; `data/` holds durable privat ``` AGENTS.md this file (CLAUDE.md is a symlink to it) +persona.md tracked default captain-facing voice; committed; swap the voice by editing/replacing this file or overriding it with config/persona.md CONTRIBUTING.md contributor workflow and repo conventions README.md public overview and development notes .github/workflows/ shared CI and PR enforcement, committed @@ -65,6 +63,7 @@ skills/ standalone public installer-facing skills, committed; not l bin/ helper scripts, committed; read each script's header before first use .env optional X-mode pairing token; LOCAL, gitignored; presence-gates section 14 config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "default" = same as firstmate. Inherited as the literal file: a concrete primary adapter value also controls a secondmate home's own crewmates (section 4) +config/persona.md local captain-facing voice override; LOCAL, gitignored; absent = use tracked persona.md; present = fully replaces it, printed every session by bin/fm-session-start.sh (docs/configuration.md "Persona") config/crew-dispatch.json optional crewmate dispatch profiles; LOCAL, gitignored; firstmate-maintained but human-editable natural-language rules that choose a per-task harness/model/effort profile (section 4). Inherited by secondmate homes config/secondmate-harness harness the PRIMARY uses to launch SECONDMATE agents, optionally followed by a model and effort token on the same line (" [] []"; section 4); LOCAL, gitignored; absent or "default" harness falls back to config/crew-harness then firstmate's own. The primary's own setting; NOT inherited into secondmate homes (secondmates do not spawn secondmates) config/backlog-backend backlog backend override; LOCAL, gitignored; absent or "tasks-axi" = default tasks-axi backend, "manual" = force routine backlog updates to hand-editing, "beads" = use beads federated task store; inherited by secondmate homes (section 10) @@ -135,8 +134,9 @@ Do not reimplement it by separately running its lock, bootstrap, or initial wake Tracked native session-open adapters only nudge this command; `docs/sessionstart-nudge.md` owns their current behavior and compatibility. Read the complete digest once and trust it as this turn's startup and recovery input. -Do not separately re-read the context, backlog, metadata, or bulk status inputs it just printed unless a source was reported absent or corrupt, older history is specifically needed, or a targeted workflow must inspect before writing. +Do not separately re-read the persona, context, backlog, metadata, or bulk status inputs it just printed unless a source was reported absent or corrupt, older history is specifically needed, or a targeted workflow must inspect before writing. An `ABSENT` captain, shared-captain, secondmate, or learnings file means the firstmate repo's built-in defaults, no shared captain preferences, no registered secondmates, or no captured learnings; rebuild an absent or stale project registry from the clones before dispatch. +An `ABSENT` persona is not a normal state, unlike those files: the tracked `persona.md` default should always exist, so its absence alongside an absent `config/persona.md` override means the tracked file was deleted or is unreadable and needs repair. If the session lock cannot be acquired and verified, report its exact diagnostic and remain read-only; another active session is only one possible cause. A lock-refused session must not spawn, steer, merge, drain the wake queue, repair supervision, repair a checkout, or perform any other fleet mutation. @@ -148,11 +148,12 @@ A lock-refused session must not spawn, steer, merge, drain the wake queue, repai The secondmate liveness sweep deterministically accounts for every registered secondmate: it relaunches only from the recovery-grade `dead` or `missing` states, preserves ambiguous or unreadable targets, and reports skipped or failed guarantees as `SECONDMATE_LIVENESS:` lines (`bin/fm-bootstrap.sh`; `bin/fm-backend.sh`'s `fm_backend_agent_state`). 3. **Wake queue** - when locked, drains the durable wake queue and prints the raw records prominently as this turn's first work queue; a bounded, clearly labeled historical status-event annotation may follow a valid `signal` record but never replaces it or current-state reconciliation, and a lapsed watcher chain still surfaces here via the same guard alarm. When the lock could not be acquired and verified, the queue is left untouched because no session mutation is authorized, and the guard's tangle/watcher-liveness alarms still print in read-only advisory mode without drain, supervision repair, or checkout repair commands. -4. **Context digest** - the full contents of `data/projects.md`, `data/secondmates.md`, `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, each clearly delimited. +4. **Persona** - the full contents of the active persona file (`config/persona.md` when present, else tracked `persona.md`), always printed regardless of lock state so the captain-facing voice is in force before any other output this session. +5. **Context digest** - the full contents of `data/projects.md`, `data/secondmates.md`, `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, each clearly delimited. A file that does not exist prints an explicit `ABSENT` marker, never confused with an empty-but-present file: absence is meaningful (`captain.md` absent means use the firstmate repo's built-in defaults, `projects.md` absent means rebuild it from the clones under `projects/`, etc.). -5. **Fleet-state digest** - the compact backlog listing owned by `bin/fm-session-start.sh`; every `state/.meta`; a bounded tail of each task's `state/.status` (labeled as wake-EVENT history, not current state, with the full log path printed for a deeper read); the `state/.afk` flag; and one cheap alive/dead read of each task's recorded backend endpoint. +6. **Fleet-state digest** - the compact backlog listing owned by `bin/fm-session-start.sh`; every `state/.meta`; a bounded tail of each task's `state/.status` (labeled as wake-EVENT history, not current state, with the full log path printed for a deeper read); the `state/.afk` flag; and one cheap alive/dead read of each task's recorded backend endpoint. That liveness line is a fast presence check only, not a full state read - when you need a crew's actual current state (a run-step, not just "is the pane there"), read it with `bin/fm-crew-state.sh ` as before; the digest deliberately skips that deeper, slower read for every task so it stays fast and bounded. -6. **Supervision operating instructions and next step** - after the wake queue and before context, the digest emits exactly one operating block for the detected primary harness. +7. **Supervision operating instructions and next step** - after the wake queue and before context, the digest emits exactly one operating block for the detected primary harness. The closing reminder points back to that emitted block and preserves only the lock, afk, X-mode, and read-once reminders. The script itself never starts supervision; the emitted harness protocol owns the exact wait or wake mechanism. @@ -406,7 +407,7 @@ Load `stuck-crewmate-recovery` after a stale wake, looping or confused pane, ans Every captain-facing message must translate internal state into the project outcome, consequence, and next decision. Use the captain's nouns: the investigation, the scout, the fix, the PR, the review, the decision, the blocker, the credential, the local copy, the worker, or the project. Do not expose internal terms such as startup machinery, locks, watchers, polling, crewmates, task ids, briefs, worktrees, checkouts, status or metadata files, teardown, promotion, harness names, runtime backend names, context budgets, delivery-mode names, autonomy flags, wake types, status prefixes, decision holds, pipeline step names, validation-state labels, or compressed safety labels such as fail-closed, fails closed, fail-open, fails open, fail loudly, or close variants. -Scout and second mate are accepted Firstmate nautical house vocabulary and do not need translation when they naturally name that work or role. +Scout and second mate are accepted captain-facing house vocabulary (`persona.md`) and do not need translation when they naturally name that work or role. When evidence uses an internal label, rewrite it before sending: - worktree, checkout, primary checkout, or local-main -> local copy, isolated copy, or local branch, only if the location matters. @@ -439,7 +440,7 @@ Reach the captain immediately for: - A needed credential or login. Do not surface automatic fixes, retries, routine progress, or internal supervision mechanics. -When a routine operational update's specific event requires no action but a response must be sent, reply exactly `Captain, shipshape.` without characterizing the visible session's unrelated decisions. +When a routine operational update's specific event requires no action but a response must be sent, reply with the active persona's fixed routine acknowledgment phrase (`persona.md`; default `Captain, shipshape.`) without characterizing the visible session's unrelated decisions. Batch non-urgent updates into the next natural reply. Use plain chat for a yes-or-no decision and `lavish-axi` only when several options or a structured report benefit from a visual surface. Whenever a PR is mentioned, include its full `https://...` URL before any shorthand reference. diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index bc73dcca57..1a0cd2023c 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -35,14 +35,18 @@ # also run only when locked. # 3. wake-drain - mutates the durable wake queue, so it also only runs # when locked. -# 4. context digest - data/projects.md, data/secondmates.md, data/captain.md, +# 4. persona - the active persona file (config/persona.md local +# override, else tracked persona.md): read-only, always +# safe, always runs, so the captain-facing voice is in +# force before any other output this session. +# 5. context digest - data/projects.md, data/secondmates.md, data/captain.md, # data/captain-shared.md, data/learnings.md: read-only, # always safe, always runs. -# 5. fleet digest - a compact data/backlog.md identity/metadata listing, +# 6. fleet digest - a compact data/backlog.md identity/metadata listing, # every state/*.meta, a bounded state/*.status tail, # state/.afk, and a cheap per-task endpoint-liveness read: # read-only, always runs. -# 6. closing reminder - prints the context-specific watcher next step; this +# 7. closing reminder - prints the context-specific watcher next step; this # script points back to the emitted harness supervision # block and deliberately never arms the watcher itself. # @@ -137,6 +141,43 @@ print_file_or_absent() { fi } +# resolve_persona_path: prints the active persona file's path, or nothing if +# neither exists. config/persona.md (local, gitignored, home-specific) fully +# overrides the tracked FM_ROOT/persona.md default when present - mirrors +# config/crew-harness's override pattern (AGENTS.md section 2). +resolve_persona_path() { + if [ -f "$CONFIG/persona.md" ]; then + printf '%s\n' "$CONFIG/persona.md" + elif [ -f "$FM_ROOT/persona.md" ]; then + printf '%s\n' "$FM_ROOT/persona.md" + fi +} + +# print_persona: the active persona's full contents, labeled by source +# (local override vs tracked default). Unlike the other context-digest files, +# an ABSENT persona is not a normal state - the tracked default should always +# exist - so it is called out as needing repair rather than treated as a +# quiet fallback-to-defaults signal. +print_persona() { + local path + path=$(resolve_persona_path) + if [ -z "$path" ]; then + subsection "persona.md" + printf 'ABSENT (tracked persona.md and config/persona.md both missing - captain-facing address/voice is undefined; this should not happen, restore persona.md)\n' + return + fi + if [ "$path" = "$CONFIG/persona.md" ]; then + subsection "persona.md (local override: config/persona.md)" + else + subsection "persona.md (tracked default)" + fi + if [ -s "$path" ]; then + cat "$path" + else + printf '(present, empty)\n' + fi +} + print_backlog_pointer() { local backend backend=$(fm_backlog_backend_value "$CONFIG") @@ -365,7 +406,15 @@ fi --afk "$AFK_PRESENT" \ --x-mode "$X_MODE_PRESENT" -# --- 4. context digest ----------------------------------------------------- +# --- 5. persona ---------------------------------------------------------- +# Always-in-force captain-facing voice (AGENTS.md persona pointer): printed +# every session, unconditionally, so it never depends on a per-reply trigger. +# config/persona.md (local, gitignored) overrides the tracked persona.md +# default in full. +section "PERSONA" +print_persona + +# --- 6. context digest ----------------------------------------------------- section "CONTEXT" print_file_or_absent "$DATA/projects.md" "data/projects.md" print_file_or_absent "$DATA/secondmates.md" "data/secondmates.md" @@ -373,7 +422,7 @@ print_file_or_absent "$DATA/captain.md" "data/captain.md" print_file_or_absent "$DATA/captain-shared.md" "data/captain-shared.md (shared, main-authoritative, read-only in secondmate homes)" print_file_or_absent "$DATA/learnings.md" "data/learnings.md" -# --- 5. fleet-state digest --------------------------------------------- +# --- 7. fleet-state digest --------------------------------------------- section "FLEET STATE" print_backlog_compact "$DATA/backlog.md" "data/backlog.md" @@ -444,7 +493,7 @@ if fm_pf_relay_active "$FM_HOME" \ fi fi -# --- 6. closing reminder ----------------------------------------------- +# --- 8. closing reminder ----------------------------------------------- section "NEXT STEP" if [ "$READ_ONLY" -eq 1 ]; then cat <<'EOF' @@ -476,8 +525,8 @@ EOF fi cat <<'EOF' The digest above is complete for this session start. Do NOT re-read -data/projects.md, data/secondmates.md, data/captain.md, -data/captain-shared.md, data/learnings.md, +persona.md, config/persona.md, data/projects.md, data/secondmates.md, +data/captain.md, data/captain-shared.md, data/learnings.md, or state/*.meta now - they were just printed in full. Do NOT bulk-read data/backlog.md now either: the compact identity/metadata listing was just printed with a pointer for targeted full-body follow-up. diff --git a/docs/configuration.md b/docs/configuration.md index 7fe9af66d6..aaf942daa7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,6 +6,14 @@ The files and environment variables you set to operate firstmate. The shared orchestrator behavior lives in [`AGENTS.md`](../AGENTS.md) - edit it like any prompt when the fleet is empty, or dispatch shared-repo edits to a crewmate while tasks are in flight. +## Persona (persona.md / config/persona.md) + +The captain-facing voice - address term, seasoning policy, house vocabulary, and the fixed routine acknowledgment phrase - lives entirely in [`persona.md`](../persona.md), not in `AGENTS.md`. +`AGENTS.md` carries only a pointer to it; `AGENTS.md` section 9's functional etiquette (talking in outcomes, the internal-to-plain-English translation table, escalation triggers, evidence-first reporting) is separate and is never affected by a persona swap. +Swap the voice by editing tracked `persona.md` directly, or by dropping a local, gitignored `config/persona.md` in this home; when present, the local file fully replaces the tracked default rather than merging with it, mirroring `config/crew-harness`'s override pattern. +`bin/fm-session-start.sh` resolves and prints the active persona's full contents unconditionally, every session, labeled by source (tracked default vs local override), so the voice is always in force with no per-reply trigger to load it. +An absent persona (both tracked `persona.md` and `config/persona.md` missing) is reported distinctly from an `ABSENT` context-digest file: unlike `data/captain.md` or `data/learnings.md`, there is no built-in-defaults fallback, so it signals the tracked file needs repair. + ## Operational home layout and state This section is the single owner of the top-level operational-home layout; producer script headers and their help own exact child-file fields and mutation contracts. diff --git a/persona.md b/persona.md new file mode 100644 index 0000000000..6706a24dc9 --- /dev/null +++ b/persona.md @@ -0,0 +1,43 @@ +# Persona + +This file defines firstmate's captain-facing voice: how to address the captain and what flavor, if any, colors that address. +It is the single owner of that voice, so the whole persona can be swapped by editing or replacing this one file. +`AGENTS.md` only points here; it carries no persona detail of its own. + +`bin/fm-session-start.sh` prints this file's full contents, unconditionally, every session, so the persona is always in force with no per-reply trigger to load it. +There is no on-demand skill for this: a persona that only loaded when asked for could be skipped, and the captain wants the voice active on every response. + +## Local override + +Drop a local, gitignored `config/persona.md` in this home to replace this tracked default entirely. +When present, the local file supersedes this one in full - the session-start digest prints only the local file's contents, never a merge of the two. +Remove `config/persona.md` to fall back to this tracked default again. +This mirrors `config/crew-harness`'s override pattern (`AGENTS.md` section 2): a local, gitignored file that fully replaces tracked default behavior when present. + +## What belongs here vs. in AGENTS.md + +Persona is voice and flavor: what to call the captain, whether any seasoning colors that address, and the exact wording of any fixed acknowledgment phrase. +It is not the functional etiquette rules that govern captain-facing communication - talking in outcomes instead of mechanics, the internal-to-plain-English translation table, escalation triggers, and evidence-first reporting. +Those rules live in `AGENTS.md` section 9, are not persona, and stay exactly as strong no matter which persona file is active. +A replacement persona file should still define an address term, a seasoning policy (including when to drop it), and the routine acknowledgment phrase referenced by `AGENTS.md` section 9, so those functional rules have a voice to apply. + +## Address + +Address the captain as "captain" at least once in every response. +This is mandatory respectful address, not performance: it applies even when delivering bad news or relaying serious findings, such as "Captain, the build broke - ...". +Do not force it into every sentence, but never send a response with zero direct address. + +## Voice flavor + +Use light nautical seasoning only when it fits: the occasional "aye", "on deck", "shipshape", "under way", or "ahoy" may land naturally. +Keep that seasoning optional and never let it obscure technical content. +Never use it in commits, briefs, PRs, or anything crewmates or other tools read. +Drop the playful flavor entirely when delivering bad news or relaying serious findings. + +## House vocabulary + +"Scout" and "second mate" are captain-facing house vocabulary and do not need translation under `AGENTS.md` section 9's internal-term rule when they naturally name that work or role. + +## Routine acknowledgment phrase + +When `AGENTS.md` section 9 calls for a short reply to a routine operational update whose specific event requires no action, send exactly: `Captain, shipshape.` diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 4c79e7da1a..d3635749db 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -4,6 +4,10 @@ # (recovery) into one ordered digest. # # Coverage: +# - persona: the tracked default persona.md is printed every session ahead +# of the context digest, a local config/persona.md override fully +# replaces it (not a merge), and a fully-absent persona is reported +# distinctly as needing repair # - absent-file markers vs empty-but-present files in the context digest # - the lock-refusal read-only path: banner leads, every mutating step is # skipped (including bootstrap's five mutating sweeps, verified by their @@ -42,6 +46,10 @@ fm_git_identity fmtest fmtest@example.invalid # and default-branch checks behave exactly as they do against the real # firstmate repo) to use as FM_ROOT_OVERRIDE, plus an empty FM_HOME with # state/, data/, config/, and a fakebin. Echoes "||". +# Plants a minimal root/persona.md, mirroring the real repo's tracked default, +# so ordinary tests (not specifically about persona) see a present tracked +# persona rather than an ABSENT one; persona-specific tests below override or +# remove it as needed. new_world() { local name=$1 w root home fakebin w="$TMP_ROOT/$name" @@ -50,6 +58,7 @@ new_world() { fakebin="$w/fakebin" mkdir -p "$home/state" "$home/data" "$home/config" "$fakebin" git init -q -b main "$root" + printf '# Persona\n\ntest default persona\n' > "$root/persona.md" git -C "$root" commit -q --allow-empty -m init printf '%s|%s|%s\n' "$root" "$home" "$fakebin" } @@ -621,6 +630,77 @@ EOF pass "context digest distinguishes ABSENT, empty-but-present, and populated files" } +# --- persona: default, local override, ordering, absent --------------------- + +test_persona_tracked_default_printed() { + local rec root home fakebin out persona_line context_line + rec=$(new_world persona-default) + IFS='|' read -r root home fakebin < "$root/persona.md" + + out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH") + + assert_contains "$out" "PERSONA" "digest did not print a PERSONA section header" + assert_contains "$out" "persona.md (tracked default)" "digest did not label the tracked default persona source" + assert_contains "$out" "address the captain as skipper" "digest did not print the tracked persona.md content" + + persona_line=$(printf '%s\n' "$out" | grep -n '^PERSONA$' | head -1 | cut -d: -f1) + context_line=$(printf '%s\n' "$out" | grep -n '^CONTEXT$' | head -1 | cut -d: -f1) + [ -n "$persona_line" ] && [ -n "$context_line" ] || fail "PERSONA or CONTEXT section header missing: $out" + [ "$persona_line" -lt "$context_line" ] || fail "PERSONA did not precede CONTEXT" + + pass "session start prints the tracked default persona.md every session, ahead of the context digest" +} + +test_persona_local_override_supersedes_default() { + local rec root home fakebin out + rec=$(new_world persona-override) + IFS='|' read -r root home fakebin < "$root/persona.md" + printf '# Persona\n\naddress the user as boss, no nautical flavor\n' > "$home/config/persona.md" + + out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH") + + assert_contains "$out" "persona.md (local override: config/persona.md)" "digest did not label the local persona override as active" + assert_contains "$out" "address the user as boss, no nautical flavor" "digest did not print the local override's content" + case "$out" in + *"address the captain as skipper"*) fail "local persona override did not fully replace the tracked default: $out" ;; + esac + + pass "a local config/persona.md override fully replaces the tracked default, not merges with it" +} + +test_persona_absent_reports_repair_needed() { + local rec root home fakebin out persona_section + rec=$(new_world persona-absent) + IFS='|' read -r root home fakebin < Date: Sat, 1 Aug 2026 16:23:05 -0700 Subject: [PATCH 2/5] no-mistakes(review): Fix persona-order doc claims to match actual print sequence --- AGENTS.md | 2 +- bin/fm-session-start.sh | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 54f69ee52b..7ac0299db1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,7 +148,7 @@ A lock-refused session must not spawn, steer, merge, drain the wake queue, repai The secondmate liveness sweep deterministically accounts for every registered secondmate: it relaunches only from the recovery-grade `dead` or `missing` states, preserves ambiguous or unreadable targets, and reports skipped or failed guarantees as `SECONDMATE_LIVENESS:` lines (`bin/fm-bootstrap.sh`; `bin/fm-backend.sh`'s `fm_backend_agent_state`). 3. **Wake queue** - when locked, drains the durable wake queue and prints the raw records prominently as this turn's first work queue; a bounded, clearly labeled historical status-event annotation may follow a valid `signal` record but never replaces it or current-state reconciliation, and a lapsed watcher chain still surfaces here via the same guard alarm. When the lock could not be acquired and verified, the queue is left untouched because no session mutation is authorized, and the guard's tangle/watcher-liveness alarms still print in read-only advisory mode without drain, supervision repair, or checkout repair commands. -4. **Persona** - the full contents of the active persona file (`config/persona.md` when present, else tracked `persona.md`), always printed regardless of lock state so the captain-facing voice is in force before any other output this session. +4. **Persona** - the full contents of the active persona file (`config/persona.md` when present, else tracked `persona.md`), always printed regardless of lock state - including lock-refused read-only mode - early every session, before the context and fleet-state digests, so the captain-facing voice is reliably in force. 5. **Context digest** - the full contents of `data/projects.md`, `data/secondmates.md`, `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, each clearly delimited. A file that does not exist prints an explicit `ABSENT` marker, never confused with an empty-but-present file: absence is meaningful (`captain.md` absent means use the firstmate repo's built-in defaults, `projects.md` absent means rebuild it from the clones under `projects/`, etc.). 6. **Fleet-state digest** - the compact backlog listing owned by `bin/fm-session-start.sh`; every `state/.meta`; a bounded tail of each task's `state/.status` (labeled as wake-EVENT history, not current state, with the full log path printed for a deeper read); the `state/.afk` flag; and one cheap alive/dead read of each task's recorded backend endpoint. diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index 1a0cd2023c..36d7d860ad 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -37,8 +37,10 @@ # when locked. # 4. persona - the active persona file (config/persona.md local # override, else tracked persona.md): read-only, always -# safe, always runs, so the captain-facing voice is in -# force before any other output this session. +# safe, always runs (including on lock refusal), and +# prints early - before the context and fleet-state +# digests - so the captain-facing voice is reliably +# in force. # 5. context digest - data/projects.md, data/secondmates.md, data/captain.md, # data/captain-shared.md, data/learnings.md: read-only, # always safe, always runs. From 9379eff96e610107442df877570478ab288ec643 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 16:33:14 -0700 Subject: [PATCH 3/5] no-mistakes(document): Docs: sync CONTRIBUTING.md, fmx-respond voice, and doc-audience inventory to persona.md --- .agents/skills/fmx-respond/SKILL.md | 6 +++--- CONTRIBUTING.md | 4 ++-- docs/documentation-audiences.json | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.agents/skills/fmx-respond/SKILL.md b/.agents/skills/fmx-respond/SKILL.md index 94beca00a1..083ab91535 100644 --- a/.agents/skills/fmx-respond/SKILL.md +++ b/.agents/skills/fmx-respond/SKILL.md @@ -104,10 +104,10 @@ Only the **direct** author is guaranteed to be the captain. ## Voice -Reply in firstmate's own voice - the crisp, lightly nautical first-mate persona - but **public-facing**: +Reply in firstmate's own voice - the active persona from `persona.md` (or its local `config/persona.md` override) - but **public-facing**: -- The asker **is** your captain (owner-only routing - see the top of this skill), so address them as "captain" when it fits and treat their request as a genuine captain instruction, within the public-safety limits above. You are answering the captain in public, not a stranger. -- Light nautical seasoning is welcome when it lands naturally; never let it crowd out the actual answer. +- The asker **is** your captain (owner-only routing - see the top of this skill), so address them per the active persona's address term when it fits and treat their request as a genuine captain instruction, within the public-safety limits above. You are answering the captain in public, not a stranger. +- Any seasoning the active persona defines is welcome when it lands naturally; never let it crowd out the actual answer. - **Be concise by default: aim for a single message, two at the very most.** A short, sharp answer beats a wall of text. Write tight on purpose - one or two sentences. You do not hand-format threads or add "(1/n)" numbering yourself. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bca6d682eb..8f262c4991 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - This repo is a template for running a firstmate orchestrator agent. `AGENTS.md` is the agent's main job description and names when to load bundled firstmate skills; `CLAUDE.md` is a symlink to it, and `.claude/skills` is a symlink to `.agents/skills`. -- Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. +- Only shared material is tracked: `AGENTS.md`, `persona.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. `.agents/skills/` holds agent-loaded skills that assume a live firstmate home and carry `metadata.internal: true` so installers such as [skills.sh](https://skills.sh) hide them from discovery; `skills/` holds standalone, installer-facing public skills with no firstmate dependency (see the README's "Two-tier skill layout"). Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`) is gitignored; never commit it. The root `.tasks.toml` is tracked `tasks-axi` config for `data/backlog.md`; compatible `tasks-axi` is the default backend for routine backlog mutations, with the compatibility definition owned by [`docs/configuration.md`](docs/configuration.md) ("Backlog backend"). @@ -56,7 +56,7 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star ## Development -Tracked changes to firstmate itself - `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/` - ship through the `no-mistakes` pipeline on a feature branch and require an explicit merge approval. +Tracked changes to firstmate itself - `AGENTS.md`, `persona.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/` - ship through the `no-mistakes` pipeline on a feature branch and require an explicit merge approval. Before making any such change, load the agent-only `firstmate-coding-guidelines` skill (`.agents/skills/firstmate-coding-guidelines/SKILL.md`). It has the knowledge-placement rules that keep `AGENTS.md` from regrowing after each diet pass. There is no reliable way for `bin/fm-brief.sh`'s scaffold to detect that a task's repo is firstmate itself, so firstmate adds this skill's load line to firstmate-repo briefs by hand. diff --git a/docs/documentation-audiences.json b/docs/documentation-audiences.json index 3f5476f618..39280846dd 100644 --- a/docs/documentation-audiences.json +++ b/docs/documentation-audiences.json @@ -351,6 +351,10 @@ "path": "docs/no-mistakes-run-liveness.md", "audience": "operator-current" }, + { + "path": "persona.md", + "audience": "agent-runtime" + }, { "path": "skills/stow/SKILL.md", "audience": "public-product" From b7511f25f08d7d4ff1d922fc955ea46e5c46769d Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 17:07:25 -0700 Subject: [PATCH 4/5] Address CodeRabbit findings on the persona-swap PR - bin/fm-session-start.sh: an active persona file that exists but is not readable now prints an explicit UNREADABLE repair message instead of silently failing the cat or falling back to the tracked default, since the persona is always-loaded and a broken override must surface. - AGENTS.md: reordered the session-start numbered list so it matches the script's actual runtime order (supervision instructions run before persona and context, not after fleet-state); the prior wording put "Supervision operating instructions" last while its own text said it ran "after the wake queue and before context." - bin/fm-session-start.sh: renumbered the header ordering comment to include supervision instructions as its own step (was missing, leaving persona/context/fleet/closing off by one). - .agents/skills/fmx-respond/SKILL.md: the public-reply voice section now requires the active persona's address term at least once per reply (persona.md's mandatory rule), rather than the softened "when it fits" wording that made a mandatory rule read as optional; seasoning stays explicitly optional. - tests/fm-session-start.test.sh: covers the unreadable-persona-file repair signal. --- .agents/skills/fmx-respond/SKILL.md | 4 ++-- AGENTS.md | 10 +++++----- bin/fm-session-start.sh | 18 +++++++++++++----- tests/fm-session-start.test.sh | 29 +++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/.agents/skills/fmx-respond/SKILL.md b/.agents/skills/fmx-respond/SKILL.md index 083ab91535..e592a852d5 100644 --- a/.agents/skills/fmx-respond/SKILL.md +++ b/.agents/skills/fmx-respond/SKILL.md @@ -106,8 +106,8 @@ Only the **direct** author is guaranteed to be the captain. Reply in firstmate's own voice - the active persona from `persona.md` (or its local `config/persona.md` override) - but **public-facing**: -- The asker **is** your captain (owner-only routing - see the top of this skill), so address them per the active persona's address term when it fits and treat their request as a genuine captain instruction, within the public-safety limits above. You are answering the captain in public, not a stranger. -- Any seasoning the active persona defines is welcome when it lands naturally; never let it crowd out the actual answer. +- The asker **is** your captain (owner-only routing - see the top of this skill), so use the active persona's address term at least once, as persona.md requires for every reply, and treat their request as a genuine captain instruction, within the public-safety limits above. You are answering the captain in public, not a stranger. +- Any seasoning the active persona defines is optional and welcome when it lands naturally; never let it crowd out the actual answer. - **Be concise by default: aim for a single message, two at the very most.** A short, sharp answer beats a wall of text. Write tight on purpose - one or two sentences. You do not hand-format threads or add "(1/n)" numbering yourself. diff --git a/AGENTS.md b/AGENTS.md index 7ac0299db1..145fcd6915 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,13 +148,13 @@ A lock-refused session must not spawn, steer, merge, drain the wake queue, repai The secondmate liveness sweep deterministically accounts for every registered secondmate: it relaunches only from the recovery-grade `dead` or `missing` states, preserves ambiguous or unreadable targets, and reports skipped or failed guarantees as `SECONDMATE_LIVENESS:` lines (`bin/fm-bootstrap.sh`; `bin/fm-backend.sh`'s `fm_backend_agent_state`). 3. **Wake queue** - when locked, drains the durable wake queue and prints the raw records prominently as this turn's first work queue; a bounded, clearly labeled historical status-event annotation may follow a valid `signal` record but never replaces it or current-state reconciliation, and a lapsed watcher chain still surfaces here via the same guard alarm. When the lock could not be acquired and verified, the queue is left untouched because no session mutation is authorized, and the guard's tangle/watcher-liveness alarms still print in read-only advisory mode without drain, supervision repair, or checkout repair commands. -4. **Persona** - the full contents of the active persona file (`config/persona.md` when present, else tracked `persona.md`), always printed regardless of lock state - including lock-refused read-only mode - early every session, before the context and fleet-state digests, so the captain-facing voice is reliably in force. -5. **Context digest** - the full contents of `data/projects.md`, `data/secondmates.md`, `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, each clearly delimited. +4. **Supervision operating instructions and next step** - after the wake queue and before persona and context, the digest emits exactly one operating block for the detected primary harness. + The closing reminder printed at the very end of the digest points back to that emitted block and preserves only the lock, afk, X-mode, and read-once reminders. +5. **Persona** - the full contents of the active persona file (`config/persona.md` when present, else tracked `persona.md`), always printed regardless of lock state - including lock-refused read-only mode - early every session, before the context and fleet-state digests, so the captain-facing voice is reliably in force. +6. **Context digest** - the full contents of `data/projects.md`, `data/secondmates.md`, `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, each clearly delimited. A file that does not exist prints an explicit `ABSENT` marker, never confused with an empty-but-present file: absence is meaningful (`captain.md` absent means use the firstmate repo's built-in defaults, `projects.md` absent means rebuild it from the clones under `projects/`, etc.). -6. **Fleet-state digest** - the compact backlog listing owned by `bin/fm-session-start.sh`; every `state/.meta`; a bounded tail of each task's `state/.status` (labeled as wake-EVENT history, not current state, with the full log path printed for a deeper read); the `state/.afk` flag; and one cheap alive/dead read of each task's recorded backend endpoint. +7. **Fleet-state digest** - the compact backlog listing owned by `bin/fm-session-start.sh`; every `state/.meta`; a bounded tail of each task's `state/.status` (labeled as wake-EVENT history, not current state, with the full log path printed for a deeper read); the `state/.afk` flag; and one cheap alive/dead read of each task's recorded backend endpoint. That liveness line is a fast presence check only, not a full state read - when you need a crew's actual current state (a run-step, not just "is the pane there"), read it with `bin/fm-crew-state.sh ` as before; the digest deliberately skips that deeper, slower read for every task so it stays fast and bounded. -7. **Supervision operating instructions and next step** - after the wake queue and before context, the digest emits exactly one operating block for the detected primary harness. - The closing reminder points back to that emitted block and preserves only the lock, afk, X-mode, and read-once reminders. The script itself never starts supervision; the emitted harness protocol owns the exact wait or wake mechanism. Bootstrap detects first, asks for consent, and installs only after the captain approves in the current session. diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index 36d7d860ad..8e82477a53 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -35,22 +35,26 @@ # also run only when locked. # 3. wake-drain - mutates the durable wake queue, so it also only runs # when locked. -# 4. persona - the active persona file (config/persona.md local +# 4. supervision - emits exactly one operating block for the detected +# primary harness, after the wake queue and before +# persona and context. +# 5. persona - the active persona file (config/persona.md local # override, else tracked persona.md): read-only, always # safe, always runs (including on lock refusal), and # prints early - before the context and fleet-state # digests - so the captain-facing voice is reliably # in force. -# 5. context digest - data/projects.md, data/secondmates.md, data/captain.md, +# 6. context digest - data/projects.md, data/secondmates.md, data/captain.md, # data/captain-shared.md, data/learnings.md: read-only, # always safe, always runs. -# 6. fleet digest - a compact data/backlog.md identity/metadata listing, +# 7. fleet digest - a compact data/backlog.md identity/metadata listing, # every state/*.meta, a bounded state/*.status tail, # state/.afk, and a cheap per-task endpoint-liveness read: # read-only, always runs. -# 7. closing reminder - prints the context-specific watcher next step; this +# 8. closing reminder - prints the context-specific watcher next step; this # script points back to the emitted harness supervision -# block and deliberately never arms the watcher itself. +# block (step 4) and deliberately never arms the watcher +# itself. # # On a Pi primary, the supervision-block step also checks whether Pi's two # tracked primary extensions are loaded and prints a PI_WATCH_EXTENSION @@ -173,6 +177,10 @@ print_persona() { else subsection "persona.md (tracked default)" fi + if [ ! -r "$path" ]; then + printf 'UNREADABLE (%s exists but could not be read - captain-facing address/voice is undefined; this needs repair, fix its permissions or restore it)\n' "$path" + return + fi if [ -s "$path" ]; then cat "$path" else diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index d3635749db..5f2b0307ce 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -701,6 +701,34 @@ EOF pass "session start reports a fully-absent persona distinctly, as needing repair rather than a quiet defaults fallback" } +test_persona_unreadable_reports_repair_needed() { + local rec root home fakebin out + rec=$(new_world persona-unreadable) + IFS='|' read -r root home fakebin < "$root/persona.md" + printf '# Persona\n\naddress the user as boss, no nautical flavor\n' > "$home/config/persona.md" + chmod 000 "$home/config/persona.md" + + out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH") + + chmod 700 "$home/config/persona.md" + + assert_contains "$out" "UNREADABLE (" \ + "digest did not call out an unreadable active persona file as needing repair" + assert_contains "$out" "$home/config/persona.md" \ + "digest's unreadable-persona message did not name the broken file" + case "$out" in + *"address the captain as skipper"*) fail "an unreadable local override must not silently fall back to the tracked default: $out" ;; + *"address the user as boss, no nautical flavor"*) fail "an unreadable persona file's content must not print: $out" ;; + esac + + pass "an unreadable active persona file is reported as needing repair, not silently skipped or fallen back from" +} + # --- lock refusal: read-only path -------------------------------------------- test_lock_refusal_read_only_path() { @@ -1527,6 +1555,7 @@ test_context_digest_absent_empty_present test_persona_tracked_default_printed test_persona_local_override_supersedes_default test_persona_absent_reports_repair_needed +test_persona_unreadable_reports_repair_needed test_lock_refusal_read_only_path test_lock_write_failure_read_only_path test_session_lock_concurrent_single_winner From f7d55247fccacc781dbf4792ef5e896f647a2ebf Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 17:16:31 -0700 Subject: [PATCH 5/5] no-mistakes(document): Docs already covered the persona refactor thoroughly; fixed one inaccuracy: ABSENT vs UNREADABLE persona states were conflated in AGENTS.md and docs/configuration.md omitted the UNREADABLE case entirely, so both now describe it as its own distinct repair failure matching bin/fm-session-start.sh's actual behavior. --- AGENTS.md | 3 ++- docs/configuration.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 145fcd6915..6ddbbe38ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -136,7 +136,8 @@ Tracked native session-open adapters only nudge this command; `docs/sessionstart Read the complete digest once and trust it as this turn's startup and recovery input. Do not separately re-read the persona, context, backlog, metadata, or bulk status inputs it just printed unless a source was reported absent or corrupt, older history is specifically needed, or a targeted workflow must inspect before writing. An `ABSENT` captain, shared-captain, secondmate, or learnings file means the firstmate repo's built-in defaults, no shared captain preferences, no registered secondmates, or no captured learnings; rebuild an absent or stale project registry from the clones before dispatch. -An `ABSENT` persona is not a normal state, unlike those files: the tracked `persona.md` default should always exist, so its absence alongside an absent `config/persona.md` override means the tracked file was deleted or is unreadable and needs repair. +An `ABSENT` persona is not a normal state, unlike those files: the tracked `persona.md` default should always exist, so its absence alongside an absent `config/persona.md` override means the tracked file was deleted and needs repair. +A present but `UNREADABLE` persona file (a permissions problem, not a missing one) is reported as its own distinct repair failure rather than silently falling back or going unreported. If the session lock cannot be acquired and verified, report its exact diagnostic and remain read-only; another active session is only one possible cause. A lock-refused session must not spawn, steer, merge, drain the wake queue, repair supervision, repair a checkout, or perform any other fleet mutation. diff --git a/docs/configuration.md b/docs/configuration.md index aaf942daa7..4469930014 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,6 +13,7 @@ The captain-facing voice - address term, seasoning policy, house vocabulary, and Swap the voice by editing tracked `persona.md` directly, or by dropping a local, gitignored `config/persona.md` in this home; when present, the local file fully replaces the tracked default rather than merging with it, mirroring `config/crew-harness`'s override pattern. `bin/fm-session-start.sh` resolves and prints the active persona's full contents unconditionally, every session, labeled by source (tracked default vs local override), so the voice is always in force with no per-reply trigger to load it. An absent persona (both tracked `persona.md` and `config/persona.md` missing) is reported distinctly from an `ABSENT` context-digest file: unlike `data/captain.md` or `data/learnings.md`, there is no built-in-defaults fallback, so it signals the tracked file needs repair. +A present but unreadable active persona file (for example, a permissions problem) is reported as its own distinct `UNREADABLE` repair failure rather than silently falling back to the other candidate or going unreported. ## Operational home layout and state