diff --git a/.agents/skills/agent-skill-trigger-index/SKILL.md b/.agents/skills/agent-skill-trigger-index/SKILL.md index 5297d6022bd..ee9a53c69fd 100644 --- a/.agents/skills/agent-skill-trigger-index/SKILL.md +++ b/.agents/skills/agent-skill-trigger-index/SKILL.md @@ -10,7 +10,7 @@ metadata: These skills are not captain-invocable; load them only at their precise triggers. -- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `PRESENTATION_UNAVAILABLE:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `STARTUP_MEMORY_BUDGET:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `HOME_SUMMARY:`, `BACKLOG_RECONCILE:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`), or when `BOOTSTRAP_INFO:` says an interrupted backlog cleanup may have left an endpoint or local copy; silence and other `BOOTSTRAP_INFO:` facts need no load. +- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `PRESENTATION_UNAVAILABLE:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `HOME_SUMMARY:`, `BACKLOG_RECONCILE:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`), or when `BOOTSTRAP_INFO:` says an interrupted backlog cleanup may have left an endpoint or local copy; silence and other `BOOTSTRAP_INFO:` facts need no load. - `diagnostic-reasoning` - load before scoping a reported bug and before acting on a diagnostic report. - `research-first-decisions` - load before selecting a tool, library, framework, service, vendor, or approach from candidates, and before recording such a selection as decided; also load before adopting, configuring, upgrading, or integrating a library, SDK, API, CLI, framework, or service, or before debugging version-sensitive usage, where its Context7 version check is mandatory whether or not a candidate is being chosen. - `wayfinding` - load before scoping work larger than one task, such as a stage, a release, a migration, or a campaign of related changes; before dispatching a task whose backlog dependency names a stage, a release, or a final acceptance; when work is blocked only at its final step or the queue looks fully gated; and whenever the ready frontier lists only umbrellas or nothing while holds still exist. diff --git a/.agents/skills/bootstrap-diagnostics/SKILL.md b/.agents/skills/bootstrap-diagnostics/SKILL.md index a3b37c81915..c56dec29612 100644 --- a/.agents/skills/bootstrap-diagnostics/SKILL.md +++ b/.agents/skills/bootstrap-diagnostics/SKILL.md @@ -2,7 +2,7 @@ name: bootstrap-diagnostics description: >- Agent-only handling playbook for session-start bootstrap diagnostics. - Use whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, PRESENTATION_UNAVAILABLE, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, STARTUP_MEMORY_BUDGET, CREW_DISPATCH invalid, FLEET_SYNC, NETWORK_CHECKS, HOME_SUMMARY, BACKLOG_RECONCILE, SECONDMATE_SYNC, SECONDMATE_LIVENESS, SECONDMATE_HANDOFF, NUDGE_SECONDMATES, or FMX - or reports that an interrupted backlog cleanup may have left an endpoint or local copy, or when a standalone bin/fm-bootstrap.sh or bin/fm-startup-network.sh run prints one of those lines. + Use whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, PRESENTATION_UNAVAILABLE, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, CREW_DISPATCH invalid, FLEET_SYNC, NETWORK_CHECKS, HOME_SUMMARY, BACKLOG_RECONCILE, SECONDMATE_SYNC, SECONDMATE_LIVENESS, SECONDMATE_HANDOFF, NUDGE_SECONDMATES, or FMX - or reports that an interrupted backlog cleanup may have left an endpoint or local copy, or when a standalone bin/fm-bootstrap.sh or bin/fm-startup-network.sh run prints one of those lines. A silent bootstrap section, or any other BOOTSTRAP_INFO fact, means no skill load. user-invocable: false metadata: @@ -35,8 +35,6 @@ When any diagnostic needs captain attention, report the plain consequence and re - `TANGLE: ` - the primary checkout is stranded on a feature branch instead of its default branch; `AGENTS.md` section 8 explains why this guard exists and what it protects. The work is safe on that branch ref; restore the primary to its default branch with the printed `git -C checkout `, then re-validate that branch in a proper worktree. This is the only sanctioned firstmate-initiated git write to the primary, and it is a non-destructive branch switch that strands nothing. -- `STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - ` - the visible startup-memory budget is not a safe one-line positive decimal file; do not infer the default or propagate it. - Correct the local primary file, then rerun session start so the normal convergence path can deliver the validated value to secondmate homes. - `CREW_DISPATCH: invalid config/crew-dispatch.json - ` - the optional dispatch profile file exists but failed low-cost bootstrap validation; stop profile-based dispatch, report the actionable error, and require correction of the malformed schema, unverified harness name, or invalid harness/effort pair rather than falling back around it or selecting a bad profile. - `FLEET_SYNC: : skipped: ` - a benign one-off skip (offline, no origin, local-only); bootstrap continued, investigate only if it blocks work. A skip can also report the bounded fleet-refresh timeout (`FM_FLEET_SYNC_BOOTSTRAP_TIMEOUT`, or a fleet-size-aware default with a 20 second floor); a timeout never blocks startup. diff --git a/.agents/skills/operational-home-layout/SKILL.md b/.agents/skills/operational-home-layout/SKILL.md index f51340058bb..8dfb22961a5 100644 --- a/.agents/skills/operational-home-layout/SKILL.md +++ b/.agents/skills/operational-home-layout/SKILL.md @@ -32,7 +32,6 @@ config/keep-ai-trailers optional presence flag to keep AI co-author trailers in config/supervision-branch-model config/supervision-branch-effort Pi supervision-branch model and reasoning-effort pins written by /supervision-model; LOCAL, gitignored, independently settable, and not inherited; see docs/configuration.md "Pi supervision branch model and effort" config/supervision-host optional supervision-host engine setting: the host runs the supervision branch's contract on a headless engine beside a non-Pi primary, away and, on a Claude or Cursor primary, attended; absent runs it on a Claude primary and nowhere else; LOCAL, gitignored, not inherited; see docs/configuration.md "Supervision host" config/supervision-host-off optional presence flag opting this home out of the supervision host on every primary; LOCAL, gitignored; inherited by secondmate homes under the primary-authoritative contract; see docs/configuration.md "Supervision host" -config/startup-memory-budget primary-authoritative per-home startup-memory budget; LOCAL, gitignored, materialized as 7,500 estimated tokens by locked primary bootstrap and inherited into secondmate homes; see docs/configuration.md "Startup memory budget" config/stow-pass-horizon optional presence flag opting this home in to /stow's default-off pass-count decay horizon; LOCAL, gitignored, and not inherited; see docs/configuration.md "Stow pass horizon" config/herdr-presentation-spaces optional "off" opt-out from, or "on" opt-in to, Herdr's default-on disposable single-task visual projection, which is unconfigured-default-on only at or above a Herdr version floor; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Presentation spaces" config/trace-context optional presence flag enabling default-off native W3C trace-context propagation to spawned agents; LOCAL, gitignored; inherited by secondmate homes; see docs/configuration.md "Trace context propagation" and docs/trace-context.md diff --git a/.agents/skills/stow/SKILL.md b/.agents/skills/stow/SKILL.md index 01bcdb8688c..9c58da4df4d 100644 --- a/.agents/skills/stow/SKILL.md +++ b/.agents/skills/stow/SKILL.md @@ -16,7 +16,7 @@ This skill writes only through the existing Firstmate ownership and write bounda ## Memory tiers and entry markers -Markers are compact trailing HTML comments, deliberately cheap because marker bytes are counted content: +Markers are compact trailing HTML comments, deliberately cheap because marker bytes are injected content: - `` - an `aging` entry; the embedded date is its last-reinforced date. - `` - a `perishable` entry; the embedded date is its last-reinforced date. @@ -34,7 +34,7 @@ Markers are compact trailing HTML comments, deliberately cheap because marker by The tier names say what the pass does with an entry: -- `pinned` - no clock is ever read for it: exempt from decay and from budget eviction, changed only through inspect-then-update when the captain or reality changes it, except that an explicit per-item captain approval may offload it under the flow below. +- `pinned` - no clock is ever read for it: exempt from decay, changed only through inspect-then-update when the captain or reality changes it, except that an explicit per-item captain approval may offload it under the flow below. - `aging` - it must re-prove itself: an entry whose age is greater than or equal to 30 days since its last-reinforced date is stale, and a stale entry is re-validated (date refreshed) or archived, never kept by inertia alone. - `perishable` - it is stored expecting disposal: an entry whose age is greater than or equal to 7 days since its last-reinforced date is stale, and its prose must name a checkable expiry condition, such as a backlog id, a version floor, or a dated expectation. An admitted durable entry that cannot name a checkable expiry condition is not `perishable` and must be stored as `aging`. @@ -44,7 +44,7 @@ Marking rules: - Tier defaults are file-scoped: entries in `data/captain.md` and `data/captain-shared.md` default to `pinned` because preferences and authority boundaries do not age, and entries in `data/learnings.md` default to `aging` because operational facts must re-prove themselves. - An entry matching its file's `pinned` default carries no marker at all; every `aging` and `perishable` entry always carries its dated marker, whose letter names the tier, so a clock-carrying entry is never ambiguous with unmarked legacy material. -- Marker and header-pointer bytes count toward the startup-memory budget: the pass's own bookkeeping is costed content, never free, which is why the spellings above are as short as they are. +- Marker and header-pointer bytes are injected into every session like any other memory content: the pass's own bookkeeping is never free, which is why the spellings above are as short as they are. - Each memory file's header carries at most a one-line pointer naming this skill as the scheme owner, such as ``. This skill text is the single owner of tier semantics, marker spellings, and clocks, and no memory file header may restate them. The one exception is the `config/stow-pass-horizon` presence flag below, which turns a single extra horizon on for this home and changes nothing else on this page. @@ -69,7 +69,7 @@ While the flag is present: - An `aging` entry is stale at whichever horizon it reaches first: 10 passes that evaluated it without reinforcing it, or 30 days since its last-reinforced date. - A `perishable` entry is stale at whichever it reaches first: 3 unreinforced passes, or 7 days. -- Reinforcement refreshes the date and clears the counter, and nothing else clears it, so the evidence hard rule in step 4 stays the only way an entry renews its lease. +- Reinforcement refreshes the date and clears the counter, and nothing else clears it, so the evidence hard rule in step 3 stays the only way an entry renews its lease. - An existing dated marker with no `/N` reads as counter zero, so a home that opts in migrates nothing. - Removing the flag returns the home to the default contract on its next pass: any `/N` already written is then neither read nor advanced, and is left in place rather than rewritten. @@ -77,75 +77,54 @@ While the flag is present: Every `/stow` invocation performs this complete pass, even when the session contains no new finding: -1. Run `bin/fm-startup-memory-budget.sh report` before considering a write. - Record its effective budget and each file's estimated-token total. - The budget is per home: this home's three files against this home's own allowance, never a fleet total. - The helper's stable estimate is the documented conservative local approximation, not provider-exact accounting. - If it rejects the setting or a memory file, do not infer a default or silently continue. - Report that concrete exception and do not call the session reset-safe. -2. Read every current memory file completely: `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`. +1. Read every current memory file completely: `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`. Treat an absent local file as absent, not as an invitation to manufacture content. In a primary home, all three are curation inputs under their existing ownership rules. - In a secondmate home, `data/captain-shared.md` is a read-only primary-owned input: count it, never edit it, and curate only the editable local files. - Every mutation in the rest of this pass, including reinforcement, retiering, decay archival, legacy migration, consolidation, budget archival, and offload, applies only to an editable memory file. + In a secondmate home, `data/captain-shared.md` is a read-only primary-owned input: read it, never edit it, and curate only the editable local files. + Every mutation in the rest of this pass, including reinforcement, retiering, decay archival, legacy migration, consolidation, and offload, applies only to an editable memory file. When a read-only shared entry appears to require one of those changes, leave it untouched, report the required change as an ownership exception, and route it to the primary owner. -3. Build one whole-file retention plan before editing, ordered by likelihood of informing a future session. +2. Build one whole-file retention plan before editing, ordered by likelihood of informing a future session. Keep in always-loaded memory only current captain preferences, authority and safety boundaries, recurring working style, fleet-wide or frequently relevant operating facts, and concise pointers that are expensive to rediscover. - Prefer offloading current but conditional, narrow, project-specific, or context-specific material to a live on-demand owner, and archive stale, superseded, or low-recurrence material to the cold tier. - Retain lower-utility material only while budget remains. -4. Reinforce and stamp. + Prefer offloading current but conditional, narrow, project-specific, or context-specific material to a live on-demand owner under the offload section below, and archive stale, superseded, or low-recurrence material to the cold tier. +3. Reinforce and stamp. Refresh an entry's last-reinforced date to today only when this session actually exercised, confirmed, or re-derived it. Where the optional pass horizon is enabled, refreshing that date also clears the entry's unreinforced-pass counter, and nothing else clears it. **Hard rule: reinforcement requires independent evidence from this session that you can name in the receipt; plausibility, importance, prior knowledge, and the entry's own text are not evidence, and any explicit statement that no confirming session evidence exists requires the no-evidence path.** For an unmarked `data/learnings.md` entry with no such evidence, the no-evidence path is always to append `` and retain it for this entire pass; never stamp or archive it during that same invocation. Stamp each newly written entry with today's date and its tier per the marking rules, and admit a new `perishable` entry only with its named checkable expiry condition in the prose. -5. Evaluate every dated entry in each editable memory file against its tier clock. - Where the optional pass horizon is enabled, first increment the unreinforced-pass counter of every dated entry step 4 did not reinforce - that increment is the pass tick - then judge each dated entry against both of its horizons and treat it as stale at whichever it reaches first. +4. Evaluate every dated entry in each editable memory file against its tier clock. + Where the optional pass horizon is enabled, first increment the unreinforced-pass counter of every dated entry step 3 did not reinforce - that increment is the pass tick - then judge each dated entry against both of its horizons and treat it as stale at whichever it reaches first. Re-validate a stale `aging` entry from current evidence and refresh its date, or archive it. Re-confirm a stale `perishable` entry against its named condition: still open means refresh the date, while resolved, expired, or no longer checkable means archive it in this pass. Promote `perishable` to `aging` when its condition keeps proving durable past its expected life, and retier in place when a supersession changes an entry's lifetime. `pinned` is exempt from this automatic decay step entirely. -6. Consolidate every editable memory file as needed, not only the file apparently related to a new finding. +5. Consolidate every editable memory file as needed, not only the file apparently related to a new finding. Prefer one concise current rule or authoritative pointer over duplicate prose. Archive completed incident and release chronology, stale versions and paths, transient task state, resolved alternatives, old metrics, and report-sized procedures; merge or remove only superseded claims and duplicates whose facts are preserved elsewhere. Never plainly remove a unique current fact: every such exit must archive it with provenance in the recoverable cold tier or relocate it to a live JIT owner or a consolidation merge that preserves the fact. -7. When the total is still over budget after decay and consolidation, make aggressive reduction the default, using editable files only and in this order: archive every editable stale, superseded, or low-utility entry that is eligible for archival; consolidate tighter; run the over-budget offload sweep below and autonomously relocate every eligible non-pinned conditional entry into an already-existing allowed owner only after that owner holds it; then, only when the convergence precondition below holds, archive eligible `aging` entries oldest-reinforced-first until within budget. - A proposal, a future migration, or an accepted exception is never budget relief in this pass. - Budget eviction considers only editable `aging` entries that carry a last-reinforced date and are not pending offload; a `` legacy-grace entry is ineligible until its grace cycle resolves, so eviction can neither cancel a promised grace cycle nor prefer just-validated entries over unvalidated ones. - Convergence precondition: before evicting anything, total the eligible pool and check that archiving all of it would reach the budget; when even that cannot, skip the eviction rung entirely, archive nothing for budget reasons, and carry the concrete inability to the final step, naming the exempt pinned floor that crowds out the budget. - Automatic processes never move a `pinned` entry: decay clocks, legacy grace cycles, oldest-first budget eviction, immediate budget archiving, and autonomous offload do not apply to it. + Automatic processes never move a `pinned` entry: decay clocks, legacy grace cycles, and autonomous offload do not apply to it. The sole exception is relocation to a JIT owner after explicit, per-item captain approval under the offload flow below, and that entry remains in memory until its destination is live. -8. Run `bin/fm-startup-memory-budget.sh report` again after the complete pass. - Finish at or below the effective budget, or open a concrete captain decision before ending the pass. - A secondmate must explicitly report `primary-owned-shared-file-alone-exceeds-budget` when the inherited shared file alone exceeds its allowance, because local curation cannot resolve it. - Route that constraint to the primary owner and open one concrete captain decision at the primary owning level that names the shortfall, with exactly these options: raise the affected home's effective budget, or explicitly approve the primary owner trimming or offloading each named shared-file entry. - When the convergence precondition skipped eviction, report the exempt pinned floor and the remaining shortfall as that concrete inability rather than archiving eligible knowledge that could not close the gap. - Only after every safe non-pinned archival, consolidation, offload, and eligible eviction action is exhausted may a remaining excess be attributed to pinned safety, authority, or genuine captain-preference entries. - In that last-resort case, create one captain-held decision that names the shortfall and each relevant pinned entry, with exactly these options: raise the effective budget, or explicitly approve offloading or trimming a named pinned entry. - Route a read-only ownership constraint to its primary owner, and make every other unresolved excess a concrete captain decision that names the safe action still required. - Never end a pass over budget as an accepted exception. A net increase is allowed only for a genuinely new current fact with no stronger owner. -Before allowing it, consolidate enough lower-priority material to remain within budget. -Never describe the session as reset-safe while the memory total is over budget or an exception is unresolved. +Never describe the session as reset-safe while an exception is unresolved. ## The cold tier: data/memory-archive.md -Stale never means deleted: pruning an entry from an editable memory file always means moving it to `data/memory-archive.md`, this home's append-only, never-injected cold tier, gitignored with the rest of `data/` and never counted by the budget report. +Stale never means deleted: pruning an entry from an editable memory file always means moving it to `data/memory-archive.md`, this home's append-only, never-injected cold tier, gitignored with the rest of `data/`. Each archived entry keeps its provenance under a dated pass heading: source file, tier, last-reinforced date, and the reason it left. Include the unreinforced-pass counter only when the optional pass horizon itself made the entry stale, using the exact reason `unreinforced p`; omit the counter when the wall-clock horizon or any other reason caused archival, even if the active marker carried one. -Archive provenance stays verbose rather than compact because the cold tier is never budget-counted. +Archive provenance stays verbose rather than compact because the cold tier is never injected. ```markdown ## 2026-08-08 stow - (from learnings.md, tier: perishable, reinforced: 2026-06-30) While state/.afk exists, the away-daemon owns triage... [archived: unreinforced 39d] ``` -Reasons include `unreinforced d`, `unreinforced p`, `budget oldest-first`, and `legacy-unvalidated`. +Reasons include `unreinforced d`, `unreinforced p`, and `legacy-unvalidated`. Archiving is a move, not a removal, and recovery is `grep` plus copy back with no tooling. Each home keeps its own archive, the archive never cascades, and truncating a grown archive is a captain decision, not a mechanism. -## Over-budget offload to JIT-loaded owners +## Offload to JIT-loaded owners Decay handles staleness over time; offload handles scope: knowledge that is current and durable but relevant only in a nameable context, and therefore wrong to pay for in every session of every fleet member. For the offload sweep's evaluation only, each entry has exactly three outcomes decided in this fixed order: @@ -154,15 +133,15 @@ For the offload sweep's evaluation only, each entry has exactly three outcomes d 2. Offload, the scope outcome, asked only of current durable entries: is this needed in nearly every session, or only in a nameable context? 3. Keep, the default outcome for this sweep: current, durable, and either fleet-wide-relevant or safety-relevant even in sessions that never name the topic. -The offload sweep runs whenever the pass is still over budget after decay archiving and consolidation, so routine passes do not move entries speculatively. -It is an immediate reduction step for eligible non-pinned conditional material that can be added to an already-existing allowed owner, not a deferred proposal that leaves the pass over budget. +The offload sweep runs only for entries the retention plan in step 2 marked for offload, so routine passes do not move entries speculatively. +It immediately relocates eligible non-pinned conditional material that can be added to an already-existing allowed owner. Every test must hold for a candidate: - Editable source: this home owns the memory file and may relocate the entry; a read-only shared entry is routed to its primary owner instead. - Durable: not `perishable`, not stale, and expected to remain true for months. -- Eligible by authority: only a non-pinned, dated `aging` entry that is not pending offload may be autonomously relocated to an already-existing allowed owner, while a `pinned` entry may be proposed only for explicit, per-item captain-approved relocation and can never be archived or autonomously offloaded for budget relief. +- Eligible by authority: only a non-pinned, dated `aging` entry that is not pending offload may be autonomously relocated to an already-existing allowed owner, while a `pinned` entry may be proposed only for explicit, per-item captain-approved relocation and can never be archived or autonomously offloaded. - Conditional: a one-line nameable trigger exists, and a session that never touches that trigger runs no risk from omitting the fact. -- Fat enough to matter: roughly 50 estimated tokens or more, handled largest-first, because consolidation handles smaller entries. +- Fat enough to matter: a sizeable entry, handled largest-first, because consolidation handles smaller entries. - A destination below fits the entry's privacy and visibility. - Not already preserved by a stronger owner, which the consolidation counterweight already handles as ordinary curation rather than offload. @@ -189,13 +168,13 @@ A local skill exists only in this home, so offloading an entry out of `data/capt ### Flow: reduce, approve, migrate, remove 1. Reduce non-pinned material now. - For each eligible non-pinned candidate, record its first line, source file, estimated tokens, one-line trigger, live destination, privacy and visibility verdict, and actual budget relief in the completion receipt. + For each eligible non-pinned candidate, record its first line, source file, one-line trigger, live destination, and privacy and visibility verdict in the completion receipt. Autonomously relocate it only by adding it to an already-existing allowed JIT note, or by routing it through a project's established delivery path to an already-existing allowed project-level destination, then confirming that destination holds the quoted entry before removing the memory entry. - A destination that needs creation, uncompleted project delivery, or any other future work is not live and cannot count as relief, so continue with the next archival or eviction rung instead of leaving an over-budget proposal pending. + A destination that needs creation, uncompleted project delivery, or any other future work is not live, so the entry stays in memory. 2. Propose pinned relocation only. For a pinned candidate, append a `proposed-offload` section with the same fields to the completion receipt, create or refresh one durable backlog item with `bin/fm-tasks-axi.sh add`, `bin/fm-tasks-axi.sh show --full`, and `bin/fm-tasks-axi.sh update --body-file ` as appropriate, then hold it through `bin/fm-captain-hold.sh hold`. Preserve each candidate's approval state in that item, and require explicit plain-chat approval for that named item before any migration. - If the captain never answers, nothing migrates and the held item persists, but it is never treated as budget relief. + If the captain never answers, nothing migrates and the held item persists. 3. Migrate an approved pinned candidate outside this pass. Resolve `home_root` to `$FM_HOME` when it is set and otherwise to the Firstmate code root, then re-validate the approved local-skill destination under that root for both index absence with `git -C "$home_root"` and filesystem collision absence. Before creating the destination or writing any private content, resolve the exclude file with `git -C "$home_root" rev-parse --git-path info/exclude`, append the destination directory path to it, and verify the future `SKILL.md` path is ignored with `git -C "$home_root" check-ignore`. @@ -259,17 +238,15 @@ The first pass after adoption performs a one-time revalidation sweep of editable Report the outcome in plain captain-facing language with all of these facts: -- effective startup-memory budget and total estimated tokens before and after; - one or more actions for each of `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, using only `unchanged`, `added`, `rewritten`, `pruned`, `routed`, `archived`, or `proposed-offload`; adding or replacing a migration marker is `rewritten`, never a new action verb such as `migrated`; - each durable finding filed outside memory and its authoritative owner; -- each archived entry's reason, each autonomous offload's live destination and actual relief, and, when a pinned candidate was proposed, the `proposed-offload` section with every candidate's fields; -- every unresolved exception, including a primary-owned shared-file constraint in a secondmate home, and every concrete captain decision opened for an over-budget result; +- each archived entry's reason, each autonomous offload's live destination, and, when a pinned candidate was proposed, the `proposed-offload` section with every candidate's fields; +- every unresolved exception, including a primary-owned shared-file constraint in a secondmate home; - each open record this pass filed or corrected, and each one it deliberately left alone with the judgment it is waiting on; -- whether the session is safe to reset, only when all durable findings are captured, every open record this session held is filed or explicitly left with its reason, and the post-pass result is within budget with no exception or pending budget decision. +- whether the session is safe to reset, only when all durable findings are captured, every open record this session held is filed or explicitly left with its reason, and no exception is unresolved. State what reset-safe means in the same breath as the claim: nothing this session knew has been lost. It is never a claim that the home's durable records are correct, because this pass checks no record the session did not name. -Do not hide an over-budget result behind a reset-safe claim. In a primary home the receipt is written after the cascade below, not instead of it. ## Automatic cascade to secondmates @@ -279,26 +256,26 @@ In a secondmate home, `/stow` curates that home only and never cascades further. The cascade changes nothing until `/stow` is invoked: it adds no notification, no digest section, and no background work. Run `bin/fm-stow-cascade.sh` once the primary's own pass is done. -It enumerates each registered secondmate exactly once, reports that home's own budget accounting, and resolves how the sweep reaches it; its header owns the stanza fields, the bound, and the exit codes. -Every home is judged against its own `config/startup-memory-budget` allowance, so never add homes together or treat one home's excess as another's. +It enumerates each registered secondmate exactly once and resolves how the sweep reaches it; its header owns the stanza fields, the bound, and the exit codes. +Every home is curated on its own memory, so never treat one home's entries as another's. Act on each home by its reported `transport`: - `agent` - send the marked request with `bin/fm-send.sh fm- ""` so the live secondmate performs its own `/stow`, including the uncaptured knowledge that exists only in its session. Ask it for the same completion receipt this skill defines, and read its reply from its status file or the document it points to, never from its chat. -- `direct` - curate that local home's editable memory files yourself under the same retention plan, then re-run the cascade to confirm the after totals. - `data/captain-shared.md` stays a read-only counted input there, exactly as it is in any secondmate home. -- `deferred` - a remote home with no live agent. Its memory is accounted read-only and cannot be curated from here, because there is no generic remote write path for a home's own memory files. +- `direct` - curate that local home's editable memory files yourself under the same retention plan. + `data/captain-shared.md` stays a read-only input there, exactly as it is in any secondmate home. +- `deferred` - a remote home with no live agent. Its memory cannot be curated from here, because there is no generic remote write path for a home's own memory files. Report it as an unresolved exception and leave it to its next cascade. Relaunching that secondmate is a separate decision owned by `secondmate-provisioning`, never something `/stow` does on its own. -- `unavailable` - that home's own accounting did not complete. Report the concrete exception and continue; a slow or unreachable home never blocks this home's `/stow`. +- `unavailable` - that local home failed validation or its remote endpoint probe failed. Report the concrete exception and continue; a slow or unreachable home never blocks this home's `/stow`. A newly discovered shared captain preference still routes to the primary's `data/captain-shared.md` under the existing primary-authoritative contract, whichever home found it. Offload proposals and the cold archive are per-home: file proposals only in the home whose pass produced them, and never cascade either to another home. -Extend the completion receipt with one entry per secondmate alongside the primary's own, carrying that home's budget before and after, its per-file actions, its exceptions, and whether that home swept itself or was curated from here. +Extend the completion receipt with one entry per secondmate alongside the primary's own, carrying that home's per-file actions, its exceptions, and whether that home swept itself or was curated from here. Keep those entries in the same plain captain-facing language the rest of the receipt uses. -The session is reset-safe only when every home is within its own budget with no unresolved exception. +The session is reset-safe only when no home has an unresolved exception. ## Scope exclusion: no skill storage by the pass diff --git a/README.md b/README.md index c71f9e63e1c..095b31e0f03 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Claude and grok use the slash form shown here; codex uses the same names with `$ | `/ahoy` | Recap visible session events since the prior real captain message plus visibly unanswered captain decisions, then guide the captain through any open decisions one at a time in agent-judged impact order; fall back to Bearings when invoked as the session's first real captain message | | `/bearings` | Generate a concise four-section chat digest from bounded fleet state, including registered remote-home ledgers and measured follow-up for owned contributions; use `/bearings file` to also replace today's dated report in `data/`, and add `include PRs` for live GitHub enrichment | | `/updatefirstmate` | Guardedly update the running firstmate and its secondmates - fast-forward, or reconcile a redundant post-squash-merge divergence - then persist and restart every live mate successfully left on the target commit - including already-current homes - with an honest re-read nudge only when restart cannot be proven | -| `/stow` | Sweep the session for uncaptured durable knowledge, persist the open work records this session knows are unfiled or now wrong, curate tiered startup memory with decay and cold archival, enforce each home's budget or surface the required decision, cascade to registered second mates, and report what is safe to reset | +| [`/stow`](.agents/skills/stow/SKILL.md) | Sweep the session for uncaptured durable knowledge, persist the open work records this session knows are unfiled or now wrong, curate tiered startup memory with decay and cold archival, cascade to registered second mates, and report what is safe to reset | | `/push` | Take over the fleet for one bounded pass: map each tangle to its cause from live evidence, untangle by hand where the machinery refuses, judge rules and gates by minutes cost, correct sequence and method on every machine, fill every lane, and report in minutes saved. | | `/retro` | After a push or a hard period, route each lesson to one owner at the decision point, cut rules that cost time, keep `AGENTS.md` from growing, wake sleeping work on every machine, reconcile disagreeing records, and propagate to all mates. | diff --git a/bin/fm-bootstrap.sh b/bin/fm-bootstrap.sh index 48a0bf987d5..f5d78b89426 100755 --- a/bin/fm-bootstrap.sh +++ b/bin/fm-bootstrap.sh @@ -9,7 +9,6 @@ # "PRESENTATION_UNAVAILABLE: lavish-axi (requires >=; install: ) - nonvisual work may proceed with plain-text decisions and reports; install or upgrade before using Lavish", # "MISSING_MANUAL: (instructions: )", "NEEDS_GH_AUTH", # "BACKEND_INVALID: (known: )", -# "STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - ", # "CREW_DISPATCH: invalid config/crew-dispatch.json - ", # "FLEET_SYNC: : skipped|recovered|STUCK: ", # "HOME_SUMMARY: /dev/null - else - stat -c %h "$1" 2>/dev/null - fi -} - -fm_startup_memory_budget_config_dir_safe() { - local dir=$1 - if [ -L "$dir" ]; then - fm_startup_memory_budget_fail "config directory is symlinked" - return 1 - fi - if [ ! -d "$dir" ]; then - fm_startup_memory_budget_fail "config directory is not a directory" - return 1 - fi - return 0 -} - -# fm_startup_memory_budget_file_valid -# Sets FM_STARTUP_MEMORY_BUDGET_VALUE only for a regular, single-linked file -# containing exactly one positive decimal value and one terminating newline. -fm_startup_memory_budget_file_valid() { - local path=$1 links value - FM_STARTUP_MEMORY_BUDGET_VALUE="" - if [ -L "$path" ]; then - fm_startup_memory_budget_fail "file is symlinked" - return 1 - fi - if [ ! -e "$path" ]; then - fm_startup_memory_budget_fail "file is absent" - return 1 - fi - if [ ! -f "$path" ]; then - fm_startup_memory_budget_fail "file is not a regular file" - return 1 - fi - links=$(fm_startup_memory_budget_link_count "$path") || { - fm_startup_memory_budget_fail "could not inspect file link count" - return 1 - } - if [ "$links" != 1 ]; then - fm_startup_memory_budget_fail "file is hardlinked" - return 1 - fi - value=$(<"$path") || { - fm_startup_memory_budget_fail "could not read file" - return 1 - } - case "$value" in - ''|0|*[!0-9]*|0*) - fm_startup_memory_budget_fail "value must be one positive decimal integer" - return 1 - ;; - esac - if ! printf '%s\n' "$value" | cmp -s "$path" -; then - fm_startup_memory_budget_fail "file must contain exactly one value followed by one newline" - return 1 - fi - FM_STARTUP_MEMORY_BUDGET_VALUE=$value - return 0 -} - -# fm_startup_memory_budget_read -# Prints the validated decimal value. It never treats an absent or unsafe file -# as an implicit default because callers need a visible, auditable setting. -fm_startup_memory_budget_read() { - local config_dir=$1 path - fm_startup_memory_budget_config_dir_safe "$config_dir" || return 1 - path="$config_dir/$FM_STARTUP_MEMORY_BUDGET_FILE" - fm_startup_memory_budget_file_valid "$path" || return 1 - printf '%s\n' "$FM_STARTUP_MEMORY_BUDGET_VALUE" -} - -# fm_startup_memory_budget_materialize -# Atomically publishes the visible default only when the file is absent. A -# concurrent valid creator is accepted; every unsafe or malformed existing -# artifact is rejected without replacement. -fm_startup_memory_budget_materialize() { - local config_dir=$1 path tmp - if [ -e "$config_dir" ] || [ -L "$config_dir" ]; then - fm_startup_memory_budget_config_dir_safe "$config_dir" || return 1 - else - mkdir -p "$config_dir" 2>/dev/null || { - fm_startup_memory_budget_fail "could not create config directory" - return 1 - } - fm_startup_memory_budget_config_dir_safe "$config_dir" || return 1 - fi - - path="$config_dir/$FM_STARTUP_MEMORY_BUDGET_FILE" - if [ -e "$path" ] || [ -L "$path" ]; then - fm_startup_memory_budget_read "$config_dir" >/dev/null || return 1 - return 0 - fi - - tmp=$(umask 077; mktemp "$config_dir/.startup-memory-budget.XXXXXX" 2>/dev/null) || { - fm_startup_memory_budget_fail "could not create default temporary file" - return 1 - } - if ! printf '%s\n' "$FM_STARTUP_MEMORY_BUDGET_DEFAULT" > "$tmp" \ - || ! fm_startup_memory_budget_file_valid "$tmp"; then - rm -f "$tmp" - [ -n "$FM_STARTUP_MEMORY_BUDGET_ERROR" ] \ - || fm_startup_memory_budget_fail "could not write default value" - return 1 - fi - - # link(2) gives no-clobber publication in this directory. Removing the - # temporary name leaves the published file with exactly one link. - if ln "$tmp" "$path" 2>/dev/null; then - rm -f "$tmp" - fm_startup_memory_budget_read "$config_dir" >/dev/null || return 1 - return 0 - fi - rm -f "$tmp" - # Another actor may have created the file. Accept it only if it now meets - # the same safe, exact format - never replace or guess at it. - fm_startup_memory_budget_read "$config_dir" >/dev/null -} - -# fm_startup_memory_estimated_tokens_for_bytes -# The estimate is ceil(UTF-8 bytes / 3): stable, dependency-free, and -# deliberately conservative for ordinary prompt text without claiming provider -# exactness. -fm_startup_memory_estimated_tokens_for_bytes() { - local bytes=$1 tokens - case "$bytes" in - ''|*[!0-9]*) return 1 ;; - esac - tokens=$((bytes / 3)) - if [ $((bytes % 3)) -ne 0 ]; then - tokens=$((tokens + 1)) - fi - printf '%s\n' "$tokens" -} - -# fm_startup_memory_measure_file -# Prints " ". Memory files must be -# ordinary files when present so a measurement never follows a symlink or reads -# a special file. -fm_startup_memory_measure_file() { - local path=$1 bytes tokens - FM_STARTUP_MEMORY_MEASURE_BYTES="" - FM_STARTUP_MEMORY_MEASURE_TOKENS="" - FM_STARTUP_MEMORY_MEASURE_PRESENCE="" - if [ ! -e "$path" ] && [ ! -L "$path" ]; then - FM_STARTUP_MEMORY_MEASURE_BYTES=0 - FM_STARTUP_MEMORY_MEASURE_TOKENS=0 - FM_STARTUP_MEMORY_MEASURE_PRESENCE=absent - printf '0 0 absent\n' - return 0 - fi - if [ -L "$path" ] || [ ! -f "$path" ]; then - fm_startup_memory_budget_fail "memory file is not an ordinary regular file: $path" - return 1 - fi - bytes=$(LC_ALL=C wc -c < "$path" 2>/dev/null | tr -d '[:space:]') || { - fm_startup_memory_budget_fail "could not measure memory file: $path" - return 1 - } - case "$bytes" in - ''|*[!0-9]*) - fm_startup_memory_budget_fail "invalid byte count for memory file: $path" - return 1 - ;; - esac - tokens=$(fm_startup_memory_estimated_tokens_for_bytes "$bytes") || { - fm_startup_memory_budget_fail "could not estimate memory tokens for: $path" - return 1 - } - # shellcheck disable=SC2034 # Public measurement result consumed by the caller after sourcing. - FM_STARTUP_MEMORY_MEASURE_BYTES=$bytes - # shellcheck disable=SC2034 # Public measurement result consumed by the caller after sourcing. - FM_STARTUP_MEMORY_MEASURE_TOKENS=$tokens - # shellcheck disable=SC2034 # Public measurement result consumed by the caller after sourcing. - FM_STARTUP_MEMORY_MEASURE_PRESENCE=present - printf '%s %s present\n' "$bytes" "$tokens" -} - -# fm_startup_memory_decimal_le -# Decimal comparison without shell arithmetic overflow. Inputs are normalized -# non-negative decimal strings. -fm_startup_memory_decimal_le() { - local left=$1 right=$2 left_len right_len - case "$left:$right" in - *[!0-9:]*|:*|*:) return 1 ;; - esac - left_len=${#left} - right_len=${#right} - if [ "$left_len" -lt "$right_len" ]; then - return 0 - fi - if [ "$left_len" -gt "$right_len" ]; then - return 1 - fi - [ "$left" = "$right" ] && return 0 - [[ "$left" < "$right" ]] -} diff --git a/bin/fm-startup-memory-budget.sh b/bin/fm-startup-memory-budget.sh deleted file mode 100755 index 715da549482..00000000000 --- a/bin/fm-startup-memory-budget.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# Read and account for the local startup-memory budget. -# Usage: -# fm-startup-memory-budget.sh read -# fm-startup-memory-budget.sh report -# -# `read` prints the one validated effective budget from -# config/startup-memory-budget. `report` prints the stable local estimate for -# data/captain.md, data/captain-shared.md, and data/learnings.md together. -# Bootstrap owns default materialization; this command never creates or repairs -# configuration, so an absent, malformed, symlinked, hardlinked, or otherwise -# unsafe value is a concrete error rather than an inferred default. -set -eu - -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}}" -CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}" -DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}" - -# shellcheck source=bin/fm-startup-memory-budget-lib.sh -. "$SCRIPT_DIR/fm-startup-memory-budget-lib.sh" - -usage() { - sed -n '2,11{s/^# \{0,1\}//;p;}' "$0" -} - -print_error() { - printf 'startup-memory-budget: %s\n' "$1" >&2 -} - -read_budget() { - if ! fm_startup_memory_budget_read "$CONFIG" >/dev/null; then - print_error "invalid config/$FM_STARTUP_MEMORY_BUDGET_FILE - $FM_STARTUP_MEMORY_BUDGET_ERROR" - return 1 - fi - printf '%s\n' "$FM_STARTUP_MEMORY_BUDGET_VALUE" -} - -report() { - local budget bytes tokens presence total=0 shared_tokens=0 role=primary - if ! budget=$(read_budget); then - return 2 - fi - - if [ -e "$FM_HOME/.fm-secondmate-home" ] || [ -L "$FM_HOME/.fm-secondmate-home" ]; then - role=secondmate - fi - - printf 'estimator=ceil(UTF-8 bytes / 3) conservative-local-estimate\n' - printf 'role=%s\n' "$role" - printf 'effective_budget_tokens=%s\n' "$budget" - for file in captain.md captain-shared.md learnings.md; do - if ! fm_startup_memory_measure_file "$DATA/$file" >/dev/null; then - print_error "$FM_STARTUP_MEMORY_BUDGET_ERROR" - return 2 - fi - bytes=$FM_STARTUP_MEMORY_MEASURE_BYTES - tokens=$FM_STARTUP_MEMORY_MEASURE_TOKENS - presence=$FM_STARTUP_MEMORY_MEASURE_PRESENCE - total=$((total + tokens)) - [ "$file" != captain-shared.md ] || shared_tokens=$tokens - printf 'file=data/%s bytes=%s estimated_tokens=%s status=%s\n' \ - "$file" "$bytes" "$tokens" "$presence" - done - printf 'total_estimated_tokens=%s\n' "$total" - if fm_startup_memory_decimal_le "$total" "$budget"; then - printf 'budget_status=within-budget\n' - else - printf 'budget_status=over-budget\n' - fi - if [ "$role" = secondmate ] \ - && ! fm_startup_memory_decimal_le "$shared_tokens" "$budget"; then - printf 'exception=primary-owned-shared-file-alone-exceeds-budget\n' - fi -} - -case "${1:-}" in - read) - [ "$#" -eq 1 ] || { usage >&2; exit 2; } - read_budget - ;; - report) - [ "$#" -eq 1 ] || { usage >&2; exit 2; } - report - ;; - -h|--help) - usage - ;; - *) - usage >&2 - exit 2 - ;; -esac diff --git a/bin/fm-stow-cascade.sh b/bin/fm-stow-cascade.sh index 52443750ed7..a3da7d8d0e2 100755 --- a/bin/fm-stow-cascade.sh +++ b/bin/fm-stow-cascade.sh @@ -3,20 +3,17 @@ # Usage: fm-stow-cascade.sh [--help] # # The internal /stow skill owns curation judgement; this command owns only the -# mechanical inputs a cascade needs: which homes exist, what each home's own -# startup-memory accounting says right now, and how the sweep can reach it. +# mechanical inputs a cascade needs: which homes exist and how the sweep can +# reach each one. # # Enumeration comes from data/secondmates.md, the registry that already refuses # a duplicate id, a duplicate home, and an overlapping home, so each registered -# secondmate is emitted exactly once and no home is accounted twice. A home's -# budget is that home's alone: this command never sums a fleet total, because -# config/startup-memory-budget is a per-home allowance. +# secondmate is emitted exactly once. # # Each home is reported as one blank-line-separated key=value stanza: # secondmate= # placement=local|remote (host= on a remote route) # home= -# budget_report=ok|error|timeout (report lines follow on ok) # transport=agent|direct|deferred|unavailable # reason= (whenever a step did not complete) # @@ -28,13 +25,12 @@ # files in place. # deferred - a remote home with no live agent. There is deliberately no # generic remote write path (bin/fm-remote-file.sh put reaches -# only the handoff outbox), so that home is accounted read-only -# and curated by its next cascade once its agent is back. -# unavailable - the home's own accounting did not complete, so no transport -# conclusion is safe. +# only the handoff outbox), so that home is curated by its next +# cascade once its agent is back. +# unavailable - the local home failed validation or the remote endpoint probe +# failed, so no transport conclusion is safe. # -# Every step that crosses a host, plus each home's own accounting, runs under -# one hard bound (FM_STOW_CASCADE_TIMEOUT seconds, default 60), so a slow or +# Every step that crosses a host runs under one hard bound (FM_STOW_CASCADE_TIMEOUT seconds, default 60), so a slow or # unreachable home reports an exception and the sweep continues instead of # blocking the primary's own /stow. A local endpoint probe reads this host's # recorded backend in process, like every other caller of that contract. @@ -48,7 +44,7 @@ set -u usage() { - sed -n '2,47{s/^# \{0,1\}//;p;}' "$0" + sed -n '2,43{s/^# \{0,1\}//;p;}' "$0" } case "${1:-}" in @@ -64,7 +60,6 @@ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}" DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}" STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" REGISTRY="$DATA/secondmates.md" -BUDGET_CMD=fm-startup-memory-budget.sh SUB_HOME_MARKER="${SUB_HOME_MARKER:-.fm-secondmate-home}" # shellcheck source=bin/fm-ff-lib.sh @@ -146,11 +141,11 @@ resolve_remote_transport() { # fi run_step "$SCRIPT_DIR/fm-on.sh" "$id" fm-remote-secondmate-control.sh state "$id" || rc=$? if [ "$rc" -eq 124 ]; then - set_transport deferred "remote endpoint probe exceeded the ${BOUND}s bound" + set_transport unavailable "remote endpoint probe exceeded the ${BOUND}s bound" return 0 fi if [ "$rc" -ne 0 ]; then - set_transport deferred 'remote endpoint probe failed' + set_transport unavailable 'remote endpoint probe failed' return 0 fi case "$(tail -1 "$STEP_OUT")" in @@ -188,19 +183,17 @@ while IFS= read -r line || [ -n "$line" ]; do remote=$SECONDMATE_REGISTRY_REMOTE host=$SECONDMATE_REGISTRY_HOST total=$((total + 1)) - rc=0 printf '\n' emit "secondmate=$id" if [ "$remote" -eq 1 ]; then emit 'placement=remote' emit "host=$host" emit "home=$home" - run_step "$SCRIPT_DIR/fm-on.sh" "$id" "$BUDGET_CMD" report || rc=$? + resolve_remote_transport "$id" else emit 'placement=local' if ! validate_secondmate_home "$id" "$home"; then emit "home=$home" - emit 'budget_report=error' emit 'transport=unavailable' emit "reason=$VALIDATION_ERROR" exceptions=$((exceptions + 1)) @@ -208,35 +201,6 @@ while IFS= read -r line || [ -n "$line" ]; do fi resolved=$VALIDATED_HOME emit "home=$resolved" - # Every FM_*_OVERRIDE is restated so a caller's own override cannot leak - # this home's memory files into the accounting of another home. - run_step env \ - FM_ROOT_OVERRIDE="$FM_ROOT" \ - FM_HOME="$resolved" \ - FM_STATE_OVERRIDE="$resolved/state" \ - FM_DATA_OVERRIDE="$resolved/data" \ - FM_CONFIG_OVERRIDE="$resolved/config" \ - "$SCRIPT_DIR/$BUDGET_CMD" report || rc=$? - fi - if [ "$rc" -eq 124 ]; then - emit 'budget_report=timeout' - emit 'transport=unavailable' - emit "reason=this home's own accounting exceeded the ${BOUND}s bound" - exceptions=$((exceptions + 1)) - continue - fi - if [ "$rc" -ne 0 ]; then - emit 'budget_report=error' - emit 'transport=unavailable' - emit "reason=this home's own accounting failed" - exceptions=$((exceptions + 1)) - continue - fi - emit 'budget_report=ok' - cat "$STEP_OUT" - if [ "$remote" -eq 1 ]; then - resolve_remote_transport "$id" - else resolve_local_transport "$id" "$resolved" fi emit "transport=$TRANSPORT" diff --git a/bin/fm-test-run.sh b/bin/fm-test-run.sh index 222bf217d9c..617ce43d802 100755 --- a/bin/fm-test-run.sh +++ b/bin/fm-test-run.sh @@ -342,7 +342,7 @@ family_for_basename() { fm-secondmate-liveness.test.sh|fm-secondmate-reconcile.test.sh|\ fm-secondmate-restart.test.sh|fm-remote-secondmate-relaunch.test.sh|\ fm-secondmate-safety.test.sh|fm-secondmate-sync.test.sh|\ - fm-startup-memory-budget.test.sh|fm-stow-cascade.test.sh|\ + fm-stow-cascade.test.sh|\ fm-send-secondmate-marker.test.sh|fm-shared-captain-inheritance.test.sh) printf '%s\n' secondmate ;; @@ -849,7 +849,6 @@ tests/fm-spawn-dispatch-profile.test.sh 197548 tests/fm-spawn-orca-worktree.test.sh 2400 tests/fm-spawn-pool-base-freshen.test.sh 68652 tests/fm-spawn-worktree-settle.test.sh 9309 -tests/fm-startup-memory-budget.test.sh 8086 tests/fm-startup-network.test.sh 72106 tests/fm-stat-shadowing.test.sh 75 tests/fm-stow-cascade.test.sh 3058 @@ -1494,10 +1493,6 @@ families_for_changed_path() { printf '%s\n' live-harness-optin printf '%s\n' afk ;; - bin/fm-startup-memory-budget.sh|bin/fm-startup-memory-budget-lib.sh) - printf '%s\n' secondmate - printf '%s\n' session-bootstrap - ;; bin/fm-secondmate*|bin/fm-remote*|bin/fm-on.sh|bin/fm-home-seed.sh|\ bin/fm-backlog-handoff.sh|bin/fm-backlog-receive.sh|bin/fm-procevent-remote-reply.sh|\ bin/fm-config-inherit-lib.sh|bin/fm-config-push.sh|bin/fm-shared*|\ diff --git a/docs/architecture.md b/docs/architecture.md index 05db641b2c6..9ddb7d82c66 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -477,7 +477,7 @@ The same pass also persists open-work record state the session is holding - fili It is deliberately not a reconciliation of durable records against repository or PR reality: its input is the volatile context, so it can only preserve what the session still knows, and no reconciliation that outlives a session exists today. Task-scoped notes use `bin/fm-tasks-axi.sh show --full` followed by `bin/fm-tasks-axi.sh update --body-file `, adding `--archive-body` when the prior body should remain recoverable. The stow pass never writes a skill, but a separately executed, captain-approved migration may move conditional knowledge into a user-owned local skill excluded from the Firstmate clone; changes to Firstmate's tracked skills remain deliberate repository work through the normal PR pipeline. -Invoked in a primary home, `/stow` then cascades the same sweep to every registered secondmate, enumerated through `bin/fm-stow-cascade.sh`: each home is accounted and curated against its own startup-memory allowance, a live secondmate sweeps its own session, and a slow or unreachable home is reported as an exception rather than blocking the primary. +Invoked in a primary home, `/stow` then cascades the same sweep to every registered secondmate, enumerated through `bin/fm-stow-cascade.sh`: each home is curated on its own memory, a live secondmate sweeps its own session, and a slow or unreachable home is reported as an exception rather than blocking the primary. ## Local clones stay fresh diff --git a/docs/configuration.md b/docs/configuration.md index 3bf8ffee64b..d02bb057562 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,7 +10,7 @@ Start with the directory layout, then use the setting reference for the behavior | Firstmate's code, private files, or project location | [FM_HOME](#fm_home) and [operational home layout](#operational-home-layout-and-state) | | Task windows and worker tools | [Runtime backend](#runtime-backend-configbackend--fm_backend) and [harness support](#harness-support) | | Worker permissions, accounts, or environment | [Claude permission mode](#claude-permission-mode-configclaude-permission-mode), [worker account pin](#worker-account-pin-configclaude-account-configpi-account), and [worker launch environment](#worker-launch-environment-configlaunch-env-allowlist) | -| Backlog, preferences, and memory | [Backlog backend](#backlog-backend-taskstoml--configbacklog-backend), [captain preferences](#captain-preferences-datacaptainmd--datacaptain-sharedmd), and [startup memory budget](#startup-memory-budget-configstartup-memory-budget) | +| Backlog, preferences, and memory | [Backlog backend](#backlog-backend-taskstoml--configbacklog-backend), [captain preferences](#captain-preferences-datacaptainmd--datacaptain-sharedmd), and [stow pass horizon](#stow-pass-horizon-configstow-pass-horizon) | | Supervision and presentation | [Pi supervision branch](#pi-supervision-branch), [supervision host](#supervision-host-configsupervision-host), and [Calm preference](#calm-preference-configcalm) | | Persistent secondmates | [Secondmate routes](#secondmate-routes-datasecondmatesmd) | | Per-run overrides and tuning | [Environment variables](#environment-variables) | @@ -638,36 +638,13 @@ The file is created lazily on first learning and follows the internal [`stow` sk There is no shared learnings file by captain decision. -## Startup memory budget (config/startup-memory-budget) - -`config/startup-memory-budget` is the primary-authoritative per-home allowance for the startup prompt-memory surface: `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md` together. -The locked mutable bootstrap path materializes its visible default of `7500` estimated tokens in a primary home when the file is absent. - -### Set and validate the budget - -To select another allowance, replace the primary home's file with one valid positive value in the exact format below; the next locked bootstrap convergence or `bin/fm-config-push.sh` propagates it to registered secondmates. -A secondmate does not create an independent default and instead receives the primary value through the inherited-local-material contract in [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md). - -The file must be one positive base-10 integer followed by exactly one newline in a regular, single-linked file beneath a non-symlinked `config/` directory. -Malformed, multi-line, symlinked, hardlinked, special, or otherwise unsafe values are rejected rather than treated as a default. - -### Accounting and curation - -Use `bin/fm-startup-memory-budget.sh read` to validate and print the effective value, or `bin/fm-startup-memory-budget.sh report` to account for the three files. -The stable local estimate is `ceil(UTF-8 bytes / 3)` per file, a conservative portable approximation rather than a provider-exact tokenizer. - -An inherited `data/captain-shared.md` counts in a secondmate's total but remains primary-owned and read-only there. -The internal [`/stow` skill](../.agents/skills/stow/SKILL.md) owns curation and its automatic secondmate cascade, which accounts every home against this same per-home allowance separately rather than against a fleet total. - -The helper's header owns exact parsing, publication, and report output mechanics. - ## Stow pass horizon (config/stow-pass-horizon) `config/stow-pass-horizon` is an optional local, gitignored presence flag that opts this home in to the pass-count decay horizon in the internal [`/stow` skill](../.agents/skills/stow/SKILL.md). Without it a `/stow` pass decays memory entries on their wall-clock horizons alone - 30 days for `aging`, 7 days for `perishable` - which is the default and unchanged behavior. With it, an entry is also stale after 10 passes (`aging`) or 3 passes (`perishable`) that evaluated it without reinforcing it, whichever horizon it reaches first. -Opt in for a home that stows often enough that entries never sit unreinforced for a wall-clock horizon, so memory only grows against the startup-memory budget above; a home that stows rarely already exceeds its date horizon on a single pass and gains nothing. +Opt in for a home that stows often enough that entries never sit unreinforced for a wall-clock horizon, so memory only grows; a home that stows rarely already exceeds its date horizon on a single pass and gains nothing. The flag is per home and is not inherited by secondmate homes, because stow cadence is a property of the home doing the stowing. Only the file's presence is read, so its contents are ignored; remove it to return to the default contract on the next pass. diff --git a/docs/verification/stow-memory.md b/docs/verification/stow-memory.md index ba8d8c562e4..4bfae2fc181 100644 --- a/docs/verification/stow-memory.md +++ b/docs/verification/stow-memory.md @@ -4,7 +4,6 @@ Audience: maintainer verification. This record supports the active guarantee that Firstmate can discover and JIT-load a user-owned local skill excluded through the clone's `.git/info/exclude`. The internal [`stow` skill](../../.agents/skills/stow/SKILL.md) owns tiering, curation, archival, offload, and completion-receipt behavior. -[`docs/configuration.md`](../configuration.md) owns the current operator-facing startup-memory setting and estimate. ## Git-excluded local skill discovery and loading diff --git a/tests/fm-backend-herdr-presentation-e2e.test.sh b/tests/fm-backend-herdr-presentation-e2e.test.sh index bb5c247e3c1..36dbb2a1942 100755 --- a/tests/fm-backend-herdr-presentation-e2e.test.sh +++ b/tests/fm-backend-herdr-presentation-e2e.test.sh @@ -994,7 +994,7 @@ touch "$SECOND_HOME_A/state/.last-watcher-beat" "$SECOND_HOME_B/state/.last-watc # may write config/herdr-presentation-spaces. git -C "$SECOND_HOME_A" init -q git -C "$SECOND_HOME_B" init -q -printf 'config/herdr-presentation-spaces\nconfig/crew-harness\nconfig/crew-dispatch.json\nconfig/backlog-backend\nconfig/backend\nconfig/startup-memory-budget\n' \ +printf 'config/herdr-presentation-spaces\nconfig/crew-harness\nconfig/crew-dispatch.json\nconfig/backlog-backend\nconfig/backend\n' \ > "$SECOND_HOME_A/.gitignore" cp "$SECOND_HOME_A/.gitignore" "$SECOND_HOME_B/.gitignore" git -C "$SECOND_HOME_A" add .gitignore diff --git a/tests/fm-backlog-atomicity.test.sh b/tests/fm-backlog-atomicity.test.sh index 44b0534ab80..ec65cdd162e 100755 --- a/tests/fm-backlog-atomicity.test.sh +++ b/tests/fm-backlog-atomicity.test.sh @@ -543,24 +543,23 @@ SH chmod +x "$case_dir/fakebin/rm" } -remove_data_during_startup_budget_check() { # - local case_dir=$1 real data saved budget - real=$(command -v stat) +# Removes the backlog data directory from inside bootstrap's first backlog gate: +# its memoized tasks-axi version probe runs after that gate's own data checks +# passed, so reconciliation's re-check is the first to see the loss. +remove_data_during_backlog_gate() { # + local case_dir=$1 real data saved + real=$(command -v tasks-axi) data="$(home_of "$case_dir")/data" saved="$case_dir/bootstrap-data" - budget="$(home_of "$case_dir")/config/startup-memory-budget" - printf '7500\n' > "$budget" - cat > "$case_dir/fakebin/stat" < "$case_dir/fakebin/tasks-axi" < "$case_dir/data-removed" - fi -done +if [ "\${1:-}" = --version ] && [ ! -e "$case_dir/data-removed" ]; then + mv "$data" "$saved" || exit 1 + : > "$case_dir/data-removed" +fi exec "$real" "\$@" SH - chmod +x "$case_dir/fakebin/stat" + chmod +x "$case_dir/fakebin/tasks-axi" } break_meta_publication() { # @@ -2615,23 +2614,19 @@ test_bootstrap_refuses_a_symlinked_state_directory_before_reconciliation() { test_bootstrap_stops_when_data_disappears_before_reconciliation() { local case_dir id saved out rc=0 - # The data-removal fault is injected by a fake stat on PATH; on Darwin the - # budget link-count helper now calls /usr/bin/stat directly, so the fake can - # never fire there. Skip the Darwin run of this case. - if [ "$(uname)" = Darwin ]; then - pass "bootstrap data-disappears fault injection is PATH-based; skipped on Darwin where stat is /usr/bin/stat" - return - fi id=atomic-bootstrap-data-race-b11 case_dir=$(make_home bootstrap-data-race) add_item "$case_dir" "$id" start_item "$case_dir" "$id" write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-bootstrap-race" - remove_data_during_startup_budget_check "$case_dir" + remove_data_during_backlog_gate "$case_dir" saved="$case_dir/bootstrap-data" out=$(run_bootstrap "$case_dir") || rc=$? [ "$rc" -ne 0 ] || fail "bootstrap absorbed a fatal reconciliation addressing error: $out" + assert_present "$case_dir/data-removed" "the data-removal fault never fired" + assert_contains "$out" "backlog reconciliation cannot access configured data directory" \ + "the data loss was not caught by reconciliation's own re-check: $out" assert_present "$(home_of "$case_dir")/state/$id.meta" \ "fatal bootstrap reconciliation removed the task record" [ "$(tasks-axi show "$id" --file "$saved/backlog.md" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \ diff --git a/tests/fm-bootstrap.test.sh b/tests/fm-bootstrap.test.sh index 5e2988870dc..7a9d78e1fde 100755 --- a/tests/fm-bootstrap.test.sh +++ b/tests/fm-bootstrap.test.sh @@ -833,7 +833,6 @@ make_routine_bootstrap_fixture() { printf '%s\n' '.fm-secondmate-home' printf '%s\n' 'config/crew-harness' printf '%s\n' 'config/crew-dispatch.json' - printf '%s\n' 'config/startup-memory-budget' } > "$root/.gitignore" printf '%s\n' 'instructions' > "$root/AGENTS.md" mkdir -p "$root/bin" "$root/.agents/skills" @@ -889,6 +888,19 @@ test_routine_bootstrap_confirmations_are_silent() { pass "bootstrap keeps routine tasks-axi, harness, dispatch, and already-live liveness confirmations silent" } +# A primary bootstrap materializes no local setting the captain did not choose: +# the config directory holds exactly what the fixture seeded. +test_primary_bootstrap_adds_no_config() { + local case_dir out listing + case_dir="$TMP_ROOT/routine-config" + out=$(run_routine_bootstrap_fixture bash "$case_dir") + [ -z "$out" ] || fail "routine bootstrap should be silent, got: $out" + listing=$(find "$case_dir/home/config" -mindepth 1 -maxdepth 1 -exec basename {} \; | LC_ALL=C sort | tr '\n' ' ') + [ "$listing" = "crew-dispatch.json crew-harness " ] \ + || fail "primary bootstrap created config the captain did not choose: $listing" + pass "primary bootstrap adds no config file of its own" +} + test_routine_bootstrap_contract_runs_under_system_bash() { local out [ -x /bin/bash ] || { pass "bootstrap routine contract skipped without /bin/bash"; return; } @@ -1272,6 +1284,7 @@ test_fleet_sync_timeout_explicit_override_wins test_fleet_sync_timeout_empty_override_uses_default test_fleet_sync_timeout_is_computed_before_launch test_routine_bootstrap_confirmations_are_silent +test_primary_bootstrap_adds_no_config test_routine_bootstrap_contract_runs_under_system_bash test_network_phase_partitions_the_run test_network_sweeps_recheck_lock_ownership diff --git a/tests/fm-secondmate-harness.test.sh b/tests/fm-secondmate-harness.test.sh index f7a1f394f40..3866ba87f58 100755 --- a/tests/fm-secondmate-harness.test.sh +++ b/tests/fm-secondmate-harness.test.sh @@ -15,11 +15,10 @@ # B) Inheritance. The primary pushes a declared, extensible set of LOCAL # (gitignored) config items - config/crew-dispatch.json, config/crew-harness, # config/backlog-backend, config/backend, config/herdr-presentation-spaces, -# config/startup-memory-budget, config/trace-context, and -# config/supervision-host-off - +# config/trace-context, and config/supervision-host-off - # down into each secondmate home's config/, so the secondmate's OWN crewmates, # dispatch profiles, backlog backend, runtime-backend default, Herdr -# presentation choice, startup-memory budget, and trace context inherit the +# presentation choice, and trace context inherit the # primary's settings. For config/herdr-presentation-spaces, an absent # primary file and an absent destination file both mean the same # unconfigured default, so the generic absence mirror converges that item @@ -1072,7 +1071,7 @@ new_world() { printf 'projects/\nstate/\ndata/\n.no-mistakes/\n' [ "$dispatch_ignore" = no ] || printf 'config/crew-dispatch.json\n' printf 'config/crew-harness\nconfig/secondmate-harness\nconfig/backlog-backend\n' - printf 'config/backend\nconfig/herdr-presentation-spaces\nconfig/startup-memory-budget\n' + printf 'config/backend\nconfig/herdr-presentation-spaces\n' printf 'config/claude-permission-mode\nconfig/claude-worker-settings.json\n' } > "$w/main/.gitignore" printf 'v1\n' > "$w/main/AGENTS.md" @@ -1238,15 +1237,15 @@ inbox_stream() { # } reread_instruction_path() { - local home=$1 state path latest= + local home=$1 state path latest state="$(cd "$home/state" && pwd -P)" - for path in "$state"/.fm-inherited-config-reread.*; do - case "$path" in - *.pending) continue ;; - esac - [ -f "$path" ] && [ ! -L "$path" ] || continue - latest="$path" - done + latest=$( + for path in "$state"/.fm-inherited-config-reread.*; do + [[ "$path" == *.pending ]] && continue + [ -f "$path" ] && [ ! -L "$path" ] || continue + printf '%s\n' "$path" + done | fm_config_reread_sort_generations | tail -n 1 + ) [ -n "$latest" ] || return 1 printf '%s\n' "$latest" } @@ -1428,10 +1427,9 @@ test_bootstrap_sweep_defers_dispatch_on_stale_unignored_home() { pass "B9 bootstrap sweep defers new inherited config until the home ignores it" } -# The primary bootstrap always materializes the startup-memory default, so an -# otherwise empty inherited surface converges that one visible value while +# An empty inherited surface pushes nothing into the secondmate home, while # ordinary tracked-file fast-forward behavior remains unchanged. -test_bootstrap_sweep_materializes_and_inherits_memory_default() { +test_bootstrap_sweep_default_only_inherits_nothing() { local w c1 w=$(new_world boot-noop) c1=$(git -C "$w/main" rev-parse HEAD) @@ -1448,13 +1446,16 @@ test_bootstrap_sweep_materializes_and_inherits_memory_default() { [ -e "$w/sm/config/crew-dispatch.json" ] && fail "default-only sweep created a home crew-dispatch.json" [ -e "$w/sm/config/crew-harness" ] && fail "default-only sweep created a home crew-harness" [ -e "$w/sm/config/backend" ] && fail "default-only sweep created a home backend" - [ "$(cat "$w/home/config/startup-memory-budget")" = 7500 ] \ - || fail "primary bootstrap did not materialize the startup-memory default" - [ "$(cat "$w/sm/config/startup-memory-budget")" = 7500 ] \ - || fail "default-only sweep did not converge startup-memory-budget" + local created + created=$(find "$w/home/config" -mindepth 1 -maxdepth 1) + [ -z "$created" ] || fail "primary bootstrap created config of its own: $created" + if [ -d "$w/sm/config" ]; then + created=$(find "$w/sm/config" -mindepth 1 -maxdepth 1) + [ -z "$created" ] || fail "default-only sweep pushed config into the home: $created" + fi [ "$(git -C "$w/sm" rev-parse HEAD)" = "$head" ] \ || fail "default-only sweep did not still fast-forward the tracked files" - pass "B10 bootstrap sweep materializes and inherits the startup-memory default while fast-forwarding" + pass "B10 bootstrap sweep inherits nothing from an empty primary config while fast-forwarding" } # config/backend: present and absent primary state converges exactly. @@ -2175,8 +2176,8 @@ SH test_config_reread_write_failure_retains_exact_retry_generation() { local w head fakebin real_mv retry_dir out status stage_path log retry_out retry_status instr - local old_instr new_instr - w=$(new_world config-reread-write-retry) + local old_instr new_instr real_date clock=${1:-normal} + w=$(new_world "config-reread-write-retry-$clock") head=$(git -C "$w/main" rev-parse HEAD) add_sm_worktree "$w" sm "$head" mkdir -p "$w/sm/config" "$w/sm/state" @@ -2184,6 +2185,23 @@ test_config_reread_write_failure_retains_exact_retry_generation() { printf 'codex\n' > "$w/home/config/crew-harness" fakebin=$(make_fake_toolchain "$w") real_mv=$(command -v mv) + if [ "$clock" = backwards ]; then + real_date=$(command -v date) + cat > "$fakebin/date" < "$fakebin/mv" < "$w/main/bin/fm-spawn.sh" <> '$log' printf '%s' codex > '$w/sm/config/crew-harness' -printf '%s\n' 7500 > '$w/sm/config/startup-memory-budget' SH chmod +x "$w/main/bin/fm-spawn.sh" fakebin=$(make_fake_toolchain "$w") @@ -2770,7 +2787,7 @@ test_spawn_fallback_chain_and_crew_scout_unaffected test_bootstrap_sweep_propagates_and_reconverges test_bootstrap_sweep_propagates_when_tracked_current test_bootstrap_sweep_defers_dispatch_on_stale_unignored_home -test_bootstrap_sweep_materializes_and_inherits_memory_default +test_bootstrap_sweep_default_only_inherits_nothing test_backend_inheritance_present_and_absent test_spawn_secondmate_claude_permission_mode_auto test_spawn_secondmate_claude_grants_parent_inbox_dir @@ -2787,6 +2804,7 @@ test_config_reread_per_home_changed_sets_and_exact_bytes test_config_reread_isolation_and_absent_and_send_failure test_config_reread_publication_failure_retries_exact_generation test_config_reread_write_failure_retains_exact_retry_generation +test_config_reread_write_failure_retains_exact_retry_generation backwards test_config_reread_exact_temp_survives_adoption_failure test_config_reread_serializes_concurrent_pushes test_config_reread_full_retry_queue_drains_before_new_push diff --git a/tests/fm-startup-memory-budget.test.sh b/tests/fm-startup-memory-budget.test.sh deleted file mode 100755 index fa4a6daca22..00000000000 --- a/tests/fm-startup-memory-budget.test.sh +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env bash -# Behavioral coverage for the visible startup-memory budget, its safe parser, -# accounting command, primary-to-secondmate convergence, and exact reread bytes. -set -u - -# shellcheck source=tests/lib.sh -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -BASE_PATH=${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin} -TMP_ROOT=$(fm_test_tmproot fm-startup-memory-budget) -BUDGET="$ROOT/bin/fm-startup-memory-budget.sh" -BOOTSTRAP="$ROOT/bin/fm-bootstrap.sh" -CONFIG_PUSH="$ROOT/bin/fm-config-push.sh" - -make_fake_toolchain() { - local dir=$1 fakebin - fakebin=$(fm_fakebin "$dir") - fm_fake_exit0 "$fakebin" node chrome-devtools-axi - fm_fake_version_tool "$fakebin" lavish-axi FM_FAKE_LAVISH_AXI_VERSION 0.1.80 - cat > "$fakebin/gh-axi" <<'SH' -#!/usr/bin/env bash -if [ "${1:-}" = --version ]; then - printf '%s\n' '0.1.29' -fi -exit 0 -SH - cat > "$fakebin/quota-axi" <<'SH' -#!/usr/bin/env bash -if [ "${1:-}" = --version ]; then - printf '%s\n' 'quota-axi 0.1.51 (fake)' -fi -exit 0 -SH - cat > "$fakebin/gh" <<'SH' -#!/usr/bin/env bash -exit 0 -SH - cat > "$fakebin/treehouse" <<'SH' -#!/usr/bin/env bash -if [ "${1:-}" = get ] && [ "${2:-}" = --help ]; then - printf '%s\n' 'Usage: treehouse get [--lease]' -fi -SH - cat > "$fakebin/no-mistakes" <<'SH' -#!/usr/bin/env bash -if [ "${1:-}" = --version ]; then - printf '%s\n' 'no-mistakes version v1.46.0 (fake)' -fi -SH - cat > "$fakebin/tasks-axi" <<'SH' -#!/usr/bin/env bash -case "${1:-}:${2:-}" in - --version:*) printf '%s\n' '0.2.6' ;; - update:--help) printf '%s\n' '--archive-body' ;; - mv:--help) printf '%s\n' 'usage: tasks-axi mv [...]' ;; -esac -SH - cat > "$fakebin/tmux" <<'SH' -#!/usr/bin/env bash -[ -z "${FM_FAKE_TMUX_LOG:-}" ] || printf '%s\n' "$*" >> "$FM_FAKE_TMUX_LOG" -case "$*" in - *display-message*'#{pane_current_command}'*) printf '%s\n' codex ;; - *display-message*'#{pane_id}'*) printf '%s\n' '%1' ;; - *display-message*'#{cursor_y}'*) printf '%s\n' 0 ;; - *list-windows*) printf '%s\n' fm-sm ;; - *capture-pane*) printf '❯\n' ;; -esac -exit 0 -SH - chmod +x "$fakebin"/* - printf '%s\n' "$fakebin" -} - -new_bootstrap_world() { - local name=$1 world root home - world="$TMP_ROOT/$name" - root="$world/root" - home="$world/home" - mkdir -p "$home/config" "$home/data" "$home/state" "$root/bin" - git init -q -b main "$root" - printf '%s\n' 'config/' > "$root/.gitignore" - printf '%s\n' '# Firstmate test root' > "$root/AGENTS.md" - printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$root/bin/placeholder.sh" - chmod +x "$root/bin/placeholder.sh" - git -C "$root" add -A - git -C "$root" -c user.name=fmtest -c user.email=fmtest@example.invalid commit -qm initial - printf '%s|%s\n' "$root" "$home" -} - -run_bootstrap() { - local root=$1 home=$2 fakebin=$3 - PATH="$fakebin:$BASE_PATH" FM_BACKEND=tmux FM_HOME="$home" FM_ROOT_OVERRIDE="$root" \ - "$BOOTSTRAP" -} - -test_primary_bootstrap_materializes_visible_default() { - local rec root home fakebin out second - rec=$(new_bootstrap_world materialize) - root=${rec%%|*} - home=${rec#*|} - fakebin=$(make_fake_toolchain "$TMP_ROOT/materialize") - - out=$(run_bootstrap "$root" "$home" "$fakebin") - [ -z "$out" ] || fail "default materialization should stay quiet, got: $out" - [ "$(<"$home/config/startup-memory-budget")" = 7500 ] \ - || fail "bootstrap did not materialize the visible 7500 default" - [ "$(FM_HOME="$home" "$BUDGET" read)" = 7500 ] \ - || fail "read command did not expose the generated default" - - printf '321\n' > "$home/config/startup-memory-budget" - run_bootstrap "$root" "$home" "$fakebin" >/dev/null - [ "$(<"$home/config/startup-memory-budget")" = 321 ] \ - || fail "bootstrap replaced a valid captain-selected budget" - - second="$TMP_ROOT/materialize/secondmate" - mkdir -p "$second/config" "$second/data" "$second/state" - printf '%s\n' sm > "$second/.fm-secondmate-home" - run_bootstrap "$root" "$second" "$fakebin" >/dev/null - [ ! -e "$second/config/startup-memory-budget" ] \ - || fail "secondmate bootstrap created an independent budget instead of awaiting inheritance" - pass "primary bootstrap materializes only the visible default and preserves valid captain choices" -} - -expect_rejected_read() { - local home=$1 expected=$2 out rc - set +e - out=$(FM_HOME="$home" "$BUDGET" read 2>&1) - rc=$? - set -e - [ "$rc" -ne 0 ] || fail "unsafe budget unexpectedly parsed: $expected" - assert_contains "$out" "$expected" "unsafe budget rejection was not specific" -} - -test_safe_parser_rejects_ambiguous_and_unsafe_values() { - local home outside - home="$TMP_ROOT/parser-home" - mkdir -p "$home/config" "$home/data" - printf '42\n' > "$home/config/startup-memory-budget" - [ "$(FM_HOME="$home" "$BUDGET" read)" = 42 ] || fail "valid positive decimal budget was rejected" - - printf '0\n' > "$home/config/startup-memory-budget" - expect_rejected_read "$home" 'value must be one positive decimal integer' - printf '42\nextra\n' > "$home/config/startup-memory-budget" - expect_rejected_read "$home" 'value must be one positive decimal integer' - printf '+42\n' > "$home/config/startup-memory-budget" - expect_rejected_read "$home" 'value must be one positive decimal integer' - - outside="$TMP_ROOT/parser-outside" - printf '77\n' > "$outside" - rm -f "$home/config/startup-memory-budget" - ln -s "$outside" "$home/config/startup-memory-budget" - expect_rejected_read "$home" 'file is symlinked' - [ "$(<"$outside")" = 77 ] || fail "symlink rejection changed its external target" - - rm -f "$home/config/startup-memory-budget" - ln "$outside" "$home/config/startup-memory-budget" - expect_rejected_read "$home" 'file is hardlinked' - [ "$(<"$outside")" = 77 ] || fail "hardlink rejection changed its external source" - - rm -f "$home/config/startup-memory-budget" - rm -rf "$home/config" - ln -s "$TMP_ROOT/parser-config-target" "$home/config" - mkdir -p "$TMP_ROOT/parser-config-target" - printf '88\n' > "$TMP_ROOT/parser-config-target/startup-memory-budget" - expect_rejected_read "$home" 'config directory is symlinked' - pass "budget parser accepts one exact positive value and rejects malformed or unsafe inputs" -} - -test_budget_accounting_reports_all_three_files_and_safe_failure() { - local home out rc outside - home="$TMP_ROOT/accounting-home" - mkdir -p "$home/config" "$home/data" - printf '10\n' > "$home/config/startup-memory-budget" - printf 'abc\n' > "$home/data/captain.md" - printf 'abcdef\n' > "$home/data/captain-shared.md" - - out=$(FM_HOME="$home" "$BUDGET" report) - assert_contains "$out" 'estimator=ceil(UTF-8 bytes / 3) conservative-local-estimate' \ - "report did not name the stable estimator" - assert_contains "$out" 'file=data/captain.md bytes=4 estimated_tokens=2 status=present' \ - "report did not account for captain memory" - assert_contains "$out" 'file=data/captain-shared.md bytes=7 estimated_tokens=3 status=present' \ - "report did not account for shared memory" - assert_contains "$out" 'file=data/learnings.md bytes=0 estimated_tokens=0 status=absent' \ - "report did not account for absent learnings" - assert_contains "$out" 'total_estimated_tokens=5' "report total was not the sum of all three files" - assert_contains "$out" 'budget_status=within-budget' "report did not classify the initial total" - - printf 'abcdefabcdefabcdefabcdef\n' > "$home/data/learnings.md" - out=$(FM_HOME="$home" "$BUDGET" report) - assert_contains "$out" 'budget_status=over-budget' "report did not surface an over-budget total" - - outside="$TMP_ROOT/accounting-outside" - printf 'outside\n' > "$outside" - rm -f "$home/data/captain.md" - ln -s "$outside" "$home/data/captain.md" - set +e - out=$(FM_HOME="$home" "$BUDGET" report 2>&1) - rc=$? - set -e - expect_code 2 "$rc" "unsafe memory input should fail the accounting command" - assert_contains "$out" 'memory file is not an ordinary regular file' \ - "accounting failure did not identify the unsafe memory file" - [ "$(<"$outside")" = outside ] || fail "accounting failure changed a symlink target" - pass "budget accounting sums the three startup files and reports safe failures" -} - -new_propagation_world() { - local world=$1 root="$1/root" home="$1/home" sm="$1/sm" head - mkdir -p "$home/config" "$home/data" "$home/state" "$root/bin" - touch "$home/state/.last-watcher-beat" - git init -q -b main "$root" - printf '%s\n' 'config/' > "$root/.gitignore" - printf '%s\n' '# Firstmate test root' > "$root/AGENTS.md" - printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$root/bin/placeholder.sh" - chmod +x "$root/bin/placeholder.sh" - git -C "$root" add -A - git -C "$root" -c user.name=fmtest -c user.email=fmtest@example.invalid commit -qm initial - head=$(git -C "$root" rev-parse HEAD) - git -C "$root" worktree add -q --detach "$sm" "$head" - printf '%s\n' sm > "$sm/.fm-secondmate-home" - mkdir -p "$sm/config" "$sm/data" "$sm/state" "$sm/projects" - { - printf 'window=firstmate:fm-sm\n' - printf 'kind=secondmate\n' - printf 'harness=codex\n' - printf 'home=%s\n' "$sm" - } > "$home/state/sm.meta" - printf '%s|%s|%s\n' "$root" "$home" "$sm" -} - -latest_reread_instruction() { - local home=$1 state path latest= - state=$(cd "$home/state" && pwd -P) || return 1 - for path in "$state"/.fm-inherited-config-reread.*; do - case "$path" in *.pending) continue ;; esac - [ -f "$path" ] && [ ! -L "$path" ] || continue - latest=$path - done - [ -n "$latest" ] || return 1 - printf '%s\n' "$latest" -} - -inbox_record_body() { # - bash -c '. "$1"; fm_task_inbox_body "$2"' _ "$ROOT/bin/fm-task-inbox-lib.sh" "$1" -} - -run_config_push() { - local root=$1 home=$2 fakebin=$3 log=$4 - PATH="$fakebin:$BASE_PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$root" FM_SEND_SETTLE=0 \ - FM_FAKE_TMUX_LOG="$log" "$CONFIG_PUSH" -} - -test_primary_budget_converges_with_exact_reread_and_safe_failures() { - local world="$TMP_ROOT/propagation" rec root home sm fakebin log out rc instruction expected outside - mkdir -p "$world" - rec=$(new_propagation_world "$world") - root=${rec%%|*} - rec=${rec#*|} - home=${rec%%|*} - sm=${rec#*|} - fakebin=$(make_fake_toolchain "$world") - log="$world/tmux.log" - - printf '321\n' > "$home/config/startup-memory-budget" - out=$(run_config_push "$root" "$home" "$fakebin" "$log") - assert_contains "$out" 'startup-memory-budget: pushed' \ - "config push did not report the new budget as inherited" - [ "$(<"$sm/config/startup-memory-budget")" = 321 ] \ - || fail "secondmate did not receive the primary budget bytes" - instruction=$(latest_reread_instruction "$sm") || fail "budget propagation did not publish a reread instruction" - expected=$(printf '%s\n\n%s\n%s\n321\n%s' \ - 'These inherited config files changed. Re-read and apply their exact contents at every future intake. They are defaults/rules and do not remove your judgment to choose differently when warranted.' \ - 'config/startup-memory-budget' \ - '-----BEGIN config/startup-memory-budget-----' \ - '-----END config/startup-memory-budget-----') - [ "$(<"$instruction")" = "$expected" ] \ - || fail "budget reread payload was not the exact destination bytes" - assert_contains "$(inbox_record_body "$home/state/sm.inbox/001.msg")" "CONFIG_REREAD: $instruction" \ - "budget propagation did not enqueue the pointer to its exact reread generation" - assert_contains "$(<"$log")" "Firstmate instruction waiting: list " \ - "budget propagation did not ring the durable inbox doorbell" - assert_contains "$(<"$log")" "'sm.inbox' steering inbox" \ - "budget propagation doorbell did not identify the durable inbox" - - outside="$world/unsafe-budget" - printf '555\n' > "$outside" - rm -f "$sm/config/startup-memory-budget" - ln "$outside" "$sm/config/startup-memory-budget" - set +e - out=$(run_config_push "$root" "$home" "$fakebin" "$log" 2>&1) - rc=$? - set -e - expect_code 1 "$rc" "unsafe inherited destination should stop propagation" - assert_contains "$out" 'startup-memory-budget: error - unsafe or invalid destination: file is hardlinked' \ - "unsafe inherited destination did not produce a concrete propagation error" - [ "$(<"$outside")" = 555 ] || fail "unsafe destination handling changed its hardlinked source" - rm -f "$sm/config/startup-memory-budget" - run_config_push "$root" "$home" "$fakebin" "$log" >/dev/null - [ "$(<"$sm/config/startup-memory-budget")" = 321 ] \ - || fail "safe retry did not restore the converged primary budget" - - rm -f "$home/config/startup-memory-budget" - out=$(run_config_push "$root" "$home" "$fakebin" "$log") - assert_contains "$out" 'startup-memory-budget: pushed - mirrored primary absence' \ - "primary absence was not reported as a converging removal" - [ ! -e "$sm/config/startup-memory-budget" ] \ - || fail "primary absence did not remove the inherited budget" - instruction=$(latest_reread_instruction "$sm") || fail "budget absence did not publish a reread instruction" - assert_contains "$(<"$instruction")" $'-----BEGIN config/startup-memory-budget-----\nABSENT\n-----END config/startup-memory-budget-----' \ - "budget absence reread did not use the explicit ABSENT payload" - - rm -f "$sm/config/startup-memory-budget" - printf '555\n' > "$outside" - ln -s "$outside" "$home/config/startup-memory-budget" - set +e - out=$(run_config_push "$root" "$home" "$fakebin" "$log" 2>&1) - rc=$? - set -e - expect_code 1 "$rc" "unsafe primary budget should stop propagation" - assert_contains "$out" 'startup-memory-budget: error - unsafe or invalid primary source: file is symlinked' \ - "unsafe primary budget did not produce a concrete propagation error" - [ ! -e "$sm/config/startup-memory-budget" ] \ - || fail "unsafe primary budget changed the converged secondmate copy" - [ "$(<"$outside")" = 555 ] || fail "unsafe primary budget handling changed its symlink target" - pass "budget propagation converges through config push with exact rereads, absence, and safe rejection" -} - -test_primary_bootstrap_materializes_visible_default -test_safe_parser_rejects_ambiguous_and_unsafe_values -test_budget_accounting_reports_all_three_files_and_safe_failure -test_primary_budget_converges_with_exact_reread_and_safe_failures - -echo '# all fm-startup-memory-budget tests passed' diff --git a/tests/fm-stat-shadowing.test.sh b/tests/fm-stat-shadowing.test.sh index ec8ef4f72b7..63fdc6f4a59 100644 --- a/tests/fm-stat-shadowing.test.sh +++ b/tests/fm-stat-shadowing.test.sh @@ -82,7 +82,6 @@ pass "shadowing: fake GNU stat shadows /usr/bin/stat in PATH" # --- test the fixed helpers under shadowing ---------------------------------- . "$ROOT/bin/fm-supervision-lib.sh" -. "$ROOT/bin/fm-startup-memory-budget-lib.sh" TESTFILE="$TMP_ROOT/testfile" printf 'hello world\n' > "$TESTFILE" @@ -96,16 +95,7 @@ if [ -z "$RESULT_MTIME" ] || [ "$RESULT_MTIME" != "$EXPECTED_MTIME" ]; then fi pass "fm_sup_stat_mtime returns correct epoch mtime under GNU stat shadowing" -# 2. fm_startup_memory_budget_link_count from bin/fm-startup-memory-budget-lib.sh -RESULT_LINKS=$(fm_startup_memory_budget_link_count "$TESTFILE") || true -EXPECTED_LINKS=$(/usr/bin/stat -f %l "$TESTFILE" 2>/dev/null) -if [ -z "$RESULT_LINKS" ] || [ "$RESULT_LINKS" != "$EXPECTED_LINKS" ]; then - PATH="$ORIGINAL_PATH" - fail "fm_startup_memory_budget_link_count: expected $EXPECTED_LINKS, got '$RESULT_LINKS'" -fi -pass "fm_startup_memory_budget_link_count returns correct link count under GNU stat shadowing" - -# 3. _fm_status_file_size from bin/fm-classify-lib.sh +# 2. _fm_status_file_size from bin/fm-classify-lib.sh # We source it and call the internal function directly. RESULT_SIZE=$(LC_ALL=C /usr/bin/stat -f '%z' "$TESTFILE" 2>/dev/null) || true # The fixed code uses /usr/bin/stat so it should produce the same value as direct call @@ -122,7 +112,7 @@ if [ -z "$HELPER_SIZE" ] || [ "$HELPER_SIZE" != "$RESULT_SIZE" ]; then fi pass "_fm_status_file_size returns correct byte size under GNU stat shadowing" -# 4. stat_mtime from bin/fm-watch.sh +# 3. stat_mtime from bin/fm-watch.sh # fm-watch.sh runs a top-level `mkdir -p` on its state dir when sourced; pin it # to the temp root via FM_STATE_OVERRIDE so no artifact escapes into the repo's # git-ignored state/ directory. diff --git a/tests/fm-stow-cascade.test.sh b/tests/fm-stow-cascade.test.sh index 3d2527ab45e..ce7eed6e8f7 100755 --- a/tests/fm-stow-cascade.test.sh +++ b/tests/fm-stow-cascade.test.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Behavioral coverage for the internal /stow cascade enumerator: per-home budget -# accounting that never sums a fleet total, one stanza per registered home, -# local versus remote transport routing, the facts a per-home completion receipt -# is built from, and the bound that keeps one slow home from blocking the sweep. +# Behavioral coverage for the internal /stow cascade enumerator: one stanza per +# registered home, local versus remote transport routing, the facts a per-home +# completion receipt is built from, and the bound that keeps one slow home from +# blocking the sweep. set -u # shellcheck source=tests/lib.sh @@ -39,9 +39,6 @@ case "${FM_FAKE_SSH_MODE:-normal}" in hang) sleep 45; exit 0 ;; esac case "$command" in - fm-startup-memory-budget.sh) - cat "$FM_FAKE_REMOTE_BUDGET" - ;; fm-remote-secondmate-control.sh) printf '%s\n' "${FM_FAKE_REMOTE_AGENT_STATE:-alive}" ;; @@ -68,14 +65,13 @@ exit 0 SH chmod +x "$FAKEBIN/tmux" -# new_home [budget] -> path to a seeded local secondmate home. +# new_home -> path to a seeded local secondmate home. new_home() { - local name=$1 budget=${2:-10} home + local name=$1 home home="$TMP_ROOT/homes/$name" mkdir -p "$home/config" "$home/data" "$home/state" "$home/bin" "$home/projects" printf '%s\n' "$name" > "$home/.fm-secondmate-home" printf '%s\n' '# secondmate instructions' > "$home/AGENTS.md" - printf '%s\n' "$budget" > "$home/config/startup-memory-budget" printf '%s\n' "$home" } @@ -122,44 +118,6 @@ value_in() { # printf '%s\n' "$1" | sed -n "s/^$2=//p" | head -1 } -test_budget_is_enforced_per_home_and_never_summed() { - local primary a b out sa sb - primary=$(new_primary per-home) - a=$(new_home over-budget 10) - b=$(new_home within-budget 10) - # Home A alone exceeds its own 10-token allowance. - printf '%s\n' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > "$a/data/captain.md" - # Home B is comfortably inside the same allowance, and would only look - # over-budget if the cascade added another home's total to its own. - printf '%s\n' 'bbbbbb' > "$b/data/captain.md" - { - local_record over-budget "$a" - local_record within-budget "$b" - } > "$primary/data/secondmates.md" - - set +e - out=$(run_cascade "$primary") - set -e - sa=$(stanza "$out" over-budget) - sb=$(stanza "$out" within-budget) - - [ "$(value_in "$sa" effective_budget_tokens)" = 10 ] \ - || fail "over-budget home did not report its own allowance" - [ "$(value_in "$sb" effective_budget_tokens)" = 10 ] \ - || fail "within-budget home did not report its own allowance" - [ "$(value_in "$sa" total_estimated_tokens)" = 13 ] \ - || fail "over-budget home total was not its own files: $(value_in "$sa" total_estimated_tokens)" - [ "$(value_in "$sb" total_estimated_tokens)" = 3 ] \ - || fail "within-budget home total was not its own files: $(value_in "$sb" total_estimated_tokens)" - [ "$(value_in "$sa" budget_status)" = over-budget ] \ - || fail "the over-budget home was not classified against its own allowance" - [ "$(value_in "$sb" budget_status)" = within-budget ] \ - || fail "a within-budget home was penalized for another home's memory" - [ "$(value_in "$sa" role)" = secondmate ] \ - || fail "per-home accounting did not run in the secondmate home" - pass "each home is accounted against its own allowance instead of a fleet total" -} - test_every_registered_home_is_enumerated_exactly_once() { local primary a b c out count dup rc primary=$(new_primary enumerate) @@ -214,13 +172,10 @@ test_transport_routes_by_placement_and_liveness() { } > "$primary/data/secondmates.md" fm_write_secondmate_meta "$primary/state/live-local.meta" "$live" 'firstmate:fm-live-local' alpha claude fm_write_secondmate_meta "$primary/state/remote-live.meta" "$remote" 'fm-remote:fm-remote-live' alpha claude - printf 'role=secondmate\neffective_budget_tokens=7500\ntotal_estimated_tokens=100\nbudget_status=within-budget\n' \ - > "$TMP_ROOT/remote-budget.txt" set +e out=$(run_cascade "$primary" \ FM_FAKE_TMUX_WINDOW='fm-live-local' \ - FM_FAKE_REMOTE_BUDGET="$TMP_ROOT/remote-budget.txt" \ FM_FAKE_REMOTE_AGENT_STATE=alive) set -e [ "$(value_in "$(stanza "$out" live-local)" transport)" = agent ] \ @@ -234,13 +189,10 @@ test_transport_routes_by_placement_and_liveness() { [ "$(value_in "$s" host)" = remote-mac ] || fail "a remote home did not name its host" [ "$(value_in "$s" transport)" = agent ] \ || fail "a remote home with a live agent was not routed to that agent" - [ "$(value_in "$s" total_estimated_tokens)" = 100 ] \ - || fail "the remote home's own accounting was not reported" set +e out=$(run_cascade "$primary" \ FM_FAKE_TMUX_WINDOW='fm-live-local' \ - FM_FAKE_REMOTE_BUDGET="$TMP_ROOT/remote-budget.txt" \ FM_FAKE_REMOTE_AGENT_STATE=dead) set -e s=$(stanza "$out" remote-live) @@ -251,89 +203,65 @@ test_transport_routes_by_placement_and_liveness() { pass "transport follows placement and live-agent state, and a remote home without an agent defers" } -test_receipt_facts_are_complete_and_show_before_and_after() { - local primary home before after s +test_receipt_stanza_carries_routing_facts_only() { + local primary home out s primary=$(new_primary receipt) - home=$(new_home receipt-home 20) + home=$(new_home receipt-home) printf '%s\n' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > "$home/data/captain.md" - printf '%s\n' 'bbbbbb' > "$home/data/learnings.md" local_record receipt-home "$home" > "$primary/data/secondmates.md" set +e - before=$(run_cascade "$primary") + out=$(run_cascade "$primary") set -e - s=$(stanza "$before" receipt-home) + s=$(stanza "$out" receipt-home) + [ "$(value_in "$s" secondmate)" = receipt-home ] || fail "receipt stanza lacks its secondmate" [ "$(value_in "$s" placement)" = local ] || fail "receipt stanza lacks placement" - [ "$(value_in "$s" home)" = "$home" ] || fail "receipt stanza lacks the home it accounted" - [ "$(value_in "$s" budget_report)" = ok ] || fail "receipt stanza lacks an accounting outcome" - [ -n "$(value_in "$s" transport)" ] || fail "receipt stanza lacks a transport" - local file - for file in captain.md captain-shared.md learnings.md; do - assert_contains "$s" "file=data/$file " "receipt stanza lacks a per-file action input for $file" - done - [ "$(value_in "$s" budget_status)" = over-budget ] \ - || fail "the over-budget home was not surfaced before curation" - - # Curation shrinks this home's editable memory; the same command is the - # after-pass, so before and after totals come from one accounting contract. - printf '%s\n' 'aaa' > "$home/data/captain.md" - set +e - after=$(run_cascade "$primary") - set -e - s=$(stanza "$after" receipt-home) - [ "$(value_in "$s" budget_status)" = within-budget ] \ - || fail "the after pass did not reflect this home's curation" - [ "$(value_in "$(stanza "$before" receipt-home)" total_estimated_tokens)" \ - -gt "$(value_in "$s" total_estimated_tokens)" ] \ - || fail "the after total did not drop below the before total" - pass "each stanza carries the facts a per-home receipt needs, before and after curation" + [ "$(value_in "$s" home)" = "$home" ] || fail "receipt stanza lacks the home it routes" + [ "$(value_in "$s" transport)" = direct ] || fail "receipt stanza lacks a transport" + assert_not_contains "$out" 'budget_report=' "the cascade still measured a home against a memory size budget" + assert_not_contains "$out" 'total_estimated_tokens=' "the cascade still estimated a home's memory size" + pass "each stanza carries the routing facts a per-home receipt needs and no size accounting" } test_a_slow_remote_is_bounded_and_the_rest_still_report() { local primary home remote out rc started elapsed s primary=$(new_primary bounded) - home=$(new_home bounded-local 10) - printf '%s\n' 'cccccc' > "$home/data/captain.md" + home=$(new_home bounded-local) remote="$TMP_ROOT/homes/bounded-remote" { remote_record bounded-remote remote-mac "$TMP_ROOT/remote-root" "$remote" local_record bounded-local "$home" } > "$primary/data/secondmates.md" fm_write_secondmate_meta "$primary/state/bounded-remote.meta" "$remote" 'fm-remote:fm-bounded-remote' - printf 'role=secondmate\n' > "$TMP_ROOT/remote-budget.txt" started=$(date +%s) set +e out=$(run_cascade "$primary" \ FM_STOW_CASCADE_TIMEOUT=2 \ - FM_FAKE_SSH_MODE=hang \ - FM_FAKE_REMOTE_BUDGET="$TMP_ROOT/remote-budget.txt") + FM_FAKE_SSH_MODE=hang) rc=$? set -e elapsed=$(( $(date +%s) - started )) [ "$elapsed" -lt 30 ] || fail "the sweep waited $elapsed s on a host that never answered" s=$(stanza "$out" bounded-remote) - [ "$(value_in "$s" budget_report)" = timeout ] \ - || fail "an unanswered home did not report a bounded exception" [ "$(value_in "$s" transport)" = unavailable ] \ - || fail "an unanswered home claimed a transport conclusion" - s=$(stanza "$out" bounded-local) - [ "$(value_in "$s" budget_report)" = ok ] \ + || fail "an unanswered home claimed a transport conclusion: $(value_in "$s" transport)" + assert_contains "$s" 'exceeded the 2s bound' "an unanswered home did not report a bounded exception" + [ "$(value_in "$(stanza "$out" bounded-local)" transport)" = direct ] \ || fail "the healthy home was blocked by the unanswered one" - [ "$(value_in "$s" total_estimated_tokens)" = 3 ] \ - || fail "the healthy home's own accounting was lost" expect_code 3 "$rc" "a reported per-home exception should be distinguishable from a clean sweep" assert_contains "$out" 'exceptions=1' "the sweep did not count the unreachable home" set +e out=$(run_cascade "$primary" \ - FM_FAKE_SSH_MODE=unreachable \ - FM_FAKE_REMOTE_BUDGET="$TMP_ROOT/remote-budget.txt") + FM_FAKE_SSH_MODE=unreachable) rc=$? set -e - [ "$(value_in "$(stanza "$out" bounded-remote)" budget_report)" = error ] \ + s=$(stanza "$out" bounded-remote) + [ "$(value_in "$s" transport)" = unavailable ] \ || fail "an unreachable host was not reported as a per-home exception" - [ "$(value_in "$(stanza "$out" bounded-local)" budget_report)" = ok ] \ + assert_contains "$s" 'remote endpoint probe failed' "an unreachable host did not say why it is unavailable" + [ "$(value_in "$(stanza "$out" bounded-local)" transport)" = direct ] \ || fail "an unreachable host blocked the healthy home" expect_code 3 "$rc" "an unreachable host should still finish the sweep" pass "one slow or unreachable home is bounded and every other home still reports" @@ -362,9 +290,8 @@ test_no_cascade_without_secondmates_or_from_a_secondmate_home() { pass "the cascade stays silent with no secondmates and never runs from a secondmate home" } -test_budget_is_enforced_per_home_and_never_summed test_every_registered_home_is_enumerated_exactly_once test_transport_routes_by_placement_and_liveness -test_receipt_facts_are_complete_and_show_before_and_after +test_receipt_stanza_carries_routing_facts_only test_a_slow_remote_is_bounded_and_the_rest_still_report test_no_cascade_without_secondmates_or_from_a_secondmate_home