@otto-assistant/bridge 0.4.102 → 0.4.103

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 (70) hide show
  1. package/dist/agent-model.e2e.test.js +1 -0
  2. package/dist/anthropic-auth-plugin.js +22 -1
  3. package/dist/anthropic-auth-state.js +31 -0
  4. package/dist/btw-prefix-detection.js +17 -0
  5. package/dist/btw-prefix-detection.test.js +63 -0
  6. package/dist/cli.js +101 -15
  7. package/dist/commands/agent.js +21 -2
  8. package/dist/commands/ask-question.js +50 -4
  9. package/dist/commands/ask-question.test.js +92 -0
  10. package/dist/commands/btw.js +71 -66
  11. package/dist/commands/new-worktree.js +92 -35
  12. package/dist/commands/queue.js +17 -0
  13. package/dist/commands/worktrees.js +196 -139
  14. package/dist/context-awareness-plugin.js +16 -8
  15. package/dist/context-awareness-plugin.test.js +4 -2
  16. package/dist/discord-bot.js +35 -2
  17. package/dist/discord-command-registration.js +9 -2
  18. package/dist/memory-overview-plugin.js +3 -1
  19. package/dist/opencode.js +9 -0
  20. package/dist/queue-question-select-drain.e2e.test.js +135 -10
  21. package/dist/session-handler/thread-runtime-state.js +27 -0
  22. package/dist/session-handler/thread-session-runtime.js +58 -28
  23. package/dist/session-title-rename.test.js +12 -0
  24. package/dist/skill-filter.js +31 -0
  25. package/dist/skill-filter.test.js +65 -0
  26. package/dist/store.js +2 -0
  27. package/dist/system-message.js +12 -3
  28. package/dist/system-message.test.js +10 -6
  29. package/dist/thread-message-queue.e2e.test.js +109 -0
  30. package/dist/worktree-lifecycle.e2e.test.js +4 -1
  31. package/dist/worktrees.js +106 -12
  32. package/dist/worktrees.test.js +232 -6
  33. package/package.json +2 -2
  34. package/skills/goke/SKILL.md +13 -619
  35. package/skills/new-skill/SKILL.md +34 -10
  36. package/skills/npm-package/SKILL.md +336 -2
  37. package/skills/profano/SKILL.md +24 -0
  38. package/skills/zele/SKILL.md +50 -21
  39. package/src/agent-model.e2e.test.ts +1 -0
  40. package/src/anthropic-auth-plugin.ts +24 -4
  41. package/src/anthropic-auth-state.ts +45 -0
  42. package/src/btw-prefix-detection.test.ts +73 -0
  43. package/src/btw-prefix-detection.ts +23 -0
  44. package/src/cli.ts +138 -46
  45. package/src/commands/agent.ts +24 -2
  46. package/src/commands/ask-question.test.ts +111 -0
  47. package/src/commands/ask-question.ts +69 -4
  48. package/src/commands/btw.ts +105 -85
  49. package/src/commands/new-worktree.ts +107 -40
  50. package/src/commands/queue.ts +22 -0
  51. package/src/commands/worktrees.ts +246 -154
  52. package/src/context-awareness-plugin.test.ts +4 -2
  53. package/src/context-awareness-plugin.ts +16 -8
  54. package/src/discord-bot.ts +40 -2
  55. package/src/discord-command-registration.ts +12 -2
  56. package/src/memory-overview-plugin.ts +3 -1
  57. package/src/opencode.ts +9 -0
  58. package/src/queue-question-select-drain.e2e.test.ts +174 -10
  59. package/src/session-handler/thread-runtime-state.ts +36 -1
  60. package/src/session-handler/thread-session-runtime.ts +72 -32
  61. package/src/session-title-rename.test.ts +18 -0
  62. package/src/skill-filter.test.ts +83 -0
  63. package/src/skill-filter.ts +42 -0
  64. package/src/store.ts +17 -0
  65. package/src/system-message.test.ts +10 -6
  66. package/src/system-message.ts +12 -3
  67. package/src/thread-message-queue.e2e.test.ts +126 -0
  68. package/src/worktree-lifecycle.e2e.test.ts +6 -1
  69. package/src/worktrees.test.ts +274 -9
  70. package/src/worktrees.ts +144 -23
