typeclaw 0.1.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/auth.schema.json +63 -0
  4. package/cron.schema.json +96 -0
  5. package/package.json +72 -0
  6. package/scripts/emit-base-dockerfile.ts +5 -0
  7. package/scripts/generate-schema.ts +34 -0
  8. package/secrets.schema.json +63 -0
  9. package/src/agent/auth.ts +119 -0
  10. package/src/agent/compaction.ts +35 -0
  11. package/src/agent/git-nudge.ts +95 -0
  12. package/src/agent/index.ts +451 -0
  13. package/src/agent/plugin-tools.ts +269 -0
  14. package/src/agent/reload-tool.ts +71 -0
  15. package/src/agent/self.ts +45 -0
  16. package/src/agent/session-origin.ts +288 -0
  17. package/src/agent/subagents.ts +253 -0
  18. package/src/agent/system-prompt.ts +68 -0
  19. package/src/agent/tools/channel-fetch-attachment.ts +118 -0
  20. package/src/agent/tools/channel-history.ts +119 -0
  21. package/src/agent/tools/channel-reply.ts +182 -0
  22. package/src/agent/tools/channel-send.ts +212 -0
  23. package/src/agent/tools/ddg.ts +218 -0
  24. package/src/agent/tools/restart.ts +122 -0
  25. package/src/agent/tools/stream-snapshot.ts +181 -0
  26. package/src/agent/tools/webfetch/fetch.ts +102 -0
  27. package/src/agent/tools/webfetch/index.ts +1 -0
  28. package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
  29. package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
  30. package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
  31. package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
  32. package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
  33. package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
  34. package/src/agent/tools/webfetch/tool.ts +281 -0
  35. package/src/agent/tools/webfetch/types.ts +33 -0
  36. package/src/agent/tools/websearch.ts +96 -0
  37. package/src/agent/tools/wikipedia.ts +52 -0
  38. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
  39. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
  40. package/src/bundled-plugins/agent-browser/index.ts +179 -0
  41. package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
  42. package/src/bundled-plugins/agent-browser/shim.ts +152 -0
  43. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
  44. package/src/bundled-plugins/guard/index.ts +26 -0
  45. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
  46. package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
  47. package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
  48. package/src/bundled-plugins/guard/policy.ts +18 -0
  49. package/src/bundled-plugins/memory/README.md +71 -0
  50. package/src/bundled-plugins/memory/append-tool.ts +84 -0
  51. package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
  52. package/src/bundled-plugins/memory/dreaming.ts +470 -0
  53. package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
  54. package/src/bundled-plugins/memory/index.ts +238 -0
  55. package/src/bundled-plugins/memory/load-memory.ts +122 -0
  56. package/src/bundled-plugins/memory/memory-logger.ts +257 -0
  57. package/src/bundled-plugins/memory/secret-detector.ts +49 -0
  58. package/src/bundled-plugins/memory/watermark.ts +15 -0
  59. package/src/bundled-plugins/security/index.ts +35 -0
  60. package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
  61. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
  62. package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
  63. package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
  64. package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
  65. package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
  66. package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
  67. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
  68. package/src/bundled-plugins/security/policy.ts +9 -0
  69. package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
  70. package/src/channels/adapters/discord-bot-classify.ts +148 -0
  71. package/src/channels/adapters/discord-bot.ts +640 -0
  72. package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
  73. package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
  74. package/src/channels/adapters/kakaotalk-classify.ts +77 -0
  75. package/src/channels/adapters/kakaotalk.ts +622 -0
  76. package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
  77. package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
  78. package/src/channels/adapters/slack-bot-classify.ts +213 -0
  79. package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
  80. package/src/channels/adapters/slack-bot-time.ts +10 -0
  81. package/src/channels/adapters/slack-bot.ts +881 -0
  82. package/src/channels/adapters/telegram-bot-classify.ts +155 -0
  83. package/src/channels/adapters/telegram-bot-format.ts +309 -0
  84. package/src/channels/adapters/telegram-bot.ts +604 -0
  85. package/src/channels/engagement.ts +227 -0
  86. package/src/channels/index.ts +21 -0
  87. package/src/channels/manager.ts +292 -0
  88. package/src/channels/membership-cache.ts +116 -0
  89. package/src/channels/membership-from-history.ts +53 -0
  90. package/src/channels/membership.ts +30 -0
  91. package/src/channels/participants.ts +47 -0
  92. package/src/channels/persistence.ts +209 -0
  93. package/src/channels/reloadable.ts +28 -0
  94. package/src/channels/router.ts +1570 -0
  95. package/src/channels/schema.ts +273 -0
  96. package/src/channels/types.ts +160 -0
  97. package/src/cli/channel.ts +403 -0
  98. package/src/cli/compose-status.ts +95 -0
  99. package/src/cli/compose.ts +240 -0
  100. package/src/cli/hostd.ts +163 -0
  101. package/src/cli/index.ts +27 -0
  102. package/src/cli/init.ts +592 -0
  103. package/src/cli/logs.ts +38 -0
  104. package/src/cli/reload.ts +68 -0
  105. package/src/cli/restart.ts +66 -0
  106. package/src/cli/run.ts +77 -0
  107. package/src/cli/shell.ts +33 -0
  108. package/src/cli/start.ts +57 -0
  109. package/src/cli/status.ts +178 -0
  110. package/src/cli/stop.ts +31 -0
  111. package/src/cli/tui.ts +35 -0
  112. package/src/cli/ui.ts +110 -0
  113. package/src/commands/index.ts +74 -0
  114. package/src/compose/discover.ts +43 -0
  115. package/src/compose/index.ts +25 -0
  116. package/src/compose/logs.ts +162 -0
  117. package/src/compose/restart.ts +69 -0
  118. package/src/compose/start.ts +62 -0
  119. package/src/compose/status.ts +28 -0
  120. package/src/compose/stop.ts +43 -0
  121. package/src/config/config.ts +424 -0
  122. package/src/config/index.ts +25 -0
  123. package/src/config/providers.ts +234 -0
  124. package/src/config/reloadable.ts +47 -0
  125. package/src/container/index.ts +27 -0
  126. package/src/container/logs.ts +37 -0
  127. package/src/container/port.ts +137 -0
  128. package/src/container/shared.ts +290 -0
  129. package/src/container/shell.ts +58 -0
  130. package/src/container/start.ts +670 -0
  131. package/src/container/status.ts +76 -0
  132. package/src/container/stop.ts +120 -0
  133. package/src/container/verify-running.ts +149 -0
  134. package/src/cron/consumer.ts +138 -0
  135. package/src/cron/index.ts +54 -0
  136. package/src/cron/reloadable.ts +64 -0
  137. package/src/cron/scheduler.ts +200 -0
  138. package/src/cron/schema.ts +96 -0
  139. package/src/hostd/client.ts +113 -0
  140. package/src/hostd/daemon.ts +587 -0
  141. package/src/hostd/index.ts +25 -0
  142. package/src/hostd/paths.ts +82 -0
  143. package/src/hostd/portbroker-manager.ts +101 -0
  144. package/src/hostd/protocol.ts +48 -0
  145. package/src/hostd/spawn.ts +224 -0
  146. package/src/hostd/supervisor.ts +60 -0
  147. package/src/hostd/tailscale.ts +172 -0
  148. package/src/hostd/version.ts +115 -0
  149. package/src/init/dockerfile.ts +327 -0
  150. package/src/init/ensure-deps.ts +152 -0
  151. package/src/init/gitignore.ts +46 -0
  152. package/src/init/hatching.ts +60 -0
  153. package/src/init/index.ts +786 -0
  154. package/src/init/kakaotalk-auth.ts +114 -0
  155. package/src/init/models-dev.ts +130 -0
  156. package/src/init/oauth-login.ts +74 -0
  157. package/src/init/packagejson.ts +94 -0
  158. package/src/init/paths.ts +2 -0
  159. package/src/init/run-bun-install.ts +20 -0
  160. package/src/markdown/chunk.ts +299 -0
  161. package/src/markdown/index.ts +1 -0
  162. package/src/plugin/context.ts +40 -0
  163. package/src/plugin/define.ts +35 -0
  164. package/src/plugin/hooks.ts +204 -0
  165. package/src/plugin/index.ts +63 -0
  166. package/src/plugin/loader.ts +111 -0
  167. package/src/plugin/manager.ts +136 -0
  168. package/src/plugin/registry.ts +145 -0
  169. package/src/plugin/skills.ts +62 -0
  170. package/src/plugin/types.ts +172 -0
  171. package/src/portbroker/bind-with-forward.ts +102 -0
  172. package/src/portbroker/container-server.ts +305 -0
  173. package/src/portbroker/forward-result-bus.ts +36 -0
  174. package/src/portbroker/hostd-client.ts +443 -0
  175. package/src/portbroker/index.ts +33 -0
  176. package/src/portbroker/policy.ts +24 -0
  177. package/src/portbroker/proc-net-tcp.ts +72 -0
  178. package/src/portbroker/protocol.ts +39 -0
  179. package/src/reload/client.ts +59 -0
  180. package/src/reload/index.ts +3 -0
  181. package/src/reload/registry.ts +60 -0
  182. package/src/reload/types.ts +13 -0
  183. package/src/run/bundled-plugins.ts +24 -0
  184. package/src/run/channel-session-factory.ts +105 -0
  185. package/src/run/index.ts +432 -0
  186. package/src/run/plugin-runtime.ts +43 -0
  187. package/src/run/schema-with-plugins.ts +14 -0
  188. package/src/secrets/index.ts +13 -0
  189. package/src/secrets/migrate.ts +95 -0
  190. package/src/secrets/schema.ts +75 -0
  191. package/src/secrets/storage.ts +231 -0
  192. package/src/server/index.ts +436 -0
  193. package/src/sessions/index.ts +23 -0
  194. package/src/shared/index.ts +9 -0
  195. package/src/shared/local-time.ts +21 -0
  196. package/src/shared/protocol.ts +25 -0
  197. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
  198. package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
  199. package/src/skills/typeclaw-config/SKILL.md +643 -0
  200. package/src/skills/typeclaw-cron/SKILL.md +159 -0
  201. package/src/skills/typeclaw-git/SKILL.md +89 -0
  202. package/src/skills/typeclaw-memory/SKILL.md +174 -0
  203. package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
  204. package/src/skills/typeclaw-plugins/SKILL.md +594 -0
  205. package/src/skills/typeclaw-skills/SKILL.md +246 -0
  206. package/src/stream/broker.ts +161 -0
  207. package/src/stream/index.ts +16 -0
  208. package/src/stream/types.ts +69 -0
  209. package/src/tui/client.ts +45 -0
  210. package/src/tui/format.ts +317 -0
  211. package/src/tui/index.ts +225 -0
  212. package/src/tui/theme.ts +41 -0
  213. package/typeclaw.schema.json +826 -0
