Skip to content

feat(bootstrap): add config/optional-tools to decline optional tools - #2

Open
kennyg wants to merge 9 commits into
mainfrom
fm/optional-tools-k1
Open

kennyg wants to merge 9 commits into
mainfrom
fm/optional-tools-k1

Conversation

@kennyg

@kennyg kennyg commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Intent

Add a local, gitignored config/optional-tools knob so an operator can declare that they have consciously declined an OPTIONAL tool, and stop bin/fm-bootstrap.sh reporting it as MISSING: at every session start.

PROBLEM: bin/fm-bootstrap.sh hardcodes COMMON_TOOLS="node git gh no-mistakes gh-axi chrome-devtools-axi lavish-axi tasks-axi quota-axi", and every absent entry prints a MISSING: line at every single session start, forever, with no way to say 'I know, I have decided against it'. Firstmate already has a gitignored config/ surface for exactly this class of local operating choice (config/backend, config/crew-harness, config/backlog-backend, and a dozen more; see docs/configuration.md). The inconsistency is already documented: config/backlog-backend=manual exists precisely so a home can run without tasks-axi, and the bootstrap-diagnostics skill states that setting it only suppresses the verbose BOOTSTRAP_INFO fact, not the missing-tool report. So firstmate already supports running without the tool while still nagging about it.

REQUIREMENTS AS BUILT:

  1. config/optional-tools: LOCAL, gitignored, one tool name per line, comments and blank lines ignored, following existing config/ file conventions for parsing, whitespace, and malformed input (the config/wedge-alarm line shape was the model). config/ is already gitignored wholesale, so no .gitignore change was needed.
  2. A declared-optional tool that is absent produces NO MISSING: line. Silence is the point - deliberately no differently-worded substitute nag. Everything else about detection is unchanged, and an absent tool NOT in the file still reports exactly as before.
  3. Only genuinely optional tools may be declined, determined from codebase evidence of a real fallback or degraded path, not assumption: gh-axi and chrome-devtools-axi gate GitHub and browser convenience work rather than any lifecycle step; lavish-axi is explicitly the optional visual surface for decisions plain chat already carries (AGENTS.md section 9); tasks-axi has the declared config/backlog-backend=manual fallback; quota-axi is read only to resolve a crew-dispatch profile array, which a home with no config/crew-dispatch.json never has. Tools whose absence breaks safety or core operation - git, node, jq, gh, no-mistakes, and the resolved backend's own required tools from fm_backend_required_tools - MUST NOT be declinable. Naming a non-declinable tool (or a typo) is an actionable configuration error reported as its own OPTIONAL_TOOLS: diagnostic line, never silently honored and never silently ignored. The backend-required case gets its own distinct message because the fix differs.
  4. Version-floor reports are deliberately NOT suppressed. Reporting an INSTALLED tool below its minimum version is a different condition from 'absent by choice' - an operator who installed the tool has not declined it. Those paths were left alone on purpose.
  5. Inheritance: config/optional-tools was ADDED to the declared FM_INHERITABLE_CONFIG allowlist in bin/fm-config-inherit-lib.sh, after evaluating that contract. Reasoning: it is the same class of statement as config/backlog-backend=manual ('this home runs without tool X'), which is already inherited. It is safe under the primary-authoritative contract specifically because each home re-resolves the non-declinable set against ITS OWN resolved backend, so an inherited name that a downstream home actually requires is reported there rather than honored - even a remote secondmate on a different machine with a different backend. Enumerations of the allowlist in docs/configuration.md and the secondmate-provisioning skill were updated to match.
  6. Documentation: docs/configuration.md owns the config schema and gained a 'Declined optional tools' section plus a pointer from the Toolchain section; AGENTS.md section 2's layout block gained the config/optional-tools line matching surrounding entries' style and terseness; the bootstrap-diagnostics skill gained an owner entry for the new OPTIONAL_TOOLS: line, and AGENTS.md section 13 plus the skill description gained the trigger token so the skill actually loads on it.
  7. Tests are colocated in tests/fm-bootstrap.test.sh following the repo's existing table-driven conventions, extending that existing script rather than inventing a new runner. Coverage: declared-optional absent tool is silent; undeclared absent tool still reports; non-declinable tool named in the file reports the configuration error; a backend-required tool reports its distinct error; a typo is reported AND the tool still reports missing; version-floor reports are unaffected; empty and whitespace-only files decline nothing; no file at all leaves reporting unchanged.

EXPLICITLY OUT OF SCOPE - do not flag these as gaps, they were ruled out deliberately:

  • Do NOT change how any tool is used at its call sites, and do NOT improve the error message when firstmate later needs a declined tool. That is a real known follow-up (today bin/fm-pr-merge.sh dies with a bare 'gh-axi: command not found') but it is separate work and would widen this change past easy review.
  • Do NOT install, vendor, shim, or stub any tool. A fake executable on PATH would make firstmate believe a tool is present and then fail confusingly when it uses it.
  • Do NOT add a new tool or language dependency.
  • Do NOT touch the five uname-based stat call sites or anything else unrelated.

HARD CONSTRAINT: keep the change small and reviewable. A tight diff is a requirement, not a preference - this repo's upstream has left two larger PRs unreviewed for five weeks, and size is the main thing working against it. Prefer patching existing language over adding new paragraphs.

VERIFICATION ALREADY DONE: bin/fm-lint.sh clean (ShellCheck 0.11.0), bin/fm-doc-audience-check.sh clean, and the compatibility requirement was verified rather than assumed - origin/main's bootstrap and this branch's were run against the same home with no config file and diffed byte-identical. All four acceptance criteria were also reproduced against the real toolchain. The full 146-script suite was run: 14 failures, of which 12 reproduce on a clean origin/main clone and 3 pass on both baseline and branch when re-run serially (parallel-load flakes); none are caused by this change.

What Changed

  • bin/fm-bootstrap.sh reads a new local, gitignored config/optional-tools file (one tool name per line, blank and # lines ignored) and suppresses the MISSING: line for each declined tool. Only gh-axi, chrome-devtools-axi, lavish-axi, tasks-axi, and quota-axi are declinable; any other name — a typo, or a tool the resolved backend requires — gets its own OPTIONAL_TOOLS: diagnostic instead of being honored, and a quota-axi declination is refused while config/crew-dispatch.json exists. Version-floor and feature-probe reporting is deliberately untouched, and FM_BOOTSTRAP_VERBOSE_FACTS=1 surfaces the honored declinations as a BOOTSTRAP_INFO fact.
  • config/optional-tools was added to FM_INHERITABLE_CONFIG in bin/fm-config-inherit-lib.sh, so declinations propagate to secondmate homes; each home still re-resolves the non-declinable set against its own backend, so an inherited name that home requires is reported there rather than honored.
  • Documented the new knob in docs/configuration.md (new "Declined optional tools" section, plus corrections to the Toolchain and backlog-backend paragraphs that previously asserted tasks-axi and quota-axi were unconditionally required), added the config line to AGENTS.md, gave bootstrap-diagnostics an owner entry for the OPTIONAL_TOOLS: line, and extended tests/fm-bootstrap.test.sh with table-driven coverage for declined, undeclared, non-declinable, backend-required, typo'd, empty, and absent-file cases plus unaffected version floors.

Risk Assessment

✅ Low: The behavioral surface is small and well-guarded - a new opt-in local config file whose absence is a no-op, applied only to the absence branch of the existing tool loop, with the one enforced condition and the verbose fact covered by new table-driven rows in the existing test script - and the remaining issue is a one-clause documentation gap in an agent playbook.

Testing

Ran the colocated bootstrap test script (covering both new declination cases) plus the inheritance, gitignore, and documentation tests — all green — then demonstrated the feature manually against this machine's real toolchain, where the five axi-family tools are actually absent. The captured CLI transcript shows the before/after nag suppression, complete silence when all declinable tools are declined, the verbose audit fact, all four refusal paths (core tool, runtime-required tool, typo, conditional quota-axi), an unaffected version-floor upgrade report, byte-identical output versus the base commit when no config file exists, and inheritance into a second mate home with per-home re-resolution. No UI surface is involved — this is a CLI diagnostic change, so the transcript is the end-user-visible artifact. Temp homes and the version shim were removed and the working tree is clean.

Evidence: Real-toolchain bootstrap transcript: declining optional tools end-to-end

--- 1. BEFORE: no config/optional-tools -- every absent optional tool nags at every session start --- $ bin/fm-bootstrap.sh MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks) MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks) MISSING: lavish-axi (install: npm install -g lavish-axi && lavish-axi setup hooks) MISSING: tasks-axi (install: npm install -g tasks-axi) MISSING: quota-axi (install: npm install -g quota-axi) --- 2. AFTER: the operator declares the tools they have consciously declined --- $ cat config/optional-tools # declined on purpose: this home uses plain chat and the manual backlog lavish-axi tasks-axi chrome-devtools-axi $ bin/fm-bootstrap.sh MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks) MISSING: quota-axi (install: npm install -g quota-axi) (only the two tools NOT declined still report -- silence for the declined ones) --- 4. Declining ALL five genuinely-optional tools: the session start goes completely quiet --- $ bin/fm-bootstrap.sh (no output at all) --- 9. Backward compatibility: a home with NO config/optional-tools sees byte-identical output to before the change --- $ diff <(base-commit bootstrap) <(branch bootstrap) identical (5 lines each)

