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:
@@ -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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user