Skip to content

Vendoring the fleet into a foreign repo on Windows: 14 measured findings (onboard.md gaps, PowerShell/CRLF/WSL traps, script defects, and a scope-discriminator gap) #502

Description

@cmartins88

Installed the fleet into a foreign repository (a Python service, ~7.3k statements, 233 tests, deployed to three Kubernetes clusters) on Windows 11 / Git Bash / Daintree 0.29.0, following onboard.md end to end. It works now — 9 agent panes, 8 isolated worktrees, preflight at 14 ok / 6 warn / 1 fail (the remaining FAIL is the deliberate check-tools-index one).

Everything below is measured on that install, not read. Grouped by where a reader would hit it. Happy to split into separate issues, or to send PRs for the mechanical ones, if that's more useful.


A. onboard.md — a reader following it exactly gets a fleet that cannot start

A1. Step 2's copy table omits .daintree/bootstrap.sh. The table lists seven copies; there are eight. Every one of the 9 agent panes' initialPrompt opens with bash .daintree/bootstrap.sh. Copy only what the table names and all nine panes fail their first instruction.

A2. The origin/main precondition is never stated. bootstrap.sh resolves the role prompt with git rev-parse "origin/main:${NFORMA_ROLE_PROMPT}" and prints ⛔ ... does not resolve at origin/main — do NOT fall back. So the artifacts must be committed and pushed before launch. Step 5 only says "commit or stash the working tree", which a reader satisfies with a local commit. On a protected main this means the whole install is gated behind a PR merge — worth saying out loud, because it changes the install from "copy files" to "land a PR".

A3. Step 1's Daintree detection is POSIX-only and silently false on Windows. It tests ~/.daintree/cli.sock. On Windows Daintree uses a named pipe — the path is in ~/.daintree/cli-control.json as socketPath: \\.\pipe\daintree-cli-<uuid>. The check returns false with Daintree running and healthy. Given this repo's own doctrine, that is a check that establishes nothing while reading as a negative.

A4. Step 8 says the reference recipe ships NFORMA_GOAL on "1 of 9". Measured on current main: 9 of 9. Minor, but step 8 asks the reader to report that number, so the doc trains them to report a stale one.


B. Windows: the recipe cannot start a pane as shipped

B1. Daintree hardcodes PowerShell on Windows, and the initialPrompt is bash-shaped. From app.asar:

function N(){ for(e of ["pwsh.exe","powershell.exe"]) try{ where(e); return e }catch{}
              return process.env.COMSPEC||"cmd.exe" }
function H(){ if(process.platform==="win32") return N();
              if(process.env.SHELL) return process.env.SHELL; ... }

$SHELL is only consulted on the non-win32 branch, and there is no per-pane or config override (shellPath, shellOverride, windowsShell: 0 hits). Every pane is PowerShell. The prompts carry 4 $ and 8 backticks each; $NFORMA_GOAL: is drive-qualified variable syntax and backtick is PowerShell's escape character, so every agent pane dies with Variable reference is not valid before the agent ever starts.

Worth noting the obvious fix is wrong too: cat "$NFORMA_GOAL" breaks identically. What worked was naming the value bootstrap.sh has already printed in step 1, which removes every character PowerShell parses while keeping the env-var indirection the design depends on.

B2. The PREFLIGHT pane measures a different machine than the agents run on. Its command is bash scripts/fleet-preflight.sh. From PowerShell, bash resolves to C:\Windows\system32\bash.exe — the WSL launcher — not Git Bash. Same tree, same moment:

invocation result
bash (→ WSL) 9 ok, 7 warn, 11 fail, toplevel=/mnt/c/...
Git Bash, explicit 14 ok, 5 warn, 9 fail, toplevel=C:/...

The WSL run reports FAIL gh not on PATH and FAIL claude not on PATH (WSL genuinely has no gh; claude sits at ~/.local/bin, absent from a non-login PATH), and reads correctly-placed worktrees as OUTSIDE the convention because WSL sees /mnt/c/... where they registered as C:/.... All three are artifacts of the wrong shell, and the acceptance test presents them as fleet failures.

B3. CRLF stops fleet-preflight.sh from parsing at all. scripts/fleet-preflight.sh: line 10: $'\r': command not found. The committed blobs are LF, but with core.autocrlf=true (the Git-for-Windows default) the working tree is CRLF, and bash executes the working tree. The estate's own clone is CRLF for the same reason, so a plain cp during step 2 carries CR into the target. A .gitattributes pinning the executable artifacts to text eol=lf fixes it; without one, the next checkout re-breaks it.

