fleetd #362 (item 3): seed .claude/skills/ into provisioned worktrees #366
Closed
agent
wants to merge 0 commits from
worker/362-worktree-skills-c03e51-3 into main
pull from: worker/362-worktree-skills-c03e51-3
merge into: fleet:main
fleet:main
fleet:worker/fleetd-612-unita-87807e-1
fleet:worker/612-b3-mcpwirings-da2b58-3
fleet:worker/612-b2-cb185-176d3a-2
fleet:worker/612-b1-completion-457459-1
fleet:worker/612-agaps-73a926-2
fleet:worker/608-sleeps-3a64ff-3
fleet:worker/621-b4520b-1
fleet:worker/618-b83894-2
fleet:worker/fleetd-615-e05481-5
fleet:worker/lead-autocompact-5f1ab2-3
fleet:worker/fleetd-613-f85deb-3
fleet:worker/fleetd-608-flaky-nudge-test-d0c2d1-3
fleet:worker/lead-context-gauge-ad404f-1
fleet:worker/gauge-wiring-9158c1-4
fleet:worker/redeploy-slowstart-ead0e5-5
fleet:worker/charter-bytes-13668c-6
fleet:worker/rollover-outcome-291483-2
fleet:worker/589-f64303-2
fleet:worker/593-1a8025-5
fleet:worker/589-fcd2aa-1
fleet:worker/568-9fdaa2-3
fleet:worker/571-attempted-outcome-5739f7-2
fleet:worker/581-completionresolver-cas-sites-0542b7-6
fleet:worker/562-loop-health-wiring-test-99611c-5
fleet:worker/562-surface-loop-health-7df5cc-4
fleet:worker/575-waiter-cleanup-sites-62ad80-1
fleet:worker/572-answer-lock-release-46a9ae-5
fleet:worker/567-probe-channel-leak-a38fc5-6
fleet:worker/551-record-before-send-7cbf56-1
fleet:worker/561-listener-fanout-survives-a-throw-61d538-2
fleet:worker/555-redeploy-main-flow-seam-65c2f5-2
fleet:worker/556-injector-owns-registration-e027a5-1
fleet:worker/552-post-restart-mktemp-abort-bc2672-4
fleet:worker/553-onstatus-completion-leak-0da881-2
fleet:worker/550-shasum-linux-196132-1
fleet:worker/538-loop-dies-on-error-4a5eeb-6
fleet:worker/426-health-coverage-ef1fd4-4
fleet:worker/504-failed-reported-clean-3cfd66-3
fleet:worker/537-capturedlog-close-e4c437-2
fleet:worker/459-broken-link-targets-cadc17-5
fleet:worker/535-appender-leak-fe74c1-1
fleet:worker/512-part2-shutdown-detection-434701-9
fleet:worker/529-logger-level-sweep-2a5533-8
fleet:worker/528-drain-gate-call-site-5de83d-7
fleet:charter/forge-mcp-vs-token
fleet:worker/521-swap-guard-unpinned-28e931-5
fleet:worker/519-probe-test-harness-d25ab8-4
fleet:worker/525-logger-level-leak-1b4eb0-6
fleet:worker/518-fleetmcp-resolver-wiring-8ef96c-1
fleet:worker/512-drain-complete-line-7edd71-3
fleet:worker/517-abort-branch-and-jar-id-41b641-2
fleet:worker/500-9e52c9-3
fleet:worker/509-4912f4-2
fleet:worker/511-9a4b23-1
fleet:worker/493-479f45-2
fleet:worker/505-03f8b2-1
fleet:worker/492-followup-detect-unclear
fleet:worker/501-a31fa0-7
fleet:worker/498-451d1c-5
fleet:worker/494-1015ce-2
fleet:worker/492-209647-1
fleet:worker/489-001902-2
fleet:worker/480-relative-handover-path-906323-1
fleet:worker/480-b-handover-skill-45bf1f-5
fleet:worker/474-followup-source-pin-f54a55-17
fleet:worker/474-charter-check-on-reload-f54a55-17
fleet:worker/466-quarantine-repeatcount-report
fleet:worker/393-opencode-skill-seeding-71854b-13
fleet:worker/469-canonical-tool-names-2a472a-16
fleet:worker/466-quarantine-escalation-5ae9c1-15
fleet:worker/446-hot-exhausted-pattern-0af580-6
fleet:worker/464-charter-tool-name-guard-a85635-12
fleet:worker/463-listfleet-default-fails-open-f1c76c-11
fleet:worker/458-invariant-5-by-purpose-862f9a-10
fleet:worker/439-coordinator-row-gate-bc032a-8
fleet:worker/449-herdr-protocol-576015-4
fleet:worker/450-abstract-spawn-599e1c-5
fleet:worker/437-ack-refuses-177d91-1
fleet:worker/444-placement-window-feb56a-2
fleet:worker/440-helddurable-derived-d462d7-13
fleet:worker/425-rework-placement-resolve-c58ba1-9
fleet:worker/421-lead-peek-held-msgs-cdbad2-10
fleet:worker/435-fixed-policy-cap-fe11de-12
fleet:worker/422-gate-state-observability-9e79d6-11
fleet:worker/431-memberregistry-live-readers-cdbad2-10
fleet:worker/424-architect-slot-hot-038b41-7
fleet:worker/422-model-gate-spawn-c29f48-6
fleet:worker/425-default-profile-live-f55534-8
fleet:worker/415-coverage-wording-2cbf9c-5
fleet:worker/416-3ad1da-1
fleet:worker/418-588283-3
fleet:worker/deterministic-stamp-race-409-3cb7b6-10
fleet:worker/armed-reads-live-config-404-ed931f-9
fleet:worker/reply-peer-refusal-391-5a34bd-7
fleet:worker/models-allowlist-aa9e9b-3
fleet:worker/ttl-stamp-race-399-f1122f-8
fleet:worker/scrub-receipt-400-316b3e-5
fleet:worker/exhaustion-detection-395-105105-6
fleet:worker/scrub-abort-394-316b3e-5
fleet:fix/scrub-uid-abort
fleet:worker/task-scrub-517574-2
fleet:worker/t386-clock-bd5b78-4
fleet:worker/t384-scrub-813790-5
fleet:worker/t381-cc-748314-2
fleet:worker/t373-336973-2
fleet:worker/t365-3920c5-3
fleet:worker/t358-6e989b-1
fleet:worker/t355-8b321c-1
fleet:worker/fleetd-369-hermetic-git-tests-e8b19a-3
fleet:worker/fleetd-368-stale-lead-binding-f5682e-2
fleet:worker/fleetd-360-deploy-units-0d3793-1
fleet:worker/359-dead-lead-tabs-f1253b-4
fleet:worker/361-coord-visibility-655144-1
fleet:362-plugin-visibility-and-drift
fleet:worker/errscan-bed2ca-2
fleet:worker/amqp-log-identity-bed2ca-2
fleet:worker/withdefaults-guard-561704
fleet:worker/sleepguard-82076d-1
fleet:worker/fd334-9ee1b6-5
fleet:worker/fd348-f1ab27-4
fleet:worker/fd335-a71c35-1
fleet:worker/fd342-174a17-2
fleet:worker/fd345-490d0f-3
fleet:worker/fleetd-337-5ec7d4-21
fleet:worker/fleetd-341-af5a6b-24
fleet:worker/fleetd-339-5ca0a2-23
fleet:worker/fleetd-338-83a4a1-22
fleet:worker/fleetd-333-281f46-18
fleet:worker/fleetd-329-11bdbb-16
fleet:worker/fleetd-330-2770fb-17
fleet:worker/fix-326-50506e-15
fleet:worker/fix-324-3e9bbf-14
fleet:worker/fix-323-b8287d-13
fleet:worker/fix-316b-bd0860-11
fleet:worker/fix-318-76ca36-9
fleet:worker/fix-317-486aec-8
fleet:worker/fix-315-ce47c5-6
fleet:worker/fix-307-275890-6
fleet:worker/fix-308-b4f664-7
fleet:worker/fix-309-ec3939-8
fleet:worker/fix-310-7a3974-9
fleet:worker/fix-302-52ad0e-9
fleet:worker/fix-298-ce1acb-8
fleet:worker/fix-297-66bd11-7
fleet:worker/fix-296-104622-6
fleet:worker/fix-293-bare-closetab-eb22b5-3
fleet:worker/fix-280-gone-ask-lapse-bca98e-2
fleet:worker/fix-290-reapidle-guard-coverage-9b0dd1-1
fleet:worker/fix-285-trust-seed-8f3565-10
fleet:worker/fix-284-backend-error-seat-85912c-11
fleet:worker/fix-282-chained-ask-e6d0bb-8
fleet:worker/fix-283-teardown-leaks-f40dfa-9
fleet:worker/fix-281-pin-handler-actions-4921ac-7
fleet:worker/audit-rendezvous-lifecycle-d072ae-2
fleet:worker/audit-health-placement-1a2476-6
fleet:worker/audit-teardown-exits-e207a5-3
fleet:worker/audit-launcher-asymmetry-27e370-4
fleet:worker/audit-rest-authz-6ca53c-5
fleet:worker/investigate-275-abandon-asking-fdef52-8
fleet:worker/fix-274-worktree-leak-b0095d-7
fleet:worker/fix-273-exhausted-pattern-9665b5-6
fleet:worker/fleetd-267-model-check-bd8068-1
fleet:worker/fleetd-131-archunit-18b834-7
fleet:worker/fleetd-266-sshagent-rename-a014ff-6
fleet:worker/fleetd-184-uid-claim-8e1f31-4
fleet:worker/fleetd-184-warn-b381ee-10
fleet:worker/fleetd-184-docs-be1d12-9
fleet:worker/fleetd-257-9bf010-7
fleet:worker/fleetd-103-23a113-6
fleet:worker/fleetd-247-342356-5
fleet:worker/fleetd-116-04dea8-4
fleet:worker/fleetd-252-a830e0-3
fleet:worker/fleetd-111-7e8673-9
fleet:worker/fleetd-155c-f8ef4b-8
fleet:worker/fleetd-176-b928ca-3
fleet:worker/fleetd-249-7a7878-2
fleet:worker/cb248-composition-root-b-9acdf7-15
fleet:worker/cb148-envrc-default-fa6c82-12
fleet:worker/cb201-unit5-wiring-6c12e6-8
fleet:worker/cb241-fallback-echo-1175e9-11
fleet:worker/cb149-trust-dialog-2392a5-9
fleet:worker/cb134-148-overlay-visible-c9b986-10
fleet:worker/cb234-session-id-keyed-04e1fc-1
fleet:worker/cb201-unit3-nudge-abdf5c-6
fleet:worker/cb201-unit2-policy-c1102c-5
fleet:worker/cb201-unit4-outcome-a13bfa-7
fleet:worker/cb201-unit1-classifier-91b9b1-4
fleet:worker/cb201-227-refine-831980-3
fleet:worker/cb175-model-readback-0f085f-1
fleet:worker/cb222-charter-tmpdir-17f013-1
fleet:worker/cb226-architect-slot-race-cd3aa8-3
fleet:worker/cb224-worktree-root-group-024523-2
fleet:worker/cb-123-role-demotion-c600f7-2
fleet:worker/cb-219-opencode-roots-1f677e-1
fleet:worker/cb214-claude-session-id-b9eab4-4
fleet:worker/cb213-zdotdir-wrong-process-dd6de4-3
fleet:worker/cb211-exhaustion-classification-9546e0-2
fleet:worker/cb137-ambiguous-task-4df3d8-4
fleet:worker/cb209-agentsessionid-4dfdb6-2
fleet:worker/cb185-hostenvnames-2692b5-3
fleet:worker/cb206-opencode-sqlite-128718-2
fleet:worker/cb185-worktree-group-fc0c99-1
fleet:worker/cb-137-ask-ticket-e7760c-2
fleet:worker/cb-172-broker-uri-d36ae4-4
fleet:worker/cb-175-model-readback-76ead6-3
fleet:worker/cb-161-pane-ancestry-293510-1
fleet:worker/cb-164-rebase-885863-8
fleet:worker/cb-164-empty-scrape-false-success-1a80af-3
fleet:fix/cb-197-ticket-ttl-from-completion
fleet:worker/cb-189-remote-url-coverage-4692f3-1
fleet:worker/cb-185-blockers-027756-4
fleet:worker/cb-192-gap-log-11b631-2
fleet:worker/cb-633-fix-5f4396-3
fleet:worker/cb185-router-d6436d-3
fleet:worker/cb185-router-routing-gaps-9e9d33-3
fleet:worker/cb185-paneids-992586-2
fleet:worker/cb-633-allow-list-union-ed374b-1
fleet:worker/cb-157-credential-in-remote-url-496e44-2
fleet:worker/cb-641-health-herdr-evidence-8f1f54-6
fleet:worker/cb-640-health-msg-evidence-99c9cd-1
fleet:worker/cb-642-fleets-status-skill-bbbc40-5
fleet:cb-634-ide-mcp
fleet:worker/lead-comms-wiring-c014b9-7
fleet:worker/lead-mailbox-c19577-6
fleet:worker/autocompact-window-82bc2f-5
fleet:worker/cb-634-probe-18056f-4
fleet:worker/cb635-broker-urienv
fleet:worker/cb-632-config-retry-8e0efa-7
fleet:lead/cb-622e-claude-md
fleet:lead/cb-622-followup
fleet:worker/cb-622a-165dff-1
fleet:lead/cb-622d-opencode-mount
fleet:worker/cb-622b-717c67-2
fleet:worker/cb-622c-ab7759-3
fleet:worker/cb-617b2-20ca4b-3
fleet:worker/cb-617a-5c2f4a-1
fleet:worker/cb596-4e49ef-3
fleet:worker/cb586-10500c-1
fleet:worker/cb-606-b9343a-25
fleet:worker/cb604-1445f8-24
fleet:worker/cb582-477374-21
fleet:worker/cb584-8c2281-22
fleet:worker/cb600-e6b9a9-20
fleet:worker/cb602-ce257f-19
fleet:worker/cb601-b42837-18
fleet:worker/cb598-6c7ba7-17
fleet:worker/cb599-740fe4-16
fleet:worker/cb597-282224-15
fleet:worker/cb590fix-185e9a-10
fleet:worker/cb528-recovery-race
fleet:worker/cb594-96bead-8
fleet:worker/cb590-916766-2
fleet:worker/cb527-997d99-3
fleet:worker/cb592-env-leak-3cbf9c-1
fleet:worker/cb588-async-ticket-nudge-3218f7-5
fleet:worker/cb578b-9dcb13-6
fleet:worker/cb581-d24826-5
fleet:worker/m2-u5-ef8c42-15
fleet:worker/cb578a-516499-2
fleet:worker/cb576-01a04b-17
fleet:worker/cb579-lead-tab-acba06-20
fleet:worker/cb580-terminal-health-ed6058-21
fleet:worker/cb577-f36fdc-18
fleet:worker/cb573b-3db06f-16
fleet:worker/cb568c-f36fdc-18
fleet:worker/cb568-drop-cause-c3ac1c
fleet:worker/cb575-cancelled-notification-c3ac1c
fleet:worker/m4-sol-a2cbec-3
fleet:worker/cb574-async-ask-c3ac1c
fleet:worker/cb573-health-model-8ca857-14
fleet:worker/cb572-unknown-target-7f2e35-13
fleet:worker/u4-700706-9
fleet:worker/u3-b9fcb6-6
fleet:worker/u2-ef5b68-4
fleet:worker/u1-469dce-1-clean
fleet:worker/u1-469dce-1
fleet:worker/cb-564-health-events-70cf7e-2
fleet:worker/cb-565-recycle-drops-role-98e58f-3
fleet:worker/cb-563-missing-reply-df2866-1
fleet:worker/cb-562-readiness-gate-silent-6c23c9-3
fleet:worker/cb-560-architect-presence-da8155-1
fleet:worker/cb-561-architect-silent-off-a71cab-2
fleet:worker/cb-548-bind-architect-slot-fe1b8c-1
fleet:worker/parity-overlay-settings-5fb711-1
fleet:secrets-central-store
fleet:cb-559-hot-key-correction
fleet:cb-557-fleet-role-pools
fleet:worker/cb-553-maxload-explicit-spawn-305ee3-6
fleet:worker/cb-551-idle-lead-heartbeat-f1633c-1
fleet:worker/cb-544-drain-preserves-worktree-925fad-3
fleet:worker/cb-552-docs-sync-1cb9cf-4
fleet:worker/cb-548-rendezvous-guard-rebased
fleet:worker/cb-548-rendezvous-guard-116b53-10
fleet:worker/cb-548-authz-v2-586df6-8
fleet:worker/cb-548-authz-264363-5
fleet:salvage/cb-528b-codex-home
fleet:salvage/cb-528a-codex-launcher
fleet:CB-518-primary-flow
fleet:feature/peer-launcher-spi
fleet:cb-103-injector
No Reviewers
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#366
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "worker/362-worktree-skills-c03e51-3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Review fix round 2 (lead-caught, two findings on the same root cause): the XDG fallback
The lead re-verified round 1's fix live (merged
origin/main— which had picked up theconcurrent #363/items-1-2 PR — into this branch, clean merge,
mvn clean install→Tests run: 1386, BUILD SUCCESS, matching my number exactly) and confirmed the--absolute-git-dirplacement and the composition itself are correct. Two findings came back,both about the XDG fallback branch I added in round 1 (the one that fires when
core.excludesFileis unset entirely, not just the "already configured" case round 1's testcovers).
Finding 1 — the fallback was unpinned. The lead ran a mutation I hadn't: delete the fallback
so an unset key composes with
"". Green both ways (Tests run: 1386before and after). My round1 test only ever exercised the "already set" branch. Fixed by adding
seedSkillsComposesWithTheXdgDefaultExcludesFileWhenNoneIsConfigured— isolatesXDG_CONFIG_HOMEvia thegitEnvseam at a temp dir carrying a syntheticgit/ignore, pointsGIT_CONFIG_GLOBALat an empty file (socore.excludesFileis genuinely unset, forcing thefallback branch rather than the "already configured" one), seeds a skill, and asserts a file
matching the XDG-default pattern is still invisible to
git status. I re-ran the lead's exactmutation (this time against the new test) and captured the real red output before reverting:
Finding 2 — the actual code bug: the fallback bypassed the
gitEnvseam entirely. It readXDG_CONFIG_HOME/HOMEstraight from the JVM's own environment (System.getenv/user.home)instead of through
git, so no test could isolate it — and on any machine carrying a real~/.config/git/ignore(this dev machine measurably does:**/.claude/settings.local.json),every seeding test in the suite was silently composing with that real file, machine-dependently.
Fixed: added
resolveEnv(String)/resolveHome(), which check thegitEnvseam first andfall back to the real JVM environment only when the seam doesn't supply a value — production
behaviour (
gitEnvis alwaysMap.of()there) is byte-identical to before. Also added ahermeticGitEnv(Path)test helper and routed every skill-seeding test inGitWorktreesTestthrough it, so nothing in the class can reach the real machine's home directory for this
fallback anymore.
Two non-defect javadoc notes added, per the lead's request:
previouslyEffectiveExcludesFileContentcopies a snapshot at seed time, not a livereference — a later edit to the operator's own excludesFile does not propagate into an
already-seeded worktree.
excludeSeededSkillsFromGitStatusassumes a fresh worktree and is not idempotent if evercalled twice on the same one — not reachable today (only
add()callsseedSkills, andadd()always creates a fresh worktree), so no guard was added for a path nothing takes; documented so
a future second caller is warned instead.
Build after round 2, unpiped:
Tests run: 1387, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS. (GitWorktreesTest: 59 tests, all green.)wiki/11-Features.md entry (per CLAUDE.md's "the prompt is part of the product" rule — I
cannot commit to
wiki/myself, this is text for the lead to add):Review fix (lead-caught bug): compose, don't replace — added after first review
The lead found a serious bug in the mechanism below:
core.excludesFileis single-valued, sosetting it
--worktree(as the first cut of this PR did, via--replace-all) replaces —does not add to — whatever excludesFile the worktree was already resolving, most commonly an
operator's own global config. Concretely: this repo's own
.gitignoredoes not ignoretarget/— only an operator's global excludesFile does — so every worker's
mvn clean installwould maketarget/show up as untracked, andGitWorktrees#hasUncommitted's deliberatelyuntracked-inclusive
git status --porcelain(CB-576) would then read every worktree thatbuilds as dirty forever, so
SessionManagernever releases it. The lead proved this live beforeI fixed it (isolated global excludesFile ignoring
target; before seedinggit status --porcelainwas empty; after seeding it showed?? target/).Fix:
excludeSeededSkillsFromGitStatusnow reads whatevercore.excludesFileresolves tobefore writing anything — via
git config --get --type=path core.excludesFile(so~expansion happens exactly the way git itself would apply it), falling back to git's own documented
default (
$XDG_CONFIG_HOME/git/ignore, or$HOME/.config/git/ignore) when the key is unsetentirely, per
gitignore(5). That content is written into fleetd's own exclude file ahead ofthe seeded skill patterns, and only then does the worktree-scoped override point at the combined
file. Every operator-configured pattern keeps applying inside the seeded worktree, plus the seeded
skill paths.
New test,
seedSkillsComposesWithAnAlreadyEffectiveGlobalExcludesFile— the third invariant-2direction, alongside the two that already existed (seeded worktree stays clean; a sibling
worktree's own untracked files don't get hidden by the leaking exclude). It isolates a synthetic
"operator's global git config" via a new
gitEnvtest seam onGitWorktrees(GIT_CONFIG_GLOBALpointed at a throwaway temp file — never the real machine's config), drives the real
add()pathend to end, then writes a
targetfile into the seeded worktree and assertsgit status --porcelainis still empty. Reproduced the same shape with plaingitcommands outside the testsuite for a concrete before/after:
Also documented in
FleetConfig's javadoc andfleetd.example.yaml, per the lead's request: thememberSkillsdirectory's non-hidden subdirectories are copied wholesale, with no per-fileallowlist — don't park scratch files there.
This is a NEW commit on top of the original one; everything below this section describes the
original PR as first submitted. The one place it is now stale is the "Caveat" paragraph inside
"Invariant 2" below, which is corrected in place rather than deleted, so the review history stays
readable.
Scope
fleetd #362, scope item 3 only: "Seed
.claude/skills/into provisioned worktrees." Items 1and 2 of that issue (the plugin fixes,
plugin/,README.md,CLAUDE.md) are out of scope forthis PR and were not touched — verified with
git diff --statbefore committing.What changed
FleetConfig: new optional top-level keymemberSkills: <dir>— a directory of skillfolders (each holding a
SKILL.md, the same shape as this repo's own.claude/skills/).null/blank = off (today's behaviour, unchanged).GitWorktrees#add: after the existingisolateToolSurface(wt)step, calls a newseedSkills(wt). For each subdirectory of the configured source, if<worktree>/.claude/skills/<name>does not already exist (i.e. the target repo doesn't commitits own copy), it is copied in recursively. A name that already exists is left completely
untouched — never opened, never overwritten (invariant 1).
git status --porcelain, not byreasoning — see "Invariant 2" below.
Fleetd.java: passescfg.memberSkills()into theGitWorktreesconstructor at the oneconstruction site (
Fleetd.java:251).ConfigRef:memberSkillsis triaged as aDEFERREDkey — baked once into theGitWorktreesbuilt at startup and never rebuilt, exactly likeworktreeGroupandworktreeRoot. Added achangedDeferredKeysbranch and updated the coverage-test value maps(
ConfigRefTopLevelReportingCoverageTest,FleetConfigWithDefaultsPreservesEveryComponentTest)so the new key is proven, not just claimed, to have real reporting behind it — this repo has a
purpose-built test (
ConfigRefTopLevelCoverageTest) that fails the build if a newFleetConfigcomponent is left untriaged.
fleetd.example.yaml: documentedmemberSkills:(commented out), matching the doc-coveragetest
FleetConfigTest.everyKnownTopLevelKeyIsDocumentedInTheExample.Where the code goes
Right beside the existing worktree-provisioning steps in
GitWorktrees.java—isolateToolSurface(
.mcp.json/opencode.json/.autoenvneutralization) andoverlayParity— in the samelifecycle (
add()), not in a new place, per the brief.Copy vs symlink — copy, and why
A symlink into the fleetd daemon's own directory would dangle the moment that checkout moves, is
archived, or the daemon runs from a different jar/checkout than the one that provisioned a given
worktree (a real risk once a worktree can outlive a daemon restart or redeploy). A copy is
self-contained and survives all of that; the tradeoff (drift if the source skill changes after
seeding) is the same one
overlayParityalready accepts for its own copies, and is far lessharmful than a dangling link that silently makes
Load the <skill> skill.fail.Invariant 2 — proof, not reasoning
The ticket suggested the shared
.git/info/exclude(the same mechanismfleet.neutralizedConfig/ClaudeCodeLauncher#writeIdeOverlayuse forCLAUDE.local.md). Ichecked this before using it, with a real throwaway repo:
.git/info/excludeis shared across every linked worktree and the primary checkout — notper-worktree. Using it here would make a seeded skill's path invisible to
git statusin theprimary's own checkout and every sibling worktree too, not just the one it was seeded
into. That's a bigger footprint than the ticket's own invariant 2 asks for ("it is per-worktree
and local").
Instead I set
core.excludesFilescoped--worktree(the sameextensions.worktreeConfigmechanism this file already uses for the credential helper and the SSH→HTTPS rewrite) to a file
written under the worktree's own private git dir (
git rev-parse --absolute-git-dir, e.g..git/worktrees/<nonce>/fleet-seeded-skills-exclude) — outside the working tree, so the excludefile itself can never be committed either, and scoped so it affects only that one worktree.
Verified with a real git command in a throwaway repo before writing any code:
GitWorktreesTestnow proves both directions with real git commands (not the recording fake):seedSkillsHidesSeededPathsFromGitStatusproves the seeded worktree stays clean, andseedSkillsExcludeDoesNotLeakIntoASiblingWorktreeproves a second worktree of the same repo(provisioned with
memberSkillsunset) still reports an untracked.claude/skills/the ordinaryway — proving the exclude did not leak in via the shared common dir.
Recorded for the worker itself, mirroring fleetd #134's
fleet.neutralizedConfig/fleet.neutralizedConfigNote:fleet.seededSkills(one value per seeded skill) andfleet.seededSkillsNote, readable withgit config --worktree --get-all fleet.seededSkills.Caveat: this sets— FIXED, see "Review fix" at the top. This was wrong: it does notcore.excludesFileat worktree scope unconditionally when at least oneskill is seeded, which would override an operator-configured global
excludesFile's effectwithin that one worktree
merely affect that one worktree's exclude behavior as a stylistic choice, it actively discards
whatever the operator's own excludesFile was already doing there, with a concrete failure mode
(worker
mvnbuild artifacts liketarget/reading as untracked forever, soSessionManager/CB-576 never releases the worktree).excludeSeededSkillsFromGitStatusnow readsand carries forward whatever was previously effective before pointing the worktree-scoped key at
its own composed file — see the top of this PR body for the fix and its proof.
Invariant 3 — best-effort
seedSkillsnever throws out ofadd(): a missing/unreadablememberSkillssource, or acopy/exclude failure, is caught, logged with
log.warn, and the spawn proceeds — same contract asoverlayParity/isolateToolSurface. Covered byseedSkillsIsBestEffortWhenSourceDoesNotExist.Invariant 4 — teardown
No new cleanup code was needed.
GitWorktrees#removealready runsgit worktree remove --force,which deletes the whole working tree (the seeded
.claude/skills/<name>files included) AND theworktree's private git dir (
.git/worktrees/<nonce>/, which is where the exclude file and thefleet.seededSkills*config live) in one step. Nothing this PR adds lives outside that boundary,so there is nothing extra to clean up — I read the teardown path to confirm this rather than
assuming it.
Scope limit — Claude Code only
seedSkillsitself is backend-agnostic — it runs insideGitWorktrees#add, which is shared byevery launcher, the same way
isolateToolSurfaceunconditionally neutralizes BOTH.mcp.json(Claude) and
opencode.json(opencode) regardless of which backend ultimately spawns into a givenworktree, because the backend isn't chosen yet at
add()time. What makes this "Claude Codemembers only" is that
.claude/skills/<name>/SKILL.mdis a path only the Claude Code launcherever reads (
ClaudeCodeLauncher/HerdrPeerLauncher#agentDefinitionFilefor.claude/agents, andthe skill-loading convention on top of it) — a seeded
.claude/skills/in an opencode member'sworktree is just an inert, unused directory.
What opencode would still need (not implemented, per the brief): opencode's skill-equivalent
lives under
.opencode/agent, a different file shape (opencode agent-definition format, notSKILL.md), so this would need either a source-format adapter (fleetd translates each skill intoan opencode agent file) or the source directory itself carrying both shapes side by side. Out of
scope here.
FleetConfig.java— exact lines touched (per the brief: another worker is editing this file ona different ticket right now)
All edits are additive and localized to the
memberSkills-specific spots, matching the existingpattern used for
worktreeGroup/memberLoginShell:@param memberSkillsblock, appended after the existing@param memberLoginShellblock.String memberSkillsappended as the last component (aftermemberLoginShell).memberSkillskey was added"(22→23-arg forwarding), placed immediately above the existing "before
memberLoginShell"back-compat constructor — no other constructor in the chain was touched.
KNOWN_TOP_LEVEL_KEYS: appended"memberSkills".withDefaults(): one new comment +memberSkillspassed through unchanged (left-as-is, nodefault — same treatment as
worktreeGroup/memberLoginShell) in the finalreturn new FleetConfig(...)call.No existing line was modified in place except the two call-sites above that had to grow one more
trailing argument (the back-compat constructor's delegation, and
withDefaults()'s ownconstructor call) — both are pure append-at-the-end edits, chosen specifically to minimize merge
risk against the concurrent
FleetConfig.javaedit on the other ticket.Build
Ran unpiped inside the worktree:
Real result (after the review fix, current HEAD of this branch):
Tests run: 1386, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS. (The originalsubmission's run, before the fix, was
Tests run: 1385.)This run includes 7 new tests in
GitWorktreesTest(58 total there now, up from 51) covering:fresh seed with no
.claude/at all, invariant 1 (repo's own skill survives byte-for-byte evenwhen the source carries a same-named skill with different content), invariant 3 (missing source
directory never fails the spawn), invariant 2 in three directions (seeded worktree's
git statusis clean; a sibling worktree's own unrelated untracked
.claude/skills/still shows up normally;and — added in the review fix — an operator's own global excludesFile pattern still applies after
seeding, via the new
seedSkillsComposesWithAnAlreadyEffectiveGlobalExcludesFile), and theseeded/kept log-line shape. It also includes updates to two existing config coverage tests
(
ConfigRefTopLevelReportingCoverageTest,FleetConfigWithDefaultsPreservesEveryComponentTest)required to keep the new
memberSkillskey's BASE/ALT value maps in sync with the record shape —those tests fail loudly (by design) if a new top-level
FleetConfigcomponent isn't given a realvalue and triaged.
Along the way, adding
memberSkillsinitially broke three pre-existing tests that check everyFleetConfigcomponent is triaged into a reload class and given a real value(
ConfigRefTopLevelCoverageTest,ConfigRefTopLevelReportingCoverageTest,FleetConfigWithDefaultsPreservesEveryComponentTest) untilmemberSkillswas added toConfigRef.DEFERRED_KEYSwith its ownchangedDeferredKeysbranch and to those tests' valuemaps — all now green.
I have no IDE MCP tools as a worker; the above
mvn clean installoutput is the only check I ranor am claiming.
Anything else in this repo with the same shape (not fixed, per the brief)
plugin/skills/setup/SKILL.mdand the rest ofplugin/— a member-facing onboarding asset thatonly a plugin install delivers, never a provisioned worktree; issue #362 items 1–2 (someone
else's scope) cover it.
.claude/agents/*.md— already worktree-seeded viaagentDefinitionFile, so not itself a gap,but any new agent role added only to this repo's
.claude/agents/(not to whatever directory afuture
memberSkills-style config seeds) would be invisible in another repo's worktree the sameway skills were before this PR.
docs/Worker-Git-Workflow.mdanddocs/MCP-Contract.md— referenced by theimplementerskilland by this repo's own
CLAUDE.md, but neither is seeded into a worktree of a different repo;a worker there following a seeded
implementerskill that links to../../docs/...would findnothing.
wiki/— already known to be a submodule workers see as months-stale (per team memory); not thisticket's shape exactly, but the same underlying pattern ("an asset a member is expected to have
that only exists because the member happens to be in this one repo").
Caveats for review
The— fixed, see "Review fix" at the top ofcore.excludesFileworktree-scope caveat abovethis PR body.
seedSkillsdoes not restrict candidate skill names to a known allowlist (implementer,reviewer,hunter) — it copies every immediate, non-hidden subdirectory of the configuredsource. The lead reviewed this and said to leave it as-is; per that review I documented in
FleetConfig's javadoc andfleetd.example.yamlthat the directory's contents are copiedwholesale, so an operator knows not to park scratch files there.
fleetd.yaml(gitignored, and I cannot see it as a worker)— all proof is from throwaway repos exercising
GitWorktrees#addend-to-end, per the brief'sown instruction to reproduce the shape rather than reason about the live config.
Merged to
mainas92c0f16.Verified on the merge itself:
Two mutations run on the merge, both red:
Both directions of the compose behaviour are pinned now. Before round 2 the first of those was green.
One hypothesis of mine that measuring disproved, recorded so nobody re-raises it. I suspected the two compose tests could pass for the wrong reason: their assertion helper
fullStatusbuilds its ownProcessBuilderand inherits the JVM environment, so it runsgit statusagainst the operator's real global config — which on this machine has acore.excludesFilecontainingtarget. That looked like it would hide a broken composition.It does not. The worktree-scoped
core.excludesFilethe fix writes shadows the operator's global one in that process too, so with composition removedtargetreally does show up. That is exactly what the mutation above measured: 2 failures, not 0. The tests pin what they claim to pin.One real note, pre-existing, not from this PR. The test class is sensitive to the ambient environment. Running it with
XDG_CONFIG_HOMEpointed at a directory whosegit/ignoreis*turns 56 of 59 tests red. The cause is the helpers, not the fix:status()andfullStatus()set no git-isolation variables at all, andgitOutput()setsGIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEMbut notXDG_CONFIG_HOME. Both helpers are onmainin that shape and predate this work. Filed separately rather than held against this PR.The
hermeticGitEnvhelper this PR adds is the right pattern to extend to them.Pull request closed