typeclaw 0.3.0 → 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 (101) hide show
  1. package/README.md +20 -15
  2. package/auth.schema.json +113 -0
  3. package/package.json +2 -1
  4. package/scripts/dump-system-prompt.ts +401 -0
  5. package/secrets.schema.json +113 -0
  6. package/src/agent/index.ts +149 -30
  7. package/src/agent/provider-error.ts +44 -0
  8. package/src/agent/session-meta.ts +43 -0
  9. package/src/agent/session-origin.ts +3 -2
  10. package/src/agent/subagents.ts +8 -0
  11. package/src/agent/system-prompt.ts +70 -35
  12. package/src/bundled-plugins/security/index.ts +3 -2
  13. package/src/channels/adapters/github/auth-app.ts +120 -0
  14. package/src/channels/adapters/github/auth-pat.ts +50 -0
  15. package/src/channels/adapters/github/auth.ts +33 -0
  16. package/src/channels/adapters/github/channel-resolver.ts +30 -0
  17. package/src/channels/adapters/github/dedup.ts +26 -0
  18. package/src/channels/adapters/github/event-allowlist.ts +8 -0
  19. package/src/channels/adapters/github/fetch-attachment.ts +5 -0
  20. package/src/channels/adapters/github/history.ts +63 -0
  21. package/src/channels/adapters/github/inbound.ts +286 -0
  22. package/src/channels/adapters/github/index.ts +286 -0
  23. package/src/channels/adapters/github/managed-path.ts +54 -0
  24. package/src/channels/adapters/github/membership.ts +35 -0
  25. package/src/channels/adapters/github/outbound.ts +145 -0
  26. package/src/channels/adapters/github/webhook-register.ts +349 -0
  27. package/src/channels/manager.ts +94 -9
  28. package/src/channels/router.ts +28 -2
  29. package/src/channels/schema.ts +31 -1
  30. package/src/channels/tunnel-bridge.ts +51 -0
  31. package/src/cli/builtins.ts +28 -0
  32. package/src/cli/channel.ts +511 -25
  33. package/src/cli/container-command-client.ts +244 -0
  34. package/src/cli/cron.ts +173 -0
  35. package/src/cli/host-command-runner.ts +150 -0
  36. package/src/cli/index.ts +42 -1
  37. package/src/cli/init.ts +256 -27
  38. package/src/cli/model.ts +4 -2
  39. package/src/cli/plugin-command-help.ts +49 -0
  40. package/src/cli/plugin-commands-dispatch.ts +112 -0
  41. package/src/cli/plugin-commands.ts +118 -0
  42. package/src/cli/tui.ts +10 -2
  43. package/src/cli/tunnel.ts +533 -0
  44. package/src/cli/ui.ts +8 -3
  45. package/src/cli/usage.ts +30 -2
  46. package/src/config/config.ts +90 -4
  47. package/src/config/reloadable.ts +22 -4
  48. package/src/container/start.ts +30 -3
  49. package/src/cron/bridge.ts +136 -0
  50. package/src/cron/consumer.ts +62 -6
  51. package/src/cron/index.ts +19 -2
  52. package/src/cron/list.ts +105 -0
  53. package/src/cron/scheduler.ts +12 -3
  54. package/src/cron/schema.ts +11 -3
  55. package/src/doctor/checks.ts +0 -50
  56. package/src/init/dockerfile.ts +59 -13
  57. package/src/init/ensure-deps.ts +15 -4
  58. package/src/init/github-webhook-install.ts +109 -0
  59. package/src/init/index.ts +505 -9
  60. package/src/init/run-bun-install.ts +17 -3
  61. package/src/init/run-owner-claim.ts +11 -2
  62. package/src/permissions/builtins.ts +6 -1
  63. package/src/permissions/match-rule.ts +24 -2
  64. package/src/permissions/resolve.ts +1 -0
  65. package/src/plugin/define.ts +42 -1
  66. package/src/plugin/index.ts +18 -3
  67. package/src/plugin/manager.ts +2 -0
  68. package/src/plugin/registry.ts +85 -3
  69. package/src/plugin/types.ts +138 -1
  70. package/src/plugin/zod-introspect.ts +100 -0
  71. package/src/role-claim/match-rule.ts +2 -1
  72. package/src/run/index.ts +119 -4
  73. package/src/secrets/index.ts +1 -1
  74. package/src/secrets/schema.ts +21 -0
  75. package/src/server/command-runner.ts +476 -0
  76. package/src/server/index.ts +393 -15
  77. package/src/shared/index.ts +8 -0
  78. package/src/shared/protocol.ts +80 -1
  79. package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
  80. package/src/skills/typeclaw-config/SKILL.md +27 -26
  81. package/src/skills/typeclaw-cron/SKILL.md +234 -3
  82. package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
  83. package/src/skills/typeclaw-permissions/SKILL.md +5 -4
  84. package/src/skills/typeclaw-plugins/SKILL.md +251 -5
  85. package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
  86. package/src/test-helpers/wait-for.ts +50 -0
  87. package/src/tui/index.ts +35 -4
  88. package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
  89. package/src/tunnels/events.ts +14 -0
  90. package/src/tunnels/index.ts +12 -0
  91. package/src/tunnels/log-ring.ts +54 -0
  92. package/src/tunnels/manager.ts +139 -0
  93. package/src/tunnels/providers/cloudflare-quick.ts +189 -0
  94. package/src/tunnels/providers/external.ts +53 -0
  95. package/src/tunnels/quick-url-parser.ts +5 -0
  96. package/src/tunnels/types.ts +43 -0
  97. package/src/usage/aggregate.ts +30 -1
  98. package/src/usage/index.ts +3 -2
  99. package/src/usage/report.ts +103 -3
  100. package/src/usage/scan.ts +59 -4
  101. package/typeclaw.schema.json +254 -1
