Skip to content

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

Merged
doitdigital0495 merged 3 commits into
mainfrom
fm/fm-config-symlinked
Aug 23, 2026
Merged

fix(bin): resolve the startup-memory budget through a symlinked config directory#6
doitdigital0495 merged 3 commits into
mainfrom
fm/fm-config-symlinked

Conversation

@doitdigital0495

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

  • Dropped the config directory is symlinked rejection in bin/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.
  • Extended tests/fm-startup-memory-budget.test.sh to cover bootstrap materialization and read through a symlinked config/, plus rejection when the budget file behind the symlink is itself a symlink, and when config/ points at a missing path or at a regular file.
  • Anchored the .gitignore entry as /config instead of config/, and updated docs/configuration.md to state that a symlinked config/ 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 read goes from exit 1 with that error to printing 9000 and report accounting 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 temp git init inherited 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=0

$ ls -l ~/.firstmate/config          # config/ linked into a machine-configuration repo
lrwxrwxrwx 1 daan daan 57 Aug 22 22:55 ~/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=0
Evidence: 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=1

=== integrity guarantees still enforced through a symlinked config/ (AFTER) ===

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=1
Evidence: 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/

=== .gitignore anchoring: /config vs config/ (real consumer: git check-ignore) ===
IGNORED  config/startup-memory-budget   <- .gitignore:13:/config	config/startup-memory-budget
IGNORED  config/nested/deep   <- .gitignore:13:/config	config/nested/deep
TRACKED  docs/config/example.yml
TRACKED  skills/foo/config/bar.json

--- same probes against the base .gitignore (config/) ---
IGNORED  config/startup-memory-budget   <- .gitignore:13:config/	config/startup-memory-budget
IGNORED  config/nested/deep   <- .gitignore:13:config/	config/nested/deep
IGNORED  docs/config/example.yml   <- .gitignore:13:config/	docs/config/example.yml
IGNORED  skills/foo/config/bar.json   <- .gitignore:13:config/	skills/foo/config/bar.json
Evidence: Regression proof: new tests fail against base library
$ (copy of worktree with bin/fm-startup-memory-budget-lib.sh at bc57f60)
$ bash tests/fm-startup-memory-budget.test.sh
not ok - bootstrap rejected a symlinked config directory: STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - config directory is symlinked
EXIT=1

$ (target commit e17a949)
$ bash tests/fm-startup-memory-budget.test.sh
ok - primary bootstrap materializes only the visible default and preserves valid captain choices
ok - budget parser accepts one exact positive value and rejects malformed or unsafe inputs
ok - budget accounting sums the three startup files and reports safe failures
ok - budget propagation converges through config push with exact rereads, absence, and safe rejection
# all fm-startup-memory-budget tests passed

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 symlinked config/ 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 pass
  • Regression proof: same test file run against a temp copy with bin/fm-startup-memory-budget-lib.sh checked out at base commit bc57f60 - fails with not ok - bootstrap rejected a symlinked config directory: ... config directory is symlinked
  • bash tests/fm-gitignore-config.test.sh - config/ still ignored as a directory category
  • bash tests/fm-documentation-audiences.test.sh - docs/configuration.md owner and link/prose contracts still hold
  • Manual CLI e2e: firstmate home whose config/ is a symlink into a machine-config repo, FM_HOME=... bin/fm-startup-memory-budget.sh read and report before (base lib) vs after
  • Manual rejection e2e through the same symlinked config/: budget file symlinked, budget file hardlinked, config symlink to a missing path, config symlink to a regular file
  • git check-ignore -v probes for config/startup-memory-budget, config/nested/deep, docs/config/example.yml, skills/foo/config/bar.json against the new /config rule and the base config/ rule
✅ **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 merged commit 66bda59 into main Aug 23, 2026
13 checks passed
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