=== A real firstmate home on this machine (gh-axi, chrome-devtools-axi, lavish-axi, tasks-axi, quota-axi are genuinely not installed) ===

--- 1. BEFORE: no config/optional-tools -- every absent optional tool nags at every session start ---
$ bin/fm-bootstrap.sh
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks)
MISSING: lavish-axi (install: npm install -g lavish-axi && lavish-axi setup hooks)
MISSING: tasks-axi (install: npm install -g tasks-axi)
MISSING: quota-axi (install: npm install -g quota-axi)

--- 2. AFTER: the operator declares the tools they have consciously declined ---
$ cat config/optional-tools
    # declined on purpose: this home uses plain chat and the manual backlog
    lavish-axi
    tasks-axi
    
    chrome-devtools-axi
$ bin/fm-bootstrap.sh
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: quota-axi (install: npm install -g quota-axi)
    (only the two tools NOT declined still report -- silence for the declined ones)

--- 3. The declination is auditable, not invisible (verbose facts mode) ---
$ FM_BOOTSTRAP_VERBOSE_FACTS=1 bin/fm-bootstrap.sh
BOOTSTRAP_INFO: declined optional tools: lavish-axi tasks-axi chrome-devtools-axi
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: quota-axi (install: npm install -g quota-axi)

--- 4. Declining ALL five genuinely-optional tools: the session start goes completely quiet ---
$ bin/fm-bootstrap.sh
    (no output at all)

--- 5. A tool firstmate cannot run without is refused, not silently honored ---
$ echo git > config/optional-tools; bin/fm-bootstrap.sh
OPTIONAL_TOOLS: invalid config/optional-tools - 'git' is not declinable (declinable: gh-axi chrome-devtools-axi lavish-axi tasks-axi quota-axi)
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks)
MISSING: lavish-axi (install: npm install -g lavish-axi && lavish-axi setup hooks)
MISSING: tasks-axi (install: npm install -g tasks-axi)
MISSING: quota-axi (install: npm install -g quota-axi)

--- 6. A tool the running backend requires gets its own message (the fix is different) ---
$ echo treehouse > config/optional-tools; bin/fm-bootstrap.sh
OPTIONAL_TOOLS: invalid config/optional-tools - 'treehouse' is required by the tmux backend and cannot be declined
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks)
MISSING: lavish-axi (install: npm install -g lavish-axi && lavish-axi setup hooks)
MISSING: tasks-axi (install: npm install -g tasks-axi)
MISSING: quota-axi (install: npm install -g quota-axi)

--- 7. A typo is reported AND the real tool still reports missing (no silent swallow) ---
$ echo lavish-axy > config/optional-tools; bin/fm-bootstrap.sh
OPTIONAL_TOOLS: invalid config/optional-tools - 'lavish-axy' is not declinable (declinable: gh-axi chrome-devtools-axi lavish-axi tasks-axi quota-axi)
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks)
MISSING: lavish-axi (install: npm install -g lavish-axi && lavish-axi setup hooks)
MISSING: tasks-axi (install: npm install -g tasks-axi)
MISSING: quota-axi (install: npm install -g quota-axi)

--- 8. Declining quota-axi while dispatch profiles exist is refused, with the remedy ---
$ bin/fm-bootstrap.sh   (with config/crew-dispatch.json present)
OPTIONAL_TOOLS: invalid config/optional-tools - 'quota-axi' cannot be declined while config/crew-dispatch.json exists, because resolving a dispatch profile array reads it; remove the quota-axi line from config/optional-tools, install quota-axi, or remove config/crew-dispatch.json
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks)
MISSING: lavish-axi (install: npm install -g lavish-axi && lavish-axi setup hooks)
MISSING: tasks-axi (install: npm install -g tasks-axi)
MISSING: quota-axi (install: npm install -g quota-axi)

--- 9. Backward compatibility: a home with NO config/optional-tools sees byte-identical output to before the change ---
$ diff <(base-commit bootstrap) <(branch bootstrap)
    identical (5 lines each)

--- 10. Declining a tool you actually INSTALLED does not silence its upgrade request ---
$ gh-axi --version   (an old build is on PATH for this demo)
    gh-axi 0.1.1
$ cat config/optional-tools
    gh-axi
$ bin/fm-bootstrap.sh
MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
    (installed-but-outdated is a different condition from declined-because-absent, so it still reports)

--- 11. The declination follows the captain into a second mate's home ---
$ cat <primary>/config/optional-tools
    # captain declined these fleet-wide
    lavish-axi
    tasks-axi
