typeclaw 0.8.0 → 0.9.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 (92) hide show
  1. package/README.md +6 -6
  2. package/package.json +5 -3
  3. package/scripts/require-parallel.ts +41 -0
  4. package/src/agent/index.ts +55 -6
  5. package/src/agent/live-sessions.ts +34 -0
  6. package/src/agent/plugin-tools.ts +2 -0
  7. package/src/agent/session-meta.ts +21 -2
  8. package/src/agent/subagent-completion-reminder.ts +89 -0
  9. package/src/agent/subagents.ts +3 -2
  10. package/src/agent/system-prompt.ts +10 -8
  11. package/src/bundled-plugins/explorer/explorer.ts +2 -2
  12. package/src/bundled-plugins/guard/index.ts +14 -1
  13. package/src/bundled-plugins/guard/policies/managed-config.ts +43 -13
  14. package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +37 -0
  15. package/src/bundled-plugins/guard/policies/memory-topics-delete.ts +67 -0
  16. package/src/bundled-plugins/guard/policies/memory-topics-write.ts +33 -0
  17. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +8 -2
  18. package/src/bundled-plugins/guard/policy.ts +7 -0
  19. package/src/bundled-plugins/memory/README.md +76 -62
  20. package/src/bundled-plugins/memory/append-tool.ts +3 -2
  21. package/src/bundled-plugins/memory/citation-superset.ts +49 -11
  22. package/src/bundled-plugins/memory/citations.ts +19 -8
  23. package/src/bundled-plugins/memory/delete-tool.ts +57 -0
  24. package/src/bundled-plugins/memory/dreaming-state.ts +1 -1
  25. package/src/bundled-plugins/memory/dreaming.ts +364 -146
  26. package/src/bundled-plugins/memory/frontmatter.ts +165 -0
  27. package/src/bundled-plugins/memory/index.ts +236 -16
  28. package/src/bundled-plugins/memory/injection-plan.ts +15 -0
  29. package/src/bundled-plugins/memory/load-memory.ts +102 -103
  30. package/src/bundled-plugins/memory/load-shards.ts +156 -0
  31. package/src/bundled-plugins/memory/memory-logger.ts +16 -15
  32. package/src/bundled-plugins/memory/memory-retrieval.ts +105 -0
  33. package/src/bundled-plugins/memory/migration.ts +282 -1
  34. package/src/bundled-plugins/memory/paths.ts +42 -0
  35. package/src/bundled-plugins/memory/search-tool.ts +232 -0
  36. package/src/bundled-plugins/memory/secret-detector.ts +2 -2
  37. package/src/bundled-plugins/memory/shard-snapshot.ts +51 -0
  38. package/src/bundled-plugins/memory/slug.ts +59 -0
  39. package/src/bundled-plugins/memory/stream-io.ts +110 -1
  40. package/src/bundled-plugins/memory/strength.ts +3 -3
  41. package/src/bundled-plugins/memory/topics.ts +70 -16
  42. package/src/bundled-plugins/security/index.ts +24 -0
  43. package/src/bundled-plugins/security/permissions.ts +4 -0
  44. package/src/bundled-plugins/security/policies/cron-promotion.ts +349 -0
  45. package/src/bundled-plugins/security/policies/git-exfil.ts +2 -0
  46. package/src/bundled-plugins/security/policies/prompt-injection.ts +3 -0
  47. package/src/bundled-plugins/security/policies/role-promotion.ts +419 -0
  48. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +1 -0
  49. package/src/channels/adapters/kakaotalk-attachment.ts +7 -17
  50. package/src/channels/adapters/kakaotalk.ts +64 -37
  51. package/src/channels/adapters/slack-bot-classify.ts +2 -27
  52. package/src/channels/index.ts +5 -0
  53. package/src/channels/router.ts +201 -17
  54. package/src/channels/subagent-completion-bridge.ts +84 -0
  55. package/src/cli/builtins.ts +1 -0
  56. package/src/cli/index.ts +1 -0
  57. package/src/cli/init.ts +122 -14
  58. package/src/cli/inspect.ts +151 -0
  59. package/src/cron/consumer.ts +1 -1
  60. package/src/init/dockerfile.ts +268 -4
  61. package/src/init/hatching.ts +5 -6
  62. package/src/init/kakaotalk-auth.ts +6 -47
  63. package/src/init/validate-api-key.ts +121 -0
  64. package/src/inspect/index.ts +213 -0
  65. package/src/inspect/label.ts +50 -0
  66. package/src/inspect/live.ts +221 -0
  67. package/src/inspect/render.ts +163 -0
  68. package/src/inspect/replay.ts +265 -0
  69. package/src/inspect/session-list.ts +160 -0
  70. package/src/inspect/types.ts +110 -0
  71. package/src/plugin/hooks.ts +23 -1
  72. package/src/plugin/index.ts +2 -0
  73. package/src/plugin/manager.ts +1 -1
  74. package/src/plugin/registry.ts +1 -1
  75. package/src/plugin/types.ts +10 -0
  76. package/src/run/channel-session-factory.ts +7 -1
  77. package/src/run/index.ts +87 -21
  78. package/src/secrets/kakao-renewal.ts +3 -47
  79. package/src/server/index.ts +241 -60
  80. package/src/shared/index.ts +3 -0
  81. package/src/shared/protocol.ts +49 -0
  82. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +9 -9
  83. package/src/skills/typeclaw-claude-code/SKILL.md +57 -39
  84. package/src/skills/typeclaw-claude-code/references/stop-hook.md +2 -0
  85. package/src/skills/typeclaw-claude-code/references/tmux-driving.md +102 -16
  86. package/src/skills/typeclaw-config/SKILL.md +1 -1
  87. package/src/skills/typeclaw-cron/SKILL.md +1 -1
  88. package/src/skills/typeclaw-memory/SKILL.md +16 -163
  89. package/src/skills/typeclaw-permissions/SKILL.md +2 -2
  90. package/src/skills/typeclaw-plugins/SKILL.md +25 -14
  91. package/src/test-helpers/wait-for.ts +7 -1
  92. package/typeclaw.schema.json +7 -0
