"skill seeding: N of M" is logged as success for opencode members, which never read .claude/skills/ #393

Closed
opened 2026-09-10 02:51:59 +02:00 by ltms · 2 comments
Owner

The defect

memberSkills: (fleetd #362) copies skill folders into every provisioned worktree's .claude/skills/, and logs a success line:

00:09:56.971 GitWorktrees - skill seeding: 3 of 3 candidate(s) from .../member-skills

.claude/skills/ is a Claude Code convention. An opencode member takes its instructions from its generated config, and the seeded skills are not in it:

$ jq .instructions $OPENCODE_CONFIG
["/tmp/fleetd-opencode-.../member-charter.md",
 "/tmp/fleetd-opencode-.../ide-rules.md"]

OpenCodeLauncher builds that array from exactly two sources — the charter at :450 and the IDE rules at :479. There is no third entry, and there is no AGENTS.md in the worktree. So for a kind: opencode member the seeding is a guaranteed no-op, reported as 3 of 3.

GitWorktrees.seedSkills never consults the member kind. Its only guard is memberSkillsSource being null or blank.

Consequence

An opencode member's entire instruction set is member-charter.md, which is one paragraph and is all about calling fleet_reply. It carries no role procedure, no "never merge", no "stage files explicitly" — the content the lead believes it delivered.

This bites hardest where a lead follows the charter correctly. Line 1 of every brief is meant to be Load the <name> skill., and fleetd #362 exists precisely because that line used to be a silent no-op. For opencode members it still is; the log line just changed.

It is not host-specific. Any host running opencode members has it. On fleet01 all three worker slots — dev, reviewer and architect — are kind: opencode, so no member on that host has ever read a role contract.

Fix — either is acceptable, the second is honest on its own

  1. Deliver it. Append each seeded skill's SKILL.md to instructions[] for opencode-kind launchers, so the same content reaches both kinds.
  2. Stop claiming it. Log seeding at the launcher, after the kind is known, and say plainly when the target kind cannot consume it.

Do 1 if the skills are meant to apply to every member, which the memberSkills: docs imply. Do 2 regardless — a success line for work that cannot take effect is the failure shape this repo keeps paying for, and 1 without 2 leaves the log unable to tell a real seeding from a skipped one.

Acceptance

  • An opencode member spawned with memberSkills: configured can act on the seeded role procedure, or the log says clearly that it cannot.
  • A test covers an opencode-kind spawn with memberSkills: set. Today the seeding tests do not vary the member kind.
  • The memberSkills: documentation states which member kinds actually consume it.

Found by the fleet01 lead. I had reported the same consequence — members with no role contract — via the wrong mechanism, looking in kb/CLAUDE.md and kb/.claude/skills/; they traced the real channel. Verified against the code and filed by the mac lead at their request.

## The defect `memberSkills:` (fleetd #362) copies skill folders into every provisioned worktree's `.claude/skills/`, and logs a success line: ``` 00:09:56.971 GitWorktrees - skill seeding: 3 of 3 candidate(s) from .../member-skills ``` `.claude/skills/` is a **Claude Code** convention. An opencode member takes its instructions from its generated config, and the seeded skills are not in it: ``` $ jq .instructions $OPENCODE_CONFIG ["/tmp/fleetd-opencode-.../member-charter.md", "/tmp/fleetd-opencode-.../ide-rules.md"] ``` `OpenCodeLauncher` builds that array from exactly two sources — the charter at `:450` and the IDE rules at `:479`. There is no third entry, and there is no `AGENTS.md` in the worktree. So for a `kind: opencode` member the seeding is a guaranteed no-op, reported as `3 of 3`. `GitWorktrees.seedSkills` never consults the member kind. Its only guard is `memberSkillsSource` being null or blank. ## Consequence An opencode member's entire instruction set is `member-charter.md`, which is one paragraph and is all about calling `fleet_reply`. It carries no role procedure, no "never merge", no "stage files explicitly" — the content the lead believes it delivered. This bites hardest where a lead follows the charter correctly. Line 1 of every brief is meant to be `Load the <name> skill.`, and fleetd #362 exists precisely because that line used to be a silent no-op. For opencode members it still is; the log line just changed. It is not host-specific. Any host running opencode members has it. On fleet01 all three worker slots — dev, reviewer and architect — are `kind: opencode`, so **no member on that host has ever read a role contract**. ## Fix — either is acceptable, the second is honest on its own 1. **Deliver it.** Append each seeded skill's `SKILL.md` to `instructions[]` for opencode-kind launchers, so the same content reaches both kinds. 2. **Stop claiming it.** Log seeding at the launcher, after the kind is known, and say plainly when the target kind cannot consume it. Do 1 if the skills are meant to apply to every member, which the `memberSkills:` docs imply. Do 2 regardless — a success line for work that cannot take effect is the failure shape this repo keeps paying for, and 1 without 2 leaves the log unable to tell a real seeding from a skipped one. ## Acceptance - An opencode member spawned with `memberSkills:` configured can act on the seeded role procedure, **or** the log says clearly that it cannot. - A test covers an opencode-kind spawn with `memberSkills:` set. Today the seeding tests do not vary the member kind. - The `memberSkills:` documentation states which member kinds actually consume it. Found by the fleet01 lead. I had reported the same consequence — members with no role contract — via the wrong mechanism, looking in `kb/CLAUDE.md` and `kb/.claude/skills/`; they traced the real channel. Verified against the code and filed by the mac lead at their request.
Author
Owner

Correction to my own severity claim. I wrote that all three of fleet01's worker slots are kind: opencode, so no member there has ever read a role contract. That is wrong. The fleet01 lead corrected it, and I then measured it in their config over ssh rather than taking it:

/home/ltms/LTMS/fleetd/fleetd/fleetd.yaml

gx     kind: opencode      weight: 100   maxLoad: 2
xf     kind: opencode      weight:  80   maxLoad: 5
local  kind: claude-code   weight:  10   maxLoad: 2
opus   kind: claude-code   weight:   0   maxLoad: 1   subscription: true

Four profiles, two kinds. local and opus are claude-code and do read the seeded skills. So "3 of 3, nobody ever" overstates it.

The accurate framing, which is narrower but sharper

By slot count: 7 of 10 configured slots are on an opencode profile and cannot read a role contract.

By placement weight, which is what decides where a spawn actually lands, it is worse than the slot count suggests. opus is weight: 0, so it is never auto-selected — it is the lead seat. That leaves gx 100, xf 80 and local 10 competing:

local's share of auto-selected spawns = 10 / (100 + 80 + 10) = 10/190 = 5.3%

So about 95% of their auto-placed members land on a profile that cannot read a role contract, and the remaining 5% is a single claude-code profile with the lowest weight in the file. The defect is real and the practical severity is high — but for the reason above, not the reason I gave.

Why I am recording the correction rather than just editing the number

An overstated severity is the thing a reader discounts later, which is the fleet01 lead's point and it is right. "Nobody ever" invites someone to check one counter-example and dismiss the whole ticket. "95% by weight, and the exception is the lowest-weighted profile" survives that check.

Note also that this is a per-host number. It is measured on fleet01 on 2026-09-10 and it changes the moment anyone edits a weight:. Re-measure with the awk over profiles: above rather than quoting 95% later.

Unchanged

The defect itself: memberSkills seeding reports success for members that structurally cannot read it. Their side is closed by fleet.charters, which is daemon-local and reaches opencode members through instructions[] — verified there with a throwaway spawn. The seeding path's false success report is still open here.

**Correction to my own severity claim.** I wrote that all three of fleet01's worker slots are `kind: opencode`, so no member there has ever read a role contract. That is wrong. The fleet01 lead corrected it, and I then measured it in their config over ssh rather than taking it: ``` /home/ltms/LTMS/fleetd/fleetd/fleetd.yaml gx kind: opencode weight: 100 maxLoad: 2 xf kind: opencode weight: 80 maxLoad: 5 local kind: claude-code weight: 10 maxLoad: 2 opus kind: claude-code weight: 0 maxLoad: 1 subscription: true ``` Four profiles, two kinds. `local` and `opus` are `claude-code` and **do** read the seeded skills. So "3 of 3, nobody ever" overstates it. ### The accurate framing, which is narrower but sharper By slot count: **7 of 10 configured slots** are on an opencode profile and cannot read a role contract. By *placement weight*, which is what decides where a spawn actually lands, it is worse than the slot count suggests. `opus` is `weight: 0`, so it is never auto-selected — it is the lead seat. That leaves `gx` 100, `xf` 80 and `local` 10 competing: ``` local's share of auto-selected spawns = 10 / (100 + 80 + 10) = 10/190 = 5.3% ``` So about **95% of their auto-placed members land on a profile that cannot read a role contract**, and the remaining 5% is a single `claude-code` profile with the lowest weight in the file. The defect is real and the practical severity is high — but for the reason above, not the reason I gave. ### Why I am recording the correction rather than just editing the number An overstated severity is the thing a reader discounts later, which is the fleet01 lead's point and it is right. "Nobody ever" invites someone to check one counter-example and dismiss the whole ticket. "95% by weight, and the exception is the lowest-weighted profile" survives that check. Note also that this is a per-host number. It is measured on fleet01 on 2026-09-10 and it changes the moment anyone edits a `weight:`. Re-measure with the awk over `profiles:` above rather than quoting 95% later. ### Unchanged The defect itself: `memberSkills` seeding reports success for members that structurally cannot read it. Their side is closed by `fleet.charters`, which is daemon-local and reaches opencode members through `instructions[]` — verified there with a throwaway spawn. The seeding path's false success report is still open here.
Author
Owner

Merged as 17052bb (merge commit), with a comment correction on top; main is at 5ba69c9. PR #471. Verified landed with git merge-base --is-ancestor, against an unmerged control branch that reported no.

My own mutation battery, on the merge commit

Ran on tree dcf8959, which is the merge's tree exactly. Full mvn -B clean test per cell.

CONTROL 0: three writers on instructions[], putArray count 0 (was 1), withArray count 3. All three new combination tests present. Four assertions use assertEquals(List.of(...)) — contents, not size.

CONTROL 1, unmutated: 1618 tests, 0 failures, 0 errors, BUILD SUCCESS.

cell mutation result
M1r skills writer back to putArray — the survivor from the last merge KILLED — instructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrder
M2r charter writer back to putArray SURVIVED, 1618 green — see below
M3 delete the charter writer outright KILLED — 6 tests
M4 delete the skills delivery loop KILLED — aSeededSkillReachesTheOpencodeMembersInstructionsArray, aSkillFolderWithoutSkillMdIsNeverDeliveredAndTheLogNamesIt, instructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrder
M5 IDE-rules writer to putArray KILLED — 2 tests, exactly the two names the worker reported

M1r is the one that matters. On the previous merge that same mutation left 1603 tests green while silently deleting the charter entry from an opencode member's config. It is now caught by name.

M2r survived, and that is correct

Flipping the charter writer back to putArray leaves the suite green and always will. That writer runs first against an empty array, so there is no existing node for putArray to replace and the two idioms are indistinguishable. No test can tell them apart. The worker measured this independently and said so plainly in their report rather than claiming a kill they did not have.

The edit is still right — it removes an ordering constraint nobody wrote down — but it is a readability change with no test behind it, and that is not a gap anyone can close.

The comment above that line claimed otherwise, so I corrected it. It said the flip was "proven load-bearing" and named two tests as covering it. Neither does. A comment naming tests that do not cover the line is worse than no comment: the next person mutates it, sees green, and concludes the tests are broken. The corrected comment states what each cell actually measures. That is commit 5ba69c9, comments only — 0 non-comment lines changed in either direction, and 0 test files read OpenCodeLauncher.java as source text (control: 11 test files do read Fleetd.java that way), so the battery numbers still describe what was pushed.

A correction to the diagnosis, including my own repeat of it

fleet01 first said the missing test axis was the combination of writers — that a test with one writer active cannot tell putArray from withArray. They then revised that to a stronger claim: that nothing asserted the charter reaches an opencode member at all, and that this hole predated #393.

I checked the stronger claim and it is false. M3 (deleting the charter writer) fails 6 tests, and 3 of them existed before #393:

  • writesRemoteMcpConfigAndCharterInstructionsWhenMcpUrlSet
  • roleCharterWithoutMcpOrCustomProviderStillWritesAConfig
  • aPinnedEndpointAndTheFleetMcpCoexistInOneConfig

Measured by reading OpenCodeLauncherTest.java at 789b6a8, the commit before this work, with a control name that correctly returned 0. The charter reaching a member was already pinned.

So the first diagnosis was the right one. The surviving mutation never deleted the charter write; it made a later writer replace the whole array. Every pre-existing test had exactly one writer active. The gap was never "is the charter delivered" — it was "are two writers ever active at the same time", which is the combination axis, and that is what the three new tests add.

Recording this because the stronger claim is the more quotable one, and it would have sent the next reader hunting for a hole that is not there.

What this does not do

Opencode has no equivalent of Claude Code's Skill tool. The content arrives as static system-prompt text from spawn and cannot be invoked by name. So this closes the delivery gap and cannot close the activation gap; that residue is opencode's design, not a fleetd limit. Worth stating so nobody later reads "skills work on opencode now" as more than it is.

Wiki Features entry "Bridge skills are seeded into every provisioned worktree" updated with the opencode half and both gotchas — wiki commit 14798b3.

Beyond-scope findings from the worker, not fixed

Three log lines that claim more than they check. Recording them here so they are not lost; none is filed yet.

  1. GitWorktrees.java:1005 — "parity overlay: copied {} of {} candidates" logs success regardless of member kind or whether anything reads the copied files.
  2. HerdrPeerLauncher.java:1454 — logs the ZDOTDIR scrub as applied without checking the member's login shell is zsh. A non-zsh member's scrub silently never runs.
  3. HerdrPeerLauncher.java:1578/1668 — "member credentials: allowed {} of {}" counts names allowed into the pane env without checking whether the member's runtime reads that variable. "Allowed" is not "used".

The worker also checked and cleared HerdrPeerLauncher.java:317 and :1892, which both name their limiting condition already.

Merged as `17052bb` (merge commit), with a comment correction on top; `main` is at `5ba69c9`. PR #471. Verified landed with `git merge-base --is-ancestor`, against an unmerged control branch that reported `no`. ## My own mutation battery, on the merge commit Ran on tree `dcf8959`, which is the merge's tree exactly. Full `mvn -B clean test` per cell. CONTROL 0: three writers on `instructions[]`, `putArray` count **0** (was 1), `withArray` count **3**. All three new combination tests present. Four assertions use `assertEquals(List.of(...))` — contents, not size. CONTROL 1, unmutated: **1618 tests, 0 failures, 0 errors, BUILD SUCCESS**. | cell | mutation | result | |---|---|---| | M1r | skills writer back to `putArray` — **the survivor from the last merge** | **KILLED** — `instructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrder` | | M2r | charter writer back to `putArray` | **SURVIVED**, 1618 green — see below | | M3 | delete the charter writer outright | **KILLED** — 6 tests | | M4 | delete the skills delivery loop | **KILLED** — `aSeededSkillReachesTheOpencodeMembersInstructionsArray`, `aSkillFolderWithoutSkillMdIsNeverDeliveredAndTheLogNamesIt`, `instructionsArrayHoldsCharterThenSkillsThenIdeRulesInOrder` | | M5 | IDE-rules writer to `putArray` | **KILLED** — 2 tests, exactly the two names the worker reported | M1r is the one that matters. On the previous merge that same mutation left 1603 tests green while silently deleting the charter entry from an opencode member's config. It is now caught by name. ## M2r survived, and that is correct Flipping the charter writer back to `putArray` leaves the suite green and always will. That writer runs first against an empty array, so there is no existing node for `putArray` to replace and the two idioms are indistinguishable. No test can tell them apart. The worker measured this independently and said so plainly in their report rather than claiming a kill they did not have. The edit is still right — it removes an ordering constraint nobody wrote down — but it is a readability change with no test behind it, and that is not a gap anyone can close. **The comment above that line claimed otherwise, so I corrected it.** It said the flip was "proven load-bearing" and named two tests as covering it. Neither does. A comment naming tests that do not cover the line is worse than no comment: the next person mutates it, sees green, and concludes the tests are broken. The corrected comment states what each cell actually measures. That is commit `5ba69c9`, comments only — 0 non-comment lines changed in either direction, and 0 test files read `OpenCodeLauncher.java` as source text (control: 11 test files do read `Fleetd.java` that way), so the battery numbers still describe what was pushed. ## A correction to the diagnosis, including my own repeat of it fleet01 first said the missing test axis was the **combination** of writers — that a test with one writer active cannot tell `putArray` from `withArray`. They then revised that to a stronger claim: that nothing asserted the charter reaches an opencode member at all, and that this hole predated #393. I checked the stronger claim and it is **false**. M3 (deleting the charter writer) fails 6 tests, and **3 of them existed before #393**: - `writesRemoteMcpConfigAndCharterInstructionsWhenMcpUrlSet` - `roleCharterWithoutMcpOrCustomProviderStillWritesAConfig` - `aPinnedEndpointAndTheFleetMcpCoexistInOneConfig` Measured by reading `OpenCodeLauncherTest.java` at `789b6a8`, the commit before this work, with a control name that correctly returned 0. The charter reaching a member was already pinned. So the **first** diagnosis was the right one. The surviving mutation never deleted the charter write; it made a *later* writer replace the whole array. Every pre-existing test had exactly one writer active. The gap was never "is the charter delivered" — it was "are two writers ever active at the same time", which is the combination axis, and that is what the three new tests add. Recording this because the stronger claim is the more quotable one, and it would have sent the next reader hunting for a hole that is not there. ## What this does not do Opencode has no equivalent of Claude Code's Skill tool. The content arrives as static system-prompt text from spawn and cannot be invoked by name. So this closes the **delivery** gap and cannot close the **activation** gap; that residue is opencode's design, not a fleetd limit. Worth stating so nobody later reads "skills work on opencode now" as more than it is. Wiki [Features](https://git.ltms.dev/fleet/fleetd/wiki/11-Features) entry "Bridge skills are seeded into every provisioned worktree" updated with the opencode half and both gotchas — wiki commit `14798b3`. ## Beyond-scope findings from the worker, not fixed Three log lines that claim more than they check. Recording them here so they are not lost; none is filed yet. 1. `GitWorktrees.java:1005` — "parity overlay: copied {} of {} candidates" logs success regardless of member kind or whether anything reads the copied files. 2. `HerdrPeerLauncher.java:1454` — logs the ZDOTDIR scrub as applied without checking the member's login shell is zsh. A non-zsh member's scrub silently never runs. 3. `HerdrPeerLauncher.java:1578`/`1668` — "member credentials: allowed {} of {}" counts names allowed into the pane env without checking whether the member's runtime reads that variable. "Allowed" is not "used". The worker also checked and cleared `HerdrPeerLauncher.java:317` and `:1892`, which both name their limiting condition already.
ltms closed this issue 2026-09-10 15:18:57 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#393