feat(bin): compile and cap startup working-memory bundle - #2641
Open
BohnBawerick wants to merge 64 commits into
Open
feat(bin): compile and cap startup working-memory bundle#2641BohnBawerick wants to merge 64 commits into
BohnBawerick wants to merge 64 commits into
Conversation
Kimi 0.36.0 prompts Trust this folder? on every untrusted worktree path, with Don't trust preselected. The readiness gate waited only for Welcome or an empty composer, so spawn aborted and the pane died. Detect that exact dialog, send Up then Enter, and keep the existing readiness and delivery checks. Do not write ~/.kimi-code/workspace-trust/.
Checked kimi-cli and kimi-code CLI/config docs plus kimi --help. --auto and --yolo do not skip Trust this folder?, so spawn still accepts the dialog with Up then Enter.
Applied locally at the captain's explicit instruction so kimi is dispatchable in this home today. The same change is open upstream as PR kunchenguid#2328, held by GitHub pending first-time-contributor workflow approval. This diverges the primary from origin/main until that PR lands. Reconcile by dropping this merge once upstream carries the change.
Verify Antigravity CLI as a crewmate/scout adapter. Pin the launch to gemini-3.1-pro-high, omit --effort (it conflicts with *-high model ids), accept the workspace trust dialog, and install a gated global Stop hook. Secondmate, primary turn-end, and watcher-arm remain unverified.
A separated greater-than pair is unknown on zellij, cmux, and orca because agy is not verified there. Identity-capable backends keep the existing probe-then-shortcut path.
Verify Antigravity CLI as a crewmate/scout adapter. Pin the launch to gemini-3.1-pro-high, omit --effort (it conflicts with *-high model ids), accept the workspace trust dialog, and install a gated global Stop hook. Secondmate, primary turn-end, and watcher-arm remain unverified.
A separated greater-than pair is unknown on zellij, cmux, and orca because agy is not verified there. Identity-capable backends keep the existing probe-then-shortcut path.
…derive teardown pointers
Applies the same pattern as the Kimi 0.36.0 workspace-trust fix: the adapter works and the runtime is installed and funded, so the running copy gets it now rather than waiting on the PR to land. Reconcile by dropping this merge once the upstream PR merges. Adopted at fm/hz-agy-adapter e474f5a; the review fix round still in flight is not included and will arrive with the PR. # Conflicts: # bin/fm-spawn.sh # docs/configuration.md
Its substantive work is already on main: 13 of its 14 commits have equivalents here, and the two that were missing were brought across as 48952de and 2c2b7b2. The only remaining difference is one docs/configuration.md paragraph, where the branch's wording named the superseded gemini-3.1-pro-high pin; main's wording is kept deliberately and points at harness-adapters as the pin's owner instead. Recorded rather than discarded so nothing is lost and the upstream PR 2366 can stay open on its own without this task holding a local copy open to watch it.
- The mode=no-mistakes ship scaffold told the worker to invoke the /no-mistakes skill three times. A crewmate runs in a project worktree, not the firstmate home, so that skill is unreachable and the worker stalls exactly where it has just finished implementing. Overnight 2026-08-17/18 three workers each burned a supervisor round trip there and reported done: for a commit with no PR. - The scaffold now names the interface the worker actually has: the no-mistakes CLI on PATH, with the concrete run and respond commands, and it notes that firstmate's trigger may still be worded as a skill invocation so either wording lands on the same command. - Harden the failure mode the defect lands on: completion for this mode is now stated as a green PR, the implementation handoff line says in its own text that nothing has shipped yet, and a run that cannot start is routed to blocked: rather than done:. - direct-PR refused "/no-mistakes"; it now refuses the pipeline by name. local-only, scout and the secondmate charter carried no such premise. - Extend tests/fm-brief.test.sh: every variant is generated and checked for skill-invocation instructions, and the no-mistakes definition of done is pinned to the CLI commands and the PR-bound done: gate.
…e affirmatively reports working
- Add an away supervision model to bin/fm-wake-lib.sh: while state/.afk exists the away daemon owns supervision for every primary harness and runs the watcher one cycle at a time, so an unheld watcher lock is the healthy state and the daemon itself is what must be tested. - fm_turnend_supervision_healthy keeps the PID-strict watcher check everywhere except away mode, where fm_away_daemon_healthy requires a live identity-matched daemon plus a turning loop; a dead, recycled-pid or wedged daemon still blocks the turn end. - Bound daemon-tick freshness with FM_AWAY_TICK_GRACE (180s), derived from the daemon's housekeeping cadence and crash backoff rather than guessed, and read the freshest of its housekeeping tick, its watcher child's beat, and its startup stamp. - Give the pull guard the same answer, and name the daemon in both banners instead of telling the session to arm a watcher. - Let bin/fm-afk-start.sh delegate its already-running check to the same predicate so entering away mode and guarding it cannot disagree. - Away mode outranks a pinned FM_SUPERVISION_MODEL harness model, which bin/fm-spawn.sh bakes into every secondmate launch.
Suppress the possible-wedge escalation when the busy source affirmatively reports a pane as working. Away-mode housekeeping was aging a stale marker into a wedge alarm without ever asking whether the worker was busy, which produced repeated false alarms on panes that were provably mid-task. Landed locally on the captain's word so the fleet gets the fix now; upstream PR 2554 stays open on its own at kunchenguid/firstmate.
Stop the turn-end guard raising TURN WOULD END BLIND on a healthy fleet under away mode. The guard demanded a live long-lived watcher process, which away mode deliberately does not run - the away daemon owns supervision and runs the watcher one-shot. The supervision-model vocabulary in fm-wake-lib.sh now covers that fourth shape, so a healthy away fleet stops quietly while a dead or stalled daemon still blocks. Measured 3/200 false blocks before, 0/200 after. Landed locally on the captain's word so the fleet gets the fix now; upstream PR 2557 stays open on its own at kunchenguid/firstmate.
A run's recorded head is the head the PIPELINE has advanced to as it applies its own fix commits, and those commits are pushed to the configured target rather than fetched into the crew's worktree - so during a run the head does not resolve in that repository at all. The attribution rule treated an unresolvable head as a mismatch, rejected the branch's own live row, and matched the newest stale row underneath it: a failed run recorded at the very head the worktree still held. A healthy validation therefore read as `failed`, which routes firstmate into recovery and can restart a run that is working. - Code identity is now ternary: match, mismatch, and unverified for a real run head this copy cannot resolve. The strict predicate is unchanged, so teardown's abort authority still requires a verified binding. - An unverified run is bound by its submitted head - the head it was launched against - read from `axi sync --check`, the one read-only surface that reports it on the installed v1.48.0. - When nothing binds the run it may still report work under way, but never a terminal verdict: `failed` and `done` become `unknown`, which is not itself an instruction to act. - The coarse runs list stops at the branch's newest row, so a superseded run can no longer answer for the current one.
… attribution rules
Generated ship briefs told the worker to invoke the /no-mistakes skill, which a worker cannot reach: the skill lives in the firstmate home and loads for a firstmate session, while a worker runs in an isolated project copy. Every brief variant now names the CLI path instead. Also narrows the accompanying "never report done: for work that has no PR" line to the case where a validation run cannot start, resolving the contradiction with the section 7 handoff. The captain approved that scoping (option a) and separately approved removing the intermediate handoff altogether, filed as fm-validation-self-start. Landed locally on the captain's word; upstream PR 2566 stays open at kunchenguid/firstmate with all 13 checks green.
fm-crew-state.sh reported a live, healthy validation run as failed. It matched runs to a task by comparing the run's head against the worktree head, but a running pipeline commits its fix rounds to the gate repository, so that head is absent from the worktree's object store. The live row was rejected and an older failed row at the unchanged worktree head answered instead - a confident wrong verdict that routes firstmate into recovery and risks restarting healthy work. Now uses ternary code identity bound to the run's submitted head, withholds a terminal verdict when identity cannot be proven, and takes the newest matching row. 7 new regression assertions. Landed locally on the captain's word. Upstream PR 2569 stays open at kunchenguid/firstmate; its checks are held pending the repository owner's approval of outside-contributor workflow runs, which never arrived. Our own pipeline completed review, test, document and lint with no findings.
… main Firstmate's delivery path on a fork repository left tasks unable to land where the running fleet actually executes. For ordinary projects, no-mistakes tasks merge remotely via PR and sync down via fleet-sync. For Firstmate's own repository, local main is authoritative for the fleet while upstream PRs remain open outward contributions. Because fm-merge-local.sh refused mode!=local-only tasks and fm-fleet-sync.sh diverged on upstream origin/main, changes were stranded. - Teach bin/fm-merge-local.sh to fast-forward local main for Firstmate's own repository tasks (where project is FM_ROOT/FM_HOME) in any mode. - Teach bin/fm-pr-merge.sh to fast-forward local main when merging a Firstmate repository task remotely. - Teach bin/fm-fleet-sync.sh to skip FM_ROOT/FM_HOME gracefully rather than falsely alarming STUCK on upstream origin/main. - Teach bin/fm-teardown.sh to recognize work landed in the local default branch. - Add docs/verification/fork-reconciliation.md with the empirical evidence and reconciliation plan for fork/main. - Add tests/fm-merge-local.test.sh and expand fleet-sync, pr-merge, and teardown test suites with full RED-GREEN coverage.
Away-mode housekeeping treated every failed capture as a gone pane and dropped the marker with no escalation. A redraw, timeout, or backend hiccup then silently stopped watching a worker that was still there, which is the failure this path exists to prevent. Both the stale-wedge and pause-resurface sites now share stale_window_recheck: retry the capture twice (0.4s apart) before verdict, then ask fm_backend_agent_state. Only an authoritatively missing endpoint is gone. A present dead shell is ordinary idle. Every other state, including an unreadable or unverified probe, escalates and keeps the marker on the same cadence because the watcher cannot recapture an unreadable pane. target_exists is not used as a gone proof: tmux can fall back to the active window, and Orca's check is itself a capture. Tests cover gone, unreadable-present (alive/unreadable/unverified), retry-then-ordinary, and dead-is-not-gone at both call sites.
Remove the intermediate pre-PR done: handoff. The implementation worker starts its own no-mistakes CLI run immediately after the commit, reports working: when that run starts, and reports done: only with a PR.
Firstmate's own tasks could not land where the running fleet executes. On this fork, local main is authoritative and upstream PRs are an outward courtesy, but the local merge path refused any task that was not local-only and fleet sync falsely alarmed on upstream origin/main, so finished work was stranded. Teaches the local merge path, the PR merge path, fleet sync, and teardown to recognize firstmate's own repository and its authoritative local main. Adds docs/verification/fork-reconciliation.md plus a new merge-local test suite and expanded fleet-sync, pr-merge and teardown coverage. Landed locally on the captain's explicit word. Our own pipeline completed review, test, document and lint with no findings. The upstream PR 2597 check failure is the known fm-watcher-lock fork/execve flake, tracked separately as fm-racy-watcher-lock-test.
A worker that finished writing code stopped and waited for firstmate to tell it to start validation. That handoff is a message that can fail to land: one worker sat finished and idle for eighty minutes today because the start message was never submitted, and every task paid the round trip even when it worked. The worker now starts its own no-mistakes run immediately after the implementation commit, announces the start with a nonterminal working line, and must report failed or blocked if the run dies mid-pipeline, so firstmate still learns start and failure without a handoff. It may no longer claim done before a PR exists. Review, tests, gates, ask-user escalation and merge authority are untouched: the pipeline's own separate agent still does the reviewing, the author still cannot answer its own ask-user finding, and --yes remains banned. Landed locally on the captain's explicit word.
Task ids become free again after cleanup, but the previous task's data directory was left on disk. Reusing the id then handed the new worker the old task's brief, silently, and it would build the wrong thing. Scaffolding now refuses a reused id while that directory still exists, rather than writing over or reading through it. Landed locally on the captain's explicit word.
While the captain is away, the background supervisor treated every failed screen capture as proof the worker was gone, dropped its marker and silently stopped watching it. A redraw, timeout or backend hiccup was enough. That is precisely the failure the away-mode supervisor exists to prevent. Both the stale-wedge and pause-resurface sites now retry the capture twice before deciding, then ask the backend whether the endpoint actually exists. Only an authoritatively missing endpoint counts as gone; a present dead shell is ordinary idle; every other state, including unreadable or unverified, escalates and keeps the marker. Endpoint presence alone is not accepted as proof, because tmux can fall back to the active window and Orca's own check is a capture. Landed locally on the captain's explicit word.
Session start printed data/captain.md and data/learnings.md whole, so the startup memory surface grew with no read path that could refuse it: 35,526 estimated tokens against a 7,500-token budget on the reference home. - bin/fm-memory-compile.sh compiles the bundle session start injects: a standing core, a catalog of every note, and the notes whose triggers match live fleet work, capped against config/startup-memory-budget. Core is never dropped, the catalog outranks every note, and a note that does not fit is skipped rather than ending selection. - bin/fm-memory-migrate.sh splits a home's data/learnings.md into one atomic cited note per heading, publishes the catalog, and freezes plus archives the original before removing it. - fm-session-start.sh injects the compiled bundle when data/memory/ exists and keeps the whole-file print when it does not, or when the compile fails. - Two session-start fixtures forced a MISSING diagnostic by removing node from the fake bin, which proves nothing on a host that also ships /usr/bin/node. They now shadow gh-axi, which cannot exist outside the fake bin. On the reference home the surface goes from 35,526 tokens to 7,490, and to 7,480 with four hot notes once the core is trimmed to its target size.
- Each compiler mode now accepts only its own flags, so `catalog --context` or `compile --dry-run` is a usage error instead of a silently ignored option that reads as a compile which simply matched nothing. - Session start says so when it cannot create the temporary file for the compiled bundle, rather than falling back to the whole-file print with no word about why.
Trigger derivation stemmed a trailing `s` off every proper noun, so a heading about `Windows` produced the trigger `window` - which then matched nothing, because trigger matching is whole-token. The stopword test now tries the singular while the trigger keeps its original spelling. The possessive-stripping `\b` is a GNU sed extension that does nothing on BSD sed. It was also redundant: the following substitution already splits `Firstmate's` into `Firstmate` and a one-character `s` the length filter drops.
The network-partition fixture removed node from the fake bin to produce a local-half diagnostic, which proves nothing on a host that also ships /usr/bin/node: the base PATH still satisfies `command -v`, no MISSING line is emitted, and the partition assertion fails for a reason that has nothing to do with the partition. gh-axi cannot exist outside the fake bin, so the assertion now means the same thing on every host. Same change as the two session-start fixtures in this branch's first commit.
BohnBawerick
force-pushed
the
fm/fm-memory-build
branch
from
August 19, 2026 19:42
cf1ebc5 to
8c44314
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Build Memory Slice 1 for firstmate: the working-memory compiler, the startup budget cap, the data/memory/ storage layout, and the migration of existing operational memory.
PROBLEM. Session start dumps 35,526 estimated tokens, 4.7x over the 7,500-token startup budget, because bin/fm-session-start.sh unconditionally prints data/captain.md and data/learnings.md in full. The captain approved the memory architecture in data/fm-memory-design/report.md sections 5, 13 and 16, and this task implements Slice 1 of it.
DELIVERABLES.
New layout under data/memory/: core.md holding standing preferences, authority boundaries and core guidelines with a 1,500-2,500 token target; notes/*.md holding atomic notes with clear claim titles, triggers, dates and citations or provenance; catalog.md as the compiled index with one line per note carrying title, trigger, path and updated date; and drop/ as a drop tray the compiler ignores.
bin/fm-memory-compile.sh, the mechanical compiler. It selects the standing constitution (data/memory/core.md, falling back to the standing data/captain.md), the catalog, and the trigger-matched hot notes from data/memory/notes/*.md whose triggers match active projects, backlog items or current runtime context. It accounts estimated tokens using the bin/fm-startup-memory-budget.sh rules, meaning the ceil(UTF-8 bytes / 3) conservative estimate. It enforces the budget cap: when the total exceeds the budget it drops hot notes first while keeping the catalog in place, and when core.md alone exceeds the budget it prints the core and emits an explicit over-budget warning line without failing silently. It emits the compiled markdown bundle to stdout.
Session start integration. bin/fm-session-start.sh invokes bin/fm-memory-compile.sh to inject the compiled memory block rather than dumping raw captain.md and learnings.md, and preserves a safe fallback when data/memory/ is absent.
Data migration. The 51 headings in data/learnings.md become individual atomic notes under data/memory/notes/, the initial data/memory/catalog.md is generated, and originals and historical archives are retained under data/memory-archive.md so history is never deleted.
Automated tests under tests/fm-memory-compile.test.sh or a colocated test script, covering bundle compilation (core plus catalog plus trigger-matched notes), trigger filtering against active keywords, strict budget cap enforcement and note dropping under budget pressure, graceful fallback when memory files are missing, and integration with fm-session-start.sh. All scripts must pass bin/fm-lint.sh shellcheck clean.
CONSTRAINTS. Follow the firstmate-coding-guidelines skill strictly, because this changes firstmate's shared tracked material. Keep changes clean, self-contained and tested. Use one sentence per line in markdown files. Never use em dashes; use plain hyphens. Never add agent co-author lines. Local main is authoritative, origin is upstream kunchenguid/firstmate, and fork is BohnBawerick/firstmate.
TWO JUDGEMENT CALLS MADE DURING IMPLEMENTATION, both deliberate and to be reviewed as part of the contract.
First, deliverable 4 is delivered as bin/fm-memory-migrate.sh, a tested idempotent migration tool, rather than as a committed data change. data/ is gitignored and lives in each FM_HOME outside this worktree, so the live-home split cannot be a PR artifact and is firstmate's to run. The tool was verified end to end against a copy of the real 51-heading data/learnings.md: it produced 51 notes, published the catalog, froze the original under data/memory/raw/ and appended it to data/memory-archive.md before removing it, and the resulting startup surface measured 7,490 estimated tokens against the 7,500 budget, down from 35,526.
Second, data/memory/core.md is fully supported, takes precedence, and is covered by tests including the over-budget path, but the migration does not mechanically fabricate one by copying data/captain.md. Two files holding standing preferences would drift the moment the captain states one, since firstmate inspect-then-updates captain.md. The compiler therefore treats data/captain.md as the standing constitution until a home authors core.md, so no home loses anything by waiting and the split stays a captain judgement rather than a script's guess.
ALSO IN SCOPE, found and fixed rather than left. Two fixtures in tests/fm-session-start.test.sh forced a MISSING tool diagnostic by removing node from the fake bin, which proves nothing on a host that also ships /usr/bin/node and made the suite fail here before any of this work. They now shadow gh-axi, which cannot exist outside the fake bin, so the assertion means the same thing on every host.
What Changed
bin/fm-memory-compile.shto compile startup working-memory bundles fromdata/memory/core.md,data/memory/catalog.md, and keyword-matcheddata/memory/notes/*.md, enforcing startup token budgets and dropping hot notes when over capacity.bin/fm-memory-migrate.shto splitdata/learnings.mdinto atomic notes with front matter, generate the catalog index, and preserve raw originals underdata/memory-archive.mdanddata/memory/raw/.bin/fm-session-start.sh, agent instructions, and skills to inject the compiled memory block with fallback to raw memory files whendata/memory/is absent, covered by new test suites intests/fm-memory-compile.test.sh.Risk Assessment
✅ Low: The memory compilation, budget enforcement, migration tooling, and documentation changes are well-bounded, comprehensively covered by behavioral tests, and cleanly resolve all previous review findings.
Testing
Exercised targeted automated test suites covering memory compilation, migration, session start digest injection, bootstrap fixtures, and startup memory budgets (all assertions passed). Performed end-to-end product verification generating complete CLI transcripts for migration, compilation, budget capping, and session startup integration in /tmp/no-mistakes-evidence/01M0DGRCBQ3MAVMB4RGNM3B5DC.
Evidence: Memory Slice 1 Verification Overview
Source: Memory Slice 1 Verification Overview
Evidence: End-to-End Migration CLI Transcript
Source: End-to-End Migration CLI Transcript
Evidence: Working Memory Compilation and Trigger Matching Transcript
Source: Working Memory Compilation and Trigger Matching Transcript
Evidence: Budget Cap and Over-Budget Enforcement Transcript
Source: Budget Cap and Over-Budget Enforcement Transcript
Evidence: Session Start Integration Transcript
Source: Session Start Integration Transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 8 issues found → auto-fixed ✅
bin/fm-memory-migrate.sh:266- Two learnings headings that slugify to the same name silently lose the second claim. The first heading writes notes/<slug>.md; the second sees the target exists, counts it as KEPT, reports 'already present' (identical to an idempotent re-run), and its body never becomes a note. The run then freezes, archives and deletes data/learnings.md, so the operator sees a success report while the working-memory layout is missing a claim. Reproduced: a fixture with '## TheghCLI needs auth <!--a:2026-08-01-->' and '## TheghCLI needs auth. <!--a:2026-08-02-->' produced '1 note(s) created, 1 already present' and the second body was absent from data/memory/notes/. Collisions are not exotic: slugify() truncates at 60 characters (cut -c1-60), so any two headings sharing their first 60 slug characters collide too. The header comment at lines 124-125 claims 'a duplicate slug can be disambiguated', but no disambiguation is implemented. Fix: track slugs claimed during this run and suffix a within-run collision (slug-2, slug-3), counting it as created rather than kept.bin/fm-memory-compile.sh:260- die() for a missing --context-file runs inside build_context, which is the left side of the pipeline at line 270, so it only exits that subshell. Verified:fm-memory-compile.sh compile --no-auto-context --context-file /nope/missing.mdprints 'fm-memory-compile: context file not found' to stderr, exits 0, and emits a full bundle built from a truncated match context. Any --context/--context-file argument after the failing one is also discarded, so hot-note selection silently changes while the caller sees success. Fix: validate every CONTEXT_FILES path before the pipeline (or check PIPESTATUS[0]) and exit non-zero.bin/fm-session-start.sh:922- The compiled bundle is capped at config/startup-memory-budget, but data/captain-shared.md is still printed whole immediately after it and is not accounted by fm-memory-compile.sh, while bin/fm-startup-memory-budget.sh report still counts it against the same budget value. In a secondmate home the compiler can legitimately fill the entire 7500-token cap with core plus catalog plus notes, and captain-shared.md then adds an unbounded amount on top, so the real startup surface exceeds the allowance the cap is supposed to enforce and the two accounting paths disagree about the same number. Not a regression (the old path dumped everything uncapped), but it leaves a documented hole in Slice 1's cap. Decide whether captain-shared.md should be accounted by the compiler or explicitly carved out of the budget's definition in docs/configuration.md.bin/fm-memory-compile.sh:336- Once data/memory/core.md exists, a non-empty data/captain.md is neither injected nor mentioned anywhere in the bundle. Verified: with both files present, the compiled output contains zero occurrences of the string 'captain'. This is asymmetric with data/learnings.md, which gets a loud MEMORY_NOTICE at line 360 for exactly the same situation (present, not injected), and it contradicts the header's own stated invariant that everything degrades to a printed notice. It also conflicts with AGENTS.md, which this change leaves unmodified: 'Home-domain captain preferences and working style belong in data/captain.md after inspect-then-update.' A home that authors core.md and keeps following that rule writes preferences that no session start will ever read, with no signal. Either emit a MEMORY_NOTICE naming captain.md's estimated tokens when core.md shadows it, or update the AGENTS.md routing rule to point at core.md once the layout exists.docs/configuration.md:148- The intent's deliverable 1 requires 'core.md holding standing preferences, authority boundaries and core guidelines with a 1,500-2,500 token target'. The 1,500-2,500 range appears nowhere in the change (docs/configuration.md, AGENTS.md, the stow skill, or the compiler header), so a home that later authors core.md has no stated size target and only learns it went too far when the whole budget is blown. Deferring the file itself is explicitly authorized by the intent's second judgement call; the target for whoever writes it is not covered by that deferral. One sentence in the 'Compiled working memory' section would close it.bin/fm-memory-compile.sh:179- Every memory FILE is symlink-checked (core.md, catalog.md, each note, captain.md, projects.md, backlog.md, state metas), and the budget library even has a dedicated config-directory symlink check, but the containing directories are not:[ -d "$NOTES_DIR" ]and[ -d "$DATA/memory" ]in fm-session-start.sh both succeed on a symlink to a directory. A symlinked data/memory or data/memory/notes bypasses every per-file guard in one step, since the files inside the target are ordinary regular files. Adding[ ! -L ]to the two directory tests makes the guard match the model the rest of the script already applies.bin/fm-session-start.sh:902- mktemp creates MEMORY_TMP safely, but the sibling stderr file is opened as "$MEMORY_TMP.err", a name mktemp never created and never held. In a world-writable /tmp a local attacker who reads the directory after mktemp lands can plant a symlink at that exact path before the redirect opens it, and the compiler's stderr is then written through it. Both other scripts in this change already usemktemp -dand keep their working files inside; using a directory here removes the race.bin/fm-memory-compile.sh:215- render_catalog spawns printf, cut and sed per note, and tokens_of_file spawns a subshell plus wc per note, so a 51-note home costs roughly 250 processes on the session-start critical path; measured 0.68s for 51 notes with no hot notes selected (WSL2). The trigger truncation at line 215 is plain string work that fits in the awk pass that already builds the inventory, and the per-note byte counts can come from a singlewc -cover all note files. Both are behavior-preserving and would cut most of the forks.🔧 Fix: fix memory migration collisions and compiler edge cases
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
./bin/fm-test-run.sh tests/fm-memory-compile.test.sh./bin/fm-test-run.sh tests/fm-session-start.test.sh./bin/fm-test-run.sh tests/fm-bootstrap.test.sh./bin/fm-test-run.sh tests/fm-startup-memory-budget.test.shFM_HOME=<scratch> ./bin/fm-memory-migrate.sh(tested atomic note generation, front matter parsing, catalog publishing, raw archiving, durable history appending, and rerun idempotency)FM_HOME=<scratch> ./bin/fm-memory-compile.sh compile(tested core.md precedence over captain.md fallback, trigger matching against active projects/backlog keywords, drop tray exclusion, and fresh catalog rendering)FM_HOME=<scratch> ./bin/fm-memory-compile.sh catalog(tested manual catalog republishing)Budget cap enforcement verification across within-budget (7500 tokens), capped under budget pressure (180 tokens dropping notes while preserving catalog and core), over-budget core warning (20 tokens), and malformed budget rejectionFM_HOME=<scratch> ./bin/fm-session-start.sh(tested end-to-end session start digest injection of compiled capped working memory and safe fallback behavior)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.