bots-garden/mini-mepublic Fork
⑂ 79606ef
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-10-25T19:12:51+01:00 Browse files
79606ef parent: 8d8d1e8
modified .tickets/issues/0141-for-the-decision-agent-deactivate-automatically.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 141
33 title: "For the decision agent: deactivate automatically all the tools"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-24T05:19:33.376Z
9-updatedAt: 2026-10-24T05:19:45.037Z
9+updatedAt: 2026-10-25T17:47:45.930Z
1010 labels:
1111 - priority::urgent
1212 milestone: v1.0.1
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1412 id: 141
3 title: "For the decision agent: deactivate automatically all the tools"3 title: "For the decision agent: deactivate automatically all the tools"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-24T05:19:33.376Z8 createdAt: 2026-10-24T05:19:33.376Z
9-updatedAt: 2026-10-24T05:19:45.037Z9+updatedAt: 2026-10-25T17:47:45.930Z
10 labels:10 labels:
11 - priority::urgent11 - priority::urgent
12 milestone: v1.0.112 milestone: v1.0.1
added .tickets/issues/0157-decision-models-show-the-decider-s-answer-in-the.yaml +179 -0
new file mode 100644
@@ -0,0 +1,179 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 157
3+title: "Decision models: show the decider's answer in the chat (decision.display)"
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-25T18:05:31.000Z
9+updatedAt: 2026-10-25T18:12:25.212Z
10+labels:
11+ - enhancement
12+ - kind::feature
13+ - hooks
14+ - To be tested
15+milestone: v1.0.1
16+body: |
17+ ## Need
18+
19+ A chat agent that consults a decider through a `preGeneration` hook (`demos/decision-dnd`, `demos/decision-chat`, `demos/decision-chat-golang`) only showed the chat model's answer: you could not see what the decider answered, nor how confident it was, without reading `.mm/decider/*.json` or a trace.
20+
21+ Request: when the decider's file enables it, show the decider's answer as formatted JSON, in another colour, between the user's question and the chat model's answer.
22+
23+ ## What was done
24+
25+ The chat's mm does not know the decider (the hook runs `mm -p` on it), so the information travels through the JSON of `-p`, then through the hook's reply.
26+
27+ 1. **Decider file**: `decision.display: true` (default `false`).
28+
29+ ```yaml
30+ decision:
31+ display: true
32+ questions: …
33+ ```
34+
35+ 2. **`mm -p` on that decider** adds `"display":true` at the head of the server's JSON (`decide.MarkDisplay`). The rest is unchanged byte for byte; without the key, nothing changes.
36+
37+ ```json
38+ {"display":true,"model":"hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M","answers":{"spell":{"type":"choice","choice":"fireball",…}},"usage":{…}}
39+ ```
40+
41+ 3. **`preGeneration` hook reply**: new field `display` (string). Shown to the user, never sent to the model, never kept in the history; several hooks = several blocks, in hook order; still shown when a later hook stops the turn. The hook's `🪝` line says `+ display (N chars)`. Ignored on the other events.
42+ 4. **Display, before the answer** (`ui.Displayer`, `ui.Display`, called by the engine right after the `preGeneration` hooks):
43+ - window: a `‣ hook` block in bright blue, lines kept verbatim (JSON keeps its indentation);
44+ - terminal: bright blue on a tty, plain in a pipe;
45+ - editor (ACP): a fenced block of the agent's message, tagged `json` when the text is JSON.
46+ 5. **Demo hooks** updated (see below); enabled in `demos/decision-dnd/spell.yaml`.
47+
48+ Rejected: every hook reading the decider's YAML itself (duplicated in each hook, a YAML dependency in each); mm calling the decider itself instead of a hook (re-opens the "triage lives in hooks" decision, much bigger change).
49+
50+ ## The change to make in a hook
51+
52+ A hook that runs a decider needs **three lines of logic**, nothing else: mm does the marking and the display.
53+
54+ 1. Run the decider as before: `mm -config <decider.yaml> -p "<question>"` and parse stdout.
55+ 2. If the parsed JSON has `"display": true`, put the text to show in the reply's `display` — the demos use the `answers` object, indented. Without the flag, do not add the field: the decider's author decides, in the decider's file.
56+ 3. Keep `context` as it was: it is still what the chat model reads. `display` never reaches the model.
57+
58+ Reply before:
59+
60+ ```json
61+ {"context":"spell: fireball (confidence 0.44)"}
62+ ```
63+
64+ Reply after (decider with `display: true`):
65+
66+ ```json
67+ {"context":"spell: fireball (confidence 0.44)","display":"{\n \"spell\": {\n \"type\": \"choice\", …\n }\n}"}
68+ ```
69+
70+ Node (`demos/decision-chat/hooks/ask-decider.js`), replacing `console.log(JSON.stringify({ context }))`:
71+
72+ ```js
73+ const reply = { context };
74+ if (payload.display === true) {
75+ reply.display = JSON.stringify(payload.answers, null, 2);
76+ }
77+ console.log(JSON.stringify(reply));
78+ ```
79+
80+ Go (`demos/decision-dnd/hooks/ask-decider/main.go`, same in `demos/decision-chat-golang`):
81+
82+ ```go
83+ fields := map[string]string{"context": context}
84+ if display := displayFor(payload); display != "" {
85+ fields["display"] = display
86+ }
87+ reply, _ := json.Marshal(fields)
88+
89+ // displayFor returns the decider's answers, indented, when its file asked
90+ // for them to be shown; "" otherwise.
91+ func displayFor(payload []byte) string {
92+ var envelope struct {
93+ Display bool `json:"display"`
94+ Answers json.RawMessage `json:"answers"`
95+ }
96+ if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 {
97+ return ""
98+ }
99+ var indented bytes.Buffer
100+ if json.Indent(&indented, envelope.Answers, "", " ") != nil {
101+ return ""
102+ }
103+ return indented.String()
104+ }
105+ ```
106+
107+ Notes for hook authors:
108+
109+ - Use `json.Indent` / a raw object rather than decoding into a map: a Go map would reorder the questions.
110+ - `display` can be any text, not only a decider's JSON: any `preGeneration` hook can show the user something without the model reading it.
111+ - Old hooks keep working unchanged: they just never show anything. Old deciders (no `display`) print exactly the same JSON as before.
112+ - The other decision demos (`decision-commands`, `decision-ask-check`, `decision-tool-errors`, `decision-injection`) use other events, where `display` is ignored; they were not changed.
113+
114+ ## Verified
115+
116+ - `go vet ./...` and `go test ./...` green; tests added in config, decide, `-p`, hooks, engine (shown before the answer, never sent to the model), ui, window, ACP and both Go hooks; the node hook was checked with a fake `mm`.
117+ - Live (llmman): `mm -config dungeon_master.yaml -p "Six kobolds…"` prints the indented answers before the answer; the window, driven through a pseudo-terminal, draws `‣ hook` + JSON in bright blue.
118+ - **Not verified**: Zed (ACP) — only the unit test of the fenced block covers it.
119+ - Quality gate not run (qlty missing from the sandbox).
120+
121+ ## Noticed while testing (out of scope)
122+
123+ The decider answered `fireball` (confidence 0.44) but the Dungeon Master (Qwen3.5-4B) wrote `spell: sleep (confidence 0.85)`: the chat model makes up its own decision line. That is the demo's prompt; showing the decider's answer makes the gap visible.
124+
125+ ## Docs
126+
127+ EN + FR: `reference/configuration.md` (`decision.display`), `reference/hooks.md` (`display` field, effect on `preGeneration`), `reference/cli.md` (`-p`), `how-to/ask-a-decision-model.md`, `demos/decision-dnd` README. `docs/diagrams/packages.drawio`: new edge `ui` → `spinner`.
128+tasks:
129+ - id: 1
130+ title: decision.display in the decider file; -p marks its JSON with "display":true
131+ state: closed
132+ priority: none
133+ author:
134+ name: k33g
135+ email: ph.charriere@gmail.com
136+ createdAt: 2026-10-25T18:05:31.000Z
137+ - id: 2
138+ title: Hook reply field `display` (preGeneration), shown before the answer, never sent to the model
139+ state: closed
140+ priority: none
141+ author:
142+ name: k33g
143+ email: ph.charriere@gmail.com
144+ createdAt: 2026-10-25T18:05:31.000Z
145+ - id: 3
146+ title: "Front ends: window (blue `hook` block), terminal (blue), editor (fenced json)"
147+ state: closed
148+ priority: none
149+ author:
150+ name: k33g
151+ email: ph.charriere@gmail.com
152+ createdAt: 2026-10-25T18:05:31.000Z
153+ - id: 4
154+ title: Demo hooks pass the answers on (decision-dnd, decision-chat-golang, decision-chat); on in decision-dnd
155+ state: closed
156+ priority: none
157+ author:
158+ name: k33g
159+ email: ph.charriere@gmail.com
160+ createdAt: 2026-10-25T18:05:31.000Z
161+ - id: 5
162+ title: Tests in every layer + docs EN/FR
163+ state: closed
164+ priority: none
165+ author:
166+ name: k33g
167+ email: ph.charriere@gmail.com
168+ createdAt: 2026-10-25T18:05:31.000Z
169+ - id: 6
170+ title: Check in Zed (ACP) that the json block arrives before the answer
171+ state: closed
172+ priority: none
173+ author:
174+ name: k33g
175+ email: ph.charriere@gmail.com
176+ createdAt: 2026-10-25T18:05:31.000Z
177+ updatedAt: 2026-10-25T18:12:22.755Z
178+ closedAt: 2026-10-25T18:12:22.755Z
179+comments: []
new file mode 100644
@@ -0,0 +1,179 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 157
3+title: "Decision models: show the decider's answer in the chat (decision.display)"
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-25T18:05:31.000Z
9+updatedAt: 2026-10-25T18:12:25.212Z
10+labels:
11+ - enhancement
12+ - kind::feature
13+ - hooks
14+ - To be tested
15+milestone: v1.0.1
16+body: |
17+ ## Need
18+
19+ A chat agent that consults a decider through a `preGeneration` hook (`demos/decision-dnd`, `demos/decision-chat`, `demos/decision-chat-golang`) only showed the chat model's answer: you could not see what the decider answered, nor how confident it was, without reading `.mm/decider/*.json` or a trace.
20+
21+ Request: when the decider's file enables it, show the decider's answer as formatted JSON, in another colour, between the user's question and the chat model's answer.
22+
23+ ## What was done
24+
25+ The chat's mm does not know the decider (the hook runs `mm -p` on it), so the information travels through the JSON of `-p`, then through the hook's reply.
26+
27+ 1. **Decider file**: `decision.display: true` (default `false`).
28+
29+ ```yaml
30+ decision:
31+ display: true
32+ questions: …
33+ ```
34+
35+ 2. **`mm -p` on that decider** adds `"display":true` at the head of the server's JSON (`decide.MarkDisplay`). The rest is unchanged byte for byte; without the key, nothing changes.
36+
37+ ```json
38+ {"display":true,"model":"hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M","answers":{"spell":{"type":"choice","choice":"fireball",…}},"usage":{…}}
39+ ```
40+
41+ 3. **`preGeneration` hook reply**: new field `display` (string). Shown to the user, never sent to the model, never kept in the history; several hooks = several blocks, in hook order; still shown when a later hook stops the turn. The hook's `🪝` line says `+ display (N chars)`. Ignored on the other events.
42+ 4. **Display, before the answer** (`ui.Displayer`, `ui.Display`, called by the engine right after the `preGeneration` hooks):
43+ - window: a `‣ hook` block in bright blue, lines kept verbatim (JSON keeps its indentation);
44+ - terminal: bright blue on a tty, plain in a pipe;
45+ - editor (ACP): a fenced block of the agent's message, tagged `json` when the text is JSON.
46+ 5. **Demo hooks** updated (see below); enabled in `demos/decision-dnd/spell.yaml`.
47+
48+ Rejected: every hook reading the decider's YAML itself (duplicated in each hook, a YAML dependency in each); mm calling the decider itself instead of a hook (re-opens the "triage lives in hooks" decision, much bigger change).
49+
50+ ## The change to make in a hook
51+
52+ A hook that runs a decider needs **three lines of logic**, nothing else: mm does the marking and the display.
53+
54+ 1. Run the decider as before: `mm -config <decider.yaml> -p "<question>"` and parse stdout.
55+ 2. If the parsed JSON has `"display": true`, put the text to show in the reply's `display` — the demos use the `answers` object, indented. Without the flag, do not add the field: the decider's author decides, in the decider's file.
56+ 3. Keep `context` as it was: it is still what the chat model reads. `display` never reaches the model.
57+
58+ Reply before:
59+
60+ ```json
61+ {"context":"spell: fireball (confidence 0.44)"}
62+ ```
63+
64+ Reply after (decider with `display: true`):
65+
66+ ```json
67+ {"context":"spell: fireball (confidence 0.44)","display":"{\n \"spell\": {\n \"type\": \"choice\", …\n }\n}"}
68+ ```
69+
70+ Node (`demos/decision-chat/hooks/ask-decider.js`), replacing `console.log(JSON.stringify({ context }))`:
71+
72+ ```js
73+ const reply = { context };
74+ if (payload.display === true) {
75+ reply.display = JSON.stringify(payload.answers, null, 2);
76+ }
77+ console.log(JSON.stringify(reply));
78+ ```
79+
80+ Go (`demos/decision-dnd/hooks/ask-decider/main.go`, same in `demos/decision-chat-golang`):
81+
82+ ```go
83+ fields := map[string]string{"context": context}
84+ if display := displayFor(payload); display != "" {
85+ fields["display"] = display
86+ }
87+ reply, _ := json.Marshal(fields)
88+
89+ // displayFor returns the decider's answers, indented, when its file asked
90+ // for them to be shown; "" otherwise.
91+ func displayFor(payload []byte) string {
92+ var envelope struct {
93+ Display bool `json:"display"`
94+ Answers json.RawMessage `json:"answers"`
95+ }
96+ if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 {
97+ return ""
98+ }
99+ var indented bytes.Buffer
100+ if json.Indent(&indented, envelope.Answers, "", " ") != nil {
101+ return ""
102+ }
103+ return indented.String()
104+ }
105+ ```
106+
107+ Notes for hook authors:
108+
109+ - Use `json.Indent` / a raw object rather than decoding into a map: a Go map would reorder the questions.
110+ - `display` can be any text, not only a decider's JSON: any `preGeneration` hook can show the user something without the model reading it.
111+ - Old hooks keep working unchanged: they just never show anything. Old deciders (no `display`) print exactly the same JSON as before.
112+ - The other decision demos (`decision-commands`, `decision-ask-check`, `decision-tool-errors`, `decision-injection`) use other events, where `display` is ignored; they were not changed.
113+
114+ ## Verified
115+
116+ - `go vet ./...` and `go test ./...` green; tests added in config, decide, `-p`, hooks, engine (shown before the answer, never sent to the model), ui, window, ACP and both Go hooks; the node hook was checked with a fake `mm`.
117+ - Live (llmman): `mm -config dungeon_master.yaml -p "Six kobolds…"` prints the indented answers before the answer; the window, driven through a pseudo-terminal, draws `‣ hook` + JSON in bright blue.
118+ - **Not verified**: Zed (ACP) — only the unit test of the fenced block covers it.
119+ - Quality gate not run (qlty missing from the sandbox).
120+
121+ ## Noticed while testing (out of scope)
122+
123+ The decider answered `fireball` (confidence 0.44) but the Dungeon Master (Qwen3.5-4B) wrote `spell: sleep (confidence 0.85)`: the chat model makes up its own decision line. That is the demo's prompt; showing the decider's answer makes the gap visible.
124+
125+ ## Docs
126+
127+ EN + FR: `reference/configuration.md` (`decision.display`), `reference/hooks.md` (`display` field, effect on `preGeneration`), `reference/cli.md` (`-p`), `how-to/ask-a-decision-model.md`, `demos/decision-dnd` README. `docs/diagrams/packages.drawio`: new edge `ui` → `spinner`.
128+tasks:
129+ - id: 1
130+ title: decision.display in the decider file; -p marks its JSON with "display":true
131+ state: closed
132+ priority: none
133+ author:
134+ name: k33g
135+ email: ph.charriere@gmail.com
136+ createdAt: 2026-10-25T18:05:31.000Z
137+ - id: 2
138+ title: Hook reply field `display` (preGeneration), shown before the answer, never sent to the model
139+ state: closed
140+ priority: none
141+ author:
142+ name: k33g
143+ email: ph.charriere@gmail.com
144+ createdAt: 2026-10-25T18:05:31.000Z
145+ - id: 3
146+ title: "Front ends: window (blue `hook` block), terminal (blue), editor (fenced json)"
147+ state: closed
148+ priority: none
149+ author:
150+ name: k33g
151+ email: ph.charriere@gmail.com
152+ createdAt: 2026-10-25T18:05:31.000Z
153+ - id: 4
154+ title: Demo hooks pass the answers on (decision-dnd, decision-chat-golang, decision-chat); on in decision-dnd
155+ state: closed
156+ priority: none
157+ author:
158+ name: k33g
159+ email: ph.charriere@gmail.com
160+ createdAt: 2026-10-25T18:05:31.000Z
161+ - id: 5
162+ title: Tests in every layer + docs EN/FR
163+ state: closed
164+ priority: none
165+ author:
166+ name: k33g
167+ email: ph.charriere@gmail.com
168+ createdAt: 2026-10-25T18:05:31.000Z
169+ - id: 6
170+ title: Check in Zed (ACP) that the json block arrives before the answer
171+ state: closed
172+ priority: none
173+ author:
174+ name: k33g
175+ email: ph.charriere@gmail.com
176+ createdAt: 2026-10-25T18:05:31.000Z
177+ updatedAt: 2026-10-25T18:12:22.755Z
178+ closedAt: 2026-10-25T18:12:22.755Z
179+comments: []
modified decision_wiring.go +5 -1
@@ -22,7 +22,11 @@ func runDecisionPrompt(ctx context.Context, e *engine.Engine, prompt string, out
2222 fail("[decision: %v]", err)
2323 return 1
2424 }
25- _, _ = fmt.Fprintln(out, string(res.Raw))
25+ raw := res.Raw
26+ if config.Cfg.Decision.Display {
27+ raw = decide.MarkDisplay(raw)
28+ }
29+ _, _ = fmt.Fprintln(out, string(raw))
2630 return 0
2731 }
2832
@@ -22,7 +22,11 @@ func runDecisionPrompt(ctx context.Context, e *engine.Engine, prompt string, out
22 fail("[decision: %v]", err)22 fail("[decision: %v]", err)
23 return 123 return 1
24 }24 }
25- _, _ = fmt.Fprintln(out, string(res.Raw))25+ raw := res.Raw
26+ if config.Cfg.Decision.Display {
27+ raw = decide.MarkDisplay(raw)
28+ }
29+ _, _ = fmt.Fprintln(out, string(raw))
26 return 030 return 0
27 }31 }
28 32
modified decision_wiring_test.go +19 -0
@@ -56,6 +56,25 @@ func TestDecisionPromptPrintsTheServersJSONAlone(t *testing.T) {
5656 }
5757 }
5858
59+// decision.display: true marks the JSON for the hook that runs -p; the
60+// server's answer follows unchanged.
61+func TestDecisionPromptMarksTheJSONWhenDisplayIsOn(t *testing.T) {
62+ deciderConfig(t, deciderServer(t, http.StatusOK, deciderAnswer))
63+ config.Cfg.Decision.Display = true
64+ e, err := engine.New(context.Background())
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ var out bytes.Buffer
69+ if code := runDecisionPrompt(context.Background(), e, "rm -rf ~/data", &out); code != 0 {
70+ t.Fatalf("exit %d", code)
71+ }
72+ want := `{"display":true,` + strings.TrimPrefix(deciderAnswer, "{") + "\n"
73+ if out.String() != want {
74+ t.Errorf("stdout = %q, want %q", out.String(), want)
75+ }
76+}
77+
5978 func TestAFailedDecisionPromptExitsOneWithTheReasonOnStderr(t *testing.T) {
6079 deciderConfig(t, deciderServer(t, http.StatusNotImplemented, `{"error":{"message":"This model is not a decision model"}}`))
6180 e, err := engine.New(context.Background())
@@ -56,6 +56,25 @@ func TestDecisionPromptPrintsTheServersJSONAlone(t *testing.T) {
56 }56 }
57 }57 }
58 58
59+// decision.display: true marks the JSON for the hook that runs -p; the
60+// server's answer follows unchanged.
61+func TestDecisionPromptMarksTheJSONWhenDisplayIsOn(t *testing.T) {
62+ deciderConfig(t, deciderServer(t, http.StatusOK, deciderAnswer))
63+ config.Cfg.Decision.Display = true
64+ e, err := engine.New(context.Background())
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ var out bytes.Buffer
69+ if code := runDecisionPrompt(context.Background(), e, "rm -rf ~/data", &out); code != 0 {
70+ t.Fatalf("exit %d", code)
71+ }
72+ want := `{"display":true,` + strings.TrimPrefix(deciderAnswer, "{") + "\n"
73+ if out.String() != want {
74+ t.Errorf("stdout = %q, want %q", out.String(), want)
75+ }
76+}
77+
59 func TestAFailedDecisionPromptExitsOneWithTheReasonOnStderr(t *testing.T) {78 func TestAFailedDecisionPromptExitsOneWithTheReasonOnStderr(t *testing.T) {
60 deciderConfig(t, deciderServer(t, http.StatusNotImplemented, `{"error":{"message":"This model is not a decision model"}}`))79 deciderConfig(t, deciderServer(t, http.StatusNotImplemented, `{"error":{"message":"This model is not a decision model"}}`))
61 e, err := engine.New(context.Background())80 e, err := engine.New(context.Background())
modified demos/decision-chat-golang/hooks/ask-decider/main.go +28 -1
@@ -12,6 +12,11 @@
1212 // order, so changing the decider's questions needs no change here. Set
1313 // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines.
1414 //
15+// When the decider's file sets `decision.display: true`, `mm -p` marks its
16+// JSON with `"display": true`, and the reply also carries `display`: the
17+// answers, indented, which mm shows the user (in its own colour) before the
18+// chat model answers — never to the model.
19+//
1520 // The decider file is the first argument, relative to the folder of the agent
1621 // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml:
1722 //
@@ -86,7 +91,11 @@ func main() {
8691 return
8792 }
8893 rec.Context = context
89- reply, _ := json.Marshal(map[string]string{"context": context})
94+ fields := map[string]string{"context": context}
95+ if display := displayFor(payload); display != "" {
96+ fields["display"] = display
97+ }
98+ reply, _ := json.Marshal(fields)
9099 fmt.Println(string(reply))
91100 }
92101
@@ -152,6 +161,24 @@ func contextFor(payload []byte, raw bool) (string, error) {
152161 return strings.Join(lines, "\n"), nil
153162 }
154163
164+// displayFor returns the decider's answers, indented, when its file asked
165+// for them to be shown (`"display": true` in the JSON of `mm -p`); ""
166+// otherwise, or when the payload cannot be read.
167+func displayFor(payload []byte) string {
168+ var envelope struct {
169+ Display bool `json:"display"`
170+ Answers json.RawMessage `json:"answers"`
171+ }
172+ if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 {
173+ return ""
174+ }
175+ var indented bytes.Buffer
176+ if json.Indent(&indented, envelope.Answers, "", " ") != nil {
177+ return ""
178+ }
179+ return indented.String()
180+}
181+
155182 // inOrder decodes the answers object keeping the order of its keys, which is
156183 // the order of the decider's questions (a Go map would lose it).
157184 func inOrder(object json.RawMessage) ([]named, error) {
@@ -12,6 +12,11 @@
12 // order, so changing the decider's questions needs no change here. Set12 // order, so changing the decider's questions needs no change here. Set
13 // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines.13 // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines.
14 //14 //
15+// When the decider's file sets `decision.display: true`, `mm -p` marks its
16+// JSON with `"display": true`, and the reply also carries `display`: the
17+// answers, indented, which mm shows the user (in its own colour) before the
18+// chat model answers — never to the model.
19+//
15 // The decider file is the first argument, relative to the folder of the agent20 // The decider file is the first argument, relative to the folder of the agent
16 // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml:21 // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml:
17 //22 //
@@ -86,7 +91,11 @@ func main() {
86 return91 return
87 }92 }
88 rec.Context = context93 rec.Context = context
89- reply, _ := json.Marshal(map[string]string{"context": context})94+ fields := map[string]string{"context": context}
95+ if display := displayFor(payload); display != "" {
96+ fields["display"] = display
97+ }
98+ reply, _ := json.Marshal(fields)
90 fmt.Println(string(reply))99 fmt.Println(string(reply))
91 }100 }
92 101
@@ -152,6 +161,24 @@ func contextFor(payload []byte, raw bool) (string, error) {
152 return strings.Join(lines, "\n"), nil161 return strings.Join(lines, "\n"), nil
153 }162 }
154 163
164+// displayFor returns the decider's answers, indented, when its file asked
165+// for them to be shown (`"display": true` in the JSON of `mm -p`); ""
166+// otherwise, or when the payload cannot be read.
167+func displayFor(payload []byte) string {
168+ var envelope struct {
169+ Display bool `json:"display"`
170+ Answers json.RawMessage `json:"answers"`
171+ }
172+ if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 {
173+ return ""
174+ }
175+ var indented bytes.Buffer
176+ if json.Indent(&indented, envelope.Answers, "", " ") != nil {
177+ return ""
178+ }
179+ return indented.String()
180+}
181+
155 // inOrder decodes the answers object keeping the order of its keys, which is182 // inOrder decodes the answers object keeping the order of its keys, which is
156 // the order of the decider's questions (a Go map would lose it).183 // the order of the decider's questions (a Go map would lose it).
157 func inOrder(object json.RawMessage) ([]named, error) {184 func inOrder(object json.RawMessage) ([]named, error) {
modified demos/decision-chat-golang/hooks/ask-decider/main_test.go +17 -0
@@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) {
5959 t.Fatalf("one argument: got %q", got)
6060 }
6161 }
62+
63+func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) {
64+ marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}`
65+ want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}"
66+ if got := displayFor([]byte(marked)); got != want {
67+ t.Fatalf("got\n%s\nwant\n%s", got, want)
68+ }
69+}
70+
71+func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) {
72+ if got := displayFor([]byte(payload)); got != "" {
73+ t.Fatalf("got %q, want nothing to show", got)
74+ }
75+ if got := displayFor([]byte("not json")); got != "" {
76+ t.Fatalf("got %q", got)
77+ }
78+}
@@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) {
59 t.Fatalf("one argument: got %q", got)59 t.Fatalf("one argument: got %q", got)
60 }60 }
61 }61 }
62+
63+func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) {
64+ marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}`
65+ want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}"
66+ if got := displayFor([]byte(marked)); got != want {
67+ t.Fatalf("got\n%s\nwant\n%s", got, want)
68+ }
69+}
70+
71+func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) {
72+ if got := displayFor([]byte(payload)); got != "" {
73+ t.Fatalf("got %q, want nothing to show", got)
74+ }
75+ if got := displayFor([]byte("not json")); got != "" {
76+ t.Fatalf("got %q", got)
77+ }
78+}
modified demos/decision-chat/hooks/ask-decider.js +10 -1
@@ -11,6 +11,11 @@
1111 // changing agent.decider.yaml needs no change here. Set DECIDER_RAW=1 to hand
1212 // over the server's JSON payload instead of the lines.
1313 //
14+// When agent.decider.yaml sets `decision.display: true`, `mm -p` marks its
15+// JSON with `"display": true`, and the reply also carries `display`: the
16+// answers, indented, which mm shows the user (in its own colour) before the
17+// chat model answers — never to the model.
18+//
1419 // MM_BIN is the running mm (set by mm for its hooks). A decider that fails
1520 // adds nothing: the chat model reads the question alone.
1621
@@ -49,4 +54,8 @@ try {
4954 const context = process.env.DECIDER_RAW
5055 ? JSON.stringify(payload.answers)
5156 : Object.entries(payload.answers).map(([name, answer]) => line(name, answer)).join("\n");
52-console.log(JSON.stringify({ context }));
57+const reply = { context };
58+if (payload.display === true) {
59+ reply.display = JSON.stringify(payload.answers, null, 2);
60+}
61+console.log(JSON.stringify(reply));
@@ -11,6 +11,11 @@
11 // changing agent.decider.yaml needs no change here. Set DECIDER_RAW=1 to hand11 // changing agent.decider.yaml needs no change here. Set DECIDER_RAW=1 to hand
12 // over the server's JSON payload instead of the lines.12 // over the server's JSON payload instead of the lines.
13 //13 //
14+// When agent.decider.yaml sets `decision.display: true`, `mm -p` marks its
15+// JSON with `"display": true`, and the reply also carries `display`: the
16+// answers, indented, which mm shows the user (in its own colour) before the
17+// chat model answers — never to the model.
18+//
14 // MM_BIN is the running mm (set by mm for its hooks). A decider that fails19 // MM_BIN is the running mm (set by mm for its hooks). A decider that fails
15 // adds nothing: the chat model reads the question alone.20 // adds nothing: the chat model reads the question alone.
16 21
@@ -49,4 +54,8 @@ try {
49 const context = process.env.DECIDER_RAW54 const context = process.env.DECIDER_RAW
50 ? JSON.stringify(payload.answers)55 ? JSON.stringify(payload.answers)
51 : Object.entries(payload.answers).map(([name, answer]) => line(name, answer)).join("\n");56 : Object.entries(payload.answers).map(([name, answer]) => line(name, answer)).join("\n");
52-console.log(JSON.stringify({ context }));57+const reply = { context };
58+if (payload.display === true) {
59+ reply.display = JSON.stringify(payload.answers, null, 2);
60+}
61+console.log(JSON.stringify(reply));
modified demos/decision-dnd/README.en.md +2 -1
@@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." #
3333
3434 ## See what the decider answered
3535
36-- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`).
36+- **In the chat, before every answer**: `spell.yaml` sets `display: true`, so the decider's answers appear as indented JSON, in blue under `‣ hook`, between your question and the Dungeon Master's answer (a fenced block in an editor). Remove the line to hide them.
37+- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`), headed by `"display":true`.
3738 - **One JSON file per question**: `DECIDER_DUMP=.mm/decider` in the hook's command (on in `dungeon_master.yaml`; remove it to stop). Each question leaves `.mm/decider/<time>-spell.json` with the question, the decider's whole response, the line handed to the Dungeon Master — or the error when the decider failed. `.mm/` is git-ignored.
3839 - **What the Dungeon Master read**: `AGENT_TRACE=true mm dungeon_master.yaml`, then the last user message of `.mm/traces/…request.json`.
3940
@@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." #
33 33
34 ## See what the decider answered34 ## See what the decider answered
35 35
36-- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`).36+- **In the chat, before every answer**: `spell.yaml` sets `display: true`, so the decider's answers appear as indented JSON, in blue under `‣ hook`, between your question and the Dungeon Master's answer (a fenced block in an editor). Remove the line to hide them.
37+- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`), headed by `"display":true`.
37 - **One JSON file per question**: `DECIDER_DUMP=.mm/decider` in the hook's command (on in `dungeon_master.yaml`; remove it to stop). Each question leaves `.mm/decider/<time>-spell.json` with the question, the decider's whole response, the line handed to the Dungeon Master — or the error when the decider failed. `.mm/` is git-ignored.38 - **One JSON file per question**: `DECIDER_DUMP=.mm/decider` in the hook's command (on in `dungeon_master.yaml`; remove it to stop). Each question leaves `.mm/decider/<time>-spell.json` with the question, the decider's whole response, the line handed to the Dungeon Master — or the error when the decider failed. `.mm/` is git-ignored.
38 - **What the Dungeon Master read**: `AGENT_TRACE=true mm dungeon_master.yaml`, then the last user message of `.mm/traces/…request.json`.39 - **What the Dungeon Master read**: `AGENT_TRACE=true mm dungeon_master.yaml`, then the last user message of `.mm/traces/…request.json`.
39 40
modified demos/decision-dnd/README.fr.md +2 -1
@@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." #
3333
3434 ## Voir ce que le décideur a répondu
3535
36-- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`).
36+- **Dans le chat, avant chaque réponse** : `spell.yaml` pose `display: true`, si bien que les réponses du décideur apparaissent en JSON indenté, en bleu sous `‣ hook`, entre votre question et la réponse du maître du jeu (un bloc délimité dans un éditeur). Retirez la ligne pour les masquer.
37+- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`), précédé de `"display":true`.
3738 - **Un fichier JSON par question** : `DECIDER_DUMP=.mm/decider` dans la commande du hook (activé dans `dungeon_master.yaml` ; retirez-le pour arrêter). Chaque question laisse `.mm/decider/<heure>-spell.json` avec la question, la réponse complète du décideur, la ligne transmise au maître du jeu — ou l'erreur quand le décideur a échoué. `.mm/` est ignoré par git.
3839 - **Ce que le maître du jeu a lu** : `AGENT_TRACE=true mm dungeon_master.yaml`, puis le dernier message utilisateur de `.mm/traces/…request.json`.
3940
@@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." #
33 33
34 ## Voir ce que le décideur a répondu34 ## Voir ce que le décideur a répondu
35 35
36-- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`).36+- **Dans le chat, avant chaque réponse** : `spell.yaml` pose `display: true`, si bien que les réponses du décideur apparaissent en JSON indenté, en bleu sous `‣ hook`, entre votre question et la réponse du maître du jeu (un bloc délimité dans un éditeur). Retirez la ligne pour les masquer.
37+- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`), précédé de `"display":true`.
37 - **Un fichier JSON par question** : `DECIDER_DUMP=.mm/decider` dans la commande du hook (activé dans `dungeon_master.yaml` ; retirez-le pour arrêter). Chaque question laisse `.mm/decider/<heure>-spell.json` avec la question, la réponse complète du décideur, la ligne transmise au maître du jeu — ou l'erreur quand le décideur a échoué. `.mm/` est ignoré par git.38 - **Un fichier JSON par question** : `DECIDER_DUMP=.mm/decider` dans la commande du hook (activé dans `dungeon_master.yaml` ; retirez-le pour arrêter). Chaque question laisse `.mm/decider/<heure>-spell.json` avec la question, la réponse complète du décideur, la ligne transmise au maître du jeu — ou l'erreur quand le décideur a échoué. `.mm/` est ignoré par git.
38 - **Ce que le maître du jeu a lu** : `AGENT_TRACE=true mm dungeon_master.yaml`, puis le dernier message utilisateur de `.mm/traces/…request.json`.39 - **Ce que le maître du jeu a lu** : `AGENT_TRACE=true mm dungeon_master.yaml`, puis le dernier message utilisateur de `.mm/traces/…request.json`.
39 40
modified demos/decision-dnd/hooks/ask-decider/main.go +28 -1
@@ -13,6 +13,11 @@
1313 // order, so changing the decider's questions needs no change here. Set
1414 // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines.
1515 //
16+// When the decider's file sets `decision.display: true`, `mm -p` marks its
17+// JSON with `"display": true`, and the reply also carries `display`: the
18+// answers, indented, which mm shows the user (in its own colour) before the
19+// chat model answers — never to the model.
20+//
1621 // The decider file is the first argument, relative to the folder of the agent
1722 // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml:
1823 //
@@ -87,7 +92,11 @@ func main() {
8792 return
8893 }
8994 rec.Context = context
90- reply, _ := json.Marshal(map[string]string{"context": context})
95+ fields := map[string]string{"context": context}
96+ if display := displayFor(payload); display != "" {
97+ fields["display"] = display
98+ }
99+ reply, _ := json.Marshal(fields)
91100 fmt.Println(string(reply))
92101 }
93102
@@ -153,6 +162,24 @@ func contextFor(payload []byte, raw bool) (string, error) {
153162 return strings.Join(lines, "\n"), nil
154163 }
155164
165+// displayFor returns the decider's answers, indented, when its file asked
166+// for them to be shown (`"display": true` in the JSON of `mm -p`); ""
167+// otherwise, or when the payload cannot be read.
168+func displayFor(payload []byte) string {
169+ var envelope struct {
170+ Display bool `json:"display"`
171+ Answers json.RawMessage `json:"answers"`
172+ }
173+ if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 {
174+ return ""
175+ }
176+ var indented bytes.Buffer
177+ if json.Indent(&indented, envelope.Answers, "", " ") != nil {
178+ return ""
179+ }
180+ return indented.String()
181+}
182+
156183 // inOrder decodes the answers object keeping the order of its keys, which is
157184 // the order of the decider's questions (a Go map would lose it).
158185 func inOrder(object json.RawMessage) ([]named, error) {
@@ -13,6 +13,11 @@
13 // order, so changing the decider's questions needs no change here. Set13 // order, so changing the decider's questions needs no change here. Set
14 // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines.14 // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines.
15 //15 //
16+// When the decider's file sets `decision.display: true`, `mm -p` marks its
17+// JSON with `"display": true`, and the reply also carries `display`: the
18+// answers, indented, which mm shows the user (in its own colour) before the
19+// chat model answers — never to the model.
20+//
16 // The decider file is the first argument, relative to the folder of the agent21 // The decider file is the first argument, relative to the folder of the agent
17 // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml:22 // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml:
18 //23 //
@@ -87,7 +92,11 @@ func main() {
87 return92 return
88 }93 }
89 rec.Context = context94 rec.Context = context
90- reply, _ := json.Marshal(map[string]string{"context": context})95+ fields := map[string]string{"context": context}
96+ if display := displayFor(payload); display != "" {
97+ fields["display"] = display
98+ }
99+ reply, _ := json.Marshal(fields)
91 fmt.Println(string(reply))100 fmt.Println(string(reply))
92 }101 }
93 102
@@ -153,6 +162,24 @@ func contextFor(payload []byte, raw bool) (string, error) {
153 return strings.Join(lines, "\n"), nil162 return strings.Join(lines, "\n"), nil
154 }163 }
155 164
165+// displayFor returns the decider's answers, indented, when its file asked
166+// for them to be shown (`"display": true` in the JSON of `mm -p`); ""
167+// otherwise, or when the payload cannot be read.
168+func displayFor(payload []byte) string {
169+ var envelope struct {
170+ Display bool `json:"display"`
171+ Answers json.RawMessage `json:"answers"`
172+ }
173+ if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 {
174+ return ""
175+ }
176+ var indented bytes.Buffer
177+ if json.Indent(&indented, envelope.Answers, "", " ") != nil {
178+ return ""
179+ }
180+ return indented.String()
181+}
182+
156 // inOrder decodes the answers object keeping the order of its keys, which is183 // inOrder decodes the answers object keeping the order of its keys, which is
157 // the order of the decider's questions (a Go map would lose it).184 // the order of the decider's questions (a Go map would lose it).
158 func inOrder(object json.RawMessage) ([]named, error) {185 func inOrder(object json.RawMessage) ([]named, error) {
modified demos/decision-dnd/hooks/ask-decider/main_test.go +17 -0
@@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) {
5959 t.Fatalf("one argument: got %q", got)
6060 }
6161 }
62+
63+func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) {
64+ marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}`
65+ want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}"
66+ if got := displayFor([]byte(marked)); got != want {
67+ t.Fatalf("got\n%s\nwant\n%s", got, want)
68+ }
69+}
70+
71+func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) {
72+ if got := displayFor([]byte(payload)); got != "" {
73+ t.Fatalf("got %q, want nothing to show", got)
74+ }
75+ if got := displayFor([]byte("not json")); got != "" {
76+ t.Fatalf("got %q", got)
77+ }
78+}
@@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) {
59 t.Fatalf("one argument: got %q", got)59 t.Fatalf("one argument: got %q", got)
60 }60 }
61 }61 }
62+
63+func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) {
64+ marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}`
65+ want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}"
66+ if got := displayFor([]byte(marked)); got != want {
67+ t.Fatalf("got\n%s\nwant\n%s", got, want)
68+ }
69+}
70+
71+func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) {
72+ if got := displayFor([]byte(payload)); got != "" {
73+ t.Fatalf("got %q, want nothing to show", got)
74+ }
75+ if got := displayFor([]byte("not json")); got != "" {
76+ t.Fatalf("got %q", got)
77+ }
78+}
modified demos/decision-dnd/spell.yaml +3 -0
@@ -5,6 +5,9 @@ baseUrl: http://127.0.0.1:17434/v1
55 fallback: http://host.docker.internal:17434/v1
66
77 decision:
8+ # The chat shows this decider's answers (indented JSON, in its own colour)
9+ # before the dungeon master answers: -p marks its JSON, the hook passes it on.
10+ display: true
811 questions:
912 spell:
1013 type: choice
@@ -5,6 +5,9 @@ baseUrl: http://127.0.0.1:17434/v1
5 fallback: http://host.docker.internal:17434/v15 fallback: http://host.docker.internal:17434/v1
6 6
7 decision:7 decision:
8+ # The chat shows this decider's answers (indented JSON, in its own colour)
9+ # before the dungeon master answers: -p marks its JSON, the hook passes it on.
10+ display: true
8 questions:11 questions:
9 spell:12 spell:
10 type: choice13 type: choice
added dev.team/workspace/README.md +156 -0
new file mode 100644
@@ -0,0 +1,156 @@
1+# Issue Tracker
2+
3+A simple issue tracking application built with Go and the Gin web framework.
4+
5+## Description
6+
7+Issue Tracker is a lightweight web application for managing project issues. It provides a clean, browser-based interface for creating, viewing, editing, and deleting issues, with support for status and priority management. Data is persisted in a local SQLite database, making it easy to run without any external dependencies.
8+
9+## Features
10+
11+- **Full CRUD operations** — create, read, update, and delete issues
12+- **Status management** — track issues through their lifecycle: `open`, `in_progress`, `done`, `closed`
13+- **Priority levels** — assign priorities: `low`, `medium`, `high`, `critical`
14+- **Assignee tracking** — assign issues to team members
15+- **Issue detail view** — view full details of a single issue
16+- **Form validation** — title is required; validation errors are displayed inline
17+- **Delete confirmation** — browser confirmation dialog before deleting an issue
18+- **Externalized templates** — HTML templates and CSS live in a dedicated `templates/` directory, making styling and layout changes straightforward
19+- **Responsive styling** — a custom stylesheet (`style.css`) with badges, buttons, and form styling for a polished look
20+
21+## Technology Stack
22+
23+| Component | Technology |
24+|-----------|------------|
25+| Language | [Go](https://go.dev/) 1.27 |
26+| Web framework | [Gin](https://gin-gonic.com/) v1.10.0 |
27+| Database | [SQLite](https://www.sqlite.org/) via [go-sqlite3](https://github.com/mattn/go-sqlite3) |
28+| Templates | Go `html/template` (server-side rendered) |
29+
30+## Installation / Setup
31+
32+### Prerequisites
33+
34+- [Go](https://go.dev/dl/) 1.27 or later
35+- A C compiler (required by `go-sqlite3` for cgo)
36+
37+### Build and Run
38+
39+```bash
40+# Clone the repository
41+git clone <repository-url>
42+cd issueapp
43+
44+# Download dependencies
45+go mod download
46+
47+# Run the application (default port 8080)
48+go run ./cmd/issueapp
49+
50+# Or build a binary first
51+go build -o issueapp ./cmd/issueapp
52+./issueapp
53+```
54+
55+The application listens on `http://localhost:8080` by default. A SQLite database file (`issues.db`) is created automatically in the working directory on first run.
56+
57+### Configuration
58+
59+| Flag | Default | Description |
60+|------|---------|-------------|
61+| `-port` | `8080` | HTTP port to listen on |
62+
63+```bash
64+# Run on a custom port
65+go run ./cmd/issueapp -port 3000
66+```
67+
68+## Usage
69+
70+The application is a server-rendered web app. Open your browser and navigate to the base URL.
71+
72+### Routes
73+
74+| Method | Path | Description |
75+|--------|------|-------------|
76+| `GET` | `/` | Redirects to `/issues` |
77+| `GET` | `/issues` | List all issues |
78+| `GET` | `/issues/new` | Show the "new issue" form |
79+| `POST` | `/issues` | Create a new issue |
80+| `GET` | `/issues/:id` | Show a single issue's detail page |
81+| `GET` | `/issues/:id/edit` | Show the edit form for an issue |
82+| `POST` | `/issues/:id` | Update an existing issue |
83+| `POST` | `/issues/:id/delete` | Delete an issue |
84+
85+### Issue Fields
86+
87+| Field | Type | Notes |
88+|-------|------|-------|
89+| `title` | string | **Required** |
90+| `description` | string | Free-form text |
91+| `status` | string | One of: `open`, `in_progress`, `done`, `closed` |
92+| `priority` | string | One of: `low`, `medium`, `high`, `critical` |
93+| `assignee` | string | Free-form name |
94+
95+### Example Workflow
96+
97+1. Navigate to `http://localhost:8080/issues` to see the issue list.
98+2. Click **+ New Issue** (or go to `/issues/new`) to create an issue.
99+3. Fill in the title, description, status, priority, and assignee, then submit.
100+4. You are redirected to the issue detail page (`/issues/:id`).
101+5. From the detail page you can **Edit** or **Delete** the issue.
102+
103+## Project Structure
104+
105+```
106+.
107+├── cmd/
108+│ └── issueapp/
109+│ └── main.go # Entry point: flags, DB init, router setup
110+├── internal/
111+│ └── app/
112+│ ├── db.go # SQLite connection, schema, CRUD queries
113+│ ├── handlers.go # HTTP handlers and route registration
114+│ ├── models.go # Issue struct and valid status/priority values
115+│ ├── templates.go # Template and CSS loading from templates/ dir
116+│ └── review_test.go # HTTP-level tests for the app package
117+├── templates/
118+│ ├── layout.html # Shared HTML layout (header, nav, CSS include)
119+│ ├── list.html # Issue list and issue detail views
120+│ ├── form.html # Create/edit issue form
121+│ └── style.css # Application stylesheet
122+├── review_test.go # HTTP-level tests (root package)
123+├── go.mod
124+├── go.sum
125+└── issues.db # SQLite database (created at runtime)
126+```
127+
128+## Recent Changes
129+
130+- **Template externalization** — HTML templates and CSS were moved out of Go source files into the `templates/` directory. Templates are now loaded at startup via `loadTemplates()`, which reads `style.css`, `layout.html`, `list.html`, and `form.html` from disk. This makes it easy to adjust the UI without recompiling Go code.
131+- **CSS improvements** — a dedicated `style.css` stylesheet was introduced with styled badges for status and priority, consistent button styles, form styling, and a clean layout. The CSS is injected into the shared `layout.html` template.
132+
133+## Testing
134+
135+The project includes HTTP-level integration tests that exercise the router and handlers against a temporary in-memory database.
136+
137+```bash
138+# Run all tests
139+go test ./...
140+
141+# Run with verbose output
142+go test -v ./...
143+
144+# Run a specific test
145+go test -v -run TestTemplates ./internal/app/
146+```
147+
148+The tests cover:
149+
150+- Listing issues (`GET /issues`)
151+- Showing the create form (`GET /issues/new`)
152+- Handling a non-existent issue (`GET /issues/99999` → 404)
153+
154+## License
155+
156+This project is licensed under the [MIT License](LICENSE).
new file mode 100644
@@ -0,0 +1,156 @@
1+# Issue Tracker
2+
3+A simple issue tracking application built with Go and the Gin web framework.
4+
5+## Description
6+
7+Issue Tracker is a lightweight web application for managing project issues. It provides a clean, browser-based interface for creating, viewing, editing, and deleting issues, with support for status and priority management. Data is persisted in a local SQLite database, making it easy to run without any external dependencies.
8+
9+## Features
10+
11+- **Full CRUD operations** — create, read, update, and delete issues
12+- **Status management** — track issues through their lifecycle: `open`, `in_progress`, `done`, `closed`
13+- **Priority levels** — assign priorities: `low`, `medium`, `high`, `critical`
14+- **Assignee tracking** — assign issues to team members
15+- **Issue detail view** — view full details of a single issue
16+- **Form validation** — title is required; validation errors are displayed inline
17+- **Delete confirmation** — browser confirmation dialog before deleting an issue
18+- **Externalized templates** — HTML templates and CSS live in a dedicated `templates/` directory, making styling and layout changes straightforward
19+- **Responsive styling** — a custom stylesheet (`style.css`) with badges, buttons, and form styling for a polished look
20+
21+## Technology Stack
22+
23+| Component | Technology |
24+|-----------|------------|
25+| Language | [Go](https://go.dev/) 1.27 |
26+| Web framework | [Gin](https://gin-gonic.com/) v1.10.0 |
27+| Database | [SQLite](https://www.sqlite.org/) via [go-sqlite3](https://github.com/mattn/go-sqlite3) |
28+| Templates | Go `html/template` (server-side rendered) |
29+
30+## Installation / Setup
31+
32+### Prerequisites
33+
34+- [Go](https://go.dev/dl/) 1.27 or later
35+- A C compiler (required by `go-sqlite3` for cgo)
36+
37+### Build and Run
38+
39+```bash
40+# Clone the repository
41+git clone <repository-url>
42+cd issueapp
43+
44+# Download dependencies
45+go mod download
46+
47+# Run the application (default port 8080)
48+go run ./cmd/issueapp
49+
50+# Or build a binary first
51+go build -o issueapp ./cmd/issueapp
52+./issueapp
53+```
54+
55+The application listens on `http://localhost:8080` by default. A SQLite database file (`issues.db`) is created automatically in the working directory on first run.
56+
57+### Configuration
58+
59+| Flag | Default | Description |
60+|------|---------|-------------|
61+| `-port` | `8080` | HTTP port to listen on |
62+
63+```bash
64+# Run on a custom port
65+go run ./cmd/issueapp -port 3000
66+```
67+
68+## Usage
69+
70+The application is a server-rendered web app. Open your browser and navigate to the base URL.
71+
72+### Routes
73+
74+| Method | Path | Description |
75+|--------|------|-------------|
76+| `GET` | `/` | Redirects to `/issues` |
77+| `GET` | `/issues` | List all issues |
78+| `GET` | `/issues/new` | Show the "new issue" form |
79+| `POST` | `/issues` | Create a new issue |
80+| `GET` | `/issues/:id` | Show a single issue's detail page |
81+| `GET` | `/issues/:id/edit` | Show the edit form for an issue |
82+| `POST` | `/issues/:id` | Update an existing issue |
83+| `POST` | `/issues/:id/delete` | Delete an issue |
84+
85+### Issue Fields
86+
87+| Field | Type | Notes |
88+|-------|------|-------|
89+| `title` | string | **Required** |
90+| `description` | string | Free-form text |
91+| `status` | string | One of: `open`, `in_progress`, `done`, `closed` |
92+| `priority` | string | One of: `low`, `medium`, `high`, `critical` |
93+| `assignee` | string | Free-form name |
94+
95+### Example Workflow
96+
97+1. Navigate to `http://localhost:8080/issues` to see the issue list.
98+2. Click **+ New Issue** (or go to `/issues/new`) to create an issue.
99+3. Fill in the title, description, status, priority, and assignee, then submit.
100+4. You are redirected to the issue detail page (`/issues/:id`).
101+5. From the detail page you can **Edit** or **Delete** the issue.
102+
103+## Project Structure
104+
105+```
106+.
107+├── cmd/
108+│ └── issueapp/
109+│ └── main.go # Entry point: flags, DB init, router setup
110+├── internal/
111+│ └── app/
112+│ ├── db.go # SQLite connection, schema, CRUD queries
113+│ ├── handlers.go # HTTP handlers and route registration
114+│ ├── models.go # Issue struct and valid status/priority values
115+│ ├── templates.go # Template and CSS loading from templates/ dir
116+│ └── review_test.go # HTTP-level tests for the app package
117+├── templates/
118+│ ├── layout.html # Shared HTML layout (header, nav, CSS include)
119+│ ├── list.html # Issue list and issue detail views
120+│ ├── form.html # Create/edit issue form
121+│ └── style.css # Application stylesheet
122+├── review_test.go # HTTP-level tests (root package)
123+├── go.mod
124+├── go.sum
125+└── issues.db # SQLite database (created at runtime)
126+```
127+
128+## Recent Changes
129+
130+- **Template externalization** — HTML templates and CSS were moved out of Go source files into the `templates/` directory. Templates are now loaded at startup via `loadTemplates()`, which reads `style.css`, `layout.html`, `list.html`, and `form.html` from disk. This makes it easy to adjust the UI without recompiling Go code.
131+- **CSS improvements** — a dedicated `style.css` stylesheet was introduced with styled badges for status and priority, consistent button styles, form styling, and a clean layout. The CSS is injected into the shared `layout.html` template.
132+
133+## Testing
134+
135+The project includes HTTP-level integration tests that exercise the router and handlers against a temporary in-memory database.
136+
137+```bash
138+# Run all tests
139+go test ./...
140+
141+# Run with verbose output
142+go test -v ./...
143+
144+# Run a specific test
145+go test -v -run TestTemplates ./internal/app/
146+```
147+
148+The tests cover:
149+
150+- Listing issues (`GET /issues`)
151+- Showing the create form (`GET /issues/new`)
152+- Handling a non-existent issue (`GET /issues/99999` → 404)
153+
154+## License
155+
156+This project is licensed under the [MIT License](LICENSE).
modified dev.team/workspace/issues.db +0 -0
Binary files a/dev.team/workspace/issues.db and b/dev.team/workspace/issues.db differ
Binary files a/dev.team/workspace/issues.db and b/dev.team/workspace/issues.db differBinary files a/dev.team/workspace/issues.db and b/dev.team/workspace/issues.db differ
modified docs/diagrams/packages.drawio +2 -1
@@ -1,3 +1,4 @@
11 <mxfile host="app.diagrams.net" modified="2026-10-22T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1060"><root><mxCell id="0" /><mxCell id="1" parent="0" /><mxCell id="main" value="&lt;b&gt;main (mm)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;wiring: config, engine, tools, then one front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry" /></mxCell><mxCell id="agent" value="&lt;b&gt;internal/agent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal REPL: /quit /abort /compact /new&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="acp" value="&lt;b&gt;internal/acp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;editor front end: Agent Client Protocol on stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="session" value="&lt;b&gt;internal/session&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;what a conversation starts from; the /new reset&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="compact" value="&lt;b&gt;internal/compact&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;history compression into a model-written summary&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="engine" value="&lt;b&gt;internal/engine&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LLM connection + generation, one Provider per server&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="tools" value="&lt;b&gt;internal/tools&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;built-in tools: bash, read_skill, file tools, lsp, fetch_content; declared toolsbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content&lt;/font&gt;lt;/fontbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content&lt;/font&gt;gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="detector" value="&lt;b&gt;internal/detector&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;repeated-action (loop) detection&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="config" value="&lt;b&gt;internal/config&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;settings with YAML overrides (agent.yaml)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="spinner" value="&lt;b&gt;internal/spinner&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal spinner and styling&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="ui" value="&lt;b&gt;internal/ui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;human output destination + shared event sink&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="fileedit" value="&lt;b&gt;internal/fileedit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;exact-replacement file editing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="skills" value="&lt;b&gt;internal/skills&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;markdown skills catalogue (./skills)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="genkit" value="&lt;b&gt;github.com/firebase/genkit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ai.Message, tools, model calls&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="acpsdk" value="&lt;b&gt;github.com/coder/acp-go-sdk&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;JSON-RPC transport for ACP&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="openai" value="&lt;b&gt;github.com/openai/openai-go&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;OpenAI-compatible HTTP client&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="yaml" value="&lt;b&gt;gopkg.in/yaml.v3&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;YAML parsing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="version" value="&lt;b&gt;internal/version&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;which mm this is: build stamp, else the commit&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry" /></mxCell>
22 <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry" /></mxCell>
3- <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="&lt;b&gt;internal/mention&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the @path notation of a question&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="&lt;b&gt;internal/store&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;per-project session files (.mm/sessions/&lt;id&gt;.json), the questions ↑ recalls, /memory and search_memory&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="&lt;b&gt;internal/tui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;window front end: one screen on tcell (mm -tui)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="&lt;b&gt;internal/command&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the one slash-command table, per front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="&lt;b&gt;github.com/gdamore/tcell/v2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal screen, keys, mouse&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="&lt;b&gt;internal/saved&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;an answer to a new file: /save, save_answer&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="&lt;b&gt;internal/clipboard&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;system clipboard: native tool, then OSC 52&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="&lt;b&gt;internal/lsp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;language-server client: definition, references, hover, symbols, diagnostics, rename&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="&lt;b&gt;go.lsp.dev/protocol + jsonrpc2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LSP types and JSON-RPC 2.0 over stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="&lt;b&gt;mm — package dependencies&lt;/b&gt;&lt;br/&gt;Arrow: &quot;imports&quot;. Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.&lt;br/&gt;Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports &quot; &quot;}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="&lt;b&gt;internal/webmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="&lt;b&gt;golang.org/x/net/html (+ x/text)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;HTML5 parser, charset lookup&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="&lt;b&gt;internal/ask&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;/ask another mm agent: child mm -acp, ACP client side&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="&lt;b&gt;internal/consent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="&lt;b&gt;internal/mcp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;MCP clients (stdio, HTTP), their tools wrapped as mm tools&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="&lt;b&gt;internal/structured&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;structuredOutput: Go declarations → JSON Schema (go/parser)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="&lt;b&gt;internal/agentsmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the project's AGENTS.md, appended to the system prompt per conversation&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="&lt;b&gt;internal/elicit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ask_user's questions and answers: normalize, line answers, elicitation form schema&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="&lt;b&gt;internal/hooks&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="&lt;b&gt;internal/docsearch&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;BM25 over the text files of folders: /docs, search_docs&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="&lt;b&gt;internal/textfold&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;case and accents folded, rune for rune&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="&lt;b&gt;internal/agentinit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;mm init: the agent.yaml wizard, presets.yaml, the server's model facts&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="&lt;b&gt;internal/wasmtool&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="&lt;b&gt;github.com/tetratelabs/wazero&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;WebAssembly runtime in pure Go, WASI preview 1&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="&lt;b&gt;internal/decide&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;decision models: /v1/systemone client, skill and tool routing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile>
\ No newline at end of file
3+ <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="&lt;b&gt;internal/mention&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the @path notation of a question&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="&lt;b&gt;internal/store&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;per-project session files (.mm/sessions/&lt;id&gt;.json), the questions ↑ recalls, /memory and search_memory&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="&lt;b&gt;internal/tui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;window front end: one screen on tcell (mm -tui)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="&lt;b&gt;internal/command&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the one slash-command table, per front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="&lt;b&gt;github.com/gdamore/tcell/v2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal screen, keys, mouse&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="&lt;b&gt;internal/saved&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;an answer to a new file: /save, save_answer&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="&lt;b&gt;internal/clipboard&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;system clipboard: native tool, then OSC 52&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="&lt;b&gt;internal/lsp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;language-server client: definition, references, hover, symbols, diagnostics, rename&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="&lt;b&gt;go.lsp.dev/protocol + jsonrpc2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LSP types and JSON-RPC 2.0 over stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="&lt;b&gt;mm — package dependencies&lt;/b&gt;&lt;br/&gt;Arrow: &quot;imports&quot;. Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.&lt;br/&gt;Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports &quot; &quot;}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="&lt;b&gt;internal/webmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="&lt;b&gt;golang.org/x/net/html (+ x/text)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;HTML5 parser, charset lookup&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="&lt;b&gt;internal/ask&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;/ask another mm agent: child mm -acp, ACP client side&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="&lt;b&gt;internal/consent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="&lt;b&gt;internal/mcp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;MCP clients (stdio, HTTP), their tools wrapped as mm tools&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="&lt;b&gt;internal/structured&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;structuredOutput: Go declarations → JSON Schema (go/parser)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="&lt;b&gt;internal/agentsmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the project's AGENTS.md, appended to the system prompt per conversation&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="&lt;b&gt;internal/elicit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ask_user's questions and answers: normalize, line answers, elicitation form schema&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell>
4+ <mxCell id="e90" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="&lt;b&gt;internal/hooks&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="&lt;b&gt;internal/docsearch&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;BM25 over the text files of folders: /docs, search_docs&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="&lt;b&gt;internal/textfold&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;case and accents folded, rune for rune&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="&lt;b&gt;internal/agentinit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;mm init: the agent.yaml wizard, presets.yaml, the server's model facts&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="&lt;b&gt;internal/wasmtool&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="&lt;b&gt;github.com/tetratelabs/wazero&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;WebAssembly runtime in pure Go, WASI preview 1&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="&lt;b&gt;internal/decide&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;decision models: /v1/systemone client, skill and tool routing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile>
\ No newline at end of file
@@ -1,3 +1,4 @@
1 <mxfile host="app.diagrams.net" modified="2026-10-22T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1060"><root><mxCell id="0" /><mxCell id="1" parent="0" /><mxCell id="main" value="&lt;b&gt;main (mm)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;wiring: config, engine, tools, then one front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry" /></mxCell><mxCell id="agent" value="&lt;b&gt;internal/agent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal REPL: /quit /abort /compact /new&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="acp" value="&lt;b&gt;internal/acp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;editor front end: Agent Client Protocol on stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="session" value="&lt;b&gt;internal/session&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;what a conversation starts from; the /new reset&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="compact" value="&lt;b&gt;internal/compact&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;history compression into a model-written summary&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="engine" value="&lt;b&gt;internal/engine&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LLM connection + generation, one Provider per server&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="tools" value="&lt;b&gt;internal/tools&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;built-in tools: bash, read_skill, file tools, lsp, fetch_content; declared toolsbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content&lt;/font&gt;lt;/fontbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content&lt;/font&gt;gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="detector" value="&lt;b&gt;internal/detector&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;repeated-action (loop) detection&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="config" value="&lt;b&gt;internal/config&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;settings with YAML overrides (agent.yaml)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="spinner" value="&lt;b&gt;internal/spinner&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal spinner and styling&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="ui" value="&lt;b&gt;internal/ui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;human output destination + shared event sink&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="fileedit" value="&lt;b&gt;internal/fileedit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;exact-replacement file editing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="skills" value="&lt;b&gt;internal/skills&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;markdown skills catalogue (./skills)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="genkit" value="&lt;b&gt;github.com/firebase/genkit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ai.Message, tools, model calls&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="acpsdk" value="&lt;b&gt;github.com/coder/acp-go-sdk&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;JSON-RPC transport for ACP&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="openai" value="&lt;b&gt;github.com/openai/openai-go&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;OpenAI-compatible HTTP client&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="yaml" value="&lt;b&gt;gopkg.in/yaml.v3&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;YAML parsing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="version" value="&lt;b&gt;internal/version&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;which mm this is: build stamp, else the commit&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry" /></mxCell>1 <mxfile host="app.diagrams.net" modified="2026-10-22T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1060"><root><mxCell id="0" /><mxCell id="1" parent="0" /><mxCell id="main" value="&lt;b&gt;main (mm)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;wiring: config, engine, tools, then one front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry" /></mxCell><mxCell id="agent" value="&lt;b&gt;internal/agent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal REPL: /quit /abort /compact /new&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="acp" value="&lt;b&gt;internal/acp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;editor front end: Agent Client Protocol on stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="session" value="&lt;b&gt;internal/session&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;what a conversation starts from; the /new reset&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="compact" value="&lt;b&gt;internal/compact&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;history compression into a model-written summary&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="engine" value="&lt;b&gt;internal/engine&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LLM connection + generation, one Provider per server&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="tools" value="&lt;b&gt;internal/tools&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;built-in tools: bash, read_skill, file tools, lsp, fetch_content; declared toolsbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content&lt;/font&gt;lt;/fontbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content&lt;/font&gt;gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="detector" value="&lt;b&gt;internal/detector&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;repeated-action (loop) detection&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="config" value="&lt;b&gt;internal/config&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;settings with YAML overrides (agent.yaml)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="spinner" value="&lt;b&gt;internal/spinner&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal spinner and styling&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="ui" value="&lt;b&gt;internal/ui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;human output destination + shared event sink&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="fileedit" value="&lt;b&gt;internal/fileedit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;exact-replacement file editing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="skills" value="&lt;b&gt;internal/skills&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;markdown skills catalogue (./skills)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="genkit" value="&lt;b&gt;github.com/firebase/genkit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ai.Message, tools, model calls&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="acpsdk" value="&lt;b&gt;github.com/coder/acp-go-sdk&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;JSON-RPC transport for ACP&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="openai" value="&lt;b&gt;github.com/openai/openai-go&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;OpenAI-compatible HTTP client&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="yaml" value="&lt;b&gt;gopkg.in/yaml.v3&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;YAML parsing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="version" value="&lt;b&gt;internal/version&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;which mm this is: build stamp, else the commit&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry" /></mxCell>
2 <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry" /></mxCell>2 <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry" /></mxCell>
3- <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="&lt;b&gt;internal/mention&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the @path notation of a question&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="&lt;b&gt;internal/store&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;per-project session files (.mm/sessions/&lt;id&gt;.json), the questions ↑ recalls, /memory and search_memory&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="&lt;b&gt;internal/tui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;window front end: one screen on tcell (mm -tui)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="&lt;b&gt;internal/command&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the one slash-command table, per front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="&lt;b&gt;github.com/gdamore/tcell/v2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal screen, keys, mouse&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="&lt;b&gt;internal/saved&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;an answer to a new file: /save, save_answer&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="&lt;b&gt;internal/clipboard&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;system clipboard: native tool, then OSC 52&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="&lt;b&gt;internal/lsp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;language-server client: definition, references, hover, symbols, diagnostics, rename&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="&lt;b&gt;go.lsp.dev/protocol + jsonrpc2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LSP types and JSON-RPC 2.0 over stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="&lt;b&gt;mm — package dependencies&lt;/b&gt;&lt;br/&gt;Arrow: &quot;imports&quot;. Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.&lt;br/&gt;Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports &quot; &quot;}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="&lt;b&gt;internal/webmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="&lt;b&gt;golang.org/x/net/html (+ x/text)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;HTML5 parser, charset lookup&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="&lt;b&gt;internal/ask&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;/ask another mm agent: child mm -acp, ACP client side&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="&lt;b&gt;internal/consent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="&lt;b&gt;internal/mcp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;MCP clients (stdio, HTTP), their tools wrapped as mm tools&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="&lt;b&gt;internal/structured&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;structuredOutput: Go declarations → JSON Schema (go/parser)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="&lt;b&gt;internal/agentsmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the project's AGENTS.md, appended to the system prompt per conversation&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="&lt;b&gt;internal/elicit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ask_user's questions and answers: normalize, line answers, elicitation form schema&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="&lt;b&gt;internal/hooks&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="&lt;b&gt;internal/docsearch&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;BM25 over the text files of folders: /docs, search_docs&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="&lt;b&gt;internal/textfold&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;case and accents folded, rune for rune&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="&lt;b&gt;internal/agentinit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;mm init: the agent.yaml wizard, presets.yaml, the server's model facts&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="&lt;b&gt;internal/wasmtool&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="&lt;b&gt;github.com/tetratelabs/wazero&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;WebAssembly runtime in pure Go, WASI preview 1&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="&lt;b&gt;internal/decide&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;decision models: /v1/systemone client, skill and tool routing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile>
\ No newline at end of file\ No newline at end of file
3+ <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="&lt;b&gt;internal/mention&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the @path notation of a question&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="&lt;b&gt;internal/store&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;per-project session files (.mm/sessions/&lt;id&gt;.json), the questions ↑ recalls, /memory and search_memory&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="&lt;b&gt;internal/tui&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;window front end: one screen on tcell (mm -tui)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="&lt;b&gt;internal/command&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the one slash-command table, per front end&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="&lt;b&gt;github.com/gdamore/tcell/v2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;terminal screen, keys, mouse&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="&lt;b&gt;internal/saved&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;an answer to a new file: /save, save_answer&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="&lt;b&gt;internal/clipboard&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;system clipboard: native tool, then OSC 52&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="&lt;b&gt;internal/lsp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;language-server client: definition, references, hover, symbols, diagnostics, rename&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="&lt;b&gt;go.lsp.dev/protocol + jsonrpc2&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;LSP types and JSON-RPC 2.0 over stdio&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="&lt;b&gt;mm — package dependencies&lt;/b&gt;&lt;br/&gt;Arrow: &quot;imports&quot;. Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.&lt;br/&gt;Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports &quot; &quot;}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="&lt;b&gt;internal/webmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="&lt;b&gt;golang.org/x/net/html (+ x/text)&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;HTML5 parser, charset lookup&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="&lt;b&gt;internal/ask&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;/ask another mm agent: child mm -acp, ACP client side&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="&lt;b&gt;internal/consent&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="&lt;b&gt;internal/mcp&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;MCP clients (stdio, HTTP), their tools wrapped as mm tools&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="&lt;b&gt;internal/structured&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;structuredOutput: Go declarations → JSON Schema (go/parser)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="&lt;b&gt;internal/agentsmd&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the project's AGENTS.md, appended to the system prompt per conversation&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="&lt;b&gt;internal/elicit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;ask_user's questions and answers: normalize, line answers, elicitation form schema&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell>
4+ <mxCell id="e90" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="&lt;b&gt;internal/hooks&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="&lt;b&gt;internal/docsearch&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;BM25 over the text files of folders: /docs, search_docs&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="&lt;b&gt;internal/textfold&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;case and accents folded, rune for rune&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="&lt;b&gt;internal/agentinit&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;mm init: the agent.yaml wizard, presets.yaml, the server's model facts&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="&lt;b&gt;internal/wasmtool&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="&lt;b&gt;github.com/tetratelabs/wazero&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;WebAssembly runtime in pure Go, WASI preview 1&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="&lt;b&gt;internal/decide&lt;/b&gt;&lt;br/&gt;&lt;font style=&quot;font-size:10px&quot;&gt;decision models: /v1/systemone client, skill and tool routing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile>
\ No newline at end of file\ No newline at end of file
modified docs/en/how-to/ask-a-decision-model.md +1 -1
@@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92
8282 - **llmman or Ollama**: `provider: llmman` (or `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (or `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — pull it first. All three servers gave the same answers for the same request.
8383 - **Another OpenAI-compatible server**: `provider: openai` and its `baseUrl`; mm posts to `/v1/systemone` at the server's root (`baseUrl` without its trailing `/v1`), and a server without the route answers 404.
8484 - **From a sandbox**: add `fallback: http://host.docker.internal:8090/v1`.
85-- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it.
85+- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it. To see the decider's answers yourself before the chat model answers, add `display: true` to the decider's `decision:` block: `-p` then marks its JSON, and the demos' hooks return the answers, indented, as the reply's `display` — mm shows them in their own colour (`demos/decision-dnd/` has it on).
8686 - **Without any chat model**: `demos/decision-only/` triages a folder of support messages with one `mm -p` call per message, read by a short Node.js script that prints a table, most urgent first. `demos/decision-reviews/` (customer reviews) and `demos/decision-moderation/` (blog comments) do the same and check every answer against a file of expected answers.
8787 - **A smaller decision model**: `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (no Q4_K_M build is published). On the three demos above it was about five times faster than d1-3B (0.1 s per call against 0.55 s, `mm` start-up included) and less accurate: 26/28 against 28/28 on the reviews, 24/32 against 31/32 on the comments, five support categories out of six against six.
8888
@@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92
82 - **llmman or Ollama**: `provider: llmman` (or `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (or `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — pull it first. All three servers gave the same answers for the same request.82 - **llmman or Ollama**: `provider: llmman` (or `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (or `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — pull it first. All three servers gave the same answers for the same request.
83 - **Another OpenAI-compatible server**: `provider: openai` and its `baseUrl`; mm posts to `/v1/systemone` at the server's root (`baseUrl` without its trailing `/v1`), and a server without the route answers 404.83 - **Another OpenAI-compatible server**: `provider: openai` and its `baseUrl`; mm posts to `/v1/systemone` at the server's root (`baseUrl` without its trailing `/v1`), and a server without the route answers 404.
84 - **From a sandbox**: add `fallback: http://host.docker.internal:8090/v1`.84 - **From a sandbox**: add `fallback: http://host.docker.internal:8090/v1`.
85-- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it.85+- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it. To see the decider's answers yourself before the chat model answers, add `display: true` to the decider's `decision:` block: `-p` then marks its JSON, and the demos' hooks return the answers, indented, as the reply's `display` — mm shows them in their own colour (`demos/decision-dnd/` has it on).
86 - **Without any chat model**: `demos/decision-only/` triages a folder of support messages with one `mm -p` call per message, read by a short Node.js script that prints a table, most urgent first. `demos/decision-reviews/` (customer reviews) and `demos/decision-moderation/` (blog comments) do the same and check every answer against a file of expected answers.86 - **Without any chat model**: `demos/decision-only/` triages a folder of support messages with one `mm -p` call per message, read by a short Node.js script that prints a table, most urgent first. `demos/decision-reviews/` (customer reviews) and `demos/decision-moderation/` (blog comments) do the same and check every answer against a file of expected answers.
87 - **A smaller decision model**: `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (no Q4_K_M build is published). On the three demos above it was about five times faster than d1-3B (0.1 s per call against 0.55 s, `mm` start-up included) and less accurate: 26/28 against 28/28 on the reviews, 24/32 against 31/32 on the comments, five support categories out of six against six.87 - **A smaller decision model**: `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (no Q4_K_M build is published). On the three demos above it was about five times faster than d1-3B (0.1 s per call against 0.55 s, `mm` start-up included) and less accurate: 26/28 against 28/28 on the reviews, 24/32 against 31/32 on the comments, five support categories out of six against six.
88 88
modified docs/en/reference/cli.md +1 -1
@@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
137137 | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. |
138138 | Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. A call a `preToolCall` hook answered `allow` for runs: nobody had to be asked. |
139139 | With `structuredOutput` | stdout carries the JSON answer and a newline, nothing else: no streamed text, no 🤖, no statistics line. Messages go to stderr. A failed or invalid answer prints `[structured output: <error>]` on stderr, nothing on stdout, and exits with status 1; a valid answer exits with status 0. See [`structuredOutput`](configuration.md#structuredoutput). |
140-| With `decision` | stdout carries the server's JSON answer and a newline, nothing else. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). |
140+| With `decision` | stdout carries the server's JSON answer and a newline, nothing else; with `decision.display: true` the object starts with `"display":true`, added by mm. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). |
141141 | With `skillRouting` | No routing: `-p` sends the prompt as typed. |
142142 | With `toolRouting` | Routed like any turn: the `🧰` notice goes to stdout before the answer. |
143143
@@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
137 | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. |137 | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. |
138 | Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. A call a `preToolCall` hook answered `allow` for runs: nobody had to be asked. |138 | Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. A call a `preToolCall` hook answered `allow` for runs: nobody had to be asked. |
139 | With `structuredOutput` | stdout carries the JSON answer and a newline, nothing else: no streamed text, no 🤖, no statistics line. Messages go to stderr. A failed or invalid answer prints `[structured output: <error>]` on stderr, nothing on stdout, and exits with status 1; a valid answer exits with status 0. See [`structuredOutput`](configuration.md#structuredoutput). |139 | With `structuredOutput` | stdout carries the JSON answer and a newline, nothing else: no streamed text, no 🤖, no statistics line. Messages go to stderr. A failed or invalid answer prints `[structured output: <error>]` on stderr, nothing on stdout, and exits with status 1; a valid answer exits with status 0. See [`structuredOutput`](configuration.md#structuredoutput). |
140-| With `decision` | stdout carries the server's JSON answer and a newline, nothing else. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). |140+| With `decision` | stdout carries the server's JSON answer and a newline, nothing else; with `decision.display: true` the object starts with `"display":true`, added by mm. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). |
141 | With `skillRouting` | No routing: `-p` sends the prompt as typed. |141 | With `skillRouting` | No routing: `-p` sends the prompt as typed. |
142 | With `toolRouting` | Routed like any turn: the `🧰` notice goes to stdout before the answer. |142 | With `toolRouting` | Routed like any turn: the `🧰` notice goes to stdout before the answer. |
143 143
modified docs/en/reference/configuration.md +1 -0
@@ -248,6 +248,7 @@ structuredOutput: |
248248 | Key | Type | Default | Description |
249249 |-----|------|---------|-------------|
250250 | `timeout` | duration | `60s` | Bound of one answer. Must be ≥ 0; `0` = the default. |
251+| `display` | bool | `false` | `-p` adds `"display":true` at the head of the server's JSON, so a `preGeneration` hook that runs this decider can return the answers as its reply's `display`, shown to the user before the chat model answers (see [hooks](hooks.md#reply)). Nothing else changes. |
251252 | `questions` | mapping name → question | — | At least one. The order of the file is kept: in the request, in the answer lines, and for a choice's options. |
252253
253254 A question:
@@ -248,6 +248,7 @@ structuredOutput: |
248 | Key | Type | Default | Description |248 | Key | Type | Default | Description |
249 |-----|------|---------|-------------|249 |-----|------|---------|-------------|
250 | `timeout` | duration | `60s` | Bound of one answer. Must be ≥ 0; `0` = the default. |250 | `timeout` | duration | `60s` | Bound of one answer. Must be ≥ 0; `0` = the default. |
251+| `display` | bool | `false` | `-p` adds `"display":true` at the head of the server's JSON, so a `preGeneration` hook that runs this decider can return the answers as its reply's `display`, shown to the user before the chat model answers (see [hooks](hooks.md#reply)). Nothing else changes. |
251 | `questions` | mapping name → question | — | At least one. The order of the file is kept: in the request, in the answer lines, and for a choice's options. |252 | `questions` | mapping name → question | — | At least one. The order of the file is kept: in the request, in the answer lines, and for a choice's options. |
252 253
253 A question:254 A question:
modified docs/en/reference/hooks.md +2 -0
@@ -156,6 +156,7 @@ The JSON object a hook may write on stdout. **An empty stdout (or whitespace onl
156156 | `transcript` | string | `preAsk` | replaces the conversation sent to the asked agent. Absent = unchanged; `""` = send none. |
157157 | `answer` | string | `postAsk` | replaces the answer the asking agent reads. Absent = unchanged. |
158158 | `context` | string | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | text for a model. `preGeneration`: appended to the question. `postToolCall`: appended to the tool's output. `preAsk`: appended to what the asked agent receives. `postAsk`: appended to the answer the asking agent reads. Each one is introduced by the line `[context added by a hook]`. Blank = ignored. |
159+| `display` | string | `preGeneration` | text shown to the user, never sent to a model: before the answer, in its own colour (bright blue) under a `‣ hook` label in the window, in colour on a terminal (plain in a pipe), as a fenced block of the agent's message in an editor (tagged `json` when the text is JSON). Not kept in the history. Shown even when a later hook stops the turn. Blank = ignored. A hook that runs a decider whose file sets `decision.display: true` returns its answers here. |
159160 | `input` | any JSON value | `preToolCall` | replaces the tool call's arguments. Absent = unchanged. |
160161 | `reason` | string | `preGeneration` (`stop`), `preToolCall` and `preAsk` (`deny`) | why. Ignored on `allow`: write it anyway, it documents the hook's verdict for whoever reads its output. |
161162 | `response` | string | `preGeneration` (`stop`) | what the user reads instead of an answer. Absent or blank: `⛔ This request was blocked by a hook: <reason>`. |
@@ -182,6 +183,7 @@ Fields read on another event are ignored.
182183 |----------|-----------------|-------------------|---------------|
183184 | no change | the question | the question | — |
184185 | `prompt` and/or `context` | the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — |
186+| `display` | — | — | each `display`, in hook order, before anything else of the turn |
185187 | `stop` | nothing: the model is not called | `[prompt blocked by a hook: <reason>]` (or `[prompt blocked by a hook]`) followed by the `response` as the answer | the `response`, as the answer |
186188
187189 A chain: each hook reads the `prompt` the previous one left; contexts accumulate in hook order.
@@ -156,6 +156,7 @@ The JSON object a hook may write on stdout. **An empty stdout (or whitespace onl
156 | `transcript` | string | `preAsk` | replaces the conversation sent to the asked agent. Absent = unchanged; `""` = send none. |156 | `transcript` | string | `preAsk` | replaces the conversation sent to the asked agent. Absent = unchanged; `""` = send none. |
157 | `answer` | string | `postAsk` | replaces the answer the asking agent reads. Absent = unchanged. |157 | `answer` | string | `postAsk` | replaces the answer the asking agent reads. Absent = unchanged. |
158 | `context` | string | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | text for a model. `preGeneration`: appended to the question. `postToolCall`: appended to the tool's output. `preAsk`: appended to what the asked agent receives. `postAsk`: appended to the answer the asking agent reads. Each one is introduced by the line `[context added by a hook]`. Blank = ignored. |158 | `context` | string | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | text for a model. `preGeneration`: appended to the question. `postToolCall`: appended to the tool's output. `preAsk`: appended to what the asked agent receives. `postAsk`: appended to the answer the asking agent reads. Each one is introduced by the line `[context added by a hook]`. Blank = ignored. |
159+| `display` | string | `preGeneration` | text shown to the user, never sent to a model: before the answer, in its own colour (bright blue) under a `‣ hook` label in the window, in colour on a terminal (plain in a pipe), as a fenced block of the agent's message in an editor (tagged `json` when the text is JSON). Not kept in the history. Shown even when a later hook stops the turn. Blank = ignored. A hook that runs a decider whose file sets `decision.display: true` returns its answers here. |
159 | `input` | any JSON value | `preToolCall` | replaces the tool call's arguments. Absent = unchanged. |160 | `input` | any JSON value | `preToolCall` | replaces the tool call's arguments. Absent = unchanged. |
160 | `reason` | string | `preGeneration` (`stop`), `preToolCall` and `preAsk` (`deny`) | why. Ignored on `allow`: write it anyway, it documents the hook's verdict for whoever reads its output. |161 | `reason` | string | `preGeneration` (`stop`), `preToolCall` and `preAsk` (`deny`) | why. Ignored on `allow`: write it anyway, it documents the hook's verdict for whoever reads its output. |
161 | `response` | string | `preGeneration` (`stop`) | what the user reads instead of an answer. Absent or blank: `⛔ This request was blocked by a hook: <reason>`. |162 | `response` | string | `preGeneration` (`stop`) | what the user reads instead of an answer. Absent or blank: `⛔ This request was blocked by a hook: <reason>`. |
@@ -182,6 +183,7 @@ Fields read on another event are ignored.
182 |----------|-----------------|-------------------|---------------|183 |----------|-----------------|-------------------|---------------|
183 | no change | the question | the question | — |184 | no change | the question | the question | — |
184 | `prompt` and/or `context` | the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — |185 | `prompt` and/or `context` | the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — |
186+| `display` | — | — | each `display`, in hook order, before anything else of the turn |
185 | `stop` | nothing: the model is not called | `[prompt blocked by a hook: <reason>]` (or `[prompt blocked by a hook]`) followed by the `response` as the answer | the `response`, as the answer |187 | `stop` | nothing: the model is not called | `[prompt blocked by a hook: <reason>]` (or `[prompt blocked by a hook]`) followed by the `response` as the answer | the `response`, as the answer |
186 188
187 A chain: each hook reads the `prompt` the previous one left; contexts accumulate in hook order.189 A chain: each hook reads the `prompt` the previous one left; contexts accumulate in hook order.
modified docs/fr/how-to/ask-a-decision-model.md +1 -1
@@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92
8282 - **llmman ou Ollama** : `provider: llmman` (ou `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (ou `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — téléchargez-le d'abord. Les trois serveurs ont donné les mêmes réponses à la même requête.
8383 - **Un autre serveur compatible OpenAI** : `provider: openai` et son `baseUrl` ; mm envoie à `/v1/systemone` à la racine du serveur (`baseUrl` sans son `/v1` final), et un serveur sans la route répond 404.
8484 - **Depuis un sandbox** : ajoutez `fallback: http://host.docker.internal:8090/v1`.
85-- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher.
85+- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher. Pour voir vous-même les réponses du décideur avant que le modèle de chat réponde, ajoutez `display: true` au bloc `decision:` du décideur : `-p` marque alors son JSON, et les hooks des démos renvoient les réponses, indentées, dans le `display` de leur réponse — mm les affiche dans leur propre couleur (`demos/decision-dnd/` l'active).
8686 - **Sans aucun modèle de chat** : `demos/decision-only/` trie un dossier de messages de support avec un appel `mm -p` par message, lu par un court script Node.js qui affiche un tableau, du plus urgent au moins urgent. `demos/decision-reviews/` (des avis clients) et `demos/decision-moderation/` (des commentaires de blog) font de même et vérifient chaque réponse par rapport à un fichier de réponses attendues.
8787 - **Un modèle de décision plus petit** : `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (aucune version Q4_K_M n'est publiée). Sur les trois démos ci-dessus, il a été environ cinq fois plus rapide que d1-3B (0,1 s par appel contre 0,55 s, démarrage de `mm` compris) et moins juste : 26/28 contre 28/28 sur les avis, 24/32 contre 31/32 sur les commentaires, cinq catégories de support sur six contre six.
8888
@@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92
82 - **llmman ou Ollama** : `provider: llmman` (ou `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (ou `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — téléchargez-le d'abord. Les trois serveurs ont donné les mêmes réponses à la même requête.82 - **llmman ou Ollama** : `provider: llmman` (ou `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (ou `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — téléchargez-le d'abord. Les trois serveurs ont donné les mêmes réponses à la même requête.
83 - **Un autre serveur compatible OpenAI** : `provider: openai` et son `baseUrl` ; mm envoie à `/v1/systemone` à la racine du serveur (`baseUrl` sans son `/v1` final), et un serveur sans la route répond 404.83 - **Un autre serveur compatible OpenAI** : `provider: openai` et son `baseUrl` ; mm envoie à `/v1/systemone` à la racine du serveur (`baseUrl` sans son `/v1` final), et un serveur sans la route répond 404.
84 - **Depuis un sandbox** : ajoutez `fallback: http://host.docker.internal:8090/v1`.84 - **Depuis un sandbox** : ajoutez `fallback: http://host.docker.internal:8090/v1`.
85-- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher.85+- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher. Pour voir vous-même les réponses du décideur avant que le modèle de chat réponde, ajoutez `display: true` au bloc `decision:` du décideur : `-p` marque alors son JSON, et les hooks des démos renvoient les réponses, indentées, dans le `display` de leur réponse — mm les affiche dans leur propre couleur (`demos/decision-dnd/` l'active).
86 - **Sans aucun modèle de chat** : `demos/decision-only/` trie un dossier de messages de support avec un appel `mm -p` par message, lu par un court script Node.js qui affiche un tableau, du plus urgent au moins urgent. `demos/decision-reviews/` (des avis clients) et `demos/decision-moderation/` (des commentaires de blog) font de même et vérifient chaque réponse par rapport à un fichier de réponses attendues.86 - **Sans aucun modèle de chat** : `demos/decision-only/` trie un dossier de messages de support avec un appel `mm -p` par message, lu par un court script Node.js qui affiche un tableau, du plus urgent au moins urgent. `demos/decision-reviews/` (des avis clients) et `demos/decision-moderation/` (des commentaires de blog) font de même et vérifient chaque réponse par rapport à un fichier de réponses attendues.
87 - **Un modèle de décision plus petit** : `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (aucune version Q4_K_M n'est publiée). Sur les trois démos ci-dessus, il a été environ cinq fois plus rapide que d1-3B (0,1 s par appel contre 0,55 s, démarrage de `mm` compris) et moins juste : 26/28 contre 28/28 sur les avis, 24/32 contre 31/32 sur les commentaires, cinq catégories de support sur six contre six.87 - **Un modèle de décision plus petit** : `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (aucune version Q4_K_M n'est publiée). Sur les trois démos ci-dessus, il a été environ cinq fois plus rapide que d1-3B (0,1 s par appel contre 0,55 s, démarrage de `mm` compris) et moins juste : 26/28 contre 28/28 sur les avis, 24/32 contre 31/32 sur les commentaires, cinq catégories de support sur six contre six.
88 88
modified docs/fr/reference/cli.md +1 -1
@@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
137137 | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. |
138138 | Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. Un appel pour lequel un hook `preToolCall` a répondu `allow` s'exécute : personne n'avait à répondre. |
139139 | Avec `structuredOutput` | stdout porte la réponse JSON et un saut de ligne, rien d'autre : pas de texte en flux, pas de 🤖, pas de ligne de statistiques. Les messages vont sur stderr. Une réponse en échec ou invalide imprime `[structured output: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1 ; une réponse valide sort avec le code 0. Voir [`structuredOutput`](configuration.md#structuredoutput). |
140-| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). |
140+| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre ; avec `decision.display: true`, l'objet commence par `"display":true`, ajouté par mm. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). |
141141 | Avec `skillRouting` | Pas de routage : `-p` envoie l'invite telle que tapée. |
142142 | Avec `toolRouting` | Routé comme tout tour : la notice `🧰` part sur stdout avant la réponse. |
143143
@@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
137 | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. |137 | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. |
138 | Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. Un appel pour lequel un hook `preToolCall` a répondu `allow` s'exécute : personne n'avait à répondre. |138 | Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. Un appel pour lequel un hook `preToolCall` a répondu `allow` s'exécute : personne n'avait à répondre. |
139 | Avec `structuredOutput` | stdout porte la réponse JSON et un saut de ligne, rien d'autre : pas de texte en flux, pas de 🤖, pas de ligne de statistiques. Les messages vont sur stderr. Une réponse en échec ou invalide imprime `[structured output: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1 ; une réponse valide sort avec le code 0. Voir [`structuredOutput`](configuration.md#structuredoutput). |139 | Avec `structuredOutput` | stdout porte la réponse JSON et un saut de ligne, rien d'autre : pas de texte en flux, pas de 🤖, pas de ligne de statistiques. Les messages vont sur stderr. Une réponse en échec ou invalide imprime `[structured output: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1 ; une réponse valide sort avec le code 0. Voir [`structuredOutput`](configuration.md#structuredoutput). |
140-| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). |140+| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre ; avec `decision.display: true`, l'objet commence par `"display":true`, ajouté par mm. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). |
141 | Avec `skillRouting` | Pas de routage : `-p` envoie l'invite telle que tapée. |141 | Avec `skillRouting` | Pas de routage : `-p` envoie l'invite telle que tapée. |
142 | Avec `toolRouting` | Routé comme tout tour : la notice `🧰` part sur stdout avant la réponse. |142 | Avec `toolRouting` | Routé comme tout tour : la notice `🧰` part sur stdout avant la réponse. |
143 143
modified docs/fr/reference/configuration.md +1 -0
@@ -248,6 +248,7 @@ structuredOutput: |
248248 | Clé | Type | Défaut | Description |
249249 |-----|------|--------|-------------|
250250 | `timeout` | durée | `60s` | Borne d'une réponse. Doit être ≥ 0 ; `0` = la valeur par défaut. |
251+| `display` | booléen | `false` | `-p` ajoute `"display":true` en tête du JSON du serveur, pour qu'un hook `preGeneration` qui lance ce décideur puisse renvoyer les réponses dans le `display` de sa réponse, montré à l'utilisateur avant que le modèle de chat réponde (voir [hooks](hooks.md#réponse)). Rien d'autre ne change. |
251252 | `questions` | mapping nom → question | — | Au moins une. L'ordre du fichier est conservé : dans la requête, dans les lignes de réponse, et pour les options d'un choix. |
252253
253254 Une question :
@@ -248,6 +248,7 @@ structuredOutput: |
248 | Clé | Type | Défaut | Description |248 | Clé | Type | Défaut | Description |
249 |-----|------|--------|-------------|249 |-----|------|--------|-------------|
250 | `timeout` | durée | `60s` | Borne d'une réponse. Doit être ≥ 0 ; `0` = la valeur par défaut. |250 | `timeout` | durée | `60s` | Borne d'une réponse. Doit être ≥ 0 ; `0` = la valeur par défaut. |
251+| `display` | booléen | `false` | `-p` ajoute `"display":true` en tête du JSON du serveur, pour qu'un hook `preGeneration` qui lance ce décideur puisse renvoyer les réponses dans le `display` de sa réponse, montré à l'utilisateur avant que le modèle de chat réponde (voir [hooks](hooks.md#réponse)). Rien d'autre ne change. |
251 | `questions` | mapping nom → question | — | Au moins une. L'ordre du fichier est conservé : dans la requête, dans les lignes de réponse, et pour les options d'un choix. |252 | `questions` | mapping nom → question | — | Au moins une. L'ordre du fichier est conservé : dans la requête, dans les lignes de réponse, et pour les options d'un choix. |
252 253
253 Une question :254 Une question :
modified docs/fr/reference/hooks.md +2 -0
@@ -156,6 +156,7 @@ L'objet JSON qu'un hook peut écrire sur sa sortie standard. **Une sortie vide (
156156 | `transcript` | chaîne | `preAsk` | remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; `""` = n'en envoyer aucune. |
157157 | `answer` | chaîne | `postAsk` | remplace la réponse que lit l'agent qui appelle. Absent = inchangée. |
158158 | `context` | chaîne | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | du texte pour un modèle. `preGeneration` : ajouté après la question. `postToolCall` : ajouté après la sortie de l'outil. `preAsk` : ajouté à ce que reçoit l'agent appelé. `postAsk` : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne `[context added by a hook]`. Blanc = ignoré. |
159+| `display` | chaîne | `preGeneration` | du texte montré à l'utilisateur, jamais envoyé à un modèle : avant la réponse, dans sa propre couleur (bleu vif) sous une étiquette `‣ hook` dans la fenêtre, en couleur dans un terminal (sans couleur dans un pipe), en bloc délimité du message de l'agent dans un éditeur (marqué `json` quand le texte est du JSON). Pas gardé dans l'historique. Montré même quand un hook suivant arrête le tour. Blanc = ignoré. Un hook qui lance un décideur dont le fichier pose `decision.display: true` y renvoie ses réponses. |
159160 | `input` | toute valeur JSON | `preToolCall` | remplace les arguments de l'appel d'outil. Absent = inchangés. |
160161 | `reason` | chaîne | `preGeneration` (`stop`), `preToolCall` et `preAsk` (`deny`) | la raison. Ignorée sur `allow` : écrivez-la quand même, elle documente le verdict du hook pour qui lit sa sortie. |
161162 | `response` | chaîne | `preGeneration` (`stop`) | ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : `⛔ This request was blocked by a hook: <raison>`. |
@@ -182,6 +183,7 @@ Les champs lus sur un autre événement sont ignorés.
182183 |----------|---------------|--------------------|--------------------|
183184 | aucun changement | la question | la question | — |
184185 | `prompt` et/ou `context` | la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — |
186+| `display` | — | — | chaque `display`, dans l'ordre des hooks, avant tout le reste du tour |
185187 | `stop` | rien : le modèle n'est pas appelé | `[prompt blocked by a hook: <raison>]` (ou `[prompt blocked by a hook]`) suivi de la `response` comme réponse | la `response`, comme réponse |
186188
187189 En chaîne : chaque hook lit le `prompt` laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks.
@@ -156,6 +156,7 @@ L'objet JSON qu'un hook peut écrire sur sa sortie standard. **Une sortie vide (
156 | `transcript` | chaîne | `preAsk` | remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; `""` = n'en envoyer aucune. |156 | `transcript` | chaîne | `preAsk` | remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; `""` = n'en envoyer aucune. |
157 | `answer` | chaîne | `postAsk` | remplace la réponse que lit l'agent qui appelle. Absent = inchangée. |157 | `answer` | chaîne | `postAsk` | remplace la réponse que lit l'agent qui appelle. Absent = inchangée. |
158 | `context` | chaîne | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | du texte pour un modèle. `preGeneration` : ajouté après la question. `postToolCall` : ajouté après la sortie de l'outil. `preAsk` : ajouté à ce que reçoit l'agent appelé. `postAsk` : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne `[context added by a hook]`. Blanc = ignoré. |158 | `context` | chaîne | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | du texte pour un modèle. `preGeneration` : ajouté après la question. `postToolCall` : ajouté après la sortie de l'outil. `preAsk` : ajouté à ce que reçoit l'agent appelé. `postAsk` : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne `[context added by a hook]`. Blanc = ignoré. |
159+| `display` | chaîne | `preGeneration` | du texte montré à l'utilisateur, jamais envoyé à un modèle : avant la réponse, dans sa propre couleur (bleu vif) sous une étiquette `‣ hook` dans la fenêtre, en couleur dans un terminal (sans couleur dans un pipe), en bloc délimité du message de l'agent dans un éditeur (marqué `json` quand le texte est du JSON). Pas gardé dans l'historique. Montré même quand un hook suivant arrête le tour. Blanc = ignoré. Un hook qui lance un décideur dont le fichier pose `decision.display: true` y renvoie ses réponses. |
159 | `input` | toute valeur JSON | `preToolCall` | remplace les arguments de l'appel d'outil. Absent = inchangés. |160 | `input` | toute valeur JSON | `preToolCall` | remplace les arguments de l'appel d'outil. Absent = inchangés. |
160 | `reason` | chaîne | `preGeneration` (`stop`), `preToolCall` et `preAsk` (`deny`) | la raison. Ignorée sur `allow` : écrivez-la quand même, elle documente le verdict du hook pour qui lit sa sortie. |161 | `reason` | chaîne | `preGeneration` (`stop`), `preToolCall` et `preAsk` (`deny`) | la raison. Ignorée sur `allow` : écrivez-la quand même, elle documente le verdict du hook pour qui lit sa sortie. |
161 | `response` | chaîne | `preGeneration` (`stop`) | ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : `⛔ This request was blocked by a hook: <raison>`. |162 | `response` | chaîne | `preGeneration` (`stop`) | ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : `⛔ This request was blocked by a hook: <raison>`. |
@@ -182,6 +183,7 @@ Les champs lus sur un autre événement sont ignorés.
182 |----------|---------------|--------------------|--------------------|183 |----------|---------------|--------------------|--------------------|
183 | aucun changement | la question | la question | — |184 | aucun changement | la question | la question | — |
184 | `prompt` et/ou `context` | la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — |185 | `prompt` et/ou `context` | la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — |
186+| `display` | — | — | chaque `display`, dans l'ordre des hooks, avant tout le reste du tour |
185 | `stop` | rien : le modèle n'est pas appelé | `[prompt blocked by a hook: <raison>]` (ou `[prompt blocked by a hook]`) suivi de la `response` comme réponse | la `response`, comme réponse |187 | `stop` | rien : le modèle n'est pas appelé | `[prompt blocked by a hook: <raison>]` (ou `[prompt blocked by a hook]`) suivi de la `response` comme réponse | la `response`, comme réponse |
186 188
187 En chaîne : chaque hook lit le `prompt` laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks.189 En chaîne : chaque hook lit le `prompt` laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks.
added internal/acp/display_test.go +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+package acp
2+
3+import "testing"
4+
5+// A hook's display reaches the editor as a fenced block before the answer:
6+// tagged json when it parses, plain otherwise.
7+func TestDisplayBlockFencesTheText(t *testing.T) {
8+ cases := map[string]string{
9+ "{\n \"spell\": \"sleep\"\n}\n": "```json\n{\n \"spell\": \"sleep\"\n}\n```\n\n",
10+ "spell: sleep": "```\nspell: sleep\n```\n\n",
11+ }
12+ for text, want := range cases {
13+ if got := displayBlock(text); got != want {
14+ t.Errorf("displayBlock(%q) = %q, want %q", text, got, want)
15+ }
16+ }
17+}
new file mode 100644
@@ -0,0 +1,17 @@
1+package acp
2+
3+import "testing"
4+
5+// A hook's display reaches the editor as a fenced block before the answer:
6+// tagged json when it parses, plain otherwise.
7+func TestDisplayBlockFencesTheText(t *testing.T) {
8+ cases := map[string]string{
9+ "{\n \"spell\": \"sleep\"\n}\n": "```json\n{\n \"spell\": \"sleep\"\n}\n```\n\n",
10+ "spell: sleep": "```\nspell: sleep\n```\n\n",
11+ }
12+ for text, want := range cases {
13+ if got := displayBlock(text); got != want {
14+ t.Errorf("displayBlock(%q) = %q, want %q", text, got, want)
15+ }
16+ }
17+}
modified internal/acp/sink.go +22 -0
@@ -2,6 +2,8 @@ package acp
22
33 import (
44 "context"
5+ "encoding/json"
6+ "strings"
57
68 "mm/internal/config"
79 "mm/internal/consent"
@@ -37,6 +39,26 @@ func (k *sink) Text(text string) {
3739 k.send(sdk.UpdateAgentMessageText(text))
3840 }
3941
42+// Display shows what a hook asked to show the user (a decider's answer)
43+// before the answer streams. An editor has no colour of ours to give it, so
44+// it travels as a fenced block of the agent's message — never in the
45+// history: a resumed session does not replay it.
46+func (k *sink) Display(text string) {
47+ k.send(sdk.UpdateAgentMessageText(displayBlock(text)))
48+}
49+
50+// displayBlock fences a hook's display for the editor's Markdown: tagged
51+// json when it is JSON, so the editor highlights it.
52+//
53+// displayBlock(`{"spell":"sleep"}`) // "```json\n{\"spell\":\"sleep\"}\n```\n\n"
54+func displayBlock(text string) string {
55+ fence := "```"
56+ if json.Valid([]byte(text)) {
57+ fence += "json"
58+ }
59+ return fence + "\n" + strings.TrimRight(text, "\n") + "\n```\n\n"
60+}
61+
4062 // Reasoning streams what a thinking model produced before its answer. ACP
4163 // has the concept, so this is not a rendering trick: agent_thought_chunk is
4264 // a separate update and the editor shows it its own way — Zed folds it under
@@ -2,6 +2,8 @@ package acp
2 2
3 import (3 import (
4 "context"4 "context"
5+ "encoding/json"
6+ "strings"
5 7
6 "mm/internal/config"8 "mm/internal/config"
7 "mm/internal/consent"9 "mm/internal/consent"
@@ -37,6 +39,26 @@ func (k *sink) Text(text string) {
37 k.send(sdk.UpdateAgentMessageText(text))39 k.send(sdk.UpdateAgentMessageText(text))
38 }40 }
39 41
42+// Display shows what a hook asked to show the user (a decider's answer)
43+// before the answer streams. An editor has no colour of ours to give it, so
44+// it travels as a fenced block of the agent's message — never in the
45+// history: a resumed session does not replay it.
46+func (k *sink) Display(text string) {
47+ k.send(sdk.UpdateAgentMessageText(displayBlock(text)))
48+}
49+
50+// displayBlock fences a hook's display for the editor's Markdown: tagged
51+// json when it is JSON, so the editor highlights it.
52+//
53+// displayBlock(`{"spell":"sleep"}`) // "```json\n{\"spell\":\"sleep\"}\n```\n\n"
54+func displayBlock(text string) string {
55+ fence := "```"
56+ if json.Valid([]byte(text)) {
57+ fence += "json"
58+ }
59+ return fence + "\n" + strings.TrimRight(text, "\n") + "\n```\n\n"
60+}
61+
40 // Reasoning streams what a thinking model produced before its answer. ACP62 // Reasoning streams what a thinking model produced before its answer. ACP
41 // has the concept, so this is not a rendering trick: agent_thought_chunk is63 // has the concept, so this is not a rendering trick: agent_thought_chunk is
42 // a separate update and the editor shows it its own way — Zed folds it under64 // a separate update and the editor shows it its own way — Zed folds it under
modified internal/config/decision.go +14 -3
@@ -35,6 +35,7 @@ const DefaultDecisionTimeout = 60 * time.Second
3535 //
3636 // decision:
3737 // timeout: 60s
38+// display: true # -p marks its JSON: the chat shows it (hooks)
3839 // questions:
3940 // risk:
4041 // type: choice
@@ -53,7 +54,13 @@ const DefaultDecisionTimeout = 60 * time.Second
5354 // Questions keep the order of the file: the answer lines follow it, and a
5455 // choice's options reach the model in the order they are written.
5556 type DecisionConfig struct {
56- Timeout time.Duration
57+ Timeout time.Duration
58+ // Display asks the chat agent that consults this decider to show its
59+ // answer: `-p` then adds `"display": true` at the head of the server's
60+ // JSON, and a preGeneration hook that sees it hands the answer back as
61+ // the reply's `display` (ticket 141 follow-up). Off by default: the JSON
62+ // stays the server's, byte for byte.
63+ Display bool
5764 Questions []decide.Question
5865 }
5966
@@ -69,7 +76,7 @@ func (d DecisionConfig) Wait() time.Duration {
6976 // the questions and of a choice's options.
7077 func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {
7178 if node.Kind != yaml.MappingNode {
72- return fmt.Errorf("line %d: decision must be a mapping (timeout, questions)", node.Line)
79+ return fmt.Errorf("line %d: decision must be a mapping (timeout, display, questions)", node.Line)
7380 }
7481 for i := 0; i+1 < len(node.Content); i += 2 {
7582 key, value := node.Content[i], node.Content[i+1]
@@ -78,6 +85,10 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {
7885 if err := value.Decode(&d.Timeout); err != nil {
7986 return fmt.Errorf("line %d: decision.timeout: %w", value.Line, err)
8087 }
88+ case "display":
89+ if err := value.Decode(&d.Display); err != nil {
90+ return fmt.Errorf("line %d: decision.display: %w", value.Line, err)
91+ }
8192 case "questions":
8293 questions, err := decodeQuestions(value)
8394 if err != nil {
@@ -85,7 +96,7 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {
8596 }
8697 d.Questions = questions
8798 default:
88- return fmt.Errorf("line %d: decision: unknown key %q (timeout, questions)", key.Line, key.Value)
99+ return fmt.Errorf("line %d: decision: unknown key %q (timeout, display, questions)", key.Line, key.Value)
89100 }
90101 }
91102 return nil
@@ -35,6 +35,7 @@ const DefaultDecisionTimeout = 60 * time.Second
35 //35 //
36 // decision:36 // decision:
37 // timeout: 60s37 // timeout: 60s
38+// display: true # -p marks its JSON: the chat shows it (hooks)
38 // questions:39 // questions:
39 // risk:40 // risk:
40 // type: choice41 // type: choice
@@ -53,7 +54,13 @@ const DefaultDecisionTimeout = 60 * time.Second
53 // Questions keep the order of the file: the answer lines follow it, and a54 // Questions keep the order of the file: the answer lines follow it, and a
54 // choice's options reach the model in the order they are written.55 // choice's options reach the model in the order they are written.
55 type DecisionConfig struct {56 type DecisionConfig struct {
56- Timeout time.Duration57+ Timeout time.Duration
58+ // Display asks the chat agent that consults this decider to show its
59+ // answer: `-p` then adds `"display": true` at the head of the server's
60+ // JSON, and a preGeneration hook that sees it hands the answer back as
61+ // the reply's `display` (ticket 141 follow-up). Off by default: the JSON
62+ // stays the server's, byte for byte.
63+ Display bool
57 Questions []decide.Question64 Questions []decide.Question
58 }65 }
59 66
@@ -69,7 +76,7 @@ func (d DecisionConfig) Wait() time.Duration {
69 // the questions and of a choice's options.76 // the questions and of a choice's options.
70 func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {77 func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {
71 if node.Kind != yaml.MappingNode {78 if node.Kind != yaml.MappingNode {
72- return fmt.Errorf("line %d: decision must be a mapping (timeout, questions)", node.Line)79+ return fmt.Errorf("line %d: decision must be a mapping (timeout, display, questions)", node.Line)
73 }80 }
74 for i := 0; i+1 < len(node.Content); i += 2 {81 for i := 0; i+1 < len(node.Content); i += 2 {
75 key, value := node.Content[i], node.Content[i+1]82 key, value := node.Content[i], node.Content[i+1]
@@ -78,6 +85,10 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {
78 if err := value.Decode(&d.Timeout); err != nil {85 if err := value.Decode(&d.Timeout); err != nil {
79 return fmt.Errorf("line %d: decision.timeout: %w", value.Line, err)86 return fmt.Errorf("line %d: decision.timeout: %w", value.Line, err)
80 }87 }
88+ case "display":
89+ if err := value.Decode(&d.Display); err != nil {
90+ return fmt.Errorf("line %d: decision.display: %w", value.Line, err)
91+ }
81 case "questions":92 case "questions":
82 questions, err := decodeQuestions(value)93 questions, err := decodeQuestions(value)
83 if err != nil {94 if err != nil {
@@ -85,7 +96,7 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error {
85 }96 }
86 d.Questions = questions97 d.Questions = questions
87 default:98 default:
88- return fmt.Errorf("line %d: decision: unknown key %q (timeout, questions)", key.Line, key.Value)99+ return fmt.Errorf("line %d: decision: unknown key %q (timeout, display, questions)", key.Line, key.Value)
89 }100 }
90 }101 }
91 return nil102 return nil
modified internal/config/decision_test.go +17 -0
@@ -58,6 +58,22 @@ func TestTheDecisionBlockKeepsTheOrderOfQuestionsAndOptions(t *testing.T) {
5858 }
5959 }
6060
61+func TestDecisionDisplayIsOffUnlessTheFileSetsIt(t *testing.T) {
62+ if err := loadYAML(t, deciderYAML); err != nil {
63+ t.Fatal(err)
64+ }
65+ if Cfg.Decision.Display {
66+ t.Error("display is on without the key")
67+ }
68+ body := strings.Replace(deciderYAML, "decision:\n", "decision:\n display: true\n", 1)
69+ if err := loadYAML(t, body); err != nil {
70+ t.Fatal(err)
71+ }
72+ if !Cfg.Decision.Display || len(Cfg.Decision.Questions) != 3 {
73+ t.Errorf("Decision = %+v", Cfg.Decision)
74+ }
75+}
76+
6177 func TestAChatAgentHasNoDecisionBlock(t *testing.T) {
6278 if err := loadYAML(t, "model: m\n"); err != nil {
6379 t.Fatal(err)
@@ -84,6 +100,7 @@ func TestInvalidDecisionBlocksAreConfigErrors(t *testing.T) {
84100 "levels map": {head + "decision:\n questions:\n q: {type: score, instructions: x, criteria: {a: A, b: B}}\n", "2 to 10 levels"},
85101 "choice list": {head + "decision:\n questions:\n q: {type: choice, instructions: x, criteria: [a, b]}\n", "at least 2 options"},
86102 "noul criteria": {head + "decision:\n questions:\n q: {type: noul, instructions: x, criteria: [a, b]}\n", "takes no criteria"},
103+ "display text": {head + "decision:\n display: sometimes\n questions:\n q: {type: noul, instructions: x}\n", "decision.display"},
87104 "dmr": {"provider: dmr\nmodel: m\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "Docker Model Runner"},
88105 "structured too": {head + "structuredOutput: |\n type A struct {\n B bool `json:\"b\"`\n }\n var X A\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "cannot be used together"},
89106 }
@@ -58,6 +58,22 @@ func TestTheDecisionBlockKeepsTheOrderOfQuestionsAndOptions(t *testing.T) {
58 }58 }
59 }59 }
60 60
61+func TestDecisionDisplayIsOffUnlessTheFileSetsIt(t *testing.T) {
62+ if err := loadYAML(t, deciderYAML); err != nil {
63+ t.Fatal(err)
64+ }
65+ if Cfg.Decision.Display {
66+ t.Error("display is on without the key")
67+ }
68+ body := strings.Replace(deciderYAML, "decision:\n", "decision:\n display: true\n", 1)
69+ if err := loadYAML(t, body); err != nil {
70+ t.Fatal(err)
71+ }
72+ if !Cfg.Decision.Display || len(Cfg.Decision.Questions) != 3 {
73+ t.Errorf("Decision = %+v", Cfg.Decision)
74+ }
75+}
76+
61 func TestAChatAgentHasNoDecisionBlock(t *testing.T) {77 func TestAChatAgentHasNoDecisionBlock(t *testing.T) {
62 if err := loadYAML(t, "model: m\n"); err != nil {78 if err := loadYAML(t, "model: m\n"); err != nil {
63 t.Fatal(err)79 t.Fatal(err)
@@ -84,6 +100,7 @@ func TestInvalidDecisionBlocksAreConfigErrors(t *testing.T) {
84 "levels map": {head + "decision:\n questions:\n q: {type: score, instructions: x, criteria: {a: A, b: B}}\n", "2 to 10 levels"},100 "levels map": {head + "decision:\n questions:\n q: {type: score, instructions: x, criteria: {a: A, b: B}}\n", "2 to 10 levels"},
85 "choice list": {head + "decision:\n questions:\n q: {type: choice, instructions: x, criteria: [a, b]}\n", "at least 2 options"},101 "choice list": {head + "decision:\n questions:\n q: {type: choice, instructions: x, criteria: [a, b]}\n", "at least 2 options"},
86 "noul criteria": {head + "decision:\n questions:\n q: {type: noul, instructions: x, criteria: [a, b]}\n", "takes no criteria"},102 "noul criteria": {head + "decision:\n questions:\n q: {type: noul, instructions: x, criteria: [a, b]}\n", "takes no criteria"},
103+ "display text": {head + "decision:\n display: sometimes\n questions:\n q: {type: noul, instructions: x}\n", "decision.display"},
87 "dmr": {"provider: dmr\nmodel: m\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "Docker Model Runner"},104 "dmr": {"provider: dmr\nmodel: m\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "Docker Model Runner"},
88 "structured too": {head + "structuredOutput: |\n type A struct {\n B bool `json:\"b\"`\n }\n var X A\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "cannot be used together"},105 "structured too": {head + "structuredOutput: |\n type A struct {\n B bool `json:\"b\"`\n }\n var X A\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "cannot be used together"},
89 }106 }
modified internal/decide/decide.go +21 -0
@@ -365,3 +365,24 @@ func likeliest(keys, labels []string, probabilities map[string]float64) string {
365365 }
366366 return labels[best]
367367 }
368+
369+// MarkDisplay returns the server's JSON object with `"display":true` added
370+// at its head — what `-p` prints for a decider whose file sets
371+// `decision.display: true`, so the hook that runs it knows the chat should
372+// show the answer. Everything else is left byte for byte; a payload that is
373+// not an object is returned unchanged.
374+//
375+// decide.MarkDisplay([]byte(`{"model":"d1","answers":{}}`))
376+// // {"display":true,"model":"d1","answers":{}}
377+func MarkDisplay(raw []byte) []byte {
378+ trimmed := bytes.TrimSpace(raw)
379+ if len(trimmed) < 2 || trimmed[0] != '{' {
380+ return raw
381+ }
382+ rest := bytes.TrimSpace(trimmed[1:])
383+ marked := []byte(`{"display":true`)
384+ if rest[0] != '}' {
385+ marked = append(marked, ',')
386+ }
387+ return append(marked, rest...)
388+}
@@ -365,3 +365,24 @@ func likeliest(keys, labels []string, probabilities map[string]float64) string {
365 }365 }
366 return labels[best]366 return labels[best]
367 }367 }
368+
369+// MarkDisplay returns the server's JSON object with `"display":true` added
370+// at its head — what `-p` prints for a decider whose file sets
371+// `decision.display: true`, so the hook that runs it knows the chat should
372+// show the answer. Everything else is left byte for byte; a payload that is
373+// not an object is returned unchanged.
374+//
375+// decide.MarkDisplay([]byte(`{"model":"d1","answers":{}}`))
376+// // {"display":true,"model":"d1","answers":{}}
377+func MarkDisplay(raw []byte) []byte {
378+ trimmed := bytes.TrimSpace(raw)
379+ if len(trimmed) < 2 || trimmed[0] != '{' {
380+ return raw
381+ }
382+ rest := bytes.TrimSpace(trimmed[1:])
383+ marked := []byte(`{"display":true`)
384+ if rest[0] != '}' {
385+ marked = append(marked, ',')
386+ }
387+ return append(marked, rest...)
388+}
modified internal/decide/decide_test.go +15 -0
@@ -212,3 +212,18 @@ func TestLinesSayWhenAQuestionWasNotAnswered(t *testing.T) {
212212 t.Errorf("Lines = %q", got)
213213 }
214214 }
215+
216+func TestMarkDisplayAddsTheFlagAtTheHeadOfTheObject(t *testing.T) {
217+ cases := map[string]string{
218+ `{"model":"d1","answers":{}}`: `{"display":true,"model":"d1","answers":{}}`,
219+ " {\n \"model\": \"d1\"}\n": `{"display":true,"model": "d1"}`,
220+ `{}`: `{"display":true}`,
221+ `[1,2]`: `[1,2]`,
222+ ``: ``,
223+ }
224+ for raw, want := range cases {
225+ if got := string(MarkDisplay([]byte(raw))); got != want {
226+ t.Errorf("MarkDisplay(%q) = %q, want %q", raw, got, want)
227+ }
228+ }
229+}
@@ -212,3 +212,18 @@ func TestLinesSayWhenAQuestionWasNotAnswered(t *testing.T) {
212 t.Errorf("Lines = %q", got)212 t.Errorf("Lines = %q", got)
213 }213 }
214 }214 }
215+
216+func TestMarkDisplayAddsTheFlagAtTheHeadOfTheObject(t *testing.T) {
217+ cases := map[string]string{
218+ `{"model":"d1","answers":{}}`: `{"display":true,"model":"d1","answers":{}}`,
219+ " {\n \"model\": \"d1\"}\n": `{"display":true,"model": "d1"}`,
220+ `{}`: `{"display":true}`,
221+ `[1,2]`: `[1,2]`,
222+ ``: ``,
223+ }
224+ for raw, want := range cases {
225+ if got := string(MarkDisplay([]byte(raw))); got != want {
226+ t.Errorf("MarkDisplay(%q) = %q, want %q", raw, got, want)
227+ }
228+ }
229+}
modified internal/engine/hooks.go +5 -0
@@ -43,6 +43,11 @@ func (e *Engine) generateWithHooks(ctx context.Context, messages []*ai.Message,
4343 in := base
4444 in.Prompt = question.Text()
4545 d := e.hooks.PreGeneration(ctx, in)
46+ // Shown before anything else of the turn: what a hook (a decider's
47+ // answer, say) wants the user to see is about the question.
48+ for _, text := range d.Display {
49+ ui.Display(text)
50+ }
4651
4752 var (
4853 resp *ai.ModelResponse
@@ -43,6 +43,11 @@ func (e *Engine) generateWithHooks(ctx context.Context, messages []*ai.Message,
43 in := base43 in := base
44 in.Prompt = question.Text()44 in.Prompt = question.Text()
45 d := e.hooks.PreGeneration(ctx, in)45 d := e.hooks.PreGeneration(ctx, in)
46+ // Shown before anything else of the turn: what a hook (a decider's
47+ // answer, say) wants the user to see is about the question.
48+ for _, text := range d.Display {
49+ ui.Display(text)
50+ }
46 51
47 var (52 var (
48 resp *ai.ModelResponse53 resp *ai.ModelResponse
modified internal/engine/hooks_test.go +38 -2
@@ -46,10 +46,23 @@ type recordingSink struct {
4646 tools []ui.ToolEvent
4747 results []ui.ToolResult
4848 notices []string
49+ shown []string
50+ order []string // "display" and "text", in the order they arrived
4951 }
5052
51-func (s *recordingSink) WorkDir() string { return s.dir }
52-func (s *recordingSink) Text(t string) { s.mu.Lock(); s.text.WriteString(t); s.mu.Unlock() }
53+func (s *recordingSink) WorkDir() string { return s.dir }
54+func (s *recordingSink) Text(t string) {
55+ s.mu.Lock()
56+ s.text.WriteString(t)
57+ s.order = append(s.order, "text")
58+ s.mu.Unlock()
59+}
60+func (s *recordingSink) Display(text string) {
61+ s.mu.Lock()
62+ s.shown = append(s.shown, text)
63+ s.order = append(s.order, "display")
64+ s.mu.Unlock()
65+}
5366 func (s *recordingSink) Reasoning(string) {}
5467 func (s *recordingSink) ToolRunning(string) {}
5568 func (s *recordingSink) ToolStart(ev ui.ToolEvent) {
@@ -113,6 +126,29 @@ func TestPreGenerationRewritesWhatTheModelReads(t *testing.T) {
113126 }
114127 }
115128
129+// A hook's display reaches the front end before the answer streams, and
130+// never the model: it reads the context alone.
131+func TestPreGenerationDisplayIsShownBeforeTheAnswerAndNeverSent(t *testing.T) {
132+ g, seen := scriptedGenkit(t, modelTurn(ai.NewTextPart("Sleep it is.")))
133+ e := testEngine(g)
134+ sink := hookedEngine(t, e, config.HooksConfig{PreGeneration: []config.Hook{
135+ {Command: `echo '{"context":"spell: sleep","display":"{ \\"spell\\": \\"sleep\\" }"}'`},
136+ }})
137+
138+ if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("six kobolds")}, nil); err != nil {
139+ t.Fatal(err)
140+ }
141+ if len(sink.shown) != 1 || sink.shown[0] != `{ "spell": "sleep" }` {
142+ t.Errorf("shown = %q", sink.shown)
143+ }
144+ if len(sink.order) == 0 || sink.order[0] != "display" {
145+ t.Errorf("order = %v, want the display first", sink.order)
146+ }
147+ if read := (*seen)[0].Messages[0].Text(); strings.Contains(read, `"spell"`) || !strings.Contains(read, "spell: sleep") {
148+ t.Errorf("the model read %q", read)
149+ }
150+}
151+
116152 // A stopped turn never reaches the model, shows the hook's response, and
117153 // keeps a marker — never the blocked text — in the history.
118154 func TestPreGenerationStopNeverCallsTheModel(t *testing.T) {
@@ -46,10 +46,23 @@ type recordingSink struct {
46 tools []ui.ToolEvent46 tools []ui.ToolEvent
47 results []ui.ToolResult47 results []ui.ToolResult
48 notices []string48 notices []string
49+ shown []string
50+ order []string // "display" and "text", in the order they arrived
49 }51 }
50 52
51-func (s *recordingSink) WorkDir() string { return s.dir }53+func (s *recordingSink) WorkDir() string { return s.dir }
52-func (s *recordingSink) Text(t string) { s.mu.Lock(); s.text.WriteString(t); s.mu.Unlock() }54+func (s *recordingSink) Text(t string) {
55+ s.mu.Lock()
56+ s.text.WriteString(t)
57+ s.order = append(s.order, "text")
58+ s.mu.Unlock()
59+}
60+func (s *recordingSink) Display(text string) {
61+ s.mu.Lock()
62+ s.shown = append(s.shown, text)
63+ s.order = append(s.order, "display")
64+ s.mu.Unlock()
65+}
53 func (s *recordingSink) Reasoning(string) {}66 func (s *recordingSink) Reasoning(string) {}
54 func (s *recordingSink) ToolRunning(string) {}67 func (s *recordingSink) ToolRunning(string) {}
55 func (s *recordingSink) ToolStart(ev ui.ToolEvent) {68 func (s *recordingSink) ToolStart(ev ui.ToolEvent) {
@@ -113,6 +126,29 @@ func TestPreGenerationRewritesWhatTheModelReads(t *testing.T) {
113 }126 }
114 }127 }
115 128
129+// A hook's display reaches the front end before the answer streams, and
130+// never the model: it reads the context alone.
131+func TestPreGenerationDisplayIsShownBeforeTheAnswerAndNeverSent(t *testing.T) {
132+ g, seen := scriptedGenkit(t, modelTurn(ai.NewTextPart("Sleep it is.")))
133+ e := testEngine(g)
134+ sink := hookedEngine(t, e, config.HooksConfig{PreGeneration: []config.Hook{
135+ {Command: `echo '{"context":"spell: sleep","display":"{ \\"spell\\": \\"sleep\\" }"}'`},
136+ }})
137+
138+ if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("six kobolds")}, nil); err != nil {
139+ t.Fatal(err)
140+ }
141+ if len(sink.shown) != 1 || sink.shown[0] != `{ "spell": "sleep" }` {
142+ t.Errorf("shown = %q", sink.shown)
143+ }
144+ if len(sink.order) == 0 || sink.order[0] != "display" {
145+ t.Errorf("order = %v, want the display first", sink.order)
146+ }
147+ if read := (*seen)[0].Messages[0].Text(); strings.Contains(read, `"spell"`) || !strings.Contains(read, "spell: sleep") {
148+ t.Errorf("the model read %q", read)
149+ }
150+}
151+
116 // A stopped turn never reaches the model, shows the hook's response, and152 // A stopped turn never reaches the model, shows the hook's response, and
117 // keeps a marker — never the blocked text — in the history.153 // keeps a marker — never the blocked text — in the history.
118 func TestPreGenerationStopNeverCallsTheModel(t *testing.T) {154 func TestPreGenerationStopNeverCallsTheModel(t *testing.T) {
modified internal/hooks/display.go +3 -0
@@ -84,6 +84,9 @@ func decision(event string, reply Reply, err error) string {
8484 if c := strings.TrimSpace(reply.Context); c != "" && readsContext(event) {
8585 changes = append(changes, fmt.Sprintf("context (%d chars)", utf8.RuneCountInString(c)))
8686 }
87+ if s := strings.TrimSpace(reply.Display); s != "" && event == config.HookPreGeneration {
88+ changes = append(changes, fmt.Sprintf("display (%d chars)", utf8.RuneCountInString(s)))
89+ }
8790 if len(changes) == 0 {
8891 return DecisionContinue
8992 }
@@ -84,6 +84,9 @@ func decision(event string, reply Reply, err error) string {
84 if c := strings.TrimSpace(reply.Context); c != "" && readsContext(event) {84 if c := strings.TrimSpace(reply.Context); c != "" && readsContext(event) {
85 changes = append(changes, fmt.Sprintf("context (%d chars)", utf8.RuneCountInString(c)))85 changes = append(changes, fmt.Sprintf("context (%d chars)", utf8.RuneCountInString(c)))
86 }86 }
87+ if s := strings.TrimSpace(reply.Display); s != "" && event == config.HookPreGeneration {
88+ changes = append(changes, fmt.Sprintf("display (%d chars)", utf8.RuneCountInString(s)))
89+ }
87 if len(changes) == 0 {90 if len(changes) == 0 {
88 return DecisionContinue91 return DecisionContinue
89 }92 }
modified internal/hooks/display_test.go +2 -0
@@ -40,6 +40,8 @@ func TestDecisionCountsOnlyWhatTheEventReads(t *testing.T) {
4040 {"allow and input", config.HookPreToolCall, Reply{Action: ActionAllow, Input: json.RawMessage(`{}`)}, nil, "allow + input replaced"},
4141 {"input", config.HookPreToolCall, Reply{Input: json.RawMessage(`{"command":"ls"}`)}, nil, "input replaced"},
4242 {"tool output context", config.HookPostToolCall, Reply{Context: "ok"}, nil, "context (2 chars)"},
43+ {"context and display", config.HookPreGeneration, Reply{Context: "abc", Display: " {} "}, nil, "context (3 chars) + display (2 chars)"},
44+ {"display ignored outside preGeneration", config.HookPostToolCall, Reply{Display: "x"}, nil, DecisionContinue},
4345 {"ask rewrites", config.HookPreAsk, Reply{Prompt: ptr("q"), Transcript: ptr("")}, nil, "prompt replaced + transcript replaced"},
4446 {"answer", config.HookPostAsk, Reply{Answer: ptr("a")}, nil, "answer replaced"},
4547 {"postGeneration only observes", config.HookPostGeneration, Reply{Context: "ignored", Prompt: ptr("ignored")}, nil, DecisionContinue},
@@ -40,6 +40,8 @@ func TestDecisionCountsOnlyWhatTheEventReads(t *testing.T) {
40 {"allow and input", config.HookPreToolCall, Reply{Action: ActionAllow, Input: json.RawMessage(`{}`)}, nil, "allow + input replaced"},40 {"allow and input", config.HookPreToolCall, Reply{Action: ActionAllow, Input: json.RawMessage(`{}`)}, nil, "allow + input replaced"},
41 {"input", config.HookPreToolCall, Reply{Input: json.RawMessage(`{"command":"ls"}`)}, nil, "input replaced"},41 {"input", config.HookPreToolCall, Reply{Input: json.RawMessage(`{"command":"ls"}`)}, nil, "input replaced"},
42 {"tool output context", config.HookPostToolCall, Reply{Context: "ok"}, nil, "context (2 chars)"},42 {"tool output context", config.HookPostToolCall, Reply{Context: "ok"}, nil, "context (2 chars)"},
43+ {"context and display", config.HookPreGeneration, Reply{Context: "abc", Display: " {} "}, nil, "context (3 chars) + display (2 chars)"},
44+ {"display ignored outside preGeneration", config.HookPostToolCall, Reply{Display: "x"}, nil, DecisionContinue},
43 {"ask rewrites", config.HookPreAsk, Reply{Prompt: ptr("q"), Transcript: ptr("")}, nil, "prompt replaced + transcript replaced"},45 {"ask rewrites", config.HookPreAsk, Reply{Prompt: ptr("q"), Transcript: ptr("")}, nil, "prompt replaced + transcript replaced"},
44 {"answer", config.HookPostAsk, Reply{Answer: ptr("a")}, nil, "answer replaced"},46 {"answer", config.HookPostAsk, Reply{Answer: ptr("a")}, nil, "answer replaced"},
45 {"postGeneration only observes", config.HookPostGeneration, Reply{Context: "ignored", Prompt: ptr("ignored")}, nil, DecisionContinue},47 {"postGeneration only observes", config.HookPostGeneration, Reply{Context: "ignored", Prompt: ptr("ignored")}, nil, DecisionContinue},
modified internal/hooks/hooks.go +11 -0
@@ -100,6 +100,11 @@ type Reply struct {
100100 // (preGeneration), after the tool's output (postToolCall), after the
101101 // question sent to another agent (preAsk), after its answer (postAsk).
102102 Context string `json:"context,omitempty"`
103+ // Display is text shown to the user, never to a model (preGeneration):
104+ // in its own colour in the window and the terminal, as a fenced block in
105+ // an editor, before the answer. A hook that asked a decider hands back
106+ // its answer here.
107+ Display string `json:"display,omitempty"`
103108 // Input replaces the tool call's arguments (preToolCall). Absent = kept.
104109 Input json.RawMessage `json:"input,omitempty"`
105110 // Reason says why a turn was stopped or a call denied.
@@ -139,6 +144,9 @@ type PromptDecision struct {
139144 Prompt string
140145 // Context holds the text the hooks added, in hook order.
141146 Context []string
147+ // Display holds the text the hooks asked to show the user, in hook
148+ // order — kept even when a later hook stops the turn.
149+ Display []string
142150 // Stopped means the model must not be called; Response is what the user
143151 // reads instead and Reason why.
144152 Stopped bool
@@ -168,6 +176,9 @@ func (r *Runner) PreGeneration(ctx context.Context, in Input) PromptDecision {
168176 }
169177 continue
170178 }
179+ if s := strings.TrimSpace(reply.Display); s != "" {
180+ d.Display = append(d.Display, s)
181+ }
171182 if reply.Action == ActionStop {
172183 return stopped(d, reply.Reason, reply.Response)
173184 }
@@ -100,6 +100,11 @@ type Reply struct {
100 // (preGeneration), after the tool's output (postToolCall), after the100 // (preGeneration), after the tool's output (postToolCall), after the
101 // question sent to another agent (preAsk), after its answer (postAsk).101 // question sent to another agent (preAsk), after its answer (postAsk).
102 Context string `json:"context,omitempty"`102 Context string `json:"context,omitempty"`
103+ // Display is text shown to the user, never to a model (preGeneration):
104+ // in its own colour in the window and the terminal, as a fenced block in
105+ // an editor, before the answer. A hook that asked a decider hands back
106+ // its answer here.
107+ Display string `json:"display,omitempty"`
103 // Input replaces the tool call's arguments (preToolCall). Absent = kept.108 // Input replaces the tool call's arguments (preToolCall). Absent = kept.
104 Input json.RawMessage `json:"input,omitempty"`109 Input json.RawMessage `json:"input,omitempty"`
105 // Reason says why a turn was stopped or a call denied.110 // Reason says why a turn was stopped or a call denied.
@@ -139,6 +144,9 @@ type PromptDecision struct {
139 Prompt string144 Prompt string
140 // Context holds the text the hooks added, in hook order.145 // Context holds the text the hooks added, in hook order.
141 Context []string146 Context []string
147+ // Display holds the text the hooks asked to show the user, in hook
148+ // order — kept even when a later hook stops the turn.
149+ Display []string
142 // Stopped means the model must not be called; Response is what the user150 // Stopped means the model must not be called; Response is what the user
143 // reads instead and Reason why.151 // reads instead and Reason why.
144 Stopped bool152 Stopped bool
@@ -168,6 +176,9 @@ func (r *Runner) PreGeneration(ctx context.Context, in Input) PromptDecision {
168 }176 }
169 continue177 continue
170 }178 }
179+ if s := strings.TrimSpace(reply.Display); s != "" {
180+ d.Display = append(d.Display, s)
181+ }
171 if reply.Action == ActionStop {182 if reply.Action == ActionStop {
172 return stopped(d, reply.Reason, reply.Response)183 return stopped(d, reply.Reason, reply.Response)
173 }184 }
modified internal/hooks/hooks_test.go +18 -0
@@ -121,6 +121,24 @@ func TestPreGenerationStopEndsTheChain(t *testing.T) {
121121 }
122122 }
123123
124+// What hooks ask to show the user accumulates in order, apart from the
125+// context the model reads — and survives a later stop.
126+func TestPreGenerationCollectsDisplayInOrder(t *testing.T) {
127+ cfg := config.HooksConfig{PreGeneration: []config.Hook{
128+ hook(`echo '{"context":"spell: sleep","display":"{\\"spell\\": \\"sleep\\"}"}'`),
129+ hook(`echo '{"display":" second "}'`),
130+ hook(`echo '{"action":"stop","reason":"enough","display":"third"}'`),
131+ }}
132+ r, _ := runner(cfg)
133+ d := r.PreGeneration(context.Background(), Input{Prompt: "kobolds"})
134+ if strings.Join(d.Display, "|") != `{"spell": "sleep"}|second|third` {
135+ t.Errorf("Display = %q", d.Display)
136+ }
137+ if strings.Join(d.Context, "|") != "spell: sleep" || !d.Stopped {
138+ t.Errorf("decision = %+v", d)
139+ }
140+}
141+
124142 // A stop without a response still tells the user something.
125143 func TestStopWithoutResponseExplainsTheReason(t *testing.T) {
126144 r, _ := runner(config.HooksConfig{PreGeneration: []config.Hook{hook(`echo '{"action":"stop","reason":"too short"}'`)}})
@@ -121,6 +121,24 @@ func TestPreGenerationStopEndsTheChain(t *testing.T) {
121 }121 }
122 }122 }
123 123
124+// What hooks ask to show the user accumulates in order, apart from the
125+// context the model reads — and survives a later stop.
126+func TestPreGenerationCollectsDisplayInOrder(t *testing.T) {
127+ cfg := config.HooksConfig{PreGeneration: []config.Hook{
128+ hook(`echo '{"context":"spell: sleep","display":"{\\"spell\\": \\"sleep\\"}"}'`),
129+ hook(`echo '{"display":" second "}'`),
130+ hook(`echo '{"action":"stop","reason":"enough","display":"third"}'`),
131+ }}
132+ r, _ := runner(cfg)
133+ d := r.PreGeneration(context.Background(), Input{Prompt: "kobolds"})
134+ if strings.Join(d.Display, "|") != `{"spell": "sleep"}|second|third` {
135+ t.Errorf("Display = %q", d.Display)
136+ }
137+ if strings.Join(d.Context, "|") != "spell: sleep" || !d.Stopped {
138+ t.Errorf("decision = %+v", d)
139+ }
140+}
141+
124 // A stop without a response still tells the user something.142 // A stop without a response still tells the user something.
125 func TestStopWithoutResponseExplainsTheReason(t *testing.T) {143 func TestStopWithoutResponseExplainsTheReason(t *testing.T) {
126 r, _ := runner(config.HooksConfig{PreGeneration: []config.Hook{hook(`echo '{"action":"stop","reason":"too short"}'`)}})144 r, _ := runner(config.HooksConfig{PreGeneration: []config.Hook{hook(`echo '{"action":"stop","reason":"too short"}'`)}})
modified internal/tui/render.go +14 -0
@@ -22,6 +22,7 @@ const (
2222 styleDiffAdd
2323 styleDiffDel
2424 styleThinking
25+ styleDisplay
2526 )
2627
2728 // Line is one drawn line of the conversation: its text, wrapped to the width
@@ -69,6 +70,8 @@ func linesOf(e Entry, agentName string, width, previewLines int) []Line {
6970 lines = toolLines(e, width, previewLines)
7071 case KindThinking:
7172 lines = append(label("thinking"), prose(e.Text, width, styleThinking)...)
73+ case KindDisplay:
74+ lines = append(label("hook"), verbatim(e.Text, styleDisplay)...)
7275 case KindError:
7376 lines = prose(e.Text, width, styleError)
7477 default:
@@ -91,6 +94,17 @@ func prose(text string, width int, style styleKind) []Line {
9194 return out
9295 }
9396
97+// verbatim lays text out line for line, indented and never wrapped — a
98+// hook's display is often JSON, whose indentation is its meaning; the
99+// screen clips what is wider than the window, as it does fenced code.
100+func verbatim(text string, style styleKind) []Line {
101+ var out []Line
102+ for _, line := range strings.Split(text, "\n") {
103+ out = append(out, Line{Text: pad + line, Style: style})
104+ }
105+ return out
106+}
107+
94108 // body lays out an answer: prose wrapped, fenced blocks drawn as code.
95109 func body(text string, width int) []Line {
96110 var out []Line
@@ -22,6 +22,7 @@ const (
22 styleDiffAdd22 styleDiffAdd
23 styleDiffDel23 styleDiffDel
24 styleThinking24 styleThinking
25+ styleDisplay
25 )26 )
26 27
27 // Line is one drawn line of the conversation: its text, wrapped to the width28 // Line is one drawn line of the conversation: its text, wrapped to the width
@@ -69,6 +70,8 @@ func linesOf(e Entry, agentName string, width, previewLines int) []Line {
69 lines = toolLines(e, width, previewLines)70 lines = toolLines(e, width, previewLines)
70 case KindThinking:71 case KindThinking:
71 lines = append(label("thinking"), prose(e.Text, width, styleThinking)...)72 lines = append(label("thinking"), prose(e.Text, width, styleThinking)...)
73+ case KindDisplay:
74+ lines = append(label("hook"), verbatim(e.Text, styleDisplay)...)
72 case KindError:75 case KindError:
73 lines = prose(e.Text, width, styleError)76 lines = prose(e.Text, width, styleError)
74 default:77 default:
@@ -91,6 +94,17 @@ func prose(text string, width int, style styleKind) []Line {
91 return out94 return out
92 }95 }
93 96
97+// verbatim lays text out line for line, indented and never wrapped — a
98+// hook's display is often JSON, whose indentation is its meaning; the
99+// screen clips what is wider than the window, as it does fenced code.
100+func verbatim(text string, style styleKind) []Line {
101+ var out []Line
102+ for _, line := range strings.Split(text, "\n") {
103+ out = append(out, Line{Text: pad + line, Style: style})
104+ }
105+ return out
106+}
107+
94 // body lays out an answer: prose wrapped, fenced blocks drawn as code.108 // body lays out an answer: prose wrapped, fenced blocks drawn as code.
95 func body(text string, width int) []Line {109 func body(text string, width int) []Line {
96 var out []Line110 var out []Line
modified internal/tui/sink.go +7 -0
@@ -92,6 +92,13 @@ func (k *sink) ToolEnd(id string, res ui.ToolResult) {
9292 k.r.wake()
9393 }
9494
95+// Display shows what a hook asked to show the user (a decider's answer) as
96+// its own entry, in its own colour, before the answer streams.
97+func (k *sink) Display(text string) {
98+ k.r.t.Display(text)
99+ k.r.wake()
100+}
101+
95102 // Notice shows a remark about the turn (a hook that failed, say) as a dim
96103 // line of the transcript: printed on stdout, it would tear the screen.
97104 func (k *sink) Notice(text string) {
@@ -92,6 +92,13 @@ func (k *sink) ToolEnd(id string, res ui.ToolResult) {
92 k.r.wake()92 k.r.wake()
93 }93 }
94 94
95+// Display shows what a hook asked to show the user (a decider's answer) as
96+// its own entry, in its own colour, before the answer streams.
97+func (k *sink) Display(text string) {
98+ k.r.t.Display(text)
99+ k.r.wake()
100+}
101+
95 // Notice shows a remark about the turn (a hook that failed, say) as a dim102 // Notice shows a remark about the turn (a hook that failed, say) as a dim
96 // line of the transcript: printed on stdout, it would tear the screen.103 // line of the transcript: printed on stdout, it would tear the screen.
97 func (k *sink) Notice(text string) {104 func (k *sink) Notice(text string) {
modified internal/tui/style.go +2 -0
@@ -24,6 +24,7 @@ var (
2424 stAsk = tcell.StyleDefault.Bold(true).Foreground(tcell.ColorYellow)
2525 stPaste = tcell.StyleDefault.Reverse(true).Foreground(tcell.ColorTeal)
2626 stThinking = tcell.StyleDefault.Dim(true).Italic(true).Foreground(tcell.ColorPurple)
27+ stDisplay = tcell.StyleDefault.Foreground(tcell.ColorBlue)
2728 stScroll = tcell.StyleDefault.Dim(true)
2829 stThumb = tcell.StyleDefault.Reverse(true)
2930 )
@@ -41,6 +42,7 @@ var styles = map[styleKind]tcell.Style{
4142 styleDiffAdd: stDiffAdd,
4243 styleDiffDel: stDiffDel,
4344 styleThinking: stThinking,
45+ styleDisplay: stDisplay,
4446 }
4547
4648 // styleOf returns the screen style of a laid-out line's kind.
@@ -24,6 +24,7 @@ var (
24 stAsk = tcell.StyleDefault.Bold(true).Foreground(tcell.ColorYellow)24 stAsk = tcell.StyleDefault.Bold(true).Foreground(tcell.ColorYellow)
25 stPaste = tcell.StyleDefault.Reverse(true).Foreground(tcell.ColorTeal)25 stPaste = tcell.StyleDefault.Reverse(true).Foreground(tcell.ColorTeal)
26 stThinking = tcell.StyleDefault.Dim(true).Italic(true).Foreground(tcell.ColorPurple)26 stThinking = tcell.StyleDefault.Dim(true).Italic(true).Foreground(tcell.ColorPurple)
27+ stDisplay = tcell.StyleDefault.Foreground(tcell.ColorBlue)
27 stScroll = tcell.StyleDefault.Dim(true)28 stScroll = tcell.StyleDefault.Dim(true)
28 stThumb = tcell.StyleDefault.Reverse(true)29 stThumb = tcell.StyleDefault.Reverse(true)
29 )30 )
@@ -41,6 +42,7 @@ var styles = map[styleKind]tcell.Style{
41 styleDiffAdd: stDiffAdd,42 styleDiffAdd: stDiffAdd,
42 styleDiffDel: stDiffDel,43 styleDiffDel: stDiffDel,
43 styleThinking: stThinking,44 styleThinking: stThinking,
45+ styleDisplay: stDisplay,
44 }46 }
45 47
46 // styleOf returns the screen style of a laid-out line's kind.48 // styleOf returns the screen style of a laid-out line's kind.
modified internal/tui/transcript.go +6 -0
@@ -27,6 +27,9 @@ const (
2727 // KindThinking is what a thinking model streamed before its answer,
2828 // chunk by chunk into ONE entry, as KindAgent is.
2929 KindThinking
30+ // KindDisplay is text a preGeneration hook asked to show the user — a
31+ // decider's answer, say: never the answer, never sent to the model.
32+ KindDisplay
3033 )
3134
3235 // Tool statuses, as the transcript shows them.
@@ -124,6 +127,9 @@ func (t *Transcript) AddUserFull(display, full string) {
124127 // Notice appends a line the window says for itself.
125128 func (t *Transcript) Notice(text string) { t.add(Entry{Kind: KindNotice, Text: text}) }
126129
130+// Display appends what a hook asked to show the user.
131+func (t *Transcript) Display(text string) { t.add(Entry{Kind: KindDisplay, Text: text}) }
132+
127133 // Error appends a one-line failure.
128134 func (t *Transcript) Error(text string) { t.add(Entry{Kind: KindError, Text: text}) }
129135
@@ -27,6 +27,9 @@ const (
27 // KindThinking is what a thinking model streamed before its answer,27 // KindThinking is what a thinking model streamed before its answer,
28 // chunk by chunk into ONE entry, as KindAgent is.28 // chunk by chunk into ONE entry, as KindAgent is.
29 KindThinking29 KindThinking
30+ // KindDisplay is text a preGeneration hook asked to show the user — a
31+ // decider's answer, say: never the answer, never sent to the model.
32+ KindDisplay
30 )33 )
31 34
32 // Tool statuses, as the transcript shows them.35 // Tool statuses, as the transcript shows them.
@@ -124,6 +127,9 @@ func (t *Transcript) AddUserFull(display, full string) {
124 // Notice appends a line the window says for itself.127 // Notice appends a line the window says for itself.
125 func (t *Transcript) Notice(text string) { t.add(Entry{Kind: KindNotice, Text: text}) }128 func (t *Transcript) Notice(text string) { t.add(Entry{Kind: KindNotice, Text: text}) }
126 129
130+// Display appends what a hook asked to show the user.
131+func (t *Transcript) Display(text string) { t.add(Entry{Kind: KindDisplay, Text: text}) }
132+
127 // Error appends a one-line failure.133 // Error appends a one-line failure.
128 func (t *Transcript) Error(text string) { t.add(Entry{Kind: KindError, Text: text}) }134 func (t *Transcript) Error(text string) { t.add(Entry{Kind: KindError, Text: text}) }
129 135
modified internal/tui/transcript_test.go +26 -0
@@ -170,3 +170,29 @@ func TestAnswersAndPlainAreRawText(t *testing.T) {
170170 t.Errorf("Plain:\n got %q\nwant %q", got, want)
171171 }
172172 }
173+
174+// A hook's display is its own entry, labelled, drawn line for line in its
175+// own colour: JSON keeps its indentation, nothing is wrapped.
176+func TestDisplayIsDrawnVerbatimUnderItsLabel(t *testing.T) {
177+ tr := &Transcript{}
178+ tr.Display("{\n \"spell\": \"a very long spell name that is wider than the window\"\n}")
179+ entries := tr.Entries()
180+ if len(entries) != 1 || entries[0].Kind != KindDisplay {
181+ t.Fatalf("entries = %+v", entries)
182+ }
183+ lines := layout(entries, "mm", 20, 0)
184+ want := []Line{
185+ {Text: "‣ hook", Style: styleLabel},
186+ {Text: " {", Style: styleDisplay},
187+ {Text: ` "spell": "a very long spell name that is wider than the window"`, Style: styleDisplay},
188+ {Text: " }", Style: styleDisplay},
189+ }
190+ if len(lines) != len(want) {
191+ t.Fatalf("lines = %+v", lines)
192+ }
193+ for i := range want {
194+ if lines[i] != want[i] {
195+ t.Errorf("line %d = %+v, want %+v", i, lines[i], want[i])
196+ }
197+ }
198+}
@@ -170,3 +170,29 @@ func TestAnswersAndPlainAreRawText(t *testing.T) {
170 t.Errorf("Plain:\n got %q\nwant %q", got, want)170 t.Errorf("Plain:\n got %q\nwant %q", got, want)
171 }171 }
172 }172 }
173+
174+// A hook's display is its own entry, labelled, drawn line for line in its
175+// own colour: JSON keeps its indentation, nothing is wrapped.
176+func TestDisplayIsDrawnVerbatimUnderItsLabel(t *testing.T) {
177+ tr := &Transcript{}
178+ tr.Display("{\n \"spell\": \"a very long spell name that is wider than the window\"\n}")
179+ entries := tr.Entries()
180+ if len(entries) != 1 || entries[0].Kind != KindDisplay {
181+ t.Fatalf("entries = %+v", entries)
182+ }
183+ lines := layout(entries, "mm", 20, 0)
184+ want := []Line{
185+ {Text: "‣ hook", Style: styleLabel},
186+ {Text: " {", Style: styleDisplay},
187+ {Text: ` "spell": "a very long spell name that is wider than the window"`, Style: styleDisplay},
188+ {Text: " }", Style: styleDisplay},
189+ }
190+ if len(lines) != len(want) {
191+ t.Fatalf("lines = %+v", lines)
192+ }
193+ for i := range want {
194+ if lines[i] != want[i] {
195+ t.Errorf("line %d = %+v, want %+v", i, lines[i], want[i])
196+ }
197+ }
198+}
modified internal/ui/ui.go +29 -0
@@ -25,6 +25,7 @@ import (
2525 "sync/atomic"
2626
2727 "mm/internal/elicit"
28+ "mm/internal/spinner"
2829 )
2930
3031 // Out is where human-facing text goes when no Sink is active: the banner, the
@@ -155,6 +156,34 @@ func Notice(text string) {
155156 _, _ = fmt.Fprintln(Out, text)
156157 }
157158
159+// Displayer is the optional side of a Sink that shows text a hook asked to
160+// show the user (a preGeneration reply's `display`): never the answer, never
161+// sent to a model, drawn apart from both — its own colour in the window, a
162+// fenced block in an editor.
163+type Displayer interface {
164+ Display(text string)
165+}
166+
167+// displayColour is the terminal's colour for Display: bright blue, apart
168+// from the answer (plain), the notices (dim) and the ⚙ recap (green).
169+const displayColour = "\033[94m"
170+
171+// Display shows text a hook asked to show the user, before the answer: the
172+// active sink's Displayer, or the text on Out — coloured on a terminal,
173+// plain in a pipe.
174+//
175+// ui.Display("{\n \"spell\": \"sleep\"\n}")
176+func Display(text string) {
177+ if d, ok := ActiveSink().(Displayer); ok {
178+ d.Display(text)
179+ return
180+ }
181+ if spinner.Styled() {
182+ text = displayColour + text + "\033[0m"
183+ }
184+ _, _ = fmt.Fprintln(Out, text)
185+}
186+
158187 // Speaker is the optional side of a Sink that shows WHICH agent is answering:
159188 // between Speaking and Spoken, what streams through the sink (text, tool
160189 // calls) comes from another agent — the one the ask_agent tool asked — and
@@ -25,6 +25,7 @@ import (
25 "sync/atomic"25 "sync/atomic"
26 26
27 "mm/internal/elicit"27 "mm/internal/elicit"
28+ "mm/internal/spinner"
28 )29 )
29 30
30 // Out is where human-facing text goes when no Sink is active: the banner, the31 // Out is where human-facing text goes when no Sink is active: the banner, the
@@ -155,6 +156,34 @@ func Notice(text string) {
155 _, _ = fmt.Fprintln(Out, text)156 _, _ = fmt.Fprintln(Out, text)
156 }157 }
157 158
159+// Displayer is the optional side of a Sink that shows text a hook asked to
160+// show the user (a preGeneration reply's `display`): never the answer, never
161+// sent to a model, drawn apart from both — its own colour in the window, a
162+// fenced block in an editor.
163+type Displayer interface {
164+ Display(text string)
165+}
166+
167+// displayColour is the terminal's colour for Display: bright blue, apart
168+// from the answer (plain), the notices (dim) and the ⚙ recap (green).
169+const displayColour = "\033[94m"
170+
171+// Display shows text a hook asked to show the user, before the answer: the
172+// active sink's Displayer, or the text on Out — coloured on a terminal,
173+// plain in a pipe.
174+//
175+// ui.Display("{\n \"spell\": \"sleep\"\n}")
176+func Display(text string) {
177+ if d, ok := ActiveSink().(Displayer); ok {
178+ d.Display(text)
179+ return
180+ }
181+ if spinner.Styled() {
182+ text = displayColour + text + "\033[0m"
183+ }
184+ _, _ = fmt.Fprintln(Out, text)
185+}
186+
158 // Speaker is the optional side of a Sink that shows WHICH agent is answering:187 // Speaker is the optional side of a Sink that shows WHICH agent is answering:
159 // between Speaking and Spoken, what streams through the sink (text, tool188 // between Speaking and Spoken, what streams through the sink (text, tool
160 // calls) comes from another agent — the one the ask_agent tool asked — and189 // calls) comes from another agent — the one the ask_agent tool asked — and
modified internal/ui/ui_test.go +31 -0
@@ -85,3 +85,34 @@ func TestNoticeGoesToTheSinkOrToOut(t *testing.T) {
8585 t.Errorf("window saw %v", window.seen)
8686 }
8787 }
88+
89+// displayingSink is a front end that shows hook displays itself.
90+type displayingSink struct {
91+ stubSink
92+ seen []string
93+}
94+
95+func (s *displayingSink) Display(text string) { s.seen = append(s.seen, text) }
96+
97+// A hook's display goes to the sink that can show it, else to Out (plain:
98+// the tests' Out is not a terminal) — never both.
99+func TestDisplayGoesToTheSinkOrToOut(t *testing.T) {
100+ var out strings.Builder
101+ old := Out
102+ Out = &out
103+ defer func() { Out = old }()
104+
105+ SetActive(nil)
106+ Display(`{"spell": "sleep"}`)
107+ window := &displayingSink{}
108+ SetActive(window)
109+ Display("window block")
110+ SetActive(nil)
111+
112+ if got := out.String(); got != "{\"spell\": \"sleep\"}\n" {
113+ t.Errorf("Out = %q", got)
114+ }
115+ if len(window.seen) != 1 || window.seen[0] != "window block" {
116+ t.Errorf("window saw %v", window.seen)
117+ }
118+}
@@ -85,3 +85,34 @@ func TestNoticeGoesToTheSinkOrToOut(t *testing.T) {
85 t.Errorf("window saw %v", window.seen)85 t.Errorf("window saw %v", window.seen)
86 }86 }
87 }87 }
88+
89+// displayingSink is a front end that shows hook displays itself.
90+type displayingSink struct {
91+ stubSink
92+ seen []string
93+}
94+
95+func (s *displayingSink) Display(text string) { s.seen = append(s.seen, text) }
96+
97+// A hook's display goes to the sink that can show it, else to Out (plain:
98+// the tests' Out is not a terminal) — never both.
99+func TestDisplayGoesToTheSinkOrToOut(t *testing.T) {
100+ var out strings.Builder
101+ old := Out
102+ Out = &out
103+ defer func() { Out = old }()
104+
105+ SetActive(nil)
106+ Display(`{"spell": "sleep"}`)
107+ window := &displayingSink{}
108+ SetActive(window)
109+ Display("window block")
110+ SetActive(nil)
111+
112+ if got := out.String(); got != "{\"spell\": \"sleep\"}\n" {
113+ t.Errorf("Out = %q", got)
114+ }
115+ if len(window.seen) != 1 || window.seen[0] != "window block" {
116+ t.Errorf("window saw %v", window.seen)
117+ }
118+}