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.
+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
|
||||
|
||||
Reference in New Issue
Block a user