fix(bin): resolve the startup-memory budget through a symlinked config directory - #6
Merged
Merged
Conversation
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.
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
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:
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
config directory is symlinkedrejection inbin/fm-startup-memory-budget-lib.sh; the directory now only has to resolve to a real directory, while the budget file itself must still be a regular, single-linked, non-symlinked file.tests/fm-startup-memory-budget.test.shto cover bootstrap materialization andreadthrough a symlinkedconfig/, plus rejection when the budget file behind the symlink is itself a symlink, and whenconfig/points at a missing path or at a regular file..gitignoreentry as/configinstead ofconfig/, and updateddocs/configuration.mdto state that a symlinkedconfig/is a supported layout while listing the remaining rejection cases.Risk Assessment
✅ Low: Tightly scoped four-file change that removes one check at the single shared helper all callers route through, leaves the real per-file integrity guarantees intact, and adds behavioral regression tests that exercise the executables rather than source text.
Testing
Reproduced the reported failure first: with the base library, the new tests fail with the exact user-visible message 'STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config directory is symlinked', and the fixed library turns it green. Beyond the test file I drove the real CLI against a firstmate home whose config/ is a symlink into a machine-configuration repo, capturing a before/after transcript where
readgoes from exit 1 with that error to printing 9000 andreportaccounting all three startup files, plus a second transcript showing the retained integrity rejections (symlinked budget file, hardlinked budget file, config symlink to a missing path, config symlink to a regular file) all still firing through the symlink. The .gitignore change was exercised through its real consumer, git check-ignore, showing root config/ still ignored while nested docs/config and skills/**/config paths that the old unanchored rule swallowed are now visible. One test in the file initially failed for an environment reason, not a product one: the fixture's tempgit initinherited the machine-global init.templateDir pre-commit hook, which aborted the fixture commit; rerunning with an empty GIT_TEMPLATE_DIR passes, and CI has no such global template. No UI surface is involved - this is a shell CLI change, so the evidence is CLI transcripts. The worktree was left clean and matching the target commit.Evidence: Symlinked config/ CLI transcript (before base commit vs after)
Source: Symlinked config/ CLI transcript (before base commit vs after)
$ ls -l ~/.firstmate/config # config/ linked into a machine-configuration repo lrwxrwxrwx ~/home/config -> ~/machine-config-repo/firstmate-config --- BEFORE (base commit bc57f60) --- $ bin/fm-startup-memory-budget.sh read startup-memory-budget: invalid config/startup-memory-budget - config directory is symlinked exit=1 --- AFTER (e17a949) --- $ 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=0Evidence: Integrity rejections still enforced through a symlinked config/
Source: Integrity rejections still enforced through a symlinked config/
1) budget file itself a symlink startup-memory-budget: invalid config/startup-memory-budget - file is symlinked exit=1 2) budget file hardlinked startup-memory-budget: invalid config/startup-memory-budget - file is hardlinked exit=1 3) config/ symlink pointing at a missing path startup-memory-budget: invalid config/startup-memory-budget - config directory is not a directory exit=1 4) config/ symlink pointing at a regular file startup-memory-budget: invalid config/startup-memory-budget - config directory is not a directory exit=1Evidence: gitignore anchoring verified via git check-ignore
Source: gitignore anchoring verified via git check-ignore
=== /config (this change) === IGNORED config/startup-memory-budget <- .gitignore:13:/config IGNORED config/nested/deep <- .gitignore:13:/config TRACKED docs/config/example.yml TRACKED skills/foo/config/bar.json --- base .gitignore (config/) --- IGNORED config/startup-memory-budget <- .gitignore:13:config/ IGNORED config/nested/deep <- .gitignore:13:config/ IGNORED docs/config/example.yml <- .gitignore:13:config/ IGNORED skills/foo/config/bar.json <- .gitignore:13:config/Evidence: Regression proof: new tests fail against base library
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
docs/configuration.md:166- docs/configuration.md:166 still reads "Malformed, multi-line, symlinked, hardlinked, special, or otherwise unsafe values are rejected" immediately after the new line 165 declaring a symlinkedconfig/supported. "symlinked" here means the budget file, but sitting adjacent to the new sentence it can be misread as re-forbidding the directory. Suggest narrowing it to "symlinked or hardlinked budget files" since this page owns the contract.🔧 Fix: scope budget-file rejection wording in configuration docs
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-startup-memory-budget.test.sh(with GIT_TEMPLATE_DIR pointed at an empty dir to neutralize a machine-global pre-commit hook) - all 4 tests passRegression proof: same test file run against a temp copy withbin/fm-startup-memory-budget-lib.shchecked out at base commit bc57f60 - fails withnot ok - bootstrap rejected a symlinked config directory: ... config directory is symlinkedbash tests/fm-gitignore-config.test.sh- config/ still ignored as a directory categorybash tests/fm-documentation-audiences.test.sh- docs/configuration.md owner and link/prose contracts still holdManual CLI e2e: firstmate home whoseconfig/is a symlink into a machine-config repo,FM_HOME=... bin/fm-startup-memory-budget.sh readandreportbefore (base lib) vs afterManual rejection e2e through the same symlinked config/: budget file symlinked, budget file hardlinked, config symlink to a missing path, config symlink to a regular filegit check-ignore -vprobes forconfig/startup-memory-budget,config/nested/deep,docs/config/example.yml,skills/foo/config/bar.jsonagainst the new/configrule and the baseconfig/rule✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.