$ <propagate the primary's local config into the second mate home>
    exit=0
$ cat <secondmate>/config/optional-tools
    # captain declined these fleet-wide
    lavish-axi
    tasks-axi
$ bin/fm-bootstrap.sh   (run in the second mate home)
    MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)
    MISSING: chrome-devtools-axi (install: npm install -g chrome-devtools-axi && chrome-devtools-axi setup hooks)
    MISSING: quota-axi (install: npm install -g quota-axi)
    (the inherited declinations are silent there too; the rest still report)

--- 12. Each home judges the inherited list against ITS OWN runtime, so a name one home needs is reported there ---
$ cat config/optional-tools   (identical file in both homes)
    treehouse
$ bin/fm-bootstrap.sh   (primary home, tmux runtime -- needs treehouse)
    OPTIONAL_TOOLS: invalid config/optional-tools - 'treehouse' is required by the tmux backend and cannot be declined
$ bin/fm-bootstrap.sh   (second mate home, orca runtime -- does not need treehouse)
    OPTIONAL_TOOLS: invalid config/optional-tools - 'treehouse' is not declinable (declinable: gh-axi chrome-devtools-axi lavish-axi tasks-axi quota-axi)
    (same declared list, different verdict per home -- an inherited name is never honored blindly)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⚠️ **Rebase** - 1 warning

Push main to origin, or rebase your branch onto origin/main, before gating.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-bootstrap.sh:841 - gh-axi is in DECLINABLE_TOOLS, and the comment above it (line 832) justifies this as gating "GitHub ... convenience work rather than any lifecycle step". The codebase contradicts that: bin/fm-pr-merge.sh:84 invokes gh-axi pr merge directly with no fallback, and AGENTS.md section 7 mandates that script for every task PR merge ("never call a lower-level merge command around their guards"); bin/fm-brief.sh:363 likewise instructs the direct-PR worker to open the PR with gh-axi. Unlike tasks-axi (which genuinely degrades via fm_tasks_axi_backend_available falling back to hand-editing), gh-axi has no fallback or degraded path, so it does not meet the stated inclusion criterion. Either drop it from the declinable set, or correct the comment and the matching docs/configuration.md paragraph to state that declining it disables the PR merge and direct-PR delivery paths rather than claiming no lifecycle step depends on it.
  • ⚠️ bin/fm-bootstrap.sh:841 - quota-axi's declinability rests on an explicitly conditional premise (comment lines 837-839, repeated in docs/configuration.md): it is only optional for "a home without config/crew-dispatch.json". Nothing enforces that condition. A home that has dispatch profiles and declines quota-axi goes silent, yet AGENTS.md section 4 requires running quota-axi --json at every intake that resolves a matched profile array and forbids guessing or silently falling back, and docs/configuration.md already states firstmate "cannot resolve a profile array without a compatible binary" - so dispatch is blocked with no bootstrap warning. Bootstrap already reads $CONFIG/crew-dispatch.json in the same run (crew_dispatch_validate, line 1041), so honoring a quota-axi declination only when that file is absent, and otherwise emitting the OPTIONAL_TOOLS error, is a few lines and keeps the stated rationale true in code.
  • ⚠️ docs/configuration.md:308 - The Toolchain section still asserts "tasks-axi and quota-axi are required bootstrap tools in every profile, the same class as lavish-axi" without qualification, while the new "Declined optional tools" section in the same file (which docs/configuration.md owns as the single owner of the config schema) says all three may be declined. A reader following the Toolchain section reaches the opposite conclusion from the one following the new section. Qualify line 308 with the declination path the same way the neighbouring tasks-axi line at 309 already qualifies itself with the backlog-backend fallback.
  • ℹ️ bin/fm-config-inherit-lib.sh:70 - optional-tools was inserted mid-list, ahead of startup-memory-budget, trace-context, and data/captain-shared.md. On a remote secondmate whose code root has not yet fast-forwarded, bin/fm-remote-inherit.sh rejects the unknown path ("path is not inherited material") and bin/fm-remote-inherit-push.sh runs under set -eu, so the push aborts at that item - and it aborts even for a primary that has no config/optional-tools at all, because the absence mirror still sends absent config/optional-tools. This fail-closed-on-revision-skew behavior is documented and accepted (fm-config-inherit-lib.sh lines 50-56), so it is not a defect; the only observation is that appending the new item at the end of the list instead would let the pre-existing items still propagate during the skew window, shrinking the blast radius to captain-shared.md alone.

