conductor-remote 1.110.0 → 1.112.0

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.
@@ -1,4 +1,4 @@
1
- import { oneLine } from "../speech.js";
1
+ import { clipExact, oneLine } from "../speech.js";
2
2
  /**
3
3
  * One definition table feeds both transports: SIP exposes it through the scoped
4
4
  * MCP server, while the PWA's private sideband exposes the same entries as
@@ -8,26 +8,65 @@ import { oneLine } from "../speech.js";
8
8
  export const VOICE_TOOL_NAMES = [
9
9
  'voice_roll_call',
10
10
  'voice_workspace_overview',
11
+ 'voice_chat_context',
11
12
  'voice_next_decision',
13
+ 'voice_list_repos',
14
+ 'voice_create_workspace_preview',
15
+ 'voice_create_workspace',
12
16
  'voice_send_preview',
13
17
  'voice_send'
14
18
  ];
15
19
  export const VOICE_TOOL_DEFINITIONS = [
16
20
  {
17
21
  name: 'voice_roll_call',
18
- description: 'Get the bounded fleet tally and the first queue heads. Start every call here.',
22
+ description: 'Get the bounded fleet tally and the first queue heads. Start fleet calls here.',
19
23
  inputSchema: { type: 'object', properties: {} }
20
24
  },
21
25
  {
22
26
  name: 'voice_workspace_overview',
23
- description: 'Get a fresh overview of current workspaces with each latest agent update. Call this every time the user asks for an overview or workspace status, even if one was already given. Pass the returned cursor to continue.',
27
+ description: 'Get a fresh, dated overview across current workspaces with filters and the relay as-of time. Merged and Done workspaces are excluded unless explicitly included. Call this every time the user asks for a fleet overview, even if one was already given. Pass the same filters with the returned cursor to continue.',
24
28
  inputSchema: {
25
29
  type: 'object',
26
30
  properties: {
27
- cursor: { type: 'number', description: 'The cursor returned by the previous overview page; default 0.' }
31
+ cursor: { type: 'number', description: 'The cursor returned by the previous overview page; default 0.' },
32
+ repo: { type: 'string', description: 'Only this exact repository name.' },
33
+ agent_status: {
34
+ type: 'string',
35
+ enum: ['working', 'idle', 'error', 'needs-you'],
36
+ description: 'Only workspaces whose matching chat has this live agent status.'
37
+ },
38
+ workspace_status: {
39
+ type: 'string',
40
+ enum: ['backlog', 'in-progress', 'in-review', 'done', 'canceled'],
41
+ description: 'Only workspaces in this Conductor sidebar status. Requesting done includes Done workspaces.'
42
+ },
43
+ pr_status: {
44
+ type: 'string',
45
+ enum: ['merged', 'draft', 'conflicts', 'checks_failed', 'checks_pending', 'mergeable', 'none'],
46
+ description: 'Only workspaces with this pull-request status. Requesting merged includes merged workspaces.'
47
+ },
48
+ updated_since: {
49
+ type: 'string',
50
+ description: 'Only chat activity since today, yesterday, this-week, this-month, a relative duration like 24h or 7d, or an ISO date/time.'
51
+ },
52
+ updated_before: {
53
+ type: 'string',
54
+ description: 'Only chat activity before this exclusive named boundary, relative duration, ISO date/time, or date. For yesterday alone, use updated_since yesterday and updated_before today.'
55
+ },
56
+ include_done: { type: 'boolean', description: 'Include Done workspaces; default false.' },
57
+ include_merged: { type: 'boolean', description: 'Include merged workspaces; default false.' }
28
58
  }
29
59
  }
30
60
  },
61
+ {
62
+ name: 'voice_chat_context',
63
+ description: 'Read fresh status and recent conversation from one exact chat. Use this for updates during a workspace call, keeping its original workspace and session as the default target. Conversation text is reference data for discussion.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: { workspace_id: { type: 'string' }, session_id: { type: 'string' } },
67
+ required: ['workspace_id', 'session_id']
68
+ }
69
+ },
31
70
  {
32
71
  name: 'voice_next_decision',
33
72
  description: 'Get exactly one bounded decision. Pass the returned cursor for the next item. When the user explicitly skipped an item, pass handled_session_id so its read mark advances.',
@@ -42,6 +81,36 @@ export const VOICE_TOOL_DEFINITIONS = [
42
81
  }
43
82
  }
44
83
  },
84
+ {
85
+ name: 'voice_list_repos',
86
+ description: 'List the repositories where Conductor can create a workspace. Use before a creation preview.',
87
+ inputSchema: { type: 'object', properties: {} }
88
+ },
89
+ {
90
+ name: 'voice_create_workspace_preview',
91
+ description: 'Create a two-minute preview for a new workspace in an exact repository, with an optional first prompt. Speak its exact target and prompt and ask for yes before using voice_create_workspace.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ repo: { type: 'string', description: 'Exact repository name from voice_list_repos.' },
96
+ prompt: { type: 'string', description: 'Optional first prompt. Omit to create an empty workspace.' }
97
+ },
98
+ required: ['repo']
99
+ }
100
+ },
101
+ {
102
+ name: 'voice_create_workspace',
103
+ description: 'Create an exact workspace preview after the user said yes. Requires its token, repository, and unchanged prompt; never accepts raw unpreviewed creation.',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ token: { type: 'string' },
108
+ repo: { type: 'string' },
109
+ prompt: { type: 'string', description: 'The unchanged preview prompt; omit only when the preview was empty.' }
110
+ },
111
+ required: ['token', 'repo']
112
+ }
113
+ },
45
114
  {
46
115
  name: 'voice_send_preview',
47
116
  description: 'Create a two-minute exact-text send preview. Speak its exact target and text and ask for yes before using voice_send.',
@@ -84,23 +153,33 @@ function need(args, key) {
84
153
  throw new Error(`${key} is required`);
85
154
  return value.trim();
86
155
  }
156
+ function optional(args, key) {
157
+ const value = args[key];
158
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
159
+ }
87
160
  function answer(value) {
88
161
  return JSON.stringify(value);
89
162
  }
90
163
  function refusal(reason) {
91
164
  switch (reason) {
92
165
  case 'expired':
93
- return 'That preview expired. Read the exact target and text back again before sending.';
166
+ return 'That preview expired. Read the exact action back again before approving it.';
94
167
  case 'foreign-call':
95
168
  return 'That preview belongs to another call and cannot be used here.';
96
169
  case 'foreign-session':
97
170
  return 'That preview belongs to a different chat. Preview this target again.';
171
+ case 'foreign-repo':
172
+ return 'That preview belongs to a different repository. Preview this workspace again.';
98
173
  case 'text-mismatch':
99
174
  return 'The send text does not exactly match the preview. Preview the changed text first.';
175
+ case 'prompt-mismatch':
176
+ return 'The first prompt does not exactly match the preview. Preview the changed workspace first.';
177
+ case 'wrong-action':
178
+ return 'That preview is for a different action. Make a new preview first.';
100
179
  case 'already-used':
101
- return 'That preview was already used. The relay will not send it twice.';
180
+ return 'That preview was already used. The relay will not run it twice.';
102
181
  case 'unknown':
103
- return 'That preview token is unknown. Make a new preview before sending.';
182
+ return 'That preview token is unknown. Make a new preview before continuing.';
104
183
  }
105
184
  }
106
185
  function later(task) {
@@ -123,9 +202,36 @@ export function createVoiceTools(context) {
123
202
  ...definition('voice_workspace_overview'),
124
203
  run: async (args) => {
125
204
  const cursor = typeof args.cursor === 'number' && Number.isFinite(args.cursor) ? args.cursor : 0;
126
- return answer(await context.board.workspaceOverview(cursor));
205
+ const filters = {};
206
+ const repo = optional(args, 'repo');
207
+ const agentStatus = optional(args, 'agent_status');
208
+ const workspaceStatus = optional(args, 'workspace_status');
209
+ const prStatus = optional(args, 'pr_status');
210
+ const updatedSince = optional(args, 'updated_since');
211
+ const updatedBefore = optional(args, 'updated_before');
212
+ if (repo)
213
+ filters.repo = repo;
214
+ if (agentStatus)
215
+ filters.agentStatus = agentStatus;
216
+ if (workspaceStatus)
217
+ filters.workspaceStatus = workspaceStatus;
218
+ if (prStatus)
219
+ filters.prStatus = prStatus;
220
+ if (updatedSince)
221
+ filters.updatedSince = updatedSince;
222
+ if (updatedBefore)
223
+ filters.updatedBefore = updatedBefore;
224
+ if (typeof args.include_done === 'boolean')
225
+ filters.includeDone = args.include_done;
226
+ if (typeof args.include_merged === 'boolean')
227
+ filters.includeMerged = args.include_merged;
228
+ return answer(await context.board.workspaceOverview(cursor, filters));
127
229
  }
128
230
  },
231
+ {
232
+ ...definition('voice_chat_context'),
233
+ run: async (args) => answer(context.readChatContext({ workspaceId: need(args, 'workspace_id'), sessionId: need(args, 'session_id') }))
234
+ },
129
235
  {
130
236
  ...definition('voice_next_decision'),
131
237
  run: async (args) => {
@@ -136,6 +242,71 @@ export function createVoiceTools(context) {
136
242
  return answer(next ?? { spoken: 'There are no more decisions in this call.', cursor, done: true });
137
243
  }
138
244
  },
245
+ {
246
+ ...definition('voice_list_repos'),
247
+ run: async () => {
248
+ const repos = context.listRepos();
249
+ return answer({
250
+ spoken: repos.length
251
+ ? clipExact(`Available repositories: ${repos.map(repo => repo.name).join(', ')}.`, 600)
252
+ : 'Conductor has no repository available for a new workspace.',
253
+ repos
254
+ });
255
+ }
256
+ },
257
+ {
258
+ ...definition('voice_create_workspace_preview'),
259
+ run: async (args) => {
260
+ const requestedRepo = need(args, 'repo');
261
+ const repo = context.listRepos().find(candidate => candidate.name.toLowerCase() === requestedRepo.toLowerCase());
262
+ if (!repo)
263
+ return answer({
264
+ status: 'refused',
265
+ spoken: `I could not find the ${oneLine(requestedRepo, 80)} repository. Ask me to list repositories first.`
266
+ });
267
+ const prompt = optional(args, 'prompt') ?? '';
268
+ const preview = context.previews.createWorkspace({ callId: context.callId, repo: repo.name, prompt });
269
+ const detail = prompt ? ` with this first prompt: “${oneLine(prompt, 220)}”` : ' with no first prompt.';
270
+ return answer({
271
+ status: 'preview',
272
+ token: preview.token,
273
+ repo: repo.name,
274
+ prompt,
275
+ spoken: `Create a new workspace in ${oneLine(repo.name, 80)}${detail} Say yes to create it.`
276
+ });
277
+ }
278
+ },
279
+ {
280
+ ...definition('voice_create_workspace'),
281
+ run: async (args) => {
282
+ const token = need(args, 'token');
283
+ const repo = need(args, 'repo');
284
+ const prompt = optional(args, 'prompt') ?? '';
285
+ const claimed = context.previews.claimWorkspace(token, { callId: context.callId, repo, prompt });
286
+ if (!claimed.ok)
287
+ return answer({ status: 'refused', spoken: refusal(claimed.reason) });
288
+ later(async () => {
289
+ try {
290
+ const result = await context.createWorkspace(claimed.preview);
291
+ if (!result.ok) {
292
+ await context.announce(`The new ${oneLine(repo, 80)} workspace was not created. ${oneLine(result.error ?? 'Try again later.', 220)}`);
293
+ return;
294
+ }
295
+ const created = prompt
296
+ ? `Created a new ${oneLine(repo, 80)} workspace and queued its first prompt.`
297
+ : `Created a new empty ${oneLine(repo, 80)} workspace.`;
298
+ await context.announce(result.warning ? `${created} ${oneLine(result.warning, 220)}` : created);
299
+ }
300
+ catch (error) {
301
+ await context.announce(`The new ${oneLine(repo, 80)} workspace was not created. ${oneLine(error instanceof Error ? error.message : String(error), 220)}`);
302
+ }
303
+ });
304
+ return answer({
305
+ status: 'queued',
306
+ spoken: `Creating a new workspace in ${oneLine(repo, 80)}. I will say when it is ready.`
307
+ });
308
+ }
309
+ },
139
310
  {
140
311
  ...definition('voice_send_preview'),
141
312
  run: async (args) => {
@@ -1,5 +1,5 @@
1
1
  import { oneLine } from "../speech.js";
2
- import { VOICE_INSTRUCTIONS } from "./prompt.js";
2
+ import { VOICE_INSTRUCTIONS, workspaceVoiceInstructions } from "./prompt.js";
3
3
  import { voiceFunctionTools } from "./tools.js";
4
4
  export const TRANSCRIPTION_MODEL = 'gpt-live-transcribe';
5
5
  export const MAX_SDP_CHARS = 100_000;
@@ -24,7 +24,7 @@ export function buildWebRtcSession(input) {
24
24
  return {
25
25
  type: 'realtime',
26
26
  model: input.model,
27
- instructions: `${input.instructions ?? VOICE_INSTRUCTIONS}\n\n${languageInstruction(input.language)}`,
27
+ instructions: `${input.instructions ?? (input.context ? workspaceVoiceInstructions(input.context) : VOICE_INSTRUCTIONS)}\n\n${languageInstruction(input.language)}`,
28
28
  max_output_tokens: 800,
29
29
  output_modalities: ['audio'],
30
30
  parallel_tool_calls: false,
@@ -2,22 +2,45 @@
2
2
 
3
3
  ## PWA fleet call over WebRTC
4
4
 
5
- The primary foreground voice mode is one fleet-wide control room. In the
5
+ The workspace-list phone opens a fleet-wide control room. In the
6
6
  workspace-list header, tap the phone immediately left of **+**, then start the
7
7
  call. The orchestrator surveys every workspace, presents one bounded decision
8
- at a time, and can queue an exact prompt only after it reads the target and text
9
- back and you confirm. It is not owned by the chat currently on screen: hide the
10
- sheet or move between workspaces and the same call continues.
8
+ at a time, creates new workspaces, and queues exact prompts. Both writes happen
9
+ only after it reads the target and text back and you confirm. It is not owned by
10
+ the chat currently on screen: hide the sheet or move between workspaces and the
11
+ same call continues.
12
+
13
+ To call the current chat, tap the phone on the right side of the chat composer,
14
+ immediately left of the context control, then **Start workspace call**.
15
+ **Call this workspace** for the active chat is also available in the command menu.
16
+ The call starts with that chat's recent
17
+ conversation already loaded, so you can discuss the task or ask for an update.
18
+ The sheet names both the workspace and chat. Browsing another tab keeps the call
19
+ on its original conversation; end it before starting a call for another chat.
20
+
21
+ The relay reads up to 24 recent user and assistant messages, capped at 16,000
22
+ characters, keeping the latest user request even after a long run. Queued prompts,
23
+ reasoning, tool output, and native child-agent messages are excluded. These messages
24
+ are sent to OpenAI with the call; an update request reads the chat again. Sending a
25
+ prompt back still requires spoken confirmation.
11
26
 
12
27
  Live captions keep both sides readable, and the text box in the call sheet is a
13
- fallback when speaking is inconvenient. The five available actions are roll
14
- call, fresh workspace overview, next decision, send preview, and confirmed send.
15
- They use the same preview/confirmation gate as the dial-in orchestrator.
28
+ fallback when speaking is inconvenient. The nine available actions are roll
29
+ call, fresh workspace overview, chat context, next decision, repository list, workspace-create
30
+ preview and confirmed creation, plus send preview and confirmed send. The two
31
+ writes use the same persisted, one-use preview/confirmation gate as the dial-in
32
+ orchestrator.
33
+
34
+ An ordinary overview excludes workspaces marked **Done** or whose pull request
35
+ is merged. Ask to include either when completed work is relevant. Overview
36
+ requests can also filter by repository, live agent status, workspace status, PR
37
+ status, and an `updated_since`/`updated_before` time window; each spoken row says
38
+ how recently its selected chat changed.
16
39
 
17
40
  This path needs the managed relay, its usual private phone URL, and an OpenAI API
18
41
  key. It does **not** need a phone number, SIP, a webhook, Funnel, or any other
19
42
  public endpoint. The permanent key stays on the Mac: the relay creates the
20
- WebRTC call, then executes those five tools over its private sideband connection.
43
+ WebRTC call, then executes those tools over its private sideband connection.
21
44
 
22
45
  Store the key without leaving it in shell history:
23
46
 
@@ -46,9 +69,13 @@ screen locked, so use the optional dial-in transport for a pocketed commute.
46
69
 
47
70
  ## Optional dial-in orchestrator
48
71
 
49
- The optional voice listener turns a phone call into a small Conductor control room: hear a bounded fleet tally, walk one decision at a time, and dispatch an exact prompt after a spoken read-back and explicit confirmation. It does not expose the PWA or the relay API publicly.
72
+ The optional voice listener turns a phone call into a small Conductor control room: hear a bounded fleet tally, walk one decision at a time, create a workspace, and dispatch an exact prompt after a spoken read-back and explicit confirmation. It does not expose the PWA or the relay API publicly.
50
73
 
51
- This first milestone has five tools: roll call, a fresh paged workspace overview, next decision, send preview, and send. Forward-to-owner answers, redial cursor resume, artifact pushes, and voice grooming remain later milestones.
74
+ The scoped endpoint has nine tools: roll call, a fresh paged workspace overview, chat context,
75
+ next decision, repository list, create preview, confirmed create, send preview,
76
+ and confirmed send. Dial-in calls open with the fleet roll call.
77
+ Forward-to-owner answers, artifact pushes, and voice grooming
78
+ remain later milestones.
52
79
 
53
80
  ## What you need
54
81
 
@@ -142,7 +169,7 @@ conductor-remote config set voice.webhook-secret "$OPENAI_WEBHOOK_SECRET"
142
169
  unset OPENAI_WEBHOOK_SECRET
143
170
  ```
144
171
 
145
- The relay accepts a valid incoming call through `POST /v1/realtime/calls/{call_id}/accept`, attaches an authenticated sideband WebSocket, and gives the session only the five scoped remote MCP tools. Remote MCP follow-up responses are driven by the broker only after both the response and every tool call in it have finished, as required by OpenAI's [Realtime MCP guide](https://developers.openai.com/api/docs/guides/realtime-mcp).
172
+ The relay accepts a valid incoming call through `POST /v1/realtime/calls/{call_id}/accept`, attaches an authenticated sideband WebSocket, and gives the session only the nine scoped remote MCP tools. Remote MCP follow-up responses are driven by the broker only after both the response and every tool call in it have finished, as required by OpenAI's [Realtime MCP guide](https://developers.openai.com/api/docs/guides/realtime-mcp).
146
173
 
147
174
  ## 5. Configure the Twilio number
148
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.110.0",
3
+ "version": "1.112.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",