package/src/cli/init.ts CHANGED
@@ -6,6 +6,7 @@ import { defineCommand } from 'citty'
6
6
 
7
7
  import {
8
8
  KNOWN_PROVIDERS,
9
+ providerForModelRef,
9
10
  supportsApiKey as providerSupportsApiKey,
10
11
  supportsOAuth as providerSupportsOAuth,
11
12
  type KnownModelRef,
@@ -30,6 +31,7 @@ import {
30
31
  import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
31
32
  import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
32
33
  import { makeOAuthLoginRunner, type OAuthLoginResult } from '@/init/oauth-login'
34
+ import { API_KEY_DASHBOARD_URL, validateApiKey, type KeyValidationResult } from '@/init/validate-api-key'
33
35
 
34
36
  import { buildOAuthCallbacks } from './oauth-callbacks'
35
37
  import { c, done, errorLine, printSlackAppManifestSetup } from './ui'
@@ -237,14 +239,29 @@ export const init = defineCommand({
237
239
  }
238
240
 
239
241
  if (hatchingOk) {
240
- done({
241
- title: c.green('Hatched. Your agent is ready.'),
242
- hints: [
243
- { label: 'Attach TUI:', command: 'typeclaw tui' },
244
- { label: 'Follow logs:', command: 'typeclaw logs -f' },
245
- { label: 'Stop:', command: 'typeclaw stop' },
246
- ],
247
- })
242
+ const claimableChannel =
243
+ channelChoice !== 'none' && channelChoice !== 'github' ? channelDisplayName(channelChoice) : null
244
+ const hints: Array<{ label: string; command: string }> = []
245
+ if (claimableChannel !== null) {
246
+ hints.push({ label: 'Claim your agent:', command: 'typeclaw role claim' })
247
+ }
248
+ hints.push(
249
+ { label: 'Attach TUI:', command: 'typeclaw tui' },
250
+ { label: 'Follow logs:', command: 'typeclaw logs -f' },
251
+ { label: 'Stop:', command: 'typeclaw stop' },
252
+ { label: 'Diagnose issues:', command: 'typeclaw doctor' },
253
+ )
254
+ if (claimableChannel !== null) {
255
+ note(
256
+ [
257
+ `Your agent will not respond on ${claimableChannel} until you claim ownership.`,
258
+ `This prevents strangers from talking to it.`,
259
+ `Run \`typeclaw role claim\` to finish setup.`,
260
+ ].join('\n'),
261
+ 'Claim ownership before chatting',
262
+ )
263
+ }
264
+ done({ title: c.green('Hatched. Your agent is ready.'), hints })
248
265
  }
249
266
  },
250
267
  })
