diff --git a/11-Features.md b/11-Features.md index 5d46580..9ab9248 100644 --- a/11-Features.md +++ b/11-Features.md @@ -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