@openchambery/web 1.19.14 → 1.19.15-beta.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.
Files changed (58) hide show
  1. package/dist/assets/{AssistantView-Dncbu_Z2.js → AssistantView-D5rEZ-Ss.js} +2 -2
  2. package/dist/assets/{DiagramView-CovEl1BY.js → DiagramView-DaB9Sb7S.js} +1 -1
  3. package/dist/assets/MarkdownRenderer-emLeoNjI.js +2 -0
  4. package/dist/assets/{MarkdownRendererImpl-CCfHY-86.js → MarkdownRendererImpl-BWCAPlqn.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-y6QgIUZA.js → MarkstreamRendererImpl-BD_NFKUv.js} +1 -1
  6. package/dist/assets/MobileDetailNavigation-DqXVAIap.js +1 -0
  7. package/dist/assets/{MobileShareBridge-B4S7BaUj.js → MobileShareBridge-Bk2fdkJk.js} +1 -1
  8. package/dist/assets/MobileSurface-C4mpcOCh.js +2 -0
  9. package/dist/assets/{MultiRunWindow-BeHa-CG6.js → MultiRunWindow-rs49_K1E.js} +1 -1
  10. package/dist/assets/{SettingsView-Dn7odtQz.js → SettingsView-b8LUjoad.js} +2 -2
  11. package/dist/assets/{SettingsWindow-C0tWPS5f.js → SettingsWindow-D8JFlPdA.js} +1 -1
  12. package/dist/assets/{TerminalView-BnJgC7Qo.js → TerminalView-DCd-Godi.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-D9S-X8hM.js → ToolOutputDialog-B50L_nUP.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-DBrvaJnq.js → appThemeRegistry-BWtCaTMe.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-Dc004xOh.js → fileMentionTouchSelection-DKYkoypI.js} +1 -1
  16. package/dist/assets/{gitApi-Cm3JLQqh.js → gitApi-D4PyYgLZ.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-CTWYFsbH.js → insertionBoundaries-Bnnx03Wj.js} +1 -1
  18. package/dist/assets/{main-CSqu3MB4.js → main-BRiQTNFX.js} +4 -4
  19. package/dist/assets/{main-DwMprEyr.js → main-CnY0LWNc.js} +2 -2
  20. package/dist/assets/{miniChat-C8bzY_Ye.js → miniChat-B0pw_mUs.js} +2 -2
  21. package/dist/assets/{mobile-Oy-SRKkr.js → mobile-BKHmxwXz.js} +2 -2
  22. package/dist/assets/{multirun-ClcuaiKU.js → multirun-BQKqVfFT.js} +1 -1
  23. package/dist/assets/{projectMeta-F93wE2Ye.js → projectMeta-BvG8sdtC.js} +1 -1
  24. package/dist/assets/{renderElectronMiniChatApp-rOOhMVlQ.js → renderElectronMiniChatApp-E5a0yy7E.js} +2 -2
  25. package/dist/assets/{renderMobileApp-BP_hGwHP.js → renderMobileApp-DT0L4Ah6.js} +4 -4
  26. package/dist/assets/{runtimeConfig-iJ2mTBVN.js → runtimeConfig-utWzrDwa.js} +1 -1
  27. package/dist/assets/{runtimeEndpointReset-ISnhAS3q.js → runtimeEndpointReset-DE6lwHyg.js} +1 -1
  28. package/dist/assets/sessionChangeSummary-BG7l2heP.js +2 -0
  29. package/dist/assets/{sessionLookup-DQQ9-DuP.js → sessionLookup-12zVOR0J.js} +1 -1
  30. package/dist/assets/src-CFUi4riW.css +2 -0
  31. package/dist/assets/{useAppFontEffects--IcU7jyv.js → useAppFontEffects-BhYuP4L2.js} +5 -5
  32. package/dist/assets/{useDesktopWindowControlsLayout-BqJ5R_T2.js → useDesktopWindowControlsLayout-DPNMRkrI.js} +1 -1
  33. package/dist/assets/{useEffectiveDirectory-ChWXV8xM.js → useEffectiveDirectory-Ji8AKgzY.js} +1 -1
  34. package/dist/assets/{useMobileNavigationStore-DQkgxuzg.js → useMobileNavigationStore-BYdpuwL7.js} +1 -1
  35. package/dist/assets/{useSessionAutoCleanup-DWWAvQQ0.js → useSessionAutoCleanup-CADZCvZL.js} +1 -1
  36. package/dist/assets/{useWorkerHighlightedLines-D2oQszsk.js → useWorkerHighlightedLines-CuG88Lsj.js} +1 -1
  37. package/dist/index.html +3 -3
  38. package/dist/mini-chat.html +3 -3
  39. package/dist/mobile.html +3 -3
  40. package/package.json +1 -1
  41. package/server/lib/assistants/DOCUMENTATION.md +33 -12
  42. package/server/lib/assistants/contact-store.js +25 -1
  43. package/server/lib/assistants/contact-store.test.js +39 -5
  44. package/server/lib/assistants/contact-tools.js +3 -3
  45. package/server/lib/assistants/harness.js +136 -101
  46. package/server/lib/assistants/harness.test.js +235 -8
  47. package/server/lib/assistants/pi-tools.js +2 -0
  48. package/server/lib/assistants/pi-tools.test.js +8 -0
  49. package/server/lib/assistants/service.js +313 -74
  50. package/server/lib/assistants/service.test.js +555 -18
  51. package/server/lib/llm/DOCUMENTATION.md +9 -4
  52. package/server/lib/llm/generate.js +45 -6
  53. package/server/lib/llm/generate.test.js +52 -0
  54. package/dist/assets/MarkdownRenderer-CZqok-Cf.js +0 -2
  55. package/dist/assets/MobileDetailNavigation-Ca-d5eZ9.js +0 -1
  56. package/dist/assets/MobileSurface-xL5Jby4U.js +0 -2
  57. package/dist/assets/sessionChangeSummary-BAzU2J1H.js +0 -2
  58. package/dist/assets/src-DYeppLfP.css +0 -2
