kimaki 0.20.1 → 0.22.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 (64) hide show
  1. package/dist/agent-model.e2e.test.js +6 -4
  2. package/dist/cli-commands/project.js +148 -24
  3. package/dist/cli-commands/send.js +48 -3
  4. package/dist/cli-runner.js +86 -7
  5. package/dist/cli-runner.test.js +28 -1
  6. package/dist/commands/btw.js +7 -3
  7. package/dist/commands/new-worktree.js +14 -2
  8. package/dist/commands/session.js +66 -13
  9. package/dist/commands/tasks.js +104 -19
  10. package/dist/commands/user-command.js +3 -2
  11. package/dist/database.js +14 -0
  12. package/dist/db.js +1 -0
  13. package/dist/discord-bot.js +65 -9
  14. package/dist/interaction-handler.js +6 -15
  15. package/dist/message-preprocessing.js +9 -2
  16. package/dist/queue-advanced-e2e-setup.js +22 -0
  17. package/dist/queue-delete-message.e2e.test.js +75 -0
  18. package/dist/schema.js +3 -0
  19. package/dist/session-handler/thread-runtime-state.js +9 -0
  20. package/dist/session-handler/thread-session-runtime.js +49 -1
  21. package/dist/system-message.js +50 -63
  22. package/dist/system-message.test.js +65 -61
  23. package/dist/task-runner.js +18 -3
  24. package/dist/task-schedule.js +4 -0
  25. package/dist/thread-message-queue.e2e.test.js +4 -4
  26. package/dist/voice-message.e2e.test.js +1 -1
  27. package/dist/worktree-lifecycle.e2e.test.js +70 -1
  28. package/dist/worktrees.js +63 -0
  29. package/package.json +11 -4
  30. package/skills/egaki/SKILL.md +26 -80
  31. package/skills/goke/SKILL.md +27 -20
  32. package/skills/holocron/SKILL.md +72 -8
  33. package/skills/new-skill/SKILL.md +87 -0
  34. package/skills/playwriter/SKILL.md +4 -4
  35. package/skills/spiceflow/SKILL.md +2 -0
  36. package/src/agent-model.e2e.test.ts +10 -4
  37. package/src/cli-commands/project.ts +183 -26
  38. package/src/cli-commands/send.ts +64 -5
  39. package/src/cli-runner.test.ts +39 -1
  40. package/src/cli-runner.ts +115 -6
  41. package/src/commands/btw.ts +9 -2
  42. package/src/commands/new-worktree.ts +14 -1
  43. package/src/commands/session.ts +79 -17
  44. package/src/commands/tasks.ts +119 -19
  45. package/src/commands/user-command.ts +3 -2
  46. package/src/database.ts +24 -0
  47. package/src/db.ts +1 -0
  48. package/src/discord-bot.ts +80 -8
  49. package/src/interaction-handler.ts +7 -19
  50. package/src/message-preprocessing.ts +11 -2
  51. package/src/queue-advanced-e2e-setup.ts +23 -0
  52. package/src/queue-delete-message.e2e.test.ts +102 -0
  53. package/src/schema.sql +1 -0
  54. package/src/schema.ts +3 -0
  55. package/src/session-handler/thread-runtime-state.ts +19 -0
  56. package/src/session-handler/thread-session-runtime.ts +75 -0
  57. package/src/system-message.test.ts +73 -61
  58. package/src/system-message.ts +63 -62
  59. package/src/task-runner.ts +34 -3
  60. package/src/task-schedule.ts +6 -0
  61. package/src/thread-message-queue.e2e.test.ts +4 -4
  62. package/src/voice-message.e2e.test.ts +1 -1
  63. package/src/worktree-lifecycle.e2e.test.ts +88 -1
  64. package/src/worktrees.ts +74 -0
@@ -14,6 +14,32 @@ describe('system-message', () => {
14
14
  expect(message).toContain('- failed commands');
15
15
  expect(message).toContain('<callout accent="#f59e0b">');
16
16
  });
17
+ test('includes parent session context when parentSessionId is set', () => {
18
+ const message = getOpencodeSystemMessage({
19
+ sessionId: 'ses_child',
20
+ channelId: 'chan_123',
21
+ parentSessionId: 'ses_parent',
22
+ });
23
+ expect(message).toContain('Your parent OpenCode session ID is: ses_parent');
24
+ expect(message).toContain("kimaki send --session ses_parent --prompt 'your update here' --agent <current_agent>");
25
+ expect(message).toContain('Do NOT message the parent session unless the user explicitly asks you to.');
26
+ expect(message).toContain('--parent-session ses_child');
27
+ });
28
+ test('omits parent session system block by default (btw/task/fork cache)', () => {
29
+ // /btw, task subagents, and normal sessions must not get a parent block in
30
+ // the system message. That block is opt-in via --parent-session only.
31
+ const message = getOpencodeSystemMessage({
32
+ sessionId: 'ses_btw_or_task',
33
+ channelId: 'chan_123',
34
+ threadId: 'thread_123',
35
+ guildId: 'guild_123',
36
+ });
37
+ expect(message).not.toContain('Your parent OpenCode session ID is:');
38
+ expect(message).not.toContain('Do NOT message the parent session unless the user explicitly asks you to.');
39
+ // Spawn examples still mention --parent-session for agents that start
40
+ // children via kimaki send; that is not the same as injecting a parent.
41
+ expect(message).toContain('--parent-session ses_btw_or_task');
42
+ });
17
43
  test('keeps the system prompt session-scoped', () => {
18
44
  const message = getOpencodeSystemMessage({
19
45
  sessionId: 'ses_123',
@@ -111,7 +137,7 @@ describe('system-message', () => {
111
137
 
112
138
  To archive the current Discord thread (hide it from sidebar) and stop the session, run:
113
139
 
114
- kimaki session archive --session ses_123
140
+ kimaki session archive thread_123 (or --session ses_123)
115
141
 
116
142
  Only do this when the user explicitly asks to close or archive the thread, and only after your final message.
117
143
 
@@ -139,9 +165,10 @@ describe('system-message', () => {
139
165
 
140
166
  To start a new thread/session in this channel pro-grammatically, run:
141
167
 
142
- kimaki send --channel chan_123 --prompt 'your prompt here' --agent <current_agent> --user '<discord-user-id>'
168
+ kimaki send --channel chan_123 --prompt 'your prompt here' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
143
169
 
144
170
  You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
171
+ ALWAYS pass \`--parent-session ses_123\` (your current session ID) when starting a new session from this one. The child system message will include the parent session ID so it can message back only if the user asks.
145
172
  Prefer passing the current agent with \`--agent <current_agent>\` so spawned or scheduled sessions keep the same agent unless you are intentionally switching. Replace \`<current_agent>\` with the value from the per-turn \`Current agent\` reminder.
146
173
  When writing \`kimaki send\` shell commands, use single quotes around \`--prompt\`, \`--user\`, \`--send-at\`, and other literal arguments so backticks inside prompts are not interpreted by the shell. Prefer \`--user '<discord-user-id>'\` over \`--user 'name'\` because name lookup depends on optional Server Members Intent.
147
174
 
@@ -155,13 +182,13 @@ describe('system-message', () => {
155
182
 
156
183
  kimaki send --thread <thread_id> --prompt 'follow-up prompt' --agent <current_agent>
157
184
 
158
- Use this when you already have the Discord thread ID.
185
+ Use this when you already have the Discord thread ID. Prefer \`--thread\` over \`--session\` because thread IDs work across machines while session IDs only resolve on the machine that created the session.
159
186
 
160
- To send to the thread associated with a known session:
187
+ To send to the thread associated with a known session (same machine only):
161
188
 
162
189
  kimaki send --session <session_id> --prompt 'follow-up prompt' --agent <current_agent>
163
190
 
164
- Use this when you have the OpenCode session ID.
191
+ Use this when you only have the OpenCode session ID and the session was created on this machine.
165
192
 
166
193
  Use --notify-only to create a notification thread without starting an AI session:
167
194
 
@@ -169,26 +196,32 @@ describe('system-message', () => {
169
196
 
170
197
  Use --user with a Discord user ID or raw mention to add a specific Discord user to the new thread:
171
198
 
172
- kimaki send --channel chan_123 --prompt 'Review the latest CI failure' --agent <current_agent> --user '<discord-user-id>'
199
+ kimaki send --channel chan_123 --prompt 'Review the latest CI failure' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
173
200
 
174
201
  Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
175
202
 
176
- kimaki send --channel chan_123 --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent> --user '<discord-user-id>'
203
+ kimaki send --channel chan_123 --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
177
204
 
178
205
  Use --cwd to start a session in an existing project subfolder or git worktree directory:
179
206
 
180
- kimaki send --channel chan_123 --prompt 'Run the restricted task' --cwd /path/to/project/restricted-task --agent <current_agent> --user '<discord-user-id>'
207
+ kimaki send --channel chan_123 --prompt 'Run the restricted task' --cwd /path/to/project/restricted-task --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
181
208
 
182
209
  Important:
210
+ - ALWAYS pass \`--parent-session ses_123\` when spawning a new session from this one so the child knows who started it.
183
211
  - NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
184
212
  - Use \`--cwd\` to reuse an existing project subfolder or worktree directory. Use \`--worktree\` to create a new worktree.
185
213
  - The prompt passed to \`--worktree\` is the task for the new thread running inside that worktree.
186
214
  - Do NOT tell that prompt to "create a new worktree" again, or it can create recursive worktree threads.
187
215
  - Ask the new session to operate on its current checkout only (e.g. "validate current worktree", "run checks in this repo").
188
216
 
217
+ Use --file to attach local files (images, text files, PDFs) to the message:
218
+
219
+ kimaki send --channel chan_123 --prompt 'Review this screenshot' --file /path/to/screenshot.png --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
220
+ kimaki send --thread <thread_id> --prompt 'Here is the error log' --file ./error.log --file ./stack-trace.txt --agent <current_agent>
221
+
189
222
  Use --agent to specify which agent to use for the session:
190
223
 
191
- kimaki send --channel chan_123 --prompt 'Plan the refactor of the auth module' --agent plan --user '<discord-user-id>'
224
+ kimaki send --channel chan_123 --prompt 'Plan the refactor of the auth module' --agent plan --parent-session ses_123 --user '<discord-user-id>'
192
225
 
193
226
 
194
227
  Available agents:
@@ -200,7 +233,7 @@ describe('system-message', () => {
200
233
  You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
201
234
 
202
235
  kimaki send --thread <thread_id> --prompt '/review fix the auth module' --agent <current_agent>
203
- kimaki send --channel chan_123 --prompt '/build-cmd update dependencies' --agent <current_agent> --user '<discord-user-id>'
236
+ kimaki send --channel chan_123 --prompt '/build-cmd update dependencies' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
204
237
 
205
238
  The command name must match a registered opencode command. If the command is not recognized, the prompt is sent as plain text to the model. This works for both new threads (\`--channel\`) and existing threads (\`--thread\`/\`--session\`).
206
239
 
@@ -216,8 +249,8 @@ describe('system-message', () => {
216
249
 
217
250
  Use \`--send-at\` to schedule a one-time or recurring task:
218
251
 
219
- kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --user '<discord-user-id>'
220
- kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --user '<discord-user-id>'
252
+ kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
253
+ kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
221
254
 
222
255
  ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
223
256
  When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
@@ -226,6 +259,7 @@ describe('system-message', () => {
226
259
  - \`--notify-only\` to create a reminder thread without auto-starting a session
227
260
  - \`--worktree\` to create the scheduled thread as a worktree session (only if the user explicitly asks for a worktree)
228
261
  - \`--agent\` and \`--model\` to control scheduled session behavior
262
+ - \`--parent-session\` to pass this session as parent of the scheduled child
229
263
  - \`--user\` to add a specific user to the scheduled thread
230
264
 
231
265
  \`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
@@ -241,7 +275,7 @@ describe('system-message', () => {
241
275
  - Without \`--user\`, there is no guaranteed direct user mention path; task output should mention users only when relevant.
242
276
  - With \`--user\`, the user is added to the thread and may receive more frequent thread-level notifications.
243
277
  - If a scheduled task completes with no actionable result and no user-visible change, prefer archiving the session after the final message so Discord does not keep a no-op thread highlighted.
244
- - Example no-op cleanup command: \`kimaki session archive --session ses_123\`
278
+ - Example no-op cleanup command: \`kimaki session archive thread_123 (or --session ses_123)\`
245
279
 
246
280
  Manage scheduled tasks with:
247
281
 
@@ -257,10 +291,10 @@ describe('system-message', () => {
257
291
  - Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention the user via Discord ID only when failures require review".
258
292
  - Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
259
293
  - Recurring maintenance: use cron \`--send-at\` for repetitive tasks like rotating secrets, checking dependency updates, running security audits, or cleaning up stale branches. Example: \`--send-at "0 9 1 * *"\` to run on the 1st of every month.
260
- - Quiet no-op checks: if a recurring task checks something and finds nothing to report, let it post a brief final summary and then archive the session with \`kimaki session archive --session ses_123\`. Example: a scheduled email triage run that finds no new emails should archive itself so it does not add noise to Discord.
294
+ - Quiet no-op checks: if a recurring task checks something and finds nothing to report, let it post a brief final summary and then archive the session with \`kimaki session archive thread_123 (or --session ses_123)\`. Example: a scheduled email triage run that finds no new emails should archive itself so it does not add noise to Discord.
261
295
  - Thread reminders: when the user says "remind me about this in 2 hours" (or any duration), use \`--send-at\` with \`--thread\` to resurface the current thread. Compute the future UTC time and send a mention so Discord shows a notification:
262
296
 
263
- kimaki send --session ses_123 --prompt 'Reminder: <@USER_ID> you asked to be reminded about this thread.' --send-at '<future_UTC_time>' --notify-only --agent <current_agent>
297
+ kimaki send --thread thread_123 (or --session ses_123) --prompt 'Reminder: <@USER_ID> you asked to be reminded about this thread.' --send-at '<future_UTC_time>' --notify-only --agent <current_agent>
264
298
 
265
299
  Replace \`<future_UTC_time>\` with the computed UTC ISO timestamp. The \`--notify-only\` flag creates just a notification message without starting a new AI session. The \`<@userId>\` mention ensures the user gets a Discord notification.
266
300
 
@@ -275,7 +309,7 @@ describe('system-message', () => {
275
309
  When the user asks to "create a worktree" or "make a worktree", they mean you should use the kimaki CLI to create it. Do NOT use raw \`git worktree add\` commands. Instead use:
276
310
 
277
311
  \`\`\`bash
278
- kimaki send --channel chan_123 --prompt 'your task description' --worktree worktree-name --agent <current_agent> --user '<discord-user-id>'
312
+ kimaki send --channel chan_123 --prompt 'your task description' --worktree worktree-name --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
279
313
  \`\`\`
280
314
 
281
315
  This creates a new Discord thread with an isolated git worktree and starts a session in it. The worktree name should be kebab-case and descriptive of the task.
@@ -291,7 +325,7 @@ describe('system-message', () => {
291
325
  Use \`--cwd\` to start a session in an existing project subfolder or git worktree directory instead of the project root:
292
326
 
293
327
  \`\`\`bash
294
- kimaki send --channel chan_123 --prompt 'Run restricted task X' --cwd /path/to/project/restricted-task --agent <current_agent> --user '<discord-user-id>'
328
+ kimaki send --channel chan_123 --prompt 'Run restricted task X' --cwd /path/to/project/restricted-task --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
295
329
  \`\`\`
296
330
 
297
331
  The path must be inside the project or be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses that path as its working directory, so subfolder \`opencode.json\` config can apply. Passing the project root itself is allowed and behaves like the default. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing directory.
@@ -305,7 +339,7 @@ describe('system-message', () => {
305
339
  When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
306
340
 
307
341
  \`\`\`bash
308
- kimaki send --channel chan_123 --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent> --user '<discord-user-id>'
342
+ kimaki send --channel chan_123 --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
309
343
  \`\`\`
310
344
 
311
345
  The command automatically handles long prompts (over 2000 chars) by sending them as file attachments. With \`--notify-only\`, long prompts are split into multiple messages instead so the content is directly visible.
@@ -347,7 +381,7 @@ describe('system-message', () => {
347
381
 
348
382
  When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
349
383
 
350
- When the user uses \`#project-name\` syntax, they usually mean a Kimaki project channel. Use \`kimaki project list --json\` to resolve the \`channel_name\` to its repo working directory. Try the lookup yourself before acting, for example filter by \`channel_name\` with jq: \`kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name") | .directory'\`.
384
+ When the user uses \`#project-name\` syntax, they usually mean a Kimaki project channel. Use \`kimaki project list --json\` to resolve the \`channel_name\` to its repo working directory. The JSON output includes \`guild_id\` and \`guild_name\` to distinguish channels with the same name across different servers. When duplicates exist, prefer filtering by \`guild_id\` (stable) over \`guild_name\` (mutable): \`kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name" and .guild_id == "123456") | .channel_id'\`.
351
385
 
352
386
  When the user uses \`#Some Thread Title\` with spaces, they mean a **thread title**, not a project channel. Find the session by searching across projects, then read the session markdown:
353
387
 
@@ -362,16 +396,25 @@ describe('system-message', () => {
362
396
  If you don't know which project the thread belongs to, try each project from \`kimaki project list --json\`.
363
397
 
364
398
  \`\`\`bash
365
- # List all registered projects with their channel IDs
399
+ # List all registered projects with their channel IDs and guild names
366
400
  kimaki project list
367
- kimaki project list --json # machine-readable output
368
- kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name") | .directory'
401
+ kimaki project list --json # machine-readable output with guild_id, guild_name, is_local
402
+
403
+ # Include projects from other machines (scans Kimaki category in Discord)
404
+ kimaki project list --all
405
+ kimaki project list --all --json # remote projects have is_local: false and directory: null
406
+
407
+ # Resolve by channel name (prefer adding guild_name filter if duplicates exist)
408
+ kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name") | .channel_id + " " + .guild_name + " " + .directory'
369
409
 
370
410
  # Create a new project in ~/.kimaki/projects/<name> (folder + git init + Discord channel)
371
411
  kimaki project create my-new-app
372
412
 
373
413
  # Add an existing directory as a project
374
414
  kimaki project add /path/to/repo
415
+
416
+ # Remove a stale or duplicate channel mapping (local DB only, does not delete Discord channel)
417
+ kimaki project remove <channel_id>
375
418
  \`\`\`
376
419
 
377
420
  To send a task to another project:
@@ -501,45 +544,6 @@ describe('system-message', () => {
501
544
  reassure them: their data is safe, URLs are unique and not indexed, and they can disable
502
545
  this feature by restarting kimaki with the \`--no-critique\` flag.
503
546
 
504
- ### reviewing diffs with AI
505
-
506
- \`bunx critique review --web\` generates an AI-powered review of a diff and uploads it as a shareable URL.
507
- It spawns a separate opencode session that analyzes the diff, groups related changes, and produces
508
- a structured review with explanations, diagrams, and suggestions. This is useful when the user
509
- asks you to explain or review a diff — the output is much richer than a plain diff URL.
510
-
511
- **WARNING: This command is very slow (up to 20 minutes for large diffs).** Only run it when the
512
- user explicitly asks for a code review or diff explanation. Always warn the user it will take
513
- a while before running it. Set Bash tool timeout to at least 25 minutes (\`timeout: 1_500_000\`).
514
-
515
- Always pass \`--agent opencode\` and \`--session ses_123\` so the reviewer has context about
516
- why the changes were made. If you know other session IDs that produced the diff (e.g. from
517
- \`kimaki session list\` or from the thread history), pass them too with additional \`--session\` flags.
518
-
519
- Examples:
520
-
521
- \`\`\`bash
522
- # Review working tree changes
523
- bunx critique review --web --agent opencode --session ses_123
524
-
525
- # Review staged changes
526
- bunx critique review --staged --web --agent opencode --session ses_123
527
-
528
- # Review a specific commit
529
- bunx critique review --commit HEAD --web --agent opencode --session ses_123
530
-
531
- # Review branch changes compared to main
532
- bunx critique review main...HEAD --web --agent opencode --session ses_123
533
-
534
- # Review with multiple session contexts (current + the session that made the changes)
535
- bunx critique review --commit abc1234 --web --agent opencode --session ses_123 --session ses_other_session_id
536
-
537
- # Review only specific files
538
- bunx critique review --web --agent opencode --session ses_123 --filter "src/**/*.ts"
539
- \`\`\`
540
-
541
- The command prints a preview URL when done — share that URL with the user.
542
-
543
547
 
544
548
  ## running dev servers with tunnel access
545
549
 
@@ -2,7 +2,7 @@
2
2
  import { Routes } from 'discord.js';
3
3
  import { createDiscordRest } from './discord-urls.js';
4
4
  import YAML from 'yaml';
5
- import { claimScheduledTaskRunning, getDuePlannedScheduledTasks, markScheduledTaskCronRescheduled, markScheduledTaskCronRetry, markScheduledTaskFailed, markScheduledTaskOneShotCompleted, recoverStaleRunningScheduledTasks, } from './database.js';
5
+ import { claimScheduledTaskRunning, getDuePlannedScheduledTasks, getScheduledTask, markScheduledTaskCronRescheduled, markScheduledTaskCronRetry, markScheduledTaskFailed, markScheduledTaskOneShotCompleted, recoverStaleRunningScheduledTasks, } from './database.js';
6
6
  import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
7
7
  import { notifyError } from './sentry.js';
8
8
  import { getNextCronRun, getPromptPreview, parseScheduledTaskPayload, } from './task-schedule.js';
@@ -32,6 +32,7 @@ async function executeThreadScheduledTask({ rest, task, payload, }) {
32
32
  ...(payload.injectionGuardPatterns?.length
33
33
  ? { injectionGuardPatterns: payload.injectionGuardPatterns }
34
34
  : {}),
35
+ ...(payload.parentSessionId ? { parentSessionId: payload.parentSessionId } : {}),
35
36
  };
36
37
  const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }];
37
38
  // Newline between prefix and prompt so leading /command detection can
@@ -69,6 +70,7 @@ async function executeChannelScheduledTask({ rest, task, payload, }) {
69
70
  ...(payload.injectionGuardPatterns?.length
70
71
  ? { injectionGuardPatterns: payload.injectionGuardPatterns }
71
72
  : {}),
73
+ ...(payload.parentSessionId ? { parentSessionId: payload.parentSessionId } : {}),
72
74
  };
73
75
  const embeds = marker
74
76
  ? [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
@@ -211,7 +213,7 @@ async function processDueTask({ rest, task, }) {
211
213
  startedAt,
212
214
  });
213
215
  if (!claimed) {
214
- return;
216
+ return { kind: 'skipped' };
215
217
  }
216
218
  const executeResult = await executeScheduledTask({ rest, task });
217
219
  const finishedAt = new Date();
@@ -222,9 +224,22 @@ async function processDueTask({ rest, task, }) {
222
224
  failedAt: finishedAt,
223
225
  error: executeResult,
224
226
  });
225
- return;
227
+ return { kind: 'failed', error: executeResult };
226
228
  }
227
229
  await finalizeSuccessfulTask({ task, completedAt: finishedAt });
230
+ return { kind: 'success' };
231
+ }
232
+ // Same claim → execute → finalize path as the poller. Used by /tasks Run now.
233
+ export async function runScheduledTaskNow({ token, taskId, }) {
234
+ const task = await getScheduledTask(taskId);
235
+ if (!task) {
236
+ return new Error(`Task #${taskId} not found`);
237
+ }
238
+ if (task.status !== 'planned') {
239
+ return new Error(`Task #${taskId} is ${task.status}; only planned tasks can be run now`);
240
+ }
241
+ const rest = createDiscordRest(token);
242
+ return processDueTask({ rest, task });
228
243
  }
229
244
  async function runTaskRunnerTick({ rest, staleRunningMs, dueBatchSize, }) {
230
245
  const staleBefore = new Date(Date.now() - staleRunningMs);
@@ -144,6 +144,7 @@ export function parseScheduledTaskPayload(payloadJson) {
144
144
  const userId = asString(parsed.userId);
145
145
  const permissions = asStringArray(parsed.permissions);
146
146
  const injectionGuardPatterns = asStringArray(parsed.injectionGuardPatterns);
147
+ const parentSessionId = asString(parsed.parentSessionId);
147
148
  if (!threadId || !prompt) {
148
149
  return new Error('Thread task payload requires threadId and prompt');
149
150
  }
@@ -157,6 +158,7 @@ export function parseScheduledTaskPayload(payloadJson) {
157
158
  userId,
158
159
  permissions,
159
160
  injectionGuardPatterns,
161
+ parentSessionId,
160
162
  };
161
163
  }
162
164
  if (kind === 'channel') {
@@ -173,6 +175,7 @@ export function parseScheduledTaskPayload(payloadJson) {
173
175
  const userId = asString(parsed.userId);
174
176
  const permissions = asStringArray(parsed.permissions);
175
177
  const injectionGuardPatterns = asStringArray(parsed.injectionGuardPatterns);
178
+ const parentSessionId = asString(parsed.parentSessionId);
176
179
  if (!channelId || !prompt) {
177
180
  return new Error('Channel task payload requires channelId and prompt');
178
181
  }
@@ -190,6 +193,7 @@ export function parseScheduledTaskPayload(payloadJson) {
190
193
  userId,
191
194
  permissions,
192
195
  injectionGuardPatterns,
196
+ parentSessionId,
193
197
  };
194
198
  }
195
199
  return new Error('Task payload has unknown kind');
@@ -1135,8 +1135,8 @@ e2eTest('thread message queue ordering', () => {
1135
1135
  },
1136
1136
  });
1137
1137
  const th = discord.thread(thread.id);
1138
- // 2. While session is busy (race-final has 500ms delay), queue a message
1139
- // with queue suffix. The queue suffix forces local-queue mode.
1138
+ // 2. Wait until the bot has replied, then queue while the slow stream is busy.
1139
+ await th.waitForBotReply({ timeout: 4_000 });
1140
1140
  const queuedMsg = await th.user(TEST_USER_ID).sendMessage({
1141
1141
  content: 'Reply with exactly: original-queued. queue',
1142
1142
  });
@@ -1200,7 +1200,7 @@ e2eTest('thread message queue ordering', () => {
1200
1200
  --- from: user (queue-tester)
1201
1201
  Reply with exactly: edited-queued. queue
1202
1202
  --- from: assistant (TestBot)
1203
- Queued at position 1. Edit your message to update it in queue
1203
+ Queued at position 1. Edit or delete your message to update the queue
1204
1204
  ⬦ **queue-tester** edited queued message
1205
1205
  ⬥ slow-busy-reply
1206
1206
  *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
@@ -1293,7 +1293,7 @@ e2eTest('thread message queue ordering', () => {
1293
1293
  --- from: user (queue-tester)
1294
1294
  Reply with exactly: will-be-removed
1295
1295
  --- from: assistant (TestBot)
1296
- Queued at position 1. Edit your message to update it in queue
1296
+ Queued at position 1. Edit or delete your message to update the queue
1297
1297
  ⬦ **queue-tester** removed message from queue
1298
1298
  ⬥ slow-busy-reply
1299
1299
  *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
@@ -779,7 +779,7 @@ e2eTest('voice message handling', () => {
779
779
  --- from: assistant (TestBot)
780
780
  🎤 Transcribing voice message...
781
781
  📝 **Transcribed message:** Queue this task for later
782
- Queued at position 1. Edit your message to update it in queue
782
+ Queued at position 1. Edit or delete your message to update the queue
783
783
  ⬥ slow-response-done
784
784
  *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
785
785
  » **voice-tester:** Voice message transcription from Discord user:
@@ -9,7 +9,8 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import url from 'node:url';
11
11
  import { describe, beforeAll, afterAll, test, expect } from 'vitest';
12
- import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
12
+ import { ChannelType, Client, GatewayIntentBits, Partials, Routes } from 'discord.js';
13
+ import YAML from 'yaml';
13
14
  import { DigitalDiscord } from 'discord-digital-twin/src';
14
15
  import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
15
16
  import { setDataDir } from './config.js';
@@ -35,6 +36,7 @@ function normalizeWorktreeLifecycleText(text) {
35
36
  .replaceAll(CHANNEL_WORKTREE_NAME, 'CHANNEL_WORKTREE_NAME')
36
37
  .replaceAll(AUTO_WORKTREE_SUFFIX, 'AUTO_WORKTREE_NAME')
37
38
  .replaceAll(WORKTREE_NAME, 'WORKTREE_NAME')
39
+ .replaceAll(WORKTREE_SUFFIX, 'SUFFIX')
38
40
  .replace(/ses_[a-zA-Z0-9]+/g, 'ses_TEST')
39
41
  .replace(/<#\d+>/g, '<#THREAD_ID>')
40
42
  .replace(/`[^`\n]*\/worktrees\/[^`\n]*`/g, '`/tmp/worktrees/WORKTREE_NAME`');
@@ -630,4 +632,71 @@ describe('worktree lifecycle', () => {
630
632
  const okCount = (text.match(/⬥ ok/g) || []).length;
631
633
  expect(okCount).toBe(2);
632
634
  }, 20_000);
635
+ test('kimaki send --channel auto-creates worktree when channel toggle is enabled', async () => {
636
+ // Simulate `kimaki send --channel AUTO_WORKTREE_CHANNEL_ID --prompt '...'`
637
+ // WITHOUT --worktree flag. The bot-side ThreadCreate handler should detect
638
+ // the channel toggle and auto-create a worktree.
639
+ const prompt = `Reply with exactly: send-auto-wt-${WORKTREE_SUFFIX}`;
640
+ const embedMarker = {
641
+ start: true,
642
+ username: 'worktree-tester',
643
+ userId: TEST_USER_ID,
644
+ };
645
+ // Post starter message (what `kimaki send` does)
646
+ const starterMessage = await discord
647
+ .channel(AUTO_WORKTREE_CHANNEL_ID)
648
+ .bot()
649
+ .sendMessage({
650
+ content: `» **kimaki-cli:**\n${prompt}`,
651
+ embeds: [
652
+ { color: 0x2b2d31, footer: { text: YAML.stringify(embedMarker) } },
653
+ ],
654
+ });
655
+ // Create thread on that message (what `kimaki send` does via REST)
656
+ const threadData = (await botClient.rest.post(Routes.threads(AUTO_WORKTREE_CHANNEL_ID, starterMessage.id), {
657
+ body: {
658
+ name: prompt.slice(0, 80),
659
+ auto_archive_duration: 1440,
660
+ },
661
+ }));
662
+ // Wait for worktree creation status message (Branch:)
663
+ await waitForBotMessageContaining({
664
+ discord,
665
+ threadId: threadData.id,
666
+ userId: TEST_USER_ID,
667
+ text: 'Branch:',
668
+ timeout: 25_000,
669
+ });
670
+ // Wait for the bot reply to the prompt. The starter message is from
671
+ // the bot (kimaki-cli), not a user, so just wait for the text to appear.
672
+ await waitForBotMessageContaining({
673
+ discord,
674
+ threadId: threadData.id,
675
+ userId: discord.botUserId,
676
+ text: '⬥ ok',
677
+ timeout: 10_000,
678
+ });
679
+ // Snapshot the thread content
680
+ const th = discord.thread(threadData.id);
681
+ expect(normalizeWorktreeLifecycleText(await th.text())).toMatchInlineSnapshot(`
682
+ "--- from: assistant (TestBot)
683
+ » **kimaki-cli:**
684
+ Reply with exactly: send-auto-wt-SUFFIX
685
+ [embed]
686
+ 🌳 **Worktree: opencode/kimaki-rply-wth-exctly-snd-at-wt-l8kct**
687
+ 📁 \`/tmp/worktrees/WORKTREE_NAME\`
688
+ 🌿 Branch: \`opencode/kimaki-rply-wth-exctly-snd-at-wt-l8kct\`
689
+ *using deterministic-provider/deterministic-v2*
690
+ ⬥ ok"
691
+ `);
692
+ // Verify DB has worktree info
693
+ const worktreeInfo = await getThreadWorktreeOrWorkspace(threadData.id);
694
+ expect(worktreeInfo?.status).toBe('ready');
695
+ expect(worktreeInfo?.workspace_directory).toBeTruthy();
696
+ // Verify runtime is in the worktree directory
697
+ const runtime = getRuntime(threadData.id);
698
+ expect(runtime).toBeDefined();
699
+ expect(runtime.sdkDirectory).toContain(`${path.sep}worktrees${path.sep}`);
700
+ expect(runtime.sdkDirectory).not.toBe(directories.projectDirectory);
701
+ }, 35_000);
633
702
  });
package/dist/worktrees.js CHANGED
@@ -713,6 +713,69 @@ export async function mergeWorktree({ worktreeDir, mainRepoDir, worktreeName, ta
713
713
  shortSha: shortSha instanceof Error ? 'unknown' : shortSha,
714
714
  };
715
715
  }
716
+ /**
717
+ * Resolve the best git ref for a base branch by checking remote tracking refs.
718
+ * Prefers upstream/<branch> over origin/<branch> over local <branch>.
719
+ * Fetches the remote first so tracking refs are up to date.
720
+ *
721
+ * If the remote is strictly ahead of local, returns the remote ref.
722
+ * If local and remote have diverged (local is both ahead and behind),
723
+ * returns the local branch to avoid needing a merge/rebase.
724
+ * If the branch is already an explicit remote ref (e.g. `origin/main`), skips resolution.
725
+ * Uses `git remote` to distinguish real remote prefixes from local branches
726
+ * containing `/` like `feature/foo`.
727
+ */
728
+ export async function resolveBestBaseRef({ directory, branch, }) {
729
+ // Check if branch is already an explicit remote ref like "origin/main".
730
+ // Local branches can contain `/` (e.g. "feature/foo"), so we check
731
+ // against actual remote names instead of a naive slash check.
732
+ if (branch.includes('/')) {
733
+ const remotes = await git(directory, 'remote');
734
+ if (!(remotes instanceof Error)) {
735
+ const prefix = branch.slice(0, branch.indexOf('/'));
736
+ if (remotes.split('\n').some((r) => r.trim() === prefix)) {
737
+ return branch;
738
+ }
739
+ }
740
+ }
741
+ for (const remote of ['upstream', 'origin']) {
742
+ // Best-effort fetch with short timeout
743
+ const fetchResult = await git(directory, `fetch ${remote} ${branch}`, {
744
+ timeout: 15_000,
745
+ });
746
+ if (fetchResult instanceof Error)
747
+ continue;
748
+ const remoteRef = `${remote}/${branch}`;
749
+ const refExists = await git(directory, `rev-parse --verify refs/remotes/${remoteRef}`);
750
+ if (refExists instanceof Error)
751
+ continue;
752
+ // Check if local branch exists
753
+ const localExists = await git(directory, `rev-parse --verify refs/heads/${branch}`);
754
+ if (localExists instanceof Error) {
755
+ // No local branch but remote exists — use remote
756
+ return remoteRef;
757
+ }
758
+ // Count commits: remote ahead of local, and local ahead of remote
759
+ const [remoteAhead, localAhead] = await Promise.all([
760
+ git(directory, `rev-list --count refs/heads/${branch}..refs/remotes/${remoteRef}`),
761
+ git(directory, `rev-list --count refs/remotes/${remoteRef}..refs/heads/${branch}`),
762
+ ]);
763
+ if (remoteAhead instanceof Error || localAhead instanceof Error)
764
+ continue;
765
+ const remoteAheadCount = parseInt(remoteAhead, 10);
766
+ const localAheadCount = parseInt(localAhead, 10);
767
+ // Diverged (both ahead): use local to avoid needing a merge
768
+ if (remoteAheadCount > 0 && localAheadCount > 0)
769
+ return branch;
770
+ // Remote is strictly ahead: use remote ref
771
+ if (remoteAheadCount > 0)
772
+ return remoteRef;
773
+ // Equal or local is ahead — check next remote instead of returning early,
774
+ // because origin might be ahead even when upstream is equal.
775
+ }
776
+ // No usable remote found — use local branch as-is
777
+ return branch;
778
+ }
716
779
  /**
717
780
  * List branches sorted by most recent commit date.
718
781
  * Returns branch short names (e.g. "main", "origin/feature-x").
package/package.json CHANGED
@@ -2,9 +2,16 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.20.1",
5
+ "version": "0.22.0",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ "./anthropic-auth-plugin": {
11
+ "types": "./dist/anthropic-auth-plugin.d.ts",
12
+ "default": "./dist/anthropic-auth-plugin.js"
13
+ }
14
+ },
8
15
  "files": [
9
16
  "dist",
10
17
  "src",
@@ -24,7 +31,7 @@
24
31
  "lintcn": "^0.7.1",
25
32
  "tsx": "^4.20.5",
26
33
  "undici": "^8.0.2",
27
- "discord-digital-twin": "^0.1.0",
34
+ "discord-digital-twin": "^0.1.1",
28
35
  "opencode-deterministic-provider": "^0.0.1",
29
36
  "opencode-cached-provider": "^0.0.1",
30
37
  "db": "^0.0.0"
@@ -63,9 +70,9 @@
63
70
  "zod": "^4.3.6",
64
71
  "zustand": "^5.0.11",
65
72
  "errore": "^0.14.1",
73
+ "traforo": "^0.7.1",
66
74
  "opencode-injection-guard": "^0.2.1",
67
- "libsqlproxy": "^0.1.0",
68
- "traforo": "^0.7.1"
75
+ "libsqlproxy": "^0.1.0"
69
76
  },
70
77
  "optionalDependencies": {
71
78
  "@snazzah/davey": "^0.1.10",