typeclaw 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +15 -13
  2. package/auth.schema.json +41 -0
  3. package/cron.schema.json +8 -0
  4. package/package.json +1 -1
  5. package/secrets.schema.json +41 -0
  6. package/src/agent/auth.ts +45 -22
  7. package/src/agent/index.ts +189 -19
  8. package/src/agent/multimodal/index.ts +12 -0
  9. package/src/agent/multimodal/look-at.ts +185 -0
  10. package/src/agent/multimodal/looker.ts +145 -0
  11. package/src/agent/plugin-tools.ts +30 -1
  12. package/src/agent/session-origin.ts +194 -46
  13. package/src/agent/subagents.ts +57 -1
  14. package/src/agent/system-prompt.ts +1 -1
  15. package/src/agent/tool-result-budget.ts +121 -0
  16. package/src/bundled-plugins/backup/index.ts +23 -8
  17. package/src/bundled-plugins/backup/runner.ts +22 -0
  18. package/src/bundled-plugins/memory/README.md +13 -10
  19. package/src/bundled-plugins/memory/append-tool.ts +87 -61
  20. package/src/bundled-plugins/memory/dreaming.ts +137 -7
  21. package/src/bundled-plugins/memory/find-entry-tool.ts +62 -0
  22. package/src/bundled-plugins/memory/fragment-parser.ts +19 -44
  23. package/src/bundled-plugins/memory/index.ts +91 -8
  24. package/src/bundled-plugins/memory/load-memory.ts +74 -34
  25. package/src/bundled-plugins/memory/memory-logger.ts +72 -29
  26. package/src/bundled-plugins/memory/migration.ts +276 -0
  27. package/src/bundled-plugins/memory/stream-events.ts +55 -0
  28. package/src/bundled-plugins/memory/stream-io.ts +63 -0
  29. package/src/bundled-plugins/memory/watermark.ts +48 -8
  30. package/src/bundled-plugins/security/index.ts +103 -10
  31. package/src/bundled-plugins/security/permissions.ts +12 -0
  32. package/src/bundled-plugins/security/policies/git-exfil.ts +51 -18
  33. package/src/bundled-plugins/tool-result-cap/README.md +9 -4
  34. package/src/bundled-plugins/tool-result-cap/cap-jsonl.ts +115 -0
  35. package/src/bundled-plugins/tool-result-cap/cap-result.ts +25 -13
  36. package/src/bundled-plugins/tool-result-cap/index.ts +16 -2
  37. package/src/channels/adapters/discord-bot-classify.ts +2 -6
  38. package/src/channels/adapters/discord-bot.ts +4 -45
  39. package/src/channels/adapters/kakaotalk-classify.ts +3 -7
  40. package/src/channels/adapters/kakaotalk.ts +28 -47
  41. package/src/channels/adapters/slack-bot-classify.ts +2 -6
  42. package/src/channels/adapters/slack-bot.ts +4 -50
  43. package/src/channels/adapters/telegram-bot-classify.ts +8 -10
  44. package/src/channels/adapters/telegram-bot.ts +3 -16
  45. package/src/channels/index.ts +3 -2
  46. package/src/channels/manager.ts +15 -1
  47. package/src/channels/persistence.ts +44 -10
  48. package/src/channels/router.ts +228 -19
  49. package/src/channels/schema.ts +6 -156
  50. package/src/cli/channel.ts +200 -4
  51. package/src/cli/compose-usage.ts +182 -0
  52. package/src/cli/compose.ts +33 -0
  53. package/src/cli/hostd.ts +49 -1
  54. package/src/cli/index.ts +4 -0
  55. package/src/cli/init.ts +809 -300
  56. package/src/cli/model.ts +244 -0
  57. package/src/cli/provider.ts +404 -0
  58. package/src/cli/reload.ts +11 -3
  59. package/src/cli/role.ts +156 -0
  60. package/src/cli/run.ts +3 -1
  61. package/src/cli/tui.ts +13 -3
  62. package/src/cli/usage-args.ts +47 -0
  63. package/src/cli/usage.ts +97 -0
  64. package/src/compose/index.ts +1 -0
  65. package/src/compose/usage.ts +65 -0
  66. package/src/config/config.ts +491 -19
  67. package/src/config/index.ts +15 -1
  68. package/src/config/models-mutation.ts +200 -0
  69. package/src/config/providers-mutation.ts +250 -0
  70. package/src/config/providers.ts +141 -2
  71. package/src/config/reloadable.ts +15 -4
  72. package/src/container/index.ts +6 -1
  73. package/src/container/port.ts +10 -0
  74. package/src/container/require-running.ts +33 -0
  75. package/src/container/start.ts +81 -63
  76. package/src/cron/consumer.ts +22 -2
  77. package/src/cron/index.ts +45 -4
  78. package/src/cron/schema.ts +104 -0
  79. package/src/doctor/checks.ts +51 -34
  80. package/src/doctor/plugin-bridge.ts +28 -4
  81. package/src/git/system-commit.ts +103 -0
  82. package/src/hostd/daemon.ts +16 -0
  83. package/src/hostd/kakao-renewal-manager.ts +223 -0
  84. package/src/hostd/paths.ts +7 -0
  85. package/src/init/dockerfile.ts +36 -10
  86. package/src/init/gitignore.ts +1 -1
  87. package/src/init/index.ts +213 -85
  88. package/src/init/kakaotalk-auth.ts +18 -1
  89. package/src/init/models-dev.ts +26 -1
  90. package/src/init/run-owner-claim.ts +77 -0
  91. package/src/permissions/builtins.ts +70 -0
  92. package/src/permissions/grant.ts +99 -0
  93. package/src/permissions/index.ts +29 -0
  94. package/src/permissions/match-rule.ts +305 -0
  95. package/src/permissions/permissions.ts +196 -0
  96. package/src/permissions/resolve.ts +80 -0
  97. package/src/permissions/schema.ts +79 -0
  98. package/src/plugin/context.ts +8 -4
  99. package/src/plugin/define.ts +2 -0
  100. package/src/plugin/index.ts +2 -0
  101. package/src/plugin/manager.ts +41 -0
  102. package/src/plugin/registry.ts +9 -0
  103. package/src/plugin/types.ts +35 -1
  104. package/src/reload/client.ts +25 -1
  105. package/src/role-claim/client.ts +182 -0
  106. package/src/role-claim/code.ts +53 -0
  107. package/src/role-claim/controller.ts +194 -0
  108. package/src/role-claim/index.ts +19 -0
  109. package/src/role-claim/match-rule.ts +43 -0
  110. package/src/role-claim/pending.ts +100 -0
  111. package/src/run/channel-session-factory.ts +76 -5
  112. package/src/run/index.ts +68 -7
  113. package/src/secrets/encryption.ts +116 -0
  114. package/src/secrets/kakao-renewal.ts +248 -0
  115. package/src/secrets/kakao-store.ts +66 -7
  116. package/src/secrets/keys.ts +173 -0
  117. package/src/secrets/schema.ts +23 -0
  118. package/src/secrets/storage.ts +83 -0
  119. package/src/server/index.ts +198 -71
  120. package/src/shared/index.ts +4 -0
  121. package/src/shared/protocol.ts +27 -0
  122. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +3 -3
  123. package/src/skills/typeclaw-config/SKILL.md +104 -112
  124. package/src/skills/typeclaw-memory/SKILL.md +9 -9
  125. package/src/skills/typeclaw-permissions/SKILL.md +166 -0
  126. package/src/stream/types.ts +7 -1
  127. package/src/tui/client.ts +66 -5
  128. package/src/tui/index.ts +61 -9
  129. package/src/usage/aggregate.ts +117 -0
  130. package/src/usage/format.ts +30 -0
  131. package/src/usage/index.ts +68 -0
  132. package/src/usage/report.ts +354 -0
  133. package/src/usage/scan.ts +186 -0
  134. package/typeclaw.schema.json +134 -98
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: typeclaw-config
3
- description: "Read or edit typeclaw.json: model, port, mounts, plugins, channels (Discord allow rules + engagement), portForward (auto port forwarding policy), dockerfile (tmux/gh/python/ffmpeg toggles + append), gitignore.append. Also: any question about a default value or whether a behavior is already on by default — port forwarding, channel visibility, model choice, container packages (tmux/gh/python on by default; ffmpeg off), anything ending in 'by default', 'automatically', 'out of the box', 'do I need to configure', 'is X on', 'what does X default to', '기본값', '기본적으로', '자동으로', '디폴트'. MUST load before saying you do not know what X defaults to, or proposing to add a field whose default the user is asking about — most fields already default to the behavior the user expects (portForward defaults to forwarding every container LISTEN; tmux/gh/python are pre-installed in the container; no edit needed). Read it before touching typeclaw.json — strict schema, mix of live-reloadable and restart-required fields."
3
+ description: "Read or edit typeclaw.json: model, port, mounts, plugins, channels (per-adapter engagement and history; access control lives in roles — see typeclaw-permissions), portForward (auto port forwarding policy), docker.file (tmux/gh/python/ffmpeg toggles + append), git.ignore.append. Also: any question about a default value or whether a behavior is already on by default — port forwarding, channel visibility, model choice, container packages (tmux/gh/python on by default; ffmpeg off), anything ending in 'by default', 'automatically', 'out of the box', 'do I need to configure', 'is X on', 'what does X default to', '기본값', '기본적으로', '자동으로', '디폴트'. MUST load before saying you do not know what X defaults to, or proposing to add a field whose default the user is asking about — most fields already default to the behavior the user expects (portForward defaults to forwarding every container LISTEN; tmux/gh/python are pre-installed in the container; no edit needed). Read it before touching typeclaw.json — strict schema, mix of live-reloadable and restart-required fields."
4
4
  ---
