Skip to content

chore: update pull request - #9

Merged
pruge merged 4 commits into
mainfrom
fm/runtime-provisioning-t02
Aug 24, 2026
Merged

chore: update pull request#9
pruge merged 4 commits into
mainfrom
fm/runtime-provisioning-t02

Conversation

@pruge

@pruge pruge commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Intent

Complete dev-runtime provisioning for firstmate: port two tools from the legacy firstmate home into this repo so task worktrees get a runnable dev environment and port cleanup stays safe. (1) Port bin/fm-worktree-bootstrap.sh: fill a task worktree's untracked dev environment (backend .dev.vars, frontend .env files, local wrangler/D1 database state) that the project declares in its own .worktreeinclude file (one repo-relative path per line, '#' comments allowed). Deliberate adaptation decision per sibling ticket runtime-provisioning-t01's established convention: the copy SOURCE is this home's own clone of the project under FM_HOME/projects/ - never the captain's separately running dev checkout and not the legacy config/captain-projects-root config surface; follow the style of bin/fm-worktree-runtime-lib.sh rather than duplicating it. Bootstrap contract: copy what exists, skip what does not silently (no warning, still exit 0), warn on stderr for individual copy failures without failing the task (best-effort, exit 0 even when a copy fails), refuse absolute or parent-relative declaration paths loudly with non-zero exit, always overwrite destination files fresh, and have NO deletion path anywhere - stale extra files are left for teardown to discard with the whole worktree. Exit 0 on every no-op (no project clone, no .worktreeinclude, unrelated repository) so a worker elsewhere is never blocked. (2) Port bin/fm-kill-port.sh with contract unchanged: free local TCP ports BY PORT number only - boundary defaults to the caller's git top level else PWD, overridable with --boundary

; REFUSE a listener whose working directory is outside the boundary, naming the directory found and leaving it running (exit 1); --any is an explicit operator escape hatch skipping the boundary check; each listener gets TERM then KILL only after a short grace; a port with no listener is reported and is not an error; unknown flags are usage errors (exit 2); there must be NO fleet-wide name-pattern kill capability. (3) Tests ported into the current runner patterns: tests/fm-kill-port.test.sh drives real python3 http.server listeners through the command line across temp-dir-rooted inside/outside fixtures; tests/fm-worktree-bootstrap.test.sh is a new suite with synthetic git worktree + fake FM_HOME fixtures covering no-op cases, copying, silent skips, overwrite-without-deletion, unsafe-path refusal, and warn-don't-fail behavior; both use tests/lib.sh helpers and stay portable. Acceptance criteria: both suites green under bin/fm-test-run.sh; bin/fm-lint.sh clean; branch diff touches only bin/fm-worktree-bootstrap.sh, bin/fm-kill-port.sh, and tests/. Commit message: 'feat(bin): port worktree env bootstrap and boundary-safe port cleanup'.

What Changed

Final changed paths and statuses:

A	bin/fm-kill-port.sh
A	bin/fm-worktree-bootstrap.sh
M	docs/scripts.md
A	tests/fm-kill-port.test.sh
A	tests/fm-worktree-bootstrap.test.sh

Risk Assessment

✅ Low: All previously agreed fixes are correctly implemented and behaviorally verified; the only residuals are two minor diagnostic-label inaccuracies in best-effort warning paths that do not affect copy outcomes or exit codes.

Testing

Ran the two new targeted suites under the repo's official runner (both green) plus direct invocations, then manually exercised the review-fix behaviors the suites don't cover: symlink entries copied by content, missing-trailing-newline declarations kept, unreadable directories counted as failures, clone-root refusals, and both honest TERM/KILL signal-failure branches driven end-to-end through the real command line against live listeners with transcripts saved as evidence. No product failures found; worktree left clean.

Evidence: Bootstrap F1/F2 manual verification transcript (symlink content copies + missing trailing newline)

Source: Bootstrap F1/F2 manual verification transcript (symlink content copies + missing trailing newline)

Manual verification: bin/fm-worktree-bootstrap.sh review fixes F1 (symlink entries
copied by content) and F2 (.worktreeinclude final line kept without trailing newline).

Fixture: fake FM_HOME with projects/myapp clone containing:
  - .dev.vars            -> symlink to secrets/dev.vars (content SECRET=via-symlink)
  - secrets/dev.vars     (regular file, the symlink target)
  - state/v3/d1/main.sqlite        (regular file, real-db)
  - state/v3/d1/link.sqlite        -> absolute symlink OUTSIDE the clone (linked-db)
Dev material left untracked; only a README is committed so the worktree
checkout starts clean (no pre-existing destinations).

.worktreeinclude written WITHOUT a trailing newline:
  '.dev.vars\nstate\nstate/v3/d1/link.sqlite'   <- last line has no \n

