@otto-assistant/bridge 0.4.92 → 0.4.96

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 (36) hide show
  1. package/dist/agent-model.e2e.test.js +2 -1
  2. package/dist/anthropic-auth-plugin.js +30 -9
  3. package/dist/anthropic-auth-plugin.test.js +7 -1
  4. package/dist/anthropic-auth-state.js +17 -1
  5. package/dist/cli.js +2 -3
  6. package/dist/commands/agent.js +2 -2
  7. package/dist/commands/merge-worktree.js +11 -8
  8. package/dist/context-awareness-plugin.js +1 -1
  9. package/dist/context-awareness-plugin.test.js +2 -2
  10. package/dist/discord-utils.js +5 -2
  11. package/dist/kimaki-opencode-plugin.js +1 -0
  12. package/dist/logger.js +8 -2
  13. package/dist/session-handler/thread-session-runtime.js +20 -3
  14. package/dist/system-message.js +13 -0
  15. package/dist/system-message.test.js +13 -0
  16. package/dist/system-prompt-drift-plugin.js +251 -0
  17. package/dist/utils.js +5 -1
  18. package/package.json +2 -1
  19. package/skills/npm-package/SKILL.md +14 -9
  20. package/src/agent-model.e2e.test.ts +2 -1
  21. package/src/anthropic-auth-plugin.test.ts +7 -1
  22. package/src/anthropic-auth-plugin.ts +29 -9
  23. package/src/anthropic-auth-state.ts +28 -2
  24. package/src/cli.ts +2 -2
  25. package/src/commands/agent.ts +2 -2
  26. package/src/commands/merge-worktree.ts +11 -8
  27. package/src/context-awareness-plugin.test.ts +2 -2
  28. package/src/context-awareness-plugin.ts +1 -1
  29. package/src/discord-utils.ts +19 -17
  30. package/src/kimaki-opencode-plugin.ts +1 -0
  31. package/src/logger.ts +9 -2
  32. package/src/session-handler/thread-session-runtime.ts +23 -3
  33. package/src/system-message.test.ts +13 -0
  34. package/src/system-message.ts +13 -0
  35. package/src/system-prompt-drift-plugin.ts +379 -0
  36. package/src/utils.ts +5 -1
