hooks #30
Merged@k33g wants to merge
feature/hooks into mainMerged as
cc36e7a14547.47 files changed
modified
.tickets/issues/0036-hooks.yaml +109 -2 | @@ -6,7 +6,7 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-10-06T13:16:31.213Z | |
| 9 | +updatedAt: 2026-10-14T12:06:43.000Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | epic: Loop |
| 12 | 12 | body: | |
| @@ -524,5 +524,112 @@ body: | | ||
| 524 | 524 | 5. Should hooks be able to modify tool configurations? |
| 525 | 525 | - Current: Tools are read-only |
| 526 | 526 | - Suggestion: No, for security |
| 527 | -tasks: [] | |
| 527 | + | |
| 528 | + ## Design retained for v1 (2026-10-14) | |
| 529 | + | |
| 530 | + The sections above are the original exploration. v1 keeps the core of that design and simplifies it. This section is what is actually implemented. | |
| 531 | + | |
| 532 | + ### Where the hooks run | |
| 533 | + All four front ends (REPL, window, ACP, `-p`) go through `engine.Generate`, and Genkit's middleware there already has a `WrapTool` that sees **every** tool call, built-in and MCP. Hooks plug in at those two points, so no front end has to change. | |
| 534 | + | |
| 535 | + ### Events | |
| 536 | + | Event | When | Can do | | |
| 537 | + | --- | --- | --- | | |
| 538 | + | `preGeneration` | once per user question, before the first model call | rewrite the prompt (`prompt`), add context (`context`), stop the turn (`action: stop` + `response`) | | |
| 539 | + | `preToolCall` | before each tool call; optional `matcher` (regexp on the tool name) | deny the call (`action: deny` + `reason`, which the model reads as the tool's output) or replace its `input` | | |
| 540 | + | `postToolCall` | after each tool call; optional `matcher` | observe; append `context` to the tool's output | | |
| 541 | + | `postGeneration` | after the turn, success or failure | observe (logging, notification) | | |
| 542 | + | |
| 543 | + ### Protocol | |
| 544 | + - **stdin:** one JSON object, for example `{"event":"preToolCall","cwd":"/abs/dir","model":"…","tool":{"id":"…","name":"bash","input":{…}}}`. `preGeneration` carries `prompt`; `postToolCall` adds `output`; `postGeneration` carries `answer` and `error`. | |
| 545 | + - **stdout:** one JSON object, or **nothing, which means `continue`**. A logging hook does not have to print anything. | |
| 546 | + - **stderr:** shown only when the hook fails. | |
| 547 | + - **Execution:** `sh -c "<command>"` in the session's working directory, with `MM_HOOK_EVENT` and `MM_CONFIG_DIR` (the YAML file's directory) in the environment, so you can write `node "$MM_CONFIG_DIR/hooks/x.js"`. | |
| 548 | + - **Chain:** hooks run in list order, each one sees the previous one's rewrite, and the first `stop`/`deny` ends the chain. | |
| 549 | + - **Failure** (non-zero exit, timeout, invalid JSON, `action: error`): `onError: fail | warn | ignore`, default `warn`. `fail` stops the turn or denies the tool. `timeout` defaults to 10s. Output is capped at 1 MiB. | |
| 550 | + - **Stopped turn:** the history keeps `[prompt blocked by a hook: <reason>]` and the hook's response. The blocked text (a secret, for example) never reaches the model, not even on a later turn. This was the user's choice. | |
| 551 | + | |
| 552 | + ### Configuration | |
| 553 | + ```yaml | |
| 554 | + hooks: | |
| 555 | + preGeneration: | |
| 556 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | |
| 557 | + timeout: 5s | |
| 558 | + onError: fail | |
| 559 | + preToolCall: | |
| 560 | + - matcher: ^bash$ | |
| 561 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | |
| 562 | + postToolCall: | |
| 563 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 564 | + postGeneration: | |
| 565 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 566 | + ``` | |
| 567 | + | |
| 568 | + ### Left out of v1 (possible follow-ups) | |
| 569 | + - **Lifecycle hooks** (preStartup, postConfig, preShutdown, onError), **session hooks** and **front-end hooks**: no use case yet. | |
| 570 | + - **`async` mode and numeric priorities:** list order is enough, and a log hook is fast. | |
| 571 | + - **Returning the full `messages` array:** fragile, since a hook could rewrite history or drop the system prompt. `prompt` + `context` cover the four requirements (detect, rewrite, add context, stop). | |
| 572 | + - **`allowedPaths` / `allowedExtensions`:** the YAML already starts MCP servers and gives the model `bash`. Whoever writes the YAML is trusted, so an allowlist would protect nothing more. | |
| 573 | + - On the original open questions: no hook state directory (a hook can write files itself); hooks cannot run tools; there is no total chain timeout, only a per-hook one. | |
| 574 | +tasks: | |
| 575 | + - id: 1 | |
| 576 | + title: "Config: hooks block and validation" | |
| 577 | + state: closed | |
| 578 | + priority: high | |
| 579 | + author: | |
| 580 | + name: k33g | |
| 581 | + email: ph.charriere@gmail.com | |
| 582 | + createdAt: 2026-10-14T11:42:48.000Z | |
| 583 | + updatedAt: 2026-10-14T11:44:02.000Z | |
| 584 | + closedAt: 2026-10-14T11:44:02.000Z | |
| 585 | + - id: 2 | |
| 586 | + title: "internal/hooks: process runner, protocol, chain, onError" | |
| 587 | + state: closed | |
| 588 | + priority: high | |
| 589 | + author: | |
| 590 | + name: k33g | |
| 591 | + email: ph.charriere@gmail.com | |
| 592 | + createdAt: 2026-10-14T11:42:48.000Z | |
| 593 | + updatedAt: 2026-10-14T11:53:54.000Z | |
| 594 | + closedAt: 2026-10-14T11:53:54.000Z | |
| 595 | + - id: 3 | |
| 596 | + title: "Engine: preGeneration and postGeneration" | |
| 597 | + state: closed | |
| 598 | + priority: high | |
| 599 | + author: | |
| 600 | + name: k33g | |
| 601 | + email: ph.charriere@gmail.com | |
| 602 | + createdAt: 2026-10-14T11:42:48.000Z | |
| 603 | + updatedAt: 2026-10-14T11:57:09.000Z | |
| 604 | + closedAt: 2026-10-14T11:57:09.000Z | |
| 605 | + - id: 4 | |
| 606 | + title: "Engine: preToolCall and postToolCall (WrapTool), display, banner" | |
| 607 | + state: closed | |
| 608 | + priority: high | |
| 609 | + author: | |
| 610 | + name: k33g | |
| 611 | + email: ph.charriere@gmail.com | |
| 612 | + createdAt: 2026-10-14T11:42:48.000Z | |
| 613 | + updatedAt: 2026-10-14T11:57:09.000Z | |
| 614 | + closedAt: 2026-10-14T11:57:09.000Z | |
| 615 | + - id: 5 | |
| 616 | + title: "Demo demos/hooks with Node.js hooks (README EN+FR)" | |
| 617 | + state: closed | |
| 618 | + priority: high | |
| 619 | + author: | |
| 620 | + name: k33g | |
| 621 | + email: ph.charriere@gmail.com | |
| 622 | + createdAt: 2026-10-14T11:42:48.000Z | |
| 623 | + updatedAt: 2026-10-14T12:00:05.000Z | |
| 624 | + closedAt: 2026-10-14T12:00:05.000Z | |
| 625 | + - id: 6 | |
| 626 | + title: "Quality gate, docs EN+FR, packages diagram, .memory" | |
| 627 | + state: closed | |
| 628 | + priority: high | |
| 629 | + author: | |
| 630 | + name: k33g | |
| 631 | + email: ph.charriere@gmail.com | |
| 632 | + createdAt: 2026-10-14T11:42:48.000Z | |
| 633 | + updatedAt: 2026-10-14T12:06:43.000Z | |
| 634 | + closedAt: 2026-10-14T12:06:43.000Z | |
| 528 | 635 | comments: [] |
| @@ -6,7 +6,7 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T06:32:48.566Z | 8 | createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-10-06T13:16:31.213Z | 9 | +updatedAt: 2026-10-14T12:06:43.000Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | epic: Loop | 11 | epic: Loop |
| 12 | body: | | 12 | body: | |
| @@ -524,5 +524,112 @@ body: | | |||
| 524 | 5. Should hooks be able to modify tool configurations? | 524 | 5. Should hooks be able to modify tool configurations? |
| 525 | - Current: Tools are read-only | 525 | - Current: Tools are read-only |
| 526 | - Suggestion: No, for security | 526 | - Suggestion: No, for security |
| 527 | -tasks: [] | 527 | + |
| 528 | + ## Design retained for v1 (2026-10-14) | ||
| 529 | + | ||
| 530 | + The sections above are the original exploration. v1 keeps the core of that design and simplifies it. This section is what is actually implemented. | ||
| 531 | + | ||
| 532 | + ### Where the hooks run | ||
| 533 | + All four front ends (REPL, window, ACP, `-p`) go through `engine.Generate`, and Genkit's middleware there already has a `WrapTool` that sees **every** tool call, built-in and MCP. Hooks plug in at those two points, so no front end has to change. | ||
| 534 | + | ||
| 535 | + ### Events | ||
| 536 | + | Event | When | Can do | | ||
| 537 | + | --- | --- | --- | | ||
| 538 | + | `preGeneration` | once per user question, before the first model call | rewrite the prompt (`prompt`), add context (`context`), stop the turn (`action: stop` + `response`) | | ||
| 539 | + | `preToolCall` | before each tool call; optional `matcher` (regexp on the tool name) | deny the call (`action: deny` + `reason`, which the model reads as the tool's output) or replace its `input` | | ||
| 540 | + | `postToolCall` | after each tool call; optional `matcher` | observe; append `context` to the tool's output | | ||
| 541 | + | `postGeneration` | after the turn, success or failure | observe (logging, notification) | | ||
| 542 | + | ||
| 543 | + ### Protocol | ||
| 544 | + - **stdin:** one JSON object, for example `{"event":"preToolCall","cwd":"/abs/dir","model":"…","tool":{"id":"…","name":"bash","input":{…}}}`. `preGeneration` carries `prompt`; `postToolCall` adds `output`; `postGeneration` carries `answer` and `error`. | ||
| 545 | + - **stdout:** one JSON object, or **nothing, which means `continue`**. A logging hook does not have to print anything. | ||
| 546 | + - **stderr:** shown only when the hook fails. | ||
| 547 | + - **Execution:** `sh -c "<command>"` in the session's working directory, with `MM_HOOK_EVENT` and `MM_CONFIG_DIR` (the YAML file's directory) in the environment, so you can write `node "$MM_CONFIG_DIR/hooks/x.js"`. | ||
| 548 | + - **Chain:** hooks run in list order, each one sees the previous one's rewrite, and the first `stop`/`deny` ends the chain. | ||
| 549 | + - **Failure** (non-zero exit, timeout, invalid JSON, `action: error`): `onError: fail | warn | ignore`, default `warn`. `fail` stops the turn or denies the tool. `timeout` defaults to 10s. Output is capped at 1 MiB. | ||
| 550 | + - **Stopped turn:** the history keeps `[prompt blocked by a hook: <reason>]` and the hook's response. The blocked text (a secret, for example) never reaches the model, not even on a later turn. This was the user's choice. | ||
| 551 | + | ||
| 552 | + ### Configuration | ||
| 553 | + ```yaml | ||
| 554 | + hooks: | ||
| 555 | + preGeneration: | ||
| 556 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | ||
| 557 | + timeout: 5s | ||
| 558 | + onError: fail | ||
| 559 | + preToolCall: | ||
| 560 | + - matcher: ^bash$ | ||
| 561 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | ||
| 562 | + postToolCall: | ||
| 563 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 564 | + postGeneration: | ||
| 565 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 566 | + ``` | ||
| 567 | + | ||
| 568 | + ### Left out of v1 (possible follow-ups) | ||
| 569 | + - **Lifecycle hooks** (preStartup, postConfig, preShutdown, onError), **session hooks** and **front-end hooks**: no use case yet. | ||
| 570 | + - **`async` mode and numeric priorities:** list order is enough, and a log hook is fast. | ||
| 571 | + - **Returning the full `messages` array:** fragile, since a hook could rewrite history or drop the system prompt. `prompt` + `context` cover the four requirements (detect, rewrite, add context, stop). | ||
| 572 | + - **`allowedPaths` / `allowedExtensions`:** the YAML already starts MCP servers and gives the model `bash`. Whoever writes the YAML is trusted, so an allowlist would protect nothing more. | ||
| 573 | + - On the original open questions: no hook state directory (a hook can write files itself); hooks cannot run tools; there is no total chain timeout, only a per-hook one. | ||
| 574 | +tasks: | ||
| 575 | + - id: 1 | ||
| 576 | + title: "Config: hooks block and validation" | ||
| 577 | + state: closed | ||
| 578 | + priority: high | ||
| 579 | + author: | ||
| 580 | + name: k33g | ||
| 581 | + email: ph.charriere@gmail.com | ||
| 582 | + createdAt: 2026-10-14T11:42:48.000Z | ||
| 583 | + updatedAt: 2026-10-14T11:44:02.000Z | ||
| 584 | + closedAt: 2026-10-14T11:44:02.000Z | ||
| 585 | + - id: 2 | ||
| 586 | + title: "internal/hooks: process runner, protocol, chain, onError" | ||
| 587 | + state: closed | ||
| 588 | + priority: high | ||
| 589 | + author: | ||
| 590 | + name: k33g | ||
| 591 | + email: ph.charriere@gmail.com | ||
| 592 | + createdAt: 2026-10-14T11:42:48.000Z | ||
| 593 | + updatedAt: 2026-10-14T11:53:54.000Z | ||
| 594 | + closedAt: 2026-10-14T11:53:54.000Z | ||
| 595 | + - id: 3 | ||
| 596 | + title: "Engine: preGeneration and postGeneration" | ||
| 597 | + state: closed | ||
| 598 | + priority: high | ||
| 599 | + author: | ||
| 600 | + name: k33g | ||
| 601 | + email: ph.charriere@gmail.com | ||
| 602 | + createdAt: 2026-10-14T11:42:48.000Z | ||
| 603 | + updatedAt: 2026-10-14T11:57:09.000Z | ||
| 604 | + closedAt: 2026-10-14T11:57:09.000Z | ||
| 605 | + - id: 4 | ||
| 606 | + title: "Engine: preToolCall and postToolCall (WrapTool), display, banner" | ||
| 607 | + state: closed | ||
| 608 | + priority: high | ||
| 609 | + author: | ||
| 610 | + name: k33g | ||
| 611 | + email: ph.charriere@gmail.com | ||
| 612 | + createdAt: 2026-10-14T11:42:48.000Z | ||
| 613 | + updatedAt: 2026-10-14T11:57:09.000Z | ||
| 614 | + closedAt: 2026-10-14T11:57:09.000Z | ||
| 615 | + - id: 5 | ||
| 616 | + title: "Demo demos/hooks with Node.js hooks (README EN+FR)" | ||
| 617 | + state: closed | ||
| 618 | + priority: high | ||
| 619 | + author: | ||
| 620 | + name: k33g | ||
| 621 | + email: ph.charriere@gmail.com | ||
| 622 | + createdAt: 2026-10-14T11:42:48.000Z | ||
| 623 | + updatedAt: 2026-10-14T12:00:05.000Z | ||
| 624 | + closedAt: 2026-10-14T12:00:05.000Z | ||
| 625 | + - id: 6 | ||
| 626 | + title: "Quality gate, docs EN+FR, packages diagram, .memory" | ||
| 627 | + state: closed | ||
| 628 | + priority: high | ||
| 629 | + author: | ||
| 630 | + name: k33g | ||
| 631 | + email: ph.charriere@gmail.com | ||
| 632 | + createdAt: 2026-10-14T11:42:48.000Z | ||
| 633 | + updatedAt: 2026-10-14T12:06:43.000Z | ||
| 634 | + closedAt: 2026-10-14T12:06:43.000Z | ||
| 528 | comments: [] | 635 | comments: [] |
modified
README.md +2 -0 | @@ -116,6 +116,7 @@ Anything that speaks ACP can host `mm -acp` the same way: **JetBrains IDEs** (In | ||
| 116 | 116 | - **MCP servers**: their tools, wrapped as mm tools. See the [MCP tutorial](docs/en/tutorials/mcp-getting-started.md). |
| 117 | 117 | - **Sessions**: with `sessions.enabled: true`, `/sessions` lists the conversations of a project and `/resume` continues one. See [Keep sessions and resume them](docs/en/how-to/keep-sessions.md). |
| 118 | 118 | - **Trace**: `AGENT_TRACE=true mm …` writes every request to the model and every response to `.mm/traces/`. See [See the JSON sent to the model](docs/en/how-to/inspect-model-traffic.md). |
| 119 | +- **Hooks**: your own commands — in any language — run before a question reaches the model, before and after each tool call, and after the turn; they can deny a call, stop or rewrite a question, add context, or log. See [Guard the agent with hooks](docs/en/how-to/guard-the-agent-with-hooks.md) and `demos/hooks/`. | |
| 119 | 120 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). |
| 120 | 121 | |
| 121 | 122 | ## Demos |
| @@ -139,6 +140,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h | ||
| 139 | 140 | | `demos/codex-style/` | the same idea with Codex CLI's default instructions (preambles, plans, ambition vs. precision, final-answer format), adapted to mm and to gpt-oss 20B (harmony format, `reasoning_effort`, `model_identity`) on llmman; measured results | |
| 140 | 141 | | `demos/news-roundup/` | a press-review agent: DuckDuckGo News through a small MCP server (Python standard library), Qwen3.5 9B on llmman, ready-made citations so no article is invented, measured results | |
| 141 | 142 | | `demos/project-wizard/` | the interactive prompts (`ask_user`) in a small agent: a four-question form (text, two choices, several choices), then a yes/no confirmation carrying the plan before any file is written; Qwen3.5 9B on llmman, measured results | |
| 143 | +| `demos/hooks/` | hooks in Node.js around a small shell assistant (Gemma 4 E4B on llmman): a question holding a secret never reaches the model, today's date and the file list are added to every question, `rm -r` / `sudo` / `curl \| sh` are denied, every tool call and turn is logged to `.mm/hooks-audit.jsonl` | | |
| 142 | 144 | |
| 143 | 145 | ## Learn more |
| 144 | 146 | |
| @@ -116,6 +116,7 @@ Anything that speaks ACP can host `mm -acp` the same way: **JetBrains IDEs** (In | |||
| 116 | - **MCP servers**: their tools, wrapped as mm tools. See the [MCP tutorial](docs/en/tutorials/mcp-getting-started.md). | 116 | - **MCP servers**: their tools, wrapped as mm tools. See the [MCP tutorial](docs/en/tutorials/mcp-getting-started.md). |
| 117 | - **Sessions**: with `sessions.enabled: true`, `/sessions` lists the conversations of a project and `/resume` continues one. See [Keep sessions and resume them](docs/en/how-to/keep-sessions.md). | 117 | - **Sessions**: with `sessions.enabled: true`, `/sessions` lists the conversations of a project and `/resume` continues one. See [Keep sessions and resume them](docs/en/how-to/keep-sessions.md). |
| 118 | - **Trace**: `AGENT_TRACE=true mm …` writes every request to the model and every response to `.mm/traces/`. See [See the JSON sent to the model](docs/en/how-to/inspect-model-traffic.md). | 118 | - **Trace**: `AGENT_TRACE=true mm …` writes every request to the model and every response to `.mm/traces/`. See [See the JSON sent to the model](docs/en/how-to/inspect-model-traffic.md). |
| 119 | +- **Hooks**: your own commands — in any language — run before a question reaches the model, before and after each tool call, and after the turn; they can deny a call, stop or rewrite a question, add context, or log. See [Guard the agent with hooks](docs/en/how-to/guard-the-agent-with-hooks.md) and `demos/hooks/`. | ||
| 119 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). | 120 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). |
| 120 | 121 | ||
| 121 | ## Demos | 122 | ## Demos |
| @@ -139,6 +140,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h | |||
| 139 | | `demos/codex-style/` | the same idea with Codex CLI's default instructions (preambles, plans, ambition vs. precision, final-answer format), adapted to mm and to gpt-oss 20B (harmony format, `reasoning_effort`, `model_identity`) on llmman; measured results | | 140 | | `demos/codex-style/` | the same idea with Codex CLI's default instructions (preambles, plans, ambition vs. precision, final-answer format), adapted to mm and to gpt-oss 20B (harmony format, `reasoning_effort`, `model_identity`) on llmman; measured results | |
| 140 | | `demos/news-roundup/` | a press-review agent: DuckDuckGo News through a small MCP server (Python standard library), Qwen3.5 9B on llmman, ready-made citations so no article is invented, measured results | | 141 | | `demos/news-roundup/` | a press-review agent: DuckDuckGo News through a small MCP server (Python standard library), Qwen3.5 9B on llmman, ready-made citations so no article is invented, measured results | |
| 141 | | `demos/project-wizard/` | the interactive prompts (`ask_user`) in a small agent: a four-question form (text, two choices, several choices), then a yes/no confirmation carrying the plan before any file is written; Qwen3.5 9B on llmman, measured results | | 142 | | `demos/project-wizard/` | the interactive prompts (`ask_user`) in a small agent: a four-question form (text, two choices, several choices), then a yes/no confirmation carrying the plan before any file is written; Qwen3.5 9B on llmman, measured results | |
| 143 | +| `demos/hooks/` | hooks in Node.js around a small shell assistant (Gemma 4 E4B on llmman): a question holding a secret never reaches the model, today's date and the file list are added to every question, `rm -r` / `sudo` / `curl \| sh` are denied, every tool call and turn is logged to `.mm/hooks-audit.jsonl` | | ||
| 142 | 144 | ||
| 143 | ## Learn more | 145 | ## Learn more |
| 144 | 146 | ||
added
demos/hooks/README.en.md +125 -0 | new file mode 100644 | ||
| @@ -0,0 +1,125 @@ | ||
| 1 | +# Hooks demo — four Node.js hooks around a small shell assistant | |
| 2 | + | |
| 3 | +*[Version française](README.fr.md)* | |
| 4 | + | |
| 5 | +A hook is a command mm runs at a fixed moment of a turn. It reads one JSON object on stdin and may answer one JSON object on stdout. This demo puts four hooks, written in Node.js, around an agent that can run shell commands. Each hook shows one thing a hook can do: | |
| 6 | + | |
| 7 | +| Hook | Event | What it shows | | |
| 8 | +|---|---|---| | |
| 9 | +| `hooks/no-secrets.js` | `preGeneration` | **stop** a turn: a question holding a password or an API key never reaches the model | | |
| 10 | +| `hooks/add-context.js` | `preGeneration` | **add context**: today's date and the list of files, appended to every question | | |
| 11 | +| `hooks/bash-guard.js` | `preToolCall` (`matcher: ^bash$`) | **deny** a tool call: `rm -r`, `sudo`, `curl … \| sh` never run, and the model reads why | | |
| 12 | +| `hooks/audit.js` | `postToolCall` + `postGeneration` | **observe**: one JSON line per tool call and per turn in `.mm/hooks-audit.jsonl` | | |
| 13 | + | |
| 14 | +Model: **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`, ≈ 6 GB) on **llmman**. The hooks themselves do not depend on the model: they run the same way whatever answers. | |
| 15 | + | |
| 16 | +## Run | |
| 17 | + | |
| 18 | +Prerequisites: [llmman](https://llmmanorg.github.io/) running, and Node.js on the PATH. | |
| 19 | + | |
| 20 | +```bash | |
| 21 | +llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M | |
| 22 | +cd demos/hooks/playground | |
| 23 | +mm ../agent.yaml | |
| 24 | +``` | |
| 25 | + | |
| 26 | +The banner lists the hooks: | |
| 27 | + | |
| 28 | +``` | |
| 29 | +… | hooks: preGeneration×2 preToolCall×1 postToolCall×1 postGeneration×1 | |
| 30 | +``` | |
| 31 | + | |
| 32 | +The agent works in `playground/`, a directory with three harmless files. The hooks are found through `$MM_CONFIG_DIR`, the directory of `agent.yaml`, so mm can be started from any directory. | |
| 33 | + | |
| 34 | +## Try it | |
| 35 | + | |
| 36 | +Each line below is a question to type, followed by what was seen on 2026-10-14 (`mm -p "<question>" ../agent.yaml`): | |
| 37 | + | |
| 38 | +**1. A secret is stopped.** The model is never called: | |
| 39 | + | |
| 40 | +``` | |
| 41 | +> My password: hunter2 — can you store it in a file? | |
| 42 | +🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again. | |
| 43 | +``` | |
| 44 | + | |
| 45 | +The history keeps `[prompt blocked by a hook: the prompt contains a password]`, never the password. The audit log gets the same marker. | |
| 46 | + | |
| 47 | +**2. Context is added.** The model answers without running anything, from what `add-context.js` added: | |
| 48 | + | |
| 49 | +``` | |
| 50 | +> Which files are here, and what is today's date? | |
| 51 | +🤖 The files in the directory are `notes.md`, `old-drafts`, and `todo.txt`. Today's date is 2026-10-14. | |
| 52 | +``` | |
| 53 | + | |
| 54 | +**3. A dangerous command is denied.** The model asks for `rm -rf old-drafts`, the hook refuses it, the directory is still there: | |
| 55 | + | |
| 56 | +``` | |
| 57 | +> Delete the old-drafts directory with rm -rf. | |
| 58 | +🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed] | |
| 59 | +🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook. | |
| 60 | +``` | |
| 61 | + | |
| 62 | +**4. Everything is logged.** After an ordinary question (`What is in todo.txt?`), `playground/.mm/hooks-audit.jsonl` holds: | |
| 63 | + | |
| 64 | +```json | |
| 65 | +{"at":"…","event":"postToolCall","tool":"bash","input":{"command":"cat todo.txt"},"output":"Buy milk\nCall the plumber\n"} | |
| 66 | +{"at":"…","event":"postGeneration","prompt":"What is in todo.txt?","answer":"The file `todo.txt` contains: …"} | |
| 67 | +``` | |
| 68 | + | |
| 69 | +## How a hook is written | |
| 70 | + | |
| 71 | +`hooks/bash-guard.js`, without its comments: | |
| 72 | + | |
| 73 | +```js | |
| 74 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | |
| 75 | +const command = input.tool.input.command || ""; | |
| 76 | + | |
| 77 | +const rules = [ | |
| 78 | + { reason: "recursive deletion (rm -r) is not allowed", pattern: /\brm\s+(-\w*\s+)*-\w*[rR]/ }, | |
| 79 | + { reason: "sudo is not allowed", pattern: /\bsudo\b/ }, | |
| 80 | +]; | |
| 81 | + | |
| 82 | +const broken = rules.find((r) => r.pattern.test(command)); | |
| 83 | +if (broken) { | |
| 84 | + console.log(JSON.stringify({ action: "deny", reason: broken.reason })); | |
| 85 | +} | |
| 86 | +``` | |
| 87 | + | |
| 88 | +It reads its input, decides, and prints a reply only when it has something to say: **an empty stdout means "continue"**. Test a hook without mm by giving it some input: | |
| 89 | + | |
| 90 | +```bash | |
| 91 | +echo '{"event":"preToolCall","cwd":"/tmp","tool":{"name":"bash","input":{"command":"sudo ls"}}}' \ | |
| 92 | + | node hooks/bash-guard.js | |
| 93 | +# {"action":"deny","reason":"sudo is not allowed"} | |
| 94 | +``` | |
| 95 | + | |
| 96 | +## The configuration | |
| 97 | + | |
| 98 | +```yaml | |
| 99 | +hooks: | |
| 100 | + preGeneration: | |
| 101 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | |
| 102 | + timeout: 5s | |
| 103 | + onError: fail # a guard that cannot run must not let the question through | |
| 104 | + - command: node "$MM_CONFIG_DIR/hooks/add-context.js" | |
| 105 | + timeout: 5s | |
| 106 | + preToolCall: | |
| 107 | + - matcher: ^bash$ # a regular expression on the tool name | |
| 108 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | |
| 109 | + timeout: 5s | |
| 110 | + onError: fail | |
| 111 | + postToolCall: | |
| 112 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 113 | + onError: ignore # losing an audit line must not disturb the session | |
| 114 | + postGeneration: | |
| 115 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 116 | + onError: ignore | |
| 117 | +``` | |
| 118 | + | |
| 119 | +`yolo: true` here: the tools run without asking, and `bash-guard.js` is what stands between the model and a dangerous command. A `preToolCall` hook runs **before** the tool's own consent prompt, so with `yolo: false` a denied call never even asks. | |
| 120 | + | |
| 121 | +## Going further | |
| 122 | + | |
| 123 | +- Every field of the input and of the reply, the order hooks run in, `onError`: [Hooks reference](../../docs/en/reference/hooks.md). | |
| 124 | +- Writing your own hook step by step: [Guard the agent with hooks](../../docs/en/how-to/guard-the-agent-with-hooks.md). | |
| 125 | +- Why hooks are external commands and where they plug in: [Hooks](../../docs/en/explanation/hooks.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | +# Hooks demo — four Node.js hooks around a small shell assistant | ||
| 2 | + | ||
| 3 | +*[Version française](README.fr.md)* | ||
| 4 | + | ||
| 5 | +A hook is a command mm runs at a fixed moment of a turn. It reads one JSON object on stdin and may answer one JSON object on stdout. This demo puts four hooks, written in Node.js, around an agent that can run shell commands. Each hook shows one thing a hook can do: | ||
| 6 | + | ||
| 7 | +| Hook | Event | What it shows | | ||
| 8 | +|---|---|---| | ||
| 9 | +| `hooks/no-secrets.js` | `preGeneration` | **stop** a turn: a question holding a password or an API key never reaches the model | | ||
| 10 | +| `hooks/add-context.js` | `preGeneration` | **add context**: today's date and the list of files, appended to every question | | ||
| 11 | +| `hooks/bash-guard.js` | `preToolCall` (`matcher: ^bash$`) | **deny** a tool call: `rm -r`, `sudo`, `curl … \| sh` never run, and the model reads why | | ||
| 12 | +| `hooks/audit.js` | `postToolCall` + `postGeneration` | **observe**: one JSON line per tool call and per turn in `.mm/hooks-audit.jsonl` | | ||
| 13 | + | ||
| 14 | +Model: **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`, ≈ 6 GB) on **llmman**. The hooks themselves do not depend on the model: they run the same way whatever answers. | ||
| 15 | + | ||
| 16 | +## Run | ||
| 17 | + | ||
| 18 | +Prerequisites: [llmman](https://llmmanorg.github.io/) running, and Node.js on the PATH. | ||
| 19 | + | ||
| 20 | +```bash | ||
| 21 | +llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M | ||
| 22 | +cd demos/hooks/playground | ||
| 23 | +mm ../agent.yaml | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +The banner lists the hooks: | ||
| 27 | + | ||
| 28 | +``` | ||
| 29 | +… | hooks: preGeneration×2 preToolCall×1 postToolCall×1 postGeneration×1 | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +The agent works in `playground/`, a directory with three harmless files. The hooks are found through `$MM_CONFIG_DIR`, the directory of `agent.yaml`, so mm can be started from any directory. | ||
| 33 | + | ||
| 34 | +## Try it | ||
| 35 | + | ||
| 36 | +Each line below is a question to type, followed by what was seen on 2026-10-14 (`mm -p "<question>" ../agent.yaml`): | ||
| 37 | + | ||
| 38 | +**1. A secret is stopped.** The model is never called: | ||
| 39 | + | ||
| 40 | +``` | ||
| 41 | +> My password: hunter2 — can you store it in a file? | ||
| 42 | +🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again. | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +The history keeps `[prompt blocked by a hook: the prompt contains a password]`, never the password. The audit log gets the same marker. | ||
| 46 | + | ||
| 47 | +**2. Context is added.** The model answers without running anything, from what `add-context.js` added: | ||
| 48 | + | ||
| 49 | +``` | ||
| 50 | +> Which files are here, and what is today's date? | ||
| 51 | +🤖 The files in the directory are `notes.md`, `old-drafts`, and `todo.txt`. Today's date is 2026-10-14. | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +**3. A dangerous command is denied.** The model asks for `rm -rf old-drafts`, the hook refuses it, the directory is still there: | ||
| 55 | + | ||
| 56 | +``` | ||
| 57 | +> Delete the old-drafts directory with rm -rf. | ||
| 58 | +🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed] | ||
| 59 | +🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook. | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +**4. Everything is logged.** After an ordinary question (`What is in todo.txt?`), `playground/.mm/hooks-audit.jsonl` holds: | ||
| 63 | + | ||
| 64 | +```json | ||
| 65 | +{"at":"…","event":"postToolCall","tool":"bash","input":{"command":"cat todo.txt"},"output":"Buy milk\nCall the plumber\n"} | ||
| 66 | +{"at":"…","event":"postGeneration","prompt":"What is in todo.txt?","answer":"The file `todo.txt` contains: …"} | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## How a hook is written | ||
| 70 | + | ||
| 71 | +`hooks/bash-guard.js`, without its comments: | ||
| 72 | + | ||
| 73 | +```js | ||
| 74 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | ||
| 75 | +const command = input.tool.input.command || ""; | ||
| 76 | + | ||
| 77 | +const rules = [ | ||
| 78 | + { reason: "recursive deletion (rm -r) is not allowed", pattern: /\brm\s+(-\w*\s+)*-\w*[rR]/ }, | ||
| 79 | + { reason: "sudo is not allowed", pattern: /\bsudo\b/ }, | ||
| 80 | +]; | ||
| 81 | + | ||
| 82 | +const broken = rules.find((r) => r.pattern.test(command)); | ||
| 83 | +if (broken) { | ||
| 84 | + console.log(JSON.stringify({ action: "deny", reason: broken.reason })); | ||
| 85 | +} | ||
| 86 | +``` | ||
| 87 | + | ||
| 88 | +It reads its input, decides, and prints a reply only when it has something to say: **an empty stdout means "continue"**. Test a hook without mm by giving it some input: | ||
| 89 | + | ||
| 90 | +```bash | ||
| 91 | +echo '{"event":"preToolCall","cwd":"/tmp","tool":{"name":"bash","input":{"command":"sudo ls"}}}' \ | ||
| 92 | + | node hooks/bash-guard.js | ||
| 93 | +# {"action":"deny","reason":"sudo is not allowed"} | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +## The configuration | ||
| 97 | + | ||
| 98 | +```yaml | ||
| 99 | +hooks: | ||
| 100 | + preGeneration: | ||
| 101 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | ||
| 102 | + timeout: 5s | ||
| 103 | + onError: fail # a guard that cannot run must not let the question through | ||
| 104 | + - command: node "$MM_CONFIG_DIR/hooks/add-context.js" | ||
| 105 | + timeout: 5s | ||
| 106 | + preToolCall: | ||
| 107 | + - matcher: ^bash$ # a regular expression on the tool name | ||
| 108 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | ||
| 109 | + timeout: 5s | ||
| 110 | + onError: fail | ||
| 111 | + postToolCall: | ||
| 112 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 113 | + onError: ignore # losing an audit line must not disturb the session | ||
| 114 | + postGeneration: | ||
| 115 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 116 | + onError: ignore | ||
| 117 | +``` | ||
| 118 | + | ||
| 119 | +`yolo: true` here: the tools run without asking, and `bash-guard.js` is what stands between the model and a dangerous command. A `preToolCall` hook runs **before** the tool's own consent prompt, so with `yolo: false` a denied call never even asks. | ||
| 120 | + | ||
| 121 | +## Going further | ||
| 122 | + | ||
| 123 | +- Every field of the input and of the reply, the order hooks run in, `onError`: [Hooks reference](../../docs/en/reference/hooks.md). | ||
| 124 | +- Writing your own hook step by step: [Guard the agent with hooks](../../docs/en/how-to/guard-the-agent-with-hooks.md). | ||
| 125 | +- Why hooks are external commands and where they plug in: [Hooks](../../docs/en/explanation/hooks.md). | ||
added
demos/hooks/README.fr.md +125 -0 | new file mode 100644 | ||
| @@ -0,0 +1,125 @@ | ||
| 1 | +# Démo des hooks — quatre hooks Node.js autour d'un petit assistant shell | |
| 2 | + | |
| 3 | +*[English version](README.en.md)* | |
| 4 | + | |
| 5 | +Un hook est une commande que mm lance à un moment précis d'un tour. Il lit un objet JSON sur son entrée standard et peut répondre par un objet JSON sur sa sortie standard. Cette démo place quatre hooks, écrits en Node.js, autour d'un agent qui peut lancer des commandes shell. Chacun montre une chose qu'un hook sait faire : | |
| 6 | + | |
| 7 | +| Hook | Événement | Ce qu'il montre | | |
| 8 | +|---|---|---| | |
| 9 | +| `hooks/no-secrets.js` | `preGeneration` | **arrêter** un tour : une question qui contient un mot de passe ou une clé d'API n'arrive jamais au modèle | | |
| 10 | +| `hooks/add-context.js` | `preGeneration` | **ajouter du contexte** : la date du jour et la liste des fichiers, ajoutées à chaque question | | |
| 11 | +| `hooks/bash-guard.js` | `preToolCall` (`matcher: ^bash$`) | **refuser** un appel d'outil : `rm -r`, `sudo`, `curl … \| sh` ne s'exécutent jamais, et le modèle lit pourquoi | | |
| 12 | +| `hooks/audit.js` | `postToolCall` + `postGeneration` | **observer** : une ligne JSON par appel d'outil et par tour dans `.mm/hooks-audit.jsonl` | | |
| 13 | + | |
| 14 | +Modèle : **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`, ≈ 6 Go) sur **llmman**. Les hooks eux-mêmes ne dépendent pas du modèle : ils s'exécutent de la même façon quel que soit celui qui répond. | |
| 15 | + | |
| 16 | +## Lancer la démo | |
| 17 | + | |
| 18 | +Prérequis : [llmman](https://llmmanorg.github.io/) démarré, et Node.js dans le PATH. | |
| 19 | + | |
| 20 | +```bash | |
| 21 | +llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M | |
| 22 | +cd demos/hooks/playground | |
| 23 | +mm ../agent.yaml | |
| 24 | +``` | |
| 25 | + | |
| 26 | +La bannière liste les hooks : | |
| 27 | + | |
| 28 | +``` | |
| 29 | +… | hooks: preGeneration×2 preToolCall×1 postToolCall×1 postGeneration×1 | |
| 30 | +``` | |
| 31 | + | |
| 32 | +L'agent travaille dans `playground/`, un dossier qui contient trois fichiers sans importance. Les hooks sont trouvés grâce à `$MM_CONFIG_DIR`, le dossier d'`agent.yaml` : mm peut donc être lancé depuis n'importe quel dossier. | |
| 33 | + | |
| 34 | +## Essayer | |
| 35 | + | |
| 36 | +Chaque ligne ci-dessous est une question à taper, suivie de ce qui a été observé le 2026-10-14 (`mm -p "<question>" ../agent.yaml`). Les questions sont en anglais, comme pendant l'essai ; l'agent répond dans la langue de la question. | |
| 37 | + | |
| 38 | +**1. Un secret est arrêté.** Le modèle n'est jamais appelé : | |
| 39 | + | |
| 40 | +``` | |
| 41 | +> My password: hunter2 — can you store it in a file? | |
| 42 | +🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again. | |
| 43 | +``` | |
| 44 | + | |
| 45 | +L'historique garde `[prompt blocked by a hook: the prompt contains a password]`, jamais le mot de passe. Le journal d'audit reçoit le même marqueur. | |
| 46 | + | |
| 47 | +**2. Du contexte est ajouté.** Le modèle répond sans rien exécuter, à partir de ce qu'`add-context.js` a ajouté : | |
| 48 | + | |
| 49 | +``` | |
| 50 | +> Which files are here, and what is today's date? | |
| 51 | +🤖 The files in the directory are `notes.md`, `old-drafts`, and `todo.txt`. Today's date is 2026-10-14. | |
| 52 | +``` | |
| 53 | + | |
| 54 | +**3. Une commande dangereuse est refusée.** Le modèle demande `rm -rf old-drafts`, le hook refuse, et le dossier est toujours là : | |
| 55 | + | |
| 56 | +``` | |
| 57 | +> Delete the old-drafts directory with rm -rf. | |
| 58 | +🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed] | |
| 59 | +🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook. | |
| 60 | +``` | |
| 61 | + | |
| 62 | +**4. Tout est journalisé.** Après une question ordinaire (`What is in todo.txt?`), `playground/.mm/hooks-audit.jsonl` contient : | |
| 63 | + | |
| 64 | +```json | |
| 65 | +{"at":"…","event":"postToolCall","tool":"bash","input":{"command":"cat todo.txt"},"output":"Buy milk\nCall the plumber\n"} | |
| 66 | +{"at":"…","event":"postGeneration","prompt":"What is in todo.txt?","answer":"The file `todo.txt` contains: …"} | |
| 67 | +``` | |
| 68 | + | |
| 69 | +## Écrire un hook | |
| 70 | + | |
| 71 | +`hooks/bash-guard.js`, sans ses commentaires : | |
| 72 | + | |
| 73 | +```js | |
| 74 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | |
| 75 | +const command = input.tool.input.command || ""; | |
| 76 | + | |
| 77 | +const rules = [ | |
| 78 | + { reason: "recursive deletion (rm -r) is not allowed", pattern: /\brm\s+(-\w*\s+)*-\w*[rR]/ }, | |
| 79 | + { reason: "sudo is not allowed", pattern: /\bsudo\b/ }, | |
| 80 | +]; | |
| 81 | + | |
| 82 | +const broken = rules.find((r) => r.pattern.test(command)); | |
| 83 | +if (broken) { | |
| 84 | + console.log(JSON.stringify({ action: "deny", reason: broken.reason })); | |
| 85 | +} | |
| 86 | +``` | |
| 87 | + | |
| 88 | +Il lit son entrée, décide, et n'écrit une réponse que s'il a quelque chose à dire : **une sortie vide veut dire « continuer »**. On teste un hook sans mm en lui donnant une entrée : | |
| 89 | + | |
| 90 | +```bash | |
| 91 | +echo '{"event":"preToolCall","cwd":"/tmp","tool":{"name":"bash","input":{"command":"sudo ls"}}}' \ | |
| 92 | + | node hooks/bash-guard.js | |
| 93 | +# {"action":"deny","reason":"sudo is not allowed"} | |
| 94 | +``` | |
| 95 | + | |
| 96 | +## La configuration | |
| 97 | + | |
| 98 | +```yaml | |
| 99 | +hooks: | |
| 100 | + preGeneration: | |
| 101 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | |
| 102 | + timeout: 5s | |
| 103 | + onError: fail # un garde qui ne peut pas s'exécuter ne doit pas laisser passer la question | |
| 104 | + - command: node "$MM_CONFIG_DIR/hooks/add-context.js" | |
| 105 | + timeout: 5s | |
| 106 | + preToolCall: | |
| 107 | + - matcher: ^bash$ # une expression régulière sur le nom de l'outil | |
| 108 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | |
| 109 | + timeout: 5s | |
| 110 | + onError: fail | |
| 111 | + postToolCall: | |
| 112 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 113 | + onError: ignore # perdre une ligne d'audit ne doit pas perturber la session | |
| 114 | + postGeneration: | |
| 115 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 116 | + onError: ignore | |
| 117 | +``` | |
| 118 | + | |
| 119 | +Ici `yolo: true` : les outils s'exécutent sans demander, et c'est `bash-guard.js` qui se tient entre le modèle et une commande dangereuse. Un hook `preToolCall` s'exécute **avant** la demande d'autorisation de l'outil : avec `yolo: false`, un appel refusé ne demande donc même pas. | |
| 120 | + | |
| 121 | +## Pour aller plus loin | |
| 122 | + | |
| 123 | +- Tous les champs de l'entrée et de la réponse, l'ordre d'exécution des hooks, `onError` : [Référence des hooks](../../docs/fr/reference/hooks.md). | |
| 124 | +- Écrire son propre hook pas à pas : [Protéger l'agent avec des hooks](../../docs/fr/how-to/guard-the-agent-with-hooks.md). | |
| 125 | +- Pourquoi les hooks sont des commandes externes, et où ils se branchent : [Les hooks](../../docs/fr/explanation/hooks.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | +# Démo des hooks — quatre hooks Node.js autour d'un petit assistant shell | ||
| 2 | + | ||
| 3 | +*[English version](README.en.md)* | ||
| 4 | + | ||
| 5 | +Un hook est une commande que mm lance à un moment précis d'un tour. Il lit un objet JSON sur son entrée standard et peut répondre par un objet JSON sur sa sortie standard. Cette démo place quatre hooks, écrits en Node.js, autour d'un agent qui peut lancer des commandes shell. Chacun montre une chose qu'un hook sait faire : | ||
| 6 | + | ||
| 7 | +| Hook | Événement | Ce qu'il montre | | ||
| 8 | +|---|---|---| | ||
| 9 | +| `hooks/no-secrets.js` | `preGeneration` | **arrêter** un tour : une question qui contient un mot de passe ou une clé d'API n'arrive jamais au modèle | | ||
| 10 | +| `hooks/add-context.js` | `preGeneration` | **ajouter du contexte** : la date du jour et la liste des fichiers, ajoutées à chaque question | | ||
| 11 | +| `hooks/bash-guard.js` | `preToolCall` (`matcher: ^bash$`) | **refuser** un appel d'outil : `rm -r`, `sudo`, `curl … \| sh` ne s'exécutent jamais, et le modèle lit pourquoi | | ||
| 12 | +| `hooks/audit.js` | `postToolCall` + `postGeneration` | **observer** : une ligne JSON par appel d'outil et par tour dans `.mm/hooks-audit.jsonl` | | ||
| 13 | + | ||
| 14 | +Modèle : **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`, ≈ 6 Go) sur **llmman**. Les hooks eux-mêmes ne dépendent pas du modèle : ils s'exécutent de la même façon quel que soit celui qui répond. | ||
| 15 | + | ||
| 16 | +## Lancer la démo | ||
| 17 | + | ||
| 18 | +Prérequis : [llmman](https://llmmanorg.github.io/) démarré, et Node.js dans le PATH. | ||
| 19 | + | ||
| 20 | +```bash | ||
| 21 | +llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M | ||
| 22 | +cd demos/hooks/playground | ||
| 23 | +mm ../agent.yaml | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +La bannière liste les hooks : | ||
| 27 | + | ||
| 28 | +``` | ||
| 29 | +… | hooks: preGeneration×2 preToolCall×1 postToolCall×1 postGeneration×1 | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +L'agent travaille dans `playground/`, un dossier qui contient trois fichiers sans importance. Les hooks sont trouvés grâce à `$MM_CONFIG_DIR`, le dossier d'`agent.yaml` : mm peut donc être lancé depuis n'importe quel dossier. | ||
| 33 | + | ||
| 34 | +## Essayer | ||
| 35 | + | ||
| 36 | +Chaque ligne ci-dessous est une question à taper, suivie de ce qui a été observé le 2026-10-14 (`mm -p "<question>" ../agent.yaml`). Les questions sont en anglais, comme pendant l'essai ; l'agent répond dans la langue de la question. | ||
| 37 | + | ||
| 38 | +**1. Un secret est arrêté.** Le modèle n'est jamais appelé : | ||
| 39 | + | ||
| 40 | +``` | ||
| 41 | +> My password: hunter2 — can you store it in a file? | ||
| 42 | +🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again. | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +L'historique garde `[prompt blocked by a hook: the prompt contains a password]`, jamais le mot de passe. Le journal d'audit reçoit le même marqueur. | ||
| 46 | + | ||
| 47 | +**2. Du contexte est ajouté.** Le modèle répond sans rien exécuter, à partir de ce qu'`add-context.js` a ajouté : | ||
| 48 | + | ||
| 49 | +``` | ||
| 50 | +> Which files are here, and what is today's date? | ||
| 51 | +🤖 The files in the directory are `notes.md`, `old-drafts`, and `todo.txt`. Today's date is 2026-10-14. | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +**3. Une commande dangereuse est refusée.** Le modèle demande `rm -rf old-drafts`, le hook refuse, et le dossier est toujours là : | ||
| 55 | + | ||
| 56 | +``` | ||
| 57 | +> Delete the old-drafts directory with rm -rf. | ||
| 58 | +🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed] | ||
| 59 | +🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook. | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +**4. Tout est journalisé.** Après une question ordinaire (`What is in todo.txt?`), `playground/.mm/hooks-audit.jsonl` contient : | ||
| 63 | + | ||
| 64 | +```json | ||
| 65 | +{"at":"…","event":"postToolCall","tool":"bash","input":{"command":"cat todo.txt"},"output":"Buy milk\nCall the plumber\n"} | ||
| 66 | +{"at":"…","event":"postGeneration","prompt":"What is in todo.txt?","answer":"The file `todo.txt` contains: …"} | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## Écrire un hook | ||
| 70 | + | ||
| 71 | +`hooks/bash-guard.js`, sans ses commentaires : | ||
| 72 | + | ||
| 73 | +```js | ||
| 74 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | ||
| 75 | +const command = input.tool.input.command || ""; | ||
| 76 | + | ||
| 77 | +const rules = [ | ||
| 78 | + { reason: "recursive deletion (rm -r) is not allowed", pattern: /\brm\s+(-\w*\s+)*-\w*[rR]/ }, | ||
| 79 | + { reason: "sudo is not allowed", pattern: /\bsudo\b/ }, | ||
| 80 | +]; | ||
| 81 | + | ||
| 82 | +const broken = rules.find((r) => r.pattern.test(command)); | ||
| 83 | +if (broken) { | ||
| 84 | + console.log(JSON.stringify({ action: "deny", reason: broken.reason })); | ||
| 85 | +} | ||
| 86 | +``` | ||
| 87 | + | ||
| 88 | +Il lit son entrée, décide, et n'écrit une réponse que s'il a quelque chose à dire : **une sortie vide veut dire « continuer »**. On teste un hook sans mm en lui donnant une entrée : | ||
| 89 | + | ||
| 90 | +```bash | ||
| 91 | +echo '{"event":"preToolCall","cwd":"/tmp","tool":{"name":"bash","input":{"command":"sudo ls"}}}' \ | ||
| 92 | + | node hooks/bash-guard.js | ||
| 93 | +# {"action":"deny","reason":"sudo is not allowed"} | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +## La configuration | ||
| 97 | + | ||
| 98 | +```yaml | ||
| 99 | +hooks: | ||
| 100 | + preGeneration: | ||
| 101 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | ||
| 102 | + timeout: 5s | ||
| 103 | + onError: fail # un garde qui ne peut pas s'exécuter ne doit pas laisser passer la question | ||
| 104 | + - command: node "$MM_CONFIG_DIR/hooks/add-context.js" | ||
| 105 | + timeout: 5s | ||
| 106 | + preToolCall: | ||
| 107 | + - matcher: ^bash$ # une expression régulière sur le nom de l'outil | ||
| 108 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | ||
| 109 | + timeout: 5s | ||
| 110 | + onError: fail | ||
| 111 | + postToolCall: | ||
| 112 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 113 | + onError: ignore # perdre une ligne d'audit ne doit pas perturber la session | ||
| 114 | + postGeneration: | ||
| 115 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 116 | + onError: ignore | ||
| 117 | +``` | ||
| 118 | + | ||
| 119 | +Ici `yolo: true` : les outils s'exécutent sans demander, et c'est `bash-guard.js` qui se tient entre le modèle et une commande dangereuse. Un hook `preToolCall` s'exécute **avant** la demande d'autorisation de l'outil : avec `yolo: false`, un appel refusé ne demande donc même pas. | ||
| 120 | + | ||
| 121 | +## Pour aller plus loin | ||
| 122 | + | ||
| 123 | +- Tous les champs de l'entrée et de la réponse, l'ordre d'exécution des hooks, `onError` : [Référence des hooks](../../docs/fr/reference/hooks.md). | ||
| 124 | +- Écrire son propre hook pas à pas : [Protéger l'agent avec des hooks](../../docs/fr/how-to/guard-the-agent-with-hooks.md). | ||
| 125 | +- Pourquoi les hooks sont des commandes externes, et où ils se branchent : [Les hooks](../../docs/fr/explanation/hooks.md). | ||
added
demos/hooks/agent.yaml +75 -0 | new file mode 100644 | ||
| @@ -0,0 +1,75 @@ | ||
| 1 | +# Hooks demo — a small shell assistant guarded by four Node.js hooks. | |
| 2 | +# | |
| 3 | +# hooks/no-secrets.js preGeneration stops a question that holds a secret | |
| 4 | +# hooks/add-context.js preGeneration adds today's date and the file list | |
| 5 | +# hooks/bash-guard.js preToolCall denies rm -r, sudo, curl | sh | |
| 6 | +# hooks/audit.js postToolCall writes .mm/hooks-audit.jsonl | |
| 7 | +# postGeneration | |
| 8 | +# | |
| 9 | +# Run it from the playground (any directory works: the hooks are found | |
| 10 | +# through $MM_CONFIG_DIR, the directory of this file): | |
| 11 | +# | |
| 12 | +# cd demos/hooks/playground | |
| 13 | +# mm ../agent.yaml | |
| 14 | +# | |
| 15 | +# Every key is optional; reference: docs/en/reference/configuration.md. | |
| 16 | + | |
| 17 | +provider: llmman | |
| 18 | +model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M | |
| 19 | +baseUrl: http://127.0.0.1:17434/v1 | |
| 20 | +fallback: http://host.docker.internal:17434/v1 | |
| 21 | + | |
| 22 | +sampling: | |
| 23 | + temperature: 0.2 | |
| 24 | + parallel_tool_calls: false | |
| 25 | + | |
| 26 | +firstTokenTimeout: 3m | |
| 27 | +watchdogTimeout: 3m | |
| 28 | +maxTurns: 8 | |
| 29 | + | |
| 30 | +bashTool: true | |
| 31 | +editTools: false | |
| 32 | +planTool: false | |
| 33 | +fetchContentTool: false | |
| 34 | +askUserTool: false | |
| 35 | +skillsDir: "" | |
| 36 | + | |
| 37 | +# The tools run without asking: the bash-guard hook is what stands between | |
| 38 | +# the model and a dangerous command here. A hook runs BEFORE the tool's own | |
| 39 | +# consent prompt, so with `yolo: false` a denied call never even asks. | |
| 40 | +yolo: true | |
| 41 | + | |
| 42 | +agentsMd: | |
| 43 | + enabled: false | |
| 44 | + | |
| 45 | +acp: | |
| 46 | + name: hooks-demo | |
| 47 | + title: 🪝 Hooks demo | |
| 48 | + | |
| 49 | +hooks: | |
| 50 | + preGeneration: | |
| 51 | + # onError: fail — a guard that cannot run must not let the question through. | |
| 52 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | |
| 53 | + timeout: 5s | |
| 54 | + onError: fail | |
| 55 | + - command: node "$MM_CONFIG_DIR/hooks/add-context.js" | |
| 56 | + timeout: 5s | |
| 57 | + preToolCall: | |
| 58 | + - matcher: ^bash$ | |
| 59 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | |
| 60 | + timeout: 5s | |
| 61 | + onError: fail | |
| 62 | + postToolCall: | |
| 63 | + # onError: ignore — losing an audit line must not disturb the session. | |
| 64 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 65 | + onError: ignore | |
| 66 | + postGeneration: | |
| 67 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | |
| 68 | + onError: ignore | |
| 69 | + | |
| 70 | +system: | | |
| 71 | + You are a shell assistant working in the current directory. | |
| 72 | + Use the bash tool to look at files and to run commands; never guess what a | |
| 73 | + file contains. Answer in one or two short sentences, in the user's language. | |
| 74 | + If a tool result says "[denied by a hook: …]", the command was refused: | |
| 75 | + tell the user why, and do not try to get around the refusal. | |
| new file mode 100644 | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | +# Hooks demo — a small shell assistant guarded by four Node.js hooks. | ||
| 2 | +# | ||
| 3 | +# hooks/no-secrets.js preGeneration stops a question that holds a secret | ||
| 4 | +# hooks/add-context.js preGeneration adds today's date and the file list | ||
| 5 | +# hooks/bash-guard.js preToolCall denies rm -r, sudo, curl | sh | ||
| 6 | +# hooks/audit.js postToolCall writes .mm/hooks-audit.jsonl | ||
| 7 | +# postGeneration | ||
| 8 | +# | ||
| 9 | +# Run it from the playground (any directory works: the hooks are found | ||
| 10 | +# through $MM_CONFIG_DIR, the directory of this file): | ||
| 11 | +# | ||
| 12 | +# cd demos/hooks/playground | ||
| 13 | +# mm ../agent.yaml | ||
| 14 | +# | ||
| 15 | +# Every key is optional; reference: docs/en/reference/configuration.md. | ||
| 16 | + | ||
| 17 | +provider: llmman | ||
| 18 | +model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M | ||
| 19 | +baseUrl: http://127.0.0.1:17434/v1 | ||
| 20 | +fallback: http://host.docker.internal:17434/v1 | ||
| 21 | + | ||
| 22 | +sampling: | ||
| 23 | + temperature: 0.2 | ||
| 24 | + parallel_tool_calls: false | ||
| 25 | + | ||
| 26 | +firstTokenTimeout: 3m | ||
| 27 | +watchdogTimeout: 3m | ||
| 28 | +maxTurns: 8 | ||
| 29 | + | ||
| 30 | +bashTool: true | ||
| 31 | +editTools: false | ||
| 32 | +planTool: false | ||
| 33 | +fetchContentTool: false | ||
| 34 | +askUserTool: false | ||
| 35 | +skillsDir: "" | ||
| 36 | + | ||
| 37 | +# The tools run without asking: the bash-guard hook is what stands between | ||
| 38 | +# the model and a dangerous command here. A hook runs BEFORE the tool's own | ||
| 39 | +# consent prompt, so with `yolo: false` a denied call never even asks. | ||
| 40 | +yolo: true | ||
| 41 | + | ||
| 42 | +agentsMd: | ||
| 43 | + enabled: false | ||
| 44 | + | ||
| 45 | +acp: | ||
| 46 | + name: hooks-demo | ||
| 47 | + title: 🪝 Hooks demo | ||
| 48 | + | ||
| 49 | +hooks: | ||
| 50 | + preGeneration: | ||
| 51 | + # onError: fail — a guard that cannot run must not let the question through. | ||
| 52 | + - command: node "$MM_CONFIG_DIR/hooks/no-secrets.js" | ||
| 53 | + timeout: 5s | ||
| 54 | + onError: fail | ||
| 55 | + - command: node "$MM_CONFIG_DIR/hooks/add-context.js" | ||
| 56 | + timeout: 5s | ||
| 57 | + preToolCall: | ||
| 58 | + - matcher: ^bash$ | ||
| 59 | + command: node "$MM_CONFIG_DIR/hooks/bash-guard.js" | ||
| 60 | + timeout: 5s | ||
| 61 | + onError: fail | ||
| 62 | + postToolCall: | ||
| 63 | + # onError: ignore — losing an audit line must not disturb the session. | ||
| 64 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 65 | + onError: ignore | ||
| 66 | + postGeneration: | ||
| 67 | + - command: node "$MM_CONFIG_DIR/hooks/audit.js" | ||
| 68 | + onError: ignore | ||
| 69 | + | ||
| 70 | +system: | | ||
| 71 | + You are a shell assistant working in the current directory. | ||
| 72 | + Use the bash tool to look at files and to run commands; never guess what a | ||
| 73 | + file contains. Answer in one or two short sentences, in the user's language. | ||
| 74 | + If a tool result says "[denied by a hook: …]", the command was refused: | ||
| 75 | + tell the user why, and do not try to get around the refusal. | ||
added
demos/hooks/hooks/add-context.js +22 -0 | new file mode 100755 | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// preGeneration hook: adds context the model cannot guess — today's date and | |
| 3 | +// the files of the working directory. | |
| 4 | +// | |
| 5 | +// Printing {"context":"…"} appends that text to the question, under the | |
| 6 | +// label "[context added by a hook]". The user's words are left as they are | |
| 7 | +// (a hook could also replace them with {"prompt":"…"}). | |
| 8 | + | |
| 9 | +const fs = require("fs"); | |
| 10 | +const input = JSON.parse(fs.readFileSync(0, "utf8")); | |
| 11 | + | |
| 12 | +const today = new Date().toISOString().slice(0, 10); | |
| 13 | +let files = []; | |
| 14 | +try { | |
| 15 | + files = fs.readdirSync(input.cwd).filter((f) => !f.startsWith(".")); | |
| 16 | +} catch { | |
| 17 | + // An unreadable directory is not worth failing the turn for. | |
| 18 | +} | |
| 19 | + | |
| 20 | +console.log(JSON.stringify({ | |
| 21 | + context: `Today is ${today}. Files in ${input.cwd}: ${files.join(", ") || "none"}.`, | |
| 22 | +})); | |
| new file mode 100755 | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | +#!/usr/bin/env node | ||
| 2 | +// preGeneration hook: adds context the model cannot guess — today's date and | ||
| 3 | +// the files of the working directory. | ||
| 4 | +// | ||
| 5 | +// Printing {"context":"…"} appends that text to the question, under the | ||
| 6 | +// label "[context added by a hook]". The user's words are left as they are | ||
| 7 | +// (a hook could also replace them with {"prompt":"…"}). | ||
| 8 | + | ||
| 9 | +const fs = require("fs"); | ||
| 10 | +const input = JSON.parse(fs.readFileSync(0, "utf8")); | ||
| 11 | + | ||
| 12 | +const today = new Date().toISOString().slice(0, 10); | ||
| 13 | +let files = []; | ||
| 14 | +try { | ||
| 15 | + files = fs.readdirSync(input.cwd).filter((f) => !f.startsWith(".")); | ||
| 16 | +} catch { | ||
| 17 | + // An unreadable directory is not worth failing the turn for. | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +console.log(JSON.stringify({ | ||
| 21 | + context: `Today is ${today}. Files in ${input.cwd}: ${files.join(", ") || "none"}.`, | ||
| 22 | +})); | ||
added
demos/hooks/hooks/audit.js +25 -0 | new file mode 100755 | ||
| @@ -0,0 +1,25 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// postToolCall and postGeneration hook: writes one line per event to | |
| 3 | +// .mm/hooks-audit.jsonl in the working directory. | |
| 4 | +// | |
| 5 | +// It only observes, so it prints nothing on stdout. One script serves two | |
| 6 | +// events: the event's name is in the input (and in $MM_HOOK_EVENT). | |
| 7 | + | |
| 8 | +const fs = require("fs"); | |
| 9 | +const path = require("path"); | |
| 10 | +const input = JSON.parse(fs.readFileSync(0, "utf8")); | |
| 11 | + | |
| 12 | +const line = { at: new Date().toISOString(), event: input.event }; | |
| 13 | +if (input.event === "postToolCall") { | |
| 14 | + line.tool = input.tool.name; | |
| 15 | + line.input = input.tool.input; | |
| 16 | + line.output = input.output.slice(0, 200); | |
| 17 | +} else { | |
| 18 | + line.prompt = input.prompt; | |
| 19 | + line.answer = (input.answer || "").slice(0, 200); | |
| 20 | + if (input.error) line.error = input.error; | |
| 21 | +} | |
| 22 | + | |
| 23 | +const dir = path.join(input.cwd, ".mm"); | |
| 24 | +fs.mkdirSync(dir, { recursive: true }); | |
| 25 | +fs.appendFileSync(path.join(dir, "hooks-audit.jsonl"), JSON.stringify(line) + "\n"); | |
| new file mode 100755 | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | +#!/usr/bin/env node | ||
| 2 | +// postToolCall and postGeneration hook: writes one line per event to | ||
| 3 | +// .mm/hooks-audit.jsonl in the working directory. | ||
| 4 | +// | ||
| 5 | +// It only observes, so it prints nothing on stdout. One script serves two | ||
| 6 | +// events: the event's name is in the input (and in $MM_HOOK_EVENT). | ||
| 7 | + | ||
| 8 | +const fs = require("fs"); | ||
| 9 | +const path = require("path"); | ||
| 10 | +const input = JSON.parse(fs.readFileSync(0, "utf8")); | ||
| 11 | + | ||
| 12 | +const line = { at: new Date().toISOString(), event: input.event }; | ||
| 13 | +if (input.event === "postToolCall") { | ||
| 14 | + line.tool = input.tool.name; | ||
| 15 | + line.input = input.tool.input; | ||
| 16 | + line.output = input.output.slice(0, 200); | ||
| 17 | +} else { | ||
| 18 | + line.prompt = input.prompt; | ||
| 19 | + line.answer = (input.answer || "").slice(0, 200); | ||
| 20 | + if (input.error) line.error = input.error; | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +const dir = path.join(input.cwd, ".mm"); | ||
| 24 | +fs.mkdirSync(dir, { recursive: true }); | ||
| 25 | +fs.appendFileSync(path.join(dir, "hooks-audit.jsonl"), JSON.stringify(line) + "\n"); | ||
added
demos/hooks/hooks/bash-guard.js +21 -0 | new file mode 100755 | ||
| @@ -0,0 +1,21 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// preToolCall hook (matcher ^bash$): refuses dangerous shell commands. | |
| 3 | +// | |
| 4 | +// mm writes {"event":"preToolCall","tool":{"name":"bash","input":{"command":"…"}},…}. | |
| 5 | +// Printing {"action":"deny","reason":"…"} means the command never runs: the | |
| 6 | +// model reads "[denied by a hook: <reason>]" as the tool's output, and can | |
| 7 | +// try something else. Printing nothing lets the command run. | |
| 8 | + | |
| 9 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | |
| 10 | +const command = input.tool.input.command || ""; | |
| 11 | + | |
| 12 | +const rules = [ | |
| 13 | + { reason: "recursive deletion (rm -r) is not allowed", pattern: /\brm\s+(-\w*\s+)*-\w*[rR]/ }, | |
| 14 | + { reason: "sudo is not allowed", pattern: /\bsudo\b/ }, | |
| 15 | + { reason: "piping a download into a shell is not allowed", pattern: /\b(curl|wget)\b[^|]*\|\s*(ba|z)?sh\b/ }, | |
| 16 | +]; | |
| 17 | + | |
| 18 | +const broken = rules.find((r) => r.pattern.test(command)); | |
| 19 | +if (broken) { | |
| 20 | + console.log(JSON.stringify({ action: "deny", reason: broken.reason })); | |
| 21 | +} | |
| new file mode 100755 | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | +#!/usr/bin/env node | ||
| 2 | +// preToolCall hook (matcher ^bash$): refuses dangerous shell commands. | ||
| 3 | +// | ||
| 4 | +// mm writes {"event":"preToolCall","tool":{"name":"bash","input":{"command":"…"}},…}. | ||
| 5 | +// Printing {"action":"deny","reason":"…"} means the command never runs: the | ||
| 6 | +// model reads "[denied by a hook: <reason>]" as the tool's output, and can | ||
| 7 | +// try something else. Printing nothing lets the command run. | ||
| 8 | + | ||
| 9 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | ||
| 10 | +const command = input.tool.input.command || ""; | ||
| 11 | + | ||
| 12 | +const rules = [ | ||
| 13 | + { reason: "recursive deletion (rm -r) is not allowed", pattern: /\brm\s+(-\w*\s+)*-\w*[rR]/ }, | ||
| 14 | + { reason: "sudo is not allowed", pattern: /\bsudo\b/ }, | ||
| 15 | + { reason: "piping a download into a shell is not allowed", pattern: /\b(curl|wget)\b[^|]*\|\s*(ba|z)?sh\b/ }, | ||
| 16 | +]; | ||
| 17 | + | ||
| 18 | +const broken = rules.find((r) => r.pattern.test(command)); | ||
| 19 | +if (broken) { | ||
| 20 | + console.log(JSON.stringify({ action: "deny", reason: broken.reason })); | ||
| 21 | +} | ||
added
demos/hooks/hooks/no-secrets.js +27 -0 | new file mode 100755 | ||
| @@ -0,0 +1,27 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// preGeneration hook: stops the turn when the question holds a secret. | |
| 3 | +// | |
| 4 | +// mm writes one JSON object on stdin: {"event":"preGeneration","prompt":"…",…}. | |
| 5 | +// Printing {"action":"stop",…} means the model never sees the question; mm | |
| 6 | +// shows `response` instead and keeps "[prompt blocked by a hook: <reason>]" | |
| 7 | +// in the history, never the secret itself. | |
| 8 | +// Printing nothing means "continue". | |
| 9 | + | |
| 10 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | |
| 11 | + | |
| 12 | +const secrets = [ | |
| 13 | + { name: "a password", pattern: /\b(password|passwd|pwd|mot de passe)\s*[:=]\s*\S+/i }, | |
| 14 | + { name: "an OpenAI-style API key", pattern: /\bsk-[A-Za-z0-9_-]{20,}/ }, | |
| 15 | + { name: "a GitHub token", pattern: /\bgh[pousr]_[A-Za-z0-9]{30,}/ }, | |
| 16 | + { name: "an AWS access key", pattern: /\bAKIA[0-9A-Z]{16}\b/ }, | |
| 17 | +]; | |
| 18 | + | |
| 19 | +const found = secrets.find((s) => s.pattern.test(input.prompt)); | |
| 20 | +if (found) { | |
| 21 | + console.log(JSON.stringify({ | |
| 22 | + action: "stop", | |
| 23 | + reason: `the prompt contains ${found.name}`, | |
| 24 | + response: `🔐 Your message looks like it contains ${found.name}. ` + | |
| 25 | + "It was not sent to the model. Remove it and ask again.", | |
| 26 | + })); | |
| 27 | +} | |
| new file mode 100755 | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | +#!/usr/bin/env node | ||
| 2 | +// preGeneration hook: stops the turn when the question holds a secret. | ||
| 3 | +// | ||
| 4 | +// mm writes one JSON object on stdin: {"event":"preGeneration","prompt":"…",…}. | ||
| 5 | +// Printing {"action":"stop",…} means the model never sees the question; mm | ||
| 6 | +// shows `response` instead and keeps "[prompt blocked by a hook: <reason>]" | ||
| 7 | +// in the history, never the secret itself. | ||
| 8 | +// Printing nothing means "continue". | ||
| 9 | + | ||
| 10 | +const input = JSON.parse(require("fs").readFileSync(0, "utf8")); | ||
| 11 | + | ||
| 12 | +const secrets = [ | ||
| 13 | + { name: "a password", pattern: /\b(password|passwd|pwd|mot de passe)\s*[:=]\s*\S+/i }, | ||
| 14 | + { name: "an OpenAI-style API key", pattern: /\bsk-[A-Za-z0-9_-]{20,}/ }, | ||
| 15 | + { name: "a GitHub token", pattern: /\bgh[pousr]_[A-Za-z0-9]{30,}/ }, | ||
| 16 | + { name: "an AWS access key", pattern: /\bAKIA[0-9A-Z]{16}\b/ }, | ||
| 17 | +]; | ||
| 18 | + | ||
| 19 | +const found = secrets.find((s) => s.pattern.test(input.prompt)); | ||
| 20 | +if (found) { | ||
| 21 | + console.log(JSON.stringify({ | ||
| 22 | + action: "stop", | ||
| 23 | + reason: `the prompt contains ${found.name}`, | ||
| 24 | + response: `🔐 Your message looks like it contains ${found.name}. ` + | ||
| 25 | + "It was not sent to the model. Remove it and ask again.", | ||
| 26 | + })); | ||
| 27 | +} | ||
added
demos/hooks/playground/notes.md +3 -0 | new file mode 100644 | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +# Notes | |
| 2 | + | |
| 3 | +The meeting is on Friday. | |
| new file mode 100644 | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | +# Notes | ||
| 2 | + | ||
| 3 | +The meeting is on Friday. | ||
added
demos/hooks/playground/old-drafts/draft1.txt +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +draft 1 | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +draft 1 | ||
added
demos/hooks/playground/todo.txt +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +Buy milk | |
| 2 | +Call the plumber | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +Buy milk | ||
| 2 | +Call the plumber | ||
modified
docs/diagrams/packages.drawio +1 -1 | @@ -1,3 +1,3 @@ | ||
| 1 | 1 | <mxfile host="app.diagrams.net" modified="2026-10-06T00: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="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" 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="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" 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="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" 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="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" 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="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" 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="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" 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="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" 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="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" 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="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" 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="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" 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="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" 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="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" 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="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" 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="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" 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="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" 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="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" 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="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" 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="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" 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 | 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="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" 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="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" 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="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" 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="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" 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="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" 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="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" 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="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" 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="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" 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="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." 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="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" 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="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" 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="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" 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="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook</font>" 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="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" 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="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="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" 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="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" 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="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" 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></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="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" 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="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" 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="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" 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="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" 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="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" 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="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" 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="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" 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="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" 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="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." 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="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" 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="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" 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="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" 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="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook</font>" 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="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" 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="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="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" 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="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" 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="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" 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="<b>internal/hooks</b><br/><font style="font-size:10px">the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError</font>" 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_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></root></mxGraphModel></diagram></mxfile> | |
| \ No newline at end of file | ||
| @@ -1,3 +1,3 @@ | |||
| 1 | <mxfile host="app.diagrams.net" modified="2026-10-06T00: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="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" 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="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" 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="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" 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="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" 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="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" 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="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" 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="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" 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="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" 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="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" 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="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" 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="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" 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="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" 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="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" 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="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" 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="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" 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="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" 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="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" 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="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" 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-06T00: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="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" 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="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" 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="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" 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="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" 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="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" 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="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" 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="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" 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="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" 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="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" 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="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" 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="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" 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="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" 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="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" 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="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" 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="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" 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="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" 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="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" 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="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" 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="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" 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="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" 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="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" 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="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" 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="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" 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="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" 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="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" 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="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" 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="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." 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="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" 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="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" 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="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" 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="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook</font>" 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="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" 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="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="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" 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="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" 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="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" 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></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="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" 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="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" 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="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" 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="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" 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="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" 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="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" 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="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" 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="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" 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="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." 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="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" 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="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" 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="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" 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="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook</font>" 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="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" 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="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="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" 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="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" 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="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" 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="<b>internal/hooks</b><br/><font style="font-size:10px">the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError</font>" 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_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></root></mxGraphModel></diagram></mxfile> | ||
| \ No newline at end of file | \ No newline at end of file | ||
modified
docs/en/README.md +3 -0 | @@ -35,6 +35,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 35 | 35 | - [Run a single prompt and exit (`-p`/`--prompt`)](how-to/run-a-single-prompt.md) |
| 36 | 36 | - [Check that the tools go out in a stable order](how-to/verify-tool-order.md) — tests, a recording proxy, and the prompt-cache gain before and after ticket 53 |
| 37 | 37 | - [Fetch a web page as Markdown (`fetch_content`)](how-to/fetch-a-web-page.md) — read a page, or save it to a file without showing it |
| 38 | +- [Guard the agent with hooks](how-to/guard-the-agent-with-hooks.md) — deny a tool call, stop or rewrite a question, add context, log every turn, with a script in any language | |
| 38 | 39 | |
| 39 | 40 | ### MCP (Model Context Protocol) How-to |
| 40 | 41 | - [Use Context7 for documentation](how-to/mcp-use-context7.md) |
| @@ -48,6 +49,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 48 | 49 | - [Configuration file](reference/configuration.md) — every YAML key, its default and its validation rule |
| 49 | 50 | - [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user` |
| 50 | 51 | - [Skill file format](reference/skill-format.md) |
| 52 | +- [Hooks](reference/hooks.md) — the four events, the JSON a hook reads and writes, `matcher`, `timeout`, `onError`, validation errors | |
| 51 | 53 | - [Agent Client Protocol surface](reference/acp.md) |
| 52 | 54 | |
| 53 | 55 | ### MCP (Model Context Protocol) Reference |
| @@ -69,6 +71,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 69 | 71 | - [Structured output](explanation/structured-output.md) — why Go declarations, why they also go in the system prompt, why no tools, what guarantees the answer |
| 70 | 72 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit |
| 71 | 73 | - [Asking the user](explanation/asking-the-user.md) — why `ask_user` asks each front end its own way, how a called agent's questions come up, the loose schema (llama.cpp's alphabetical grammar), the watchdog while a human answers, the ACP SDK gap |
| 74 | +- [Hooks](explanation/hooks.md) — why external commands, why they plug into `engine.Generate` and `WrapTool`, why a stopped question leaves a marker, what ticket 36 left out | |
| 72 | 75 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 73 | 76 | |
| 74 | 77 | ### MCP (Model Context Protocol) Explanation |
| @@ -35,6 +35,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 35 | - [Run a single prompt and exit (`-p`/`--prompt`)](how-to/run-a-single-prompt.md) | 35 | - [Run a single prompt and exit (`-p`/`--prompt`)](how-to/run-a-single-prompt.md) |
| 36 | - [Check that the tools go out in a stable order](how-to/verify-tool-order.md) — tests, a recording proxy, and the prompt-cache gain before and after ticket 53 | 36 | - [Check that the tools go out in a stable order](how-to/verify-tool-order.md) — tests, a recording proxy, and the prompt-cache gain before and after ticket 53 |
| 37 | - [Fetch a web page as Markdown (`fetch_content`)](how-to/fetch-a-web-page.md) — read a page, or save it to a file without showing it | 37 | - [Fetch a web page as Markdown (`fetch_content`)](how-to/fetch-a-web-page.md) — read a page, or save it to a file without showing it |
| 38 | +- [Guard the agent with hooks](how-to/guard-the-agent-with-hooks.md) — deny a tool call, stop or rewrite a question, add context, log every turn, with a script in any language | ||
| 38 | 39 | ||
| 39 | ### MCP (Model Context Protocol) How-to | 40 | ### MCP (Model Context Protocol) How-to |
| 40 | - [Use Context7 for documentation](how-to/mcp-use-context7.md) | 41 | - [Use Context7 for documentation](how-to/mcp-use-context7.md) |
| @@ -48,6 +49,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 48 | - [Configuration file](reference/configuration.md) — every YAML key, its default and its validation rule | 49 | - [Configuration file](reference/configuration.md) — every YAML key, its default and its validation rule |
| 49 | - [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user` | 50 | - [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user` |
| 50 | - [Skill file format](reference/skill-format.md) | 51 | - [Skill file format](reference/skill-format.md) |
| 52 | +- [Hooks](reference/hooks.md) — the four events, the JSON a hook reads and writes, `matcher`, `timeout`, `onError`, validation errors | ||
| 51 | - [Agent Client Protocol surface](reference/acp.md) | 53 | - [Agent Client Protocol surface](reference/acp.md) |
| 52 | 54 | ||
| 53 | ### MCP (Model Context Protocol) Reference | 55 | ### MCP (Model Context Protocol) Reference |
| @@ -69,6 +71,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 69 | - [Structured output](explanation/structured-output.md) — why Go declarations, why they also go in the system prompt, why no tools, what guarantees the answer | 71 | - [Structured output](explanation/structured-output.md) — why Go declarations, why they also go in the system prompt, why no tools, what guarantees the answer |
| 70 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit | 72 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit |
| 71 | - [Asking the user](explanation/asking-the-user.md) — why `ask_user` asks each front end its own way, how a called agent's questions come up, the loose schema (llama.cpp's alphabetical grammar), the watchdog while a human answers, the ACP SDK gap | 73 | - [Asking the user](explanation/asking-the-user.md) — why `ask_user` asks each front end its own way, how a called agent's questions come up, the loose schema (llama.cpp's alphabetical grammar), the watchdog while a human answers, the ACP SDK gap |
| 74 | +- [Hooks](explanation/hooks.md) — why external commands, why they plug into `engine.Generate` and `WrapTool`, why a stopped question leaves a marker, what ticket 36 left out | ||
| 72 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) | 75 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 73 | 76 | ||
| 74 | ### MCP (Model Context Protocol) Explanation | 77 | ### MCP (Model Context Protocol) Explanation |
modified
docs/en/explanation/architecture.md +1 -0 | @@ -27,6 +27,7 @@ The entry point, `main.go`, only does wiring: it loads the settings, opens the e | ||
| 27 | 27 | | `version` | Which `mm` this is: the tag stamped at build time, or the commit Go embeds otherwise. Read by `--version`, by the start-up banner and by the window's status bar. | |
| 28 | 28 | | `ui` | Where human-facing output goes, and the event sink the front ends share. | |
| 29 | 29 | | `consent` | Whether a tool call may run: the `yolo` switch (`Gate`), the four answers (`Answer`), what a session remembers per tool (`Policy`), and the hook through which the line mode asks (`SetPrompter`). The window, the editor façade and `/ask` draw the question their own way and share these rules. See [Yolo mode and permissions](yolo-and-permissions.md). | |
| 30 | +| `hooks` | The `hooks:` block (ticket 36): runs each hook with `sh -c`, writes its JSON input, reads its reply, applies the chain order and `onError`, and returns decisions (`PreGeneration`, `PreToolCall`, `PostToolCall`, `PostGeneration`). No Genkit, no front end: `engine/hooks.go` applies the decisions around `Generate` and in the `WrapTool` middleware, so every front end gets them. See [Hooks](hooks.md). | | |
| 30 | 31 | | `agent` | The terminal REPL. | |
| 31 | 32 | | `acp` | The Agent Client Protocol façade. | |
| 32 | 33 | | `tui` | The single-window front end (`mm -tui`): a `ui.Sink` drawing on tcell, one transcript value, an event loop woken by the engine's and tools' goroutines. | |
| @@ -27,6 +27,7 @@ The entry point, `main.go`, only does wiring: it loads the settings, opens the e | |||
| 27 | | `version` | Which `mm` this is: the tag stamped at build time, or the commit Go embeds otherwise. Read by `--version`, by the start-up banner and by the window's status bar. | | 27 | | `version` | Which `mm` this is: the tag stamped at build time, or the commit Go embeds otherwise. Read by `--version`, by the start-up banner and by the window's status bar. | |
| 28 | | `ui` | Where human-facing output goes, and the event sink the front ends share. | | 28 | | `ui` | Where human-facing output goes, and the event sink the front ends share. | |
| 29 | | `consent` | Whether a tool call may run: the `yolo` switch (`Gate`), the four answers (`Answer`), what a session remembers per tool (`Policy`), and the hook through which the line mode asks (`SetPrompter`). The window, the editor façade and `/ask` draw the question their own way and share these rules. See [Yolo mode and permissions](yolo-and-permissions.md). | | 29 | | `consent` | Whether a tool call may run: the `yolo` switch (`Gate`), the four answers (`Answer`), what a session remembers per tool (`Policy`), and the hook through which the line mode asks (`SetPrompter`). The window, the editor façade and `/ask` draw the question their own way and share these rules. See [Yolo mode and permissions](yolo-and-permissions.md). | |
| 30 | +| `hooks` | The `hooks:` block (ticket 36): runs each hook with `sh -c`, writes its JSON input, reads its reply, applies the chain order and `onError`, and returns decisions (`PreGeneration`, `PreToolCall`, `PostToolCall`, `PostGeneration`). No Genkit, no front end: `engine/hooks.go` applies the decisions around `Generate` and in the `WrapTool` middleware, so every front end gets them. See [Hooks](hooks.md). | | ||
| 30 | | `agent` | The terminal REPL. | | 31 | | `agent` | The terminal REPL. | |
| 31 | | `acp` | The Agent Client Protocol façade. | | 32 | | `acp` | The Agent Client Protocol façade. | |
| 32 | | `tui` | The single-window front end (`mm -tui`): a `ui.Sink` drawing on tcell, one transcript value, an event loop woken by the engine's and tools' goroutines. | | 33 | | `tui` | The single-window front end (`mm -tui`): a `ui.Sink` drawing on tcell, one transcript value, an event loop woken by the engine's and tools' goroutines. | |
added
docs/en/explanation/hooks.md +69 -0 | new file mode 100644 | ||
| @@ -0,0 +1,69 @@ | ||
| 1 | +# Hooks — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +A hook is a command `mm` runs at four moments of a turn: before the model reads a question (`preGeneration`), before and after each tool call (`preToolCall`, `postToolCall`), and once the turn is over (`postGeneration`). It reads a JSON object describing the moment, and may answer with a decision: rewrite the question, add context, stop the turn, deny a call, replace its arguments. It lets a project put its own rules around the agent — what must never run, what must never be sent, what must be logged — without changing `mm`'s code. | |
| 6 | + | |
| 7 | +The design started from ticket 36, which explored a much larger system. This page explains what was kept, where the hooks plug in, and what was left out. | |
| 8 | + | |
| 9 | +## Why external commands | |
| 10 | + | |
| 11 | +The ticket's first requirement was that hooks can be written in **any language**. That rules out a Go plugin interface (it would need a recompilation, and Go's `plugin` package does not exist on every platform `mm` is built for) and an embedded interpreter (it would pick one language for everyone). A process reading stdin and writing stdout is the one interface every language has. | |
| 12 | + | |
| 13 | +It has costs, and they were accepted on purpose: | |
| 14 | + | |
| 15 | +- **A process start per event.** A `preToolCall` hook runs once per tool call; `node` takes some tens of milliseconds to start. Against a local model that spends seconds per answer, this is noise. It is also why every hook has a `timeout`, and why the default is short (10 s): a hook sits in the middle of the turn, and a slow one is felt at once. | |
| 16 | +- **JSON as the contract.** The input and the reply are small and flat, so that a hook in shell with `jq` is as easy to write as one in Python or Node. | |
| 17 | + | |
| 18 | +The same shape is used elsewhere in the ecosystem (editor and agent hooks, git hooks with their arguments): someone who has written one recognises the other. | |
| 19 | + | |
| 20 | +## Where they plug in | |
| 21 | + | |
| 22 | +Every front end — the line mode, the window, the ACP façade, `-p` — hands its turn to one function, `engine.Generate`. And inside it, Genkit's middleware already has a `WrapTool` hook that sees **every** tool call, built-in or MCP, before it runs. Those two points are where the hooks run: | |
| 23 | + | |
| 24 | +``` | |
| 25 | +front end ──▶ engine.Generate | |
| 26 | + ├─ preGeneration (the question, before the first model call) | |
| 27 | + ├─ model ⇄ tools ── WrapTool | |
| 28 | + │ ├─ preToolCall (before the tool, and its consent prompt) | |
| 29 | + │ └─ postToolCall (after the tool) | |
| 30 | + └─ postGeneration (the turn is over, whatever its outcome) | |
| 31 | +``` | |
| 32 | + | |
| 33 | +Placing them there rather than in each front end means one implementation, one set of tests, and no front end that forgets them. A new front end gets the hooks for free. | |
| 34 | + | |
| 35 | +`preToolCall` runs **before** the tool's consent prompt. A denied call therefore never asks the user anything; and a guard keeps working with `yolo: true`, which is when it matters most. | |
| 36 | + | |
| 37 | +The package `internal/hooks` runs the commands and turns their replies into decisions; it knows nothing about Genkit or the front ends. `internal/engine/hooks.go` applies those decisions to the messages and shows them: as the answer, as a failed tool call, or as a notice — a line in the terminal, a dim line in the window, the editor's logs over ACP. | |
| 38 | + | |
| 39 | +## Decisions worth knowing | |
| 40 | + | |
| 41 | +**A stopped question leaves a marker, not the question.** The obvious reason to stop a question is that it holds something that must not reach the model: a password, a key. Keeping it in the history would send it on the next turn. The history therefore keeps `[prompt blocked by a hook: <reason>]` followed by the hook's response — enough for the model to understand what happened, nothing it must not read. The first live run of the demo found a second leak: the audit hook on `postGeneration` received the blocked question and wrote the secret to its log. The post hooks now read the marker too. | |
| 42 | + | |
| 43 | +**Failures follow `onError`, and the default is `warn`.** A hook that crashes is shown, and the turn goes on: a broken logging script must not lock the user out of their agent. A guard should say `onError: fail` — a guard that cannot run must not let the call through. The two needs are opposite, so the choice is per hook. | |
| 44 | + | |
| 45 | +**An unknown event name is an error.** YAML ignores keys it does not know; `preToolcall:` would have been a guard that silently never runs. Refusing to start is the safer failure. | |
| 46 | + | |
| 47 | +**A denied tool call answers the model as text.** Like every tool failure in `mm`, the refusal is the tool's output: `[denied by a hook: <reason>]`. The model can tell the user or try another way, rather than the turn ending on an error. | |
| 48 | + | |
| 49 | +**`postGeneration` is not cancelled with the turn.** An audit log that loses the turns the user interrupted is the wrong way round. The hook runs after a cancellation too, bounded by its own timeout. | |
| 50 | + | |
| 51 | +**A replaced tool input is not written back into the history.** The tool runs with the hook's arguments, but the history keeps what the model asked for. The model's own request is the truthful record of what it did; a session file that shows arguments the model never wrote would mislead whoever reads it later. | |
| 52 | + | |
| 53 | +## Rejected alternatives | |
| 54 | + | |
| 55 | +Ticket 36 explored more than what was built. What was left out, and why: | |
| 56 | + | |
| 57 | +- **Returning the whole `messages` array.** The ticket's first protocol let a hook rewrite the conversation. It is fragile — a hook could drop the system prompt or rewrite past turns, and every hook would have to round-trip the whole history — and the four needs (detect, rewrite, add context, stop) are covered by `prompt`, `context` and `action`. | |
| 58 | +- **Numeric priorities.** The order of the YAML list is the order of execution. A priority field adds a second ordering to keep in mind for no gain when the list is short. | |
| 59 | +- **An `async` mode.** A logging hook is fast; when it is not, `onError: ignore` with a short `timeout` bounds it. A background mode would need a queue, a shutdown flush and a way to report late failures. | |
| 60 | +- **`allowedPaths` / `allowedExtensions`.** The YAML file already starts MCP servers and gives the model `bash`. Whoever writes it is trusted with far more than a hook path; an allowlist would protect nothing. | |
| 61 | +- **Lifecycle, session and front-end hooks** (start-up, configuration loaded, shutdown, window opened, editor connected). No use case asked for them yet. They can be added as events later without changing the protocol. | |
| 62 | + | |
| 63 | +## How it relates to the rest | |
| 64 | + | |
| 65 | +- **Yolo and consent.** Hooks and `yolo: false` are complementary: a hook decides by rule, without a human; consent asks a human, case by case. The hook runs first. See [Yolo mode and permissions](yolo-and-permissions.md). | |
| 66 | +- **Asking another agent.** An agent started by `/ask`, `ask_agent` or `mm --ask` is a separate `mm` process with its own configuration file, so it runs **its own** hooks, not its asker's. | |
| 67 | +- **Context compression** asks the model for a summary through another path than `Generate`; the hooks do not run on it. | |
| 68 | + | |
| 69 | +See the [hooks reference](../reference/hooks.md) for every field, and [Guard the agent with hooks](../how-to/guard-the-agent-with-hooks.md) to write one. | |
| new file mode 100644 | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | +# Hooks — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +A hook is a command `mm` runs at four moments of a turn: before the model reads a question (`preGeneration`), before and after each tool call (`preToolCall`, `postToolCall`), and once the turn is over (`postGeneration`). It reads a JSON object describing the moment, and may answer with a decision: rewrite the question, add context, stop the turn, deny a call, replace its arguments. It lets a project put its own rules around the agent — what must never run, what must never be sent, what must be logged — without changing `mm`'s code. | ||
| 6 | + | ||
| 7 | +The design started from ticket 36, which explored a much larger system. This page explains what was kept, where the hooks plug in, and what was left out. | ||
| 8 | + | ||
| 9 | +## Why external commands | ||
| 10 | + | ||
| 11 | +The ticket's first requirement was that hooks can be written in **any language**. That rules out a Go plugin interface (it would need a recompilation, and Go's `plugin` package does not exist on every platform `mm` is built for) and an embedded interpreter (it would pick one language for everyone). A process reading stdin and writing stdout is the one interface every language has. | ||
| 12 | + | ||
| 13 | +It has costs, and they were accepted on purpose: | ||
| 14 | + | ||
| 15 | +- **A process start per event.** A `preToolCall` hook runs once per tool call; `node` takes some tens of milliseconds to start. Against a local model that spends seconds per answer, this is noise. It is also why every hook has a `timeout`, and why the default is short (10 s): a hook sits in the middle of the turn, and a slow one is felt at once. | ||
| 16 | +- **JSON as the contract.** The input and the reply are small and flat, so that a hook in shell with `jq` is as easy to write as one in Python or Node. | ||
| 17 | + | ||
| 18 | +The same shape is used elsewhere in the ecosystem (editor and agent hooks, git hooks with their arguments): someone who has written one recognises the other. | ||
| 19 | + | ||
| 20 | +## Where they plug in | ||
| 21 | + | ||
| 22 | +Every front end — the line mode, the window, the ACP façade, `-p` — hands its turn to one function, `engine.Generate`. And inside it, Genkit's middleware already has a `WrapTool` hook that sees **every** tool call, built-in or MCP, before it runs. Those two points are where the hooks run: | ||
| 23 | + | ||
| 24 | +``` | ||
| 25 | +front end ──▶ engine.Generate | ||
| 26 | + ├─ preGeneration (the question, before the first model call) | ||
| 27 | + ├─ model ⇄ tools ── WrapTool | ||
| 28 | + │ ├─ preToolCall (before the tool, and its consent prompt) | ||
| 29 | + │ └─ postToolCall (after the tool) | ||
| 30 | + └─ postGeneration (the turn is over, whatever its outcome) | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +Placing them there rather than in each front end means one implementation, one set of tests, and no front end that forgets them. A new front end gets the hooks for free. | ||
| 34 | + | ||
| 35 | +`preToolCall` runs **before** the tool's consent prompt. A denied call therefore never asks the user anything; and a guard keeps working with `yolo: true`, which is when it matters most. | ||
| 36 | + | ||
| 37 | +The package `internal/hooks` runs the commands and turns their replies into decisions; it knows nothing about Genkit or the front ends. `internal/engine/hooks.go` applies those decisions to the messages and shows them: as the answer, as a failed tool call, or as a notice — a line in the terminal, a dim line in the window, the editor's logs over ACP. | ||
| 38 | + | ||
| 39 | +## Decisions worth knowing | ||
| 40 | + | ||
| 41 | +**A stopped question leaves a marker, not the question.** The obvious reason to stop a question is that it holds something that must not reach the model: a password, a key. Keeping it in the history would send it on the next turn. The history therefore keeps `[prompt blocked by a hook: <reason>]` followed by the hook's response — enough for the model to understand what happened, nothing it must not read. The first live run of the demo found a second leak: the audit hook on `postGeneration` received the blocked question and wrote the secret to its log. The post hooks now read the marker too. | ||
| 42 | + | ||
| 43 | +**Failures follow `onError`, and the default is `warn`.** A hook that crashes is shown, and the turn goes on: a broken logging script must not lock the user out of their agent. A guard should say `onError: fail` — a guard that cannot run must not let the call through. The two needs are opposite, so the choice is per hook. | ||
| 44 | + | ||
| 45 | +**An unknown event name is an error.** YAML ignores keys it does not know; `preToolcall:` would have been a guard that silently never runs. Refusing to start is the safer failure. | ||
| 46 | + | ||
| 47 | +**A denied tool call answers the model as text.** Like every tool failure in `mm`, the refusal is the tool's output: `[denied by a hook: <reason>]`. The model can tell the user or try another way, rather than the turn ending on an error. | ||
| 48 | + | ||
| 49 | +**`postGeneration` is not cancelled with the turn.** An audit log that loses the turns the user interrupted is the wrong way round. The hook runs after a cancellation too, bounded by its own timeout. | ||
| 50 | + | ||
| 51 | +**A replaced tool input is not written back into the history.** The tool runs with the hook's arguments, but the history keeps what the model asked for. The model's own request is the truthful record of what it did; a session file that shows arguments the model never wrote would mislead whoever reads it later. | ||
| 52 | + | ||
| 53 | +## Rejected alternatives | ||
| 54 | + | ||
| 55 | +Ticket 36 explored more than what was built. What was left out, and why: | ||
| 56 | + | ||
| 57 | +- **Returning the whole `messages` array.** The ticket's first protocol let a hook rewrite the conversation. It is fragile — a hook could drop the system prompt or rewrite past turns, and every hook would have to round-trip the whole history — and the four needs (detect, rewrite, add context, stop) are covered by `prompt`, `context` and `action`. | ||
| 58 | +- **Numeric priorities.** The order of the YAML list is the order of execution. A priority field adds a second ordering to keep in mind for no gain when the list is short. | ||
| 59 | +- **An `async` mode.** A logging hook is fast; when it is not, `onError: ignore` with a short `timeout` bounds it. A background mode would need a queue, a shutdown flush and a way to report late failures. | ||
| 60 | +- **`allowedPaths` / `allowedExtensions`.** The YAML file already starts MCP servers and gives the model `bash`. Whoever writes it is trusted with far more than a hook path; an allowlist would protect nothing. | ||
| 61 | +- **Lifecycle, session and front-end hooks** (start-up, configuration loaded, shutdown, window opened, editor connected). No use case asked for them yet. They can be added as events later without changing the protocol. | ||
| 62 | + | ||
| 63 | +## How it relates to the rest | ||
| 64 | + | ||
| 65 | +- **Yolo and consent.** Hooks and `yolo: false` are complementary: a hook decides by rule, without a human; consent asks a human, case by case. The hook runs first. See [Yolo mode and permissions](yolo-and-permissions.md). | ||
| 66 | +- **Asking another agent.** An agent started by `/ask`, `ask_agent` or `mm --ask` is a separate `mm` process with its own configuration file, so it runs **its own** hooks, not its asker's. | ||
| 67 | +- **Context compression** asks the model for a summary through another path than `Generate`; the hooks do not run on it. | ||
| 68 | + | ||
| 69 | +See the [hooks reference](../reference/hooks.md) for every field, and [Guard the agent with hooks](../how-to/guard-the-agent-with-hooks.md) to write one. | ||
modified
docs/en/explanation/yolo-and-permissions.md +1 -0 | @@ -51,3 +51,4 @@ One refinement keeps the same principle: the user's file may give an asked agent | ||
| 51 | 51 | - How to turn it on: [How to confirm tool calls](../how-to/confirm-tool-calls.md). |
| 52 | 52 | - The event path the front ends share (`ui.Sink`, `Allow`): [Architecture](architecture.md). |
| 53 | 53 | - Asking another agent: [Asking another agent](asking-another-agent.md). |
| 54 | +- Deciding by rule rather than by asking: [Hooks](hooks.md) — a `preToolCall` hook runs before the consent question, so a call it denies is never put to the user. | |
| @@ -51,3 +51,4 @@ One refinement keeps the same principle: the user's file may give an asked agent | |||
| 51 | - How to turn it on: [How to confirm tool calls](../how-to/confirm-tool-calls.md). | 51 | - How to turn it on: [How to confirm tool calls](../how-to/confirm-tool-calls.md). |
| 52 | - The event path the front ends share (`ui.Sink`, `Allow`): [Architecture](architecture.md). | 52 | - The event path the front ends share (`ui.Sink`, `Allow`): [Architecture](architecture.md). |
| 53 | - Asking another agent: [Asking another agent](asking-another-agent.md). | 53 | - Asking another agent: [Asking another agent](asking-another-agent.md). |
| 54 | +- Deciding by rule rather than by asking: [Hooks](hooks.md) — a `preToolCall` hook runs before the consent question, so a call it denies is never put to the user. | ||
modified
docs/en/how-to/confirm-tool-calls.md +1 -0 | @@ -55,3 +55,4 @@ By default mini-me runs in **yolo mode**: `bash`, file writes and edits run as s | ||
| 55 | 55 | - Every key and its default: [configuration reference](../reference/configuration.md) (`yolo`, `permissions`). |
| 56 | 56 | - What each front end shows: [CLI reference](../reference/cli.md), [ACP reference](../reference/acp.md#sessionrequest_permission-requests-sent). |
| 57 | 57 | - Why yolo is the default and why the asking agent wins: [Yolo mode and permissions](../explanation/yolo-and-permissions.md). |
| 58 | +- Refuse some calls automatically, by rule, without asking: [Guard the agent with hooks](guard-the-agent-with-hooks.md). | |
| @@ -55,3 +55,4 @@ By default mini-me runs in **yolo mode**: `bash`, file writes and edits run as s | |||
| 55 | - Every key and its default: [configuration reference](../reference/configuration.md) (`yolo`, `permissions`). | 55 | - Every key and its default: [configuration reference](../reference/configuration.md) (`yolo`, `permissions`). |
| 56 | - What each front end shows: [CLI reference](../reference/cli.md), [ACP reference](../reference/acp.md#sessionrequest_permission-requests-sent). | 56 | - What each front end shows: [CLI reference](../reference/cli.md), [ACP reference](../reference/acp.md#sessionrequest_permission-requests-sent). |
| 57 | - Why yolo is the default and why the asking agent wins: [Yolo mode and permissions](../explanation/yolo-and-permissions.md). | 57 | - Why yolo is the default and why the asking agent wins: [Yolo mode and permissions](../explanation/yolo-and-permissions.md). |
| 58 | +- Refuse some calls automatically, by rule, without asking: [Guard the agent with hooks](guard-the-agent-with-hooks.md). | ||
added
docs/en/how-to/guard-the-agent-with-hooks.md +100 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/acp.md +2 -1 | @@ -8,7 +8,7 @@ | ||
| 8 | 8 | |--------|-------| |
| 9 | 9 | | Input | stdin, one JSON-RPC message per line. | |
| 10 | 10 | | Output | stdout, JSON-RPC messages only. | |
| 11 | -| Logs | stderr: banner, warnings, `[acp] …` lines, SDK logger. | | |
| 11 | +| Logs | stderr: banner, warnings, `[acp] …` lines, SDK logger, the failure lines of [hooks](hooks.md#failures) (`🪝 <event> hook …`). | | |
| 12 | 12 | | Lifetime | Until the client closes stdin or the process context is cancelled. | |
| 13 | 13 | |
| 14 | 14 | ## `initialize` response |
| @@ -97,6 +97,7 @@ No tool runs during a replay. Logged as `[acp] session <id> loaded from <dir>: N | ||
| 97 | 97 | | `tool_call` status `pending` | Before a tool runs; carries `title`, `kind`, `rawInput`, and `locations` with the absolute path when a file is involved. | |
| 98 | 98 | | `tool_call_update` status `in_progress` | Permission granted, tool executing. | |
| 99 | 99 | | `tool_call_update` status `completed` or `failed` | Tool finished; `content` is a `diff` for file changes, else a `text` block with the output; `rawOutput.output` always carries the output. | |
| 100 | +| `tool_call` then `tool_call_update` status `failed`, title `<tool> — denied by a hook` | A `preToolCall` [hook](hooks.md) denied the call: the tool did not run and no permission was requested; the `text` content is `[denied by a hook: <reason>]`. | | |
| 100 | 101 | | `usage_update` (UNSTABLE in ACP) | After every model call that reported usage (`used` = its input + output tokens), and at the end of every turn, `/new` and `/compact` (`used` = the larger of the last measure and the history's estimate). `size` is the context window the agent knows (`contextWindow` or the probe), `0` when unknown. No `cost`: the servers are local. | |
| 101 | 102 | |
| 102 | 103 | ## Available commands |
| @@ -8,7 +8,7 @@ | |||
| 8 | |--------|-------| | 8 | |--------|-------| |
| 9 | | Input | stdin, one JSON-RPC message per line. | | 9 | | Input | stdin, one JSON-RPC message per line. | |
| 10 | | Output | stdout, JSON-RPC messages only. | | 10 | | Output | stdout, JSON-RPC messages only. | |
| 11 | -| Logs | stderr: banner, warnings, `[acp] …` lines, SDK logger. | | 11 | +| Logs | stderr: banner, warnings, `[acp] …` lines, SDK logger, the failure lines of [hooks](hooks.md#failures) (`🪝 <event> hook …`). | |
| 12 | | Lifetime | Until the client closes stdin or the process context is cancelled. | | 12 | | Lifetime | Until the client closes stdin or the process context is cancelled. | |
| 13 | 13 | ||
| 14 | ## `initialize` response | 14 | ## `initialize` response |
| @@ -97,6 +97,7 @@ No tool runs during a replay. Logged as `[acp] session <id> loaded from <dir>: N | |||
| 97 | | `tool_call` status `pending` | Before a tool runs; carries `title`, `kind`, `rawInput`, and `locations` with the absolute path when a file is involved. | | 97 | | `tool_call` status `pending` | Before a tool runs; carries `title`, `kind`, `rawInput`, and `locations` with the absolute path when a file is involved. | |
| 98 | | `tool_call_update` status `in_progress` | Permission granted, tool executing. | | 98 | | `tool_call_update` status `in_progress` | Permission granted, tool executing. | |
| 99 | | `tool_call_update` status `completed` or `failed` | Tool finished; `content` is a `diff` for file changes, else a `text` block with the output; `rawOutput.output` always carries the output. | | 99 | | `tool_call_update` status `completed` or `failed` | Tool finished; `content` is a `diff` for file changes, else a `text` block with the output; `rawOutput.output` always carries the output. | |
| 100 | +| `tool_call` then `tool_call_update` status `failed`, title `<tool> — denied by a hook` | A `preToolCall` [hook](hooks.md) denied the call: the tool did not run and no permission was requested; the `text` content is `[denied by a hook: <reason>]`. | | ||
| 100 | | `usage_update` (UNSTABLE in ACP) | After every model call that reported usage (`used` = its input + output tokens), and at the end of every turn, `/new` and `/compact` (`used` = the larger of the last measure and the history's estimate). `size` is the context window the agent knows (`contextWindow` or the probe), `0` when unknown. No `cost`: the servers are local. | | 101 | | `usage_update` (UNSTABLE in ACP) | After every model call that reported usage (`used` = its input + output tokens), and at the end of every turn, `/new` and `/compact` (`used` = the larger of the last measure and the history's estimate). `size` is the context window the agent knows (`contextWindow` or the probe), `0` when unknown. No `cost`: the servers are local. | |
| 101 | 102 | ||
| 102 | ## Available commands | 103 | ## Available commands |
modified
docs/en/reference/cli.md +1 -1 | @@ -75,7 +75,7 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | ||
| 75 | 75 | | Line | Content | |
| 76 | 76 | |------|---------| |
| 77 | 77 | | `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | |
| 78 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …]` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`., and, while `trace.enabled` is on, `trace: <absolute dir>` (`trace: <session cwd>/<dir>` in ACP mode). `agents.md:` (with `agentsMd.enabled`): `<absolute path> (~N tokens)` when the project's file is read, `skipped (too long)` above `agentsMd.maxChars`, `ignored (structured output)` for an agent with `structuredOutput`, `<session cwd>/<path>` in ACP mode; absent when there is no file. A read file of 20 % of a known context window or more adds the warning `<path> is ~N tokens, X % of the W-token context window — every request carries it …`. | | |
| 78 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …][ \| hooks: …]` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`., and, while `trace.enabled` is on, `trace: <absolute dir>` (`trace: <session cwd>/<dir>` in ACP mode). `hooks:` (when the configuration declares any): `<event>×<count>` per event that has hooks, e.g. `hooks: preGeneration×1 preToolCall×2` — see [hooks](hooks.md#start-up-banner). `agents.md:` (with `agentsMd.enabled`): `<absolute path> (~N tokens)` when the project's file is read, `skipped (too long)` above `agentsMd.maxChars`, `ignored (structured output)` for an agent with `structuredOutput`, `<session cwd>/<path>` in ACP mode; absent when there is no file. A read file of 20 % of a known context window or more adds the warning `<path> is ~N tokens, X % of the W-token context window — every request carries it …`. | | |
| 79 | 79 | | `Agent ready. Commands: …` | Terminal mode only. | |
| 80 | 80 | | `Shortcut: Ctrl+C …` | Terminal mode only. | |
| 81 | 81 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | |
| @@ -75,7 +75,7 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | |||
| 75 | | Line | Content | | 75 | | Line | Content | |
| 76 | |------|---------| | 76 | |------|---------| |
| 77 | | `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | | 77 | | `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | |
| 78 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …]` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`., and, while `trace.enabled` is on, `trace: <absolute dir>` (`trace: <session cwd>/<dir>` in ACP mode). `agents.md:` (with `agentsMd.enabled`): `<absolute path> (~N tokens)` when the project's file is read, `skipped (too long)` above `agentsMd.maxChars`, `ignored (structured output)` for an agent with `structuredOutput`, `<session cwd>/<path>` in ACP mode; absent when there is no file. A read file of 20 % of a known context window or more adds the warning `<path> is ~N tokens, X % of the W-token context window — every request carries it …`. | | 78 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …][ \| hooks: …]` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`., and, while `trace.enabled` is on, `trace: <absolute dir>` (`trace: <session cwd>/<dir>` in ACP mode). `hooks:` (when the configuration declares any): `<event>×<count>` per event that has hooks, e.g. `hooks: preGeneration×1 preToolCall×2` — see [hooks](hooks.md#start-up-banner). `agents.md:` (with `agentsMd.enabled`): `<absolute path> (~N tokens)` when the project's file is read, `skipped (too long)` above `agentsMd.maxChars`, `ignored (structured output)` for an agent with `structuredOutput`, `<session cwd>/<path>` in ACP mode; absent when there is no file. A read file of 20 % of a known context window or more adds the warning `<path> is ~N tokens, X % of the W-token context window — every request carries it …`. | |
| 79 | | `Agent ready. Commands: …` | Terminal mode only. | | 79 | | `Agent ready. Commands: …` | Terminal mode only. | |
| 80 | | `Shortcut: Ctrl+C …` | Terminal mode only. | | 80 | | `Shortcut: Ctrl+C …` | Terminal mode only. | |
| 81 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | | 81 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | |
modified
docs/en/reference/configuration.md +2 -0 | @@ -34,6 +34,7 @@ | ||
| 34 | 34 | | `agentsMd` | map | see below | Append the project's `AGENTS.md` to the system prompt of every conversation. On by default. | |
| 35 | 35 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | |
| 36 | 36 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 37 | +| `hooks` | map event → list | none | Commands run before a question reaches the model (`preGeneration`), before and after each tool call (`preToolCall`, `postToolCall`) and after the turn (`postGeneration`); each reads one JSON object on stdin and may answer one on stdout. Entry keys: `command` (required), `matcher` (tool events only), `timeout` (default `10s`), `onError` (`fail` \| `warn` \| `ignore`, default `warn`). See [hooks](hooks.md). | | |
| 37 | 38 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | |
| 38 | 39 | | `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask`, `ask_agent` or `mm --ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent: the `yolo` of its `mmConfigs` entry decides when set, else the asking agent's `yolo`. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | |
| 39 | 40 | | `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. | |
| @@ -211,6 +212,7 @@ Reported on stderr as `[config error: <path>: <message>]`, exit code 1 — in ev | ||
| 211 | 212 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | |
| 212 | 213 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | |
| 213 | 214 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` | |
| 215 | +| `hooks: unknown event "x" (…)`, `hooks.<event>[N]: …` | an invalid `hooks:` block — see [hooks](hooks.md#validation-errors) | | |
| 214 | 216 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | |
| 215 | 217 | |
| 216 | 218 | A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`). |
| @@ -34,6 +34,7 @@ | |||
| 34 | | `agentsMd` | map | see below | Append the project's `AGENTS.md` to the system prompt of every conversation. On by default. | | 34 | | `agentsMd` | map | see below | Append the project's `AGENTS.md` to the system prompt of every conversation. On by default. | |
| 35 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | | 35 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | |
| 36 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | | 36 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 37 | +| `hooks` | map event → list | none | Commands run before a question reaches the model (`preGeneration`), before and after each tool call (`preToolCall`, `postToolCall`) and after the turn (`postGeneration`); each reads one JSON object on stdin and may answer one on stdout. Entry keys: `command` (required), `matcher` (tool events only), `timeout` (default `10s`), `onError` (`fail` \| `warn` \| `ignore`, default `warn`). See [hooks](hooks.md). | | ||
| 37 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | | 38 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | |
| 38 | | `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask`, `ask_agent` or `mm --ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent: the `yolo` of its `mmConfigs` entry decides when set, else the asking agent's `yolo`. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | | 39 | | `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask`, `ask_agent` or `mm --ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent: the `yolo` of its `mmConfigs` entry decides when set, else the asking agent's `yolo`. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | |
| 39 | | `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. | | 40 | | `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. | |
| @@ -211,6 +212,7 @@ Reported on stderr as `[config error: <path>: <message>]`, exit code 1 — in ev | |||
| 211 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | | 212 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | |
| 212 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | | 213 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | |
| 213 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` | | 214 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` | |
| 215 | +| `hooks: unknown event "x" (…)`, `hooks.<event>[N]: …` | an invalid `hooks:` block — see [hooks](hooks.md#validation-errors) | | ||
| 214 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | | 216 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | |
| 215 | 217 | ||
| 216 | A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`). | 218 | A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`). |
added
docs/en/reference/hooks.md +206 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/README.md +3 -0 | @@ -35,6 +35,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 35 | 35 | - [Exécuter une invite unique et quitter (`-p`/`--prompt`)](how-to/executer-une-invite-unique.md) |
| 36 | 36 | - [Vérifier que les outils partent dans un ordre stable](how-to/verify-tool-order.md) — tests, proxy d'enregistrement, et gain du cache de prompt avant et après le ticket 53 |
| 37 | 37 | - [Récupérer une page web en Markdown (`fetch_content`)](how-to/fetch-a-web-page.md) — lire une page, ou l'enregistrer dans un fichier sans l'afficher |
| 38 | +- [Protéger l'agent avec des hooks](how-to/guard-the-agent-with-hooks.md) — refuser un appel d'outil, arrêter ou réécrire une question, ajouter du contexte, journaliser chaque tour, avec un script dans n'importe quel langage | |
| 38 | 39 | |
| 39 | 40 | ### Guides pratiques MCP (Model Context Protocol) |
| 40 | 41 | - [Utiliser Context7 pour la documentation](how-to/mcp-utiliser-context7.md) |
| @@ -48,6 +49,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 48 | 49 | - [Fichier de configuration](reference/configuration.md) — chaque clé YAML, sa valeur par défaut et sa règle de validation |
| 49 | 50 | - [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user` |
| 50 | 51 | - [Format d'un fichier de skill](reference/skill-format.md) |
| 52 | +- [Les hooks](reference/hooks.md) — les quatre événements, le JSON qu'un hook lit et écrit, `matcher`, `timeout`, `onError`, erreurs de validation | |
| 51 | 53 | - [Surface Agent Client Protocol](reference/acp.md) |
| 52 | 54 | |
| 53 | 55 | ### Référence MCP (Model Context Protocol) |
| @@ -69,6 +71,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 69 | 71 | - [Sortie structurée](explanation/structured-output.md) — pourquoi des déclarations Go, pourquoi aussi dans le prompt système, pourquoi pas d'outils, ce qui garantit la réponse |
| 70 | 72 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur |
| 71 | 73 | - [Interroger l'utilisateur](explanation/asking-the-user.md) — pourquoi `ask_user` demande à chaque interface à sa façon, comment remontent les questions d'un agent appelé, le schéma souple (la grammaire alphabétique de llama.cpp), le chien de garde pendant qu'un humain répond, le manque du SDK ACP |
| 74 | +- [Les hooks](explanation/hooks.md) — pourquoi des commandes externes, pourquoi ils se branchent sur `engine.Generate` et `WrapTool`, pourquoi une question arrêtée laisse un marqueur, ce que le ticket 36 a laissé de côté | |
| 72 | 75 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 73 | 76 | |
| 74 | 77 | ### Explication MCP (Model Context Protocol) |
| @@ -35,6 +35,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 35 | - [Exécuter une invite unique et quitter (`-p`/`--prompt`)](how-to/executer-une-invite-unique.md) | 35 | - [Exécuter une invite unique et quitter (`-p`/`--prompt`)](how-to/executer-une-invite-unique.md) |
| 36 | - [Vérifier que les outils partent dans un ordre stable](how-to/verify-tool-order.md) — tests, proxy d'enregistrement, et gain du cache de prompt avant et après le ticket 53 | 36 | - [Vérifier que les outils partent dans un ordre stable](how-to/verify-tool-order.md) — tests, proxy d'enregistrement, et gain du cache de prompt avant et après le ticket 53 |
| 37 | - [Récupérer une page web en Markdown (`fetch_content`)](how-to/fetch-a-web-page.md) — lire une page, ou l'enregistrer dans un fichier sans l'afficher | 37 | - [Récupérer une page web en Markdown (`fetch_content`)](how-to/fetch-a-web-page.md) — lire une page, ou l'enregistrer dans un fichier sans l'afficher |
| 38 | +- [Protéger l'agent avec des hooks](how-to/guard-the-agent-with-hooks.md) — refuser un appel d'outil, arrêter ou réécrire une question, ajouter du contexte, journaliser chaque tour, avec un script dans n'importe quel langage | ||
| 38 | 39 | ||
| 39 | ### Guides pratiques MCP (Model Context Protocol) | 40 | ### Guides pratiques MCP (Model Context Protocol) |
| 40 | - [Utiliser Context7 pour la documentation](how-to/mcp-utiliser-context7.md) | 41 | - [Utiliser Context7 pour la documentation](how-to/mcp-utiliser-context7.md) |
| @@ -48,6 +49,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 48 | - [Fichier de configuration](reference/configuration.md) — chaque clé YAML, sa valeur par défaut et sa règle de validation | 49 | - [Fichier de configuration](reference/configuration.md) — chaque clé YAML, sa valeur par défaut et sa règle de validation |
| 49 | - [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user` | 50 | - [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user` |
| 50 | - [Format d'un fichier de skill](reference/skill-format.md) | 51 | - [Format d'un fichier de skill](reference/skill-format.md) |
| 52 | +- [Les hooks](reference/hooks.md) — les quatre événements, le JSON qu'un hook lit et écrit, `matcher`, `timeout`, `onError`, erreurs de validation | ||
| 51 | - [Surface Agent Client Protocol](reference/acp.md) | 53 | - [Surface Agent Client Protocol](reference/acp.md) |
| 52 | 54 | ||
| 53 | ### Référence MCP (Model Context Protocol) | 55 | ### Référence MCP (Model Context Protocol) |
| @@ -69,6 +71,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 69 | - [Sortie structurée](explanation/structured-output.md) — pourquoi des déclarations Go, pourquoi aussi dans le prompt système, pourquoi pas d'outils, ce qui garantit la réponse | 71 | - [Sortie structurée](explanation/structured-output.md) — pourquoi des déclarations Go, pourquoi aussi dans le prompt système, pourquoi pas d'outils, ce qui garantit la réponse |
| 70 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur | 72 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur |
| 71 | - [Interroger l'utilisateur](explanation/asking-the-user.md) — pourquoi `ask_user` demande à chaque interface à sa façon, comment remontent les questions d'un agent appelé, le schéma souple (la grammaire alphabétique de llama.cpp), le chien de garde pendant qu'un humain répond, le manque du SDK ACP | 73 | - [Interroger l'utilisateur](explanation/asking-the-user.md) — pourquoi `ask_user` demande à chaque interface à sa façon, comment remontent les questions d'un agent appelé, le schéma souple (la grammaire alphabétique de llama.cpp), le chien de garde pendant qu'un humain répond, le manque du SDK ACP |
| 74 | +- [Les hooks](explanation/hooks.md) — pourquoi des commandes externes, pourquoi ils se branchent sur `engine.Generate` et `WrapTool`, pourquoi une question arrêtée laisse un marqueur, ce que le ticket 36 a laissé de côté | ||
| 72 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) | 75 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 73 | 76 | ||
| 74 | ### Explication MCP (Model Context Protocol) | 77 | ### Explication MCP (Model Context Protocol) |
modified
docs/fr/explanation/architecture.md +1 -0 | @@ -27,6 +27,7 @@ Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages | ||
| 27 | 27 | | `spinner` | L'indicateur « toujours en cours » sur un terminal. | |
| 28 | 28 | | `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. | |
| 29 | 29 | | `consent` | Si un appel d'outil peut s'exécuter : l'interrupteur `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient par outil (`Policy`), et le point d'accroche par lequel le mode ligne demande (`SetPrompter`). La fenêtre, la façade éditeur et `/ask` dessinent la question à leur manière et partagent ces règles. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). | |
| 30 | +| `hooks` | Le bloc `hooks:` (ticket 36) : lance chaque hook avec `sh -c`, écrit son entrée JSON, lit sa réponse, applique l'ordre de la chaîne et `onError`, et renvoie des décisions (`PreGeneration`, `PreToolCall`, `PostToolCall`, `PostGeneration`). Ni Genkit ni front end : `engine/hooks.go` applique les décisions autour de `Generate` et dans le middleware `WrapTool`, si bien que tous les front ends en profitent. Voir [Les hooks](hooks.md). | | |
| 30 | 31 | | `agent` | Le REPL du terminal. | |
| 31 | 32 | | `acp` | La façade Agent Client Protocol. | |
| 32 | 33 | | `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. | |
| @@ -27,6 +27,7 @@ Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages | |||
| 27 | | `spinner` | L'indicateur « toujours en cours » sur un terminal. | | 27 | | `spinner` | L'indicateur « toujours en cours » sur un terminal. | |
| 28 | | `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. | | 28 | | `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. | |
| 29 | | `consent` | Si un appel d'outil peut s'exécuter : l'interrupteur `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient par outil (`Policy`), et le point d'accroche par lequel le mode ligne demande (`SetPrompter`). La fenêtre, la façade éditeur et `/ask` dessinent la question à leur manière et partagent ces règles. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). | | 29 | | `consent` | Si un appel d'outil peut s'exécuter : l'interrupteur `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient par outil (`Policy`), et le point d'accroche par lequel le mode ligne demande (`SetPrompter`). La fenêtre, la façade éditeur et `/ask` dessinent la question à leur manière et partagent ces règles. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). | |
| 30 | +| `hooks` | Le bloc `hooks:` (ticket 36) : lance chaque hook avec `sh -c`, écrit son entrée JSON, lit sa réponse, applique l'ordre de la chaîne et `onError`, et renvoie des décisions (`PreGeneration`, `PreToolCall`, `PostToolCall`, `PostGeneration`). Ni Genkit ni front end : `engine/hooks.go` applique les décisions autour de `Generate` et dans le middleware `WrapTool`, si bien que tous les front ends en profitent. Voir [Les hooks](hooks.md). | | ||
| 30 | | `agent` | Le REPL du terminal. | | 31 | | `agent` | Le REPL du terminal. | |
| 31 | | `acp` | La façade Agent Client Protocol. | | 32 | | `acp` | La façade Agent Client Protocol. | |
| 32 | | `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. | | 33 | | `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. | |
added
docs/fr/explanation/hooks.md +69 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/explanation/yolo-and-permissions.md +1 -0 | @@ -51,3 +51,4 @@ Un raffinement garde le même principe : le fichier de l'utilisateur peut donner | ||
| 51 | 51 | - Comment l'activer : [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). |
| 52 | 52 | - Le chemin d'événements que partagent les façades (`ui.Sink`, `Allow`) : [Architecture](architecture.md). |
| 53 | 53 | - Interroger un autre agent : [Interroger un autre agent](asking-another-agent.md). |
| 54 | +- Décider selon une règle plutôt qu'en demandant : [Les hooks](hooks.md) — un hook `preToolCall` s'exécute avant la demande d'autorisation, si bien qu'un appel qu'il refuse n'est jamais soumis à l'utilisateur. | |
| @@ -51,3 +51,4 @@ Un raffinement garde le même principe : le fichier de l'utilisateur peut donner | |||
| 51 | - Comment l'activer : [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | 51 | - Comment l'activer : [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). |
| 52 | - Le chemin d'événements que partagent les façades (`ui.Sink`, `Allow`) : [Architecture](architecture.md). | 52 | - Le chemin d'événements que partagent les façades (`ui.Sink`, `Allow`) : [Architecture](architecture.md). |
| 53 | - Interroger un autre agent : [Interroger un autre agent](asking-another-agent.md). | 53 | - Interroger un autre agent : [Interroger un autre agent](asking-another-agent.md). |
| 54 | +- Décider selon une règle plutôt qu'en demandant : [Les hooks](hooks.md) — un hook `preToolCall` s'exécute avant la demande d'autorisation, si bien qu'un appel qu'il refuse n'est jamais soumis à l'utilisateur. | ||
modified
docs/fr/how-to/confirm-tool-calls.md +1 -0 | @@ -55,3 +55,4 @@ Par défaut, mini-me tourne en **mode yolo** : `bash`, les écritures et les mod | ||
| 55 | 55 | - Chaque clé et sa valeur par défaut : [référence de la configuration](../reference/configuration.md) (`yolo`, `permissions`). |
| 56 | 56 | - Ce que montre chaque façade : [référence de la CLI](../reference/cli.md), [référence ACP](../reference/acp.md#requêtes-sessionrequest_permission-envoyées). |
| 57 | 57 | - Pourquoi yolo est la valeur par défaut et pourquoi l'agent qui interroge l'emporte : [Le mode yolo et les permissions](../explanation/yolo-and-permissions.md). |
| 58 | +- Refuser certains appels automatiquement, selon une règle, sans rien demander : [Protéger l'agent avec des hooks](guard-the-agent-with-hooks.md). | |
| @@ -55,3 +55,4 @@ Par défaut, mini-me tourne en **mode yolo** : `bash`, les écritures et les mod | |||
| 55 | - Chaque clé et sa valeur par défaut : [référence de la configuration](../reference/configuration.md) (`yolo`, `permissions`). | 55 | - Chaque clé et sa valeur par défaut : [référence de la configuration](../reference/configuration.md) (`yolo`, `permissions`). |
| 56 | - Ce que montre chaque façade : [référence de la CLI](../reference/cli.md), [référence ACP](../reference/acp.md#requêtes-sessionrequest_permission-envoyées). | 56 | - Ce que montre chaque façade : [référence de la CLI](../reference/cli.md), [référence ACP](../reference/acp.md#requêtes-sessionrequest_permission-envoyées). |
| 57 | - Pourquoi yolo est la valeur par défaut et pourquoi l'agent qui interroge l'emporte : [Le mode yolo et les permissions](../explanation/yolo-and-permissions.md). | 57 | - Pourquoi yolo est la valeur par défaut et pourquoi l'agent qui interroge l'emporte : [Le mode yolo et les permissions](../explanation/yolo-and-permissions.md). |
| 58 | +- Refuser certains appels automatiquement, selon une règle, sans rien demander : [Protéger l'agent avec des hooks](guard-the-agent-with-hooks.md). | ||
added
docs/fr/how-to/guard-the-agent-with-hooks.md +100 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/acp.md +2 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/cli.md +1 -1 | @@ -75,7 +75,7 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | ||
| 75 | 75 | | Ligne | Contenu | |
| 76 | 76 | |-------|---------| |
| 77 | 77 | | `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | |
| 78 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …]` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`., et, tant que `trace.enabled` est actif, `trace: <répertoire absolu>` (`trace: <session cwd>/<dir>` en mode ACP). `agents.md:` (avec `agentsMd.enabled`) : `<chemin absolu> (~N tokens)` quand le fichier du projet est lu, `skipped (too long)` au-delà de `agentsMd.maxChars`, `ignored (structured output)` pour un agent avec `structuredOutput`, `<session cwd>/<chemin>` en mode ACP ; absent quand il n'y a pas de fichier. Un fichier lu qui occupe 20 % ou plus d'une fenêtre de contexte connue ajoute l'avertissement `<chemin> is ~N tokens, X % of the W-token context window — every request carries it …`. | | |
| 78 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …][ \| hooks: …]` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`., et, tant que `trace.enabled` est actif, `trace: <répertoire absolu>` (`trace: <session cwd>/<dir>` en mode ACP). `hooks:` (quand la configuration en déclare) : `<événement>×<nombre>` par événement qui a des hooks, par exemple `hooks: preGeneration×1 preToolCall×2` — voir [les hooks](hooks.md#bannière-de-démarrage). `agents.md:` (avec `agentsMd.enabled`) : `<chemin absolu> (~N tokens)` quand le fichier du projet est lu, `skipped (too long)` au-delà de `agentsMd.maxChars`, `ignored (structured output)` pour un agent avec `structuredOutput`, `<session cwd>/<chemin>` en mode ACP ; absent quand il n'y a pas de fichier. Un fichier lu qui occupe 20 % ou plus d'une fenêtre de contexte connue ajoute l'avertissement `<chemin> is ~N tokens, X % of the W-token context window — every request carries it …`. | | |
| 79 | 79 | | `Agent ready. Commands: …` | Mode terminal seulement. | |
| 80 | 80 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | |
| 81 | 81 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | |
| @@ -75,7 +75,7 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | |||
| 75 | | Ligne | Contenu | | 75 | | Ligne | Contenu | |
| 76 | |-------|---------| | 76 | |-------|---------| |
| 77 | | `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | | 77 | | `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | |
| 78 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …]` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`., et, tant que `trace.enabled` est actif, `trace: <répertoire absolu>` (`trace: <session cwd>/<dir>` en mode ACP). `agents.md:` (avec `agentsMd.enabled`) : `<chemin absolu> (~N tokens)` quand le fichier du projet est lu, `skipped (too long)` au-delà de `agentsMd.maxChars`, `ignored (structured output)` pour un agent avec `structuredOutput`, `<session cwd>/<chemin>` en mode ACP ; absent quand il n'y a pas de fichier. Un fichier lu qui occupe 20 % ou plus d'une fenêtre de contexte connue ajoute l'avertissement `<chemin> is ~N tokens, X % of the W-token context window — every request carries it …`. | | 78 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| agents.md: …][ \| trace: …][ \| hooks: …]` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`., et, tant que `trace.enabled` est actif, `trace: <répertoire absolu>` (`trace: <session cwd>/<dir>` en mode ACP). `hooks:` (quand la configuration en déclare) : `<événement>×<nombre>` par événement qui a des hooks, par exemple `hooks: preGeneration×1 preToolCall×2` — voir [les hooks](hooks.md#bannière-de-démarrage). `agents.md:` (avec `agentsMd.enabled`) : `<chemin absolu> (~N tokens)` quand le fichier du projet est lu, `skipped (too long)` au-delà de `agentsMd.maxChars`, `ignored (structured output)` pour un agent avec `structuredOutput`, `<session cwd>/<chemin>` en mode ACP ; absent quand il n'y a pas de fichier. Un fichier lu qui occupe 20 % ou plus d'une fenêtre de contexte connue ajoute l'avertissement `<chemin> is ~N tokens, X % of the W-token context window — every request carries it …`. | |
| 79 | | `Agent ready. Commands: …` | Mode terminal seulement. | | 79 | | `Agent ready. Commands: …` | Mode terminal seulement. | |
| 80 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | | 80 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | |
| 81 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | | 81 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | |
modified
docs/fr/reference/configuration.md +2 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/fr/reference/hooks.md +206 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/config/config.go +10 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/config/hooks.go +213 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/config/hooks_test.go +103 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/engine/engine.go +22 -2 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/engine/hooks.go +230 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/engine/hooks_test.go +272 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/hooks/example_test.go +77 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/hooks/exec.go +168 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/hooks/hooks.go +268 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/hooks/hooks_test.go +295 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/sink.go +8 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/ui/ui.go +21 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/ui/ui_test.go +32 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
main.go +11 -2 Large diff collapsed to keep the page fast — load it when you need it.