Skip to content

fix(bin): resolve the startup-memory budget through a symlinked config directory - #5

Closed
doitdigital0495 wants to merge 2 commits into
mainfrom
fm/fm-config-symlinked
Closed

fix(bin): resolve the startup-memory budget through a symlinked config directory#5
doitdigital0495 wants to merge 2 commits into
mainfrom
fm/fm-config-symlinked

Conversation

@doitdigital0495

@doitdigital0495 doitdigital0495 commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Intent

Take the already-written, captain-reviewed branch fm/fm-config-symlinked through the no-mistakes pipeline so it lands as a PR with every CI check green, including this repo's own 'PR must be raised via no-mistakes' body-signature gate (the only failing check on the existing PR #5, which was pushed directly rather than through the gate).

Goal of the change itself: bin/fm-startup-memory-budget-lib.sh must resolve the startup-memory budget through a symlinked config/ directory. Every other setting under config/ already reads fine through such a symlink - a firstmate home whose config/ is linked into a machine-configuration repo is a supported deployment layout - but the startup-memory budget alone refused with 'config directory is symlinked'. That made every /stow incomplete and printed 'STARTUP_MEMORY_BUDGET: invalid' at every session start.

Deliberate decisions already made and approved by the captain, which should not be reverted or re-litigated:

  • The config-directory symlink rejection is removed outright, not softened or made configurable. It bought no integrity property: the check could only ever see the final path component, so a symlinked ancestor already passed silently.
  • The real integrity guarantees stay exactly as they were: the budget file itself must still be a regular, single-linked, non-symlinked file, and the directory must still resolve to a real directory. Tests were added for a symlinked config/ whose budget file is itself a symlink (rejected), a config/ symlink pointing at a missing path (rejected), and a config/ symlink pointing at a regular file (rejected).
  • .gitignore changes 'config/' to '/config' so the ignore rule anchors at the repo root and does not swallow unrelated nested config directories.
  • docs/configuration.md is updated to state the supported symlinked-config layout, keeping the one-owner rule: that page is the owner of this contract.

Constraints: this is firstmate's own repo, so the firstmate-coding-guidelines skill applies - one sentence per line in tracked Markdown, plain dashes only, no agent co-author on commits, bin/*.sh must pass bin/fm-lint.sh (pinned shellcheck), tests colocated in tests/ as .test.sh extending the existing script, and tests must exercise behavior through the executable interface rather than asserting implementation source bytes. Scope is deliberately limited to these four files; do not expand it. Do not merge the PR - the captain approves the merge.

What Changed

  • bin/fm-startup-memory-budget-lib.sh no longer rejects a config/ directory that is itself a symlink; fm_startup_memory_budget_config_dir_safe now only requires the path to resolve to a real directory, while the existing per-file regular/single-linked/non-symlinked checks are unchanged.
  • tests/fm-startup-memory-budget.test.sh covers bootstrap materialization and read through a symlinked config/, plus rejection when the budget file behind the symlink is itself a symlink, and when the config/ symlink points at a missing path or at a regular file.
  • docs/configuration.md documents the symlinked-config/ layout as supported, and .gitignore anchors the rule at the repo root by changing config/ to /config.

Risk Assessment

✅ Low: Small, well-bounded change that removes a check with no integrity value at the single shared boundary all consumers route through, keeps every per-file safety guard, and adds executable-interface regression coverage for both the read and bootstrap-materialize paths.

Testing

Exercised the startup-memory budget through a symlinked config/ directory end-to-end: the colocated suite passes at HEAD and its new symlinked-config assertion fails against the pre-fix library, and CLI transcripts show session start going from "STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config directory is symlinked" with nothing materialized, to a silent, complete start that materializes and reads the budget through the symlink while still rejecting a symlinked budget file, a dangling config/ symlink, and a config/ symlink to a regular file. The .gitignore anchoring was verified through git check-ignore rather than by reading the file, and tests/fm-gitignore-config.test.sh passes; tests/fm-bootstrap.test.sh has one pre-existing environment failure identical at the base commit. No UI surface is involved - this is a shell CLI and session-start path, so evidence is CLI transcripts rather than screenshots.

Evidence: Session start through a symlinked config/ - before vs after

Source: Session start through a symlinked config/ - before vs after

################ BEFORE fix (base bc57f60) ################ === session start (bin/fm-bootstrap.sh) === STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config directory is symlinked $ cat $FM_HOME/config/startup-memory-budget cat: .../firstmate-config/startup-memory-budget: No such file or directory ################ AFTER fix (c362928) ################ === session start (bin/fm-bootstrap.sh) === (no output - clean, complete session start) $ cat $FM_HOME/config/startup-memory-budget 7500 === integrity guarantees still enforced through the symlink === budget file itself a symlink: startup-memory-budget: invalid config/startup-memory-budget - file is symlinked (exit=1) config/ symlink -> missing path: startup-memory-budget: invalid config/startup-memory-budget - config directory is not a directory (exit=1) config/ symlink -> regular file: startup-memory-budget: invalid config/startup-memory-budget - config directory is not a directory (exit=1)