package/src/worktrees.ts CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  import crypto from 'node:crypto'
6
6
  import fs from 'node:fs'
7
- import os from 'node:os'
8
7
  import path from 'node:path'
9
8
  import * as errore from 'errore'
9
+ import { getDataDir } from './config.js'
10
10
  import { execAsync } from './exec-async.js'
11
11
  import { createLogger, LogPrefix } from './logger.js'
12
12
 
@@ -530,24 +530,38 @@ async function resolveDefaultWorktreeTarget(
530
530
  return 'HEAD'
531
531
  }
532
532
 
533
- function getManagedWorktreeDirectory({
533
+ /**
534
+ * Build the on-disk directory for a managed worktree.
535
+ *
536
+ * Layout: `<kimakiDataDir>/worktrees/<8charProjectHash>/<basename>`
537
+ *
538
+ * - Lives under the kimaki data dir instead of the long
539
+ * `~/.local/share/opencode/worktree/<40-char-hash>/<name>` path so folder
540
+ * names stay short and readable (agents tend to give up and reuse the old
541
+ * worktree when paths get absurdly long).
542
+ * - The 8-char project hash keeps worktrees from different projects that
543
+ * happen to share a slug from colliding.
544
+ * - Strips the `opencode/kimaki-` (or `opencode-kimaki-`) prefix from the
545
+ * folder name since it's redundant noise on disk. The git branch name
546
+ * itself still uses `opencode/kimaki-<slug>` so merge/cleanup logic is
547
+ * unchanged.
548
+ */
549
+ export function getManagedWorktreeDirectory({
534
550
  directory,
535
551
  name,
536
552
  }: {
537
553
  directory: string
538
554
  name: string
539
555
  }): string {
540
- const projectHash = crypto.createHash('sha1').update(directory).digest('hex')
541
- const safeName = name.replaceAll('/', '-')
542
- return path.join(
543
- os.homedir(),
544
- '.local',
545
- 'share',
546
- 'opencode',
547
- 'worktree',
548
- projectHash,
549
- safeName,
550
- )
556
+ const projectHash = crypto
557
+ .createHash('sha1')
558
+ .update(directory)
559
+ .digest('hex')
560
+ .slice(0, 8)
561
+ const withoutPrefix = name
562
+ .replace(/^opencode\/kimaki-/, '')
563
+ .replaceAll('/', '-')
564
+ return path.join(getDataDir(), 'worktrees', projectHash, withoutPrefix)
551
565
  }
552
566
 
553
567
  /**
@@ -724,6 +738,8 @@ export async function deleteWorktree({
724
738
  }: {
725
739
  projectDirectory: string
726
740
  worktreeDirectory: string
741
+ // Branch name to delete after removing the worktree.
742
+ // Pass empty string for detached HEAD worktrees — branch deletion is skipped.
727
743
  worktreeName: string
728
744
  }): Promise<void | Error> {
729
745
  let removeResult = await git(
@@ -749,24 +765,27 @@ export async function deleteWorktree({
749
765
  }
750
766
  }
751
767
  if (removeResult instanceof Error) {
752
- return new Error(`Failed to remove worktree ${worktreeName}`, {
768
+ return new Error(`Failed to remove worktree ${worktreeName || worktreeDirectory}`, {
753
769
  cause: removeResult,
754
770
  })
755
771
  }
756
772
 
757
- const deleteBranchResult = await git(
758
- projectDirectory,
759
- `branch -d ${JSON.stringify(worktreeName)}`,
760
- )
761
- if (deleteBranchResult instanceof Error) {
762
- return new Error(`Failed to delete branch ${worktreeName}`, {
763
- cause: deleteBranchResult,
764
- })
773
+ // Skip branch deletion for detached HEAD worktrees (no branch to delete)
774
+ if (worktreeName) {
775
+ const deleteBranchResult = await git(
776
+ projectDirectory,
777
+ `branch -d ${JSON.stringify(worktreeName)}`,
778
+ )
779
+ if (deleteBranchResult instanceof Error) {
780
+ return new Error(`Failed to delete branch ${worktreeName}`, {
781
+ cause: deleteBranchResult,
782
+ })
783
+ }
765
784
  }
766
785
 
767
786
  const pruneResult = await git(projectDirectory, 'worktree prune')
768
787
  if (pruneResult instanceof Error) {
769
- logger.warn(`Failed to prune worktrees after deleting ${worktreeName}`)
788
+ logger.warn(`Failed to prune worktrees after deleting ${worktreeName || worktreeDirectory}`)
770
789
  }
771
790
  }
772
791
 
@@ -1246,3 +1265,105 @@ export async function validateWorktreeDirectory({
1246
1265
 
1247
1266
  return absoluteCandidate
1248
1267
  }
1268
+
1269
+ // Parsed entry from `git worktree list --porcelain`.
1270
+ // Represents any worktree (kimaki, opencode, manual) visible to git.
1271
+ export type GitWorktree = {
1272
+ directory: string
1273
+ branch: string | null // null for detached HEAD
1274
+ head: string
1275
+ detached: boolean
1276
+ locked: boolean
1277
+ prunable: boolean
1278
+ }
1279
+
1280
+ type PartialGitWorktree = {
1281
+ directory?: string
1282
+ branch?: string | null
1283
+ head?: string
1284
+ detached?: boolean
1285
+ locked?: boolean
1286
+ prunable?: boolean
1287
+ }
1288
+
1289
+ function flushGitWorktreeEntry(current: PartialGitWorktree): GitWorktree | null {
1290
+ if (!current.directory) {
1291
+ return null
1292
+ }
1293
+ return {
1294
+ directory: current.directory,
1295
+ branch: current.branch ?? null,
1296
+ head: current.head ?? '',
1297
+ detached: current.detached ?? false,
1298
+ locked: current.locked ?? false,
1299
+ prunable: current.prunable ?? false,
1300
+ }
1301
+ }
1302
+
1303
+ // Parse `git worktree list --porcelain` output into structured entries.
1304
+ // Skips the first entry (the main checkout) since that's the project root.
1305
+ export function parseGitWorktreeListPorcelain(
1306
+ output: string,
1307
+ ): GitWorktree[] {
1308
+ const entries: GitWorktree[] = []
1309
+ let current: PartialGitWorktree = {}
1310
+
1311
+ for (const line of output.split('\n')) {
1312
+ if (line.startsWith('worktree ')) {
1313
+ const flushed = flushGitWorktreeEntry(current)
1314
+ if (flushed) {
1315
+ entries.push(flushed)
1316
+ }
1317
+ current = { directory: line.slice('worktree '.length) }
1318
+ continue
1319
+ }
1320
+ if (line.startsWith('HEAD ')) {
1321
+ current.head = line.slice('HEAD '.length)
1322
+ continue
1323
+ }
1324
+ if (line.startsWith('branch ')) {
1325
+ // "branch refs/heads/opencode/kimaki-foo" → "opencode/kimaki-foo"
1326
+ current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '')
1327
+ continue
1328
+ }
1329
+ if (line === 'detached') {
1330
+ current.detached = true
1331
+ continue
1332
+ }
1333
+ // "locked" or "locked <reason>"
1334
+ if (line === 'locked' || line.startsWith('locked ')) {
1335
+ current.locked = true
1336
+ continue
1337
+ }
1338
+ if (line.startsWith('prunable')) {
1339
+ current.prunable = true
1340
+ continue
1341
+ }
1342
+ }
1343
+ // Flush last entry
1344
+ const flushed = flushGitWorktreeEntry(current)
1345
+ if (flushed) {
1346
+ entries.push(flushed)
1347
+ }
1348
+
1349
+ // Skip the first entry — it's the main checkout (project root)
1350
+ return entries.slice(1)
1351
+ }
1352
+
1353
+ // List all git worktrees for a project directory (excluding the main checkout).
1354
+ // Returns Error on git failure, empty array if no worktrees exist.
1355
+ export async function listGitWorktrees({
1356
+ projectDirectory,
1357
+ timeout,
1358
+ }: {
1359
+ projectDirectory: string
1360
+ timeout?: number
1361
+ }): Promise<GitWorktree[] | Error> {
1362
+ const result = await git(projectDirectory, 'worktree list --porcelain', {
1363
+ timeout,
1364
+ })
1365
+ if (result instanceof Error) {
1366
+ return result
1367
+ }
1368
+ return parseGitWorktreeListPorcelain(result)
1369
+ }