Command: FM_HOME=<fake home> bin/fm-worktree-bootstrap.sh <worktree>

--- actual output ---
exit=0
[fm-worktree-bootstrap] from: /private/var/folders/.../home/projects/myapp
[fm-worktree-bootstrap] into: /private/var/folders/.../pool/myapp
[fm-worktree-bootstrap] done - 3 entries copied, 0 absent in the project clone, 0 copy failures (warnings above)
---
.dev.vars -> SECRET=via-symlink          (F1: symlink-to-file copied BY CONTENT)
main.sqlite -> real-db                   (declared directory traversed)
link.sqlite -> linked-db                 (symlink inside declared dir copied by content)
dest .dev.vars: REGULAR-FILE             (content copied, not a dangling link)

Result: PASS - exit 0, all 3 entries counted copied, no warnings,
all destinations hold the source content despite symlinks and a missing
trailing newline in the declaration.
Evidence: Kill-port F4 honest signal-failure label verification transcript (TERM and KILL failure branches)

Source: Kill-port F4 honest signal-failure label verification transcript (TERM and KILL failure branches)

Manual verification: bin/fm-kill-port.sh review fix F4 - honest outcome labels when
a TERM/KILL signal send fails. Both failure branches driven end-to-end through the
real command line against real TCP listeners; a shell function named `kill`
simulates a permission-denied send (returns 1 for one verb) while delegating all
other invocations to /bin/kill.

== Case A: TERM send fails (simulated EPERM), python3 http.server listener ==
caseA exit=1
failed: port 64993 listener pid 90031 - could not send TERM, it may still be running
listener-after=yes

Contract held: non-zero exit, "failed:" label instead of a false "stopped",
listener left running (no deletion path taken on a failed kill).
The normal paths around it are covered by tests/fm-kill-port.test.sh
(stop/killed/no-listener/refusal messages and exit codes all green).

== Case B: TERM delivered but ignored by a TERM-immune python listener,
   grace expires, KILL send fails (simulated EPERM) ==
TERM-immune listener pid=90927 port=64994 up=yes
caseB exit=1
failed: port 64994 listener pid 90927 - could not send KILL after TERM grace, it may still be running
listener-after=yes

Contract held: the script did not print "killed pid" for a process that
survived; exit 1; listener still up.