5
5
 
6
6
  # typeclaw-config
@@ -18,9 +18,9 @@ The runtime reads `typeclaw.json` at container startup. Some fields are picked u
18
18
  - `mounts` — additional host directories the user has chosen to expose to you. Each entry produces a `docker run -v <hostPath>:/agent/mounts/<name>` flag at `typeclaw start` time, so the directory shows up at `mounts/<name>` inside your agent folder. **The launcher reads this; the running container does not.** Editing `mounts` only takes effect on the next `typeclaw start`. **Restart-required.**
19
19
  - `plugins` — array of plugin package names loaded at server boot. **Restart-required.**
20
20
  - `alias` — additional names the agent answers to when a channel message contains its name in plain text (no `<@id>` mention). The agent folder's directory name (`basename(agentDir)`) is always implicit; `alias` adds further forms (Latin transliteration, nicknames, Korean particles, etc.). Used by the channel engagement layer alongside the structural mention/reply/dm triggers. **Live-reloadable.**
21
- - `channels` — per-adapter allow rules and engagement triggers that gate which external messenger channels (today: Discord) you can read from and post to. **Live-reloadable** — edits take effect on the next `reload` without a container restart.
22
- - `dockerfile` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated apt packages (`tmux`, `gh`, `python` default `true`; `ffmpeg` defaults `false`) — set the toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python` is boolean-only). (2) **`append`** — extra Dockerfile lines spliced in right before `ENTRYPOINT` for anything the toggles don't cover. The whole Dockerfile is rewritten on every `start` from the typeclaw template. **Restart-required** (next `typeclaw start` rebuilds the image).
23
- - `gitignore.append` — extra `.gitignore` patterns `typeclaw start` splices into the TypeClaw-owned `.gitignore` before the protected TypeClaw rules. The whole `.gitignore` is rewritten and auto-committed on every `start` when it changes; `append` is the supported escape hatch for local ignore patterns without editing the managed file by hand. **Restart-required** (next `typeclaw start` refreshes and commits `.gitignore`).
21
+ - `channels` — per-adapter engagement triggers and history-prefetch knobs for external messengers (Discord, Slack, Telegram, KakaoTalk). Access control lives in `roles`, not here. **Live-reloadable** — edits take effect on the next `reload` without a container restart.
22
+ - `docker.file` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated apt packages (`tmux`, `gh`, `python` default `true`; `ffmpeg` defaults `false`) — set the toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python` is boolean-only). (2) **`append`** — extra Dockerfile lines spliced in right before `ENTRYPOINT` for anything the toggles don't cover. The whole Dockerfile is rewritten on every `start` from the typeclaw template. Lives under the `docker` namespace alongside future Docker-related blocks (e.g. `docker.compose`). **Restart-required** (next `typeclaw start` rebuilds the image).
23
+ - `git.ignore.append` — extra `.gitignore` patterns `typeclaw start` splices into the TypeClaw-owned `.gitignore` before the protected TypeClaw rules. The whole `.gitignore` is rewritten and auto-committed on every `start` when it changes; `append` is the supported escape hatch for local ignore patterns without editing the managed file by hand. Lives under the `git` namespace. **Restart-required** (next `typeclaw start` refreshes and commits `.gitignore`).
24
24
  - `portForward` — allow/deny policy for the auto port-forwarder (the host-stage `_hostd` daemon's portbroker). When the agent runs a server inside the container that LISTENs on a TCP port, the broker proxies it to the same port number on `127.0.0.1` of the host so the user can hit it directly. `portForward` decides which ports are allowed through. **Restart-required** — the broker captures the policy at register time on `typeclaw start`.
25
25
 
26
26
  ### Reload vs. restart
@@ -28,7 +28,7 @@ The runtime reads `typeclaw.json` at container startup. Some fields are picked u
28
28
  There is no file watcher, but there is a `reload` mechanism. When `typeclaw.json` changes:
29
29
 
30
30
  - **Live-reloadable fields** (`model`, `alias`, `channels`) take effect on the next `reload` — no container restart.
31
- - **Restart-required fields** (`port`, `mounts`, `plugins`, `portForward`, `dockerfile`, `gitignore`) are reported as "reload landed but change won't apply until restart". The diff returns success; the runtime still has the old value in memory. Tell the user explicitly which one they're hitting. `dockerfile` additionally requires an image rebuild — that happens automatically on the next `typeclaw start`, no extra flag needed. `gitignore` refreshes the managed `.gitignore` and auto-commits it on the next `typeclaw start` if content changed.
31
+ - **Restart-required fields** (`port`, `mounts`, `plugins`, `portForward`, `docker.file`, `git.ignore`) are reported as "reload landed but change won't apply until restart". The diff returns success; the runtime still has the old value in memory. Tell the user explicitly which one they're hitting. `docker.file` additionally requires an image rebuild — that happens automatically on the next `typeclaw start`, no extra flag needed. `git.ignore` refreshes the managed `.gitignore` and auto-commits it on the next `typeclaw start` if content changed.
32
32
  - **`$schema`** changes are ignored.
33
33
 
34
34
  When you edit `typeclaw.json`, name the effect: "Edited `channels` — live-reloadable, takes effect on the next `reload`." vs. "Edited `port` — restart-required, run `typeclaw restart` (host stage) to pick up the change." Conflating the two misleads the user into restarting unnecessarily, or worse, into believing a restart-required edit took effect when it did not.
@@ -39,22 +39,22 @@ You yourself cannot run `typeclaw restart` — that is a host-stage command and
39
39
 
40
40
  `typeclaw.json` is a single JSON object with these fields:
41
41
 
42
- | Field | Required | Type | Notes |
43
- | ------------- | -------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
- | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
- | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
- | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
- | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
- | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
- | `channels` | no | object | Per-adapter allow rules and engagement triggers for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds a `discord-bot` block only if the user said yes to "Wire a Discord bot?" during the wizard and supplied a token. **Live-reloadable.** See **Channels** section below. |
51
- | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
- | `dockerfile` | no | object | Customizations for the autogenerated container image build. Toggles (`tmux`, `gh`, `python`, `ffmpeg`) gate opinionated apt packages; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. Defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
- | `gitignore` | no | object | Customizations for the autogenerated `.gitignore`. Today the only field is `append` — extra patterns spliced in before TypeClaw's protected ignore rules. Defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
42
+ | Field | Required | Type | Notes |
43
+ | ------------- | -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
+ | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
+ | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
+ | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
+ | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
+ | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
+ | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` see the `typeclaw-permissions` skill. **Live-reloadable.** See **Channels** section below. |
51
+ | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
+ | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` toggles (`tmux`, `gh`, `python`, `ffmpeg`) gate opinionated apt packages; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
+ | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
54
54
 
55
55
  > **Top-level keys not in this table are not "ignored unknowns" anymore** — they are reserved for **plugin config blocks**. The schema's `catchall(z.unknown())` preserves them, and the plugin loader hands each block to its owning plugin's `configSchema` for validation. The bundled memory plugin owns `memory` at the top level — see the `typeclaw-memory` skill for that block's semantics. Do not write a top-level key unless you know which plugin owns it.
56
56
 
57
- Within the well-known ten (`$schema`, `port`, `model`, `mounts`, `plugins`, `alias`, `channels`, `portForward`, `dockerfile`, `gitignore`), **fields the schema doesn't predeclare are silently dropped**. Do not invent runtime fields like `provider`, `apiKey`, `temperature`, `maxTokens`, `systemPrompt`, `tools`, `timeout`, etc. — those are not plugin blocks, they are imaginary. If the user asks for one, say it is not yet supported and (if it makes sense) suggest they file a request.
57
+ Within the well-known ten (`$schema`, `port`, `models`, `mounts`, `plugins`, `alias`, `channels`, `portForward`, `docker`, `git`), **fields the schema doesn't predeclare are silently dropped**. Legacy top-level `dockerfile` and `gitignore` keys are migrated to `docker.file` / `git.ignore` automatically the first time the CLI loads the file — see **Legacy migration** below. Do not invent runtime fields like `provider`, `apiKey`, `temperature`, `maxTokens`, `systemPrompt`, `tools`, `timeout`, etc. — those are not plugin blocks, they are imaginary. If the user asks for one, say it is not yet supported and (if it makes sense) suggest they file a request.
58
58
 
59
59
  A scaffolded `typeclaw.json` looks like:
60
60
 
@@ -65,17 +65,17 @@ A scaffolded `typeclaw.json` looks like:
65
65
  }