🔧 Fix: enforce quota-axi declination condition, append inherited optional-tools
1 warning still open:

  • ⚠️ bin/fm-bootstrap.sh:852 - The tasks-axi entry in DECLINABLE_TOOLS justifies itself with "tasks-axi has the declared config/backlog-backend=manual fallback", but that fallback is scoped to firstmate's own routine backlog mutations only. bin/fm-backlog-handoff.sh delegates the secondmate item move to tasks-axi mv unconditionally (its header: "The move needs compatible tasks-axi on PATH, including atomic multi-ID mv"), and fm_tasks_axi_backend_available in bin/fm-tasks-axi-lib.sh:119 is not consulted on that path. docs/configuration.md:43 states this in so many words: "Secondmate handoffs are separate and unconditional: fm-backlog-handoff.sh ... always delegates the item move to tasks-axi mv ... Because bootstrap requires tasks-axi on PATH on every profile, that delegation works fleet-wide, and the config/backlog-backend=manual knob governs firstmate's own hand-editing of its backlog, not this validated helper." Line 45 repeats the same unqualified premise. This change makes that premise false whenever tasks-axi is declined, yet only the parallel sentence at line 308 was qualified by FIX 3. Consequence: declaring tasks-axi in config/optional-tools without also setting config/backlog-backend=manual silences the only signal (docs:309 - "it hand-edits data/backlog.md until installation is approved and completed") and leaves the secondmate handoff to fail at use time with no bootstrap warning. This is the same shape as the quota-axi issue already fixed in commit 3ece094 - a conditional fallback stated as rationale but not enforced - and unlike gh-axi it is not merely a pre-existing call-site failure, because the code comment and docs actively assert a guarantee this change invalidates. Minimal fix: qualify docs/configuration.md:43 and :45 the same way line 308 was patched, and correct the DECLINABLE_TOOLS comment to scope the tasks-axi fallback to firstmate's own backlog editing. Stronger fix, mirroring FIX 1: extend declination_blocked_reason so a tasks-axi declination is refused unless config/backlog-backend is manual, reusing the existing OPTIONAL_TOOLS line shape.

🔧 Fix: document tasks-axi declination handoff limitation
4 infos still open:

  • ℹ️ .agents/skills/bootstrap-diagnostics/SKILL.md:19 - The MISSING: &lt;tool&gt; entry is the playbook an agent actually loads when a tool is absent, and its prescribed response is still only "list the missing tools ... wait for consent ... then run bin/fm-bootstrap.sh install". Nothing there references the new config/optional-tools path, so an agent facing MISSING: gh-axi every session has no sanctioned way to act on a captain who says "I've decided against it" - which is the exact problem this change exists to solve. The pointer was added to docs/configuration.md:313 ("To stop reporting an optional tool you have decided against, see 'Declined optional tools' below"), but that is operator-facing prose, not the agent-facing trigger surface. Minimal fix: add one clause to the MISSING entry noting that a genuinely optional tool the captain has decided against can be recorded in config/optional-tools instead of re-proposed each session, cross-referencing the docs section as the one owner rather than restating the declinable set.
  • ℹ️ .agents/skills/bootstrap-diagnostics/SKILL.md:39 - The new entry's remediation is "have the captain correct or remove the offending line", with no mention that config/optional-tools is now primary-authoritative inherited material (added to FM_INHERITABLE_CONFIG in bin/fm-config-inherit-lib.sh:70). Per that lib's own contract, "the primary's value wins and is re-pushed on every convergence", so on a secondmate home a local correction is silently reverted at the next bootstrap sweep or fm-config-push.sh run and the same OPTIONAL_TOOLS line returns. The STARTUP_MEMORY_BUDGET entry two lines above already handles exactly this shape correctly - "Correct the local primary file, then rerun session start so the normal convergence path can deliver the validated value to secondmate homes" - and is the pattern to mirror. Reachable case: an invalid or typo'd name is never removed from the file by bootstrap, so it propagates downstream and is reported on every home.
  • ℹ️ docs/configuration.md:342 - The rationale that now backs the enforced condition states flatly that "quota-axi is read only to resolve a crew-dispatch profile array, which a home with no config/crew-dispatch.json never has", and bin/fm-bootstrap.sh:855 repeats it. That is not the only read: .agents/skills/harness-adapters/SKILL.md:139 - a skill AGENTS.md section 4 requires loading before every spawn, not only before array resolution - directs firstmate to "establish which credential store a tuple reads from the discovery surfaces below plus quota-axi auth --json's per-provider sources" for ambiguous harness/model tuples such as harness=pi with model=xai/grok-*. That path is reachable in a home with no dispatch profiles at all, via an explicit per-task captain harness/model override. Consequence is narrow and non-silent (the agent hits an absent binary and must report it), but the enforced condition is presented as exactly matching the hazard when it covers only the array-resolution half. Minimal fix: qualify the sentence so the condition is scoped to the profile-array read it actually gates rather than claimed as the tool's only use. No behavior change implied - this is not an argument to widen the enforcement.
  • ℹ️ docs/configuration.md:344 - The newly documented tasks-axi residual gap names bin/fm-backlog-handoff.sh as the consequence. There is a second, harder one it does not mention: bin/fm-remote-doctor.sh:59 declares REQUIRED_TOOLS=(git jq herdr tasks-axi treehouse) and report_required_tools prints required tasks-axi=MISSING for an absent or incompatible build, which bin/fm-remote-readiness-lib.sh treats as a readiness failure and bin/fm-remote-home-seed.sh:241 turns into "remote runtime preflight failed; nothing was provisioned". So declining tasks-axi silences the local report while a remote secondmate route still hard-blocks at provisioning, and that enforcement point is not reached by the declinable-set logic at all. The failure is loud and self-describing, so this is a documentation-completeness gap rather than a hazard - but it is a second consequence a reader of the current sentence would not predict. Worth noting for whoever extends declination support later: bin/fm-remote-doctor.sh:61 already binds a variable literally named OPTIONAL_TOOLS to an unrelated meaning (tools whose absence is merely reported), so the two concepts will collide by name there.

