clay-server 3.4.0-beta.1 → 3.4.0-beta.11

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 (68) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +1 -1
  3. package/lib/project-connection.js +20 -5
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-shell-command.js +160 -0
  8. package/lib/project-user-message.js +10 -0
  9. package/lib/project.js +19 -4
  10. package/lib/public/antigravity-avatar.png +0 -0
  11. package/lib/public/app.js +28 -0
  12. package/lib/public/copilot-avatar.svg +5 -0
  13. package/lib/public/css/command-palette.css +22 -2
  14. package/lib/public/css/icon-strip.css +29 -13
  15. package/lib/public/css/input.css +75 -0
  16. package/lib/public/css/mates.css +6 -0
  17. package/lib/public/css/menus.css +48 -14
  18. package/lib/public/css/messages.css +9 -0
  19. package/lib/public/css/pane.css +4 -0
  20. package/lib/public/css/pwa-mobile.css +17 -0
  21. package/lib/public/css/title-bar.css +19 -0
  22. package/lib/public/grok-avatar.svg +4 -0
  23. package/lib/public/index.html +38 -7
  24. package/lib/public/junie-avatar.svg +11 -0
  25. package/lib/public/kimi-avatar.svg +4 -0
  26. package/lib/public/modules/app-header.js +4 -0
  27. package/lib/public/modules/app-messages.js +28 -5
  28. package/lib/public/modules/app-panels.js +28 -8
  29. package/lib/public/modules/app-projects.js +3 -1
  30. package/lib/public/modules/app-rendering.js +19 -4
  31. package/lib/public/modules/input.js +58 -28
  32. package/lib/public/modules/mate-sidebar.js +11 -3
  33. package/lib/public/modules/notifications.js +7 -1
  34. package/lib/public/modules/pane-bridge.js +11 -0
  35. package/lib/public/modules/pane-links.js +23 -0
  36. package/lib/public/modules/project-switcher.js +7 -6
  37. package/lib/public/modules/shell-command.js +148 -0
  38. package/lib/public/modules/sidebar-mates.js +1 -1
  39. package/lib/public/modules/sidebar-mobile.js +2 -1
  40. package/lib/public/modules/sidebar-projects.js +34 -43
  41. package/lib/public/modules/sidebar-sessions.js +6 -6
  42. package/lib/public/modules/split-pair-ui.js +3 -2
  43. package/lib/public/modules/tools.js +6 -1
  44. package/lib/public/modules/vendor-priority.js +14 -0
  45. package/lib/public/modules/vendor-selection.js +20 -0
  46. package/lib/public/modules/worktree-location.js +17 -0
  47. package/lib/public/qwen-avatar.svg +4 -0
  48. package/lib/public/style.css +3 -2
  49. package/lib/sdk-bridge.js +49 -25
  50. package/lib/sdk-message-processor.js +10 -1
  51. package/lib/session-notes-mcp-server.js +7 -1
  52. package/lib/worktree.js +9 -4
  53. package/lib/ws-schema.js +3 -0
  54. package/lib/yoke/acp-agent-profiles.js +64 -14
  55. package/lib/yoke/adapters/antigravity.js +417 -0
  56. package/lib/yoke/adapters/codex.js +98 -3
  57. package/lib/yoke/adapters/copilot.js +7 -0
  58. package/lib/yoke/adapters/grok.js +7 -0
  59. package/lib/yoke/adapters/junie.js +7 -0
  60. package/lib/yoke/adapters/kimi.js +7 -0
  61. package/lib/yoke/adapters/qwen.js +7 -0
  62. package/lib/yoke/codex-app-server.js +25 -8
  63. package/lib/yoke/index.js +67 -28
  64. package/lib/yoke/instructions.js +3 -0
  65. package/lib/yoke/vendor-registry.js +61 -6
  66. package/package.json +1 -1
  67. package/lib/public/gemini-avatar.svg +0 -11
  68. package/lib/yoke/adapters/gemini.js +0 -7