@@ -329,6 +346,7 @@ export interface WizardPrompts {
329
346
  initial: 'api-key' | 'oauth' | undefined,
330
347
  ) => Promise<StepResult<'api-key' | 'oauth'>>
331
348
  askApiKey: (provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]) => Promise<StepResult<string>>
349
+ validateApiKey: (providerId: KnownProviderId, key: string) => Promise<KeyValidationResult>
332
350
  pickVisionProvider: (
333
351
  options: ModelOption[],
334
352
  initial: KnownProviderId | undefined,
@@ -368,6 +386,7 @@ export const defaultWizardPrompts: WizardPrompts = {
368
386
  askReuseExistingKey,
369
387
  pickAuthMethod,
370
388
  askApiKey,
389
+ validateApiKey,
371
390
  pickVisionProvider,
372
391
  pickVisionModel,
373
392
  pickChannel,
@@ -502,12 +521,18 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
502
521
  }
503
522
 
504
523
  case 'enter-api-key': {
505
- const provider = KNOWN_PROVIDERS[state.providerId!]
524
+ const providerId = state.providerId!
525
+ const provider = KNOWN_PROVIDERS[providerId]
506
526
  const result = onResult(step, await prompts.askApiKey(provider))
507
527
  if (result.kind === 'back') {
508
528
  step = 'pick-auth-method'
509
529
  break
510
530
  }
531
+ const verdict = await runApiKeyValidation(prompts, providerId, result.value)
532
+ if (verdict === 'retry') {
533
+ step = 'enter-api-key'
534
+ break
535
+ }
511
536
  state.llmAuth = { kind: 'api-key', apiKey: result.value }
512
537
  step = stepAfterDefaultAuth(state)
513
538
  break
@@ -608,12 +633,18 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
608
633
  }
609
634
 
610
635
  case 'enter-vision-api-key': {
611
- const provider = KNOWN_PROVIDERS[state.visionProviderId!]
636
+ const providerId = state.visionProviderId!
637
+ const provider = KNOWN_PROVIDERS[providerId]
612
638
  const result = onResult(step, await prompts.askApiKey(provider))
613
639
  if (result.kind === 'back') {
614
640
  step = 'pick-vision-auth-method'
615
641
  break
616
642
  }
643
+ const verdict = await runApiKeyValidation(prompts, providerId, result.value)
644
+ if (verdict === 'retry') {
645
+ step = 'enter-vision-api-key'
646
+ break
647
+ }
617
648
  state.visionLlmAuth = { kind: 'api-key', apiKey: result.value }
618
649
  step = 'pick-channel'
619
650
  break
@@ -771,12 +802,12 @@ async function pickModelForProvider(
771
802
  providerId: KnownProviderId,
772
803
  initial: KnownModelRef | undefined,
773
804
  ): Promise<StepResult<ModelOption>> {
774
- const candidates = options.filter((o) => o.providerId === providerId)
805
+ const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
775
806
  const choice = await select<KnownModelRef>({
776
807
  message: `Pick a ${KNOWN_PROVIDERS[providerId].name} model`,
777
808
  options: candidates.map((o) => ({
778
809
  value: o.ref,
779
- label: o.modelName,
810
+ label: formatModelLabel(o),
780
811
  hint: formatModelHint(o),
781
812
  })),
782
813
  initialValue: initial ?? candidates[0]?.ref,
@@ -863,12 +894,12 @@ async function pickVisionModel(
863
894
  providerId: KnownProviderId,
864
895
  initial: KnownModelRef | undefined,
865
896
  ): Promise<StepResult<ModelOption>> {
866
- const candidates = options.filter((o) => o.providerId === providerId)
897
+ const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
867
898
  const choice = await select<KnownModelRef>({
868
899
  message: `Pick a vision-capable ${KNOWN_PROVIDERS[providerId].name} model`,
869
900
  options: candidates.map((o) => ({
870
901
  value: o.ref,
871
- label: o.modelName,
902
+ label: formatModelLabel(o),
872
903
  hint: formatModelHint(o),
873
904
  })),
874
905
  initialValue: initial ?? candidates[0]?.ref,
@@ -879,7 +910,60 @@ async function pickVisionModel(
879
910
  return value(picked)
880
911
  }
881
912
 
913
+ async function runApiKeyValidation(
914
+ prompts: WizardPrompts,
915
+ providerId: KnownProviderId,
916
+ key: string,
917
+ ): Promise<'accepted' | 'retry'> {
918
+ const provider = KNOWN_PROVIDERS[providerId]
919
+ const s = spinner()
920
+ s.start(`Checking your ${provider.name} key...`)
921
+ let result: KeyValidationResult
922
+ try {
923
+ result = await prompts.validateApiKey(providerId, key)
924
+ } catch {
925
+ s.stop(`Couldn't reach ${provider.name} to verify the key. Saving it anyway.`)
926
+ return 'accepted'
927
+ }
928
+ if (result.kind === 'ok') {
929
+ s.stop(`${provider.name} key looks good.`)
930
+ return 'accepted'
931
+ }
932
+ if (result.kind === 'skipped') {
933
+ s.stop(`Couldn't reach ${provider.name} to verify the key. Saving it anyway.`)
934
+ return 'accepted'
935
+ }
936
+ s.error(`${provider.name} rejected the key (HTTP ${result.status}).`)
937
+ const dashboardUrl = API_KEY_DASHBOARD_URL[providerId]
938
+ const lines = [
939
+ 'The provider says this key is not valid.',
940
+ 'Common causes: typo, expired key, wrong account, or pasting a project-scoped key.',
941
+ ]
942
+ if (dashboardUrl) {
943
+ lines.push('', `Get a fresh one at ${dashboardUrl}`)
944
+ }
945
+ note(lines.join('\n'), `${provider.name} key rejected`)
946
+ const choice = await select<'retry' | 'accept'>({
947
+ message: 'What do you want to do?',
948
+ options: [
949
+ { value: 'retry', label: 'Try a different key' },
950
+ { value: 'accept', label: 'Save this key anyway', hint: 'init continues, but the agent may fail to start' },
951
+ ],
952
+ initialValue: 'retry',
953
+ })
954
+ if (isCancel(choice) || choice === 'retry') return 'retry'
955
+ return 'accepted'
956
+ }
957
+
882
958
  async function askApiKey(provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]): Promise<StepResult<string>> {
959
+ const providerId = provider.id as KnownProviderId
960
+ const dashboardUrl = API_KEY_DASHBOARD_URL[providerId]
961
+ if (dashboardUrl) {
962
+ note(
963
+ [`Don't have a key yet?`, `Get one at ${dashboardUrl}`, `Then come back and paste it below.`].join('\n'),
964
+ `Get a ${provider.name} API key`,
965
+ )
966
+ }
883
967
  const apiKey = await password({
884
968
  message: `Put your ${provider.name} API key (will be saved to secrets.json)`,
885
969
  validate: (v) => (v && v.length > 0 ? undefined : 'API key is required'),
@@ -1371,6 +1455,30 @@ function uniqueProviders(options: ModelOption[]): KnownProviderId[] {
1371
1455
  return out
1372
1456
  }
1373
1457
 
1458
+ // Per-provider recommended model refs. Surfaces a "(Recommended)" suffix in
1459
+ // the picker label and floats the entry to the top of the list (which also
1460
+ // makes it the default `initialValue` when the caller has no prior choice).
1461
+ // Kept narrow on purpose: one recommendation per provider. gpt-5.4-mini is
1462
+ // listed under both `openai` and `openai-codex` because the same model is
1463
+ // the right default whether the user authenticates with an API key or with
1464
+ // a ChatGPT Plus/Pro subscription. claude-sonnet-4-6 follows Anthropic's
1465
+ // own current-tier guidance (see the model lineup notes in providers.ts).
1466
+ const RECOMMENDED_MODEL_REFS: ReadonlySet<KnownModelRef> = new Set<KnownModelRef>([
1467
+ 'openai/gpt-5.4-mini',
1468
+ 'openai-codex/gpt-5.4-mini',
1469
+ 'anthropic/claude-sonnet-4-6',
1470
+ ])
1471
+
1472
+ export function formatModelLabel(o: ModelOption): string {
1473
+ return RECOMMENDED_MODEL_REFS.has(o.ref) ? `${o.modelName} (Recommended)` : o.modelName
1474
+ }
1475
+
1476
+ export function sortRecommendedFirst(options: ModelOption[]): ModelOption[] {
1477
+ const recommended = options.filter((o) => RECOMMENDED_MODEL_REFS.has(o.ref))
1478
+ const rest = options.filter((o) => !RECOMMENDED_MODEL_REFS.has(o.ref))
1479
+ return [...recommended, ...rest]
1480
+ }
1481
+
1374
1482
  function formatModelHint(o: ModelOption): string {
1375
1483
  const parts: string[] = []
1376
1484
  if (o.contextWindow !== null) parts.push(`${(o.contextWindow / 1000).toFixed(0)}K ctx`)
@@ -0,0 +1,151 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
4
+ import { findAgentDir } from '@/init'
5
+ import { runInspect, streamLive, type LiveSourceFactory, type SessionSummary } from '@/inspect'
6
+ import { originLabel, shortSessionId } from '@/inspect/label'
7
+
8
+ import { cancel, c, errorLine, isCancel } from './ui'
9
+
10
+ export const inspectCommand = defineCommand({
11
+ meta: {
12
+ name: 'inspect',
13
+ description: 'replay a session transcript and tail live activity (host stage)',
14
+ },
15
+ args: {
16
+ session: {
17
+ type: 'positional',
18
+ description: 'session id or short prefix (omit to pick from a list)',
19
+ required: false,
20
+ },
21
+ filter: {
22
+ type: 'string',
23
+ description:
24
+ 'category filter: comma-separated meta/user/assistant/tool/error/done/broadcast/cron-fire; prefix with ! to exclude',
25
+ },
26
+ since: {
27
+ type: 'string',
28
+ description: 'only events newer than this (forms: 30s, 5m, 1h, 7d)',
29
+ },
30
+ json: {
31
+ type: 'boolean',
32
+ description: 'emit one JSON event per line; requires an explicit session id',
33
+ default: false,
34
+ },
35
+ follow: {
36
+ type: 'boolean',
37
+ description:
38
+ 'tail live activity after replay (default: true when the container is running); pass --no-follow to replay-then-exit',
39
+ default: true,
40
+ },
41
+ },
42
+ async run({ args }) {
43
+ const cwd = findAgentDir(process.cwd()) ?? process.cwd()
44
+ const color = useColor()
45
+ const sessionArg = typeof args.session === 'string' ? args.session : undefined
46
+ const filterArg = typeof args.filter === 'string' ? args.filter : undefined
47
+ const sinceArg = typeof args.since === 'string' ? args.since : undefined
48
+ const follow = args.follow !== false
49
+
50
+ const isJson = args.json === true
51
+ const liveSource = !follow || isJson ? undefined : await buildLiveSource(cwd)
52
+ const signal = installSigintAbort()
53
+
54
+ const result = await runInspect({
55
+ agentDir: cwd,
56
+ ...(sessionArg !== undefined ? { sessionIdOrPrefix: sessionArg } : {}),
57
+ ...(filterArg !== undefined ? { filter: filterArg } : {}),
58
+ ...(sinceArg !== undefined ? { since: sinceArg } : {}),
59
+ json: isJson,
60
+ color,
61
+ selectSession: clackSelect,
62
+ ...(liveSource !== undefined ? { liveSource } : {}),
63
+ signal,
64
+ stdout: (line) => process.stdout.write(`${line}\n`),
65
+ stderr: (line) => process.stderr.write(`${line}\n`),
66
+ })
67
+
68
+ if (!result.ok) {
69
+ process.stderr.write(`${errorLine(result.reason)}\n`)
70
+ process.exit(result.exitCode)
71
+ }
72
+ process.exit(result.exitCode)
73
+ },
74
+ })
75
+
76
+ async function buildLiveSource(cwd: string): Promise<LiveSourceFactory | undefined> {
77
+ const precheck = await requireContainerRunning({ cwd })
78
+ if (!precheck.ok) {
79
+ process.stderr.write(`${c.yellow('⚠')} ${precheck.reason}; tailing live events disabled\n`)
80
+ return undefined
81
+ }
82
+ const port = await resolveHostPort({ cwd })
83
+ const token = await resolveTuiToken({ cwd })
84
+ const baseUrl = new URL(`ws://127.0.0.1:${port}/inspect`)
85
+ if (token !== null) baseUrl.searchParams.set('token', token)
86
+ const url = baseUrl.toString()
87
+ return ({ sessionId, sinceMs, signal, onSubscribed }) =>
88
+ streamLive({
89
+ url,
90
+ sessionId,
91
+ ...(sinceMs !== undefined ? { sinceMs } : {}),
92
+ ...(signal !== undefined ? { signal } : {}),
93
+ ...(onSubscribed !== undefined ? { onSubscribed } : {}),
94
+ })
95
+ }
96
+
97
+ function installSigintAbort(): AbortSignal {
98
+ const ctrl = new AbortController()
99
+ const onSig = (): void => {
100
+ ctrl.abort()
101
+ }
102
+ process.once('SIGINT', onSig)
103
+ process.once('SIGTERM', onSig)
104
+ return ctrl.signal
105
+ }
106
+
107
+ function useColor(): boolean {
108
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return false
109
+ if (process.env.FORCE_COLOR === '0') return false
110
+ if (process.env.FORCE_COLOR) return true
111
+ return Boolean(process.stdout.isTTY)
112
+ }
113
+
114
+ async function clackSelect(sessions: SessionSummary[]): Promise<SessionSummary | null> {
115
+ const { select } = await import('@clack/prompts')
116
+ const picked = await select<string>({
117
+ message: `Pick a session to inspect (showing ${sessions.length})`,
118
+ options: sessions.map((s) => ({
119
+ value: s.sessionId,
120
+ label: formatRowLabel(s),
121
+ ...(s.firstPrompt !== null ? { hint: truncate(s.firstPrompt, 60) } : { hint: '(no prompt)' }),
122
+ })),
123
+ initialValue: sessions[0]?.sessionId,
124
+ })
125
+ if (isCancel(picked)) {
126
+ cancel('Cancelled.')
127
+ return null
128
+ }
129
+ return sessions.find((s) => s.sessionId === picked) ?? null
130
+ }
131
+
132
+ function formatRowLabel(s: SessionSummary): string {
133
+ const id = shortSessionId(s.sessionId)
134
+ const label = s.origin === null ? '(unknown origin)' : originLabel(s.origin)
135
+ const when = formatRelative(s.mtimeMs)
136
+ return `${c.cyan(id)} ${label} ${c.dim(when)}`
137
+ }
138
+
139
+ function formatRelative(ms: number): string {
140
+ const diff = Date.now() - ms
141
+ if (diff < 60_000) return 'just now'
142
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
143
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
144
+ return `${Math.floor(diff / 86_400_000)}d ago`
145
+ }
146
+
147
+ function truncate(text: string, max: number): string {
148
+ const oneline = text.replace(/\s+/g, ' ').trim()
149
+ if (oneline.length <= max) return oneline
150
+ return `${oneline.slice(0, max)}…`
151
+ }
@@ -195,7 +195,7 @@ async function runPromptOnce(
195
195
  }
196
196
  : undefined
197
197
  if (created.hooks && turnEvent !== undefined) {
198
- await created.hooks.runSessionTurnStart(turnEvent)
198
+ await created.hooks.runSessionTurnStart({ ...turnEvent, userPrompt: job.prompt })
199
199
  }
200
200
  // Bridge the CronSession wrapper into the AgentSession surface the
201
201
  // fallback helper expects: