typeclaw 0.3.1 → 0.4.0

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 (89) hide show
  1. package/README.md +20 -15
  2. package/auth.schema.json +113 -0
  3. package/package.json +1 -1
  4. package/secrets.schema.json +113 -0
  5. package/src/agent/session-meta.ts +1 -1
  6. package/src/agent/session-origin.ts +3 -2
  7. package/src/bundled-plugins/security/index.ts +3 -2
  8. package/src/channels/adapters/github/auth-app.ts +120 -0
  9. package/src/channels/adapters/github/auth-pat.ts +50 -0
  10. package/src/channels/adapters/github/auth.ts +33 -0
  11. package/src/channels/adapters/github/channel-resolver.ts +30 -0
  12. package/src/channels/adapters/github/dedup.ts +26 -0
  13. package/src/channels/adapters/github/event-allowlist.ts +8 -0
  14. package/src/channels/adapters/github/fetch-attachment.ts +5 -0
  15. package/src/channels/adapters/github/history.ts +63 -0
  16. package/src/channels/adapters/github/inbound.ts +286 -0
  17. package/src/channels/adapters/github/index.ts +286 -0
  18. package/src/channels/adapters/github/managed-path.ts +54 -0
  19. package/src/channels/adapters/github/membership.ts +35 -0
  20. package/src/channels/adapters/github/outbound.ts +145 -0
  21. package/src/channels/adapters/github/webhook-register.ts +349 -0
  22. package/src/channels/manager.ts +94 -9
  23. package/src/channels/schema.ts +31 -1
  24. package/src/channels/tunnel-bridge.ts +51 -0
  25. package/src/cli/builtins.ts +28 -0
  26. package/src/cli/channel.ts +511 -25
  27. package/src/cli/container-command-client.ts +244 -0
  28. package/src/cli/cron.ts +173 -0
  29. package/src/cli/host-command-runner.ts +150 -0
  30. package/src/cli/index.ts +42 -1
  31. package/src/cli/init.ts +256 -27
  32. package/src/cli/model.ts +4 -2
  33. package/src/cli/plugin-command-help.ts +49 -0
  34. package/src/cli/plugin-commands-dispatch.ts +112 -0
  35. package/src/cli/plugin-commands.ts +118 -0
  36. package/src/cli/tui.ts +10 -2
  37. package/src/cli/tunnel.ts +533 -0
  38. package/src/cli/ui.ts +8 -3
  39. package/src/config/config.ts +75 -0
  40. package/src/container/start.ts +30 -3
  41. package/src/cron/bridge.ts +136 -0
  42. package/src/cron/consumer.ts +45 -5
  43. package/src/cron/index.ts +19 -2
  44. package/src/cron/list.ts +105 -0
  45. package/src/cron/scheduler.ts +12 -3
  46. package/src/cron/schema.ts +11 -3
  47. package/src/doctor/checks.ts +0 -50
  48. package/src/init/dockerfile.ts +59 -13
  49. package/src/init/ensure-deps.ts +15 -4
  50. package/src/init/github-webhook-install.ts +109 -0
  51. package/src/init/index.ts +505 -9
  52. package/src/init/run-bun-install.ts +17 -3
  53. package/src/init/run-owner-claim.ts +11 -2
  54. package/src/permissions/builtins.ts +6 -1
  55. package/src/permissions/match-rule.ts +24 -2
  56. package/src/permissions/resolve.ts +1 -0
  57. package/src/plugin/define.ts +42 -1
  58. package/src/plugin/index.ts +18 -3
  59. package/src/plugin/manager.ts +2 -0
  60. package/src/plugin/registry.ts +85 -3
  61. package/src/plugin/types.ts +138 -1
  62. package/src/plugin/zod-introspect.ts +100 -0
  63. package/src/role-claim/match-rule.ts +2 -1
  64. package/src/run/index.ts +110 -3
  65. package/src/secrets/index.ts +1 -1
  66. package/src/secrets/schema.ts +21 -0
  67. package/src/server/command-runner.ts +476 -0
  68. package/src/server/index.ts +388 -5
  69. package/src/shared/index.ts +8 -0
  70. package/src/shared/protocol.ts +80 -1
  71. package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
  72. package/src/skills/typeclaw-config/SKILL.md +27 -26
  73. package/src/skills/typeclaw-cron/SKILL.md +234 -3
  74. package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
  75. package/src/skills/typeclaw-permissions/SKILL.md +5 -4
  76. package/src/skills/typeclaw-plugins/SKILL.md +251 -5
  77. package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
  78. package/src/test-helpers/wait-for.ts +50 -0
  79. package/src/tui/index.ts +35 -4
  80. package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
  81. package/src/tunnels/events.ts +14 -0
  82. package/src/tunnels/index.ts +12 -0
  83. package/src/tunnels/log-ring.ts +54 -0
  84. package/src/tunnels/manager.ts +139 -0
  85. package/src/tunnels/providers/cloudflare-quick.ts +189 -0
  86. package/src/tunnels/providers/external.ts +53 -0
  87. package/src/tunnels/quick-url-parser.ts +5 -0
  88. package/src/tunnels/types.ts +43 -0
  89. package/typeclaw.schema.json +254 -1
@@ -19,7 +19,7 @@ The runtime reads `typeclaw.json` at container startup. Some fields are picked u
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
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).
22
+ - `docker.file` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated apt packages (`tmux`, `gh`, `python`, `cjkFonts` 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` and `cjkFonts` are 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
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
 
@@ -39,18 +39,18 @@ 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 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. |
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`, `cjkFonts`) gate opinionated apt packages; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: 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
 
@@ -65,7 +65,7 @@ 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), `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.
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, "cjkFonts": true, "append": [] } }` (tmux/gh/python/cjkFonts 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
 
@@ -337,18 +337,19 @@ Off switch — the broker is constructed but never opens a WS, no LISTEN gets fo
337
337
 
338
338
  The `docker.file` block has two layers of customization:
339
339
 
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.
340
+ 1. **Toggles** for opinionated apt packages typeclaw knows how to install with proper layer caching (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`). 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.
341
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.
342
342
 
343
343
  ### Fields
344
344
 
345
- | Field | Required | Type | Notes |
346
- | -------- | -------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
- | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
348
- | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
349
- | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
350
- | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
351
- | `append` | no | array of strings | Each entry is a single Dockerfile line schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
345
+ | Field | Required | Type | Notes |
346
+ | ---------- | -------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
+ | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
348
+ | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
349
+ | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
350
+ | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
351
+ | `cjkFonts` | no | boolean | Default `true`. Installs `fonts-noto-cjk` (~56 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). Boolean-only: the package is a metapackage tracking upstream Noto, no useful apt pin. |
352
+ | `append` | no | array of strings | Each entry is a single Dockerfile line — schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
352
353
 
353
354
  Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
354
355
 
@@ -385,7 +386,7 @@ CMD ["run"]
385
386
 
386
387
  The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/var/cache/apt` and `/var/lib/apt/lists`, so toggling `ffmpeg: true` (or pinning `gh: "2.40.0"`) only re-fetches what changed. The `gh` keyring bootstrap is in its own earlier layer that's gated on `gh` being enabled — turning `gh: false` saves the network roundtrip even on cold builds.
387
388
 
388
- `append` runs after every cache-friendly base layer (apt setup, the toggle-driven apt install, `agent-browser`, Chrome for Testing on amd64), so changing `append` invalidates only the final layer. Conversely, putting `apt-get install` in `append` is **slower than using a toggle** (no BuildKit cache mount) — and if the package you want is `tmux/gh/python/ffmpeg`, just use the toggle.
389
+ `append` runs after every cache-friendly base layer (apt setup, the toggle-driven apt install, `agent-browser`, Chrome for Testing on amd64), so changing `append` invalidates only the final layer. Conversely, putting `apt-get install` in `append` is **slower than using a toggle** (no BuildKit cache mount) — and if the package you want is `tmux/gh/python/ffmpeg/cjkFonts`, just use the toggle.
389
390
 
390
391
  ### Restart and rebuild semantics
391
392
 
@@ -396,7 +397,7 @@ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/v
396
397
  ### When the user asks "install <package> in the container" / "add a Dockerfile line"
397
398
 
398
399
  1. **Read `typeclaw.json`.**
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`.
400
+ 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, `ffmpeg`, or `cjkFonts` (CJK glyph rendering for `agent-browser` screenshots), 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`.
400
401
  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.
401
402
  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.
402
403
  5. **Append to `docker.file.append`** (creating the field if it doesn't exist). Preserve existing entries.
@@ -617,7 +618,7 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
617
618
  - `channels.<adapter>.allow` (legacy) is silently dropped on parse; `migrateLegacyConfigShape` lifts it into `roles.member.match` on load. See the `typeclaw-permissions` skill.
618
619
  - 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`)
619
620
  - 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
621
+ - If any `docker.file` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python` and `cjkFonts` are boolean only
621
622
  - 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.
622
623
 
623
624
  ## Things you must not do
@@ -636,7 +637,7 @@ Never echo, log, or commit values from `.env` or `secrets.json`. Both are gitign
636
637
  - **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
638
  - **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
639
  - **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 reach for `docker.file.append` when a toggle covers it.** If the user wants tmux, gh, python, ffmpeg, or fonts-noto-cjk (cjkFonts) installed (or removed, or pinned), use the toggle — it's the cache-mounted path. `append` for these is slower and harder to read.
640
641
  - **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
642
  - **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.
642
643
  - **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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: typeclaw-cron
3
- description: Use this skill whenever the user asks you to schedule recurring work, run something on a cron, do something every day/hour/week, set up a periodic task, or read or edit your cron schedule. Triggers include "every morning", "every Monday", "schedule a", "remind me every", "set up a cron", "run X periodically", "what's on my cron", "when does X run", or any mention of `cron.json`. Read it before touching `cron.json` — the file has a strict schema, restart semantics, and a best-effort execution model that you must not misrepresent to the user.
3
+ description: Use this skill whenever the user asks you to schedule recurring work, run something on a cron, do something every day/hour/week, set up a periodic task, list or inspect scheduled jobs, or read or edit your cron schedule. Triggers include "every morning", "every Monday", "schedule a", "remind me every", "set up a cron", "run X periodically", "list cron jobs", "list scheduled jobs", "show me the cron", "what cron jobs do you have", "what's on my cron", "when does X run", or any mention of `cron.json`. Read it before touching `cron.json` — the file has a strict schema, restart semantics, and a best-effort execution model that you must not misrepresent to the user.
4
4
  ---
5
5
 
6
6
  # typeclaw-cron
@@ -65,10 +65,217 @@ What this means for how you write prompts:
65
65
 
66
66
  The runtime spawns the command directly with `Bun.spawn` from the agent folder (`/agent` inside the container). No agent session is created. No LLM call happens. The command's exit code and stderr are captured to container logs.
67
67
 
68
- Use `exec` only for jobs that are pure mechanics — no judgement required. Examples that fit: git snapshots, log rotation, calling a script that already exists. Examples that **don't** fit: anything where "what do I commit" or "what should I write" depends on context. Use `prompt` for those.
68
+ Use `exec` only for jobs that are pure mechanics — no judgement required. Examples that fit: git snapshots, log rotation, calling a script that already exists. Examples that **don't** fit: anything where "what do I commit" or "what should I write" depends on context. Use `prompt` for those — **or**, when the work needs imperative control flow that mixes shell calls and LLM calls (probe → maybe prompt → write file) and both the cadence and the logic belong to the same plugin, write a `kind: 'handler'` plugin cron job (see below). That's the best practice for the `exec → LLM` pattern; a cron `exec` pointing at `typeclaw <plugin-cmd>` is a narrower fallback for reusable / host-surface cases.
69
69
 
70
70
  `command` is an array. Index 0 is the executable, the rest are argv. Do **not** put a single shell pipeline in `command[0]` — that won't be parsed by a shell. If you need shell features (`|`, `>`, `&&`), wrap explicitly: `["sh", "-c", "your | pipeline | here"]`.
71
71
 
72
+ ## `exec → LLM`: write a plugin cron handler (best practice)
73
+
74
+ If a scheduled job needs imperative control flow that mixes shell calls and LLM calls (probe → maybe prompt → write file), the best practice is a **plugin cron handler**: a TypeScript function the plugin registers under its own `cronJobs` with `kind: 'handler'`. The cron consumer invokes it directly — no shell-out, no WS round-trip, no `Bun.spawn`. Prefer this whenever the cadence and the logic both belong to the same plugin (which is almost always — see "When to reach for the exec bridge instead" below for the two narrow exceptions).
75
+
76
+ `cron.json` itself supports only `prompt` and `exec` — `kind: 'handler'` is plugin-only because the handler is a TypeScript function reference (not JSON-serializable). User-authored cron files that try to declare `kind: 'handler'` are rejected by `parseCronFile`.
77
+
78
+ ```ts
79
+ // packages/dev-audits/index.ts
80
+ import { definePlugin } from 'typeclaw/plugin'
81
+
82
+ export default definePlugin({
83
+ plugin: async () => ({
84
+ cronJobs: {
85
+ daily: {
86
+ schedule: '0 22 * * *',
87
+ timezone: 'Asia/Seoul',
88
+ kind: 'handler',
89
+ handler: async (ctx) => {
90
+ const { stdout } = await ctx.exec`git log --since='24h' --pretty=format:'%h %s'`
91
+ if (stdout.trim().length === 0) return
92
+ await ctx.prompt(
93
+ `These commits landed in the last 24h:\n${stdout}\nAppend a critique of weak commit messages to memory/audits/$(date +%F)-commits.md. Be specific — quote bad messages and suggest rewrites.`,
94
+ )
95
+ },
96
+ },
97
+ },
98
+ }),
99
+ })
100
+ ```
101
+
102
+ `typeclaw.json`:
103
+
104
+ ```json
105
+ {
106
+ "plugins": ["./packages/dev-audits"]
107
+ }
108
+ ```
109
+
110
+ That's the whole installation. No `cron.json` edit, no CLI command shim. `typeclaw restart` and the job is live.
111
+
112
+ ### The `CronHandlerContext` surface
113
+
114
+ The handler receives a `ctx` with the LLM-call surface of a container plugin command, minus the CLI-shaped fields:
115
+
116
+ | Field | Type | Notes |
117
+ | ----------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `ctx.jobId` | `string` | The global cron id (`__plugin_<plugin-name>_<key>`). Useful for log lines. |
119
+ | `ctx.name` | `string` | The plugin name that registered this cron job. Mirrors `ContainerCommandContext.name`. |
120
+ | `ctx.agentDir` | `string` | `/agent` in the container. |
121
+ | `ctx.logger` | `PluginLogger` | Plugin-prefixed `info` / `warn` / `error` going to container stdout. |
122
+ | `ctx.signal` | `AbortSignal` | Reserved for future cancellation; currently never aborted by the runtime (matches existing prompt/exec cron behavior — in-flight work runs to completion on container shutdown). Already threaded into `ctx.prompt` and `ctx.exec`, so future aborts propagate without handler-author changes. |
123
+ | `ctx.permissions` | `PermissionService` | Same service the rest of the runtime uses. Most handlers don't need it; the LLM session resolves permissions through `ctx.origin` automatically. |
124
+ | `ctx.origin` | `SessionOrigin` (cron-shaped) | `{ kind: 'cron', jobId, jobKind: 'handler', scheduledByRole, scheduledByOrigin }`. Plugin-contributed jobs default to `scheduledByRole: 'owner'`. |
125
+ | `ctx.prompt` | `(text: string) => Promise<string>` | Opens a brand-new agent session with the full toolset, sends `text`, returns the final assistant message. Uses **slim system prompt mode** (saves ~2000 tokens per LLM call vs a TUI session). |
126
+ | `ctx.subagent` | `(name: string, payload?) => Promise<void>` | Invokes a registered subagent. Same dispatch path as `PluginContext.spawnSubagent`. |
127
+ | `ctx.exec` | `` ctx.exec`shell pipeline` `` → `Promise<CommandExecResult>` | Tagged template; runs in the agent folder with `ctx.signal` threaded through. Abort kills the entire process group (SIGTERM → 5s grace → SIGKILL). |
128
+
129
+ What's NOT on the handler ctx (and why):
130
+
131
+ - **`stdin` / `stdout` / `stderr`** — cron has no caller piping bytes in or reading bytes out. Use `ctx.logger` or write files for output.
132
+ - **`args`** — handlers are scheduled, not invoked with flags. Configurable values come through the plugin's `configSchema`.
133
+ - **Return value** — the function returns `Promise<void>`. Throw to signal failure; the cron consumer catches and logs. (Note: do NOT write `return 0` — handler return is `void`, not a numeric exit code like a container command's `run`.)
134
+
135
+ ### Trust model
136
+
137
+ Plugin-contributed cron handlers run with `scheduledByRole: 'owner'` because installed plugins already execute arbitrary in-process TypeScript at boot, on every hook, and inside every tool — granting cron handlers a tighter role wouldn't be a security boundary anyway, since the plugin code already has full process privileges. The role is real (every tool call inside `ctx.prompt` resolves against it) and a future API could tighten it for specific contexts, but today plugin authors are trusted runtime contributions, not user input. See `typeclaw-permissions` for the broader model.
138
+
139
+ ### When to reach for the exec bridge instead
140
+
141
+ The `kind: 'handler'` path is the right answer for plugin-internal scheduled imperative work. The exec bridge — a `kind: 'exec'` cron job invoking `["typeclaw", "<plugin-command>", ...]` — is the right answer ONLY when **reusability is a real requirement**, not just because the work is scheduled. The bridge buys you a callable CLI surface; the handler does not. Use the bridge when one of these holds:
142
+
143
+ 1. **The same logic must also be invocable as a CLI command.** The user wants to run `typeclaw audit-commits --since=7d` manually from the TUI or a shell, or another plugin / `compose` orchestration wants to call it, or the work needs flags that are part of a public command interface. Write the logic once inside a `surface: 'container'` plugin command's `run`, then point cron at it. Same imperative control flow lives in the command body; cron just provides a different trigger.
144
+
145
+ 2. **The user owns the cadence.** Someone else's plugin ships `audit-commits` (as a container command, see `typeclaw-plugins` §5.7) but no cron registration, or its default cadence doesn't match what the user wants. The user adds a `cron.json` exec job pointing at the command — no need to fork the plugin to change the schedule.
146
+
147
+ 3. **The scheduled job needs to invoke a `surface: 'host'` command.** Host commands run outside the container with no agent runtime — neither `ctx.prompt` nor `ctx.subagent` is available there. A cron `exec` job invoking `typeclaw <host-cmd>` is the only way to schedule host-side work from inside the container's cron.
148
+
149
+ If none of those apply — the plugin owns both the cadence and the logic, and nothing else needs to call the logic — write a `kind: 'handler'` job. "It's scheduled work that needs LLM judgement" alone is NOT a reason to reach for the bridge; the bridge costs a shell-out, a WS round-trip, and an args-parse round-trip that the handler avoids entirely.
150
+
151
+ In both cases the `command` array is `['typeclaw', '<cmd>', ...]` and the runtime injects `TYPECLAW_PARENT_ORIGIN_JSON` so the spawned subprocess inherits the cron job's role through the same mechanism that protects plugin-contributed handlers from silent elevation.
152
+
153
+ ```json
154
+ // cron.json — user wants someone else's plugin command on a custom schedule
155
+ {
156
+ "jobs": [
157
+ {
158
+ "id": "weekly-commit-audit",
159
+ "schedule": "0 22 * * 0",
160
+ "timezone": "Asia/Seoul",
161
+ "kind": "exec",
162
+ "command": ["typeclaw", "audit-commits", "--since=7d"],
163
+ "scheduledByRole": "owner"
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ A plugin can ship a `kind: 'handler'` default in `cronJobs` AND the user can add a different cadence in `cron.json` for the same command. They are independent cron jobs at the scheduler layer.
170
+
171
+ ### Decision rules — which arm picks what
172
+
173
+ ```
174
+ "I have scheduled work" → start here
175
+
176
+ Is the work pure mechanics (git commit, log rotation, calling a known script)?
177
+ └─ Yes → kind: 'exec' in cron.json. No plugin needed.
178
+
179
+ Does it need LLM judgement?
180
+
181
+ ├─ One-shot natural-language prompt, no probes, no shell pre-work?
182
+ │ └─ kind: 'prompt' in cron.json.
183
+
184
+ ├─ Imperative control flow (probe → maybe prompt → write file)?
185
+ │ │
186
+ │ ├─ Default: cadence + logic both belong to the same plugin,
187
+ │ │ nothing outside cron needs to call this logic?
188
+ │ │ └─ kind: 'handler' in the plugin's cronJobs. ← BEST PRACTICE
189
+ │ │
190
+ │ ├─ The same logic ALSO needs to be a callable CLI command
191
+ │ │ (TUI / manual shell / compose), or the user owns the
192
+ │ │ cadence for someone else's command?
193
+ │ │ └─ kind: 'exec' in cron.json, command: ['typeclaw', '<cmd>']
194
+ │ │ (write the command as surface: 'container')
195
+ │ │
196
+ │ └─ The work needs a `surface: 'host'` plugin command?
197
+ │ └─ kind: 'exec' in cron.json, command: ['typeclaw', '<host-cmd>']
198
+ ```
199
+
200
+ ### What this pattern is NOT
201
+
202
+ - It is **not** a way to bypass permissions. Plugin `kind: 'handler'` jobs run under the plugin-default role (`'owner'`). Plugin `kind: 'exec'` and `cron.json` `kind: 'exec'` stamp `scheduledByRole` into the spawned subprocess via `TYPECLAW_PARENT_ORIGIN_JSON`; the plugin command's `ctx.origin` carries that role into every tool call inside `ctx.prompt`'s session. A cron scheduled as `scheduledByRole: 'member'` runs as a member — no silent elevation. See `typeclaw-permissions`.
203
+ - It is **not** a wrapper for shell pipelines you already have working. If `bash some-script.sh` does the job, just use that as the `command` array directly. Reach for handlers (or the exec bridge) only when LLM judgement is genuinely required inside the periodic work.
204
+
205
+ Read `typeclaw-plugins` §5.3 for the `cronJobs` registration shape, §5.7 for the full `commands` surface (host/container/either, `args` schema, `ctx.prompt` / `ctx.subagent` / `ctx.exec`, permission gating). Read `typeclaw-monorepo` for where the plugin package lives in `packages/`.
206
+
207
+ ### Conditional LLM calls: gate `ctx.prompt` behind a cheap check
208
+
209
+ Most polling-style cron jobs are skewed: they fire often (every 5 minutes, every hour) and **most ticks find no work**. A plain `kind: "prompt"` job spends a full LLM round-trip every tick just to discover there's nothing to do. That gets expensive fast — a 5-minute "check for new emails" prompt is ~290 LLM calls a day, even on days where nothing arrived.
210
+
211
+ `kind: 'handler'` fixes this naturally because `ctx.exec` runs **before** `ctx.prompt`. Do the cheap check first; only spend tokens when there's actual work:
212
+
213
+ ```ts
214
+ // packages/inbox-watch/index.ts
215
+ import { definePlugin } from 'typeclaw/plugin'
216
+
217
+ export default definePlugin({
218
+ plugin: async () => ({
219
+ cronJobs: {
220
+ watch: {
221
+ schedule: '*/15 * * * *',
222
+ kind: 'handler',
223
+ handler: async (ctx) => {
224
+ // Cheap shell check: 0 LLM cost, ~100ms.
225
+ const { stdout, exitCode } = await ctx.exec`gmail unread --since=15m --count`
226
+ if (exitCode !== 0) {
227
+ // Don't drag the LLM into shell failures — log and bail.
228
+ ctx.logger.error(`gmail probe failed (exit ${exitCode})`)
229
+ return
230
+ }
231
+ const count = Number.parseInt(stdout.trim(), 10)
232
+ if (!Number.isFinite(count) || count === 0) {
233
+ // Nothing to do. Return silently so cron logs stay quiet.
234
+ return
235
+ }
236
+ // Expensive LLM path: only reached when there's actual work.
237
+ await ctx.prompt(
238
+ `There are ${count} unread emails since 15m ago. Use the gmail skill to read them, summarize anything that needs a human reply, and append to memory/inbox/$(date +%F).md.`,
239
+ )
240
+ },
241
+ },
242
+ },
243
+ }),
244
+ })
245
+ ```
246
+
247
+ The shape that matters:
248
+
249
+ 1. **Probe with `ctx.exec` (or an `await` on a Node API) first.** Anything that returns a yes/no signal cheaply: a CLI tool exit code, a count, a file mtime, an HTTP HEAD, a `git log -1 --since=...` output.
250
+ 2. **Return early when the probe says "no work".** A bare `return` exits the handler cleanly, cron logs nothing scary, and zero LLM tokens were spent. Critically: do NOT call `ctx.prompt` to "decide whether to act" — that defeats the entire optimization.
251
+ 3. **Reach for `ctx.prompt` only on the work path.** Pass the probe's output into the prompt so the agent doesn't have to re-discover what triggered the run (e.g. `${count} unread emails`, the list of changed files, the new commit hash). This also shortens the LLM's first turn — it gets to act, not investigate.
252
+
253
+ Concrete signals you can probe cheaply (in rough order of common use):
254
+
255
+ | Question | Cheap probe |
256
+ | ----------------------------------- | --------------------------------------------------------------------- |
257
+ | Are there new emails? | `gmail unread --since=15m --count` (or your skill's CLI) |
258
+ | Did anyone commit since last check? | `git log --since=15m --pretty=oneline` (empty = no) |
259
+ | Did a file change? | `find <path> -newer .inbox-watch.stamp -type f` |
260
+ | Is there a new PR/issue? | `gh pr list --search 'created:>15m' --json number` (empty array = no) |
261
+ | Did a service go down? | `curl -fsS https://... > /dev/null` (non-zero = down) |
262
+ | Is there a new line in a log? | `wc -l <log>` vs a stamp file |
263
+ | Did `last-run.txt` rot to stale? | `find last-run.txt -mmin +60` (empty = fresh) |
264
+
265
+ For "since last run" semantics, write a stamp file at the end of every successful run: `await ctx.exec\`touch .inbox-watch.stamp\``. The next tick's probe compares against it via `-newer`or`mtime`. Stamp files belong in `workspace/`or under`memory/state/` — never at the agent root.
266
+
267
+ When NOT to gate:
268
+
269
+ - **The work is small enough that the LLM probe is the action.** A daily "summarize today" job that always has something to summarize doesn't need a gate; the prompt does the work.
270
+ - **The probe is as expensive as the prompt.** If your "is there work?" check requires reading 200 files anyway, just let the LLM do it once with the full toolset.
271
+ - **You genuinely want the LLM to decide intent on every tick.** Rare, but valid — e.g. a "morning standup" job that always produces output regardless of how busy yesterday was.
272
+
273
+ Pitfalls to avoid:
274
+
275
+ - **Don't promise the user "the agent checks every 5 minutes" if you've written `*/5 * * * *` without a gate.** That's 12 LLM calls an hour for empty inboxes. Either gate it, or slow the schedule to match what the work actually warrants.
276
+ - **Don't gate inside `ctx.prompt` itself** ("if there are new emails, do X; else do nothing"). The LLM still ran. The gate has to be in shell code outside `ctx.prompt`.
277
+ - **Don't leak probe failures into the LLM session.** If `ctx.exec` exits non-zero, decide explicitly: log via `ctx.logger.error` and bail (`return`), `throw` to surface the failure in cron logs, or recover with a fallback path. Don't fall through into `ctx.prompt` with no input — the agent will improvise, and the improvisation is usually worse than a clean `cron failed: ...` log line.
278
+
72
279
  ## Schedule syntax
73
280
 
74
281
  Standard cron, parsed by [`cron-parser`](https://github.com/harrisiirak/cron-parser). 5-field is the common form.
@@ -142,12 +349,36 @@ If you finished an edit and the user only sees an in-flight job from the previou
142
349
 
143
350
  Pick `kind` first, then schedule, then timezone:
144
351
 
145
- 1. **Does the work need judgement?** → `prompt`. Otherwise → `exec`.
352
+ 1. **Pick the kind.**
353
+ - **Pure mechanics, no judgement** (git snapshots, log rotation, calling an existing script) → `kind: 'exec'` in `cron.json`. Done.
354
+ - **One-shot natural-language instruction, no shell pre-work, no conditional logic** → `kind: 'prompt'` in `cron.json`. Done.
355
+ - **Imperative control flow mixing shell calls and LLM calls** (probe → maybe prompt → write file, "if there are new emails then triage", etc.) → **write a `kind: 'handler'` plugin cron job** (see "`exec → LLM`: write a plugin cron handler" above). This is the default for scheduled `exec → LLM` work.
356
+ - **Reuse a CLI command on a custom cadence** — the same logic must ALSO be invocable from the TUI / manual shell / `compose` orchestration, or the schedule is owned by the user (`cron.json`) rather than the plugin author, or the work must run as a `surface: 'host'` command → `kind: 'exec'` in `cron.json` with `command: ["typeclaw", "<plugin-command>", ...]`. Reach for this ONLY when reusability is the actual requirement, not just because the work is scheduled. See "When to reach for the exec bridge instead" above.
146
357
  2. **Translate the cadence to cron.** "Every morning at 7" → `0 7 * * *`. "Every weekday at 9:30" → `30 9 * * 1-5`. "Every five minutes" → `*/5 * * * *`. If you are not sure, ask once. Don't guess on tricky cases like "every other Friday".
147
358
  3. **Timezone.** If the user mentioned a wall-clock time, set `timezone` to their zone. If unknown, ask once or default to the timezone in `USER.md` if it's recorded there.
148
359
  4. **Pick a stable `id`.** Use kebab-case that describes the job, not the schedule. `daily-summary` not `0-23-30`.
149
360
  5. **Write it. Call `reload`. If reload succeeded, commit it.** If reload failed, fix `cron.json` based on the error and retry — do not commit a broken file.
150
361
 
362
+ ## Listing what is currently scheduled
363
+
364
+ When the user asks _"what cron jobs do you have?"_, _"list cron jobs"_, _"show me the cron schedule"_, _"when does X next run"_, the answer is **`typeclaw cron list`**, not `read cron.json`.
365
+
366
+ ```sh
367
+ bash$ typeclaw cron list
368
+ ```
369
+
370
+ The command runs on the host stage and asks the running container for its merged registry: every job authored in `cron.json` PLUS every job contributed by plugins (e.g. the bundled memory plugin's `dreaming` cron, which is invisible from `cron.json` alone). Output includes id, source (`user` vs `plugin:<name>.<localId>`), schedule, next-fire timestamp + relative duration, scheduled-by role, and kind-specific tail. Use `--json` if you want to pipe into anything.
371
+
372
+ Why not just `read cron.json`?
373
+
374
+ - It misses every plugin-contributed job. The user almost always wants the merged view.
375
+ - It does not show next-fire times. The user is usually asking about _when_, not _what_.
376
+ - It does not validate the file — a `cron list` will surface invalid schedules and unknown subagent references the live scheduler would reject.
377
+
378
+ Read `cron.json` directly only when you are **editing** it. For any read-only "what is scheduled" question, use `typeclaw cron list`.
379
+
380
+ `typeclaw cron list` requires the container to be running. If `cron list` reports the agent is unreachable, suggest `typeclaw start` or fall back to reading `cron.json` directly (with a note that plugin jobs are missing from the view).
381
+
151
382
  ## Reading cron history
152
383
 
153
384
  There is no "list past fires" tool. To see what cron has done:
@@ -170,6 +170,6 @@ If you see `Cannot find module 'my-utility'` after creating a new workspace pack
170
170
 
171
171
  ## Cross-references
172
172
 
173
- - **`typeclaw-plugins`** — read this before authoring any custom plugin. Covers the plugin SDK, hooks, tools, subagents, cron, naming derivation, and failure modes.
173
+ - **`typeclaw-plugins`** — read this before authoring any custom plugin. Covers the plugin SDK, hooks, tools, subagents, cron, plugin commands (`typeclaw <name>`), naming derivation, and failure modes.
174
174
  - **`typeclaw-git`** — commit policy. Every new package and every meaningful edit to `package.json` (root or workspace) gets a commit immediately with decision context.
175
- - **`typeclaw-cron`** — if your package is invoked from a cron job, the `prompt` vs `exec` choice and the schedule semantics live there.
175
+ - **`typeclaw-cron`** — if your package is invoked from a cron job, the `prompt` / `exec` / `handler` choice and the schedule semantics live there. The best practice for scheduled `exec → LLM` work is a plugin cron job with `kind: 'handler'`; the cron-exec → plugin-CLI-command shell-out is the fallback when the same logic must also be invocable as a reusable CLI command.
@@ -24,7 +24,7 @@ You always have these four, even if `typeclaw.json` declares zero `roles`. User-
24
24
  | Role | Built-in `match[]` | Default `permissions[]` |
25
25
  | --------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
26
26
  | `owner` | `["tui"]` (always prepended) | `channel.respond`, `cron.schedule`, `cron.modify`, **all `security.bypass.*` contributed by plugins** (wildcard sentinel) |
27
- | `trusted` | none | `channel.respond`, `cron.schedule`, `security.bypass.secretExfilBash` |
27
+ | `trusted` | none | `channel.respond`, `cron.schedule`, `security.bypass.secretExfilBash`, `security.bypass.gitExfil` |
28
28
  | `member` | none | `channel.respond` |
29
29
  | `guest` | none (fallback when nothing else matches, or stamped role is bad) | none |
30
30
 
@@ -82,7 +82,7 @@ Three sources contribute permission strings:
82
82
  2. **Bundled security plugin** (always loaded): `security.bypass.secretExfilBash`, `security.bypass.gitExfil`, `security.bypass.gitRemoteTainted`, `security.bypass.secretExfilRead`, `security.bypass.ssrf`, `security.bypass.sessionSearchSecrets`, `security.bypass.systemPromptLeak`, `security.bypass.outboundSecret`.
83
83
  3. **User-declared plugins** (variable): each plugin can contribute its own strings via `definePlugin({ permissions: [...] })`.
84
84
 
85
- `owner` carries every `security.bypass.*` from sources 2 and 3 by default (via a wildcard sentinel expanded at boot). `trusted` carries only `security.bypass.secretExfilBash` by default. `member` and `guest` carry no `security.bypass.*` strings.
85
+ `owner` carries every `security.bypass.*` from sources 2 and 3 by default (via a wildcard sentinel expanded at boot). `trusted` carries `security.bypass.secretExfilBash` and `security.bypass.gitExfil` by default (so a trusted actor can run dangerous bash and `git push` without per-call acks) but **deliberately not** `security.bypass.gitRemoteTainted` — the two-step social-attack defense (re-point remote, then push to it) still fires for trusted, so a prompt-injection mid-session that swaps the remote URL still blocks the eventual push. `member` and `guest` carry no `security.bypass.*` strings.
86
86
 
87
87
  User-declared `permissions[]` strings that don't appear in any of the three sources are **logged as warnings at boot** (`[permissions] role "X" declares unknown permission "Y" — did you mean 'Z'?`) but the role still resolves with the unknown string in its list. This is intentional — the runtime is forward-compatible with strings from plugins that aren't loaded yet — but it also means typos silently fail to bypass guards. If you wrote `security.bypass.secretExfilBach` instead of `Bash`, no guard will be skipped and you will only notice when you read the boot logs.
88
88
 
@@ -94,7 +94,8 @@ The security plugin's `tool.before` hook produces block messages of the form:
94
94
  Guard `<guardName>` blocked <what>. If this is genuinely intentional and the user
95
95
  explicitly asked for it, retry with `acknowledgeGuards.<guardName>: true` in the
96
96
  <tool> arguments. Or run as a role carrying `<permission>` (owner has all
97
- security.bypass.*; trusted has security.bypass.secretExfilBash).
97
+ security.bypass.*; trusted has security.bypass.secretExfilBash and
98
+ security.bypass.gitExfil — but not gitRemoteTainted).
98
99
  ```
99
100
 
100
101
  Three escape hatches, ordered from least to most invasive:
@@ -120,7 +121,7 @@ To distinguish cause 1/2 from cause 3: if `typeclaw logs <container> -f` (host s
120
121
  This is a `roles` edit. The full procedure:
121
122
 
122
123
  1. **Resolve the coordinates.** Get the platform name (`slack | discord | telegram | kakao`), the workspace ID, the chat ID. If the user gave you names, ask them or look them up in the participants list of a previous inbound from that channel.
123
- 2. **Pick a role.** Default to `member` for "give them normal channel access". Use `trusted` if they should also be able to schedule cron and bypass the bash secret guard. Only use `owner` if they should have full bypass on every security guard — typically the agent's primary operator.
124
+ 2. **Pick a role.** Default to `member` for "give them normal channel access". Use `trusted` if they should also be able to schedule cron, bypass the bash secret guard, and run `git push` / `git remote add` / `git add -f` without per-call acks (the two-step taint defense still fires for trusted so a mid-session remote re-point still blocks the eventual push). Only use `owner` if they should have full bypass on every security guard, including the taint defense — typically the agent's primary operator.
124
125
  3. **Edit `typeclaw.json` `roles.<role>.match[]`.** Append the canonical DSL string. Example: `roles.member.match` adds `"slack:T0123/C0ABCDE"`. If the user wants only a specific person in that channel, append `slack:T0123/C0ABCDE author:U_ME` instead.
125
126
  4. **Restart.** `roles` is **restart-required** — `typeclaw reload` does not re-evaluate role config. Tell the user: "edited `roles.<role>.match` — restart-required. Run `typeclaw restart` (host stage)."
126
127
  5. **Commit the change.** See the `typeclaw-git` skill. The decision context in the commit message should name the role, the channel, and the author/scope ("let @X talk to me as `member` in #foo in workspace bar").