Skip to content

feat: add explicit worktree provisioning overrides - #202

Open
ruby-dlee wants to merge 11 commits into
mainfrom
fm/fm-provisioning-cont-r7
Open

feat: add explicit worktree provisioning overrides#202
ruby-dlee wants to merge 11 commits into
mainfrom
fm/fm-provisioning-cont-r7

Conversation

@ruby-dlee

Copy link
Copy Markdown
Owner

Intent

Recover and ship the abandoned Firstmate worktree-provisioning change from exact preserved commit ef049ac onto a fresh current-main lineage without reviving predecessor workspace, stale branch, or obsolete PR 96. Preserve every reviewed security correction: project path_prepend must affect only the crewmate session after Firstmate's complete launch line resolves; it must never repoint harnesses, wrappers, interpreters, shells, or turn-end hook commands; component-level path_prepend overrides manifest-level order; unexpected explicit-provisioner outcomes fail closed under block policy while documented declaration-driven capability gaps remain explicit and launchable. Reconcile with current main's declaration-driven provisioner rather than replacing or weakening it, recover the known fixture isolation and obsolete continuation-assertion corrections from predecessor evidence, make no unsupported attribution claim about the other 27 historic failures, validate with focused/full tests and a real isolated Treehouse ready/block/capability-gap exercise, preserve ef049ac remotely on a new non-force ref, keep PR 96 closed and unmerged, then push one new branch and open one replacement PR. Do not merge.

What Changed

  • Add explicit worktree provisioning manifests, CLI overrides, lifecycle handling, and documentation for project/component setup and runtime path publication.
  • Integrate provisioning with crewmate spawning while preserving declaration-driven capability gaps and failing closed when pinned raw launch commands cannot be resolved safely before endpoint creation.
  • Expand provisioning and spawn coverage, isolate shared test fixtures, and harden watcher, AFK, Herdr, and teardown cleanup against cross-run process contention.

Risk Assessment

✅ Low: The final correction accurately documents the implemented early and shared pre-endpoint refusal boundaries, and the full branch review found no remaining material source-verifiable risks or intent contradictions.

Testing

The successful baseline full test step was supplemented with focused real-watcher pause/cleanup validation and end-to-end provisioning exercises covering ready, block-policy refusal, capability-gap launch, PATH isolation, component override, runtime pins, raw-launch refusal, and durable lane reporting; CLI evidence was captured, no watcher-owned orphan remained, and the worktree stayed clean.