B4. The checkers crash exactly when they find something. check-goal-conformance.py, check-tools-index.py and friends print / only on their FAIL branches. Windows Python defaults stdout to cp1252, so those branches raise UnicodeEncodeError: 'charmap' codec can't encode character '\u26d4'. The scripts run clean when all is well and die when they detect a defect — and a crashed checker reports nothing at all. One sys.stdout.reconfigure(encoding="utf-8") (or PYTHONIOENCODING) at the top of each fixes it.


C. scripts/ — defects surfaced by an independent reviewer, verified here

These were raised by GitHub Copilot's review on the vendoring PR. I confirmed each and left them unfixed rather than forking your instruments.

C1. validate-recipe.pyjson.load accepts any JSON value, so a top-level list or string reaches recipe.get and raises AttributeError. For a validator whose entire purpose is refusing malformed input, crashing rather than reporting is the wrong failure mode.

C2. fleet-worktree.sh:30 — the worktree parser takes awk field 2, so any worktree path containing a space is truncated. Not hypothetical on Windows, where C:\Program Files\... is a normal location; the failure is silent.

C3. fleet-worktree.sh:177NODOCTRINE is counted and the operator is told to run create <ref>, but the branch does not handle that state.

C4. fleet-preflight.sh:236 and prompts/README.md:314 both state the preflight's exit code is always 0 and that the pane "never gates". It exits 1 whenever fail > 0 — which it did on this install. Two documents and the implementation disagree.

C5. check-orientation.py:32TOKEN matches any backticked token containing /, over-matching against a contract that says these are repo-relative paths. :201 — when tools/README.md is missing or unreadable it prints that the pin doctrine is UNCHECKED but returns a value that reads as a pass. That second one is this repository's own silence-as-success failure, inside the instrument built to catch it.

C6. tools/discriminates.py:68subprocess.run(cmd, shell=True) trips security review as a High finding. Not a problem inside this estate, but it blocks vendoring the tool into a repo with a security gate.


D. The vendoring path defeats the scope discriminator

This is the one I'd most like your read on, because it is a doctrine question rather than a bug.

Step 3 says to re-scope each goal's **Repository:** line. Doing exactly that makes check-goal-conformance.py report FOR-THIS-REPO for all five role goals — while their bodies still describe nForma-NEXT. In my target repo the goals asserted:

  • "This repository has no test infrastructure at all — no pytest.ini, no pyproject.toml, no conftest.py ... no test files of any kind" → actual: pytest, 233 test files, pyproject.toml, conftest.py, a required coverage gate.
  • "No cloud provider, no Kubernetes, no deployment system ... no runtime to observe" → actual: 19 k8s manifests, a deploy script, three production clusters with Prometheus/Loki/Grafana live.

A DEVOPS agent would have been told, by a file the checker had just certified as in-scope, that its repo has no Kubernetes. Those paragraphs carry [measured: nForma-NEXT 2026-08-19] — they are honest about their provenance, and nothing reads that tag.

This is #16's failure mode one level up: the discriminator separates declared scope from mentioned scope, but not declared scope from measured scope. A file can now pass while every factual claim in it was measured somewhere else.

Two things that would have caught it, if either appeals:

  1. Have check-goal-conformance.py also scan for [measured: <slug>] stamps and FAIL (or at least WARN) when a stamp names a repository other than the declaring one. The provenance is already machine-readable; nothing currently reads it.
  2. Have onboard.md step 3 say that re-scoping the declaration line is necessary and not sufficient — the body has to be re-measured too, or the file deleted. Step 3 currently reads as though the line is the whole job, and the checker going green confirms that reading.

Related: goals/dx-friction-sweep.md carries no **Repository:** declaration at all, so it reads NO-DECLARATION/MENTION-ONLY wherever it is resolved.


What the install ended up needing

For anyone hitting this: 8 copies not 7; commit and push to origin/main before launch; strip $, backtick and " from every initialPrompt; pin the preflight pane to Git Bash explicitly; .gitattributes text eol=lf on the executable artifacts; and re-measure the goal bodies, not just their declaration lines.

Thanks for the estate — the doctrine in the prompts and goals transferred well, and fleet-worktree.sh's "report before create" and the MOVE-don't-create distinction both did real work here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions