conductor-remote 1.106.3 → 1.108.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.
package/dist/index.html CHANGED
@@ -25,8 +25,8 @@
25
25
  <title>Conductor Remote</title>
26
26
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
27
27
  <script src="/self-heal.js"></script>
28
- <script type="module" crossorigin src="/assets/index-BiWQQKon.js"></script>
29
- <link rel="stylesheet" crossorigin href="/assets/index-BfTQtpd5.css">
28
+ <script type="module" crossorigin src="/assets/index-DWbWwEOp.js"></script>
29
+ <link rel="stylesheet" crossorigin href="/assets/index-DM7eCDXg.css">
30
30
  <link rel="manifest" href="/manifest.webmanifest"></head>
31
31
  <body>
32
32
  <div id="root"></div>
@@ -0,0 +1,48 @@
1
+ VS Code Seti file icon theme
2
+ Source: https://github.com/microsoft/vscode/tree/main/extensions/theme-seti
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2015 - present Microsoft Corporation
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ The theme incorporates Seti UI:
27
+ https://github.com/jesseweed/seti-ui
28
+
29
+ Copyright (c) 2014 Jesse Weed
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ "Software"), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
45
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
46
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
47
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
48
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),a={module:{uri:l},exports:o,require:t};s[l]=Promise.all(n.map(e=>a[e]||t(e))).then(e=>(r(...e),o))}}define(["./workbox-dcde9eb3"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"b43f5a48bcca63e18b6da382760d00ce"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/pierre-shiki-wasm-C8ETeZem.js",revision:null},{url:"assets/pierre-light-480U9XYS.js",revision:null},{url:"assets/pierre-dark-CyvmCCZW.js",revision:null},{url:"assets/index-BiWQQKon.js",revision:null},{url:"assets/index-BfTQtpd5.css",revision:null},{url:"assets/PierrePatch-CEuLSHL_.js",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]})),e.registerRoute(({url:e})=>e.pathname.startsWith("/assets/diff-syntax/"),new e.CacheFirst({cacheName:"diff-syntax",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:31536e3})]}),"GET")});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const a=e=>i(e,l),t={module:{uri:l},exports:o,require:a};s[l]=Promise.all(n.map(e=>t[e]||a(e))).then(e=>(r(...e),o))}}define(["./workbox-dcde9eb3"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"991cad8015a1f151f99868cf55ba55dc"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/pierre-shiki-wasm-C8ETeZem.js",revision:null},{url:"assets/pierre-light-480U9XYS.js",revision:null},{url:"assets/pierre-dark-CyvmCCZW.js",revision:null},{url:"assets/index-DWbWwEOp.js",revision:null},{url:"assets/index-DM7eCDXg.css",revision:null},{url:"assets/PierrePatch-DF-STc4J.js",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]})),e.registerRoute(({url:e})=>e.pathname.startsWith("/assets/diff-syntax/"),new e.CacheFirst({cacheName:"diff-syntax",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:31536e3})]}),"GET")});
@@ -21,6 +21,7 @@ import fs from 'node:fs';
21
21
  import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import util from 'node:util';
24
+ import { scrubWorkflowSecrets } from "./shared.js";
24
25
  /** Kept small enough that a phone on a slow link can pull the whole thing; ~a day of a quiet relay. */
25
26
  const CAPACITY = 600;
26
27
  /** One AppleScript failure can carry a long stack; keep a single entry from crowding out the rest. */