Result: PASS - both signal-send failures now report honestly with non-zero
exit while every existing message format (stopped/killed/no listener/
refusal) remains unchanged in the passing suite.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ bin/fm-worktree-bootstrap.sh:112 - A declared entry that is a symlink to a file (e.g. .dev.vars -> secrets/dev.vars, or symlinks inside a declared directory) passes the [ -e &#34;$src&#34; ] check but find &#34;$src&#34; -type f emits nothing for it, so copy_entry copies zero files yet returns 0 and the entry is counted as 'copied'. Verified locally: plain find &lt;symlink-to-file&gt; -type f -print0 outputs nothing while find -L finds the target. Result: the exact silent-provisioning-miss this tool exists to prevent, reported as success. Fix: when $src is a plain file/symlink-to-file copy it directly with cp instead of relying on find, or use find -L.
  • ⚠️ bin/fm-worktree-bootstrap.sh:115 - while IFS= read -r rel; do ... done &lt; &#34;$DECL&#34; drops the last line of .worktreeinclude when the file lacks a trailing newline (read returns non-zero at EOF even though rel is populated, so the body never runs). A project whose declaration ends without a newline gets its final entry silently ignored - not copied, not even counted as absent in the summary. Standard fix: while IFS= read -r rel || [ -n &#34;$rel&#34; ]; do.
  • ℹ️ bin/fm-worktree-bootstrap.sh:120 - The unsafe-path pattern /*|*..* refuses any path merely containing ".." as a substring, e.g. a legitimate entry named vendor/a..b/config.env would be loudly refused even though it cannot escape. Over-refusal fails safe and matches the loud-refusal contract, so this is acceptable deliberate conservatism; noting only so the author knows benign names can trip it.
  • ℹ️ bin/fm-kill-port.sh:158 - Both signal sends discard failure via || true, so if TERM/KILL is permission-denied the script still prints 'port N: stopped/killed pid P' - a wrong outcome label without erroring (exit 0 since status stays 0). Marginal in practice because lsof-visible own-listener pids are almost always killable, but checking the kill result and reporting/refusing honestly would make the label trustworthy.
  • ℹ️ bin/fm-worktree-bootstrap.sh:133 - An unsafe path midway through .worktreeinclude dies after earlier entries have already been copied, leaving a partially filled worktree plus a non-zero exit. The contract only requires loud refusal with non-zero exit, which holds; validating the whole declaration before copying anything would be cleaner but is not required.

🔧 Fix: Fix bootstrap symlink/no-newline copy gaps and honest kill-port signal labels
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-worktree-bootstrap.sh:124 - Residual silent-miss in the directory branch of copy_entry: done &lt; &lt;(find -L &#34;$src&#34; -type f -print0 2&gt;/dev/null) discards find's exit status. If find fails outright for an existing directory entry (unreadable directory such as chmod 000, broken mount, or a symlink loop that makes find skip subtrees), zero files are emitted, the loop body never runs, copy_entry returns 0, and the entry is counted as 'copied' in the summary — the same wrong-success outcome class as round-1 F1, now narrowed to directories. Verified locally: on an unreadable dir, find &lt;dir&gt; -type f -print0 2&gt;/dev/null outputs nothing and exits 1. Concrete reachable path: project declares state/v3/d1; that directory in $FM_HOME/projects/<name> becomes unreadable or contains a symlink loop -> [ -e &#34;$src&#34; ] passes -> nothing copied -> summary reports success. Fix at the shared boundary inside copy_entry: capture find's status (e.g. write its output via a temp file or check the substitution's pipeline status) and return 1 with the existing warn-don't-fail stderr warning when find errored, or at minimum warn when a declared directory entry yields zero files.
  • ℹ️ bin/fm-worktree-bootstrap.sh:132 - The unsafe-path guard refuses absolute and '..'-containing entries but not '.' (or './'), which passes the pattern check, resolves -d true, and causes find -L to emit every file in the project clone — including tracked sources — each overwritten into the worktree root via cp -p. A single-line '.worktreeinclude' typo of '.' would overwrite the whole task worktree checkout (including any worker-uncommitted tracked edits) with the clone's working-tree content, contradicting the tool's stated purpose of provisioning only untracked dev material. The declaration is project-authored so this needs deliberate misconfiguration, but a loud refusal for entries resolving to '.' would match the existing refusal contract's spirit. Author decision, not auto-fixable without changing contract scope.

🔧 Fix: Refuse clone-root declaration paths and report unreadable directory copies as failures
2 infos still open:

  • ℹ️ bin/fm-worktree-bootstrap.sh:126 - A declared directory entry that is readable but contains zero regular files makes find -L ... &gt;&#34;$list&#34; exit 0 with an empty list, so copy_entry returns 0 and the main loop increments copied even though no file was copied. Verified locally: find -L &lt;empty-dir&gt; -type f -print0 exits 0 with 0 bytes. This leaves the other half of the round-2 prescription (warn when a declared directory yields zero files) unimplemented. A [ -s &#34;$list&#34; ] check after the loop would warn and count a failure instead of a silent wrong 'copied' label.
  • ℹ️ bin/fm-worktree-bootstrap.sh:137 - When find partially succeeds then fails (one readable subdir plus one unreadable subdir), the files found before the error ARE copied by the loop, yet the subsequent warning reads "could not read '$src' of '$rel', nothing copied from it" - a false statement in the diagnostic. The return-1/count-as-failed outcome itself is correctly conservative. Rewording to drop the 'nothing copied' claim (e.g. 'could not fully read') keeps the label truthful.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-kill-port.test.sh tests/fm-worktree-bootstrap.test.sh - FM_TEST_SUMMARY total=2 failed=0, all bootstrap no-op/copy/silent-skip/overwrite-no-delete/unsafe-path-refusal/warn-don't-fail/unreadable-dir cases green
  • bash tests/fm-kill-port.test.sh directly - real python3 http.server listeners across inside/outside temp fixtures: boundary kill, outside-boundary refusal naming the directory, --any override, multi-port, one-bad-arg-doesn't-abandon
  • bash tests/fm-worktree-bootstrap.test.sh directly - synthetic git worktree + fake FM_HOME fixtures including chmod-000 declared directory warning as '1 copy failures'
  • Manual F1/F2 fixture: fake FM_HOME clone with .dev.vars -&gt; secrets/dev.vars, an absolute out-of-clone symlink inside state/v3/d1, and a no-trailing-newline declaration - exit 0, '3 entries copied', all destinations hold source content, destination is regular file not dangling link
  • Manual F2/R2 negative checks: declaration ending without newline still processed its last line; '.', './' entries hit 'refusing unsafe path' with non-zero exit (also suite-covered)
  • Manual F4 case A: injected failing TERM send via shell-function kill shim against a live http.server listener - printed 'failed: ... could not send TERM', exit 1, listener left running
  • Manual F4 case B: TERM-immune python listener surviving the grace period with injected failing KILL send - printed 'failed: ... could not send KILL after TERM grace', exit 1, listener left running
  • git diff --stat 8802737..HEAD - only bin/fm-worktree-bootstrap.sh, bin/fm-kill-port.sh, tests/fm-kill-port.test.sh, tests/fm-worktree-bootstrap.test.sh changed
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@pruge
pruge merged commit cd60d0b into main Aug 24, 2026
16 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.

1 participant