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.
- package/dist/agent-model.e2e.test.js +6 -4
- package/dist/cli-commands/project.js +148 -24
- package/dist/cli-commands/send.js +48 -3
- package/dist/cli-runner.js +86 -7
- package/dist/cli-runner.test.js +28 -1
- package/dist/commands/btw.js +7 -3
- package/dist/commands/new-worktree.js +14 -2
- package/dist/commands/session.js +66 -13
- package/dist/commands/tasks.js +104 -19
- package/dist/commands/user-command.js +3 -2
- package/dist/database.js +14 -0
- package/dist/db.js +1 -0
- package/dist/discord-bot.js +65 -9
- package/dist/interaction-handler.js +6 -15
- package/dist/message-preprocessing.js +9 -2
- package/dist/queue-advanced-e2e-setup.js +22 -0
- package/dist/queue-delete-message.e2e.test.js +75 -0
- package/dist/schema.js +3 -0
- package/dist/session-handler/thread-runtime-state.js +9 -0
- package/dist/session-handler/thread-session-runtime.js +49 -1
- package/dist/system-message.js +50 -63
- package/dist/system-message.test.js +65 -61
- package/dist/task-runner.js +18 -3
- package/dist/task-schedule.js +4 -0
- package/dist/thread-message-queue.e2e.test.js +4 -4
- package/dist/voice-message.e2e.test.js +1 -1
- package/dist/worktree-lifecycle.e2e.test.js +70 -1
- package/dist/worktrees.js +63 -0
- package/package.json +11 -4
- package/skills/egaki/SKILL.md +26 -80
- package/skills/goke/SKILL.md +27 -20
- package/skills/holocron/SKILL.md +72 -8
- package/skills/new-skill/SKILL.md +87 -0
- package/skills/playwriter/SKILL.md +4 -4
- package/skills/spiceflow/SKILL.md +2 -0
- package/src/agent-model.e2e.test.ts +10 -4
- package/src/cli-commands/project.ts +183 -26
- package/src/cli-commands/send.ts +64 -5
- package/src/cli-runner.test.ts +39 -1
- package/src/cli-runner.ts +115 -6
- package/src/commands/btw.ts +9 -2
- package/src/commands/new-worktree.ts +14 -1
- package/src/commands/session.ts +79 -17
- package/src/commands/tasks.ts +119 -19
- package/src/commands/user-command.ts +3 -2
- package/src/database.ts +24 -0
- package/src/db.ts +1 -0
- package/src/discord-bot.ts +80 -8
- package/src/interaction-handler.ts +7 -19
- package/src/message-preprocessing.ts +11 -2
- package/src/queue-advanced-e2e-setup.ts +23 -0
- package/src/queue-delete-message.e2e.test.ts +102 -0
- package/src/schema.sql +1 -0
- package/src/schema.ts +3 -0
- package/src/session-handler/thread-runtime-state.ts +19 -0
- package/src/session-handler/thread-session-runtime.ts +75 -0
- package/src/system-message.test.ts +73 -61
- package/src/system-message.ts +63 -62
- package/src/task-runner.ts +34 -3
- package/src/task-schedule.ts +6 -0
- package/src/thread-message-queue.e2e.test.ts +4 -4
- package/src/voice-message.e2e.test.ts +1 -1
- package/src/worktree-lifecycle.e2e.test.ts +88 -1
- 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.
|
|
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
|
-
|
|
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
|
|
package/dist/task-runner.js
CHANGED
|
@@ -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);
|
package/dist/task-schedule.js
CHANGED
|
@@ -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.
|
|
1139
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|