@@ -60,7 +61,7 @@ export function installLogCapture() {
60
61
  const wrap = (level, original) => {
61
62
  return (...args) => {
62
63
  const at = new Date();
63
- const text = util.format(...args);
64
+ const text = scrubWorkflowSecrets(util.format(...args));
64
65
  push(level, text, at.getTime());
65
66
  original(prefixed(text, level, at));
66
67
  };
@@ -142,8 +143,8 @@ export function tailLogFile(name, limit) {
142
143
  */
143
144
  export function redactSecrets(text, token) {
144
145
  const masked = token ? text.split(token).join('<token>') : text;
145
- return masked
146
+ return scrubWorkflowSecrets(masked
146
147
  .replace(/\bsk-[A-Za-z0-9_-]{8,}/g, '<openai-key>')
147
148
  .replace(/\bwhsec_[A-Za-z0-9+/=_-]{8,}/g, '<webhook-secret>')
148
- .replace(/(token=)[^\s&"'`]+/gi, '$1<token>');
149
+ .replace(/(token=)[^\s&"'`]+/gi, '$1<token>'));
149
150
  }
@@ -14,11 +14,11 @@
14
14
  *
15
15
  * Every tool is an HTTP call to the relay, injected as `call`, and that is the
16
16
  * load-bearing decision rather than a convenience. Conductor has one shared window,
17
- * so the only thing that makes writes safe is `writes.ts` ▸ `uiTurn`, and that lock is
18
- * *process-local*. A tool that drove AppleScript itself would sit outside it, and two
19
- * agents focusing different workspaces would land each other's prompts — the exact
20
- * failure every fail-closed assertion cannot catch. Routed through the relay, the
21
- * phone, both delivery queues and every agent share one lock.
17
+ * so the only thing that makes writes safe is `writes.ts` ▸ `uiTurn`, backed by the
18
+ * coordinator's relay-shared mutex. A tool that drove AppleScript itself would sit
19
+ * outside it, and two agents focusing different workspaces would land each other's
20
+ * prompts — the exact failure every fail-closed assertion cannot catch. Routed through
21
+ * the relay, the phone, delivery queues and every agent share the same gate.
22
22
  *
23
23
  * The in-relay transport calls the relay's own API over loopback rather than reaching
24
24
  * into `reads`/`writes` directly. That is a sub-millisecond hop against a 1000-line
@@ -27,11 +27,11 @@
27
27
  */
28
28
  import { chatCursor, parseChatCursor } from "./chat-cursor.js";
29
29
  import { routes } from "./routes.js";
30
- import { agentTypeCanExposeEffort, agentTypeCanExposeFastMode, HIT_CLOSE, HIT_OPEN, isToolResult, modelAgentType, timestampMs, workspaceTitle } from "./shared.js";
30
+ import { HIT_CLOSE, HIT_OPEN, isToolResult, timestampMs, workspaceTitle } from "./shared.js";
31
31
  /** Versions we know how to speak. The client's choice wins when we know it. */
32
32
  export const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
33
33
  export const SERVER_INFO = { name: 'conductor-remote', version: '1' };
34
- export const INSTRUCTIONS = 'Drives local Conductor agents through the conductor-remote relay. search_chats and read_chat reach archived workspaces, which is where most finished work lives. delegate_task later drives the real Mac UI and launches another agent through a persisted cross-provider queue; use it only when the user explicitly asked for delegation. It never uses Conductor Plan mode. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. Its workflow option is itself an explicit delegation request: the configured planning role owns the root chat and may use delegate_task, still in ordinary chat mode and never Plan mode. plan_usage, dev_server status, dismiss_prompt, keep_awake and relay_logs touch no UI. Use dev_server instead of launching a long-lived development server from a shell: its start/stop actions use Conductor’s configured Run task, drive the real Mac UI and steal focus for a few seconds. send_prompt, split_chat, stop_turn, close_chat, set_agent_options, set_default_model, a live list_models call, set_workspace_status and archive_workspace also drive the real Mac UI and steal focus for a few seconds — confirm with the user before using them on a chat or workspace they did not name. archive_workspace also deletes the worktree and stops whatever is running there.';
34
+ export const INSTRUCTIONS = 'Drives local Conductor agents through the conductor-remote relay. search_chats and read_chat reach archived workspaces, which is where most finished work lives. delegate_task operates only inside a Workflow already authorized from the phone: the current root envelope supplies its workflow id and phase capability, and the relay later drives the real Mac UI to launch the tracked child. MCP cannot start a Workflow, edit its frozen roles, or perform recovery actions. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. plan_usage, dev_server status, dismiss_prompt, keep_awake and relay_logs touch no UI. Use dev_server instead of launching a long-lived development server from a shell: its start/stop actions use Conductor’s configured Run task, drive the real Mac UI and steal focus for a few seconds. send_prompt, split_chat, stop_turn, close_chat, set_agent_options, set_default_model, a live list_models call, set_workspace_status and archive_workspace also drive the real Mac UI and steal focus for a few seconds — confirm with the user before using them on a chat or workspace they did not name. archive_workspace also deletes the worktree and stops whatever is running there.';
35
35
  /** Reads are quick; a UI write is measured in tens of seconds (writes.ts ▸ SEND_ATTEMPT_MS). */
36
36
  export const READ_TIMEOUT_MS = 10_000;
37
37
  export const WRITE_TIMEOUT_MS = 75_000;
@@ -137,6 +137,12 @@ function need(args, key) {
137
137
  throw new Error(`${key} is required`);
138
138
  return v;
139
139
  }
140
+ function rejectUnknown(args, allowed) {
141
+ const accepted = new Set(allowed);
142
+ const unknown = Object.keys(args).filter(key => !accepted.has(key));
143
+ if (unknown.length)
144
+ throw new Error(`unknown ${unknown.length === 1 ? 'field' : 'fields'}: ${unknown.join(', ')}`);
145
+ }
140
146
  /** Build the tool set against a given relay transport. */
141
147
  export function createTools(call) {
142
148
  return [
@@ -371,7 +377,7 @@ export function createTools(call) {
371
377
  },
372
378
  {
373
379
  name: 'list_roles',
374
- description: 'List the relay’s picker-backed cross-provider roles. A role marked invalid cannot be delegated until its exact model is selected. Delegated roles never enable Conductor Plan mode.',
380
+ description: 'List the relay’s picker-backed cross-provider roles. A role marked invalid cannot be used by a new Workflow until its exact model is selected. Active Workflows keep their frozen snapshot when this configuration changes.',
375
381
  inputSchema: { type: 'object', properties: {} },
376
382
  run: async () => {
377
383
  const data = await call(routes.roles.path());
@@ -386,109 +392,41 @@ export function createTools(call) {
386
392
  return lines.join('\n') || 'no delegated roles are configured';
387
393
  }
388
394
  },
389
- {
390
- name: 'set_role',
391
- description: 'Set one cross-provider delegated role using an exact label from list_models. Effort and Fast are available only when Conductor exposes those controls for the selected provider; omit them for Cursor and OpenCode. This changes relay config only and does not drive Conductor. Plan mode is intentionally not a role option.',
392
- inputSchema: {
393
- type: 'object',
394
- properties: {
395
- role: { type: 'string', description: 'Lowercase role name, for example exploration.' },
396
- model: { type: 'string', description: 'Exact picker label from list_models.' },
397
- effort: {
398
- type: 'string',
399
- enum: ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
400
- description: 'Optional Claude/Codex reasoning control. Omit for Cursor and OpenCode.'
401
- },
402
- fast: { type: 'boolean', description: 'Optional Claude/Codex Fast control.' },
403
- preamble: { type: 'string', description: 'Optional instructions prepended to each delegated task.' }
404
- },
405
- required: ['role', 'model']
406
- },
407
- run: async (args) => {
408
- const name = need(args, 'role');
409
- const model = need(args, 'model');
410
- const effort = str(args.effort);
411
- const efforts = ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
412
- if (effort && !efforts.includes(effort))
413
- throw new Error('effort is invalid');
414
- const agentType = modelAgentType(model);
415
- if (effort && agentType && !agentTypeCanExposeEffort(agentType)) {
416
- throw new Error('the selected provider has no Conductor reasoning control; omit effort');
417
- }
418
- if (typeof args.fast === 'boolean' && agentType && !agentTypeCanExposeFastMode(agentType)) {
419
- throw new Error('the selected provider has no Conductor Fast control; omit fast');
420
- }
421
- const current = await call(routes.roles.path());
422
- const prior = current.roles[name];
423
- const preserveEffort = agentTypeCanExposeEffort(agentType);
424
- const preserveFast = agentTypeCanExposeFastMode(agentType);
425
- const role = {
426
- model,
427
- ...(effort
428
- ? { effort: effort }
429
- : preserveEffort && prior?.effort
430
- ? { effort: prior.effort }
431
- : {}),
432
- ...(typeof args.fast === 'boolean'
433
- ? { fast: args.fast }
434
- : preserveFast && prior?.fast !== undefined
435
- ? { fast: prior.fast }
436
- : {}),
437
- ...(typeof args.preamble === 'string'
438
- ? { preamble: args.preamble }
439
- : prior?.preamble !== undefined
440
- ? { preamble: prior.preamble }
441
- : {})
442
- };
443
- const config = { version: 1, roles: { ...current.roles, [name]: role } };
444
- const result = await call(routes.updateRoles.path(), {
445
- method: routes.updateRoles.method,
446
- body: config
447
- });
448
- if (!result.ok)
449
- throw new Error(`${result.error.code}: ${result.error.message}`);
450
- return `${name}: ${[role.model, role.effort, role.fast ? 'fast' : null].filter(Boolean).join(' · ')}`;
451
- }
452
- },
453
395
  {
454
396
  name: 'delegate_task',
455
- description: 'Queue work in a real sibling Conductor chat using a configured role on a different provider. The relay attaches this chat’s transcript, configures the child without Plan mode, sends the task, waits for a stable answer, and returns its Baton here. Returns immediately with a delegation id; use list_delegations to observe it. Only use it when the user asked for cross-provider delegation because the queue launches a real agent and drives the Mac UI after this call returns.',
397
+ description: 'Queue a tracked child in the Workflow this root already belongs to. Use only the workflow id and current phase capability delivered in the root’s private orchestration envelope. The relay owns the frozen child settings, transcript boundary, Baton routing, and later Mac UI work. Returns immediately with a delegation id; use list_delegations to observe it. This tool cannot start or recover a Workflow.',
456
398
  inputSchema: {
457
399
  type: 'object',
458
400
  properties: {
459
- session_id: { type: 'string', description: 'Parent chat that receives the eventual Baton.' },
460
- workspace_id: { type: 'string', description: 'Optional assertion for the parent workspace.' },
461
- role: { type: 'string', description: 'Exact role name from list_roles.' },
462
- prompt: { type: 'string' },
463
- through: { type: 'string', description: 'Optional cursor from read_chat; later rows are omitted.' },
464
- include_thinking: { type: 'boolean', description: 'Include parent reasoning in the handoff (default true).' },
465
- return_mode: {
401
+ workflow_id: { type: 'string', description: 'Workflow id from the private root envelope.' },
402
+ phase_capability: {
466
403
  type: 'string',
467
- enum: ['queue', 'steer'],
468
- description: 'Queue a new parent turn by default; steer explicitly injects into a running turn.'
469
- }
404
+ description: 'Opaque current-phase capability from that same envelope; never copy it elsewhere.'
405
+ },
406
+ session_id: { type: 'string', description: 'The persisted Workflow root session.' },
407
+ role: { type: 'string', enum: ['exploration', 'implementation'] },
408
+ prompt: { type: 'string', description: 'The independent question or implementation brief.' }
470
409
  },
471
- required: ['session_id', 'role', 'prompt']
410
+ required: ['workflow_id', 'phase_capability', 'session_id', 'role', 'prompt'],
411
+ additionalProperties: false
472
412
  },
473
413
  run: async (args) => {
414
+ rejectUnknown(args, ['workflow_id', 'phase_capability', 'session_id', 'role', 'prompt']);
415
+ const workflowId = need(args, 'workflow_id');
474
416
  const sessionId = need(args, 'session_id');
475
- const through = str(args.through);
476
- const throughRowid = through ? parseChatCursor(through) : undefined;
477
- if (through && throughRowid === null)
478
- throw new Error('through must be a cursor returned by read_chat');
479
- const returnMode = str(args.return_mode);
480
- if (returnMode && returnMode !== 'queue' && returnMode !== 'steer')
481
- throw new Error('return_mode is invalid');
482
- const result = await call(routes.delegateTask.path(sessionId), {
483
- method: routes.delegateTask.method,
484
- body: {
485
- role: need(args, 'role'),
486
- prompt: need(args, 'prompt'),
487
- workspaceId: str(args.workspace_id),
488
- throughRowid,
489
- includeThinking: args.include_thinking === undefined ? undefined : args.include_thinking === true,
490
- returnMode
491
- }
417
+ const role = need(args, 'role');
418
+ if (role !== 'exploration' && role !== 'implementation')
419
+ throw new Error('role is invalid');
420
+ const body = {
421
+ workflow_id: workflowId,
422
+ phase_capability: need(args, 'phase_capability'),
423
+ session_id: sessionId,
424
+ role,
425
+ prompt: need(args, 'prompt')
426
+ };
427
+ const result = await call(routes.workflowDelegation.path(workflowId), {
428
+ method: routes.workflowDelegation.method,
429
+ body
492
430
  });
493
431
  if (!result.ok)
494
432
  throw new Error(`${result.error.code}: ${result.error.message}`);
@@ -517,24 +455,6 @@ export function createTools(call) {
517
455
  .join('\n');
518
456
  }
519
457
  },
520
- {
521
- name: 'dismiss_delegation',
522
- description: 'Dismiss one failed delegation job. Its parent/child chats and role identity remain.',
523
- inputSchema: {
524
- type: 'object',
525
- properties: { delegation_id: { type: 'string' } },
526
- required: ['delegation_id']
527
- },
528
- run: async (args) => {
529
- const id = need(args, 'delegation_id');
530
- const result = await call(routes.dismissDelegation.path(id), {
531
- method: routes.dismissDelegation.method
532
- });
533
- if (!result.ok)
534
- throw new Error(`${result.error.code}: ${result.error.message}`);
535
- return `dismissed delegation ${result.delegationId}`;
536
- }
537
- },
538
458
  {
539
459
  name: 'workspace_diff',
540
460
  description: 'The git diff of a live workspace against its target branch, untracked files included.',
@@ -584,7 +504,7 @@ export function createTools(call) {
584
504
  },
585
505
  {
586
506
  name: 'create_workspace',
587
- description: 'Start a new Conductor workspace in a repo, optionally with a first prompt and agent settings. The workspace starts from a Conductor deep link, so creation itself needs no Accessibility. The relay applies requested model, effort, plan, and fast settings through Conductor’s UI after it creates the first chat and before it delivers the prompt. Workflow mode instead configures the root from the planning role and explicitly authorizes cross-provider delegate_task calls; it never enables Conductor Plan mode. Returns as soon as the workspace row exists (~2s), before the worktree is built.',
507
+ description: 'Start an ordinary new Conductor workspace in a repo, optionally with a first prompt and agent settings. The workspace starts from a Conductor deep link, so creation itself needs no Accessibility. The relay applies requested model, effort, plan, and fast settings through Conductor’s UI after it creates the first chat and before it delivers the prompt. This utility cannot start or join a Workflow. Returns as soon as the workspace row exists (~2s), before the worktree is built.',
588
508
  inputSchema: {
589
509
  type: 'object',
590
510
  properties: {
@@ -601,10 +521,6 @@ export function createTools(call) {
601
521
  },
602
522
  plan: { type: 'boolean', description: 'Start the first chat in Conductor’s Plan mode.' },
603
523
  fast: { type: 'boolean', description: 'Set Fast mode before the first prompt.' },
604
- workflow: {
605
- type: 'boolean',
606
- description: 'Start the first prompt as a delegated workflow rooted in the configured planning role. Requires a prompt and cannot be combined with model, effort, plan, or fast. Uses ordinary chat mode, never Conductor Plan mode.'
607
- },
608
524
  wait_for_send: {
609
525
  type: 'boolean',
610
526
  description: 'Block until the first prompt is actually delivered (tens of seconds, and longer behind other queued sends). Default false.'
@@ -614,39 +530,31 @@ export function createTools(call) {
614
530
  description: "Send the first prompt without waiting for the worktree to finish building, which is how Conductor's own New workspace box behaves. Default true. Pass false only when the agent's first move needs what the repo's setup script installs."
615
531
  }
616
532
  },
617
- required: ['repo']
533
+ required: ['repo'],
534
+ additionalProperties: false
618
535
  },
619
536
  run: async (args) => {
537
+ rejectUnknown(args, ['repo', 'prompt', 'model', 'effort', 'plan', 'fast', 'wait_for_send', 'send_immediately']);
620
538
  const repo = need(args, 'repo');
621
539
  const prompt = str(args.prompt);
622
540
  const model = str(args.model);
623
541
  const effort = str(args.effort);
624
542
  const plan = typeof args.plan === 'boolean' ? args.plan : undefined;
625
543
  const fast = typeof args.fast === 'boolean' ? args.fast : undefined;
626
- const workflow = args.workflow === true;
627
544
  const explicitAgent = model !== undefined || effort !== undefined || plan !== undefined || fast !== undefined;
628
- if (workflow && explicitAgent) {
629
- throw new Error('workflow cannot be combined with model, effort, plan, or fast');
630
- }
631
- if (workflow && !prompt)
632
- throw new Error('workflow needs a first prompt');
633
- const configured = workflow || explicitAgent;
545
+ const configured = explicitAgent;
634
546
  const send = args.wait_for_send === true;
635
547
  // Default on, matching the relay: an omitted flag must never mean "wait".
636
548
  const sendImmediately = args.send_immediately !== false;
637
549
  const data = await call(routes.createWorkspace.path(), {
638
550
  method: routes.createWorkspace.method,
639
- body: workflow
640
- ? { repo, prompt, workflow: true, send, sendImmediately }
641
- : { repo, prompt, model, effort, plan, fast, send, sendImmediately },
551
+ body: { repo, prompt, model, effort, plan, fast, send, sendImmediately },
642
552
  timeoutMs: send ? WRITE_TIMEOUT_MS : 30_000
643
553
  });
644
554
  const lines = [
645
555
  `created ${data.workspace ? workspaceTitle(data.workspace) : data.workspaceId}`,
646
556
  `workspace_id: ${data.workspaceId}`
647
557
  ];
648
- if (data.workflow)
649
- lines.push(`workflow root: ${data.workflow.role} · ${data.workflow.model}`);
650
558
  if (data.warning)
651
559
  lines.push(`! ${data.warning}`);
652
560
  else if (data.pendingPrompt && !data.sent)
@@ -671,9 +579,11 @@ export function createTools(call) {
671
579
  },
672
580
  text: { type: 'string' }
673
581
  },
674
- required: ['session_id', 'text']
582
+ required: ['session_id', 'text'],
583
+ additionalProperties: false
675
584
  },
676
585
  run: async (args) => {
586
+ rejectUnknown(args, ['session_id', 'workspace_id', 'text']);
677
587
  const sessionId = need(args, 'session_id');
678
588
  const text = need(args, 'text');
679
589
  const data = await call(routes.sendPrompt.path(sessionId), {
@@ -42,13 +42,15 @@ export class ModelCache {
42
42
  return;
43
43
  const key = group(agentType ?? '');
44
44
  const selectedDefault = clean(defaultModel ? [defaultModel] : [])[0] ?? this.defaultModel();
45
+ const now = Date.now();
45
46
  const entry = {
46
47
  agentType: key,
47
48
  // A live menu is authoritative. Replacing its group drops a model Conductor
48
49
  // stopped offering instead of leaving a stale choice in a later workspace.
49
50
  models: next,
50
51
  defaultModel: selectedDefault,
51
- updatedAt: Date.now()
52
+ snapshotAt: now,
53
+ updatedAt: now
52
54
  };
53
55
  this.entries = [...this.entries.filter(existing => existing.agentType !== key), entry]
54
56
  .map(existing => ({ ...existing, defaultModel: selectedDefault ?? existing.defaultModel }))
@@ -65,6 +67,9 @@ export class ModelCache {
65
67
  agentType: key,
66
68
  models: clean([...(current?.models ?? []), model]),
67
69
  defaultModel: current?.defaultModel ?? this.defaultModel(),
70
+ // Selecting one row proves that row, not the rest of the picker. Preserve
71
+ // an existing whole-menu observation but mark a brand-new group partial.
72
+ snapshotAt: current ? (current.snapshotAt === undefined ? current.updatedAt : current.snapshotAt) : null,
68
73
  updatedAt: Date.now()
69
74
  };
70
75
  this.entries = [...this.entries.filter(existing => existing.agentType !== key), entry].sort((a, b) => a.agentType.localeCompare(b.agentType));
@@ -85,12 +90,22 @@ export class ModelCache {
85
90
  return [];
86
91
  return parsed
87
92
  .filter(entry => typeof entry?.agentType === 'string' && Array.isArray(entry.models))
88
- .map(entry => ({
89
- agentType: group(entry.agentType),
90
- models: clean(entry.models.filter((model) => typeof model === 'string')),
91
- defaultModel: typeof entry.defaultModel === 'string' ? clean([entry.defaultModel])[0] || undefined : undefined,
92
- updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : 0
93
- }))
93
+ .map(entry => {
94
+ const snapshotAt = Object.hasOwn(entry, 'snapshotAt')
95
+ ? entry.snapshotAt === null
96
+ ? null
97
+ : Number.isFinite(entry.snapshotAt)
98
+ ? Number(entry.snapshotAt)
99
+ : null
100
+ : undefined;
101
+ return {
102
+ agentType: group(entry.agentType),
103
+ models: clean(entry.models.filter((model) => typeof model === 'string')),
104
+ defaultModel: typeof entry.defaultModel === 'string' ? clean([entry.defaultModel])[0] || undefined : undefined,
105
+ ...(snapshotAt === undefined ? {} : { snapshotAt }),
106
+ updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : 0
107
+ };
108
+ })
94
109
  .filter(entry => entry.models.length);
95
110
  }
96
111
  catch {