@openchambery/web 1.19.11 → 1.19.13-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 (123) hide show
  1. package/dist/assets/{AgentAvatar-CX0a1rKE.js → AgentAvatar-CiQIIM0d.js} +1 -1
  2. package/dist/assets/AssistantView-YmeyxehG.js +2 -0
  3. package/dist/assets/{DiagramView-BQO1cBLs.js → DiagramView-BpZb-G9P.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-CTlNE4Hg.js → FadeInOnReveal-nf6-ZCrs.js} +1 -1
  5. package/dist/assets/{Icon-BYM02QyU.js → Icon-CeKHYyKZ.js} +1 -1
  6. package/dist/assets/MarkdownRenderer-wpeGdciL.js +2 -0
  7. package/dist/assets/{MarkdownRendererImpl--takeMXN.js → MarkdownRendererImpl-Peu0GVSQ.js} +1 -1
  8. package/dist/assets/{MarkstreamRendererImpl-DGSqWcPN.js → MarkstreamRendererImpl-B-odd2Gz.js} +1 -1
  9. package/dist/assets/MobileDetailNavigation-CtYke_1C.js +1 -0
  10. package/dist/assets/{MobileShareBridge-D-BUjACe.js → MobileShareBridge-DIfOl8EZ.js} +3 -3
  11. package/dist/assets/MobileSurface-_KoX75Ff.js +2 -0
  12. package/dist/assets/{MultiRunWindow-BNR9ZWmD.js → MultiRunWindow-BT5Dwgzu.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-DS2nugLX.js → OnboardingScreen-BPusUgFG.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-KUpvYHdv.js → OverlayScrollbar-DUghqFGg.js} +1 -1
  15. package/dist/assets/{SettingsView-DGerg-C2.js → SettingsView-BwQpbIZl.js} +11 -11
  16. package/dist/assets/{SettingsWindow-CSpd5JbJ.js → SettingsWindow-DfEGfyr8.js} +1 -1
  17. package/dist/assets/{TerminalView-DS-oUxJr.js → TerminalView-8402vpcO.js} +1 -1
  18. package/dist/assets/{ToolOutputDialog-Q1k9-IaY.js → ToolOutputDialog-B5RdkhsG.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-CG7D-3-q.js → appThemeRegistry-CIZ-shMD.js} +1 -1
  20. package/dist/assets/{button-C0cth9tl.js → button-DFPOdZ0g.js} +1 -1
  21. package/dist/assets/{clipboard-DJ4nQEuY.js → clipboard-C05anBwU.js} +1 -1
  22. package/dist/assets/{collapsible-DFT9cE80.js → collapsible-oGbEvbe1.js} +1 -1
  23. package/dist/assets/{context-menu-CAp74NfV.js → context-menu-DGz4_2hs.js} +1 -1
  24. package/dist/assets/{desktopHosts-Dc2PS66M.js → desktopHosts-BVJXQq1S.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-T9y0orqa.js → desktopRelayRestore-RDfuj1M7.js} +1 -1
  26. package/dist/assets/{diagram-BryzMuta.js → diagram-Csk5GpDC.js} +1 -1
  27. package/dist/assets/{dialog-CgbV5ljg.js → dialog-rrqB-H6q.js} +1 -1
  28. package/dist/assets/{es-BHqnSjoZ.js → es-FE0sJ9EC.js} +1 -1
  29. package/dist/assets/{fileQueries-BY-9OWzp.js → fileQueries-BcdWOPlD.js} +1 -1
  30. package/dist/assets/{fr-BfrlJY9i.js → fr-DNDCSfGt.js} +1 -1
  31. package/dist/assets/{gitApi-Cr-joUEw.js → gitApi-_P0FsoXe.js} +1 -1
  32. package/dist/assets/{input-DFLpqjGR.js → input-BO3r20op.js} +1 -1
  33. package/dist/assets/{insertionBoundaries-X0xbikAA.js → insertionBoundaries-CWoioTdE.js} +1 -1
  34. package/dist/assets/{ja-DP-7qt-x.js → ja-BDwrk0cM.js} +1 -1
  35. package/dist/assets/{ko-E2B5vGQP.js → ko-BeeU2o4I.js} +1 -1
  36. package/dist/assets/{main-D0L7fIg3.js → main-CLj_WS1o.js} +2 -2
  37. package/dist/assets/{main-BI7fNQoi.js → main-CnSZ89pD.js} +4 -4
  38. package/dist/assets/{markdown-worker-CTsrtpPh.js → markdown-worker-CZWE1F1M.js} +1 -1
  39. package/dist/assets/{markdownHeightCache-CYDlisxr.js → markdownHeightCache-Cdd3hkMl.js} +1 -1
  40. package/dist/assets/{markdownTheme-DyJ4Gfcj.js → markdownTheme-DIB8WVmz.js} +1 -1
  41. package/dist/assets/miniChat-B9c3y2pD.js +2 -0
  42. package/dist/assets/{mobile-OcCu_0mG.js → mobile-DvbXUfNS.js} +2 -2
  43. package/dist/assets/{multirun-B0G_BEIN.js → multirun-CxhTsmRq.js} +1 -1
  44. package/dist/assets/{persistence-DdUac14I.js → persistence-CVTWFaqe.js} +2 -2
  45. package/dist/assets/{pl-r8X5gRM5.js → pl-BYuzpcI5.js} +1 -1
  46. package/dist/assets/{platform-B4KKLnZ5.js → platform-BY_Gtsqr.js} +1 -1
  47. package/dist/assets/{projectMeta-BNOzn1QS.js → projectMeta-Dl5iPCB6.js} +1 -1
  48. package/dist/assets/{pt-BR-DcpdXCf5.js → pt-BR-Bn7cwEzv.js} +1 -1
  49. package/dist/assets/{renderElectronMiniChatApp-BREc3uhX.js → renderElectronMiniChatApp-CkJ51qCh.js} +2 -2
  50. package/dist/assets/renderMobileApp-DcjQPhWw.js +28 -0
  51. package/dist/assets/{runtimeConfig-P8nVMYzE.js → runtimeConfig-Bsk2kWsW.js} +1 -1
  52. package/dist/assets/runtimeEndpointReset-BBLFgl85.js +147 -0
  53. package/dist/assets/{sessionChangeSummary-hVby3sJk.js → sessionChangeSummary-DDVT7O5J.js} +1 -1
  54. package/dist/assets/{sessionLookup-C_9-9ggp.js → sessionLookup-C8VZn0gJ.js} +1 -1
  55. package/dist/assets/{sortable-tabs-strip-C8_9kHqu.js → sortable-tabs-strip-s7UUxsa6.js} +1 -1
  56. package/dist/assets/src-BpHDMgud.css +2 -0
  57. package/dist/assets/{store-BDFdnk6l.js → store-n94dSb3M.js} +2 -2
  58. package/dist/assets/{terminalApi-947l7Ra3.js → terminalApi-C54_rKEK.js} +1 -1
  59. package/dist/assets/{textarea-Cgo_nWUU.js → textarea-BjX-KhMN.js} +1 -1
  60. package/dist/assets/{tooltip-BifLJ8o0.js → tooltip-CQGqGAG_.js} +1 -1
  61. package/dist/assets/{uk-Di0vl9V-.js → uk-CwpbHEa3.js} +1 -1
  62. package/dist/assets/{url-Pk6tQ1EZ.js → url-BMjqccCo.js} +1 -1
  63. package/dist/assets/{useAppFontEffects-CEmOsrr6.js → useAppFontEffects-BIFw36SA.js} +5 -5
  64. package/dist/assets/{useAssistantUIStore-me6s63b2.js → useAssistantUIStore-Cg9bsT4b.js} +1 -1
  65. package/dist/assets/{useDesktopWindowControlsLayout-CxaK-zXh.js → useDesktopWindowControlsLayout-DoygEcN4.js} +1 -1
  66. package/dist/assets/{useEffectiveDirectory-BIwv5r5l.js → useEffectiveDirectory-DJSYYkVp.js} +1 -1
  67. package/dist/assets/{useFontPreferences-BCOBlaAd.js → useFontPreferences-B5_apbBC.js} +1 -1
  68. package/dist/assets/{useMobileNavigationStore-DHm5kZPx.js → useMobileNavigationStore-DuTu0Mzh.js} +5 -5
  69. package/dist/assets/{useSessionAutoCleanup-CPLP9KWH.js → useSessionAutoCleanup-C-L9teva.js} +1 -1
  70. package/dist/assets/{useUIStore-CuaA8-Hp.js → useUIStore-DwpQTjLO.js} +1 -1
  71. package/dist/assets/{useWorkerHighlightedLines-CZ4ZTWEH.js → useWorkerHighlightedLines-DxTfgdEi.js} +1 -1
  72. package/dist/assets/{utils-BiK3tBGM.js → utils-81nUI_xp.js} +1 -1
  73. package/dist/assets/{zh-CN-coFCfeQE.js → zh-CN-BpzFZ-LU.js} +1 -1
  74. package/dist/assets/{zh-TW-oDRZ1-yb.js → zh-TW-Bopgc5Eo.js} +1 -1
  75. package/dist/index.html +9 -9
  76. package/dist/mini-chat.html +9 -9
  77. package/dist/mobile.html +9 -9
  78. package/package.json +1 -1
  79. package/server/index.js +19 -0
  80. package/server/lib/assistants/DOCUMENTATION.md +21 -6
  81. package/server/lib/assistants/assign.js +455 -30
  82. package/server/lib/assistants/assign.test.js +351 -0
  83. package/server/lib/assistants/contact-active-turn.http.test.js +134 -0
  84. package/server/lib/assistants/contact-store.js +64 -0
  85. package/server/lib/assistants/contact-store.test.js +12 -0
  86. package/server/lib/assistants/contact-tools.js +216 -25
  87. package/server/lib/assistants/contact-tools.test.js +167 -0
  88. package/server/lib/assistants/contracts.js +2 -2
  89. package/server/lib/assistants/harness.js +20 -3
  90. package/server/lib/assistants/harness.test.js +120 -1
  91. package/server/lib/assistants/pi-tools.js +50 -3
  92. package/server/lib/assistants/pi-tools.test.js +25 -0
  93. package/server/lib/assistants/service.js +320 -27
  94. package/server/lib/assistants/service.test.js +581 -9
  95. package/server/lib/llm/DOCUMENTATION.md +31 -18
  96. package/server/lib/llm/catalog.js +91 -16
  97. package/server/lib/llm/catalog.test.js +152 -11
  98. package/server/lib/llm/generate.js +43 -7
  99. package/server/lib/llm/generate.test.js +98 -1
  100. package/server/lib/llm/temp-directory.js +34 -13
  101. package/server/lib/llm/temp-directory.test.js +57 -0
  102. package/server/lib/notifications/APNS.md +20 -4
  103. package/server/lib/notifications/DOCUMENTATION.md +5 -0
  104. package/server/lib/notifications/apns-runtime.js +136 -18
  105. package/server/lib/notifications/apns-runtime.test.js +128 -0
  106. package/server/lib/notifications/apns-titles.js +21 -1
  107. package/server/lib/notifications/live-activity-refresh-runtime.js +185 -0
  108. package/server/lib/notifications/live-activity-refresh-runtime.test.js +242 -0
  109. package/server/lib/notifications/runtime.js +93 -2
  110. package/server/lib/notifications/runtime.test.js +167 -4
  111. package/server/lib/opencode/settings-helpers.js +12 -0
  112. package/server/lib/opencode/settings-runtime.js +16 -0
  113. package/server/lib/opencode/settings-runtime.test.js +21 -0
  114. package/server/lib/scheduled-tasks/runtime.js +21 -0
  115. package/server/lib/scheduled-tasks/runtime.test.js +24 -0
  116. package/dist/assets/AssistantView-BwrcqJzu.js +0 -2
  117. package/dist/assets/MarkdownRenderer-C49bfcHx.js +0 -2
  118. package/dist/assets/MobileDetailNavigation-LZU2vFAs.js +0 -1
  119. package/dist/assets/MobileSurface-OAJjAQCZ.js +0 -2
  120. package/dist/assets/miniChat-BJ9AclZX.js +0 -2
  121. package/dist/assets/renderMobileApp-C4igZ42L.js +0 -28
  122. package/dist/assets/runtimeEndpointReset-6lVRspis.js +0 -147
  123. package/dist/assets/src-B-P5qIBc.css +0 -2
@@ -229,6 +229,10 @@ describe('runContactTurn', () => {
229
229
  const createChatCompletion = vi.fn(async ({ body }) => {
230
230
  expect(body.messages[0].content).toContain('assign_session')
231
231
  expect(body.messages[0].content).toContain('docs')
232
+ expect(body.messages[0].content).toContain('arguments schema:')
233
+ expect(body.messages[0].content).toContain('"edits"')
234
+ expect(body.messages[0].content).toContain('"content"')
235
+ expect(body.messages[0].content).toContain('not OpenCode native tools and not MCP')
232
236
  const content = replies[call++]
233
237
  if (!content) throw new Error('Unexpected completion')
234
238
  return { completion: { choices: [{ message: { content } }] } }
@@ -242,11 +246,126 @@ describe('runContactTurn', () => {
242
246
  expect(result.tools.map((tool) => tool.name)).toEqual([...PI_CODING_TOOL_NAMES, ...tools.map((tool) => tool.name)])
243
247
  expect(assignWork).toHaveBeenCalledTimes(1)
244
248
  expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_mixed' })])
245
- expect(createChatCompletion).toHaveBeenCalledTimes(4)
249
+ // read → text (miss) → missed-fence retry assign; assign terminates (no post-assign LLM).
250
+ expect(createChatCompletion).toHaveBeenCalledTimes(3)
251
+ // Tool call/result association: after read, the next completion sees a named tool result.
252
+ const secondMessages = createChatCompletion.mock.calls[1][0].body.messages
253
+ const toolResultTurn = secondMessages.find((message) => (
254
+ message.role === 'user' && typeof message.content === 'string' && message.content.includes('tool result name=read')
255
+ ))
256
+ expect(toolResultTurn).toBeTruthy()
257
+ expect(toolResultTurn.content).toMatch(/Read project docs|documentation/i)
246
258
  } finally {
247
259
  fs.rmSync(workspace, { recursive: true, force: true })
248
260
  }
249
261
  })
262
+ it('real Agent loop: looping assign fences create only one worker + one card (terminate)', async () => {
263
+ const assignWork = vi.fn(async () => ({
264
+ sessionID: 'ses_once',
265
+ directory: '/repo',
266
+ title: 'Once',
267
+ status: 'busy',
268
+ }))
269
+ const tools = createContactTools({ assignWork })
270
+ let completions = 0
271
+ const createChatCompletion = vi.fn(async () => {
272
+ completions += 1
273
+ // Safety: a non-terminating loop must not mint 37 sessions in tests.
274
+ if (completions > 8) {
275
+ throw new Error('harness safety stop: too many completions without terminate')
276
+ }
277
+ return {
278
+ completion: {
279
+ choices: [{
280
+ message: {
281
+ content: '```openchamber-tool\n{"name":"assign_session","arguments":{"prompt":"Fix login","projectPath":"/repo"}}\n```',
282
+ },
283
+ }],
284
+ },
285
+ }
286
+ })
287
+ const result = await runContactTurn({
288
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
289
+ history: [],
290
+ userText: '建会话修 login',
291
+ tools,
292
+ projects: [{ id: 'p1', path: '/repo', label: 'Repo' }],
293
+ createChatCompletion,
294
+ })
295
+ expect(assignWork).toHaveBeenCalledTimes(1)
296
+ expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_once', cardType: 'session' })])
297
+ expect(result.cards).toHaveLength(1)
298
+ expect(result.bubbles).toContain('Opened a coding session.')
299
+ // First completion issues assign; terminate skips the auto follow-up LLM.
300
+ expect(createChatCompletion).toHaveBeenCalledTimes(1)
301
+ expect(completions).toBe(1)
302
+ })
303
+
304
+ it('real Agent loop: list_projects then assign keeps prereq + one session (terminate after assign)', async () => {
305
+ const listProjects = vi.fn(async () => [{ id: 'p1', path: '/repo', label: 'Repo' }])
306
+ const assignWork = vi.fn(async () => ({
307
+ sessionID: 'ses_prereq',
308
+ directory: '/repo',
309
+ title: 'Prereq',
310
+ status: 'busy',
311
+ }))
312
+ const tools = createContactTools({ listProjects, assignWork })
313
+ let completions = 0
314
+ const createChatCompletion = vi.fn(async ({ body }) => {
315
+ completions += 1
316
+ if (completions > 6) {
317
+ throw new Error('harness safety stop: too many completions')
318
+ }
319
+ const last = body.messages.at(-1)
320
+ const lastText = typeof last?.content === 'string' ? last.content : ''
321
+ if (lastText.includes('tool result name=list_projects') || lastText.includes('Registered projects')) {
322
+ return {
323
+ completion: {
324
+ choices: [{
325
+ message: {
326
+ content: '```openchamber-tool\n{"name":"assign_session","arguments":{"prompt":"Fix","projectPath":"/repo"}}\n```',
327
+ },
328
+ }],
329
+ },
330
+ }
331
+ }
332
+ if (completions === 1) {
333
+ return {
334
+ completion: {
335
+ choices: [{
336
+ message: {
337
+ content: '```openchamber-tool\n{"name":"list_projects","arguments":{"query":"repo"}}\n```',
338
+ },
339
+ }],
340
+ },
341
+ }
342
+ }
343
+ // If assign did not terminate, a looping model would keep assigning — fail closed.
344
+ return {
345
+ completion: {
346
+ choices: [{
347
+ message: {
348
+ content: '```openchamber-tool\n{"name":"assign_session","arguments":{"prompt":"Fix again","projectPath":"/repo"}}\n```',
349
+ },
350
+ }],
351
+ },
352
+ }
353
+ })
354
+ const result = await runContactTurn({
355
+ assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
356
+ history: [],
357
+ userText: '找项目 repo 然后建会话修问题',
358
+ tools,
359
+ projects: [{ id: 'p1', path: '/repo', label: 'Repo' }],
360
+ createChatCompletion,
361
+ })
362
+ expect(listProjects).toHaveBeenCalledTimes(1)
363
+ expect(assignWork).toHaveBeenCalledTimes(1)
364
+ expect(result.cards).toEqual([expect.objectContaining({ sessionID: 'ses_prereq' })])
365
+ expect(result.cards).toHaveLength(1)
366
+ expect(createChatCompletion.mock.calls.length).toBeLessThanOrEqual(3)
367
+ })
368
+
250
369
  it('retries assignment after an unrelated read result', async () => {
251
370
  const prompts = []
252
371
  function AgentImpl(options) {
@@ -70,21 +70,68 @@ const bindHarnessTool = (tool, context) => ({
70
70
  ),
71
71
  });
72
72
 
73
- export function formatPiCodingPrompt({ cwd, skillsPrompt = '' } = {}) {
73
+ /**
74
+ * Serialize a tool parameter schema for the contact system prompt.
75
+ * Mirrors contact-tools.serializeToolParametersForPrompt without a circular import.
76
+ */
77
+ const serializeParameters = (parameters) => {
78
+ if (parameters == null) return '{"type":"object","properties":{}}';
79
+ try {
80
+ const text = JSON.stringify(parameters, (_key, value) => {
81
+ if (typeof value === 'function' || typeof value === 'symbol') return undefined;
82
+ return value;
83
+ });
84
+ if (typeof text === 'string' && text.trim() && text !== 'undefined') return text;
85
+ } catch {
86
+ // Fall through.
87
+ }
88
+ return '{"type":"object","properties":{}}';
89
+ };
90
+
91
+ const formatCodingToolEntry = (tool) => {
92
+ if (!tool || typeof tool.name !== 'string' || !tool.name.trim()) return '';
93
+ const name = tool.name.trim();
94
+ const description = typeof tool.description === 'string' && tool.description.trim()
95
+ ? tool.description.trim()
96
+ : name;
97
+ return `- ${name}: ${description}\n arguments schema: ${serializeParameters(tool.parameters)}`;
98
+ };
99
+
100
+ export function formatPiCodingPrompt({ cwd, skillsPrompt = '', tools = [] } = {}) {
74
101
  if (typeof cwd !== 'string' || !cwd.trim()) return '';
102
+ const codingTools = Array.isArray(tools)
103
+ ? tools.filter((tool) => isPiCodingToolName(tool?.name))
104
+ : [];
105
+ const catalog = codingTools.length > 0
106
+ ? codingTools.map(formatCodingToolEntry).filter(Boolean)
107
+ : [
108
+ '- bash: Execute a bash command in the working directory.\n arguments schema: {"type":"object","required":["command"],"properties":{"command":{"type":"string"},"timeout":{"type":"number"}}}',
109
+ '- read: Read a file (path required; optional offset/limit).\n arguments schema: {"type":"object","required":["path"],"properties":{"path":{"type":"string"},"offset":{"type":"number"},"limit":{"type":"number"}}}',
110
+ '- write: Write/create a file (path + content required).\n arguments schema: {"type":"object","required":["path","content"],"properties":{"path":{"type":"string"},"content":{"type":"string"}}}',
111
+ '- edit: Exact text replacement (path + edits[{oldText,newText}] required).\n arguments schema: {"type":"object","required":["path","edits"],"properties":{"path":{"type":"string"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string"},"newText":{"type":"string"}}}}}}',
112
+ ];
75
113
  return [
76
114
  `Working directory: ${cwd}`,
77
- 'You have pi coding tools in this directory: read, write, edit, bash.',
115
+ 'You have application-owned pi coding tools in this directory: read, write, edit, bash.',
116
+ '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.).',
78
117
  'This is your working directory. Ignore any other cwd from the environment, including temporary generator workspaces under /var/folders or os.tmpdir.',
79
118
  '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.',
80
119
  'Skills are merged from ~/.claude/skills, ~/.agents/skills, and this project\'s .claude/skills plus .agents/skills (project wins on name). Read a skill file when the task matches its description.',
81
- 'Call these tools with the same fenced JSON as other tools:',
120
+ 'Call these tools with the same fenced JSON as other tools. Match the argument schemas exactly:',
82
121
  '```openchamber-tool',
83
122
  '{"name":"bash","arguments":{"command":"pwd"}}',
84
123
  '```',
85
124
  '```openchamber-tool',
86
125
  '{"name":"read","arguments":{"path":".agents/skills/example/SKILL.md"}}',
87
126
  '```',
127
+ '```openchamber-tool',
128
+ '{"name":"write","arguments":{"path":"notes.txt","content":"hello"}}',
129
+ '```',
130
+ '```openchamber-tool',
131
+ '{"name":"edit","arguments":{"path":"notes.txt","edits":[{"oldText":"hello","newText":"hello world"}]}}',
132
+ '```',
133
+ 'Available pi coding tools (full argument schemas):',
134
+ ...catalog,
88
135
  typeof skillsPrompt === 'string' && skillsPrompt.trim() ? skillsPrompt.trim() : '',
89
136
  ].filter(Boolean).join('\n');
90
137
  }
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest';
5
5
  import {
6
6
  collectAssistantSkillDirectories,
7
7
  createPiCodingRuntime,
8
+ formatPiCodingPrompt,
8
9
  mergeSkillsByName,
9
10
  PI_CODING_TOOL_NAMES,
10
11
  resolveAssistantCwd,
@@ -78,6 +79,30 @@ describe('mergeSkillsByName', () => {
78
79
  });
79
80
  });
80
81
 
82
+ describe('formatPiCodingPrompt', () => {
83
+ it('includes full pi tool argument schemas and distinguishes skills from tools', async () => {
84
+ const cwd = temp();
85
+ const home = temp();
86
+ const runtime = await createPiCodingRuntime(cwd, { homeDir: home });
87
+ const prompt = formatPiCodingPrompt({
88
+ cwd: runtime.cwd,
89
+ skillsPrompt: runtime.skillsPrompt,
90
+ tools: runtime.tools,
91
+ });
92
+ expect(prompt).toContain('arguments schema:');
93
+ expect(prompt).toContain('"path"');
94
+ expect(prompt).toContain('"content"');
95
+ expect(prompt).toContain('"edits"');
96
+ expect(prompt).toContain('"oldText"');
97
+ expect(prompt).toContain('"newText"');
98
+ expect(prompt).toContain('"command"');
99
+ expect(prompt).toContain('not OpenCode native tools and not MCP');
100
+ expect(prompt).toContain('never invent skill or MCP tool names');
101
+ expect(prompt).toContain('context7');
102
+ await runtime.close();
103
+ });
104
+ });
105
+
81
106
  describe('createPiCodingRuntime', () => {
82
107
  it('installs read/write/edit/bash and merges global plus project skills', async () => {
83
108
  const home = temp();