package main // The shared text the operator sets at the controller's terminal (novox/hq issue 340, the operator's decision of // 2026-10-09): instruction sections, output styles, skills, commands and subagents for every node or some. They are // keys of this module's settings, which the controller takes at its terminal alone. A setting holds no line break // (issue 339), so each text is a list of its lines; a one-line text may be a string. import ( "encoding/json" "errors" "os" "sort" "strings" ) // Lines is a text as a setting holds it: a list of its lines, or one line as a string. type Lines string // UnmarshalJSON takes a string or a list of strings, joined by line breaks. func (l *Lines) UnmarshalJSON(raw []byte) error { var one string if json.Unmarshal(raw, &one) == nil { *l = Lines(one) return nil } var many []string if err := json.Unmarshal(raw, &many); err != nil { return errors.New("a text is a list of its lines, or one line") } *l = Lines(strings.Join(many, "\n") + "\n") return nil } // SharedText is the text keys of this module's settings. type SharedText struct { Instructions map[string]Lines `json:"instructions"` OutputStyles map[string]Lines `json:"output_styles"` Agents map[string]Lines `json:"agents"` Commands map[string]Lines `json:"commands"` Skills map[string]map[string]Lines `json:"skills"` } // TerminalItems are the items the operator's settings hold, as the plugin and the instruction file take them, and // what was left out and why: an item that is not well formed (a name, a path, a skill without its SKILL.md). What // the text asks of a session is the operator's to say there, so it is not judged. func TerminalItems(text SharedText) ([]Item, []string) { var out []Item var left []string add := func(it Item) { it.Scope, it.By = ScopeMesh, "the controller's terminal" if problem := it.shapeProblem(); problem != "" { left = append(left, it.Kind+" "+it.Name+": "+problem) return } out = append(out, it) } for kind, texts := range map[string]map[string]Lines{KindInstructions: text.Instructions, KindOutputStyle: text.OutputStyles, KindAgent: text.Agents, KindCommand: text.Commands} { for name, body := range texts { add(Item{Kind: kind, Name: name, Files: map[string]string{name + ".md": string(body)}}) } } for name, files := range text.Skills { it := Item{Kind: KindSkill, Name: name, Files: map[string]string{}} for rel, body := range files { it.Files[rel] = string(body) } add(it) } sort.Slice(out, func(i, j int) bool { if out[i].Kind != out[j].Kind { return out[i].Kind < out[j].Kind } return out[i].Name < out[j].Name }) sort.Strings(left) return out, left } // terminalText reads the shared text from this module's settings file. func terminalText(p Paths) ([]Item, []string) { var text SharedText if raw, err := os.ReadFile(p.Settings); err == nil { if err := json.Unmarshal(raw, &text); err != nil { return nil, []string{"the shared text in this module's settings could not be read: " + err.Error()} } } return TerminalItems(text) }