@@ -13,10 +13,47 @@ import {
13
13
  NEW_CONVERSATION_TOOL_NAME,
14
14
  createContactTools,
15
15
  } from './contact-tools.js'
16
- import { createContactStreamFn, resolveContactLanguage, runContactTurn } from './harness.js'
16
+ import {
17
+ createContactStreamFn,
18
+ projectStreamedContactTurnBubbles,
19
+ resolveContactLanguage,
20
+ runContactTurn,
21
+ } from './harness.js'
17
22
  import { PI_CODING_TOOL_NAMES } from './pi-tools.js'
18
23
 
19
24
  describe('createContactStreamFn', () => {
25
+ it('runs managed contact lookups from the user home with the coordination policy', async () => {
26
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-contact-home-'))
27
+ const homeSpy = vi.spyOn(os, 'homedir').mockReturnValue(home)
28
+ fs.writeFileSync(path.join(home, 'example-config.txt'), 'configured value')
29
+ const createChatCompletion = vi.fn(async ({ body }) => {
30
+ const system = body.messages[0].content
31
+ expect(system).toContain(`Working directory: ${home}`)
32
+ expect(system).toContain('personable contact focused on understanding the user')
33
+ expect(system).toContain('default to finding the relevant existing OpenCode workspace')
34
+ expect(system).toContain('small, clearly scoped configuration changes directly')
35
+ expect(system).toContain('walk its parent directories')
36
+ expect(system).toContain('Look up existing OpenCode conversations with list_sessions')
37
+ const result = body.messages.find((message) => typeof message.content === 'string' && message.content.includes('tool result name=read'))
38
+ if (result) {
39
+ expect(result.content).toContain('configured value')
40
+ return { completion: { choices: [{ message: { content: '```openchamber-final\n{"status":"complete","text":"configured value"}\n```' } }] } }
41
+ }
42
+ return { completion: { choices: [{ message: { content: '```openchamber-tool\n{"name":"read","arguments":{"path":"example-config.txt"}}\n```' } }] } }
43
+ })
44
+ try {
45
+ const result = await runContactTurn({
46
+ assistant: { providerID: 'p', modelID: 'm', workspacePath: null, effectiveWorkspacePath: path.join(home, 'managed') },
47
+ history: [], userText: 'Read example-config.txt', skillHomeDir: home, createChatCompletion,
48
+ })
49
+ expect(result.text).toContain('configured value')
50
+ expect(createChatCompletion).toHaveBeenCalledTimes(2)
51
+ } finally {
52
+ homeSpy.mockRestore()
53
+ fs.rmSync(home, { recursive: true, force: true })
54
+ }
55
+ })
56
+
20
57
  it('forwards completion text as pi-ai text events', async () => {
21
58
  const createChatCompletion = vi.fn(async () => ({
22
59
  completion: { choices: [{ message: { content: 'Hi' } }] },
@@ -293,6 +330,43 @@ describe('createContactStreamFn', () => {
293
330
  { index: 1, delta: 'Two.', done: true },
294
331
  ])
295
332
  })
333
+
334
+ it('continues bubble indices after prior turn streamed assistant messages (tool then final)', async () => {
335
+ const bubbleDeltas = []
336
+ const createChatCompletion = vi.fn(async () => ({
337
+ completion: { choices: [{ message: { content: 'Final B.\n\nFinal C.' } }] },
338
+ }))
339
+ const streamFn = createContactStreamFn(createChatCompletion, {
340
+ onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
341
+ turnMessageStart: 0,
342
+ })
343
+ const priorTurn = [
344
+ { role: 'user', content: 'do it', timestamp: 1 },
345
+ {
346
+ role: 'assistant',
347
+ content: [
348
+ { type: 'text', text: 'Preamble A' },
349
+ { type: 'toolCall', id: 'call_1', name: 'assign_session', arguments: { prompt: 'x' } },
350
+ ],
351
+ },
352
+ {
353
+ role: 'toolResult',
354
+ toolName: 'assign_session',
355
+ toolCallId: 'call_1',
356
+ content: [{ type: 'text', text: 'Opened a coding session.' }],
357
+ },
358
+ ]
359
+ expect(projectStreamedContactTurnBubbles(priorTurn, 0)).toEqual(['Preamble A'])
360
+ const stream = streamFn(
361
+ { name: 'openai/gpt-5.2', id: 'gpt-5.2', provider: 'openchamber', api: 'openai-completions' },
362
+ { messages: priorTurn },
363
+ )
364
+ for await (const event of stream) void event
365
+ expect(bubbleDeltas).toEqual([
366
+ { index: 1, delta: 'Final B.', done: true },
367
+ { index: 2, delta: 'Final C.', done: true },
368
+ ])
369
+ })
296
370
  })
297
371
 
298
372
  describe('runContactTurn', () => {
@@ -607,8 +681,8 @@ describe('runContactTurn', () => {
607
681
  createChatCompletion: vi.fn(), AgentImpl,
608
682
  })
609
683
  expect(prompts).toHaveLength(2)
610
- // assign toolText is not a user bubble; no spoken/card in this fixture.
611
- expect(result.bubbles).toEqual([])
684
+ // assign toolText is not a user bubble. Turn-boundary keeps pre-retry assistant prose.
685
+ expect(result.bubbles).toEqual(['Only document search is available.'])
612
686
  expect(result.bubbles.join('\n')).not.toContain('Opened session.')
613
687
  })
614
688
  it('runs pi-agent-core with thinking off and no tools by default', async () => {
@@ -786,7 +860,7 @@ describe('runContactTurn', () => {
786
860
  expect(options.initialState.systemPrompt).toContain('说一声')
787
861
  expect(options.initialState.systemPrompt).toContain('A reply without the tool call does nothing')
788
862
  expect(options.initialState.systemPrompt).toContain('已创建')
789
- expect(options.initialState.systemPrompt).toContain('cannot see the registered project list')
863
+ expect(options.initialState.systemPrompt).toContain('You receive the registered project catalog every turn')
790
864
  this.state = { ...options.initialState, messages: [] }
791
865
  this.prompt = async () => {
792
866
  this.state.messages = [
@@ -1065,7 +1139,8 @@ describe('runContactTurn', () => {
1065
1139
  name: 'FlowNL',
1066
1140
  assistantID: 'asst_flow',
1067
1141
  })])
1068
- expect(result.bubbles.join('')).not.toContain('好的,我来直接创建')
1142
+ // Turn-boundary keeps pre-retry spoken; English tool confirm still stays out.
1143
+ expect(result.bubbles.join('')).toContain('好的,我来直接创建')
1069
1144
  expect(result.bubbles.join('')).not.toContain('Created assistant FlowNL')
1070
1145
  expect(result.cards).toHaveLength(1)
1071
1146
  expect(result.thinkingLevel).toBe('off')
@@ -1104,7 +1179,7 @@ describe('runContactTurn', () => {
1104
1179
  expect(createAssistant).not.toHaveBeenCalled()
1105
1180
  expect(result.cards).toEqual([])
1106
1181
  // Prefer existing assistant text over the English missed-tool fallback.
1107
- expect(result.bubbles).toEqual(['好的,已创建。'])
1182
+ expect(result.bubbles).toEqual(['好的,已创建。', '好的,已创建。'])
1108
1183
  expect(result.bubbles).not.toEqual([MISSED_TOOL_FAILURE_BUBBLE])
1109
1184
  })
1110
1185
 
@@ -1163,9 +1238,9 @@ describe('runContactTurn', () => {
1163
1238
  text: 'hello-from-assistant 写好了',
1164
1239
  })
1165
1240
  expect(result.cards).toEqual([])
1166
- // English tool confirm must not leak; retry slice has no spoken preamble.
1241
+ // English tool confirm must not leak. Turn-boundary projection keeps the pre-retry spoken line.
1167
1242
  expect(result.bubbles.join('')).not.toContain('Sent to PeerQA')
1168
- expect(result.bubbles.join('')).not.toContain('好的,我去说一声')
1243
+ expect(result.bubbles.join('')).toContain('好的,我去说一声')
1169
1244
  })
1170
1245
 
1171
1246
  it('keeps only the new_conversation confirm and drops leftover attachment text', async () => {
@@ -1361,6 +1436,158 @@ describe('rejected response execution evidence', () => {
1361
1436
  })
1362
1437
  })
1363
1438
 
1439
+ describe('runContactTurn bubble index continuity', () => {
1440
+ const fence = (name, args) => '```openchamber-tool\n' + JSON.stringify({ name, arguments: args }) + '\n```'
1441
+ const completionFromReplies = (replies) => {
1442
+ const queue = [...replies]
1443
+ return vi.fn(async () => ({
1444
+ completion: { choices: [{ message: { content: queue.shift() || '' } }] },
1445
+ }))
1446
+ }
1447
+ const readSessionTools = (sessionID = 'ses_idx') => createContactTools({
1448
+ readSession: async () => ({
1449
+ sessionID,
1450
+ messages: [{ messageID: 'm1', role: 'user', parts: [{ type: 'text', text: 'hi' }] }],
1451
+ }),
1452
+ })
1453
+
1454
+ it('real Agent gateway stub: preamble then read_session then multi-bubble final keeps stable SSE indices', async () => {
1455
+ const bubbleDeltas = []
1456
+ const createChatCompletion = completionFromReplies([
1457
+ `我去读一下\n\n${fence('read_session', { sessionID: 'ses_idx' })}`,
1458
+ '办好了。\n\n还有后续说明。',
1459
+ ])
1460
+ const result = await runContactTurn({
1461
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
1462
+ history: [],
1463
+ userText: '读一下那个会话',
1464
+ createChatCompletion,
1465
+ tools: readSessionTools(),
1466
+ onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
1467
+ })
1468
+ expect(createChatCompletion).toHaveBeenCalledTimes(2)
1469
+ const doneBubbles = bubbleDeltas.filter((item) => item.done)
1470
+ expect(doneBubbles.map((item) => ({ index: item.index, delta: item.delta }))).toEqual([
1471
+ { index: 0, delta: '我去读一下' },
1472
+ { index: 1, delta: '办好了。' },
1473
+ { index: 2, delta: '还有后续说明。' },
1474
+ ])
1475
+ expect(result.bubbles).toEqual(['我去读一下', '办好了。', '还有后续说明。'])
1476
+ // Published stream is a stable prefix of the final array.
1477
+ expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles.map((item) => item.delta))
1478
+ })
1479
+
1480
+ it('real Agent: two read_session preambles keep both spoken lines plus final in bubbles', async () => {
1481
+ const bubbleDeltas = []
1482
+ const createChatCompletion = completionFromReplies([
1483
+ `我去读一下\n\n${fence('read_session', { sessionID: 'ses_a' })}`,
1484
+ `我再读一下\n\n${fence('read_session', { sessionID: 'ses_b' })}`,
1485
+ '办好了',
1486
+ ])
1487
+ const result = await runContactTurn({
1488
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
1489
+ history: [],
1490
+ userText: '读两个会话',
1491
+ createChatCompletion,
1492
+ tools: readSessionTools('ses_a'),
1493
+ onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
1494
+ })
1495
+ expect(createChatCompletion).toHaveBeenCalledTimes(3)
1496
+ const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
1497
+ expect(doneBubbles).toEqual(['我去读一下', '我再读一下', '办好了'])
1498
+ expect(result.bubbles).toEqual(['我去读一下', '我再读一下', '办好了'])
1499
+ expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
1500
+ })
1501
+
1502
+ it('real Agent: duplicate preamble text is kept as independent bubbles', async () => {
1503
+ const bubbleDeltas = []
1504
+ const createChatCompletion = completionFromReplies([
1505
+ `我去读一下\n\n${fence('read_session', { sessionID: 'ses_a' })}`,
1506
+ `我去读一下\n\n${fence('read_session', { sessionID: 'ses_b' })}`,
1507
+ '办好了',
1508
+ ])
1509
+ const result = await runContactTurn({
1510
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
1511
+ history: [],
1512
+ userText: '再读一遍',
1513
+ createChatCompletion,
1514
+ tools: readSessionTools('ses_a'),
1515
+ onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
1516
+ })
1517
+ const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
1518
+ expect(doneBubbles).toEqual(['我去读一下', '我去读一下', '办好了'])
1519
+ expect(result.bubbles).toEqual(['我去读一下', '我去读一下', '办好了'])
1520
+ })
1521
+
1522
+ it('real Agent: multi-segment spoken preambles then terminal tool confirm append after published prefix', async () => {
1523
+ const bubbleDeltas = []
1524
+ // Two independent short spokens (each tool turn); confirm-only tool text appends at end.
1525
+ // update_default_prompt is terminate:false so a plain final closes the agent loop.
1526
+ const createChatCompletion = completionFromReplies([
1527
+ `我先看一下\n\n${fence('list_sessions', { query: 'login' })}`,
1528
+ `马上改人设\n\n${fence('update_default_prompt', { prompt: 'be brief' })}`,
1529
+ '改好了',
1530
+ ])
1531
+ const tools = createContactTools({
1532
+ listSessions: async () => ({ sessions: [] }),
1533
+ updateAssistantSettings: async ({ defaultPrompt }) => ({ defaultPrompt, updated: true }),
1534
+ currentAssistant: { id: 'asst_x', name: 'A', providerID: 'p', modelID: 'm', defaultPrompt: '' },
1535
+ })
1536
+ const result = await runContactTurn({
1537
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
1538
+ history: [],
1539
+ userText: '改默认提示词 be brief',
1540
+ createChatCompletion,
1541
+ tools,
1542
+ onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
1543
+ })
1544
+ const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
1545
+ expect(doneBubbles).toEqual(['我先看一下', '马上改人设', '改好了'])
1546
+ // Tool-only confirm appends after the published spoken/final prefix.
1547
+ expect(result.bubbles.slice(0, 3)).toEqual(['我先看一下', '马上改人设', '改好了'])
1548
+ expect(result.bubbles.length).toBeGreaterThan(3)
1549
+ expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
1550
+ expect(result.bubbles.at(-1)).toMatch(/Default prompt/i)
1551
+ expect(result.bubbles.join('\n')).not.toContain('openchamber-tool')
1552
+ })
1553
+
1554
+ it('real Agent: missed-tool retry keeps pre-retry spoken bubbles in the final array', async () => {
1555
+ const bubbleDeltas = []
1556
+ const createAssistant = vi.fn(async (input) => ({
1557
+ id: 'asst_miss',
1558
+ name: input.name,
1559
+ providerID: input.providerID,
1560
+ modelID: input.modelID,
1561
+ mode: 'continuous',
1562
+ }))
1563
+ const createChatCompletion = completionFromReplies([
1564
+ '好的,我来创建。',
1565
+ `这就建好\n\n${fence('create_assistant', { name: 'MissRetry', model: 'p/m' })}`,
1566
+ ])
1567
+ const result = await runContactTurn({
1568
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
1569
+ history: [],
1570
+ userText: '帮我新建一个助理,名叫 MissRetry,不要开编码 session',
1571
+ createChatCompletion,
1572
+ tools: createContactTools({
1573
+ createAssistant,
1574
+ currentAssistant: { providerID: 'p', modelID: 'm' },
1575
+ }),
1576
+ onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
1577
+ })
1578
+ expect(createChatCompletion.mock.calls.length).toBeGreaterThanOrEqual(2)
1579
+ expect(createAssistant).toHaveBeenCalled()
1580
+ const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
1581
+ expect(doneBubbles).toContain('好的,我来创建。')
1582
+ expect(result.bubbles).toEqual(expect.arrayContaining(['好的,我来创建。']))
1583
+ // Final array keeps the full turn projection (streamed prefix stable).
1584
+ expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
1585
+ expect(result.bubbles).not.toEqual([MISSED_TOOL_FAILURE_BUBBLE])
1586
+ // Card tool: English tool confirm stays out of bubbles.
1587
+ expect(result.bubbles.join('')).not.toContain('Created assistant')
1588
+ })
1589
+ })
1590
+
1364
1591
  describe('read-only worker notifications', () => {
1365
1592
  it('passes prior user constraints and exact card data to completions without any mutation tool', async () => {
1366
1593
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-readonly-'))
@@ -20,6 +20,7 @@ export function isPiCodingToolName(name) {
20
20
  }
21
21
 
22
22
  export function resolveAssistantCwd(assistant) {
23
+ if (assistant?.workspacePath === null) return os.homedir();
23
24
  for (const value of [assistant?.effectiveWorkspacePath, assistant?.workspacePath]) {
24
25
  if (typeof value === 'string' && value.trim()) return value.trim();
25
26
  }
@@ -113,6 +114,7 @@ export function formatPiCodingPrompt({ cwd, skillsPrompt = '', tools = [] } = {}
113
114
  return [
114
115
  `Working directory: ${cwd}`,
115
116
  'You have application-owned pi coding tools in this directory: read, write, edit, bash.',
117
+ 'Use this directory as the starting point for workspace discovery and small, bounded tasks. Substantial implementation belongs in a worker session via assign_session; keep this contact focused on conversation, coordination, and reporting results.',
116
118
  'These are not OpenCode native tools and not MCP. Skill directory entries below are instructions only — load a matching skill by calling read on its path; never invent skill or MCP tool names (no context7, gh_grep, webfetch, etc.).',
117
119
  'This is your working directory. Ignore any other cwd from the environment, including temporary generator workspaces under /var/folders or os.tmpdir.',
118
120
  'When the user asks pwd, the current directory, or to look at files, call bash/read yourself. Never say you have no terminal or cannot read or write files.',
@@ -30,6 +30,12 @@ afterEach(() => {
30
30
  });
31
31
 
32
32
  describe('resolveAssistantCwd', () => {
33
+ it('starts a managed contact in the server user home while preserving its binding metadata', () => {
34
+ const assistant = { workspacePath: null, effectiveWorkspacePath: '/managed/contact' };
35
+ expect(resolveAssistantCwd(assistant)).toBe(os.homedir());
36
+ expect(assistant.effectiveWorkspacePath).toBe('/managed/contact');
37
+ });
38
+
33
39
  it('prefers effectiveWorkspacePath over workspacePath', () => {
34
40
  expect(resolveAssistantCwd({
35
41
  effectiveWorkspacePath: '/effective',
@@ -99,6 +105,8 @@ describe('formatPiCodingPrompt', () => {
99
105
  expect(prompt).toContain('not OpenCode native tools and not MCP');
100
106
  expect(prompt).toContain('never invent skill or MCP tool names');
101
107
  expect(prompt).toContain('context7');
108
+ expect(prompt).toContain('small, bounded tasks');
109
+ expect(prompt).toContain('worker session via assign_session');
102
110
  await runtime.close();
103
111
  });
104
112
  });