conductor-remote 1.109.0 → 1.111.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
@@ -9,6 +9,9 @@ export const VOICE_TOOL_NAMES = [
9
9
  'voice_roll_call',
10
10
  'voice_workspace_overview',
11
11
  'voice_next_decision',
12
+ 'voice_list_repos',
13
+ 'voice_create_workspace_preview',
14
+ 'voice_create_workspace',
12
15
  'voice_send_preview',
13
16
  'voice_send'
14
17
  ];
@@ -20,11 +23,37 @@ export const VOICE_TOOL_DEFINITIONS = [
20
23
  },
21
24
  {
22
25
  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.',
26
+ description: 'Get a fresh, dated overview of 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 an overview or workspace status, even if one was already given. Pass the same filters with the returned cursor to continue.',
24
27
  inputSchema: {
25
28
  type: 'object',
26
29
  properties: {
27
- cursor: { type: 'number', description: 'The cursor returned by the previous overview page; default 0.' }
30
+ cursor: { type: 'number', description: 'The cursor returned by the previous overview page; default 0.' },
31
+ repo: { type: 'string', description: 'Only this exact repository name.' },
32
+ agent_status: {
33
+ type: 'string',
34
+ enum: ['working', 'idle', 'error', 'needs-you'],
35
+ description: 'Only workspaces whose matching chat has this live agent status.'
36
+ },
37
+ workspace_status: {
38
+ type: 'string',
39
+ enum: ['backlog', 'in-progress', 'in-review', 'done', 'canceled'],
40
+ description: 'Only workspaces in this Conductor sidebar status. Requesting done includes Done workspaces.'
41
+ },
42
+ pr_status: {
43
+ type: 'string',
44
+ enum: ['merged', 'draft', 'conflicts', 'checks_failed', 'checks_pending', 'mergeable', 'none'],
45
+ description: 'Only workspaces with this pull-request status. Requesting merged includes merged workspaces.'
46
+ },
47
+ updated_since: {
48
+ type: 'string',
49
+ description: 'Only chat activity since today, yesterday, this-week, this-month, a relative duration like 24h or 7d, or an ISO date/time.'
50
+ },
51
+ updated_before: {
52
+ type: 'string',
53
+ 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.'
54
+ },
55
+ include_done: { type: 'boolean', description: 'Include Done workspaces; default false.' },
56
+ include_merged: { type: 'boolean', description: 'Include merged workspaces; default false.' }
28
57
  }
29
58
  }
30
59
  },
@@ -42,6 +71,36 @@ export const VOICE_TOOL_DEFINITIONS = [
42
71
  }
43
72
  }
44
73
  },