🔧 Fix: qualify quota-axi and tasks-axi declination limits in docs
2 infos still open:

  • ℹ️ bin/fm-bootstrap.sh:788 - The refusal message for a conditionally-blocked quota-axi declination reads "...; remove config/crew-dispatch.json or install quota-axi", and docs/configuration.md:343 mirrors it ("clear it by removing the dispatch profiles or installing quota-axi"). Both omit the least-destructive and most likely correct remedy: delete the quota-axi line from config/optional-tools. As written, the first suggested action is to delete the captain's dispatch profiles - a real configuration loss - in order to silence a report. The bootstrap-diagnostics skill entry gets this right ("have the captain correct or remove the offending line"), but the emitted line is what a reader or a yolo-authorized agent acts on first, and it is the one surface that never mentions the declination line itself. Suggest reordering so the primary remedy is removing the quota-axi entry from config/optional-tools, then installing the tool, and dropping or de-emphasizing the dispatch-profile deletion. The same wording change applies to docs/configuration.md:343.
  • ℹ️ docs/configuration.md:345 - Lines 345 and 347 state the same fact twice: 345 says the partial quota-axi guard is worth keeping "unlike a tasks-axi condition, which was rejected for guarding nothing at all", and 347 says "Nothing gates that declination, since gating it on config/backlog-backend=manual would guard nothing". Beyond the duplication, line 345 references a rejection decision with no antecedent anywhere in the document - a reader who was not in the review has no idea what tasks-axi condition was rejected, and it appears two sentences before the tasks-axi paragraph that would give it context. Line 347 carries the reasoning in a place where it reads naturally. Suggest dropping the comparative clause from 345 (keeping "real enforcement of the hazard it does cover") and letting 347 own the rejected-alternative rationale, which also matches the one-owner rule the rest of this section follows.

🔧 Fix: lead quota-axi declination refusal with least-destructive remedy
1 warning still open:

  • ⚠️ docs/configuration.md:347 - The residual-limitation wording says a tasks-axi declination "matters only for a home with registered secondmates", and the DECLINABLE_TOOLS rationale at bin/fm-bootstrap.sh:852-855 names only secondmate handoff and remote provisioning as the gaps. Both miss a lifecycle hard-block that applies to EVERY home. bin/fm-teardown.sh:2325 runs fm-decision-hold.sh verify for every scout teardown that is not --force, and command_verify calls require_tasks_axi (bin/fm-decision-hold.sh:473 -> :159 fm_tasks_axi_compatible || fail), which is NOT routed through fm_tasks_axi_backend_available and so is not relaxed by config/backlog-backend=manual. Reachable case: a home declines tasks-axi in config/optional-tools, bootstrap goes silent, a scout finishes and writes its report, and bin/fm-teardown.sh &lt;id&gt; prints REFUSED: scout task &lt;id&gt; has not passed the unresolved-decision completion gate - a message that misattributes the cause, since the gate did not fail, the tool needed to evaluate it is absent. The operator cannot clear it either: review, resolve, decline, and repair all call the same require_tasks_axi (lines 361, 402, 515, 584, 623), so the decisions_reviewed marker the gate checks can never be set. The only remaining exit is --force, which AGENTS.md hard rule 3 reserves for explicit captain discard authority. bin/fm-public-followup.sh:142 (die &#34;tasks-axi is required&#34;) is a second unconditional dependency on the Relay path. Minimal fix, matching how round 3 handled the remote-doctor gap: extend the existing sentence at docs/configuration.md:346 to name the scout unresolved-decision completion gate (bin/fm-decision-hold.sh, enforced by bin/fm-teardown.sh) as a third consequence, drop or correct the "matters only for a home with registered secondmates" clause at line 347 since scout cleanup is universal, and adjust the bin/fm-bootstrap.sh comment's brief rationale to stop implying the gaps are secondmate-scoped. Whether that instead argues for removing tasks-axi from DECLINABLE_TOOLS is the captain's call - a lifecycle step that refuses with a misleading message is a different class from the convenience gaps the inclusion criterion was written for.

🔧 Fix: correct tasks-axi declination limitation scope and completeness
2 infos still open:

  • ℹ️ docs/configuration.md:347 - The sentence claims "every command of that script needs a compatible tasks-axi" for bin/fm-decision-hold.sh. Two of its seven commands do not: command_id (bin/fm-decision-hold.sh:338) is a pure ID-derivation helper with no require_tasks_axi call, and -h|--help (line 660) dispatches straight to usage. require_tasks_axi is called only from hold, complete, verify, resolve, decline, and repair (lines 361, 402, 473, 515, 584, 623). The substantive conclusion is unaffected and correct - verify cannot pass and resolve/decline/repair cannot clear it - but the universal quantifier is the one part of the newly-completed limitation list that does not survive a check against the script. Narrow it to the gate-relevant commands (verify, resolve, decline, repair) so the sentence is exactly as verifiable as the rest of the list.
  • ℹ️ bin/fm-bootstrap.sh:848 - The DECLINABLE_TOOLS comment has grown to sixteen lines (848-863) that restate material docs/configuration.md "Declined optional tools" is designated to own: the per-tool fallback evidence (docs:342), the enforced quota-axi condition and its uncovered second read surface (docs:343-345), the tasks-axi residual gap (docs:346-348), and the never-declinable list, which is close to verbatim against docs:350. Two rounds of review instruction asked for "a brief accurate rationale that points at the docs owner", and this is the one place the change carries the same contract twice - the practical cost is that a future correction to the declination contract must land in both places or they drift, which is exactly the failure mode rounds 2 through 5 kept surfacing. Reducing it to the inclusion criterion (a real fallback or degraded path), the fact that declination_blocked_reason enforces the quota-axi condition, and a pointer to the docs section would keep every fact recoverable from one owner.

🔧 Fix: narrow decision-hold claim, trim declinable-tools comment
2 infos still open:

  • ℹ️ bin/fm-bootstrap.sh:67 - The file-header comment says "The declinable set and its per-tool fallback evidence are owned beside DECLINABLE_TOOLS below", but commit 903c510 trimmed that comment (lines 848-853) precisely so it no longer owns that material - it now reads "docs/configuration.md 'Declined optional tools' owns the per-tool rationale, the never-declinable set, the conditions declination_blocked_reason enforces, and what a declination still leaves broken." So the header now sends a reader to a pointer rather than to evidence; only the declinable set itself is genuinely owned at that line. Minimal fix: point the header at docs/configuration.md "Declined optional tools" for the per-tool rationale and keep the DECLINABLE_TOOLS reference for the set alone. While editing that block, note lines 61-65 immediately above still assert unqualified that "tasks-axi and quota-axi are required bootstrap tools" and that "quota-axi is required for the agent-owned dispatch-profile array procedure" - the same sentence FIX 3 qualified at docs/configuration.md:314 with "unless declined through config/optional-tools". A one-clause qualification there keeps the header from contradicting the paragraph six lines below it. Comment-only, no behavior change.
  • ℹ️ bin/fm-bootstrap.sh:816 - An honored declination is durable, primary-authoritative, and inherited into every secondmate home, yet it leaves no trace on any bootstrap surface - DECLINED_TOOLS is consumed only by tool_declined and never reported. Silence in the default session start is the explicit requirement and should stay, but this repo already has a channel for exactly this class of fact: FM_BOOTSTRAP_VERBOSE_FACTS=1 is off by default and never set by bin/fm-session-start.sh, and it is what surfaces BOOTSTRAP_INFO: crew harness override active: &lt;crew&gt; (line 1243) and BOOTSTRAP_INFO: crew dispatch active config/crew-dispatch.json (line 1128) - both durable local operating choices with the same forgettability problem. Reachable case: a captain declines gh-axi on the primary, it propagates fleet-wide, and months later there is no way to notice the declination short of reading the file on each home. One line guarded by the same flag (BOOTSTRAP_INFO: declined optional tools: $DECLINED_TOOLS) would restore on-demand discoverability without adding a substitute nag to any normal session. Raising this as a question rather than a fix, since requirement 2 deliberately chose silence and the flag semantics are the author's call.

🔧 Fix: surface honored tool declinations as verbose bootstrap fact
1 info still open:

  • ℹ️ .agents/skills/bootstrap-diagnostics/SKILL.md:25 - The new clause in the MISSING: entry tells an agent to "record that once in config/optional-tools" without saying which home's file to write. The OPTIONAL_TOOLS: entry two rules below (line 41) states the missing half correctly - "config/optional-tools is primary-authoritative inherited material, so correcting it on a secondmate home is reverted at the next convergence and the same line returns; correct the local primary file" - but that entry only fires on a configuration error, while line 25 is the clause an agent reaches on the ordinary path (captain says "I've decided against gh-axi" in response to a MISSING line). Reachable loop on a secondmate home: the agent writes the secondmate's own config/optional-tools, the tool goes quiet for that session, propagate_inheritable_config (bin/fm-config-inherit-lib.sh:521-526) mirrors the primary's absence downstream at the next bootstrap sweep or bin/fm-config-push.sh, and the MISSING line returns next session - so the agent re-records it, indefinitely. Minimal fix: add "on the primary home" (or the same cross-reference line 41 already uses) to the line 25 clause. The STARTUP_MEMORY_BUDGET entry at line 37 is the established phrasing to mirror.
✅ **Test** - passed

✅ No issues found.

  • ./bin/fm-test-run.sh tests/fm-bootstrap.test.sh (includes new test_optional_tools_declination and test_optional_tools_do_not_suppress_version_floors)
  • ./bin/fm-test-run.sh tests/fm-trace-context-lib.test.sh tests/fm-secondmate-harness.test.sh tests/fm-gitignore-config.test.sh tests/fm-documentation-audiences.test.sh
  • Manual: real-toolchain FM_HOME=&lt;temp home&gt; bin/fm-bootstrap.sh with no config/optional-tools, capturing the five MISSING lines an operator sees at every session start
  • Manual: same home with config/optional-tools declaring lavish-axi/tasks-axi/chrome-devtools-axi (plus a comment and blank lines) — only the two undeclared tools report
  • Manual: FM_BOOTSTRAP_VERBOSE_FACTS=1 bin/fm-bootstrap.sh — honored declinations surface as a BOOTSTRAP_INFO fact
  • Manual: all five declinable tools declined — bootstrap emits no output at all
  • Manual: git, treehouse, lavish-axy, and quota-axi-with-crew-dispatch.json each declined in turn — distinct OPTIONAL_TOOLS errors, tool still reported missing
  • Manual: base-commit bin/fm-bootstrap.sh (via git archive f1a4af4 bin) vs branch, same home, no config file — diff byte-identical
  • Manual: temp PATH shim reporting gh-axi 0.1.1 while gh-axi is declined — version-floor report still printed
  • Manual: propagate_inheritable_config &lt;primary&gt;/config &lt;secondmate&gt;/config then bootstrap in the second mate home — inherited declinations honored there
  • Manual: identical config/optional-tools (treehouse) run against a tmux-runtime home and an orca-runtime home — different verdict per home
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

kennyg added 9 commits August 15, 2026 12:35
An operator who has consciously decided against an optional tool had no way
to say so: bin/fm-bootstrap.sh reported every absent COMMON_TOOLS entry as a
MISSING: line at every session start, forever. config/backlog-backend=manual
already lets a home run without tasks-axi while still nagging about it.

Add a local, gitignored config/optional-tools file, one declined tool per
non-empty non-comment line (same line shape as config/wedge-alarm). An absent
tool named there prints no MISSING: line.

Only genuinely optional tools are declinable, each with a real fallback or
degraded path: gh-axi and chrome-devtools-axi gate convenience work rather
than a lifecycle step, lavish-axi is the optional visual surface for decisions
plain chat already carries, tasks-axi has the declared backlog-backend=manual
fallback, and quota-axi is read only to resolve a crew-dispatch profile array.
node, git, gh, jq, no-mistakes, and the resolved backend's own required tools
from fm_backend_required_tools are never declinable; naming one, or a typo,
prints OPTIONAL_TOOLS: instead of being honored, so declination can never
silence a tool a home actually needs.

Declination covers absence only. The version-floor and feature-probe checks
are untouched: installing a tool is not declining it.

The file joins the declared FM_INHERITABLE_CONFIG allowlist, matching the
backlog-backend precedent for a "this home runs without tool X" choice. That
is safe under the primary-authoritative contract because each home resolves
the non-declinable set against its OWN backend, so an inherited name that home
requires is reported there rather than honored.

With no config/optional-tools present, bootstrap output is byte-identical.
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.

1 participant