@@ -12,7 +12,7 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
12
12
  import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
15
+ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
16
16
  import { openGroup, separateGroup } from './split-view.js';
17
17
  import { groupedSessionIds } from './split-group-helpers.js';
18
18
  import { openPairDialog } from './split-pair-ui.js';
@@ -299,13 +299,10 @@ function appendSessionCloseButton(el, session) {
299
299
  el.appendChild(closeBtn);
300
300
  }
301
301
 
302
- // Resolve the vendor the "New session" button should launch: the project's
303
- // last-used vendor when its CLI is still installed, else the first installed
304
- // vendor, else Claude (so the button always has a sane label to render).
302
+ // Resolve the vendor the "New session" button should launch from the stable
303
+ // installed-vendor priority (Claude, then Codex, then the remaining vendors).
305
304
  export function resolveDefaultVendor() {
306
305
  var installed = store.get('installedVendors') || [];
307
- var last = store.get('lastVendor') || "";
308
- if (last && installed.indexOf(last) !== -1) return last;
309
306
  for (var i = 0; i < VENDOR_ORDER.length; i++) {
310
307
  if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
311
308
  }
@@ -330,6 +327,7 @@ export function startNewSession(vendor, extra) {
330
327
  currentModel: "",
331
328
  currentModels: [],
332
329
  vendorSelectionLocked: true,
330
+ sessionVendorBound: true,
333
331
  });
334
332
  }
335
333
 
@@ -396,10 +394,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
396
394
  item.type = "button";
397
395
  item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
398
396
  '<span class="session-new-vendor-name">' + name + '</span>' +