66
66
  ```
67
67
 
68
- The runtime fills in defaults for any omitted field: `port` → `8973`, `mounts` → `[]` (no host paths exposed), `plugins` → `[]`, `channels` → `{}` (no adapters configured), `portForward` → `{ "allow": "*" }` (forward every container LISTEN port), `dockerfile` → `{ "ffmpeg": false, "gh": true, "python": true, "tmux": true, "append": [] }` (tmux/gh/python pre-installed, ffmpeg off, no custom build steps), `gitignore` → `{ "append": [] }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `dockerfile`, and `gitignore` default via `configSchema`, and the bundled memory plugin owns its own `memory` defaults — so the scaffolded file stays minimal and the user sees only fields they actually need to think about. Add a `memory` block (a **plugin config block** owned by the bundled memory plugin) only when overriding its defaults; see the `typeclaw-memory` skill for the schema.
68
+ The runtime fills in defaults for any omitted field: `port` → `8973`, `mounts` → `[]` (no host paths exposed), `plugins` → `[]`, `channels` → `{}` (no adapters configured), `portForward` → `{ "allow": "*" }` (forward every container LISTEN port), `docker` → `{ "file": { "ffmpeg": false, "gh": true, "python": true, "tmux": true, "append": [] } }` (tmux/gh/python pre-installed, ffmpeg off, no custom build steps), `git` → `{ "ignore": { "append": [] } }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `docker`, and `git` default via `configSchema`, and the bundled memory plugin owns its own `memory` defaults — so the scaffolded file stays minimal and the user sees only fields they actually need to think about. Add a `memory` block (a **plugin config block** owned by the bundled memory plugin) only when overriding its defaults; see the `typeclaw-memory` skill for the schema.
69
69
 
70
70
  If the user said yes to "Wire a Discord bot?" during `typeclaw init`, the scaffold also includes:
71
71
 
72
72
  ```json
73
73
  "channels": {
74
- "discord-bot": { "allow": ["*"] }
74
+ "discord-bot": {}
75
75
  }
76
76
  ```
77
77
 
78
- `allow: ["*"]` means "every guild channel and every DM"appropriate for a single-user dev setup. The wizard asks the user to confirm `["*"]` and warns them when they decline, but the current scaffold writes `["*"]` either way and expects the user to narrow it by hand afterwards. **If the user said they wanted a narrower allow list during init, do not assume the scaffold honored that read `typeclaw.json` first.** For shared servers, narrow the allow list before joining (see **Channels** below).
78
+ The empty block declares the adapter to the runtimethe schema fills in `enabled: true`, default engagement triggers, and history prefetch. **Access control is separate**: by default an adapter with no `roles` block matching the speaker resolves to `guest` and the router drops every inbound. To let conversations through, declare a `roles` block (see the `typeclaw-permissions` skill). For example, `"roles": { "member": { "match": ["discord:<guild>"] } }` lets every member of that guild reach the agent on the `channel.respond` permission.
79
79
 
80
80
  ## Mounts
81
81
 
@@ -128,39 +128,21 @@ The `mounts/` directory itself is **gitignored** in your agent folder. The mount
128
128
 
129
129
  ## Channels
130
130
 
131
- `channels` configures which external messenger channels you can read from and post to. **Today the only adapter is `discord-bot`.** The shape is `channels: { "<adapter-id>": { allow, engagement, enabled } }`.
131
+ `channels` configures which external messenger adapters are enabled and how the engagement layer should behave on each. **Access control lives in `roles`, not here** — to admit a chat, declare a role match-rule that covers it (see `typeclaw-permissions`). The shape is `channels: { "<adapter-id>": { engagement, history, enabled } }`. Today the adapters are `discord-bot`, `slack-bot`, `telegram-bot`, and `kakaotalk`.
132
132
 
133
- The channels block is **live-reloadable** — edits take effect on the next `reload`, no container restart. This is intentional: allow-rule changes need to feel immediate, otherwise the user has to restart you every time they want to add a channel.
133
+ The channels block is **live-reloadable** — edits take effect on the next `reload`, no container restart.
134
134
 
135
135
  ### Adapter block
136
136
 
137
137
  Each entry in `channels` is keyed by adapter id and has this shape:
138
138
 
139
- | Field | Required | Type | Notes |
140
- | ------------ | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
141
- | `allow` | no | array of strings | List of allow rules. Defaults to `[]`, which means **the agent cannot post anywhere and the inbound bridge gates every message**. See **Allow rules** below. |
142
- | `engagement` | no | object | When the agent should auto-reply vs. stay silent. Defaults to mention/reply/dm with 5-minute reply stickiness. See **Engagement** below. |
143
- | `enabled` | no | boolean | Defaults to `true`. Set `false` to disable the adapter entirely without removing its config. |
139
+ | Field | Required | Type | Notes |
140
+ | ------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `engagement` | no | object | When the agent should auto-reply vs. stay silent. Defaults to mention/reply/dm with 5-minute reply stickiness. See **Engagement** below. |
142
+ | `history` | no | object | Cold-start prefetch windows for `(thread.head, thread.tail, channel.tail)`. Set any to `0` to disable that side. Defaults to `{ thread: { head: 3, tail: 10 }, channel: { tail: 10 } }`. |
143
+ | `enabled` | no | boolean | Defaults to `true`. Set `false` to disable the adapter entirely without removing its config. |
144
144
 
145
- `allow: []` is **not** the same as "deny everything from being delivered to the agent" the inbound side may still hand you events for processing depending on engagement, but the `channel_send` tool will refuse to post anywhere. Conversely, `allow: ["*"]` does not turn engagement off; engagement and allow are independent gates.
146
-
147
- ### Allow rules
148
-
149
- Each entry in `allow` is a string matching one of the patterns below. Workspace `@dm` is the literal placeholder for direct messages (Discord doesn't expose a guild id for DMs).
150
-
151
- | Rule | Matches |
152
- | ---------------------- | ---------------------------------------------------------------------- |
153
- | `*` | Every guild channel **and** every DM (full firehose). |
154
- | `guild:*` | Every channel in every guild. Does **not** include DMs. |
155
- | `guild:<id>` | Every channel in guild `<id>`. |
156
- | `guild:<id>/<channel>` | Channel `<channel>` in guild `<id>` only. |
157
- | `channel:<id>` | Channel `<id>` in any guild (Discord channel IDs are globally unique). |
158
- | `dm:*` | Every DM channel. Does **not** include guild channels. |
159
- | `dm:<id>` | DM channel `<id>` only. |
160
-
161
- The schema validates each rule string at load. **Bad rule = config load fails**, the runtime refuses to boot or refuses the reload. Don't fudge this; the regex is strict (numeric IDs only).
162
-
163
- `channel:<id>` is the most surgical option and the right default when the user says "let me talk to you in #the-channel-i'm-pointing-at" — channel IDs are globally unique so you don't need the guild id. `guild:<id>` is convenient on a server you fully trust. `*` and `guild:*` are firehose patterns; only set them in single-user setups.
145
+ To stop the agent answering in a specific channel, narrow the `roles` block so the speaking author's role no longer carries `channel.respond` engagement triggers gate wake-up _given_ the message is admitted; `channel.respond` gates whether the message is admitted at all.
164
146
 
165
147
  ### Engagement
166
148
 
@@ -181,59 +163,51 @@ The schema validates each rule string at load. **Bad rule = config load fails**,
181
163
  - `perReply` means: after the agent replies to a user, follow-up messages from that same user in that same channel within the window also wake the loop, even without a mention. The window is bounded server-side (`1` to `86_400_000` ms — 1 ms to 24 hours).
182
164
  - `"off"` disables stickiness — the agent only wakes on explicit triggers.
183
165
 
184
- There is also a **solo-human fallback** built into the runtime that is **not configurable** through `engagement`: in any allow-listed channel where the participants cache currently holds at most one distinct human author, every admitted inbound wakes the loop, regardless of `trigger` or `stickiness`. The fallback turns off the moment a second distinct human posts in that channel. This makes "private dev channel with one human and the bot" work without forcing an `@mention` on every message; clearing `trigger` to `[]` does **not** override it. If a user wants strict mention-only behavior in a one-human channel today, the answer is "wait for the post-v0.3 engagement-config work" — not a config edit.
166
+ There is also a **solo-human fallback** built into the runtime that is **not configurable** through `engagement`: in any channel where the participants cache currently holds at most one distinct human author, every admitted inbound wakes the loop, regardless of `trigger` or `stickiness`. The fallback turns off the moment a second distinct human posts in that channel. This makes "private dev channel with one human and the bot" work without forcing an `@mention` on every message; clearing `trigger` to `[]` does **not** override it.
185
167
 
186
- **Engagement does not gate posting.** It gates whether you wake up. If you decide to call `channel_send` to a channel that isn't in `allow`, the tool returns `{ ok: false, error }` regardless of engagement.
187
-
188
- To make the agent silent in a channel without removing it from `allow`, the right move is usually `engagement: { trigger: [], stickiness: "off" }`, **not** removing the allow rule — removing the allow rule cuts off both inbound visibility and outbound posting, which is rarely what the user means by "stop replying". Caveat: in a one-human channel the solo-human fallback overrides `trigger: []` and the agent will still wake; the only way to silence the bot in that case is to remove the allow rule (or add a second human to the channel).
168
+ **Engagement does not gate access.** Access is gated by `permissions.has(origin, 'channel.respond')` see the `typeclaw-permissions` skill. Engagement decides whether an _admitted_ inbound wakes the loop or sits in the context buffer.
189
169
 
190
170
  ### Example
191
171
 
192
172
  ```json
