Features: opencode members actually receive seeded skills, and a charter's text is checked

Two entries updated, both for behaviour an operator can configure or observe.

memberSkills (fleetd #393): the entry said fleetd copies skill folders into
each worktree's .claude/skills/, which was the whole story only for Claude
Code members. Opencode never reads that directory, so an opencode member
was seeded correctly and read nothing. Added what now happens (each seeded
skill's SKILL.md goes into the member's instructions[] array), and two
gotchas: delivery is not activation (opencode has no Skill tool, so the
text is static system-prompt content from spawn and cannot be invoked by
name), and anyone adding a fourth writer to instructions[] must use
withArray and assert contents rather than size.

fleet.charters (fleetd #469 and #474): the entry described validateCharters()
checking the shape of the map only. Added the new check on the charter's
TEXT against the canonical tool set, and - more important - the limit of
it. That check runs at startup only. The entry's own existing sentence says
validation runs on both the startup and reload paths, and that is still
true of the shape checks but not of the new one. Charters are hot and read
live per spawn, so a charter edit without a restart is currently unchecked.
Said so plainly and pointed at #474.

Entry count unchanged at 157 - both are edits to existing entries, not new
ones.
Dai Ha
2026-09-10 20:17:53 +07:00
parent 3dde75ebca
commit 14798b39e7
+45
@@ -992,6 +992,23 @@ treating it as absent would quietly strip that role of its contract.
Validation runs on **both** the startup path and the reload path. Wiring only one of the two is the
whole bug: a config that a running daemon refuses but a restart accepts, or the reverse.
**The charter's text is checked against the live tool surface too (fleetd #469).** Validation used
to look only at the shape of the map: is the key a role name, is the text non-blank. It never read
what the text said. So a charter telling a member to call `bridge_send` — a tool name the CB-634
rename removed — started the daemon cleanly, and the member found out at run time by calling
something that was not there. Now `CharterToolSurface` pulls every `fleet_*` and `bridge_*` token
out of each charter and asks `FleetTool`, the one enum the MCP server derives its registrations
from, whether that tool exists. If one does not, the daemon refuses to start and the message names
both the charter key and the unknown tool.
**Know the limit of that check: it runs at startup only (fleetd #474).** The paragraph above says
validation runs on both the startup and the reload path, and that is true of the shape checks. It
is **not** yet true of the tool-name check, which has one call site, in `Fleetd.main`. Charters are
hot and are read live at each spawn, so editing `fleet.charters:` on a running daemon to name a tool
that does not exist is accepted, applied, and delivered to the next member. A restart would refuse
the same file. Until #474 lands, treat a charter edit made without a restart as unchecked, and read
the startup log after the next restart to find out whether it was valid.
**Do not put secrets in charter text.** There is deliberately no `${ENV}` interpolation. The
OpenCode adapter writes the composed charter to a temp file so its CLI can read it, and that file is
world-readable.
@@ -4653,6 +4670,34 @@ travel in the plugin either: `ClaudeCodeLauncher` exports `CLAUDE_CONFIG_DIR`, s
reads the operator's plugin store. The worktree is the only channel that reaches a member.
fleetd #362 item 3.
**Opencode members need a second step, and they now get it (fleetd #393).** Copying the folders is
the whole feature for a Claude Code member, because Claude Code reads `.claude/skills/` natively.
Opencode never reads that directory. So for the first weeks this key existed, an opencode member
was seeded correctly and read nothing: the copy succeeded, the files were right, and no test failed
because there was nothing to fail. The feature worked at the only layer it implemented.
An opencode member's only channel for static guidance text is the `instructions[]` array in the
config `OpenCodeLauncher` generates for it. Each seeded skill's `SKILL.md` is now added there, by
absolute path. A skill folder with no `SKILL.md` is never delivered, and the log names the folder
so a typo is visible instead of silent.
**The gotcha that matters here is delivery versus activation.** Opencode has no equivalent of
Claude Code's Skill tool. The text arrives as part of the system prompt from spawn and stays there;
a member cannot load one skill by name when it needs it. So `Load the implementer skill.` means
something different on the two backends: on Claude Code it is an instruction the member acts on, on
opencode the content is simply already present. Do not read "skills work on opencode now" as more
than that. This is opencode's design, not a fleetd limit, and it is why the delivery gap could be
closed here and the activation gap could not.
**A second gotcha, for anyone adding a third kind of guidance file.** Three writers append to
`instructions[]`: the role charter, the seeded skills, and the IDE rules. All three now use
Jackson's `withArray` (get-or-create). One of them used `putArray` (create-or-**replace**), which
was safe only because it happened to run first against an empty array — an ordering rule nothing
wrote down and nothing tested. Measured before the fix: switching the skills writer to `putArray`
left the whole suite green while silently deleting the charter entry, so an opencode member launched
with **no role contract at all**. If you add a writer, use `withArray`, and assert the array's
**contents** — a size assertion passes when `putArray` swaps two entries for two different ones.
**One thing to know for maintenance.** `core.excludesFile` is **single-valued**. The seeded paths
are hidden from `git status` by pointing that key at a fleetd-written file, scoped `--worktree` —
and a worktree-scoped value *replaces* the operator's global one rather than adding to it. The