kimaki 0.9.0 → 0.9.1

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.
@@ -89,25 +89,25 @@ cli
89
89
  });
90
90
  cli
91
91
  .command('tunnel', 'Expose a local port via tunnel')
92
- .option('-p, --port <port>', 'Local port to expose (required)')
92
+ .option('-p, --port <port>', 'Local port to expose (optional when command output reveals one)')
93
93
  .option('-t, --tunnel-id [id]', 'Custom tunnel ID (only for services safe to expose publicly; prefer random default)')
94
94
  .option('-h, --host [host]', 'Local host (default: localhost)')
95
95
  .option('-s, --server [url]', 'Tunnel server URL')
96
96
  .option('-k, --kill', 'Kill any existing process on the port before starting')
97
97
  .action(async (options) => {
98
- const { runTunnel, parseCommandFromArgv, CLI_NAME } = await import('traforo/run-tunnel');
99
- if (!options.port) {
100
- cliLogger.error('Error: --port is required');
101
- cliLogger.error(`\nUsage: kimaki tunnel -p <port> [-- command]`);
98
+ const { runTunnel, parseCommandFromArgv } = await import('traforo/run-tunnel');
99
+ const { command } = parseCommandFromArgv(process.argv);
100
+ if (!options.port && command.length === 0) {
101
+ cliLogger.error('Error: --port is required unless a command is provided after --');
102
+ cliLogger.error(`\nUsage: kimaki tunnel [-- command]`);
103
+ cliLogger.error(` or: kimaki tunnel --port <port>`);
102
104
  process.exit(EXIT_NO_RESTART);
103
105
  }
104
- const port = parseInt(options.port, 10);
105
- if (isNaN(port) || port < 1 || port > 65535) {
106
+ const port = options.port ? parseInt(options.port, 10) : undefined;
107
+ if (options.port && (!port || port < 1 || port > 65535)) {
106
108
  cliLogger.error(`Error: Invalid port number: ${options.port}`);
107
109
  process.exit(EXIT_NO_RESTART);
108
110
  }
109
- // Parse command after -- from argv
110
- const { command } = parseCommandFromArgv(process.argv);
111
111
  await runTunnel({
112
112
  port,
113
113
  tunnelId: options.tunnelId || undefined,
@@ -4,7 +4,7 @@
4
4
  import { initDatabase, closeDatabase, getThreadWorktree, getThreadSession, getChannelWorktreesEnabled, getChannelMentionMode, getChannelDirectory, getPrisma, cancelAllPendingIpcRequests, deleteChannelDirectoryById, createPendingWorktree, setWorktreeReady, } from './database.js';
5
5
  import { stopOpencodeServer, } from './opencode.js';
6
6
  import { formatAutoWorktreeName, createWorktreeInBackground, worktreeCreatingMessage } from './commands/new-worktree.js';
7
- import { validateWorktreeDirectory, git } from './worktrees.js';
7
+ import { validateWorktreeDirectory, git, isGitRepositoryRoot } from './worktrees.js';
8
8
  import { WORKTREE_PREFIX } from './commands/merge-worktree.js';
9
9
  import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, sendThreadMessage, SILENT_MESSAGE_FLAGS, NOTIFY_MESSAGE_FLAGS, reactToThread, stripMentions, hasKimakiBotPermission, hasNoKimakiRole, } from './discord-utils.js';
10
10
  import { getOpencodeSystemMessage, isInjectedPromptMarker, } from './system-message.js';
@@ -634,8 +634,16 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
634
634
  : stripMentions(message.content || '')
635
635
  .replace(/\s+/g, ' ')
636
636
  .trim() || 'kimaki thread';
637
- // Check if worktrees should be enabled (CLI flag OR channel setting)
638
- const shouldUseWorktrees = useWorktrees || (await getChannelWorktreesEnabled(channel.id));
637
+ // Check if worktrees should be enabled (CLI flag OR channel setting).
638
+ // Only create worktrees from the configured project directory when that
639
+ // directory is itself the git root. If the user registered a non-git
640
+ // workspace folder under a larger repo, git would create the worktree
641
+ // from the parent repo and strand follow-up messages on failure.
642
+ const wantsWorktrees = useWorktrees || (await getChannelWorktreesEnabled(channel.id));
643
+ const shouldUseWorktrees = wantsWorktrees && (await isGitRepositoryRoot(projectDirectory));
644
+ if (wantsWorktrees && !shouldUseWorktrees) {
645
+ discordLogger.warn(`[WORKTREE] Skipping automatic worktree for non-git project directory: ${projectDirectory}`);
646
+ }
639
647
  // Add worktree prefix if worktrees are enabled
640
648
  const threadName = shouldUseWorktrees
641
649
  ? `${WORKTREE_PREFIX}${baseThreadName}`
@@ -800,7 +808,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
800
808
  // The runtime is created immediately so follow-up messages queue
801
809
  // naturally; the worktree promise is awaited inside enqueueIncoming.
802
810
  let worktreePromise;
803
- if (marker.worktree) {
811
+ if (marker.worktree && (await isGitRepositoryRoot(projectDirectory))) {
804
812
  discordLogger.log(`[BOT_SESSION] Creating worktree: ${marker.worktree}`);
805
813
  const worktreeStatusMessage = await thread
806
814
  .send({
@@ -816,6 +824,9 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
816
824
  rest: discordClient.rest,
817
825
  });
818
826
  }
827
+ else if (marker.worktree) {
828
+ discordLogger.warn(`[BOT_SESSION] Skipping requested worktree for non-git project directory: ${projectDirectory}`);
829
+ }
819
830
  // --cwd: reuse an existing worktree directory. Revalidate at bot-time
820
831
  // (CLI validated at send-time but the path could become stale).
821
832
  // Store in thread_worktrees as ready with origin=external so
@@ -132,7 +132,7 @@ Pick a random port between 3000-9000 to avoid conflicts:
132
132
 
133
133
  ${backticks}bash
134
134
  PORT=$((RANDOM % 6000 + 3000))
135
- bunx tuistory launch "PORT=$PORT kimaki tunnel -p $PORT -- bun run server.ts" -s game-dev --cwd "$PWD"
135
+ bunx tuistory launch "PORT=$PORT kimaki tunnel -- bun run server.ts" -s game-dev --cwd "$PWD"
136
136
  ${backticks}
137
137
 
138
138
  Wait a moment, then get the tunnel URL:
@@ -136,11 +136,11 @@ Use a tuistory session with a descriptive name like \`projectname-dev\` so you c
136
136
 
137
137
  Use random tunnel IDs by default. Only pass \`-t\` when exposing a service that is safe to be publicly discoverable.
138
138
 
139
- \`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost.
139
+ \`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost. The local port is detected from the child process output, so do not pass \`-p\` when launching a dev server command unless detection fails.
140
140
 
141
141
  \`\`\`bash
142
142
  # Start the dev server in a named background session
143
- bunx tuistory launch "kimaki tunnel -p 3000 -- pnpm dev" -s myapp-dev
143
+ bunx tuistory launch "kimaki tunnel -- pnpm dev" -s myapp-dev
144
144
 
145
145
  # Wait until the dev server prints something useful, then inspect it
146
146
  bunx tuistory -s myapp-dev wait "/ready|local|tunnel/i" --timeout 30000
@@ -149,7 +149,7 @@ bunx tuistory read -s myapp-dev
149
149
 
150
150
  ### passing the public URL to your app
151
151
 
152
- If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups, so \`--port\` is often unnecessary.
152
+ If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups. Use \`--port\` only when the dev server does not print a detectable localhost URL or port line.
153
153
 
154
154
  \`\`\`bash
155
155
  # Your app can read process.env.TRAFORO_URL directly
@@ -176,13 +176,13 @@ bunx tuistory read -s myapp-dev
176
176
 
177
177
  \`\`\`bash
178
178
  # Next.js project
179
- bunx tuistory launch "kimaki tunnel -p 3000 -- pnpm dev" -s projectname-nextjs-dev-3000
179
+ bunx tuistory launch "kimaki tunnel -- pnpm dev" -s projectname-nextjs-dev
180
180
 
181
- # Vite project on port 5173
182
- bunx tuistory launch "kimaki tunnel -p 5173 -- pnpm dev" -s vite-dev-5173
181
+ # Vite project
182
+ bunx tuistory launch "kimaki tunnel -- pnpm dev" -s vite-dev
183
183
 
184
184
  # Custom tunnel ID (only for intentionally public-safe services)
185
- bunx tuistory launch "kimaki tunnel -p 3000 -t holocron -- pnpm dev" -s holocron-dev
185
+ bunx tuistory launch "kimaki tunnel -t holocron -- pnpm dev" -s holocron-dev
186
186
  \`\`\`
187
187
 
188
188
  ### stopping the dev server
@@ -87,23 +87,26 @@ describe('system-message', () => {
87
87
 
88
88
  Only do this when the user explicitly asks to close or archive the thread, and only after your final message.
89
89
 
90
- ## searching discord users
90
+ ## discord user mentions
91
91
 
92
- To search for Discord users in a guild (needed for mentions like <@userId>), run:
92
+ Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
93
+ The current user's ID is available in the per-turn \`<discord-user ... user-id="..." />\` metadata.
94
+
95
+ To search for Discord users in a guild as a best-effort fallback, run:
93
96
 
94
97
  kimaki user list --guild guild_123 --query "username"
95
98
 
96
- This returns user IDs you can use for Discord mentions.
99
+ This returns user IDs you can use for Discord mentions. It can fail when Server Members Intent is disabled, so prefer IDs from existing Discord metadata or raw mentions when possible.
97
100
 
98
101
  ## starting new sessions from CLI
99
102
 
100
103
  To start a new thread/session in this channel pro-grammatically, run:
101
104
 
102
- kimaki send --channel chan_123 --prompt 'your prompt here' --agent <current_agent> --user 'Tommy'
105
+ kimaki send --channel chan_123 --prompt 'your prompt here' --agent <current_agent> --user '<discord-user-id>'
103
106
 
104
107
  You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
105
108
  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.
106
- 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.
109
+ 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.
107
110
 
108
111
  IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
109
112
 
@@ -121,19 +124,19 @@ describe('system-message', () => {
121
124
 
122
125
  Use --notify-only to create a notification thread without starting an AI session:
123
126
 
124
- kimaki send --channel chan_123 --prompt 'User cancelled subscription' --notify-only --agent <current_agent> --user 'Tommy'
127
+ kimaki send --channel chan_123 --prompt 'User cancelled subscription' --notify-only --agent <current_agent> --user '<discord-user-id>'
125
128
 
126
- Use --user to add a specific Discord user to the new thread:
129
+ Use --user with a Discord user ID or raw mention to add a specific Discord user to the new thread:
127
130
 
128
- kimaki send --channel chan_123 --prompt 'Review the latest CI failure' --agent <current_agent> --user 'Tommy'
131
+ kimaki send --channel chan_123 --prompt 'Review the latest CI failure' --agent <current_agent> --user '<discord-user-id>'
129
132
 
130
133
  Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
131
134
 
132
- kimaki send --channel chan_123 --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent> --user 'Tommy'
135
+ kimaki send --channel chan_123 --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent> --user '<discord-user-id>'
133
136
 
134
137
  Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
135
138
 
136
- kimaki send --channel chan_123 --prompt 'Continue work on feature' --cwd /path/to/existing-worktree --agent <current_agent> --user 'Tommy'
139
+ kimaki send --channel chan_123 --prompt 'Continue work on feature' --cwd /path/to/existing-worktree --agent <current_agent> --user '<discord-user-id>'
137
140
 
138
141
  Important:
139
142
  - NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
@@ -144,7 +147,7 @@ describe('system-message', () => {
144
147
 
145
148
  Use --agent to specify which agent to use for the session:
146
149
 
147
- kimaki send --channel chan_123 --prompt 'Plan the refactor of the auth module' --agent plan --user 'Tommy'
150
+ kimaki send --channel chan_123 --prompt 'Plan the refactor of the auth module' --agent plan --user '<discord-user-id>'
148
151
 
149
152
 
150
153
  Available agents:
@@ -156,7 +159,7 @@ describe('system-message', () => {
156
159
  You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
157
160
 
158
161
  kimaki send --thread <thread_id> --prompt '/review fix the auth module' --agent <current_agent>
159
- kimaki send --channel chan_123 --prompt '/build-cmd update dependencies' --agent <current_agent> --user 'Tommy'
162
+ kimaki send --channel chan_123 --prompt '/build-cmd update dependencies' --agent <current_agent> --user '<discord-user-id>'
160
163
 
161
164
  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\`).
162
165
 
@@ -172,8 +175,8 @@ describe('system-message', () => {
172
175
 
173
176
  Use \`--send-at\` to schedule a one-time or recurring task:
174
177
 
175
- kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --user 'Tommy'
176
- kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --user 'Tommy'
178
+ kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --user '<discord-user-id>'
179
+ 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>'
177
180
 
178
181
  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).
179
182
  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.
@@ -231,7 +234,7 @@ describe('system-message', () => {
231
234
  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:
232
235
 
233
236
  \`\`\`bash
234
- kimaki send --channel chan_123 --prompt 'your task description' --worktree worktree-name --agent <current_agent> --user 'Tommy'
237
+ kimaki send --channel chan_123 --prompt 'your task description' --worktree worktree-name --agent <current_agent> --user '<discord-user-id>'
235
238
  \`\`\`
236
239
 
237
240
  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.
@@ -247,7 +250,7 @@ describe('system-message', () => {
247
250
  Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
248
251
 
249
252
  \`\`\`bash
250
- kimaki send --channel chan_123 --prompt 'Continue work on feature X' --cwd /path/to/existing-worktree --agent <current_agent> --user 'Tommy'
253
+ kimaki send --channel chan_123 --prompt 'Continue work on feature X' --cwd /path/to/existing-worktree --agent <current_agent> --user '<discord-user-id>'
251
254
  \`\`\`
252
255
 
253
256
  The path must be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses the worktree as its working directory. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing one.
@@ -261,7 +264,7 @@ describe('system-message', () => {
261
264
  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:
262
265
 
263
266
  \`\`\`bash
264
- kimaki send --channel chan_123 --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent> --user 'Tommy'
267
+ kimaki send --channel chan_123 --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent> --user '<discord-user-id>'
265
268
  \`\`\`
266
269
 
267
270
  The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
@@ -494,11 +497,11 @@ describe('system-message', () => {
494
497
 
495
498
  Use random tunnel IDs by default. Only pass \`-t\` when exposing a service that is safe to be publicly discoverable.
496
499
 
497
- \`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost.
500
+ \`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost. The local port is detected from the child process output, so do not pass \`-p\` when launching a dev server command unless detection fails.
498
501
 
499
502
  \`\`\`bash
500
503
  # Start the dev server in a named background session
501
- bunx tuistory launch "kimaki tunnel -p 3000 -- pnpm dev" -s myapp-dev
504
+ bunx tuistory launch "kimaki tunnel -- pnpm dev" -s myapp-dev
502
505
 
503
506
  # Wait until the dev server prints something useful, then inspect it
504
507
  bunx tuistory -s myapp-dev wait "/ready|local|tunnel/i" --timeout 30000
@@ -507,7 +510,7 @@ describe('system-message', () => {
507
510
 
508
511
  ### passing the public URL to your app
509
512
 
510
- If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups, so \`--port\` is often unnecessary.
513
+ If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups. Use \`--port\` only when the dev server does not print a detectable localhost URL or port line.
511
514
 
512
515
  \`\`\`bash
513
516
  # Your app can read process.env.TRAFORO_URL directly
@@ -534,13 +537,13 @@ describe('system-message', () => {
534
537
 
535
538
  \`\`\`bash
536
539
  # Next.js project
537
- bunx tuistory launch "kimaki tunnel -p 3000 -- pnpm dev" -s projectname-nextjs-dev-3000
540
+ bunx tuistory launch "kimaki tunnel -- pnpm dev" -s projectname-nextjs-dev
538
541
 
539
- # Vite project on port 5173
540
- bunx tuistory launch "kimaki tunnel -p 5173 -- pnpm dev" -s vite-dev-5173
542
+ # Vite project
543
+ bunx tuistory launch "kimaki tunnel -- pnpm dev" -s vite-dev
541
544
 
542
545
  # Custom tunnel ID (only for intentionally public-safe services)
543
- bunx tuistory launch "kimaki tunnel -p 3000 -t holocron -- pnpm dev" -s holocron-dev
546
+ bunx tuistory launch "kimaki tunnel -t holocron -- pnpm dev" -s holocron-dev
544
547
  \`\`\`
545
548
 
546
549
  ### stopping the dev server
@@ -689,6 +689,13 @@ e2eTest('voice message handling', () => {
689
689
  transcription: 'Queue this task for later',
690
690
  queueMessage: true,
691
691
  });
692
+ await waitForBotMessageContaining({
693
+ discord,
694
+ threadId: thread.id,
695
+ userId: TEST_USER_ID,
696
+ text: 'using deterministic-provider/deterministic-v2',
697
+ timeout: 4_000,
698
+ });
692
699
  await th.user(TEST_USER_ID).sendVoiceMessage();
693
700
  // 3. Transcription should appear, followed by queue notification
694
701
  await waitForBotMessageContaining({
@@ -17,13 +17,14 @@ import { setDataDir } from './config.js';
17
17
  import { store } from './store.js';
18
18
  import { startDiscordBot } from './discord-bot.js';
19
19
  import { getRuntime } from './session-handler/thread-session-runtime.js';
20
- import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, } from './database.js';
20
+ import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelWorktreesEnabled, } from './database.js';
21
21
  import { startHranaServer, stopHranaServer } from './hrana-server.js';
22
22
  import { initializeOpencodeForDirectory, stopOpencodeServer, } from './opencode.js';
23
23
  import { chooseLockPort, cleanupTestSessions, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
24
24
  import { execAsync } from './worktrees.js';
25
25
  const TEST_USER_ID = '200000000000000901';
26
26
  const TEXT_CHANNEL_ID = '200000000000000902';
27
+ const NON_GIT_CHANNEL_ID = '200000000000000903';
27
28
  // Unique worktree name per run to avoid collisions with leftover worktrees
28
29
  const WORKTREE_SUFFIX = Date.now().toString(36).slice(-6);
29
30
  const WORKTREE_NAME = `wt-e2e-${WORKTREE_SUFFIX}`;
@@ -32,8 +33,10 @@ function createRunDirectories() {
32
33
  fs.mkdirSync(root, { recursive: true });
33
34
  const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
34
35
  const projectDirectory = path.join(root, 'project');
36
+ const nonGitDirectory = path.join(root, 'non-git-project');
35
37
  fs.mkdirSync(projectDirectory, { recursive: true });
36
- return { root, dataDir, projectDirectory };
38
+ fs.mkdirSync(nonGitDirectory, { recursive: true });
39
+ return { root, dataDir, projectDirectory, nonGitDirectory };
37
40
  }
38
41
  function createDiscordJsClient({ restUrl }) {
39
42
  return new Client({
@@ -77,7 +80,7 @@ function createDeterministicMatchers() {
77
80
  priority: 10,
78
81
  when: {
79
82
  lastMessageRole: 'user',
80
- rawPromptIncludes: 'Reply with exactly:',
83
+ latestUserTextIncludes: 'Reply with exactly:',
81
84
  },
82
85
  then: {
83
86
  parts: [
@@ -122,6 +125,11 @@ describe('worktree lifecycle', () => {
122
125
  name: 'worktree-e2e',
123
126
  type: ChannelType.GuildText,
124
127
  },
128
+ {
129
+ id: NON_GIT_CHANNEL_ID,
130
+ name: 'non-git-worktree-e2e',
131
+ type: ChannelType.GuildText,
132
+ },
125
133
  ],
126
134
  users: [
127
135
  {
@@ -146,6 +154,7 @@ describe('worktree lifecycle', () => {
146
154
  },
147
155
  });
148
156
  fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
157
+ fs.writeFileSync(path.join(directories.nonGitDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
149
158
  // Initialize git repo after writing opencode.json so the initial commit
150
159
  // includes it. Worktrees require at least one commit.
151
160
  await initGitRepo(directories.projectDirectory);
@@ -162,7 +171,14 @@ describe('worktree lifecycle', () => {
162
171
  directory: directories.projectDirectory,
163
172
  channelType: 'text',
164
173
  });
174
+ await setChannelDirectory({
175
+ channelId: NON_GIT_CHANNEL_ID,
176
+ directory: directories.nonGitDirectory,
177
+ channelType: 'text',
178
+ });
165
179
  await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text');
180
+ await setChannelVerbosity(NON_GIT_CHANNEL_ID, 'tools_and_text');
181
+ await setChannelWorktreesEnabled(NON_GIT_CHANNEL_ID, true);
166
182
  botClient = createDiscordJsClient({ restUrl: discord.restUrl });
167
183
  await startDiscordBot({
168
184
  token: discord.botToken,
@@ -213,7 +229,11 @@ describe('worktree lifecycle', () => {
213
229
  }
214
230
  }).catch(() => { return; });
215
231
  await execAsync(`git branch -D ${JSON.stringify(`opencode/kimaki-${WORKTREE_NAME}`)}`, { cwd: directories.projectDirectory }).catch(() => { return; });
216
- fs.rmSync(directories.dataDir, { recursive: true, force: true });
232
+ fs.rmSync(directories.dataDir, {
233
+ recursive: true,
234
+ force: true,
235
+ maxRetries: 3,
236
+ });
217
237
  }
218
238
  }, 5_000);
219
239
  test('session responds after /new-worktree switches sdkDirectory in existing thread', async () => {
@@ -308,4 +328,60 @@ describe('worktree lifecycle', () => {
308
328
  const okCount = (text.match(/⬥ ok/g) || []).length;
309
329
  expect(okCount).toBe(2);
310
330
  }, 30_000);
331
+ test('auto-worktrees fall back to normal sessions outside git repositories', async () => {
332
+ await discord.channel(NON_GIT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
333
+ content: 'Reply with exactly: non-git-first',
334
+ });
335
+ const thread = await discord.channel(NON_GIT_CHANNEL_ID).waitForThread({
336
+ timeout: 4_000,
337
+ predicate: (t) => {
338
+ return Boolean(t.name?.includes('Reply with exactly: non-git-first'));
339
+ },
340
+ });
341
+ const th = discord.thread(thread.id);
342
+ await waitForBotReplyAfterUserMessage({
343
+ discord,
344
+ threadId: thread.id,
345
+ userId: TEST_USER_ID,
346
+ userMessageIncludes: 'non-git-first',
347
+ timeout: 4_000,
348
+ });
349
+ await th.user(TEST_USER_ID).sendMessage({
350
+ content: 'Reply with exactly: non-git-second',
351
+ });
352
+ await waitForBotMessageContaining({
353
+ discord,
354
+ threadId: thread.id,
355
+ userId: TEST_USER_ID,
356
+ text: '⬥ ok',
357
+ afterUserMessageIncludes: 'non-git-second',
358
+ timeout: 4_000,
359
+ });
360
+ let text = await th.text();
361
+ for (let attempt = 0; attempt < 40; attempt++) {
362
+ if ((text.match(/⬥ ok/g) || []).length >= 2) {
363
+ break;
364
+ }
365
+ await new Promise((resolve) => {
366
+ setTimeout(resolve, 100);
367
+ });
368
+ text = await th.text();
369
+ }
370
+ expect(text).toMatchInlineSnapshot(`
371
+ "--- from: user (worktree-tester)
372
+ Reply with exactly: non-git-first
373
+ --- from: assistant (TestBot)
374
+ *using deterministic-provider/deterministic-v2*
375
+ --- from: user (worktree-tester)
376
+ Reply with exactly: non-git-second
377
+ --- from: assistant (TestBot)
378
+ ⬥ ok
379
+ ⬥ ok"
380
+ `);
381
+ expect(text).toContain('Reply with exactly: non-git-first');
382
+ expect(text).toContain('Reply with exactly: non-git-second');
383
+ expect(text).not.toContain('Worktree creation failed');
384
+ const okCount = (text.match(/⬥ ok/g) || []).length;
385
+ expect(okCount).toBe(2);
386
+ }, 20_000);
311
387
  });
package/dist/worktrees.js CHANGED
@@ -574,6 +574,13 @@ export async function isDirty(dir, opts) {
574
574
  }
575
575
  return status.length > 0;
576
576
  }
577
+ export async function isGitRepositoryRoot(directory) {
578
+ const topLevel = await git(directory, 'rev-parse --show-toplevel');
579
+ if (topLevel instanceof Error) {
580
+ return false;
581
+ }
582
+ return path.resolve(topLevel) === path.resolve(directory);
583
+ }
577
584
  async function getGitCommonDir(dir) {
578
585
  const commonDir = await git(dir, 'rev-parse --git-common-dir');
579
586
  if (commonDir instanceof Error) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.9.0",
5
+ "version": "0.9.1",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -26,8 +26,8 @@
26
26
  "tsx": "^4.20.5",
27
27
  "undici": "^8.0.2",
28
28
  "discord-digital-twin": "^0.1.0",
29
- "opencode-deterministic-provider": "^0.0.1",
30
29
  "opencode-cached-provider": "^0.0.1",
30
+ "opencode-deterministic-provider": "^0.0.1",
31
31
  "db": "^0.0.0"
32
32
  },
33
33
  "dependencies": {
@@ -65,9 +65,9 @@
65
65
  "zod": "^4.3.6",
66
66
  "zustand": "^5.0.11",
67
67
  "errore": "^0.14.1",
68
- "opencode-injection-guard": "^0.2.1",
69
68
  "libsqlproxy": "^0.1.0",
70
- "traforo": "^0.5.0"
69
+ "traforo": "^0.5.0",
70
+ "opencode-injection-guard": "^0.2.1"
71
71
  },
72
72
  "optionalDependencies": {
73
73
  "@snazzah/davey": "^0.1.10",