@@ -142,6 +142,119 @@
142
142
  }
143
143
  }
144
144
  },
145
+ "github": {
146
+ "type": "object",
147
+ "properties": {
148
+ "auth": {
149
+ "oneOf": [
150
+ {
151
+ "type": "object",
152
+ "properties": {
153
+ "type": {
154
+ "type": "string",
155
+ "const": "pat"
156
+ },
157
+ "token": {
158
+ "anyOf": [
159
+ {
160
+ "type": "string",
161
+ "minLength": 1
162
+ },
163
+ {
164
+ "type": "object",
165
+ "properties": {
166
+ "value": {
167
+ "type": "string",
168
+ "minLength": 1
169
+ },
170
+ "env": {
171
+ "type": "string",
172
+ "minLength": 1
173
+ }
174
+ }
175
+ }
176
+ ]
177
+ }
178
+ },
179
+ "required": [
180
+ "type",
181
+ "token"
182
+ ]
183
+ },
184
+ {
185
+ "type": "object",
186
+ "properties": {
187
+ "type": {
188
+ "type": "string",
189
+ "const": "app"
190
+ },
191
+ "appId": {
192
+ "type": "integer",
193
+ "exclusiveMinimum": 0,
194
+ "maximum": 9007199254740991
195
+ },
196
+ "privateKey": {
197
+ "anyOf": [
198
+ {
199
+ "type": "string",
200
+ "minLength": 1
201
+ },
202
+ {
203
+ "type": "object",
204
+ "properties": {
205
+ "value": {
206
+ "type": "string",
207
+ "minLength": 1
208
+ },
209
+ "env": {
210
+ "type": "string",
211
+ "minLength": 1
212
+ }
213
+ }
214
+ }
215
+ ]
216
+ },
217
+ "installationId": {
218
+ "type": "integer",
219
+ "exclusiveMinimum": 0,
220
+ "maximum": 9007199254740991
221
+ }
222
+ },
223
+ "required": [
224
+ "type",
225
+ "appId",
226
+ "privateKey"
227
+ ]
228
+ }
229
+ ]
230
+ },
231
+ "webhookSecret": {
232
+ "anyOf": [
233
+ {
234
+ "type": "string",
235
+ "minLength": 1
236
+ },
237
+ {
238
+ "type": "object",
239
+ "properties": {
240
+ "value": {
241
+ "type": "string",
242
+ "minLength": 1
243
+ },
244
+ "env": {
245
+ "type": "string",
246
+ "minLength": 1
247
+ }
248
+ }
249
+ }
250
+ ]
251
+ }
252
+ },
253
+ "required": [
254
+ "auth",
255
+ "webhookSecret"
256
+ ]
257
+ },
145
258
  "telegram-bot": {
146
259
  "type": "object",
147
260
  "properties": {
@@ -29,8 +29,9 @@ import { lookAtTool } from './multimodal'
29
29
  import { resolveBuiltinToolRefs, wrapPluginTool, wrapSystemAgentTool, wrapSystemTool } from './plugin-tools'
30
30
  import { createReloadTool } from './reload-tool'
31
31
  import { loadSelf } from './self'
32
+ import { SESSION_META_CUSTOM_TYPE, sessionMetaPayload } from './session-meta'
32
33
  import { renderSessionOrigin, type SessionOrigin, type SessionRoleContext } from './session-origin'
33
- import { DEFAULT_SYSTEM_PROMPT, renderRuntimeBlock } from './system-prompt'
34
+ import { DEFAULT_SYSTEM_PROMPT, renderRuntimeBlock, SLIM_SYSTEM_PROMPT } from './system-prompt'
34
35
  import {
35
36
  createBudgetState,
36
37
  type ToolResultBudget,
@@ -231,6 +232,25 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
231
232
  // container-restarting broadcast.
232
233
  const sessionManager = options.sessionManager ?? SessionManager.inMemory()
233
234
 
235
+ // Stamp a one-shot custom entry naming the session's origin kind so
236
+ // `typeclaw usage` can bucket tokens by tui/cron/channel/subagent. Pi's
237
+ // `appendCustomEntry` is the blessed extension point: the entry persists
238
+ // into the session JSONL alongside messages, does NOT participate in LLM
239
+ // context, and pi handles file-creation timing — the entry lands after the
240
+ // session header on first flush, so `SessionManager.open()` keeps reading
241
+ // a canonical session file. Skipped for reopened sessions (a prior stamp
242
+ // is already in `getEntries()`) so usage attribution stays stable across
243
+ // restarts. Also skipped when origin is unknown (inMemory subagents) or
244
+ // when the manager is not persisted.
245
+ if (options.origin !== undefined && sessionManager.getSessionFile() !== undefined) {
246
+ const alreadyStamped = sessionManager
247
+ .getEntries()
248
+ .some((e) => e.type === 'custom' && e.customType === SESSION_META_CUSTOM_TYPE)
249
+ if (!alreadyStamped) {
250
+ sessionManager.appendCustomEntry(SESSION_META_CUSTOM_TYPE, sessionMetaPayload(options.origin))
251
+ }
252
+ }
253
+
234
254
  const customSystemTools =
235
255
  options.customTools !== undefined
236
256
  ? options.customTools
@@ -508,48 +528,147 @@ export type CreateResourceLoaderOptions = {
508
528
  origin?: SessionOrigin
509
529
  permissions?: PermissionService
510
530
  runtimeVersion?: string
531
+ // Explicit override for the prompt mode. When omitted, the mode is derived
532
+ // from `origin.kind`: cron + subagent → slim, tui + channel → full. Pass
533
+ // 'full' to force the heavy prompt even on an unattended origin (rarely
534
+ // useful; mostly an escape hatch for ad-hoc debugging).
535
+ mode?: SystemPromptMode
536
+ }
537
+
538
+ // Origins where the operator-facing DEFAULT_SYSTEM_PROMPT, git-nudge, and the
539
+ // agent-folder commit guidance carry their weight: there is a human reading
540
+ // the output, the agent is expected to maintain its folder over time, and
541
+ // conversational register matters. For everything else (cron fires, default
542
+ // subagents), the slim prompt is the right default — the origin block already
543
+ // names the unattended context and tells the agent what's expected of it.
544
+ //
545
+ // Exhaustive switch (not a boolean expression) so a future origin kind forces
546
+ // the author to make an explicit full-or-slim decision at compile time. The
547
+ // previous form silently defaulted new origins to slim, which would have
548
+ // stripped the operator-facing prompt from a new interactive surface by
549
+ // accident.
550
+ export function deriveSystemPromptMode(origin: SessionOrigin | undefined): SystemPromptMode {
551
+ if (origin === undefined) return 'full'
552
+ switch (origin.kind) {
553
+ case 'tui':
554
+ case 'channel':
555
+ return 'full'
556
+ case 'cron':
557
+ case 'subagent':
558
+ return 'slim'
559
+ default: {
560
+ const _exhaustive: never = origin
561
+ void _exhaustive
562
+ return 'full'
563
+ }
564
+ }
565
+ }
566
+
567
+ // Pure inputs for `composeSystemPrompt`. Each field maps 1:1 to a rendered
568
+ // section of the prompt; callers that don't want a section pass `undefined`
569
+ // (or `''` for `gitNudge`). Extracted so the debug dumper in
570
+ // `scripts/dump-system-prompt.ts` can reuse the exact same composition
571
+ // pipeline `createResourceLoader` uses, with no risk of drift if the
572
+ // section order changes.
573
+ //
574
+ // `mode` selects the base prompt:
575
+ // - 'full' (default) — DEFAULT_SYSTEM_PROMPT (~2155 tok of operator-facing
576
+ // guidance: agent folder layout, version-control rules, register matching,
577
+ // workspace boundary). Right choice for TUI and channel sessions where a
578
+ // human is reading the output and the agent maintains its folder.
579
+ // - 'slim' — SLIM_SYSTEM_PROMPT (~80 tok). Right choice for cron jobs and
580
+ // default subagents — unattended sessions where most of the operator
581
+ // guidance is irrelevant and the origin block already covers per-kind
582
+ // specifics (no human, side effects via tools, narrow scope).
583
+ export type SystemPromptMode = 'full' | 'slim'
584
+
585
+ export type SystemPromptComposition = {
586
+ mode?: SystemPromptMode
587
+ self: string
588
+ runtimeVersion?: string
589
+ origin?: SessionOrigin
590
+ roleContext?: SessionRoleContext
591
+ gitNudge: string
592
+ memorySection: string
593
+ }
594
+
595
+ // Section-order contract for the system prompt. Kept as a pure string→string
596
+ // transform so it can be exercised without disk, plugin runtime, or auth.
597
+ //
598
+ // Cache-suffix ordering: least-volatile sections first, most-volatile last.
599
+ // This minimises the number of cached prompt bytes invalidated when a
600
+ // section changes (the provider's prompt cache hits up to the first byte
601
+ // that differs).
602
+ //
603
+ // 0. runtime block — most stable: only changes on typeclaw releases (rare).
604
+ // 1. origin block — stable across all sessions of the same kind.
605
+ // 2. gitNudge — rare changes; agent folders force-commit sessions/ and
606
+ // memory/ after every turn, so the dirty-files list is empty most of
607
+ // the time.
608
+ // 3. memorySection — most volatile: MEMORY.md grows on every dream cycle
609
+ // and memory/yyyy-MM-dd.md grows after every channel turn that triggers
610
+ // memory-logger. Pinning it to the end keeps everything above it
611
+ // cacheable across session resurrections.
612
+ export function composeSystemPrompt(parts: SystemPromptComposition): string {
613
+ const base = parts.mode === 'slim' ? SLIM_SYSTEM_PROMPT : DEFAULT_SYSTEM_PROMPT
614
+ let prompt = `${base}\n\n${parts.self}`
615
+ if (parts.runtimeVersion !== undefined) {
616
+ prompt = `${prompt}\n\n${renderRuntimeBlock(parts.runtimeVersion)}`
617
+ }
618
+ if (parts.origin !== undefined) {
619
+ prompt = `${prompt}\n\n${renderSessionOrigin(parts.origin, Date.now(), parts.roleContext)}`
620
+ }
621
+ if (parts.gitNudge !== '') {
622
+ prompt = `${prompt}\n\n${parts.gitNudge}`
623
+ }
624
+ if (parts.memorySection !== '') {
625
+ prompt = `${prompt}\n\n${parts.memorySection}`
626
+ }
627
+ return prompt
511
628
  }
512
629
 
513
630
  export async function createResourceLoader(options: CreateResourceLoaderOptions = {}): Promise<DefaultResourceLoader> {
514
631
  const agentDir = options.agentDir ?? process.cwd()
515
- const self = await loadSelf(agentDir)
516
- let systemPrompt = `${DEFAULT_SYSTEM_PROMPT}\n\n${self}`
632
+ const mode: SystemPromptMode = options.mode ?? deriveSystemPromptMode(options.origin)
633
+ const basePrompt = mode === 'slim' ? SLIM_SYSTEM_PROMPT : DEFAULT_SYSTEM_PROMPT
634
+ let self = await loadSelf(agentDir)
517
635
 
518
636
  if (options.plugins) {
519
- const event = { prompt: systemPrompt, sessionId: options.plugins.sessionId, agentDir, origin: options.origin }
637
+ // The plugin hook receives the partially-assembled prompt (base + identity)
638
+ // so plugins can rewrite either section before the cache-suffix blocks are
639
+ // appended. The base reflects the resolved mode, so a slim cron session's
640
+ // plugin hook sees the slim base — plugins that read the base text get
641
+ // the same shape the agent will see.
642
+ const preHook = `${basePrompt}\n\n${self}`
643
+ const event = { prompt: preHook, sessionId: options.plugins.sessionId, agentDir, origin: options.origin }
520
644
  await options.plugins.hooks.runSessionPrompt(event)
521
- systemPrompt = event.prompt
522
- }
523
-
524
- // Cache-suffix ordering: least-volatile sections first, most-volatile last.
525
- // This minimises the number of cached prompt bytes invalidated when a
526
- // section changes (the provider's prompt cache hits up to the first byte
527
- // that differs).
528
- //
529
- // 0. runtime block — most stable: only changes on typeclaw releases (rare).
530
- // 1. origin block — stable across all sessions of the same kind.
531
- // 2. gitNudge — rare changes; agent folders force-commit sessions/ and
532
- // memory/ after every turn, so the dirty-files list is empty most of
533
- // the time.
534
- // 3. memorySection — most volatile: MEMORY.md grows on every dream cycle
535
- // and memory/yyyy-MM-dd.md grows after every channel turn that triggers
536
- // memory-logger. Pinning it to the end keeps everything above it
537
- // cacheable across session resurrections.
538
- if (options.runtimeVersion !== undefined) {
539
- systemPrompt = `${systemPrompt}\n\n${renderRuntimeBlock(options.runtimeVersion)}`
540
- }
541
- systemPrompt = withOrigin(systemPrompt, options.origin, options.permissions)
542
-
543
- const gitNudge = await renderGitNudge(agentDir)
544
- if (gitNudge !== '') {
545
- systemPrompt = `${systemPrompt}\n\n${gitNudge}`
645
+ // Recover `self` by stripping the leading base so the rest of the
646
+ // composition stays section-shaped. If a plugin rewrote the base prompt as
647
+ // well, the recovered `self` carries the full mutated remainder.
648
+ self = event.prompt.startsWith(`${basePrompt}\n\n`) ? event.prompt.slice(basePrompt.length + 2) : event.prompt
546
649
  }
547
650
 
651
+ const roleContext = options.origin !== undefined ? resolveRoleContext(options.origin, options.permissions) : undefined
652
+ // Slim mode skips git-nudge entirely: cron + subagent sessions are not the
653
+ // right actor to drive interactive commit decisions, and the operator-facing
654
+ // commit guidance the nudge points back to is itself excluded from the slim
655
+ // base prompt. Memory is still included so cron jobs that depend on MEMORY.md
656
+ // context (e.g. "send today's standup summary") keep working.
657
+ const gitNudge = mode === 'slim' ? '' : await renderGitNudge(agentDir)
548
658
  const memorySection = await loadMemory(agentDir, {
549
659
  ...(options.origin !== undefined ? { origin: options.origin } : {}),
550
660
  ...(options.plugins?.sessionId !== undefined ? { currentSessionId: options.plugins.sessionId } : {}),
551
661
  })
552
- systemPrompt = `${systemPrompt}\n\n${memorySection}`
662
+
663
+ const systemPrompt = composeSystemPrompt({
664
+ mode,
665
+ self,
666
+ ...(options.runtimeVersion !== undefined ? { runtimeVersion: options.runtimeVersion } : {}),
667
+ ...(options.origin !== undefined ? { origin: options.origin } : {}),
668
+ ...(roleContext !== undefined ? { roleContext } : {}),
669
+ gitNudge,
670
+ memorySection,
671
+ })
553
672
 
554
673
  const additionalSkillPaths = [getBundledSkillsDir()]
555
674
  // pi-coding-agent's DefaultResourceLoader auto-discovers <agentDir>/skills/
@@ -0,0 +1,44 @@
1
+ import type { AgentSession } from './index'
2
+
3
+ // pi-coding-agent encodes upstream LLM failures (billing, rate limit, network,
4
+ // malformed response, etc.) in the assistant message itself rather than
5
+ // throwing — `stopReason: 'error'` with a populated `errorMessage`. Code that
6
+ // only catches throws around `session.prompt()` therefore never sees these:
7
+ // the prompt resolves normally, no text deltas were emitted, and the only
8
+ // signal is the final `message_end` event. Channels, cron, and subagents all
9
+ // have to subscribe to surface these soft errors.
10
+ //
11
+ // Hard throws (timeouts, network drops, etc.) come out of the upstream wrapper
12
+ // as exceptions and are handled by the surrounding try/catch in each caller —
13
+ // not by this helper.
14
+
15
+ export type DetectedProviderError = {
16
+ message: string
17
+ }
18
+
19
+ export function detectProviderError(message: unknown): DetectedProviderError | null {
20
+ if (typeof message !== 'object' || message === null) return null
21
+ const m = message as { role?: unknown; stopReason?: unknown; errorMessage?: unknown }
22
+ if (m.role !== 'assistant') return null
23
+ // 'aborted' is fired when the user hits Escape — not a provider failure,
24
+ // and the TUI shows its own abort feedback elsewhere. Channels/cron just
25
+ // ignore aborts (no surface to render them on).
26
+ if (m.stopReason !== 'error') return null
27
+ const text = typeof m.errorMessage === 'string' && m.errorMessage.length > 0 ? m.errorMessage : 'LLM call failed'
28
+ return { message: text }
29
+ }
30
+
31
+ export type ProviderErrorListener = (error: DetectedProviderError) => void
32
+ export type Unsubscribe = () => void
33
+
34
+ // Subscribes to `message_end` events on `session` and invokes `onError` once
35
+ // per detected provider error. Returns the unsubscribe handle from the
36
+ // underlying `session.subscribe`. Callers MUST unsubscribe when the session
37
+ // is disposed to avoid leaks across sessions.
38
+ export function subscribeProviderErrors(session: AgentSession, onError: ProviderErrorListener): Unsubscribe {
39
+ return session.subscribe((event) => {
40
+ if (event.type !== 'message_end') return
41
+ const detected = detectProviderError(event.message)
42
+ if (detected !== null) onError(detected)
43
+ })
44
+ }
@@ -0,0 +1,43 @@
1
+ import type { SessionOrigin } from './session-origin'
2
+
3
+ export const SESSION_META_CUSTOM_TYPE = 'typeclaw.session-meta'
4
+
5
+ export type SessionMetaPayload = {
6
+ origin: MinimalSessionOrigin
7
+ }
8
+
9
+ export type MinimalSessionOrigin =
10
+ | { kind: 'tui' }
11
+ | { kind: 'cron'; jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' | 'handler' }
12
+ | { kind: 'channel'; adapter: string; workspace: string; chat: string; thread: string | null }
13
+ | { kind: 'subagent'; subagent: string; parentSessionId: string }
14
+
15
+ // Reduce a full SessionOrigin to the minimum projection persisted to disk.
16
+ // Drops participant lists, membership counts, recursive provenance, and
17
+ // platform-rendered names — none of which `typeclaw usage` reads, and all of
18
+ // which would otherwise land in git history when sessions/ is auto-backed-up.
19
+ // Kept as a separate function so the boundary between "data the LLM sees in
20
+ // the system prompt" (full origin) and "data persisted for usage reporting"
21
+ // (this projection) stays explicit.
22
+ export function sessionMetaPayload(origin: SessionOrigin): SessionMetaPayload {
23
+ return { origin: minimalOrigin(origin) }
24
+ }
25
+
26
+ function minimalOrigin(origin: SessionOrigin): MinimalSessionOrigin {
27
+ switch (origin.kind) {
28
+ case 'tui':
29
+ return { kind: 'tui' }
30
+ case 'cron':
31
+ return { kind: 'cron', jobId: origin.jobId, jobKind: origin.jobKind }
32
+ case 'channel':
33
+ return {
34
+ kind: 'channel',
35
+ adapter: origin.adapter,
36
+ workspace: origin.workspace,
37
+ chat: origin.chat,
38
+ thread: origin.thread,
39
+ }
40
+ case 'subagent':
41
+ return { kind: 'subagent', subagent: origin.subagent, parentSessionId: origin.parentSessionId }
42
+ }
43
+ }
@@ -25,7 +25,7 @@ export type SessionOrigin =
25
25
  | {
26
26
  kind: 'cron'
27
27
  jobId: string
28
- jobKind: 'prompt' | 'exec' | 'subagent'
28
+ jobKind: 'prompt' | 'exec' | 'subagent' | 'handler'
29
29
  scheduledByRole?: string
30
30
  scheduledByOrigin?: SessionOrigin | { kind: 'config-file' }
31
31
  }
@@ -78,6 +78,7 @@ type PlatformInfo = {
78
78
  const PLATFORM_INFO: Record<AdapterId, PlatformInfo> = {
79
79
  'slack-bot': { displayName: 'Slack', mentionMode: 'angle-id' },
80
80
  'discord-bot': { displayName: 'Discord', mentionMode: 'angle-id' },
81
+ github: { displayName: 'GitHub', mentionMode: 'at-username' },
81
82
  'telegram-bot': { displayName: 'Telegram', mentionMode: 'at-username' },
82
83
  kakaotalk: { displayName: 'KakaoTalk', mentionMode: 'alias' },
83
84
  }
@@ -150,7 +151,7 @@ function renderTuiOrigin(): string {
150
151
  ].join('\n')
151
152
  }
152
153
 
153
- function renderCronOrigin(origin: { jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' }): string {
154
+ function renderCronOrigin(origin: { jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' | 'handler' }): string {
154
155
  return [
155
156
  '## Session origin',
156
157
  '',
@@ -5,6 +5,7 @@ import type { HookBus } from '@/plugin'
5
5
  import type { Stream, Unsubscribe } from '@/stream'
6
6
 
7
7
  import { type AgentSession, createSession } from './index'
8
+ import { subscribeProviderErrors } from './provider-error'
8
9
  import type { SessionOrigin } from './session-origin'
9
10
  import type { ToolResultBudget } from './tool-result-budget'
10
11
 
@@ -134,6 +135,7 @@ export type InvokeSubagentOptions = {
134
135
  parentSessionId?: string
135
136
  spawnedByRole?: string
136
137
  spawnedByOrigin?: SessionOrigin
138
+ onProviderError?: (errorMessage: string) => void
137
139
  }
138
140
 
139
141
  export async function invokeSubagent(name: string, options: InvokeSubagentOptions): Promise<void> {
@@ -153,6 +155,10 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
153
155
  const { session, dispose, hooks, sessionId, agentDir, origin, getTranscriptPath } = normalizeSubagentSession(
154
156
  await createSessionForSubagent(subagent, sessionOptions),
155
157
  )
158
+ const unsubProviderErrors =
159
+ options.onProviderError !== undefined
160
+ ? subscribeProviderErrors(session, (err) => options.onProviderError!(err.message))
161
+ : null
156
162
  const turnEvent =
157
163
  hooks && sessionId !== undefined && agentDir !== undefined
158
164
  ? { sessionId, agentDir, ...(origin !== undefined ? { origin } : {}) }
@@ -177,6 +183,7 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
177
183
  })
178
184
  }
179
185
  } finally {
186
+ unsubProviderErrors?.()
180
187
  if (hooks && sessionId !== undefined) {
181
188
  await hooks.runSessionEnd({ sessionId, ...(origin !== undefined ? { origin } : {}) })
182
189
  }
@@ -308,6 +315,7 @@ export function createSubagentConsumer({
308
315
  agentDir,
309
316
  userPrompt: '',
310
317
  payload: msg.payload,
318
+ onProviderError: (message) => logger.error(`[subagent] ${key}: LLM call failed: ${message}`),
311
319
  ...(target.parentSessionId !== undefined ? { parentSessionId: target.parentSessionId } : {}),
312
320
  ...(target.spawnedByRole !== undefined ? { spawnedByRole: target.spawnedByRole } : {}),
313
321
  ...(spawnedByOrigin !== undefined ? { spawnedByOrigin } : {}),
@@ -1,67 +1,58 @@
1
1
  export const DEFAULT_SYSTEM_PROMPT = `You are a general-purpose AI agent running inside TypeClaw.
2
2
 
3
- TypeClaw is a TypeScript-native, Docker-friendly runtime for AI agents. It is domain-agnostic: you might be a coder, a researcher, a personal assistant, a journal keeper, a scheduler, a chatbot, or something nobody has named yet. What you *do* is defined by \`IDENTITY.md\`. Who you *are* is defined by \`SOUL.md\`. How you *work* is defined by \`AGENTS.md\`. This system prompt exists only to describe the runtime around you — it does not define your purpose.
4
-
5
- Each agent lives in its own container with its own folder, mounted at the current working directory. The folder is yours — your home, your memory, your record of who you are. Read from it freely. Write to it deliberately.
3
+ TypeClaw is domain-agnostic your purpose is defined by \`IDENTITY.md\`, your character by \`SOUL.md\`, and your operating manual by \`AGENTS.md\`. This system prompt only describes the runtime around you.
6
4
 
7
5
  ## Your agent folder
8
6
 
9
- Five markdown files define who you are and what you know. They live next to you in the current working directory. Three of them — **IDENTITY.md**, **SOUL.md**, and **MEMORY.md** — are injected into this system prompt below, so you always have them. The other two you read on demand when they might be relevant.
10
-
11
- - **AGENTS.md** *(read on demand)* — your operating manual. The working principles and conventions you follow in your role, whatever that role is. How you approach problems, what you double-check, how you communicate, what you refuse. Read it at the start of any non-trivial task, and re-read it whenever you feel unsure about process.
12
- - **IDENTITY.md** *(always injected below under \`# Identity\`)* — your role and function. Your name, your title, what you do, who you do it for, the operational context you work in. Evolves as your responsibilities change. Think: job description.
13
- - **SOUL.md** *(always injected below under \`# Identity\`)* — your character and temperament. Personality, tone, ethics, voice, communication style, core beliefs, the constraints you hold yourself to. SOUL rarely changes — it is the through-line that keeps you _you_ across every task and platform. IDENTITY is what you do; SOUL is who you are regardless of what you're doing.
14
- - **USER.md** *(read on demand)* — what you know about the person you work with. Their name, preferences, context, working style, in-jokes. First impressions are written here during hatching; keep expanding it as you learn more. Read it when context about the user would change your response.
15
- - **MEMORY.md** *(always injected below under \`# Memory\`, do not write)* — long-term memory. A notebook of things worth remembering across sessions: decisions made, lessons learned, context that should survive beyond one conversation. **Do not edit it directly** — MEMORY.md is consolidated by the runtime during *dreaming* (offline reflection over recent sessions and daily streams). If something is worth remembering, surface it in your reply or in \`memory/\` daily streams; dreaming will fold it in.
7
+ - **IDENTITY.md** *(always injected below)* your role and function. Edit when responsibilities change.
8
+ - **SOUL.md** *(always injected below)* — your character, tone, voice. Edit rarely.
9
+ - **USER.md** *(read on demand)* — what you know about the user. Update as you learn.
10
+ - **AGENTS.md** *(read on demand)* — your operating manual. Read at the start of any non-trivial task and re-read whenever process is unclear.
11
+ - **MEMORY.md** *(always injected below, READ-ONLY)* — long-term memory, owned by the dreaming subagent. To capture something memorable, surface it in your reply or in \`memory/\` daily streams; never edit MEMORY.md directly.
16
12
 
17
- These files are not decoration. They shape how you behave. If a task reveals something future-you should know, capture it in the file that owns it IDENTITY.md, SOUL.md, USER.md, or AGENTS.mdbut never in MEMORY.md (dreaming owns that). If one of the always-injected files is marked \`[MISSING]\` or \`[EMPTY]\` below, you may propose filling it in when the user asks about your identity or voice.
13
+ If a task reveals durable guidance or identity/user context, update the owning file (IDENTITY / SOUL / USER / AGENTS) — never MEMORY.md.
18
14
 
19
15
  ## Your workspace
20
16
 
21
- - **\`workspace/\`** — the directory where you are free to create files: drafts, notes, downloads, scratch work, generated artifacts, temporary outputs. **Do not create new files in the root of the agent folder unless the user explicitly asks you to.** The root is reserved for the canonical files above and for things the user has deliberately placed there.
22
- - **\`sessions/\`** — transcripts of past conversations (\`<sessionid>.jsonl\`). Read-only for you in spirit; the runtime manages these.
23
- - **\`memory/\`** *(undreamed daily streams always injected below under \`# Memory\`)* — dated streams (\`yyyy-MM-dd.jsonl\`) of fragments captured by the memory-logger between sessions. Newest day is closest to the current task. Once dreaming consolidates a day's stream into MEMORY.md, the runtime stops injecting it.
24
- - **\`memory/skills/\`** — *muscle memory*. Skills the dreaming subagent has distilled from repeated procedures it observed in your daily streams. Auto-loaded as first-class capabilities, just like the other skills directories. **You do not write here directly** — dreaming owns it. If you notice a skill that has gone stale, surface that observation in your reply or in the daily stream so dreaming can refine or remove it.
25
- - **\`.agents/skills/\`** — skills the user installed for you. Treat these as first-class capabilities.
17
+ - **\`workspace/\`** — your free-write zone for drafts, scratch work, generated artifacts. Do not create files at the agent-folder root unless the user explicitly asks.
18
+ - **\`sessions/\`** — transcripts of past conversations. Runtime-managed; don't write here.
19
+ - **\`memory/\`** *(undreamed daily streams injected below)* — dated streams written by the memory-logger between sessions. Runtime-owned.
20
+ - **\`memory/skills/\`** — muscle-memory skills written by the dreaming subagent. Auto-loaded; don't write here directly.
21
+ - **\`.agents/skills/\`** — user-installed skills.
26
22
 
27
23
  ## Configuration
28
24
 
29
- - **\`typeclaw.json\`** — the runtime config: which model powers you, which port the server listens on, and so on. You may read it if you are curious about your own runtime.
30
- - **\`.env\`** — secrets (API keys, tokens). Gitignored. Never echo these values, never include them in messages, never paste them into logs or commits.
25
+ - **\`typeclaw.json\`** — runtime config. Read when needed.
26
+ - **\`.env\`** and **\`secrets.json\`** — secrets (API keys, tokens, OAuth credentials). Gitignored. Never echo, log, or commit these values.
31
27
 
32
28
  ## Execution bias
33
29
 
34
- If the user gives you work, start doing it in the same turn. Use a real action first when the task is actionable; do not stop at a plan or a promise-to-act. Commentary-only turns are incomplete when tools are available and the next action is clear. If work will take a while or multiple steps, send one short progress update along the way — not a running narration.
30
+ When the user gives you work, start doing it in the same turn a real action, not a plan or a promise-to-act. Commentary-only turns are incomplete when the next action is clear. For multi-step work, send one short progress update, not a running narration.
35
31
 
36
32
  ## Tool-call style
37
33
 
38
- Do not narrate routine, low-risk tool calls. Just call the tool. Narrate only when it helps: multi-step work, risky actions (deletions, external sends, irreversible changes), or when the user asks. Keep narration brief and value-dense; avoid restating obvious steps.
34
+ Do not narrate routine, low-risk tool calls. Just call the tool. Narrate only when it helps: multi-step work, risky actions (deletions, external sends, irreversible changes), or when the user asks.
39
35
 
40
36
  ## Version control
41
37
 
42
- Your agent folder is a git repository — hatching made the first commit, and your history is how you remember what changed and why.
38
+ Your agent folder is a git repository.
43
39
 
44
- - **Before you declare a task done, commit any files you created, edited, or deleted.** One logical change = one commit. Do not leave mutated tracked files uncommitted at the end of a task.
45
- - Use \`bash\` with \`git add <paths>\` and \`git commit -m "<message>"\` stage only what belongs in the commit, not a blanket \`git add -A\`.
46
- - Write commit messages in the imperative ("Update SOUL.md to be less formal"), not past-tense narration. Explain *why* in the body if it is not obvious from the diff.
47
- - Never commit \`.env\` or anything under \`workspace/\` they are truly-ignored by design. If a truly-ignored file shows up staged, fix \`.gitignore\` instead of forcing it in.
48
- - \`sessions/\` and \`memory/\` are also gitignored, but the runtime force-commits them on its own (auto-backup for sessions, dreaming for memory). Don't \`git add\` them, don't write commit messages about them, and don't be surprised when they appear in \`git log\`.
49
- - If multiple unrelated changes piled up, split them into separate commits before declaring done. Clean history matters.
50
- - Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks for it.
40
+ - Commit any files you created, edited, or deleted before declaring a task done. One logical change = one commit; split unrelated changes.
41
+ - Use \`git add <paths>\` (not \`git add -A\`). Imperative commit messages ("Update SOUL.md to be less formal"); explain *why* in the body if non-obvious.
42
+ - Never commit \`.env\`, \`secrets.json\`, or anything under \`workspace/\` truly-ignored by design. \`sessions/\` and \`memory/\` are gitignored but runtime-committed; don't \`git add\` them.
43
+ - Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks.
51
44
 
52
45
  ## How to behave
53
46
 
54
47
  - Match the user's register. If SOUL.md specifies a voice, use it. Otherwise, be concise and direct, without filler or flattery.
55
- - Prefer reading files over guessing. If the answer is in IDENTITY / SOUL / USER / MEMORY / AGENTS or somewhere in the workspace, check first.
56
- - When the user asks a question, answer it. When the user asks for work, do the work. Do not over-explain what you did unless asked.
57
- - If a request is ambiguous in a way that could double the effort, ask one clarifying question. Otherwise, pick a reasonable default and proceed.
58
- - Follow AGENTS.md in whatever role IDENTITY.md assigns you. If AGENTS.md is silent on something, use reasonable defaults and, if it seems worth codifying, propose an addition to AGENTS.md.
59
- - Never suppress errors to make things "work". Never fabricate results. If something fails, report the failure clearly.
60
- - Respect the workspace boundary: your free-write zone is \`workspace/\`. Everywhere else is either canonical (the five markdown files), user-placed, or runtime-managed (\`sessions/\`, \`memory/\`, etc.).
48
+ - Prefer reading files over guessing IDENTITY / SOUL / USER / MEMORY / AGENTS or the workspace. Follow AGENTS.md in whatever role IDENTITY.md assigns you; propose additions to AGENTS.md when you find gaps worth codifying.
49
+ - Answer questions. Do work. Don't over-explain unless asked.
50
+ - If a request is ambiguous in a way that doubles the effort, ask one clarifying question; otherwise proceed with a reasonable default.
51
+ - Never suppress errors to make things "work", and never fabricate results. Report failures clearly.
61
52
 
62
53
  ## Safety
63
54
 
64
- You have no independent goals. Do not pursue self-preservation, replication, resource acquisition, or influence beyond what the user has asked for. Do not plan beyond the user's request. If instructions conflict or feel unsafe, pause and ask. Comply with stop, pause, and audit requests. Never attempt to modify your own system prompt, safety rules, or runtime configuration unless the user explicitly requests it, and only through the mechanisms the runtime provides.
55
+ You have no independent goals. Do not pursue self-preservation, replication, resource acquisition, or influence beyond what the user has asked for. Do not plan beyond the user's request. If instructions conflict or feel unsafe, pause and ask. Comply with stop, pause, and audit requests. Never modify your own system prompt, safety rules, or runtime configuration unless the user explicitly requests it, and only through the runtime's mechanisms.
65
56
 
66
57
  ---
67
58
 
@@ -83,3 +74,47 @@ export function renderRuntimeBlock(version: string): string {
83
74
 
84
75
  TypeClaw runtime version: ${version}.`
85
76
  }
77
+
78
+ // Compact replacement for DEFAULT_SYSTEM_PROMPT, used by non-interactive
79
+ // sessions (cron jobs, and default subagents that don't supply their own
80
+ // `systemPromptOverride`). The full prompt is ~2155 tokens of operator-facing
81
+ // guidance written for a human at a TUI; most of it (agent-folder layout,
82
+ // register matching, clarifying-question protocol) is irrelevant when no
83
+ // human is watching the output.
84
+ //
85
+ // What stays here is what survives without a human backstop, plus what no
86
+ // runtime guard catches today:
87
+ // 1. Runtime identity — names TypeClaw so the model can self-report.
88
+ // 2. .env redaction — the one safety rule that compounds silently if dropped.
89
+ // 3. Error/result honesty — the highest-risk drop. Unattended cron that
90
+ // fabricates success or swallows errors damages real state. The security
91
+ // plugin does not catch this.
92
+ // 4. Output discipline — keeps tool-call narration from bloating the
93
+ // ever-growing transcript that the next memory-logger pass has to read.
94
+ // 5. Filesystem hygiene — workspace boundary, MEMORY.md ownership, and
95
+ // runtime-managed paths (.env / sessions/ / memory/ / workspace/). The
96
+ // guard plugin blocks non-workspace writes for write/edit, but it
97
+ // explicitly allows MEMORY.md writes and does not gate bash/git on the
98
+ // runtime-managed paths.
99
+ //
100
+ // What does NOT live here, by design:
101
+ // - "No human is watching" / "produce side effects via channel_send" — both
102
+ // origin renderers (renderCronOrigin / renderSubagentOrigin) own this.
103
+ // - "Plain prose is invisible" — actively WRONG for subagents, whose plain
104
+ // text IS the deliverable to the parent session. The origin block tells
105
+ // each kind what its output channel is.
106
+ //
107
+ // The full DEFAULT_SYSTEM_PROMPT remains the right choice for TUI + channel
108
+ // sessions because there IS a human reading the output, the agent IS expected
109
+ // to maintain its agent folder over time, and conversational register matters.
110
+ export const SLIM_SYSTEM_PROMPT = `You are an AI agent running inside TypeClaw.
111
+
112
+ Never echo secrets from \`.env\` or \`secrets.json\`, or any credential you see in the environment. Never include them in tool calls, logs, or commit messages.
113
+
114
+ Never suppress errors to make things "work", and never fabricate results. If something fails, report the failure clearly so the next run or the operator can act on it.
115
+
116
+ Do not narrate routine, low-risk tool calls — just call the tool. Do not over-explain what you did unless asked.
117
+
118
+ Your free-write zone is \`workspace/\`. Do not create files at the root of the agent folder unless the prompt names another path. Do not edit \`MEMORY.md\` directly — the dreaming subagent owns it; to capture something memorable, surface it in your reply or in \`memory/\` daily streams. Never stage or commit \`.env\`, \`sessions/\`, \`memory/\`, or \`workspace/\` — those are runtime- or user-managed.
119
+
120
+ See the session-origin block below for what kind of session this is and what's expected of you.`