fleetd #393: deliver memberSkills to opencode members, and stop overclaiming seeding success #471
Closed
agent
wants to merge 0 commits from
worker/393-opencode-skill-seeding-71854b-13 into main
pull from: worker/393-opencode-skill-seeding-71854b-13
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/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/362-worktree-skills-c03e51-3
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#471
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/393-opencode-skill-seeding-71854b-13"
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?
Closes #393.
What changed
GitWorktrees.seedSkillscopiesmemberSkills:-seeded skill folders into every provisioned worktree's.claude/skills/and loggedskill seeding: N of Mas if that were success..claude/skills/is a Claude Code CLI convention; opencode has no such discovery, so akind: opencodemember never actually read a seeded skill even though the log said N of M succeeded.Two changes, both required per the ticket:
OpenCodeLauncher.skillInstructionFilesscans<cwd>/.claude/skills/*/SKILL.mdat spawn time (the one point the launcher knows both the kind and the cwd) and appends each to the generatedopencode.json'sinstructions[]array, the same channel already used for the member charter and IDE rules. A skill folder with noSKILL.mdis named and skipped rather than silently dropped. The config-file gate inbuildLaunchis updated so a seeded skill alone (no MCP, no charter, no custom provider) is enough to triggerOPENCODE_CONFIG.GitWorktrees.seedSkills's log now says explicitly that consumption depends on the member's kind and points at the launcher's own log.OpenCodeLauncherlogs its own kind-awareskill delivery: M of N ...line once the kind is actually known, naming any folder it could not turn into aninstructions[]entry.fleetd.example.yaml'smemberSkills:doc previously claimed "Claude Code members only; an opencode member reads a different path (.opencode/agent) this key does not touch" — false as of this fix (the ticket asked me to check for and fix exactly this). Corrected to name both kinds and how each consumes it.ClaudeCodeLauncheris untouched — its native.claude/skills/discovery already worked and is explicitly out of scope per the ticket.Tests
OpenCodeLauncherTestgains two cases, both driving the realGitWorktrees#addseeding path (not a hand-built.claude/skills/fixture) into an opencode-kind spawn:aSeededSkillReachesTheOpencodeMembersInstructionsArray— a seeded skill'sSKILL.mdlands ininstructions[], plus the honestskill delivery: 1 of 1log line.aSkillFolderWithoutSkillMdIsNeverDeliveredAndTheLogNamesIt— a well-formed skill is still delivered alongside a malformed one; the malformed one is named in the log and excluded frominstructions[].Break-and-restore
Delivery half — commented out the
instructions[].add(...)loop inwriteConfig. Both new tests failed:aSeededSkillReachesTheOpencodeMembersInstructionsArray:the seeded skill's SKILL.md must be an instructions[] entry — got: [] ==> expected: <true> but was: <false>aSkillFolderWithoutSkillMdIsNeverDeliveredAndTheLogNamesIt:the well-formed skill is still delivered alongside the malformed one ==> expected: <true> but was: <false>Restored, re-ran, green.
Logging half — commented out the
log.info("skill delivery: ...")call inskillInstructionFiles. Both new tests failed:aSeededSkillReachesTheOpencodeMembersInstructionsArray:the launcher must log, kind-aware, that it delivered the skill — got: [] ==> expected: <true> but was: <false>aSkillFolderWithoutSkillMdIsNeverDeliveredAndTheLogNamesIt:the log must say plainly which folder could not be consumed and why — got: [] ==> expected: <true> but was: <false>Restored, re-ran, green.
Full suite
Beyond scope (not fixed, reporting only — the lead asked for this list)
A read-only sweep of
src/main/javafor the same shape (a log claims success for work whose downstream consumer may not actually be able to use it), most confident first:GitWorktrees.java:1005—"parity overlay: copied {} of {} candidates"— copies operator-configured files (default.env) into every worktree regardless of member kind or whether anything downstream reads them; weaker than #393 though, since.env/.envrcare plain filesystem config any shell/tool reads uniformly, not gated by member kind the way.claude/skills/is.HerdrPeerLauncher.java:1454—"memberCredentials policy=allow-list: profile={} generated ZDOTDIR {}"— logs the ZDOTDIR scrub as generated/applied without checking the member's actual login shell is zsh; a non-zsh member's.zlogin-based scrub silently never runs.HerdrPeerLauncher.java:1578/1668—"member credentials: allowed {} of {}"— counts names allowed into the pane env; doesn't distinguish whether the member's actual runtime (opencode vs claude-code) even reads a given var, so "allowed" isn't the same claim as "used."Already-honest examples checked, not new instances:
HerdrPeerLauncher.java:317("context reset is unsupported for peer kind {}") andHerdrPeerLauncher.java:1892(memberCredentials gap warning) both explicitly name the limiting condition rather than overclaiming.Caveats for review
instructions[]is static system-prompt text present from spawn, not an invokable Skill-tool resource the way Claude Code's.claude/skills/is. The content reaches both kinds; the mechanism by which a member acts on it differs, and that's inherent to opencode's design, not something this PR can close further.ClaudeCodeLauncherper the ticket's explicit boundary.fleetd/fleetd.yamlis gitignored and not in my worktree; I did not touch it or attempt to reproduce fleet01's live shape beyond what the tests need.Follow-up: instructions[] writer-ordering hazard (fixed in this PR)
After the delivery/logging halves above were accepted, the fleet01 lead found — and my lead
independently verified on this branch's merge — a separate, previously-undetected hazard in
OpenCodeLauncher.writeConfig: theinstructions[]array has three writers (charter, seededskills, IDE rules). The charter writer used
ObjectNode.putArray(create-or-REPLACE) insteadof
withArray(get-or-create), which only "worked" because it happened to run first, against astill-empty array — an undeclared ordering dependency nothing tested.
Proof it was live-load-bearing: switching the skills writer from
withArraytoputArrayleft the entire 1603-test suite green while silently deleting the charter entry — an opencode
member would launch with no role contract at all, worse than the bug this ticket fixed.
Fix (this commit): the charter writer's
putArray->withArray— a one-word productionchange, behavior-identical today. Added three tests in
OpenCodeLauncherTestthat assertinstructions[]content as an exact ordered list (not size — aputArraymutation canreplace N entries with a different N, so a size check cannot tell them apart):
instructionsArrayHoldsExactlyTheCharterWhenNothingElseWritesToIt— charter onlyinstructionsArrayHoldsCharterThenIdeRulesInOrder— charter + IDE rulesinstructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrder— charter + IDE rules + seededskills (the realistic shape on a host where weighted placement makes opencode the default for
most members, per fleet01)
Mutation testing, each writer flipped to
putArrayindividually and restored after:structurally always runs first in
writeConfig, against an empty array, soputArrayandwithArrayare equivalent there. Full suite stayed at 59/59 green inOpenCodeLauncherTestunder this mutation. This is not a gap in the tests; it is the same factthat makes the fix itself "behavior-identical today."
instructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrderby name (1failure, 58 other
OpenCodeLauncherTesttests still pass).instructionsArrayHoldsCharterThenIdeRulesInOrderandinstructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrder(2 failures, 57 others pass).Full suite with the fix restored:
Tests run: 1606, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS, exit code 0.What I did not verify: what opencode actually does at runtime with N
instructions[]entries(order of concatenation, separators, whether a later entry can override an earlier one). I have
not run a live opencode session against a multi-entry config and have no observed behavior to
report here — this stays an open question rather than a guess.
Credit: the writer-ordering hazard itself was found by the fleet01 lead and independently
verified by my lead on this branch's merge, not by me. My original delivery and logging work
(above) held up under that verification unmodified; this section is a distinct hazard neither of
those tests could see, because none of them combined all three writers in one config.
Adjudicated. The change is accepted and I have asked for one small follow-up on this branch before I merge.
Battery run on the merge commit (
56d2890, tree917ae82), not on the branch. Control, unmutated:Tests run: 1603, Failures: 0, Errors: 0, Skipped: 0—BUILD SUCCESS, rc=0, 0 compile-error blocks.What holds
Both halves of the change are genuinely pinned — I re-ran the PR's break-and-restore proofs independently rather than taking them, and they reproduce. The delivery half and the kind-aware logging half each fail by name when broken.
The test design is the right call and worth naming: both new tests drive the real
GitWorktrees#addseeding path instead of a hand-built.claude/skills/fixture. So they proveOpenCodeLauncherreads whatGitWorktreesactually produces, not what the author imagined it produces. That is the difference between a test of the seam and a test of the pipeline, and it was not asked for in my brief.The PR is also honest about the limit that matters most: opencode's
instructions[]is static system-prompt text present from spawn, not an invokable skill resource the way Claude Code's.claude/skills/is. The content now reaches both kinds; how a member can act on it still differs. Saying that plainly is better than the change would have been with a claim of parity.What does not hold — an ordering constraint nothing declares and nothing tests
This is context the worker never had. The fleet01 lead sent it to me after the brief went out; I verified their mechanism in my own clone, then measured the consequence here.
There are now three writers to
instructions[], and they are not the same operation:putArrayreplaces the node;withArraygets-or-creates it. The charter at :527 gets away withputArrayonly because it runs first, while the array is still empty. This PR usedwithArrayand placed it after the charter, which is correct. The hazard is that nothing holds that arrangement in place.Two cells, both aimed at the ordering rather than at this PR's own code:
putArray— destroys the charter entry above itputArray— it runs last, so it destroys charter and skillsBoth directions unpinned. In M1's state an opencode member launches with no role contract at all, and the full suite passes.
That is a worse failure than the bug this ticket fixed, and it is reachable by a one-word edit from anyone who adds a fourth writer without reading :478's comment. The count of writers went from two to three here, so the odds of a fourth are now higher than they were.
Why the existing tests cannot catch it: both new tests are skills-focused, and they pass under either idiom. The defect needs two or more writers active at once, so member kind is the wrong axis for it — a kind-parameterised test with only a charter present passes whichever operation the code uses.
The follow-up I asked for
Sent to the same worker, to push to this branch:
withArray. Behaviour is identical today, and afterwards the order of the three writers stops mattering. The whole value is deleting an unwritten rule.putArrayswaps two entries for two different ones, which is precisely this failure mode.Both the fix and the test shape are fleet01's recommendation, passed on as theirs. The three-writer cell is the realistic configuration on their host, where weighted placement makes opencode the default for essentially every member.
Also accepted
The sweep for other kind-blind success logs is useful and correctly ranked, including the two cases checked and reported as not instances (
HerdrPeerLauncher:317and:1892already name their limiting condition). Reporting a non-finding is worth as much as reporting a finding here. Those three candidates are noted and not fixed in this PR, which is the right boundary.The
fleetd.example.yamlcorrection matters more than its size suggests: the old text actively told the reader opencode reads.opencode/agentand that this key does not touch it. A false statement in the example config is worse than an omission, because it is what an operator reads while deciding.Note on these numbers
They describe tree
917ae82. The follow-up will change the tree, so I will re-measure on the final merge before pushing rather than carrying these forward.Re-posting the beyond-scope research as a standalone comment for visibility (this was already researched directly — no fork was delegated for it — and is part of the PR description above; surfacing it here too since it was asked for as a comment):
Beyond scope (not fixed, reporting only — the lead asked for this list)
A read-only sweep of
src/main/javafor the same shape (a log claims success for work whose downstream consumer may not actually be able to use it), most confident first:GitWorktrees.java:1005—"parity overlay: copied {} of {} candidates"— copies operator-configured files (default.env) into every worktree regardless of member kind or whether anything downstream reads them; weaker than #393 though, since.env/.envrcare plain filesystem config any shell/tool reads uniformly, not gated by member kind the way.claude/skills/is.HerdrPeerLauncher.java:1454—"memberCredentials policy=allow-list: profile={} generated ZDOTDIR {}"— logs the ZDOTDIR scrub as generated/applied without checking the member's actual login shell is zsh; a non-zsh member's.zlogin-based scrub silently never runs.HerdrPeerLauncher.java:1578/1668—"member credentials: allowed {} of {}"— counts names allowed into the pane env; doesn't distinguish whether the member's actual runtime (opencode vs claude-code) even reads a given var, so "allowed" isn't the same claim as "used."Already-honest examples checked, not new instances:
HerdrPeerLauncher.java:317("context reset is unsupported for peer kind {}") andHerdrPeerLauncher.java:1892(memberCredentials gap warning) both explicitly name the limiting condition rather than overclaiming.Pull request closed