@@ -0,0 +1,251 @@
1
+ // OpenCode plugin that detects per-session system prompt drift across turns.
2
+ // When the effective system prompt changes after the first user message, it
3
+ // writes a debug diff file and shows a toast because prompt-cache invalidation
4
+ // increases rate-limit usage and usually means another plugin is mutating the
5
+ // system prompt unexpectedly.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { createPatch, diffLines } from 'diff';
9
+ import * as errore from 'errore';
10
+ import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath } from './plugin-logger.js';
11
+ import { initSentry, notifyError } from './sentry.js';
12
+ import { abbreviatePath } from './utils.js';
13
+ const logger = createPluginLogger('OPENCODE');
14
+ const TOAST_SESSION_MARKER_SEPARATOR = ' ';
15
+ function getSystemPromptDiffDir({ dataDir }) {
16
+ return path.join(dataDir, 'system-prompt-diffs');
17
+ }
18
+ function normalizeSystemPrompt({ system }) {
19
+ return system.join('\n');
20
+ }
21
+ function appendToastSessionMarker({ message, sessionId, }) {
22
+ return `${message}${TOAST_SESSION_MARKER_SEPARATOR}${sessionId}`;
23
+ }
24
+ function buildTurnContext({ input, directory, }) {
25
+ const model = input.model
26
+ ? `${input.model.providerID}/${input.model.modelID}${input.variant ? `:${input.variant}` : ''}`
27
+ : undefined;
28
+ return {
29
+ agent: input.agent,
30
+ model,
31
+ directory,
32
+ };
33
+ }
34
+ function shouldSuppressDiffNotice({ previousContext, currentContext, }) {
35
+ if (!previousContext || !currentContext) {
36
+ return false;
37
+ }
38
+ return (previousContext.agent !== currentContext.agent
39
+ || previousContext.model !== currentContext.model
40
+ || previousContext.directory !== currentContext.directory);
41
+ }
42
+ function buildPatch({ beforeText, afterText, beforeLabel, afterLabel, }) {
43
+ const changes = diffLines(beforeText, afterText);
44
+ const additions = changes.reduce((count, change) => {
45
+ if (!change.added) {
46
+ return count;
47
+ }
48
+ return count + change.count;
49
+ }, 0);
50
+ const deletions = changes.reduce((count, change) => {
51
+ if (!change.removed) {
52
+ return count;
53
+ }
54
+ return count + change.count;
55
+ }, 0);
56
+ const patch = createPatch(afterLabel, beforeText, afterText, beforeLabel, afterLabel);
57
+ return {
58
+ additions,
59
+ deletions,
60
+ patch,
61
+ };
62
+ }
63
+ function writeSystemPromptDiffFile({ dataDir, sessionId, beforePrompt, afterPrompt, }) {
64
+ const diff = buildPatch({
65
+ beforeText: beforePrompt,
66
+ afterText: afterPrompt,
67
+ beforeLabel: 'system-before.txt',
68
+ afterLabel: 'system-after.txt',
69
+ });
70
+ const timestamp = new Date().toISOString().replaceAll(':', '-');
71
+ const sessionDir = path.join(getSystemPromptDiffDir({ dataDir }), sessionId);
72
+ const filePath = path.join(sessionDir, `${timestamp}.diff`);
73
+ const latestPromptPath = path.join(sessionDir, `${timestamp}.md`);
74
+ const fileContent = [
75
+ `Session: ${sessionId}`,
76
+ `Created: ${new Date().toISOString()}`,
77
+ `Additions: ${diff.additions}`,
78
+ `Deletions: ${diff.deletions}`,
79
+ '',
80
+ diff.patch,
81
+ ].join('\n');
82
+ return errore.try({
83
+ try: () => {
84
+ fs.mkdirSync(sessionDir, { recursive: true });
85
+ fs.writeFileSync(filePath, fileContent);
86
+ // fs.writeFileSync(latestPromptPath, afterPrompt)
87
+ return {
88
+ additions: diff.additions,
89
+ deletions: diff.deletions,
90
+ filePath,
91
+ latestPromptPath,
92
+ };
93
+ },
94
+ catch: (error) => {
95
+ return new Error('Failed to write system prompt diff file', { cause: error });
96
+ },
97
+ });
98
+ }
99
+ function getOrCreateSessionState({ sessions, sessionId, }) {
100
+ const existing = sessions.get(sessionId);
101
+ if (existing) {
102
+ return existing;
103
+ }
104
+ const state = {
105
+ userTurnCount: 0,
106
+ previousTurnPrompt: undefined,
107
+ latestTurnPrompt: undefined,
108
+ latestTurnPromptTurn: 0,
109
+ comparedTurn: 0,
110
+ previousTurnContext: undefined,
111
+ currentTurnContext: undefined,
112
+ };
113
+ sessions.set(sessionId, state);
114
+ return state;
115
+ }
116
+ async function handleSystemTransform({ input, output, sessions, dataDir, client, }) {
117
+ const sessionId = input.sessionID;
118
+ if (!sessionId) {
119
+ return;
120
+ }
121
+ const currentPrompt = normalizeSystemPrompt({ system: output.system });
122
+ const state = getOrCreateSessionState({
123
+ sessions,
124
+ sessionId,
125
+ });
126
+ const currentTurn = state.userTurnCount;
127
+ state.latestTurnPrompt = currentPrompt;
128
+ state.latestTurnPromptTurn = currentTurn;
129
+ if (currentTurn <= 1) {
130
+ return;
131
+ }
132
+ if (state.comparedTurn === currentTurn) {
133
+ return;
134
+ }
135
+ const previousPrompt = state.previousTurnPrompt;
136
+ state.comparedTurn = currentTurn;
137
+ if (!previousPrompt || previousPrompt === currentPrompt) {
138
+ return;
139
+ }
140
+ if (shouldSuppressDiffNotice({
141
+ previousContext: state.previousTurnContext,
142
+ currentContext: state.currentTurnContext,
143
+ })) {
144
+ return;
145
+ }
146
+ if (!dataDir) {
147
+ return;
148
+ }
149
+ const diffFileResult = writeSystemPromptDiffFile({
150
+ dataDir,
151
+ sessionId,
152
+ beforePrompt: previousPrompt,
153
+ afterPrompt: currentPrompt,
154
+ });
155
+ if (diffFileResult instanceof Error) {
156
+ throw diffFileResult;
157
+ }
158
+ await client.tui.showToast({
159
+ body: {
160
+ variant: 'info',
161
+ title: 'Context cache discarded',
162
+ message: appendToastSessionMarker({
163
+ sessionId,
164
+ message: `system prompt changed since the previous message (+${diffFileResult.additions} / -${diffFileResult.deletions}). ` +
165
+ `Diff: \`${abbreviatePath(diffFileResult.filePath)}\`. ` +
166
+ `Latest prompt: \`${abbreviatePath(diffFileResult.latestPromptPath)}\``,
167
+ }),
168
+ },
169
+ });
170
+ }
171
+ const systemPromptDriftPlugin = async ({ client, directory }) => {
172
+ initSentry();
173
+ const dataDir = process.env.KIMAKI_DATA_DIR;
174
+ if (dataDir) {
175
+ setPluginLogFilePath(dataDir);
176
+ }
177
+ const sessions = new Map();
178
+ return {
179
+ 'chat.message': async (input) => {
180
+ const sessionId = input.sessionID;
181
+ if (!sessionId) {
182
+ return;
183
+ }
184
+ const state = getOrCreateSessionState({ sessions, sessionId });
185
+ if (state.userTurnCount > 0
186
+ && state.latestTurnPromptTurn === state.userTurnCount) {
187
+ state.previousTurnPrompt = state.latestTurnPrompt;
188
+ state.previousTurnContext = state.currentTurnContext;
189
+ }
190
+ state.currentTurnContext = buildTurnContext({ input, directory });
191
+ state.userTurnCount += 1;
192
+ },
193
+ 'experimental.chat.system.transform': async (input, output) => {
194
+ const result = await errore.tryAsync({
195
+ try: async () => {
196
+ await handleSystemTransform({
197
+ input,
198
+ output,
199
+ sessions,
200
+ dataDir,
201
+ client,
202
+ });
203
+ },
204
+ catch: (error) => {
205
+ return new Error('system prompt drift transform hook failed', {
206
+ cause: error,
207
+ });
208
+ },
209
+ });
210
+ if (result instanceof Error) {
211
+ logger.warn(`[system-prompt-drift-plugin] ${formatPluginErrorWithStack(result)}`);
212
+ void notifyError(result, 'system prompt drift plugin transform hook failed');
213
+ }
214
+ },
215
+ event: async ({ event }) => {
216
+ const result = await errore.tryAsync({
217
+ try: async () => {
218
+ if (event.type !== 'session.deleted') {
219
+ return;
220
+ }
221
+ const deletedSessionId = getDeletedSessionId({ event });
222
+ if (!deletedSessionId) {
223
+ return;
224
+ }
225
+ sessions.delete(deletedSessionId);
226
+ },
227
+ catch: (error) => {
228
+ return new Error('system prompt drift event hook failed', {
229
+ cause: error,
230
+ });
231
+ },
232
+ });
233
+ if (result instanceof Error) {
234
+ logger.warn(`[system-prompt-drift-plugin] ${formatPluginErrorWithStack(result)}`);
235
+ void notifyError(result, 'system prompt drift plugin event hook failed');
236
+ }
237
+ },
238
+ };
239
+ };
240
+ function getDeletedSessionId({ event }) {
241
+ if (event.type !== 'session.deleted') {
242
+ return undefined;
243
+ }
244
+ const sessionInfo = event.properties?.info;
245
+ if (!sessionInfo || typeof sessionInfo !== 'object') {
246
+ return undefined;
247
+ }
248
+ const id = 'id' in sessionInfo ? sessionInfo.id : undefined;
249
+ return typeof id === 'string' ? id : undefined;
250
+ }
251
+ export { systemPromptDriftPlugin };
package/dist/utils.js CHANGED
@@ -2,7 +2,11 @@
2
2
  // Includes Discord OAuth URL generation, array deduplication,