################ BEFORE fix (base bc57f60) ################
$ ls -l $FM_HOME/config
lrwxrwxrwx 1 daan daan   55 Aug 22 17:43 config -> /tmp/fm-evidence.xxrZCx/machine-config/firstmate-config

=== session start (bin/fm-bootstrap.sh) ===
STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config directory is symlinked
(bootstrap output above; empty means a clean, complete session start)

$ ls -l $FM_HOME/config/startup-memory-budget   # materialized through the symlink
ls: cannot access '<tmp>/machine-config/firstmate-config/startup-memory-budget': No such file or directory
$ cat $FM_HOME/config/startup-memory-budget
cat: /tmp/fm-evidence.xxrZCx/machine-config/firstmate-config/startup-memory-budget: No such file or directory


################ AFTER fix (c362928) ################
$ ls -l $FM_HOME/config
lrwxrwxrwx 1 daan daan   55 Aug 22 17:43 config -> /tmp/fm-evidence.Fj37VE/machine-config/firstmate-config

=== session start (bin/fm-bootstrap.sh) ===
(bootstrap output above; empty means a clean, complete session start)

$ ls -l $FM_HOME/config/startup-memory-budget   # materialized through the symlink
-rw------- 1 daan daan 5 Aug 22 17:43 <tmp>/machine-config/firstmate-config/startup-memory-budget
$ cat $FM_HOME/config/startup-memory-budget
7500

=== integrity guarantees still enforced through the symlink ===
$ # budget file itself a symlink:
startup-memory-budget: invalid config/startup-memory-budget - file is symlinked
exit=1
$ # config/ symlink pointing at a missing path:
startup-memory-budget: invalid config/startup-memory-budget - config directory is not a directory
exit=1
$ # config/ symlink pointing at a regular file:
startup-memory-budget: invalid config/startup-memory-budget - config directory is not a directory
exit=1
Evidence: bin/fm-startup-memory-budget.sh read/report on a symlinked config - before vs after

Source: bin/fm-startup-memory-budget.sh read/report on a symlinked config - before vs after

$ ls -l ~/.firstmate/config config -> /tmp/fm-demo/machine-config/firstmate-config $ cat .../startup-memory-budget -> 9000 === BEFORE (bc57f60) === $ bin/fm-startup-memory-budget.sh read startup-memory-budget: invalid config/startup-memory-budget - config directory is symlinked exit=1 $ bin/fm-startup-memory-budget.sh report startup-memory-budget: invalid config/startup-memory-budget - config directory is symlinked exit=2 === AFTER (c362928) === $ bin/fm-startup-memory-budget.sh read 9000 exit=0 $ bin/fm-startup-memory-budget.sh report estimator=ceil(UTF-8 bytes / 3) conservative-local-estimate role=primary effective_budget_tokens=9000 file=data/captain.md bytes=14 estimated_tokens=5 status=present total_estimated_tokens=5 budget_status=within-budget exit=0

$ ls -l ~/.firstmate/config          # config/ linked into a machine-configuration repo
lrwxrwxrwx 1 daan daan   44 Aug 22 17:42 config -> /tmp/fm-demo/machine-config/firstmate-config
$ cat machine-config/firstmate-config/startup-memory-budget
9000

=== BEFORE (bc57f60) ===========================================
$ bin/fm-startup-memory-budget.sh read
startup-memory-budget: invalid config/startup-memory-budget - config directory is symlinked
exit=1
$ bin/fm-startup-memory-budget.sh report
startup-memory-budget: invalid config/startup-memory-budget - config directory is symlinked
exit=2

=== AFTER (c362928) ============================================
$ bin/fm-startup-memory-budget.sh read
9000
exit=0
$ bin/fm-startup-memory-budget.sh report
estimator=ceil(UTF-8 bytes / 3) conservative-local-estimate
role=primary
effective_budget_tokens=9000
file=data/captain.md bytes=14 estimated_tokens=5 status=present
file=data/captain-shared.md bytes=0 estimated_tokens=0 status=absent
file=data/learnings.md bytes=0 estimated_tokens=0 status=absent
total_estimated_tokens=5
budget_status=within-budget
exit=0
Evidence: .gitignore anchoring checked through git check-ignore

Source: .gitignore anchoring checked through git check-ignore

--- .gitignore @ bc57f60 (... config/) config/startup-memory-budget IGNORED bin/backends/config/settings IGNORED --- .gitignore @ HEAD (... /config) config/startup-memory-budget IGNORED bin/backends/config/settings tracked