397
+ (isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
399
398
  (isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
400
399
  item.title = isInstalled
401
400
  ? "New " + name + " session"
402
401
  : name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
402
+ if (isExperimentalVendor(vendor)) item.title += " · Experimental integration; not yet validated through direct use testing";
403
403
  item.addEventListener("click", function (e) {
404
404
  e.stopPropagation();
405
405
  closeSessionCtxMenu();
@@ -1,6 +1,6 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER } from './app-rendering.js';
3
+ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, isExperimentalVendor } from './app-rendering.js';
4
4
  import { effortLevelsFor, effortDisplayName } from './app-panels.js';
5
5
  import { showToast } from './utils.js';
6
6
 
@@ -46,7 +46,8 @@ function buildVendorSelect(installed, preferred) {
46
46
  if (installed.indexOf(VENDOR_ORDER[i]) === -1) continue;
47
47
  var option = document.createElement("option");
48
48
  option.value = VENDOR_ORDER[i];
49
- option.textContent = VENDOR_NAMES[VENDOR_ORDER[i]] || VENDOR_ORDER[i];
49
+ option.textContent = (isExperimentalVendor(VENDOR_ORDER[i]) ? "🧪 " : "") + (VENDOR_NAMES[VENDOR_ORDER[i]] || VENDOR_ORDER[i]);
50
+ if (isExperimentalVendor(VENDOR_ORDER[i])) option.title = "Experimental integration; not yet validated through direct use testing";
50
51
  select.appendChild(option);
51
52
  }
52
53
  if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
@@ -817,8 +817,13 @@ function resolvePermissionIdentity(mateId, vendor) {
817
817
  var vendorAvatars = {
818
818
  claude: "/claude-code-avatar.png",
819
819
  codex: "/codex-avatar.png",
820
- gemini: "/gemini-avatar.svg",
820
+ antigravity: "/antigravity-avatar.png",
821
821
  opencode: "/opencode-avatar.svg",
822
+ kimi: "/kimi-avatar.svg",
823
+ grok: "/grok-avatar.svg",
824
+ copilot: "/copilot-avatar.svg",
825
+ qwen: "/qwen-avatar.svg",
826
+ junie: "/junie-avatar.svg",
822
827
  kiro: "/kiro-avatar.svg",
823
828
  };
824
829
  var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
@@ -0,0 +1,14 @@
1
+ export var VENDOR_ORDER = ["claude", "codex", "grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode", "kiro"];
2
+ export var EXPERIMENTAL_VENDORS = ["grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode"];
3
+
4
+ export function isExperimentalVendor(vendor) {
5
+ return EXPERIMENTAL_VENDORS.indexOf(vendor) !== -1;
6
+ }
7
+
8
+ export function firstInstalledVendor(installedVendors) {
9
+ var installed = Array.isArray(installedVendors) ? installedVendors : [];
10
+ for (var i = 0; i < VENDOR_ORDER.length; i++) {
11
+ if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
12
+ }
13
+ return "";
14
+ }
@@ -0,0 +1,20 @@
1
+ import { store } from './store.js';
2
+ import { firstInstalledVendor } from './vendor-priority.js';
3
+
4
+ export function selectDefaultVendorForBlankSession() {
5
+ var state = store.snap();
6
+ if (!state.activeSessionId || state.sessionHasHistory || state.sessionVendorBound || state.dmMode) return "";
7
+ var vendor = firstInstalledVendor(state.installedVendors);
8
+ if (!vendor) return "";
9
+ if (vendor === state.currentVendor) {
10
+ if (state.vendorSelectionLocked) store.set({ vendorSelectionLocked: false });
11
+ return vendor;
12
+ }
13
+ store.set({
14
+ currentVendor: vendor,
15
+ currentModel: "",
16
+ currentModels: [],
17
+ vendorSelectionLocked: false,
18
+ });
19
+ return vendor;
20
+ }
@@ -0,0 +1,17 @@
1
+ export function isExternalWorktree(project) {
2
+ return !!(project && project.isWorktree && project.worktreeExternal === true);
3
+ }
4
+
5
+ export function externalWorktreeTooltip(name) {
6
+ return name + " — outside the main project folder";
7
+ }
8
+
9
+ export function appendExternalWorktreeBadge(container, className) {
10
+ var badge = document.createElement("span");
11
+ badge.className = className;
12
+ badge.setAttribute("role", "img");
13
+ badge.setAttribute("aria-label", "Outside the main project folder");
14
+ badge.innerHTML = '<i data-lucide="external-link"></i>';
15
+ container.appendChild(badge);
16
+ return badge;
17
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Qwen Code">
2
+ <rect width="64" height="64" rx="14" fill="#6b4eff"/>
3
+ <path d="M32 12a20 20 0 1 0 12 36l7 5-3-11a20 20 0 0 0-16-30zm0 9a11 11 0 1 1-7.8 18.8A11 11 0 0 1 32 21z" fill="#fff"/>
4
+ </svg>
@@ -1,12 +1,12 @@
1
1
  @import url("css/base.css");
2
2
  @import url("css/icon-strip.css");
3
- @import url("css/title-bar.css");
3
+ @import url("css/title-bar.css?v=20260824");
4
4
  @import url("css/pane.css");
5
5
  @import url("css/worker-proposal.css");
6
6
  @import url("css/sidebar.css");
7
7
  @import url("css/overlays.css");
8
8
  @import url("css/menus.css");
9
- @import url("css/messages.css?v=20260407");
9
+ @import url("css/messages.css?v=20260824");
10
10
  @import url("css/rewind.css");
11
11
  @import url("css/input.css");
12
12
  @import url("css/filebrowser.css");
@@ -35,3 +35,4 @@
35
35
  @import url("css/debate.css");
36
36
  @import url("css/notifications-center.css");
37
37
  @import url("css/tui-attention.css");
38
+ @import url("css/pwa-mobile.css?v=20260824f");
package/lib/sdk-bridge.js CHANGED
@@ -110,6 +110,7 @@ function createSDKBridge(opts) {
110
110
  var adapters = opts.adapters || {};
111
111
  var mateDisplayName = opts.mateDisplayName || "";
112
112
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
113
+ var osUsers = !!opts.osUsers;
113
114
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
114
115
  // mcpServers may be either a static object or a getter function. The
115
116
  // getter form lets callers gate individual servers at call time (e.g.
@@ -323,6 +324,14 @@ function createSDKBridge(opts) {
323
324
  var mergeSkills = skills.mergeSkills;
324
325
 
325
326
  // --- Message processing (extracted to sdk-message-processor.js) ---
327
+ function getSessionLinuxUser(session) {
328
+ if (!osUsers || !session) return null;
329
+ if (session.lastLinuxUser) return session.lastLinuxUser;
330
+ if (!session.ownerId) return null;
331
+ var owner = usersModule.findUserById(session.ownerId);
332
+ return owner && owner.linuxUser ? owner.linuxUser : null;
333
+ }
334
+
326
335
  // Auto-generate a session title via YOKE adapter.generateTitle().
327
336
  // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
328
337
  function autoGenerateTitle(session) {
@@ -345,7 +354,10 @@ function createSDKBridge(opts) {
345
354
  }
346
355
  console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
347
356
 
348
- sessionAdapter.generateTitle(userMessages, { cwd: cwd }).then(function(title) {
357
+ sessionAdapter.generateTitle(userMessages, {
358
+ cwd: cwd,
359
+ linuxUser: getSessionLinuxUser(session) || undefined,
360
+ }).then(function(title) {
349
361
  if (!title || title.length < 2) return;
350
362
  title = title.substring(0, 100);
351
363
  if (!session.titleManuallySet) {
@@ -1131,7 +1143,9 @@ function createSDKBridge(opts) {
1131
1143
  async function rollbackConversation(session, numTurns) {
1132
1144
  var sessionAdapter = getAdapterForSession(session);
1133
1145
  if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
1134
- await sessionAdapter.rollbackThread(session.cliSessionId, numTurns);
1146
+ await sessionAdapter.rollbackThread(session.cliSessionId, numTurns, {
1147
+ linuxUser: getSessionLinuxUser(session) || undefined,
1148
+ });
1135
1149
  }
1136
1150
  // Claude: conversation rollback is handled by rewindFiles + local history trim
1137
1151
  }
@@ -1143,7 +1157,11 @@ function createSDKBridge(opts) {
1143
1157
 
1144
1158
  async function forkSessionUnified(session, uuid) {
1145
1159
  var sessionAdapter = getAdapterForSession(session);
1146
- var result = await sessionAdapter.forkSession(session.cliSessionId, { upToMessageId: uuid, dir: cwd });
1160
+ var result = await sessionAdapter.forkSession(session.cliSessionId, {
1161
+ upToMessageId: uuid,
1162
+ dir: cwd,
1163
+ linuxUser: getSessionLinuxUser(session) || undefined,
1164
+ });
1147
1165
  if (!result || !result.sessionId) throw new Error("Fork returned no session id");
1148
1166
 
1149
1167
  // Adapters with rollbackThread (e.g. Codex) use local history copy
@@ -1471,6 +1489,7 @@ function createSDKBridge(opts) {
1471
1489
 
1472
1490
  var queryOpts = {
1473
1491
  cwd: cwd,
1492
+ linuxUser: linuxUser || undefined,
1474
1493
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1475
1494
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1476
1495
  // would strip the driver session down to the two-sentence pair preset.
@@ -1528,6 +1547,10 @@ function createSDKBridge(opts) {
1528
1547
  ACP: {
1529
1548
  mcpServers: getAcpMcpServers(session),
1530
1549
  },
1550
+ ANTIGRAVITY: {
1551
+ dangerouslySkipPermissions: dangerouslySkipPermissions
1552
+ || session.permissionMode === "bypassPermissions",
1553
+ },
1531
1554
  KIRO: {
1532
1555
  engine: kiroConfig.engine,
1533
1556
  mode: kiroConfig.mode,
@@ -1790,16 +1813,20 @@ function createSDKBridge(opts) {
1790
1813
  } catch (e) {}
1791
1814
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1792
1815
 
1793
- var acpVendorKeys = ["gemini", "opencode"];
1816
+ var subprocessVendorKeys = ["antigravity", "opencode", "kimi", "grok", "copilot", "qwen", "junie"];
1794
1817
  var acpProfiles = require("./yoke/acp-agent-profiles");
1795
- for (var acpVendorIndex = 0; acpVendorIndex < acpVendorKeys.length; acpVendorIndex++) {
1796
- var acpVendor = acpVendorKeys[acpVendorIndex];
1797
- var acpInfo = yoke.getVendorInfo(acpVendor);
1798
- if (!linuxUser || acpInfo.osUserIsolation) {
1799
- var acpProfile = acpProfiles.getAcpAgentProfile(acpVendor);
1800
- if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(acpVendor);
1818
+ for (var subprocessVendorIndex = 0; subprocessVendorIndex < subprocessVendorKeys.length; subprocessVendorIndex++) {
1819
+ var subprocessVendor = subprocessVendorKeys[subprocessVendorIndex];
1820
+ var subprocessInfo = yoke.getVendorInfo(subprocessVendor);
1821
+ if (!linuxUser || subprocessInfo.osUserIsolation) {
1822
+ if (subprocessVendor === "antigravity") {
1823
+ if (tryLookup(subprocessInfo.binaryName)) result.push(subprocessVendor);
1824
+ } else {
1825
+ var acpProfile = acpProfiles.getAcpAgentProfile(subprocessVendor);
1826
+ if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(subprocessVendor);
1827
+ }
1801
1828
  } else {
1802
- console.log("[sdk-bridge] " + acpInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1829
+ console.log("[sdk-bridge] " + subprocessInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1803
1830
  }
1804
1831
  }
1805
1832
 
@@ -1832,6 +1859,13 @@ function createSDKBridge(opts) {
1832
1859
  var defaultVendor = adapter ? adapter.vendor : "claude";
1833
1860
  sm.defaultVendor = defaultVendor;
1834
1861
 
1862
+ // Installation detection is independent of adapter initialization. Publish
1863
+ // it before warmup so a new project does not render the initial empty state
1864
+ // as if every CLI were missing while the default adapter starts up.
1865
+ sm.installedVendors = detectInstalledVendors(linuxUser);
1866
+ sm.availableVendors = getAvailableVendors(linuxUser);
1867
+ sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1868
+
1835
1869
  // Initialize default adapter first (provides skills, slash commands, etc.)
1836
1870
  if (adapter) {
1837
1871
  try {
@@ -1883,19 +1917,8 @@ function createSDKBridge(opts) {
1883
1917
  // actually issues a query with it.
1884
1918
  sm.modelsByVendor = sm.modelsByVendor || {};
1885
1919
 
1886
- // Detect installed vendors per-user (binary existence check)
1887
- sm.installedVendors = detectInstalledVendors(linuxUser);
1888
- sm.availableVendors = getAvailableVendors(linuxUser);
1889
-
1890
- // Send initial state to client
1891
- send({
1892
- type: "model_info",
1893
- model: sm.currentModel || "",
1894
- models: getModelsForVendor(defaultVendor),
1895
- vendor: defaultVendor,
1896
- availableVendors: sm.availableVendors,
1897
- installedVendors: sm.installedVendors,
1898
- });
1920
+ // Send the fully initialized state to the client.
1921
+ sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1899
1922
  }
1900
1923
 
1901
1924
  async function setModel(session, model) {
@@ -2009,7 +2032,7 @@ function createSDKBridge(opts) {
2009
2032
  // Creates a mention session that can be reused across multiple mentions
2010
2033
  // within a conversation flow (session continuity).
2011
2034
  async function createMentionSession(opts) {
2012
- // opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2035
+ // opts: { vendor, linuxUser, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2013
2036
  var abortController = new AbortController();
2014
2037
 
2015
2038
  // Current response callbacks (swapped on each pushMessage)
@@ -2029,6 +2052,7 @@ function createSDKBridge(opts) {
2029
2052
  try {
2030
2053
  handle = await mentionAdapter.createQuery({
2031
2054
  cwd: cwd,
2055
+ linuxUser: opts.linuxUser || undefined,
2032
2056
  systemPrompt: opts.claudeMd,
2033
2057
  model: opts.model || undefined,
2034
2058
  toolServers: opts.includeMcpServers ? (mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined) : undefined,
@@ -792,7 +792,16 @@ function attachMessageProcessor(ctx) {
792
792
  // processQueryStream can finish the turn when the iterator closes.
793
793
  var adapterErrorText = parsed.text || parsed.message || parsed.error || "Agent runtime error";
794
794
  session._lastAdapterError = adapterErrorText;
795
- sendAndRecord(session, { type: "error", text: adapterErrorText });
795
+ var isSessionWriterConflict = /thread-store conflict|already has an active writer/i.test(adapterErrorText);
796
+ if (isSessionWriterConflict) {
797
+ sendAndRecord(session, {
798
+ type: "session_writer_conflict",
799
+ vendor: session.vendor || "codex",
800
+ text: "This Codex session is already open in another Clay or Codex process. Stop the other server or close the other session, then try again.",
801
+ });
802
+ } else {
803
+ sendAndRecord(session, { type: "error", text: adapterErrorText });
804
+ }
796
805
 
797
806
  } else if (parsed.yokeType === "model_refusal") {
798
807
  // Model declined the request. "fallback" => the CLI retried on another
@@ -2,7 +2,13 @@
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
- var MEMORY_CONTRACT = "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
5
+ var MEMORY_CONTRACT =
6
+ "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
+ "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
9
+ "Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. Include the observable evidence, affected component, likely impact, and a clear next action. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
10
+ "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
11
+ "Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
6
12
 
7
13
  function getToolDefs(handlers) {
8
14
  return [
package/lib/worktree.js CHANGED
@@ -43,9 +43,14 @@ function isWorktree(projectPath) {
43
43
  }
44
44
  }
45
45
 
46
+ function isPathInside(parentPath, candidatePath) {
47
+ var relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
48
+ return relative !== "" && relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
49
+ }
50
+
46
51
  // Scan worktrees for a given project path
47
- // Returns array of { path, branch, bare, detached, accessible }
48
- // accessible = true if worktree path is inside parentPath
52
+ // Returns array of { path, branch, bare, detached, external }
53
+ // external = true when Git registered the worktree outside the main project folder
49
54
  function scanWorktrees(projectPath) {
50
55
  var resolvedParent = path.resolve(projectPath);
51
56
  try {
@@ -63,7 +68,7 @@ function scanWorktrees(projectPath) {
63
68
  if (wt.bare) continue;
64
69
  var resolvedWt = path.resolve(wt.path);
65
70
  if (resolvedWt === resolvedParent) continue;
66
- wt.accessible = resolvedWt.indexOf(resolvedParent + path.sep) === 0;
71
+ wt.external = !isPathInside(resolvedParent, resolvedWt);
67
72
  wt.dirName = path.basename(wt.path);
68
73
  results.push(wt);
69
74
  }
@@ -131,4 +136,4 @@ function removeWorktree(projectPath, worktreeDirName) {
131
136
  }
132
137
  }
133
138
 
134
- module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree };
139
+ module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };
package/lib/ws-schema.js CHANGED
@@ -145,6 +145,7 @@ var schema = {
145
145
  "kill_process": { direction: "c2s", handler: "lib/project-sessions.js", description: "Kill a system process by PID" },
146
146
  "process_killed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Process was successfully killed" },
147
147
  "process_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Conflict: another process is using the session" },
148
+ "session_writer_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Codex session is already open in another process" },
148
149
 
149
150
  // -----------------------------------------------------------------------
150
151
  // Context / usage
@@ -353,6 +354,8 @@ var schema = {
353
354
  "term_closed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal session was closed" },
354
355
  "term_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of open terminals" },
355
356
  "term_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal error (e.g. access denied)" },
357
+ "shell_command": { direction: "c2s", handler: "lib/project-shell-command.js", description: "Run a one-shot shell command for agent context" },
358
+ "shell_command_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "One-shot shell command output and exit status" },
356
359
 
357
360
  // -----------------------------------------------------------------------
358
361
  // Sticky notes
@@ -96,21 +96,16 @@ function validateOpenCodeConfig(config) {
96
96
  }
97
97
  }
98
98
 
99
+ function trustDefaultWhenModeIsNotExposed(ctx, next) {
100
+ var options = ctx.state && ctx.state.configOptions;
101
+ for (var i = 0; i < (options || []).length; i++) {
102
+ if (options[i].category === "mode" || options[i].id === "mode") return next();
103
+ }
104
+ if (ctx.state && ctx.state.modes) return next();
105
+ return Promise.resolve();
106
+ }
107
+
99
108
  var ACP_AGENT_PROFILES = {
100
- gemini: {
101
- vendor: "gemini",
102
- displayName: "Gemini CLI",
103
- binaryName: "gemini",
104
- overrideName: "GEMINI_CLI_PATH",
105
- args: ["--acp", "--approval-mode=default"],
106
- defaultModels: ["auto"],
107
- defaultModel: "auto",
108
- // Gemini advertises loadSession, but releases through 0.46.0 could load
109
- // the transcript without restoring the model's conversation memory.
110
- // Keep resume gated until Clay's live contract test proves it reliable.
111
- sessionResume: false,
112
- permissionModeGuaranteed: true,
113
- },
114
109
  opencode: {
115
110
  vendor: "opencode",
116
111
  displayName: "OpenCode",
@@ -161,6 +156,61 @@ var ACP_AGENT_PROFILES = {
161
156
  return Promise.resolve();
162
157
  },
163
158
  },
159
+ kimi: {
160
+ vendor: "kimi",
161
+ displayName: "Kimi Code",
162
+ binaryName: "kimi",
163
+ overrideName: "KIMI_CLI_PATH",
164
+ args: ["acp"],
165
+ defaultModels: ["auto"],
166
+ defaultModel: "auto",
167
+ sessionResume: null,
168
+ ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
169
+ },
170
+ grok: {
171
+ vendor: "grok",
172
+ displayName: "Grok Build",
173
+ binaryName: "grok",
174
+ overrideName: "GROK_CLI_PATH",
175
+ args: ["--no-auto-update", "--permission-mode", "ask", "agent", "stdio"],
176
+ defaultModels: ["auto"],
177
+ defaultModel: "auto",
178
+ sessionResume: null,
179
+ permissionModeGuaranteed: true,
180
+ },
181
+ copilot: {
182
+ vendor: "copilot",
183
+ displayName: "GitHub Copilot CLI",
184
+ binaryName: "copilot",
185
+ overrideName: "COPILOT_CLI_PATH",
186
+ args: ["--acp"],
187
+ defaultModels: ["auto"],
188
+ defaultModel: "auto",
189
+ sessionResume: null,
190
+ ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
191
+ },
192
+ qwen: {
193
+ vendor: "qwen",
194
+ displayName: "Qwen Code",
195
+ binaryName: "qwen",
196
+ overrideName: "QWEN_CLI_PATH",
197
+ args: ["--acp", "--approval-mode", "default"],
198
+ defaultModels: ["auto"],
199
+ defaultModel: "auto",
200
+ sessionResume: null,
201
+ permissionModeGuaranteed: true,
202
+ },
203
+ junie: {
204
+ vendor: "junie",
205
+ displayName: "Junie CLI",
206
+ binaryName: "junie",
207
+ overrideName: "JUNIE_CLI_PATH",
208
+ args: ["--acp", "true"],
209
+ defaultModels: ["auto"],
210
+ defaultModel: "auto",
211
+ sessionResume: null,
212
+ ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
213
+ },
164
214
  };
165
215
 
166
216
  function getAcpAgentProfile(vendor) {