193
173
  "channels": {
194
174
  "discord-bot": {
195
- "allow": [
196
- "guild:123456789012345678/987654321098765432",
197
- "dm:*"
198
- ],
199
175
  "engagement": {
200
176
  "trigger": ["mention", "reply", "dm"],
201
177
  "stickiness": { "perReply": { "window": 300000 } }
202
178
  },
203
179
  "enabled": true
204
180
  }
181
+ },
182
+ "roles": {
183
+ "member": { "match": ["discord:123456789012345678/987654321098765432", "discord:dm/*"] }
205
184
  }
206
185
  ```
207
186
 
208
- This says: only one specific channel in one specific guild plus all DMs are visible/postable; auto-reply on mentions, replies, or DMs; sticky for 5 minutes after replying to a user.
187
+ This says: the `discord-bot` adapter is enabled with default engagement; one specific channel in one specific guild plus all DMs admit speakers as `member` (which carries `channel.respond` by default).
209
188
 
210
189
  ### When the user asks "let me talk to you in this channel"
211
190
 
212
- 1. **Read `typeclaw.json`.**
213
- 2. **Identify the adapter.** Today this is always `discord-bot`. If `channels["discord-bot"]` is missing, create it: `{ "allow": [], "engagement": { ... defaults ... }, "enabled": true }`.
214
- 3. **Get the channel ID from the user.** Discord channel IDs are 17–20 digit numbers. If they only gave a `#channel-name`, ask for the ID names aren't unique and the schema needs IDs.
215
- 4. **Pick the rule shape:**
216
- - DM: `dm:<channelId>` (or `dm:*` if they want all DMs).
217
- - Guild channel they fully trust: `guild:<guildId>/<channelId>` is most explicit; `channel:<channelId>` is shorter and equivalent.
218
- - Whole guild: `guild:<guildId>` — confirm explicitly that they want every channel.
219
- 5. **Append to `allow`.** Preserve existing entries.
220
- 6. **Write the file back** (pretty-printed, 2-space indent, trailing newline).
221
- 7. **Commit** with a message naming the rule and why (`typeclaw-git` skill).
222
- 8. **Tell the user the effect:** "Added `<rule>` to `channels.discord-bot.allow`. This is live-reloadable — it takes effect on the next `reload`, no restart needed."
191
+ This is a **`roles`** edit, not a `channels` edit. See the `typeclaw-permissions` skill for the full procedure. Short version:
192
+
193
+ 1. Get the platform ID (Discord channel ID, Slack channel ID, Telegram chat ID, KakaoTalk chat ID).
194
+ 2. Append a match-rule to `roles.member.match` using the canonical DSL (`discord:<guild>/<channel>`, `slack:<team>/<channel>`, `telegram:<chat>`, `kakao:<chat>`).
195
+ 3. **`roles` is restart-required** `typeclaw reload` won't apply it; the user needs `typeclaw restart`.
223
196
 
224
197
  ### When the user asks "stop replying in this channel"
225
198
 
226
199
  Two interpretations — ask if unclear:
227
200
 
228
- - **"Stop everything"** — remove the matching allow rule. The agent loses both inbound visibility and outbound `channel_send` permission for that channel.
229
- - **"Just stop auto-replying"** — leave the allow rule, but adjust `engagement` (set `trigger: []` and/or `stickiness: "off"`). The agent can still see the channel and can still post if you tell it to. Caveat: this approach does NOT silence the agent in a channel that currently has only one human posting — the solo-human fallback (see Engagement) overrides `trigger: []`. In that case the only way to go silent today is to remove the allow rule.
201
+ - **"Stop everything"** — remove the match-rule from `roles.<role>.match`. The agent loses both inbound visibility and outbound posting on that channel.
202
+ - **"Just stop auto-replying"** — leave the match-rule, but adjust `engagement` on the adapter (set `trigger: []` and/or `stickiness: "off"`). The agent can still receive the channel and can still post if you tell it to. Caveat: this approach does NOT silence the agent in a channel that currently has only one human posting — the solo-human fallback (see Engagement) overrides `trigger: []`. In that case the only way to go silent today is to remove the match-rule.
230
203
 
231
204
  The second is usually what people mean by "be quieter".
232
205
 
233
206
  ### When the user asks "what channels can you see / are you in"
234
207
 
235
- 1. **Read `typeclaw.json`**, list each adapter under `channels`: which is enabled, the full `allow` list, the engagement triggers and stickiness window.
236
- 2. Note that the live runtime may have a different view if `typeclaw.json` was edited but `reload` hasn't run yet say so when relevant.
208
+ 1. **Read `typeclaw.json`**, list each adapter under `channels`: which is enabled, the engagement triggers and stickiness window.
209
+ 2. Also read `roles.<role>.match` for every role those are the actual admit lists.
210
+ 3. Note that the live runtime may have a different view if `typeclaw.json` was edited but `reload` hasn't run yet — say so when relevant.
237
211
 
238
212
  ## Alias
239
213
 
@@ -361,10 +335,10 @@ Off switch — the broker is constructed but never opens a WS, no LISTEN gets fo
361
335
 