Evidence: Provisioning end-to-end CLI transcript
== tests/fm-provision.test.sh ==
ok - a project with no manifest provisions nothing and exits clean
ok - a fresh worktree is built and its probes are run
ok - an unchanged, healthy environment is reused instead of rebuilt
ok - a matching fingerprint over a broken environment rebuilds instead of trusting the directory
ok - a changed declared input rebuilds the environment
ok - a fingerprint that could not be computed forces a rebuild every time
ok - a version command that depends on what it fingerprints is reported, not silently recorded
ok - a fingerprint that appears only after installation is refused
ok - a runtime mismatch fails before anything is built under the wrong runtime
ok - a step's expect compares against its stdout, not against a trailing stderr notice
ok - a version command's stderr never contributes to the fingerprint it computes
ok - a failing step is still diagnosed from its stderr
ok - a component's own path_prepend wins over the manifest-level default
ok - a path_prepend entry carrying the PATH separator is refused at both levels
ok - a runtime check that prints nothing never satisfies an expected value
ok - probe expectations use the same exact-output boundary as runtime checks
ok - a hanging step is killed at its bound and reported as a timeout
ok - bounded steps supervise and kill their complete process group
ok - reset deletion cannot outlive the whole-run provisioning budget
ok - a block-policy failure is a distinct exit code from a warn-policy failure
ok - provisioning is limited to the manifest's kinds and --force overrides it
ok - component and reset paths that escape their base are refused
ok - reset targets can never resolve to the component root
ok - fingerprint paths cannot name their component root
ok - component roots retain the deliberate worktree-root asymmetry
ok - symlinked reset and fingerprint paths cannot escape the worktree
ok - a manifest cannot smuggle a runtime past the runtime checks through env PATH
ok - an unreadable or unrecognized policy fails closed while an absent one still warns
ok - the pinned runtime is published only by a verdict that proved it
ok - a components field that is not an array fails closed instead of reporting ready
ok - a step list that is not an array fails closed instead of provisioning nothing
ok - a step that reads stdin cannot skip the remaining steps of its own list
ok - a task-scoped run leaves a durable verdict and step log behind
ok - a project's manifest resolves from config/provision/<project>.json
ok - the pinned launcher resolves against the inherited PATH and only then applies the pin
ok - a ready provisioning run delivers its proven runtime to the crewmate's session
ok - a runtime pin decides nothing firstmate names, in the launch line or in a turn-end hook
ok - a turn-end hook that rides the launch line names both its shell and its touch absolutely
ok - a raw launch command resolves before its child session receives the pin
ok - an unpinned raw launch command remains unchanged
ok - unpinnable raw launches fail closed before endpoint creation
ok - relative raw paths are validated in the leased worktree before endpoints
ok - a missing or unrunnable pinned launcher refuses before the lease, never downgrades
ok - the pre-lease hook pin gate refuses only a spawn that could publish a pin
ok - a provisioner that lost its exec bit is a provisioning failure, not a silent skip
ok - a missing provisioner is a policy-governed failure, not a silent skip
ok - the warn default continues loudly when the provisioner is missing
ok - a warn-policy provisioning failure launches the crewmate but tells it and firstmate
ok - a block-policy project refuses to launch an agent into an unready worktree
ok - --no-provision retires stale evidence and skips provisioning
ok - a skipped provisioning verdict publishes no readiness evidence
ok - provisioning residue is caught while the spawn can still refuse
ok - on_failure=block fails closed when the provisioner dies without a verdict
ok - the warn default continues loudly when the provisioner dies without a verdict
ok - an unreadable failure policy is resolved as block, not as the warn default
ok - an unreadable policy aborts the spawn on the verdict-bearing path too
ok - a resolved block policy survives a provisioner that reported the warn exit code
ok - rewriting the readiness section over an existing one keeps the brief byte-stable
# all fm-provision tests passed
== tests/fm-spawn-provision.test.sh ==
ok - a worktree declaring no recognized manifest provisions nothing and succeeds
ok - provisioning installs once and reuses an unchanged worktree without install cost
ok - a changed dependency manifest invalidates the cache
ok - installer configuration that changes what is installed invalidates the cache
ok - a fingerprint match with a broken environment is a miss, not a hit
ok - Node installs include declared validation dependencies deterministically
ok - an environment whose recorded runtime no longer matches is reprovisioned
ok - pip directives and includes are parsed, so an unchanged component stays cached
ok - build output written inside node_modules does not invalidate a healthy cache
ok - a declared manifest that cannot be read refuses instead of hashing to nothing
ok - a component nested past the scan depth is reported everywhere, not dropped
ok - a dependency scan that outlives its bound warns, reports, and launches unprovisioned
ok - a uv pip check that could not run is recorded as unverified, never as a verdict
ok - a python project with no committed lock or requirements file is named, not silently skipped
ok - the pyproject declaration read is bounded and degrades to undetermined
ok - a pyproject.toml holding only tool configuration provisions nothing and reports nothing
ok - a reported metadata inconsistency is recorded and launches; an unusable environment still refuses
ok - a failing install refuses the spawn and records no fingerprint
ok - an install that hangs is bounded and refuses instead of wedging the spawn
ok - a signal-killed bounded install is reported as a failure
ok - empty, zero, and nonnumeric provisioning tunables refuse before detection
ok - a traversal failure is distinct from a successful undeclared-worktree no-op
ok - a directory name containing another as a token is not deduped away
ok - a bounded call to a command that cannot be executed is a failure
ok - an environment signature that captures nothing refuses instead of hashing nothing
ok - a host without python3 warns and launches unprovisioned rather than refusing
ok - a host with no bounding mechanism runs nothing and launches unprovisioned
ok - a python project with no uv is left unprovisioned and reported, not refused
ok - a recognized but unsupported package manager is a recorded gap, not a refusal
ok - a worktree over the component budget provisions what it can and reports what it skipped
ok - the component budget is spent on what the task names before detection order
ok - a host capability gap is reported to the lane, not only to the operator
ok - a symlink planted at the report path is replaced rather than written through
ok - a requirements graph past the traversal cap is a gap, never a fingerprint over a prefix
ok - the JS package manager comes from what the project declares, not from lockfile precedence
ok - detection is driven by what the worktree declares and skips installed trees
ok - a uv workspace syncs every member while a single project keeps the plain sync
ok - uv workspace member manifests participate in the cache fingerprint
ok - a Node pin is read from what the project declares, and only when unambiguous
ok - a declared Node runtime that cannot be found leaves the JS components unprovisioned
ok - a declared Node runtime is resolved to its highest installed match and exported for the lane
ok - a Node pin wins for the Node toolchain and changes resolution for nothing else
ok - a shared pinned Node prefix is published once and never rebuilt under a running lane
ok - concurrent publishes each return a complete prefix and never nest one inside the other
ok - a published prefix that stops validating is stepped over, leaving the stale one untouched
ok - a spawn provisions its worktree after verification and before endpoint creation
ok - provisioning failure refuses the spawn instead of launching a lane that cannot validate
ok - a worktree over the component budget launches, warns, and records what it skipped
ok - the crewmate can read, inside its own worktree, what provisioning skipped and why
ok - teardown removes the provisioning log with the rest of the per-task state
ok - a later provisioning failure leaves the worktree clean without writing to the main clone
ok - an unignored install directory refuses before any installer runs, and never writes to the main clone
ok - provisioning is opt-out per spawn and per home, and the choice is recorded
ok - a lease that provisions nothing clears the previous task's report instead of inheriting it
ok - an unparseable provisioning setting refuses rather than silently disabling the gate
ok - an automatic runtime pin reaches the agent session only after the launch command resolves
ok - automatic pins refuse unresolved raw launches before endpoint creation
ok - a project declaring no recognized manifest spawns exactly as before
# all fm-spawn-provision tests passed
Evidence: Watcher pause/cleanup CLI transcript
== tests/fm-watch-pause-absorb.test.sh ==
ok - live-but-idle paused pane: a parked run-step no longer vetoes a declared pause
ok - dead paused pane: a declared pause is honoured with no readable terminal at all
ok - paused with an OPEN decision: an unanswered question still surfaces, pause verb notwithstanding
ok - paused with a CLOSED decision: the resolved-then-paused c8 shape absorbs
ok - a cached pause verdict is re-proven whenever the status stream changes under it
ok - a pause invalidated during pipeline-state validation fails closed
ok - busy fleet: a declared pause is registered before an actionable sibling signal exits the watcher
ok - busy fleet: a registered pause still re-surfaces on its bounded long cadence
ok - cached pause: a newly active run outranks unchanged durable pause status
ok - crew_absorb_class: a declared pause outranks every verdict but working, and an open decision blocks it
- Outcome: 🔧 1 issue found → auto-fixed (6) ✅ across 7 runs (55h25m28s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (4) ✅
  • 🚨 bin/fm-spawn.sh:4357 - The required invariant says project path_prepend "must affect only the crewmate session after Firstmate's complete launch line resolves," but this branch deliberately launches raw commands unpinned when their first word is a shell builtin, alias, function, construct, or otherwise not PATH-resolvable. A manifest can therefore finish ready, publish a pin, and still start a crewmate whose session never receives it. Either refuse this explicitly opted-in pinned spawn, or introduce the pin at the earliest boundary after the shell has resolved the complete raw launch while preserving shell semantics.

🔧 Fix: Fail closed on unpinnable raw launches
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-spawn.sh:2722 - A slash-containing raw first word is treated as safely pinnable without checking that it names an executable. With --harness &#39;/missing/crewtool --go&#39; and a manifest that publishes path_prepend, the pre-lease gate passes, provisioning runs, the tmux/zellij endpoint and metadata are created, and only the in-pane launcher later exits 127. Validate slash-containing words as executable before declaring the line deliverable so unresolved raw launches fail before endpoint creation as required.
  • ⚠️ docs/configuration.md:506 - The configuration contract still says every launch runs under a pin and that unpinnable commands are never refused, directly contradicting the new fail-closed behavior. Update this paragraph to state that builtin, alias, function, construct, and unresolved raw first words are refused when an applicable manifest may publish path_prepend, while remaining unchanged without a pin.

🔧 Fix: Validate raw executable paths before spawning
2 errors still open:

  • 🚨 bin/fm-spawn.sh:3418 - The fail-closed gate only checks explicit manifests, but declaration-driven provisioning can also publish PROVISION_PATH_PREFIX. An unresolved raw launch with no explicit manifest can therefore acquire/provision a worktree; on tmux/zellij/cmux, spawn_pin_launch_line rejects it only after endpoint creation and metadata installation. Apply the raw-launch deliverability gate to any declaration-driven component that may publish a runtime pin, or move the definitive check to the shared post-provision/pre-endpoint boundary.
  • 🚨 bin/fm-spawn.sh:2736 - Relative executable paths are validated against PROJ_ABS, but the launcher later executes them from the leased worktree. If the primary checkout and leased live-default worktree differ, ./crewtool can pass preflight because it exists only in the primary and then fail 127 after endpoint creation, or be falsely refused when it exists only in the lease. Validate relative raw paths against the acquired WT at the common post-acquisition/pre-endpoint boundary; absolute paths can remain preflight-checked.

🔧 Fix: Gate published pins before endpoint creation
1 warning still open:

  • ⚠️ docs/configuration.md:507 - The documentation says every unpinnable raw path is refused before leasing, but relative paths and declaration-driven pins are intentionally validated only after worktree acquisition and provisioning (while still before endpoint creation). State the actual boundary: early refusal when knowable from an explicit manifest, otherwise definitive post-provision/pre-endpoint refusal.

🔧 Fix: Clarify runtime pin refusal boundaries
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed (6) ✅
  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: Confirm failures stem from cross-run resource contention
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: Confirm isolated Herdr teardown and suite seal pass
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: Exec Herdr event reader to prevent orphaning
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: Keep legacy migration fresh-report fixture current
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: Retry busy AFK launcher namespace guards
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"

🔧 Fix: Reap watcher poll sleep during shutdown
✅ Re-checked - no issues remain.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"
  • Baseline already completed successfully: command -v tmux &gt;/dev/null || { echo &#34;tmux is required for e2e tests&#34; &gt;&amp;2; exit 1; }; tmux -V; rc=0; tests/run.sh || rc=1; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit &#34;$rc&#34;
  • tests/fm-watch-pause-absorb.test.sh under the repository isolation runner, including post-run owned-process sealing
  • tests/fm-provision.test.sh tests/fm-spawn-provision.test.sh
  • Manual post-test inspection: git status --short and ps -axo pid=,ppid=,pgid=,command= | grep &#39;[s]leep 1&#39; || true
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@ruby-dlee
ruby-dlee force-pushed the fm/fm-provisioning-cont-r7 branch from 3de004b to 26f6417 Compare August 14, 2026 23:23
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