74
+ {
75
+ name: 'voice_list_repos',
76
+ description: 'List the repositories where Conductor can create a workspace. Use before a creation preview.',
77
+ inputSchema: { type: 'object', properties: {} }
78
+ },
79
+ {
80
+ name: 'voice_create_workspace_preview',
81
+ 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.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ repo: { type: 'string', description: 'Exact repository name from voice_list_repos.' },
86
+ prompt: { type: 'string', description: 'Optional first prompt. Omit to create an empty workspace.' }
87
+ },
88
+ required: ['repo']
89
+ }
90
+ },
91
+ {
92
+ name: 'voice_create_workspace',
93
+ description: 'Create an exact workspace preview after the user said yes. Requires its token, repository, and unchanged prompt; never accepts raw unpreviewed creation.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ token: { type: 'string' },
98
+ repo: { type: 'string' },
99
+ prompt: { type: 'string', description: 'The unchanged preview prompt; omit only when the preview was empty.' }
100
+ },
101
+ required: ['token', 'repo']
102
+ }
103
+ },
45
104
  {
46
105
  name: 'voice_send_preview',
47
106
  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 +143,33 @@ function need(args, key) {
84
143
  throw new Error(`${key} is required`);
85
144
  return value.trim();
86
145
  }
146
+ function optional(args, key) {
147
+ const value = args[key];
148
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
149
+ }
87
150
  function answer(value) {
88
151
  return JSON.stringify(value);
89
152
  }
90
153
  function refusal(reason) {
91
154
  switch (reason) {
92
155
  case 'expired':
93
- return 'That preview expired. Read the exact target and text back again before sending.';
156
+ return 'That preview expired. Read the exact action back again before approving it.';
94
157
  case 'foreign-call':
95
158
  return 'That preview belongs to another call and cannot be used here.';
96
159
  case 'foreign-session':
97
160
  return 'That preview belongs to a different chat. Preview this target again.';
161
+ case 'foreign-repo':
162
+ return 'That preview belongs to a different repository. Preview this workspace again.';
98
163
  case 'text-mismatch':
99
164
  return 'The send text does not exactly match the preview. Preview the changed text first.';
165
+ case 'prompt-mismatch':
166
+ return 'The first prompt does not exactly match the preview. Preview the changed workspace first.';
167
+ case 'wrong-action':
168
+ return 'That preview is for a different action. Make a new preview first.';
100
169
  case 'already-used':
101
- return 'That preview was already used. The relay will not send it twice.';
170
+ return 'That preview was already used. The relay will not run it twice.';
102
171
  case 'unknown':
103
- return 'That preview token is unknown. Make a new preview before sending.';
172
+ return 'That preview token is unknown. Make a new preview before continuing.';
104
173
  }
105
174
  }
106
175
  function later(task) {
@@ -123,7 +192,30 @@ export function createVoiceTools(context) {
123
192
  ...definition('voice_workspace_overview'),
124
193
  run: async (args) => {
125
194
  const cursor = typeof args.cursor === 'number' && Number.isFinite(args.cursor) ? args.cursor : 0;
126
- return answer(await context.board.workspaceOverview(cursor));
195
+ const filters = {};
196
+ const repo = optional(args, 'repo');
197
+ const agentStatus = optional(args, 'agent_status');
198
+ const workspaceStatus = optional(args, 'workspace_status');
199
+ const prStatus = optional(args, 'pr_status');
200
+ const updatedSince = optional(args, 'updated_since');
201
+ const updatedBefore = optional(args, 'updated_before');
202
+ if (repo)
203
+ filters.repo = repo;
204
+ if (agentStatus)
205
+ filters.agentStatus = agentStatus;
206
+ if (workspaceStatus)
207
+ filters.workspaceStatus = workspaceStatus;
208
+ if (prStatus)
209
+ filters.prStatus = prStatus;
210
+ if (updatedSince)
211
+ filters.updatedSince = updatedSince;
212
+ if (updatedBefore)
213
+ filters.updatedBefore = updatedBefore;
214
+ if (typeof args.include_done === 'boolean')
215
+ filters.includeDone = args.include_done;
216
+ if (typeof args.include_merged === 'boolean')
217
+ filters.includeMerged = args.include_merged;
218
+ return answer(await context.board.workspaceOverview(cursor, filters));
127
219
  }
128
220
  },
129
221
  {
@@ -136,6 +228,71 @@ export function createVoiceTools(context) {
136
228
  return answer(next ?? { spoken: 'There are no more decisions in this call.', cursor, done: true });
137
229
  }
138
230
  },
231
+ {
232
+ ...definition('voice_list_repos'),
233
+ run: async () => {
234
+ const repos = context.listRepos();
235
+ return answer({
236
+ spoken: repos.length
237
+ ? clipExact(`Available repositories: ${repos.map(repo => repo.name).join(', ')}.`, 600)
238
+ : 'Conductor has no repository available for a new workspace.',
239
+ repos
240
+ });
241
+ }
242
+ },
243
+ {
244
+ ...definition('voice_create_workspace_preview'),
245
+ run: async (args) => {
246
+ const requestedRepo = need(args, 'repo');
247
+ const repo = context.listRepos().find(candidate => candidate.name.toLowerCase() === requestedRepo.toLowerCase());
248
+ if (!repo)
249
+ return answer({
250
+ status: 'refused',
251
+ spoken: `I could not find the ${oneLine(requestedRepo, 80)} repository. Ask me to list repositories first.`
252
+ });
253
+ const prompt = optional(args, 'prompt') ?? '';
254
+ const preview = context.previews.createWorkspace({ callId: context.callId, repo: repo.name, prompt });
255
+ const detail = prompt ? ` with this first prompt: “${oneLine(prompt, 220)}”` : ' with no first prompt.';
256
+ return answer({
257
+ status: 'preview',
258
+ token: preview.token,
259
+ repo: repo.name,
260
+ prompt,
261
+ spoken: `Create a new workspace in ${oneLine(repo.name, 80)}${detail} Say yes to create it.`
262
+ });
263
+ }
264
+ },
265
+ {
266
+ ...definition('voice_create_workspace'),
267
+ run: async (args) => {
268
+ const token = need(args, 'token');
269
+ const repo = need(args, 'repo');
270
+ const prompt = optional(args, 'prompt') ?? '';
271
+ const claimed = context.previews.claimWorkspace(token, { callId: context.callId, repo, prompt });
272
+ if (!claimed.ok)
273
+ return answer({ status: 'refused', spoken: refusal(claimed.reason) });
274
+ later(async () => {
275
+ try {
276
+ const result = await context.createWorkspace(claimed.preview);
277
+ if (!result.ok) {
278
+ await context.announce(`The new ${oneLine(repo, 80)} workspace was not created. ${oneLine(result.error ?? 'Try again later.', 220)}`);
279
+ return;
280
+ }
281
+ const created = prompt
282
+ ? `Created a new ${oneLine(repo, 80)} workspace and queued its first prompt.`
283
+ : `Created a new empty ${oneLine(repo, 80)} workspace.`;
284
+ await context.announce(result.warning ? `${created} ${oneLine(result.warning, 220)}` : created);
285
+ }
286
+ catch (error) {
287
+ await context.announce(`The new ${oneLine(repo, 80)} workspace was not created. ${oneLine(error instanceof Error ? error.message : String(error), 220)}`);
288
+ }
289
+ });
290
+ return answer({
291
+ status: 'queued',
292
+ spoken: `Creating a new workspace in ${oneLine(repo, 80)}. I will say when it is ready.`
293
+ });
294
+ }
295
+ },
139
296
  {
140
297
  ...definition('voice_send_preview'),
141
298
  run: async (args) => {
@@ -5,19 +5,28 @@
5
5
  The primary foreground voice mode is one 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.
11
12
 
12
13
  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.
14
+ fallback when speaking is inconvenient. The eight available actions are roll
15
+ call, fresh workspace overview, next decision, repository list, workspace-create
16
+ preview and confirmed creation, plus send preview and confirmed send. The two
17
+ writes use the same persisted, one-use preview/confirmation gate as the dial-in
18
+ orchestrator.
19
+
20
+ An ordinary overview excludes workspaces marked **Done** or whose pull request
21
+ is merged. Ask to include either when completed work is relevant. Overview
22
+ requests can also filter by repository, live agent status, workspace status, PR
23
+ status, and an `updated_since`/`updated_before` time window; each spoken row says
24
+ how recently its selected chat changed.
16
25
 
17
26
  This path needs the managed relay, its usual private phone URL, and an OpenAI API
18
27
  key. It does **not** need a phone number, SIP, a webhook, Funnel, or any other
19
28
  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.
29
+ WebRTC call, then executes those eight tools over its private sideband connection.
21
30
 
22
31
  Store the key without leaving it in shell history:
23
32
 
@@ -46,9 +55,12 @@ screen locked, so use the optional dial-in transport for a pocketed commute.
46
55
 
47
56
  ## Optional dial-in orchestrator
48
57
 
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.
58
+ 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
59
 
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.
60
+ The scoped endpoint has eight tools: roll call, a fresh paged workspace overview,
61
+ next decision, repository list, create preview, confirmed create, send preview,
62
+ and confirmed send. Forward-to-owner answers, artifact pushes, and voice grooming
63
+ remain later milestones.
52
64
 
53
65
  ## What you need
54
66
 
@@ -142,7 +154,7 @@ conductor-remote config set voice.webhook-secret "$OPENAI_WEBHOOK_SECRET"
142
154
  unset OPENAI_WEBHOOK_SECRET
143
155
  ```
144
156
 
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).
157
+ 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 eight 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
158
 
147
159
  ## 5. Configure the Twilio number
148
160
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.109.0",
3
+ "version": "1.111.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.",