3
3
  // abort error detection, and date/time formatting helpers.
4
4
  import os from 'node:os';
5
- import { PermissionsBitField } from 'discord.js';
5
+ // Use namespace import for CJS interop discord.js is CJS and its named
6
+ // exports aren't detectable by all ESM loaders (e.g. tsx/esbuild) because
7
+ // discord.js uses tslib's __exportStar which is opaque to static analysis.
8
+ import * as discord from 'discord.js';
9
+ const { PermissionsBitField } = discord;
6
10
  import * as errore from 'errore';
7
11
  export function generateBotInstallUrl({ clientId, permissions = [
8
12
  PermissionsBitField.Flags.ViewChannel,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@otto-assistant/bridge",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.92",
5
+ "version": "0.4.96",
6
6
  "scripts": {
7
7
  "dev": "tsx src/bin.ts",
8
8
  "prepublishOnly": "pnpm generate && pnpm tsc",
@@ -66,6 +66,7 @@
66
66
  "@prisma/client": "7.4.2",
67
67
  "@purinton/resampler": "^1.0.4",
68
68
  "cron-parser": "^5.5.0",
69
+ "diff": "^8.0.4",
69
70
  "discord.js": "^14.25.1",
70
71
  "domhandler": "^6.0.1",
71
72
  "errore": "workspace:^",
@@ -39,15 +39,20 @@ Use this skill when scaffolding or fixing npm packages.
39
39
  - any runtime-required extra files (for example `schema.prisma`)
40
40
  - docs like `README.md` and `CHANGELOG.md`
41
41
  - if tests are inside src and gets included in dist, it's fine. don't try to exclude them
42
- 10. `scripts.build` should be `rimraf dist "*.tsbuildinfo" && tsc && chmod +x dist/cli.js` (skip the chmod
43
- if the package has no bin). No bundling. We remove dist to cleanup old transpiled files. Use `rimraf` here instead of bare shell globs so the script behaves the same in zsh, bash, and Windows shells even when no `.tsbuildinfo` file exists. This also removes the tsc incremental compilation state. Without that tsc would not generate again files to dist.
44
- Optionally include running scripts with tsx if needed to generate build artifacts.
45
- 11. `prepublishOnly` must always run `build` (optionally run generation before
46
- build when required). Always add this script:
42
+ 10. `scripts.build` should be `tsc && chmod +x dist/cli.js` (skip the chmod if
43
+ the package has no bin). No bundling. Do not delete `dist/` in `build` by
44
+ default because forcing a clean build on every local build can cause
45
+ issues. Optionally include running scripts with `tsx` if needed to
46
+ generate build artifacts.
47
+ 11. `prepublishOnly` must always do the cleanup before `build` (optionally run
48
+ generation before build when required). Always add this script:
47
49
  ```json
48
- { "prepublishOnly": "pnpm build" }
50
+ { "prepublishOnly": "rimraf dist \"*.tsbuildinfo\" && pnpm build" }
49
51
  ```
50
- This ensures `dist/` is fresh before every `npm publish`.
52
+ This ensures `dist/` is fresh before every `npm publish`, so deleted files
53
+ do not accidentally stay in the published package. Use `rimraf` here
54
+ instead of bare shell globs so the script behaves the same in zsh, bash,
55
+ and Windows shells even when no `.tsbuildinfo` file exists.
51
56
 
52
57
  ## bin field
53
58
 
@@ -71,8 +76,8 @@ Add the shebang as the first line of the source file (`src/cli.ts`):
71
76
  ```
72
77
 
73
78
  `tsc` preserves the shebang in the emitted `.js` file. The `chmod +x` is
74
- already part of the `build` script, so `prepublishOnly: "pnpm build"` handles
75
- it automatically.
79
+ already part of the `build` script, so `prepublishOnly` still gets it through
80
+ `pnpm build` after the cleanup step.
76
81
 
77
82
  ## Reading package version at runtime
78
83
 
@@ -950,7 +950,8 @@ describe('agent model resolution', () => {
950
950
  --- from: assistant (TestBot)
951
951
  ⬥ ok
952
952
  *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
953
- Switched to **plan** agent for this session next messages (was **test-agent**)
953
+ Switched to **plan** agent for this session (was **test-agent**)
954
+ The agent will change on the next message.
954
955
  --- from: user (agent-model-tester)
955
956
  Reply with exactly: after-switch-msg
956
957
  --- from: assistant (TestBot)
@@ -88,7 +88,13 @@ describe('rotateAnthropicAccount', () => {
88
88
  anthropic?: { refresh?: string }
89
89
  }
90
90
 
91
- expect(rotated).toMatchObject({ refresh: 'refresh-second' })
91
+ expect(rotated).toMatchObject({
92
+ auth: { refresh: 'refresh-second' },
93
+ fromLabel: '#1 (refresh-...irst)',
94
+ toLabel: '#2 (refresh-...cond)',
95
+ fromIndex: 0,
96
+ toIndex: 1,
97
+ })
92
98
  expect(store.activeIndex).toBe(1)
93
99
  expect(authJson.anthropic?.refresh).toBe('refresh-second')
94
100
  expect(authSetCalls).toEqual([
@@ -528,17 +528,26 @@ function toClaudeCodeToolName(name: string) {
528
528
  return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name
529
529
  }
530
530
 
531
- function sanitizeSystemText(text: string) {
532
- return text.replaceAll(OPENCODE_IDENTITY, CLAUDE_CODE_IDENTITY)
531
+ function sanitizeSystemText(text: string, onError?: (msg: string) => void) {
532
+ const startIdx = text.indexOf(OPENCODE_IDENTITY)
533
+ if (startIdx === -1) return text
534
+ const codeRefsMarker = '# Code References'
535
+ const endIdx = text.indexOf(codeRefsMarker, startIdx)
536
+ if (endIdx === -1) {
537
+ onError?.(`sanitizeSystemText: could not find '# Code References' after OpenCode identity`)
538
+ return text
539
+ }
540
+ // Remove everything from the OpenCode identity up to (but not including) '# Code References'
541
+ return text.slice(0, startIdx) + text.slice(endIdx)
533
542
  }
534
543
 
535
- function prependClaudeCodeIdentity(system: unknown) {
544
+ function prependClaudeCodeIdentity(system: unknown, onError?: (msg: string) => void) {
536
545
  const identityBlock = { type: 'text', text: CLAUDE_CODE_IDENTITY }
537
546
 
538
547
  if (typeof system === 'undefined') return [identityBlock]
539
548
 
540
549
  if (typeof system === 'string') {
541
- const sanitized = sanitizeSystemText(system)
550
+ const sanitized = sanitizeSystemText(system, onError)
542
551
  if (sanitized === CLAUDE_CODE_IDENTITY) return [identityBlock]
543
552
  return [identityBlock, { type: 'text', text: sanitized }]
544
553
  }
@@ -546,11 +555,11 @@ function prependClaudeCodeIdentity(system: unknown) {
546
555
  if (!Array.isArray(system)) return [identityBlock, system]
547
556
 
548
557
  const sanitized = system.map((item) => {
549
- if (typeof item === 'string') return { type: 'text', text: sanitizeSystemText(item) }
558
+ if (typeof item === 'string') return { type: 'text', text: sanitizeSystemText(item, onError) }
550
559
  if (item && typeof item === 'object' && (item as { type?: unknown }).type === 'text') {
551
560
  const text = (item as { text?: unknown }).text
552
561
  if (typeof text === 'string') {
553
- return { ...(item as Record<string, unknown>), text: sanitizeSystemText(text) }
562
+ return { ...(item as Record<string, unknown>), text: sanitizeSystemText(text, onError) }
554
563
  }
555
564
  }
556
565
  return item
@@ -568,7 +577,7 @@ function prependClaudeCodeIdentity(system: unknown) {
568
577
  return [identityBlock, ...sanitized]
569
578
  }
570
579
 
571
- function rewriteRequestPayload(body: string | undefined) {
580
+ function rewriteRequestPayload(body: string | undefined, onError?: (msg: string) => void) {
572
581
  if (!body) return { body, modelId: undefined, reverseToolNameMap: new Map<string, string>() }
573
582
 
574
583
  try {
@@ -589,7 +598,7 @@ function rewriteRequestPayload(body: string | undefined) {
589
598
  }
590
599
 
591
600
  // Rename system prompt
592
- payload.system = prependClaudeCodeIdentity(payload.system)
601
+ payload.system = prependClaudeCodeIdentity(payload.system, onError)
593
602
 
594
603
  // Rename tool_choice
595
604
  if (
@@ -779,7 +788,11 @@ const AnthropicAuthPlugin: Plugin = async ({ client }) => {
779
788
  .catch(() => undefined)
780
789
  : undefined
781
790
 
782
- const rewritten = rewriteRequestPayload(originalBody)
791
+ const rewritten = rewriteRequestPayload(originalBody, (msg) => {
792
+ client.tui.showToast({
793
+ body: { message: msg, variant: 'error' },
794
+ }).catch(() => {})
795
+ })
783
796
  const headers = new Headers(init?.headers)
784
797
  if (input instanceof Request) {
785
798
  input.headers.forEach((v, k) => {
@@ -823,6 +836,13 @@ const AnthropicAuthPlugin: Plugin = async ({ client }) => {
823
836
  if (shouldRotateAuth(response.status, bodyText)) {
824
837
  const rotated = await rotateAnthropicAccount(freshAuth, client)
825
838
  if (rotated) {
839
+ // Show toast notification so Discord thread shows the rotation
840
+ client.tui.showToast({
841
+ body: {
842
+ message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
843
+ variant: 'info',
844
+ },
845
+ }).catch(() => {})
826
846
  const retryAuth = await getFreshOAuth(getAuth, client)
827
847
  if (retryAuth) {
828
848
  response = await runRequest(retryAuth)
@@ -133,6 +133,21 @@ export async function saveAccountStore(store: AccountStore) {
133
133
  await writeJson(accountsFilePath(), normalizeAccountStore(store))
134
134
  }
135
135
 
136
+ /** Short label for an account: first 8 + last 4 chars of refresh token. */
137
+ export function accountLabel(account: OAuthStored, index?: number): string {
138
+ const r = account.refresh
139
+ const short = r.length > 12 ? `${r.slice(0, 8)}...${r.slice(-4)}` : r
140
+ return index !== undefined ? `#${index + 1} (${short})` : short
141
+ }
142
+
143
+ export type RotationResult = {
144
+ auth: OAuthStored
145
+ fromLabel: string
146
+ toLabel: string
147
+ fromIndex: number
148
+ toIndex: number
149
+ }
150
+
136
151
  function findCurrentAccountIndex(store: AccountStore, auth: OAuthStored) {
137
152
  if (!store.accounts.length) return 0
138
153
  const byRefresh = store.accounts.findIndex((account) => {
@@ -206,16 +221,21 @@ export async function setAnthropicAuth(
206
221
  export async function rotateAnthropicAccount(
207
222
  auth: OAuthStored,
208
223
  client: Parameters<Plugin>[0]['client'],
209
- ) {
224
+ ): Promise<RotationResult | undefined> {
210
225
  return withAuthStateLock(async () => {
211
226
  const store = await loadAccountStore()
212
227
  if (store.accounts.length < 2) return undefined
213
228
 
214
229
  const currentIndex = findCurrentAccountIndex(store, auth)
230
+ const currentAccount = store.accounts[currentIndex]
215
231
  const nextIndex = (currentIndex + 1) % store.accounts.length
216
232
  const nextAccount = store.accounts[nextIndex]
217
233
  if (!nextAccount) return undefined
218
234
 
235
+ const fromLabel = currentAccount
236
+ ? accountLabel(currentAccount, currentIndex)
237
+ : accountLabel(auth, currentIndex)
238
+
219
239
  nextAccount.lastUsed = Date.now()
220
240
  store.activeIndex = nextIndex
221
241
  await saveAccountStore(store)
@@ -227,7 +247,13 @@ export async function rotateAnthropicAccount(
227
247
  expires: nextAccount.expires,
228
248
  }
229
249
  await setAnthropicAuth(nextAuth, client)
230
- return nextAuth
250
+ return {
251
+ auth: nextAuth,
252
+ fromLabel,
253
+ toLabel: accountLabel(nextAccount, nextIndex),
254
+ fromIndex: currentIndex,
255
+ toIndex: nextIndex,
256
+ }
231
257
  })
232
258
  }
233
259
 
package/src/cli.ts CHANGED
@@ -125,6 +125,7 @@ import {
125
125
  type ScheduledTaskPayload,
126
126
  } from './task-schedule.js'
127
127
  import {
128
+ accountLabel,
128
129
  accountsFilePath,
129
130
  loadAccountStore,
130
131
  removeAccount,
@@ -3178,8 +3179,7 @@ cli
3178
3179
 
3179
3180
  store.accounts.forEach((account, index) => {
3180
3181
  const active = index === store.activeIndex ? '*' : ' '
3181
- const label = `${account.refresh.slice(0, 8)}...${account.refresh.slice(-4)}`
3182
- console.log(`${active} ${index + 1}. ${label}`)
3182
+ console.log(`${active} ${index + 1}. ${accountLabel(account)}`)
3183
3183
  })
3184
3184
 
3185
3185
  process.exit(0)
@@ -379,7 +379,7 @@ export async function handleAgentSelectMenu(
379
379
 
380
380
  if (context.isThread && context.sessionId) {
381
381
  await interaction.editReply({
382
- content: `Agent preference set for this session next messages: **${selectedAgent}**`,
382
+ content: `Agent preference set for this session: **${selectedAgent}**\nThe agent will change on the next message.`,
383
383
  components: [],
384
384
  })
385
385
  } else {
@@ -457,7 +457,7 @@ export async function handleQuickAgentCommand({
457
457
 
458
458
  if (context.isThread && context.sessionId) {
459
459
  await command.editReply({
460
- content: `Switched to **${resolvedAgentName}** agent for this session next messages${previousText}`,
460
+ content: `Switched to **${resolvedAgentName}** agent for this session${previousText}\nThe agent will change on the next message.`,
461
461
  })
462
462
  } else {
463
463
  await command.editReply({
@@ -155,15 +155,18 @@ export async function handleMergeWorktreeCommand({
155
155
  )
156
156
  await sendPromptToModel({
157
157
  prompt: [
158
- 'A rebase conflict occurred while merging this worktree into the default branch.',
158
+ `A rebase conflict occurred while merging this worktree into \`${result.target}\`.`,
159
159
  'Rebasing multiple commits can pause on each commit that conflicts, so you may need to repeat the resolve/continue loop several times.',
160
- 'Please resolve the rebase conflicts:',
161
- '1. Check `git status` to see which files have conflicts',
162
- '2. Edit the conflicted files to resolve the merge markers',
163
- '3. Stage resolved files with `git add`',
164
- '4. Continue the rebase with `git rebase --continue`',
165
- '5. If git reports more conflicts, repeat steps 1-4 until the rebase finishes (no more MERGE markers, `git status` shows no rebase in progress)',
166
- '6. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
160
+ 'Before editing anything, first understand both sides so you preserve both intentions and do not drop features or fixes.',
161
+ '1. Check `git status` to see which files have conflicts and confirm the rebase is paused',
162
+ `2. Find the merge base between this worktree and \`${result.target}\`, then read the commit messages from both sides since that merge base so you understand the goal of each change`,
163
+ `3. Read the diffs from that merge base to both sides so you understand exactly what changed on this branch and on \`${result.target}\` before resolving conflicts`,
164
+ '4. Read the commit currently being replayed in the rebase so you know the intent of the specific conflicting patch',
165
+ '5. Edit the conflicted files to preserve both intended changes where possible instead of choosing one side wholesale',
166
+ '6. Stage resolved files with `git add`',
167
+ '7. Continue the rebase with `git rebase --continue`',
168
+ '8. If git reports more conflicts, repeat steps 1-7 until the rebase finishes (no more rebase in progress, `git status` is clean)',
169
+ '9. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
167
170
  ].join('\n'),
168
171
  thread,
169
172
  projectDirectory: worktreeInfo.project_directory,
@@ -46,7 +46,7 @@ describe('shouldInjectPwd', () => {
46
46
  {
47
47
  "inject": true,
48
48
  "text": "
49
- [working directory changed. Previous working directory: /repo/main. Current working directory: /repo/worktree. You MUST read, write, and edit files only under /repo/worktree. Do NOT read, write, or edit files under /repo/main.]",
49
+ [working directory changed. Previous working directory: /repo/main. Current working directory: /repo/worktree. You should read, write, and edit files under /repo/worktree. Do NOT read, write, or edit files under /repo/main.]",
50
50
  }
51
51
  `)
52
52
  })
@@ -62,7 +62,7 @@ describe('shouldInjectPwd', () => {
62
62
  {
63
63
  "inject": true,
64
64
  "text": "
65
- [working directory changed. Previous working directory: /repo/worktree-a. Current working directory: /repo/worktree-b. You MUST read, write, and edit files only under /repo/worktree-b. Do NOT read, write, or edit files under /repo/worktree-a.]",
65
+ [working directory changed. Previous working directory: /repo/worktree-a. Current working directory: /repo/worktree-b. You should read, write, and edit files under /repo/worktree-b. Do NOT read, write, or edit files under /repo/worktree-a.]",
66
66
  }
67
67
  `)
68
68
  })
@@ -126,7 +126,7 @@ export function shouldInjectPwd({
126
126
  text:
127
127
  `\n[working directory changed. Previous working directory: ${priorDirectory}. ` +
128
128
  `Current working directory: ${currentDir}. ` +
129
- `You MUST read, write, and edit files only under ${currentDir}. ` +
129
+ `You should read, write, and edit files under ${currentDir}. ` +
130
130
  `Do NOT read, write, or edit files under ${priorDirectory}.]`,
131
131
  }
132
132
  }