362
336
  `typeclaw start` rewrites the agent folder's `Dockerfile` from a template baked into the typeclaw CLI on **every** invocation — not just on `init`. The Dockerfile is in the truly-ignored `.gitignore` category specifically because it's regenerated; the source of truth for the template is `src/init/dockerfile.ts` in the typeclaw repo, not the agent folder. This means: editing the Dockerfile by hand inside the agent folder is pointless (the next `typeclaw start` overwrites it), and a clean clone of an agent folder onto a fresh machine works only because `start` materializes the Dockerfile before `docker build` reads it.
363
337
 
364
- The `dockerfile` block has two layers of customization:
338
+ The `docker.file` block has two layers of customization:
365
339
 
366
340
  1. **Toggles** for opinionated apt packages typeclaw knows how to install with proper layer caching (`tmux`, `gh`, `python`, `ffmpeg`). Boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`). Use these whenever they cover what the user wants — they get BuildKit cache-mount benefits and, for `gh`, automatic keyring layer gating.
367
- 2. **`append`** is the escape hatch for everything the toggles don't cover. An array of single-line Dockerfile instructions spliced in right before `ENTRYPOINT`, prefixed with a `# Custom lines from typeclaw.json#dockerfile.append.` comment.
341
+ 2. **`append`** is the escape hatch for everything the toggles don't cover. An array of single-line Dockerfile instructions spliced in right before `ENTRYPOINT`, prefixed with a `# Custom lines from typeclaw.json#docker.file.append.` comment.
368
342
 
369
343
  ### Fields
370
344
 
@@ -383,11 +357,13 @@ Toggle version strings reject whitespace and `=` (apt-injection guard) — pass
383
357
  Each entry of `append` must be one Dockerfile instruction's worth of source — a `RUN`, `ENV`, `COPY`, `ARG`, etc. The schema enforces "no embedded newlines" because a multiline string in the JSON would silently break Dockerfile syntax (Dockerfile line continuations require backslashes at end-of-line, and a JSON multiline doesn't carry those). If the user wants a logically multi-step instruction, give them two entries:
384
358
 
385
359
  ```json
386
- "dockerfile": {
387
- "append": [
388
- "RUN apt-get update && apt-get install -y --no-install-recommends ripgrep fd-find",
389
- "ENV CUSTOM_TOOL=1"
390
- ]
360
+ "docker": {
361
+ "file": {
362
+ "append": [
363
+ "RUN apt-get update && apt-get install -y --no-install-recommends ripgrep fd-find",
364
+ "ENV CUSTOM_TOOL=1"
365
+ ]
366
+ }
391
367
  }
392
368
  ```
393
369
 
@@ -401,7 +377,7 @@ The template's last layers are roughly:
401
377
  RUN apt-get install ... <baseline + enabled toggle packages> ← toggles fan out into this line
402
378
  ...
403
379
  ENV NODE_ENV=production
404
- # Custom lines from typeclaw.json#dockerfile.append. ← only emitted when append is non-empty
380
+ # Custom lines from typeclaw.json#docker.file.append. ← only emitted when append is non-empty
405
381
  <your appended lines>
406
382
  ENTRYPOINT ["/usr/local/bin/typeclaw-entrypoint"]
407
383
  CMD ["run"]
@@ -413,23 +389,23 @@ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/v
413
389
 
414
390
  ### Restart and rebuild semantics
415
391
 
416
- - **Restart-required.** `dockerfile` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired` and the live container keeps running on the old image.
417
- - **The next `typeclaw start` rebuilds the image automatically.** No `--build` flag is needed; the CLI re-runs `docker build` whenever the Dockerfile content has changed (it rewrites the file from the current template + current `dockerfile` block every start). Tell the user: "Edited `dockerfile` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image."
392
+ - **Restart-required.** `docker.file` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired` and the live container keeps running on the old image.
393
+ - **The next `typeclaw start` rebuilds the image automatically.** No `--build` flag is needed; the CLI re-runs `docker build` whenever the Dockerfile content has changed (it rewrites the file from the current template + current `docker.file` block every start). Tell the user: "Edited `docker.file` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image."
418
394
  - **Pre-existing host-side edits to the Dockerfile are clobbered.** If the user manually edited the Dockerfile before, the next `start` overwrites it and (if the working tree was dirty) auto-commits the cleanup. This is by design; don't try to preserve manual edits.
419
395
 
420
396
  ### When the user asks "install <package> in the container" / "add a Dockerfile line"
421
397
 
422
398
  1. **Read `typeclaw.json`.**
423
- 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, or `ffmpeg`, prefer the toggle: `"dockerfile": { "ffmpeg": true }`. For a pinned version, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`.
399
+ 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, or `ffmpeg`, prefer the toggle: `"docker": { "file": { "ffmpeg": true } }`. For a pinned version, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`.
424
400
  3. **Otherwise, use `append`.** Decide on a single-line entry — for apt installs, prefer one `RUN apt-get update && apt-get install -y --no-install-recommends <pkg> && rm -rf /var/lib/apt/lists/*` line. For env vars, one `ENV` line per variable.
425
401
  4. **Validate no embedded newlines** (`append` only). Multi-step logic must be `&&`-chained on one line, not split across array entries unless those entries are independent Dockerfile instructions.
426
- 5. **Append to `dockerfile.append`** (creating the field if it doesn't exist). Preserve existing entries.
427
- 6. **Write, commit, restart-required**: "Edited `dockerfile` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image. The new layer will be at the end of the build, so unrelated cache layers stay valid."
402
+ 5. **Append to `docker.file.append`** (creating the field if it doesn't exist). Preserve existing entries.
403
+ 6. **Write, commit, restart-required**: "Edited `docker.file` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image. The new layer will be at the end of the build, so unrelated cache layers stay valid."
428
404
 
429
405
  ### When the user asks "uninstall <package>" / "make the image smaller"
430
406
 
431
407
  1. **Read `typeclaw.json`.**
432
- 2. **If the package is one of the toggles**, set it to `false`: `"dockerfile": { "tmux": false }`. Don't try to remove it via `append` — the toggle is the only way to omit a baseline package from the apt install line.
408
+ 2. **If the package is one of the toggles**, set it to `false`: `"docker": { "file": { "tmux": false } }`. Don't try to remove it via `append` — the toggle is the only way to omit a baseline package from the apt install line.
433
409
  3. **If it's an `append` entry**, remove that entry from the array.
434
410
  4. **Write, commit, restart-required.** Same rebuild story.
435
411
 
@@ -442,14 +418,14 @@ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/v
442
418
  ### When the user asks "remove that custom Dockerfile line"
443
419
 
444
420
  1. **Read `typeclaw.json`.**
445
- 2. **Remove the entry from `dockerfile.append`.** If the resulting array is empty AND no toggles are overridden, you may either leave it as `"append": []` or drop the whole `dockerfile` block — both are equivalent. Dropping it keeps the file minimal and matches the scaffold convention.
421
+ 2. **Remove the entry from `docker.file.append`.** If the resulting array is empty AND no toggles are overridden, you may either leave it as `"append": []` or drop the whole `docker` block — both are equivalent. Dropping it keeps the file minimal and matches the scaffold convention.
446
422
  3. **Write, commit, restart-required.** Same restart story as adding: next `typeclaw start` rebuilds.
447
423
 
448
424
  ## Gitignore
449
425
 
450
426
  `typeclaw start` rewrites the agent folder's `.gitignore` from a template baked into the typeclaw CLI on **every** invocation, then auto-commits it when the agent folder is a git repo and the file changed. The template protects two categories: truly-ignored paths (`.env`, `node_modules/`, `workspace/`, `mounts/`, `Dockerfile`, `.DS_Store`) and system-managed runtime state (`sessions/`, `memory/`, `channels/`) that TypeClaw, not the agent, commits on its own schedule. Editing `.gitignore` by hand is temporary; the next `typeclaw start` overwrites it.
451
427
 
452
- The `gitignore.append` field is the supported escape hatch for additional local ignore patterns. It is an array of strings, each treated as a single `.gitignore` line. The CLI splices them into the autogenerated `.gitignore` before TypeClaw's protected rules, prefixed with a `# Custom entries from typeclaw.json#gitignore.append.` comment.
428
+ The `git.ignore.append` field (introduced when the legacy top-level `gitignore` key was nested under the `git` namespace for future extensibility — see **Legacy migration**) is the supported escape hatch for additional local ignore patterns. It is an array of strings, each treated as a single `.gitignore` line. The CLI splices them into the autogenerated `.gitignore` before TypeClaw's protected rules, prefixed with a `# Custom entries from typeclaw.json#git.ignore.append.` comment.
453
429
 
454
430
  ### Field
455
431
 
@@ -459,12 +435,12 @@ The `gitignore.append` field is the supported escape hatch for additional local
459
435
 
460
436
  ### Ordering and protected paths
461
437
 
462
- `.gitignore` is order-sensitive: later `!` negation rules can unignore earlier ignore rules. TypeClaw therefore renders `gitignore.append` **before** its own truly-ignored and system-managed entries, so even a custom `!sessions/` or `!.env` cannot override TypeClaw's protections. Custom ordinary ignore patterns still work because they add additional ignores; they just do not get the final word over TypeClaw-owned paths.
438
+ `.gitignore` is order-sensitive: later `!` negation rules can unignore earlier ignore rules. TypeClaw therefore renders `git.ignore.append` **before** its own truly-ignored and system-managed entries, so even a custom `!sessions/` or `!.env` cannot override TypeClaw's protections. Custom ordinary ignore patterns still work because they add additional ignores; they just do not get the final word over TypeClaw-owned paths.
463
439
 
464
440
  Materialized shape when `append` is non-empty:
465
441
 
466
442
  ```gitignore
467
- # Custom entries from typeclaw.json#gitignore.append.
443
+ # Custom entries from typeclaw.json#git.ignore.append.
468
444
  scratch/
469
445
  *.local.log
470
446
 
@@ -480,26 +456,42 @@ channels/
480
456
 
481
457
  ### Restart semantics
482
458
 
483
- - **Restart-required.** `gitignore` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired`; the already-materialized `.gitignore` on disk remains unchanged until the next host-stage start.
484
- - **The next `typeclaw start` refreshes and auto-commits `.gitignore`.** Tell the user: "Edited `gitignore.append` — restart-required. The next `typeclaw start` will rewrite `.gitignore` and TypeClaw will auto-commit it if the file changes."
459
+ - **Restart-required.** `git.ignore` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired`; the already-materialized `.gitignore` on disk remains unchanged until the next host-stage start.
460
+ - **The next `typeclaw start` refreshes and auto-commits `.gitignore`.** Tell the user: "Edited `git.ignore.append` — restart-required. The next `typeclaw start` will rewrite `.gitignore` and TypeClaw will auto-commit it if the file changes."
485
461
 
486
462
  ### When the user asks "ignore this path" / "add a gitignore entry"
487
463
 
488
464
  1. **Read `typeclaw.json`.**
489
465
  2. **Decide on single-line patterns.** Use one array entry per `.gitignore` pattern. Do not embed newlines.
490
- 3. **Append to `gitignore.append`** (creating the field if it doesn't exist). Preserve existing entries.
466
+ 3. **Append to `git.ignore.append`** (creating the field if it doesn't exist). Preserve existing entries.
491
467
  4. **Do not edit `.gitignore` directly.** It is managed and will be overwritten on `typeclaw start`.
492
- 5. **Write, commit, restart-required**: "Edited `gitignore.append` — restart-required. The next `typeclaw start` will rewrite and auto-commit `.gitignore`."
468
+ 5. **Write, commit, restart-required**: "Edited `git.ignore.append` — restart-required. The next `typeclaw start` will rewrite and auto-commit `.gitignore`."
493
469
 
494
470
  ### When the user asks "remove that custom ignore entry"
495
471
 
496
472
  1. **Read `typeclaw.json`.**
497
- 2. **Remove the entry from `gitignore.append`.** If the resulting array is empty, you may either leave it as `"append": []` or drop the whole `gitignore` block — both are equivalent. Dropping it keeps the file minimal and matches the scaffold convention.
473
+ 2. **Remove the entry from `git.ignore.append`.** If the resulting array is empty, you may either leave it as `"append": []` or drop the whole `git` block — both are equivalent. Dropping it keeps the file minimal and matches the scaffold convention.
498
474
  3. **Write, commit, restart-required.** Same refresh story as adding: next `typeclaw start` rewrites and auto-commits `.gitignore` if content changed.
499
475
 
476
+ ## Legacy migration
477
+
478
+ Pre-namespace `typeclaw.json` files carried `dockerfile` and `gitignore` as top-level keys. The current schema nests them under `docker.file` and `git.ignore` so the `docker` and `git` namespaces stay free for future siblings (`docker.compose`, `git.attributes`, etc.) without a second rename.
479
+
480
+ The first time `validateConfig` or `loadConfigSync` reads a legacy file:
481
+
482
+ 1. The in-memory JSON is rewritten: top-level `dockerfile` → `docker.file`, top-level `gitignore` → `git.ignore`.
483
+ 2. The same rewrite is persisted back to `typeclaw.json` on disk (pretty-printed, trailing newline). Best-effort: a read-only filesystem just retries next start.
484
+ 3. If the file already has a `docker` or `git` block AND the legacy key, the new shape wins — the legacy duplicate is dropped silently. The new shape would have shadowed the legacy at parse time anyway.
485
+
486
+ What this means for you:
487
+
488
+ - **Do not write top-level `dockerfile` or `gitignore` keys** when editing `typeclaw.json`. They'll be migrated away on the next CLI invocation; meanwhile the file is briefly in an inconsistent shape.
489
+ - **Old documentation or examples that still mention `typeclaw.json#dockerfile.append` are stale.** The current path is `typeclaw.json#docker.file.append`. Same for `git.ignore.append`.
490
+ - **An auto-commit may appear** the next time `typeclaw start` runs against a freshly-migrated agent folder. The diff is mechanical (top-level rename → nested) — surface it to the user as a one-time migration, not a behavior change.
491
+
500
492
  ## Plugin config blocks
501
493
 
502
- Top-level keys in `typeclaw.json` that are **not** in the well-known ten (`$schema`, `port`, `model`, `mounts`, `plugins`, `alias`, `channels`, `portForward`, `dockerfile`, `gitignore`) are treated as plugin config blocks. The schema preserves them via `catchall(z.unknown())`, and `extractPluginConfigs` hands each block to the owning plugin's `configSchema` for validation at boot.
494
+ Top-level keys in `typeclaw.json` that are **not** in the well-known ten (`$schema`, `port`, `models`, `mounts`, `plugins`, `alias`, `channels`, `portForward`, `docker`, `git`) are treated as plugin config blocks. The schema preserves them via `catchall(z.unknown())`, and `extractPluginConfigs` hands each block to the owning plugin's `configSchema` for validation at boot.
503
495
 
504
496
  This skill does **not** document individual plugin blocks. For schema, defaults, and reload semantics of a specific plugin's config, defer to that plugin's own skill:
505
497
 
@@ -598,14 +590,14 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
598
590
 
599
591
  1. **Read the whole file first** with the `read` tool. Don't assume what's in it — the user may have customized it.
600
592
  2. **Modify in memory.** Change only the field(s) the user asked about. Leave `$schema` alone.
601
- 3. **Write the whole file back** with the `write` tool. Always pretty-printed (2-space indent), trailing newline, fields in stable order: `$schema` first, then alphabetical for the rest (`alias`, `channels`, `dockerfile`, `gitignore`, `model`, `mounts`, `plugins`, `port`, `portForward`, then any plugin config blocks like `memory`).
593
+ 3. **Write the whole file back** with the `write` tool. Always pretty-printed (2-space indent), trailing newline, fields in stable order: `$schema` first, then alphabetical for the rest (`alias`, `channels`, `docker`, `git`, `model`, `mounts`, `plugins`, `port`, `portForward`, then any plugin config blocks like `memory`).
602
594
  4. **Validate before declaring done.** A malformed `typeclaw.json` will refuse to boot the agent on next restart, and a malformed reload-time edit will be rejected by `reload`. Sanity-check your JSON manually or with `bash` (`cat typeclaw.json | jq .`) before considering the edit done.
603
595
  5. **Commit the change.** See the `typeclaw-git` skill for the commit-message rule (decision context required). `typeclaw.json` is not gitignored, so an uncommitted edit will pollute your next commit.
604
596
  6. **Tell the user the right next step.** Match the field's effect class:
605
597
  - `model`, `alias`, `channels` → "Live-reloadable, takes effect on the next `reload`."
606
598
  - `port`, `mounts`, `plugins`, `portForward` → "Restart-required. Run `typeclaw restart` (host stage) to pick up the change."
607
- - `dockerfile` → "Restart-required, and the next `typeclaw start` will rebuild the image automatically (no `--build` flag needed)."
608
- - `gitignore` → "Restart-required, and the next `typeclaw start` will rewrite and auto-commit `.gitignore` if content changed."
599
+ - `docker.file` → "Restart-required, and the next `typeclaw start` will rebuild the image automatically (no `--build` flag needed)."
600
+ - `git.ignore` → "Restart-required, and the next `typeclaw start` will rewrite and auto-commit `.gitignore` if content changed."
609
601
  - Plugin config blocks (e.g. `memory`) → restart-required by convention because plugins read their config once at boot. Defer to the plugin's own skill for the exact semantics.
610
602
  - Mixed edits in one go → spell out which is which; do not collapse to "restart" if part of the change is live.
611
603
 
@@ -620,12 +612,12 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
620
612
  - If `model` is set: exactly one of the values in **Allowed models** above
621
613
  - If `plugins` is set: array of non-empty strings
622
614
  - If `alias` is set: array of strings, each non-empty after trimming surrounding whitespace
623
- - If `channels.discord-bot.allow` is set: every entry matches one of the **Allow rules** patterns above (numeric IDs only)
624
- - If `channels.discord-bot.engagement.trigger` is set: array of `"mention"`, `"reply"`, `"dm"` (any subset, including empty)
625
- - If `channels.discord-bot.engagement.stickiness` is set: either the literal `"off"` or `{ "perReply": { "window": <int 1..86400000> } }`
615
+ - If `channels.<adapter>.engagement.trigger` is set: array of `"mention"`, `"reply"`, `"dm"` (any subset, including empty)
616
+ - If `channels.<adapter>.engagement.stickiness` is set: either the literal `"off"` or `{ "perReply": { "window": <int 1..86400000> } }`
617
+ - `channels.<adapter>.allow` (legacy) is silently dropped on parse; `migrateLegacyConfigShape` lifts it into `roles.member.match` on load. See the `typeclaw-permissions` skill.
626
618
  - If `portForward` is set: `allow` is either `"*"` or an array of integers (1–65535); `deny`, if present, is an array of integers and **only valid when `allow` is `"*"`** (the schema rejects `deny` paired with a number-array `allow`)
627
- - If `dockerfile.append` is set: array of strings, each with no embedded `\n` or `\r` (multi-step shell logic goes in a single `&&`-chained `RUN` entry)
628
- - If any `dockerfile` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python` is boolean only
619
+ - If `docker.file.append` is set: array of strings, each with no embedded `\n` or `\r` (multi-step shell logic goes in a single `&&`-chained `RUN` entry)
620
+ - If any `docker.file` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python` is boolean only
629
621
  - No unknown top-level keys you invented — keys outside the well-known ten are interpreted as **plugin config blocks** and only do something if a plugin owns them. Inventing one means the user thinks it took effect and it did not.
630
622
 
631
623
  ## Things you must not do
@@ -639,14 +631,14 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
639
631
  - **Do not re-add `"mounts": []` "for clarity" if the user has none.** The scaffold deliberately omits it; defaults live in `configSchema`. Re-emitting it adds maintenance noise (the user has to keep two sources of truth in sync) without changing behavior.
640
632
  - **Do not promise to write to a `readOnly: true` mount.** Docker enforces it via `:ro`; writes will fail with EROFS. If the user wants you to edit a read-only mount, the fix is to flip `readOnly` to `false` in `typeclaw.json` and restart, not to retry the write.
641
633
  - **Do not invent mount entries the user did not request.** Mounts expose host paths to your container; adding them silently is a security surprise.
642
- - **Do not add channel allow rules the user did not request, especially `*` or `guild:*`.** Allow rules grant the agent visibility (and, for outbound, posting permission) on real Discord channels with real people in them. Widening the allow list silently is the same class of security surprise as adding a mount.
643
- - **Do not promise the user that an allow-rule edit took effect immediately just because you wrote the file.** Live-reloadable means "applied on the next `reload`", not "applied the instant the file changes". Until `reload` runs (or the container restarts), the runtime is still using the old `channels` config.
644
- - **Do not promise to post to a channel that isn't in `allow`.** `channel_send` will refuse with `{ ok: false, error }` regardless of what you tell the user. If they want you to post somewhere new, the prerequisite is an allow-rule edit, not a retry.
645
- - **Do not conflate "stop replying" with "remove allow rule".** Removing the allow rule cuts off both inbound visibility and outbound posting. If the user just wants quieter behavior, edit `engagement` instead.
646
- - **Do not edit the `Dockerfile` directly.** It is autogenerated and rewritten on every `typeclaw start` from `src/init/dockerfile.ts` in the typeclaw repo. Manual edits will be silently overwritten (and auto-committed away if the working tree is dirty). Customizations belong in the `dockerfile` block (toggles or `append`).
647
- - **Do not reach for `dockerfile.append` when a toggle covers it.** If the user wants tmux, gh, python, or ffmpeg installed (or removed, or pinned), use the toggle — it's the cache-mounted path. `append` for these is slower and harder to read.
648
- - **Do not use `dockerfile.append` for things that belong in the template.** If the user wants a system package _every_ typeclaw user should have, that's a typeclaw release, not a per-agent `append`. Suggest filing an issue.
649
- - **Do not put multiline strings in `dockerfile.append`.** The schema rejects entries with embedded `\n`/`\r`. Use one entry per Dockerfile instruction; chain shell logic with `&&` on one line.
634
+ - **Do not add `roles.<role>.match` entries the user did not request, especially `*` or platform-wildcards (`discord:*`, `slack:*`).** Match-rules grant the agent visibility (and, for outbound, posting permission) on real channels with real people in them. Widening them silently is the same class of security surprise as adding a mount. See the `typeclaw-permissions` skill.
635
+ - **Do not promise the user that a `roles` edit took effect immediately just because you wrote the file.** `roles` is **restart-required** `typeclaw reload` returns it under `restartRequired`; the live runtime keeps the old role table until `typeclaw restart`.
636
+ - **Do not promise to post to a channel the speaker's role does not cover.** The router drops every inbound where the speaking author resolves to a role without `channel.respond`. If the user wants you to post somewhere new, the prerequisite is a `roles` edit + restart, not a retry.
637
+ - **Do not conflate "stop replying" with "remove the role's match-rule".** Removing the match-rule cuts off both inbound visibility and outbound posting. If the user just wants quieter behavior, edit `engagement` instead.
638
+ - **Do not edit the `Dockerfile` directly.** It is autogenerated and rewritten on every `typeclaw start` from `src/init/dockerfile.ts` in the typeclaw repo. Manual edits will be silently overwritten (and auto-committed away if the working tree is dirty). Customizations belong in the `docker.file` block (toggles or `append`).
639
+ - **Do not reach for `docker.file.append` when a toggle covers it.** If the user wants tmux, gh, python, or ffmpeg installed (or removed, or pinned), use the toggle — it's the cache-mounted path. `append` for these is slower and harder to read.
640
+ - **Do not use `docker.file.append` for things that belong in the template.** If the user wants a system package _every_ typeclaw user should have, that's a typeclaw release, not a per-agent `append`. Suggest filing an issue.
641
+ - **Do not put multiline strings in `docker.file.append`.** The schema rejects entries with embedded `\n`/`\r`. Use one entry per Dockerfile instruction; chain shell logic with `&&` on one line.
650
642
  - **Do not pass `pkg=ver` as a toggle version string.** The schema rejects `=` in version strings. Pass just the version (`"gh": "2.40.0"`); the renderer prepends `pkg=` itself. Same for whitespace — version strings cannot contain spaces.
651
643
  - **Do not list `8973` (or whatever `port` is set to) in `portForward.allow`/`deny`.** That port is owned by `docker run -p`; the broker quietly excludes it regardless. Listing it is misleading.
652
644
  - **Do not combine `portForward.deny` with a number-array `allow`.** The schema rejects this; the deny rule would have no effect even if the schema allowed it. `deny` is only meaningful with `allow: "*"`.
@@ -678,5 +670,5 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
678
670
  - **Identity files** (`IDENTITY.md`, `SOUL.md`, `USER.md`, `AGENTS.md`) — these are not runtime config; they are _you_. Edit them directly when relevant; no skill needed.
679
671
  - **`MEMORY.md` and `memory/`** — explicit exception to the line above. `MEMORY.md` is **dreaming-owned** and you must not write to it directly; the `memory/` directory holds runtime-managed daily streams and muscle-memory skills. See the `typeclaw-memory` skill before touching anything memory-shaped.
680
672
  - **Skills directories** (`.agents/skills/`, `memory/skills/`, the bundled `src/skills/`) — these are loaded from disk by the runtime; they are not driven by `typeclaw.json`. See the `typeclaw-skills` skill for the three layers, the `bunx skills` CLI, and the lockfile-based "downloaded vs hand-authored" rule.
681
- - **The Dockerfile template itself** (the autogenerated layers in `Dockerfile`: bun base image, apt setup, GitHub CLI, `agent-browser`, Chrome for Testing) — that is host-stage, controlled by `src/init/dockerfile.ts` in the typeclaw repo, not by `typeclaw.json`. `typeclaw.json#dockerfile.append` (covered above) is the only piece of the build customizable per-agent; everything else requires a typeclaw release.
673
+ - **The Dockerfile template itself** (the autogenerated layers in `Dockerfile`: bun base image, apt setup, GitHub CLI, `agent-browser`, Chrome for Testing) — that is host-stage, controlled by `src/init/dockerfile.ts` in the typeclaw repo, not by `typeclaw.json`. `typeclaw.json#docker.file.append` (covered above) is the only piece of the build customizable per-agent; everything else requires a typeclaw release.
682
674
  - **The host-stage launcher's invocation flags** (`docker run` arguments synthesized by `typeclaw start`, the `_hostd` daemon's lifecycle, the host port allocation that maps to `port` inside the container) — those are host-stage code, not config. The pieces of that flow that **are** user-configurable through `typeclaw.json` (`port`, `mounts`, `portForward`) are documented above; the rest is not.
@@ -45,7 +45,7 @@ When dreaming fires, it reads:
45
45
  1. `MEMORY.md`
46
46
  2. The **undreamed tail** of every `memory/yyyy-MM-dd.md` (the runtime tells it the exact line range — earlier lines are already consolidated into `MEMORY.md` and must NOT be re-read)
47
47
 
48
- It rewrites `MEMORY.md` with the merged result, advances the per-day watermark in `memory/.dreaming-state.json`, optionally writes muscle-memory skills under `memory/skills/<name>/SKILL.md`, then `git commit -m "Dream"` the snapshot. After the commit, the runtime sets the `skip-worktree` index flag on the tracked memory artifacts so the user's `git status` and `git diff` stay clean. The flag is cleared and re-applied around every commit.
48
+ It rewrites `MEMORY.md` with the merged result, advances the per-day watermark in `memory/.dreaming-state.json`, optionally writes muscle-memory skills under `memory/skills/<name>/SKILL.md`, then commits the snapshot with a message shaped like `dream: <summary> <emoji>` — e.g. `dream: 3 fragments + new skill 'pr-review' 🔮`. The summary is derived from the staged diff (line additions in daily streams, newly-added skills, etc.), and the emoji is a random pick from a small thematic pool. After the commit, the runtime sets the `skip-worktree` index flag on the tracked memory artifacts so the user's `git status` and `git diff` stay clean. The flag is cleared and re-applied around every commit.
49
49
 
50
50
  The dreaming subagent has only three tools: `read`, `write`, `ls`. No `bash`. No `edit`. It cannot run shell commands.
51
51
 
@@ -74,7 +74,7 @@ If the undreamed tails contain only watermarks, or every new fragment is already
74
74
 
75
75
  ### What gets injected into your prompt every turn
76
76
 
77
- The plugin's `session.prompt` hook appends a `# Memory` section to your system prompt with:
77
+ Core's `createResourceLoader` appends a `# Memory` section as the LAST block of your system prompt (after `gitNudge`) by calling `loadMemory`. It is pinned to the cache-suffix end so growth in the daily stream invalidates only the memory section itself, not the skills/tools/history above. The section contains:
78
78
 
79
79
  - `MEMORY.md` (truncated to 12 KB; if larger, the rest is dropped with a `[truncated]` marker)
80
80
  - The **undreamed tails** of each `memory/yyyy-MM-dd.md`, with bare watermark lines stripped (they are bookkeeping for the memory-logger, no signal for you)
@@ -115,19 +115,19 @@ You cannot remove a fragment cleanly. The right response depends on what X is:
115
115
 
116
116
  ## When the user asks "what did you dream?" / "when do you dream next?"
117
117
 
118
- 1. **What you dreamed**: read the most recent `Dream` git commit on your agent folder (`git log --grep='^Dream' -1`) and show the diff against `MEMORY.md` if useful. The commit timestamp tells you when dreaming last ran. If the answer is "no `Dream` commits yet", say that — `MEMORY.md` may exist but be the auto-created empty file from the first dreaming attempt.
118
+ 1. **What you dreamed**: read the most recent `dream:` git commit on your agent folder (`git log --grep='^dream:' -1`) and show the diff against `MEMORY.md` if useful. The commit timestamp tells you when dreaming last ran. If the answer is "no `dream:` commits yet", say that — `MEMORY.md` may exist but be the auto-created empty file from the first dreaming attempt.
119
119
  2. **When you dream next**: read `memory.dreaming.schedule` from `typeclaw.json` (default `"*/30 * * * *"` — every 30 minutes). Translate the cron expression to a wall-clock time in the agent's `TZ`. The dreaming cron job is **always registered** even when `memory.dreaming` is omitted; the default schedule applies. Tell the user honestly when the next fire is in the agent's local time.
120
120
 
121
121
  ## When the user asks "what's a daily stream?" / "where is your memory stored?"
122
122
 
123
123
  Stay concrete. Use this map:
124
124
 
125
- | File / dir | What it is | Who writes it | Tracked in git |
126
- | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------- |
127
- | `MEMORY.md` | Long-term memory, consolidated topics with fragment citations. | Dreaming subagent (rewrites in full on each run). | Yes (force-committed under `Dream` commits, skip-worktree). |
128
- | `memory/yyyy-MM-dd.md` | Daily fragment streams. Append-only during the day. | Memory-logger subagent (one fragment ≈ one prompt completion). | Gitignored, but force-committed in the dreaming snapshot. |
129
- | `memory/skills/<name>/SKILL.md` | Muscle-memory skills distilled from recurring procedures. | Dreaming subagent only. | Gitignored, force-committed in the dreaming snapshot. |
130
- | `memory/.dreaming-state.json` | Per-day watermarks (line counts already consolidated). Plain JSON, fail-open. | Dreaming subagent. | Gitignored, force-committed in the dreaming snapshot. |
125
+ | File / dir | What it is | Who writes it | Tracked in git |
126
+ | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------ |
127
+ | `MEMORY.md` | Long-term memory, consolidated topics with fragment citations. | Dreaming subagent (rewrites in full on each run). | Yes (force-committed under `dream:` commits, skip-worktree). |
128
+ | `memory/yyyy-MM-dd.md` | Daily fragment streams. Append-only during the day. | Memory-logger subagent (one fragment ≈ one prompt completion). | Gitignored, but force-committed in the dreaming snapshot. |
129
+ | `memory/skills/<name>/SKILL.md` | Muscle-memory skills distilled from recurring procedures. | Dreaming subagent only. | Gitignored, force-committed in the dreaming snapshot. |
130
+ | `memory/.dreaming-state.json` | Per-day watermarks (line counts already consolidated). Plain JSON, fail-open. | Dreaming subagent. | Gitignored, force-committed in the dreaming snapshot. |
131
131
 
132
132
  `typeclaw init` does **not** scaffold any of these. They appear when needed — `MEMORY.md` and `memory/` are created by the first dreaming run; daily streams appear when the first memory-logger fires.
133
133