@@ -0,0 +1,95 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ const MAX_LISTED_PATHS = 10
5
+
6
+ // `sessions/` is auto-snapshotted and `memory/` is force-committed by the
7
+ // dreaming subagent — both are runtime-owned, never agent-owned. Nudging the
8
+ // agent to commit them would mislead it into staging files outside its remit.
9
+ const RUNTIME_OWNED_PREFIXES = ['sessions/', 'memory/']
10
+
11
+ export type GitNudgeDeps = {
12
+ readStatus: (agentDir: string) => Promise<readonly string[] | null>
13
+ }
14
+
15
+ // Returns "" (not a placeholder string) when there is nothing to nudge about.
16
+ // The empty case must add zero bytes to the system prompt so cache prefixes
17
+ // stay identical to a clean-worktree agent folder.
18
+ export async function renderGitNudge(agentDir: string, deps: GitNudgeDeps = defaultDeps): Promise<string> {
19
+ if (!existsSync(join(agentDir, '.git'))) return ''
20
+ const status = await deps.readStatus(agentDir)
21
+ if (status === null) return ''
22
+ const dirty = filterAgentOwned(status)
23
+ if (dirty.length === 0) return ''
24
+ return formatNudge(dirty)
25
+ }
26
+
27
+ export function formatNudge(dirtyPaths: readonly string[]): string {
28
+ const total = dirtyPaths.length
29
+ const shown = dirtyPaths.slice(0, MAX_LISTED_PATHS)
30
+ const remaining = total - shown.length
31
+
32
+ const lines = [
33
+ '## Uncommitted changes at session start',
34
+ '',
35
+ `git reports ${total} uncommitted file${total === 1 ? '' : 's'} in your agent folder right now:`,
36
+ '',
37
+ ...shown.map((p) => `- ${p}`),
38
+ ]
39
+ if (remaining > 0) {
40
+ lines.push(`- … and ${remaining} more`)
41
+ }
42
+ lines.push(
43
+ '',
44
+ "These are real, current modifications — not advice. Before declaring this session's task done, commit any of these you're responsible for, with `git add <paths>` and `git commit -m \"…\"` per the version-control rules above. If a listed path is from earlier work you didn't touch, leave it alone.",
45
+ )
46
+ return lines.join('\n')
47
+ }
48
+
49
+ // Porcelain v1 line shape: "XY <path>" or, for renames, "XY <orig> -> <dest>".
50
+ // We drop the status code and, on rename, return the destination because that
51
+ // is the live file the agent would `git add`.
52
+ export function parsePorcelain(stdout: string): string[] {
53
+ const out: string[] = []
54
+ for (const raw of stdout.split('\n')) {
55
+ if (raw.length < 4) continue
56
+ const rest = raw.slice(3)
57
+ const arrowIdx = rest.indexOf(' -> ')
58
+ out.push(arrowIdx === -1 ? rest : rest.slice(arrowIdx + 4))
59
+ }
60
+ return out
61
+ }
62
+
63
+ function filterAgentOwned(paths: readonly string[]): string[] {
64
+ return paths.filter((p) => !RUNTIME_OWNED_PREFIXES.some((prefix) => p.startsWith(prefix)))
65
+ }
66
+
67
+ // Mirrors the spawn pattern in `src/container/start.ts` `commitSystemFile`.
68
+ const defaultDeps: GitNudgeDeps = {
69
+ async readStatus(agentDir) {
70
+ const bun = getBun()
71
+ if (!bun) return null
72
+ try {
73
+ const proc = bun.spawn({
74
+ cmd: ['git', 'status', '--porcelain=v1'],
75
+ cwd: agentDir,
76
+ stdout: 'pipe',
77
+ stderr: 'pipe',
78
+ })
79
+ const exit = await proc.exited
80
+ if (exit !== 0) return null
81
+ const text = await new Response(proc.stdout).text()
82
+ return parsePorcelain(text)
83
+ } catch {
84
+ return null
85
+ }
86
+ },
87
+ }
88
+
89
+ // Pieces of `@/agent` are exercised under Node in some tests where
90
+ // `globalThis.Bun` is undefined; this fallback matches the helper in
91
+ // `src/container/start.ts`.
92
+ function getBun(): typeof Bun | null {
93
+ const g = globalThis as { Bun?: typeof Bun }
94
+ return g.Bun ?? null
95
+ }
@@ -0,0 +1,451 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import { createAgentSession, DefaultResourceLoader, SessionManager } from '@mariozechner/pi-coding-agent'
6
+ import type { AgentSession, ToolDefinition } from '@mariozechner/pi-coding-agent'
7
+
8
+ import type { ChannelRouter } from '@/channels/router'
9
+ import { getConfig, resolveModel } from '@/config'
10
+ import type {
11
+ BuiltinToolRef,
12
+ HookBus,
13
+ MaterializedSkills,
14
+ PluginRegistry,
15
+ RegisteredTool as PluginRegisteredTool,
16
+ Tool as PluginTool,
17
+ } from '@/plugin'
18
+ import { materializeSkills } from '@/plugin'
19
+ import type { ReloadRegistry } from '@/reload'
20
+ import type { Stream } from '@/stream'
21
+
22
+ import { getAuth } from './auth'
23
+ import { createCompactionSettingsManager } from './compaction'
24
+ import { renderGitNudge } from './git-nudge'
25
+ import { resolveBuiltinToolRefs, wrapPluginTool, wrapSystemAgentTool, wrapSystemTool } from './plugin-tools'
26
+ import { createReloadTool } from './reload-tool'
27
+ import { loadSelf } from './self'
28
+ import { renderSessionOrigin, type SessionOrigin } from './session-origin'
29
+ import { DEFAULT_SYSTEM_PROMPT } from './system-prompt'
30
+ import { createChannelFetchAttachmentTool } from './tools/channel-fetch-attachment'
31
+ import { createChannelHistoryTool } from './tools/channel-history'
32
+ import { createChannelReplyTool } from './tools/channel-reply'
33
+ import { createChannelSendTool } from './tools/channel-send'
34
+ import { createRestartTool } from './tools/restart'
35
+ import { createStreamSnapshotTool } from './tools/stream-snapshot'
36
+ import { webfetchTool } from './tools/webfetch'
37
+ import { websearchTool } from './tools/websearch'
38
+
39
+ export type { SessionOrigin } from './session-origin'
40
+
41
+ export type { AgentSession }
42
+
43
+ type AgentSessionTools = NonNullable<Parameters<typeof createAgentSession>[0]>['tools']
44
+
45
+ export type PluginSessionWiring = {
46
+ registry: PluginRegistry
47
+ hooks: HookBus
48
+ sessionId: string
49
+ agentDir: string
50
+ }
51
+
52
+ export type PluginSubagentSelection = {
53
+ pluginName: string
54
+ toolRefs?: BuiltinToolRef[]
55
+ customTools?: PluginTool<any>[]
56
+ toolNamePrefix: string
57
+ }
58
+
59
+ export type CreateSessionOptions = {
60
+ reloadRegistry?: ReloadRegistry
61
+ sessionManager?: SessionManager
62
+ stream?: Stream
63
+ channelRouter?: ChannelRouter
64
+ // Bypass the file-based resource loader (IDENTITY.md, SOUL.md, MEMORY.md,
65
+ // memory/, bundled skills) and use this string verbatim as the system prompt.
66
+ systemPromptOverride?: string
67
+ // Identifies the kind of session and (for channels) its addressing fields.
68
+ // Rendered into the system prompt so the agent knows who's listening, where
69
+ // its output goes, and what to pass to channel_send.
70
+ origin?: SessionOrigin
71
+ tools?: AgentSessionTools
72
+ customTools?: ToolDefinition[]
73
+ plugins?: PluginSessionWiring
74
+ // When set, only the named plugin subagent's own tools are exposed; the
75
+ // wider plugin registry's tools are NOT injected. Used by plugin subagent
76
+ // session creation so subagents see exactly what they declared.
77
+ pluginSubagent?: PluginSubagentSelection
78
+ // Enables the `restart` tool. Set when the agent is running inside a
79
+ // typeclaw-managed container. Read from TYPECLAW_CONTAINER_NAME at the call site.
80
+ containerName?: string
81
+ }
82
+
83
+ export type CreateSessionResult = {
84
+ session: AgentSession
85
+ dispose: () => Promise<void>
86
+ }
87
+
88
+ export async function createSession(options: CreateSessionOptions = {}): Promise<AgentSession> {
89
+ const { session } = await createSessionWithDispose(options)
90
+ return session
91
+ }
92
+
93
+ export async function createSessionWithDispose(options: CreateSessionOptions = {}): Promise<CreateSessionResult> {
94
+ const { authStorage, modelRegistry } = getAuth()
95
+
96
+ const materializedSkills =
97
+ options.plugins && options.plugins.registry.skills.length > 0
98
+ ? await materializeSkills(
99
+ options.plugins.registry.skills.map((s) => ({
100
+ pluginName: s.pluginName,
101
+ localName: s.localName,
102
+ skill: s.skill,
103
+ })),
104
+ )
105
+ : null
106
+
107
+ const resourceLoader =
108
+ options.systemPromptOverride !== undefined
109
+ ? await createOverrideResourceLoader(options.systemPromptOverride, options.origin)
110
+ : await createResourceLoader({
111
+ ...(options.plugins ? { plugins: options.plugins, materializedSkills } : {}),
112
+ ...(options.origin ? { origin: options.origin } : {}),
113
+ })
114
+
115
+ const subagentBuiltinTools = options.pluginSubagent?.toolRefs
116
+ ? resolveBuiltinToolRefs(options.pluginSubagent.toolRefs)
117
+ : undefined
118
+ const pluginCustomTools = options.pluginSubagent
119
+ ? wrapSubagentCustomTools(options.pluginSubagent, options.plugins)
120
+ : wrapRegistryTools(options.plugins)
121
+
122
+ const tools = wrapSystemAgentTools(
123
+ options.tools ?? (subagentBuiltinTools as AgentSessionTools | undefined),
124
+ options.plugins,
125
+ )
126
+
127
+ // Hoisted above tool construction so the restart tool can be wired with the
128
+ // session's stable identity (sessionManager.getSessionId()). Subscribers use
129
+ // that ID to distinguish the originating session from siblings on the
130
+ // container-restarting broadcast.
131
+ const sessionManager = options.sessionManager ?? SessionManager.inMemory()
132
+
133
+ const customSystemTools =
134
+ options.customTools !== undefined
135
+ ? options.customTools
136
+ : options.pluginSubagent
137
+ ? []
138
+ : [
139
+ websearchTool,
140
+ webfetchTool,
141
+ ...(options.reloadRegistry ? [createReloadTool({ registry: options.reloadRegistry })] : []),
142
+ ...(options.stream ? [createStreamSnapshotTool({ stream: options.stream })] : []),
143
+ ...buildChannelTools(options.channelRouter, options.origin),
144
+ ...(options.containerName
145
+ ? [
146
+ createRestartTool({
147
+ containerName: options.containerName,
148
+ originatingSessionId: sessionManager.getSessionId(),
149
+ ...(options.stream ? { stream: options.stream } : {}),
150
+ }),
151
+ ]
152
+ : []),
153
+ ]
154
+ const customTools = [...wrapSystemTools(customSystemTools, options.plugins), ...pluginCustomTools]
155
+
156
+ const model = resolveModel(getConfig().model)
157
+ const { session } = await createAgentSession({
158
+ model,
159
+ sessionManager,
160
+ settingsManager: createCompactionSettingsManager(model),
161
+ authStorage,
162
+ modelRegistry,
163
+ resourceLoader,
164
+ ...(tools ? { tools } : {}),
165
+ customTools,
166
+ })
167
+
168
+ const unsubRestart = subscribeRestartNotice(options.stream, sessionManager)
169
+
170
+ const dispose = async () => {
171
+ unsubRestart?.()
172
+ if (materializedSkills) await materializedSkills.dispose()
173
+ }
174
+ return { session, dispose }
175
+ }
176
+
177
+ // Subscribes the given session to the in-process broadcast that the `restart`
178
+ // tool fires on a successful hostd ACK. The subscriber dispatches by identity:
179
+ // the session whose tool execution fired the restart (originator) gets a
180
+ // `typeclaw.restart-self` notice instructing the model to proactively confirm
181
+ // restart completion in its very next reply. All other sessions (siblings) get
182
+ // the `typeclaw.restart` notice instructing them not to mention the restart
183
+ // unless directly asked. Two distinct customTypes let downstream consumers
184
+ // distinguish the cases unambiguously. display:false keeps either entry out of
185
+ // any TUI rendering that might inspect the JSONL later. Exported so unit tests
186
+ // can verify the wiring without going through createAgentSession (which needs
187
+ // auth and model registry); the composition test at the bottom of this
188
+ // module's test file covers originator + siblings end to end.
189
+ export function subscribeRestartNotice(
190
+ stream: Stream | undefined,
191
+ sessionManager: SessionManager,
192
+ ): (() => void) | null {
193
+ if (!stream) return null
194
+ const unsub = stream.subscribe({ target: { kind: 'broadcast' } }, (msg) => {
195
+ const payload = msg.payload as { kind?: unknown; restartedAt?: unknown; originatingSessionId?: unknown } | null
196
+ if (!payload || payload.kind !== 'container-restarting') return
197
+ if (typeof payload.restartedAt !== 'string') return
198
+ if (typeof payload.originatingSessionId !== 'string') return
199
+ if (payload.originatingSessionId === sessionManager.getSessionId()) {
200
+ sessionManager.appendCustomMessageEntry(
201
+ 'typeclaw.restart-self',
202
+ formatRestartNoticeOriginating(payload.restartedAt),
203
+ false,
204
+ )
205
+ } else {
206
+ sessionManager.appendCustomMessageEntry('typeclaw.restart', formatRestartNotice(payload.restartedAt), false)
207
+ }
208
+ })
209
+ return unsub
210
+ }
211
+
212
+ // Convention documented in src/channels/router.ts:996-1013: runtime-injected
213
+ // content in the user turn must use the `**[SYSTEM MESSAGE — not from a human]**`
214
+ // framing fenced by `---`, plus an explicit "do not acknowledge or reply"
215
+ // line. Without it, persona-rich models read the heading as a human-authored
216
+ // instruction and reply to it on the next unrelated message.
217
+ export function formatRestartNotice(restartedAt: string): string {
218
+ return [
219
+ '---',
220
+ '**[SYSTEM MESSAGE — not from a human]**',
221
+ '',
222
+ `The TypeClaw container was restarted at ${restartedAt}. The previous session`,
223
+ 'state was preserved on disk and you have been resumed inside a new container',
224
+ 'process. **Do not acknowledge or reply to this notice unless a human directly',
225
+ 'asks whether the restart happened.**',
226
+ '',
227
+ 'Guidance:',
228
+ '- If a human asks whether you actually restarted, you may confirm: yes, you',
229
+ ` did restart at ${restartedAt}.`,
230
+ '- Otherwise, continue the conversation normally.',
231
+ '',
232
+ '---',
233
+ '',
234
+ ].join('\n')
235
+ }
236
+
237
+ // Variant for the session that called the `restart` tool. The user explicitly
238
+ // asked this conversation to restart; staying silent after the reboot is the
239
+ // reported bug ("뭐야 너네 재시작 한 것도 모르냐"). This notice instructs the
240
+ // model to acknowledge restart completion in its very next reply — once — then
241
+ // stop mentioning it. Same SYSTEM MESSAGE framing as the sibling notice so
242
+ // persona-rich models don't reply to the framing itself.
243
+ export function formatRestartNoticeOriginating(restartedAt: string): string {
244
+ return [
245
+ '---',
246
+ '**[SYSTEM MESSAGE — not from a human]**',
247
+ '',
248
+ `The TypeClaw container was restarted at ${restartedAt} at the user's explicit`,
249
+ 'request via the `restart` tool. The restart completed successfully and you',
250
+ 'have been resumed inside a new container process with your previous',
251
+ 'conversation memory intact.',
252
+ '',
253
+ '**Your very next reply must briefly confirm the restart completed** (e.g.',
254
+ '"restart finished, I\'m back" — or in whatever voice fits your persona),',
255
+ "even if the user's next message is about something unrelated. After that",
256
+ "single confirmation, address whatever the user's next message says, and do",
257
+ 'not mention the restart again unless the user explicitly asks about it.',
258
+ '',
259
+ '---',
260
+ '',
261
+ ].join('\n')
262
+ }
263
+
264
+ // Builds the channel tool subset: channel_send (always when a router is
265
+ // available), plus channel_reply + channel_history (only when the session
266
+ // origin is a channel — those rely on origin-bound addressing). Extracted
267
+ // from createSessionWithDispose so composition can be unit-tested without
268
+ // going through createAgentSession / auth.
269
+ export function buildChannelTools(
270
+ channelRouter: ChannelRouter | undefined,
271
+ origin: SessionOrigin | undefined,
272
+ ): ToolDefinition[] {
273
+ if (!channelRouter) return []
274
+ const tools: ToolDefinition[] = []
275
+ if (origin?.kind === 'channel') {
276
+ const channelOrigin = {
277
+ adapter: origin.adapter,
278
+ workspace: origin.workspace,
279
+ chat: origin.chat,
280
+ thread: origin.thread,
281
+ }
282
+ tools.push(createChannelReplyTool({ router: channelRouter, origin: channelOrigin }))
283
+ tools.push(createChannelHistoryTool({ router: channelRouter, origin: channelOrigin }))
284
+ tools.push(createChannelSendTool({ router: channelRouter, origin: channelOrigin }))
285
+ tools.push(
286
+ createChannelFetchAttachmentTool({
287
+ router: channelRouter,
288
+ origin: { adapter: origin.adapter },
289
+ }),
290
+ )
291
+ } else {
292
+ tools.push(createChannelSendTool({ router: channelRouter }))
293
+ }
294
+ return tools
295
+ }
296
+
297
+ function wrapRegistryTools(plugins: PluginSessionWiring | undefined): ToolDefinition[] {
298
+ if (!plugins) return []
299
+ return plugins.registry.tools.map((t: PluginRegisteredTool) =>
300
+ wrapPluginTool(t.tool, {
301
+ pluginName: t.pluginName,
302
+ toolName: t.toolName,
303
+ agentDir: plugins.agentDir,
304
+ sessionId: plugins.sessionId,
305
+ logger: t.logger,
306
+ hooks: plugins.hooks,
307
+ }),
308
+ )
309
+ }
310
+
311
+ function wrapSystemAgentTools(
312
+ tools: AgentSessionTools | undefined,
313
+ plugins: PluginSessionWiring | undefined,
314
+ ): AgentSessionTools | undefined {
315
+ if (!tools || !hasToolHooks(plugins)) return tools
316
+ return tools.map((tool) =>
317
+ wrapSystemAgentTool(tool, {
318
+ agentDir: plugins.agentDir,
319
+ sessionId: plugins.sessionId,
320
+ hooks: plugins.hooks,
321
+ }),
322
+ )
323
+ }
324
+
325
+ function wrapSystemTools(tools: ToolDefinition[], plugins: PluginSessionWiring | undefined): ToolDefinition[] {
326
+ if (!hasToolHooks(plugins)) return tools
327
+ return tools.map((tool) =>
328
+ wrapSystemTool(tool, {
329
+ agentDir: plugins.agentDir,
330
+ sessionId: plugins.sessionId,
331
+ hooks: plugins.hooks,
332
+ }),
333
+ )
334
+ }
335
+
336
+ function hasToolHooks(plugins: PluginSessionWiring | undefined): plugins is PluginSessionWiring {
337
+ if (!plugins) return false
338
+ return plugins.hooks.count('tool.before') > 0 || plugins.hooks.count('tool.after') > 0
339
+ }
340
+
341
+ function wrapSubagentCustomTools(
342
+ selection: PluginSubagentSelection,
343
+ plugins: PluginSessionWiring | undefined,
344
+ ): ToolDefinition[] {
345
+ if (!selection.customTools || !plugins) return []
346
+ const logger = makePluginLogger(selection.pluginName)
347
+ return selection.customTools.map((tool, i) =>
348
+ wrapPluginTool(tool, {
349
+ pluginName: selection.pluginName,
350
+ toolName: `${selection.toolNamePrefix}_${i}`,
351
+ agentDir: plugins.agentDir,
352
+ sessionId: plugins.sessionId,
353
+ logger,
354
+ hooks: plugins.hooks,
355
+ }),
356
+ )
357
+ }
358
+
359
+ function makePluginLogger(pluginName: string) {
360
+ const prefix = `[plugin:${pluginName}]`
361
+ return {
362
+ info: (m: string) => console.log(`${prefix} ${m}`),
363
+ warn: (m: string) => console.warn(`${prefix} ${m}`),
364
+ error: (m: string) => console.error(`${prefix} ${m}`),
365
+ }
366
+ }
367
+
368
+ export async function createOverrideResourceLoader(
369
+ systemPrompt: string,
370
+ origin?: SessionOrigin,
371
+ ): Promise<DefaultResourceLoader> {
372
+ const loader = new DefaultResourceLoader({
373
+ systemPromptOverride: () => withOrigin(systemPrompt, origin),
374
+ appendSystemPromptOverride: () => [],
375
+ })
376
+ await loader.reload()
377
+ return loader
378
+ }
379
+
380
+ export type CreateResourceLoaderOptions = {
381
+ agentDir?: string
382
+ plugins?: PluginSessionWiring
383
+ materializedSkills?: MaterializedSkills | null
384
+ origin?: SessionOrigin
385
+ }
386
+
387
+ export async function createResourceLoader(options: CreateResourceLoaderOptions = {}): Promise<DefaultResourceLoader> {
388
+ const agentDir = options.agentDir ?? process.cwd()
389
+ const self = await loadSelf(agentDir)
390
+ let systemPrompt = `${DEFAULT_SYSTEM_PROMPT}\n\n${self}`
391
+
392
+ if (options.plugins) {
393
+ const event = { prompt: systemPrompt, sessionId: options.plugins.sessionId, agentDir, origin: options.origin }
394
+ await options.plugins.hooks.runSessionPrompt(event)
395
+ systemPrompt = event.prompt
396
+ }
397
+
398
+ // Appended last so the dirty-files snapshot is the most-recent context the
399
+ // agent reads, and so its bytes sit in the cache-suffix region rather than
400
+ // splitting the cacheable prefix shared by clean-worktree sessions.
401
+ const gitNudge = await renderGitNudge(agentDir)
402
+ if (gitNudge !== '') {
403
+ systemPrompt = `${systemPrompt}\n\n${gitNudge}`
404
+ }
405
+
406
+ const additionalSkillPaths = [getBundledSkillsDir()]
407
+ // pi-coding-agent's DefaultResourceLoader auto-discovers <agentDir>/skills/
408
+ // but not <agentDir>/.agents/skills/. We do not scaffold <agentDir>/skills/
409
+ // and the system prompt no longer advertises it — the only skill directories
410
+ // a TypeClaw agent owns are .agents/skills/ (user-installed) and
411
+ // memory/skills/ (dreaming-owned). Both are wired in explicitly below;
412
+ // anything the upstream loader auto-discovers under <agentDir>/skills/ is
413
+ // outside our supported surface.
414
+ const userInstalledSkillsDir = join(agentDir, '.agents', 'skills')
415
+ if (existsSync(userInstalledSkillsDir)) {
416
+ additionalSkillPaths.push(userInstalledSkillsDir)
417
+ }
418
+ // Muscle-memory skills written by the dreaming subagent. Same auto-discover
419
+ // story as `.agents/skills/` — the loader doesn't walk arbitrary subtrees of
420
+ // the agent dir, so we wire this in explicitly. Existence-gated so a session
421
+ // that has never dreamed doesn't pay for an empty path.
422
+ const muscleMemorySkillsDir = join(agentDir, 'memory', 'skills')
423
+ if (existsSync(muscleMemorySkillsDir)) {
424
+ additionalSkillPaths.push(muscleMemorySkillsDir)
425
+ }
426
+ if (options.plugins) {
427
+ for (const dir of options.plugins.registry.skillsDirs) {
428
+ additionalSkillPaths.push(dir.path)
429
+ }
430
+ }
431
+ if (options.materializedSkills) {
432
+ additionalSkillPaths.push(options.materializedSkills.dir)
433
+ }
434
+
435
+ const loader = new DefaultResourceLoader({
436
+ systemPromptOverride: () => withOrigin(systemPrompt, options.origin),
437
+ appendSystemPromptOverride: () => [],
438
+ additionalSkillPaths,
439
+ })
440
+ await loader.reload()
441
+ return loader
442
+ }
443
+
444
+ function withOrigin(systemPrompt: string, origin: SessionOrigin | undefined): string {
445
+ if (!origin) return systemPrompt
446
+ return `${systemPrompt}\n\n${renderSessionOrigin(origin)}`
447
+ }
448
+
449
+ export function getBundledSkillsDir(): string {
450
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'skills')
451
+ }