claude-code: set shared agent text at the controller's terminal, for now

Instructions, output styles, skills, commands and subagents for every node
or some reach every session as the mesh's own word, and the tools cannot
tell an agent from the operator; the operator decided they are set at the
terminal until a surface of their own updates them (hq issue 340). They
are settings keys, each text a list of lines; the home scope stays free.
This commit is contained in:
jochen
2026-10-09 09:41:52 +02:00
parent c613efe189
commit e139fb4dd9
9 changed files with 267 additions and 23 deletions
+40 -7
View File
@@ -32,6 +32,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path"
@@ -264,6 +265,15 @@ func knownKind(k string) bool {
// Problem says why an item cannot be registered, or "".
func (it Item) Problem() string {
if problem := it.shapeProblem(); problem != "" {
return problem
}
return runsProblem(it)
}
// shapeProblem says why an item is not well formed, or "": its kind, scope, name, paths, files and size, without
// judging what its text asks of a session — which the operator's own text, set at the terminal, may.
func (it Item) shapeProblem() string {
if !knownKind(it.Kind) {
return fmt.Sprintf("%q is not a kind: %s", it.Kind, strings.Join(Kinds, ", "))
}
@@ -299,7 +309,7 @@ func (it Item) Problem() string {
if n := it.size(); n > MaxItemBytes {
return fmt.Sprintf("%d bytes: more than the %d one registration may carry", n, MaxItemBytes)
}
return runsProblem(it)
return ""
}
// pathProblem says why a file's path is not one inside the item, or "": relative, slash-separated, every
@@ -420,6 +430,9 @@ func (v *ConfigView) writeThroughLocked() bool {
// Config is what applies to this node, sorted for rendering.
type Config struct {
Mesh, Node, Home []Item
// Terminal is the shared text the operator set at the controller's terminal, in this module's settings
// (novox/hq issue 340): laid over what was registered for the mesh and the node, for every session here.
Terminal []Item
}
// ConfigOf sorts the items that apply here by scope, each scope by kind and name.
@@ -498,7 +511,7 @@ func MarketplaceWith(c Config, guard *GuardFiles) map[string]PluginFile {
})},
}
chosen := map[string]Item{}
for _, layer := range [][]Item{c.Mesh, c.Node} {
for _, layer := range [][]Item{c.Mesh, c.Node, c.Terminal} {
for _, it := range layer {
chosen[it.Kind+"/"+it.Name] = it
}
@@ -593,10 +606,12 @@ func mergeSettings(a, b map[string]any) map[string]any {
// registered for the mesh, then those for this node.
func InstructionSections(c Config) string {
var b strings.Builder
registered := "Registered through the `claude-code` module before that moved to the controller's terminal; change them there, in the module's `instructions` setting."
for _, part := range []struct {
title string
items []Item
}{{"Instructions for every node", c.Mesh}, {"Instructions for this node", c.Node}} {
title, note string
items []Item
}{{"Instructions for every node", registered, c.Mesh}, {"Instructions for this node", registered, c.Node},
{"Instructions set at the controller's terminal", "Set in the `claude-code` module's `instructions` setting, at the controller's terminal.", c.Terminal}} {
var sections []Item
for _, it := range part.items {
if it.Kind == KindInstructions {
@@ -606,7 +621,7 @@ func InstructionSections(c Config) string {
if len(sections) == 0 {
continue
}
fmt.Fprintf(&b, "\n## %s\n\nRegistered through the `claude-code` module's instruction tools; change them there.\n", part.title)
fmt.Fprintf(&b, "\n## %s\n\n%s\n", part.title, part.note)
for _, it := range sections {
fmt.Fprintf(&b, "\n### %s\n\n%s\n", it.Name, strings.TrimSpace(it.Files[it.Name+".md"]))
}
@@ -807,7 +822,7 @@ func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
var placed Placed
_ = readJSON(p.placed(), &placed)
inPlugin := map[string]bool{}
for _, layer := range [][]Item{c.Mesh, c.Node} {
for _, layer := range [][]Item{c.Mesh, c.Node, c.Terminal} {
for _, it := range layer {
inPlugin[it.Kind+"/"+it.Name] = true
}
@@ -910,10 +925,28 @@ type ConfigState interface {
Get(key string) (json.RawMessage, bool, error)
}
// SharedTextAtTheTerminal is why text for every node or some is not registered through this module (novox/hq issue
// 340, the operator's decision of 2026-10-09): an instruction section, an output style, a skill, a command or a
// subagent at the mesh or node scope reaches every session on those nodes as the mesh's own word, and the tool
// runner cannot tell an agent from the operator. Until a surface of the operator's own updates it, it is set at
// the controller's terminal, in this module's settings; a person's own home stays theirs to register into.
func SharedTextAtTheTerminal(kind string) string {
key := map[string]string{KindSkill: "skills", KindAgent: "agents", KindCommand: "commands",
KindOutputStyle: "output_styles", KindInstructions: "instructions"}[kind]
return "a " + kind + " for every node or some reaches every session there as the mesh's own word, and this tool " +
"cannot tell an agent from the operator. It is set at the controller's terminal only, under `" + key +
"` in this module's settings, its text as a list of lines: mesh-controller settings set claude-code " +
"'{\"" + key + "\": {\"<name>\": [\"line\", …]}}' [--node <node>] (novox/hq issue 340). The home scope, " +
"one account's own directory, is still registered here. Nothing was registered"
}
// Register puts an item (or, with unregister, removes it) at its scope: at the mesh scope one key, at the
// node and home scopes one key per node — this node when none is given. Taken into this node's view at once,
// so the answer says what it did here.
func Register(p Paths, it Item, nodes []string, unregister bool, state ConfigState, v *ConfigView, write WriteManaged) (map[string]any, error) {
if !unregister && it.Scope != ScopeHome && it.Kind != KindHook && it.Kind != KindSettings && knownKind(it.Kind) {
return nil, errors.New(SharedTextAtTheTerminal(it.Kind))
}
if !unregister {
it.By, it.At = p.Node, time.Now().UTC().Format(time.RFC3339)
if problem := it.Problem(); problem != "" {