$ # real consumer: git check-ignore inside a clone of this repo tree
--- .gitignore @ bc57f60: projects/ state/ data/ scratchpad/ .no-mistakes/ .lavish/ .fm-secondmate-home .fm-secondmate-parent .DS_Store __pycache__/ *.pyc .env config/ 
  config/startup-memory-budget       IGNORED
  bin/backends/config/settings       IGNORED
--- .gitignore @ HEAD: projects/ state/ data/ scratchpad/ .no-mistakes/ .lavish/ .fm-secondmate-home .fm-secondmate-parent .DS_Store __pycache__/ *.pyc .env /config 
  config/startup-memory-budget       IGNORED
  bin/backends/config/settings       tracked
- Outcome: ⚠️ 1 info across 1 run (7m53s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ tests/fm-startup-memory-budget.test.sh:172 - The added coverage only exercises read through a symlinked config/, but the reported symptom (STARTUP_MEMORY_BUDGET: invalid ... at every session start, incomplete /stow) is produced by fm_startup_memory_budget_materialize via bin/fm-bootstrap.sh:1095, which routes through the same fm_startup_memory_budget_config_dir_safe gate and additionally does mktemp/ln inside the symlinked directory. Nothing fails before the fix and passes after it for that path. Add one case (e.g. in test_primary_bootstrap_materializes_visible_default) that runs the bootstrap against a home whose config/ is a symlink to an empty real directory and asserts the 7500 default is published into the symlink target.

🔧 Fix: test bootstrap materialization through symlinked config dir
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 info
  • ℹ️ tests/fm-bootstrap.test.sh - tests/fm-bootstrap.test.sh fails identically at the base commit bc57f60 and at the target commit (not ok - the unsplit run lost its local diagnostic, output NEEDS_GH_AUTH). Pre-existing and environment-driven (no gh auth in this sandbox), not caused by this change. Note: this suite's git fixtures also pick up the machine's global git template hooks, so GIT_TEMPLATE_DIR must be neutralized to run them locally.
  • bash tests/fm-startup-memory-budget.test.sh (with an empty GIT_TEMPLATE_DIR so the machine's global git hooks do not block fixture commits) - all 4 cases pass
  • Regression proof: replaced bin/fm-startup-memory-budget-lib.sh with its bc57f60 version and reran the same suite - fails with not ok - bootstrap rejected a symlinked config directory: STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config directory is symlinked; restored afterwards (worktree verified clean)
  • Manual CLI check on a home with config/ -&gt; machine-config/firstmate-config: FM_HOME=... bin/fm-startup-memory-budget.sh read and report, before (exit 1 / exit 2, "config directory is symlinked") vs after (9000, full report, exit 0)
  • Manual session-start check: FM_BACKEND=tmux FM_HOME=&lt;symlinked-config home&gt; FM_ROOT_OVERRIDE=&lt;fixture root&gt; bin/fm-bootstrap.sh with a faked toolchain - before: prints STARTUP_MEMORY_BUDGET: invalid ... and materializes no budget file; after: silent, materializes 7500 through the symlink
  • Manual integrity checks through the symlink: budget file itself a symlink -> file is symlinked; config/ symlink to a missing path and to a regular file -> config directory is not a directory (all exit 1)
  • git check-ignore in throwaway repos using the base and HEAD .gitignore: config/startup-memory-budget ignored in both, bin/backends/config/settings ignored at base but tracked at HEAD
  • bash tests/fm-gitignore-config.test.sh - passes
  • bash tests/fm-bootstrap.test.sh at both base and target - identical pre-existing failure (NEEDS_GH_AUTH), unrelated to this change
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

A home whose config/ is a symlink into a machine-configuration repo is a
supported deployment layout, and every other setting under config/ is already
read through it. Only the startup-memory budget refused, so
fm-startup-memory-budget.sh read/report failed outright and every session start
printed "STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config
directory is symlinked".

The rejection bought no integrity property. It tested only the final path
component, so a symlinked ancestor passed silently, and it could never detect
the thing it looked like it was guarding: two homes resolving to one physical
config directory. The checks that actually stop this scalar from being aliased
or shared are per-file and are unchanged - the budget file itself must still be
a regular, non-symlinked, single-linked file holding exactly one positive
decimal value and one newline.

The directory now only has to resolve to a real directory, so a dangling
symlink and a symlink to a file are still rejected, with the existing
"config directory is not a directory" diagnostic.

Also anchors the config ignore rule to /config so a symlinked config/ is
ignored rather than showing up as an untracked entry in a captain's home.
@doitdigital0495 doitdigital0495 changed the title fix(startup-memory-budget): resolve through a symlinked config directory fix(bin): resolve the startup-memory budget through a symlinked config directory Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants