@borgee/agents-host 0.2.35 → 0.2.56

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 (74) hide show
  1. package/README.md +23 -27
  2. package/dist/agents-host.d.ts +28 -5
  3. package/dist/agents-host.js +274 -236
  4. package/dist/chat/chat-control-plane.d.ts +3 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +4 -3
  6. package/dist/chat/sdk-chat-control-plane.js +6 -0
  7. package/dist/cli.js +1 -5
  8. package/dist/compatibility-gates.d.ts +4 -0
  9. package/dist/compatibility-gates.js +18 -1
  10. package/dist/context/claude-file-brief.d.ts +2 -0
  11. package/dist/context/claude-file-brief.js +83 -0
  12. package/dist/context/compaction.d.ts +20 -0
  13. package/dist/context/compaction.js +59 -0
  14. package/dist/context/injection.d.ts +44 -9
  15. package/dist/context/injection.js +360 -53
  16. package/dist/context/main-session-delegation.d.ts +1 -1
  17. package/dist/context/projection-strategy.d.ts +24 -0
  18. package/dist/context/projection-strategy.js +90 -0
  19. package/dist/context/prompt.d.ts +16 -1
  20. package/dist/context/prompt.js +475 -53
  21. package/dist/context/resolved-workspace.d.ts +2 -0
  22. package/dist/context/resolved-workspace.js +64 -0
  23. package/dist/context/skill-manual.d.ts +14 -0
  24. package/dist/context/skill-manual.js +21 -0
  25. package/dist/context/turn-preparation.d.ts +8 -2
  26. package/dist/context/turn-preparation.js +68 -17
  27. package/dist/gateway/localhost-gateway.js +18 -8
  28. package/dist/hosted-turn-content.d.ts +15 -0
  29. package/dist/hosted-turn-content.js +50 -0
  30. package/dist/managed-daemon.d.ts +3 -2
  31. package/dist/managed-daemon.js +198 -37
  32. package/dist/plugin-sdk.js +276 -359
  33. package/dist/plugin-sdk.js.map +4 -4
  34. package/dist/progress-to-activity.d.ts +16 -0
  35. package/dist/progress-to-activity.js +24 -0
  36. package/dist/projection-strategy-values.d.ts +4 -0
  37. package/dist/projection-strategy-values.js +28 -0
  38. package/dist/providers/acp-progress-collector.d.ts +44 -0
  39. package/dist/providers/acp-progress-collector.js +130 -0
  40. package/dist/providers/awaiting-user.d.ts +2 -3
  41. package/dist/providers/awaiting-user.js +5 -7
  42. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  43. package/dist/providers/claude/activity-metadata.js +81 -0
  44. package/dist/providers/claude/adapter.d.ts +3 -1
  45. package/dist/providers/claude/adapter.js +10 -0
  46. package/dist/providers/claude/cli-client.d.ts +9 -1
  47. package/dist/providers/claude/cli-client.js +270 -126
  48. package/dist/providers/codex/adapter.d.ts +3 -1
  49. package/dist/providers/codex/adapter.js +10 -0
  50. package/dist/providers/codex/cli-client.d.ts +10 -2
  51. package/dist/providers/codex/cli-client.js +88 -104
  52. package/dist/providers/codex/project-doc.js +22 -37
  53. package/dist/providers/copilot/adapter.d.ts +3 -1
  54. package/dist/providers/copilot/adapter.js +10 -0
  55. package/dist/providers/copilot/cli-client.d.ts +9 -1
  56. package/dist/providers/copilot/cli-client.js +68 -85
  57. package/dist/providers/create-provider.d.ts +3 -1
  58. package/dist/providers/create-provider.js +36 -9
  59. package/dist/providers/provider-adapter.d.ts +35 -0
  60. package/dist/providers/provider-adapter.js +44 -1
  61. package/dist/state-paths.d.ts +10 -2
  62. package/dist/state-paths.js +25 -6
  63. package/dist/task-thread-resolution.d.ts +3 -2
  64. package/dist/types.d.ts +163 -8
  65. package/package.json +2 -2
  66. package/skills/borgee-agent/SKILL.md +127 -38
  67. package/skills/borgee-agent/references/errors.md +38 -0
  68. package/skills/borgee-agent/references/task-properties.md +33 -0
  69. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  70. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  71. package/dist/durable-cursor-store.d.ts +0 -5
  72. package/dist/durable-cursor-store.js +0 -7
  73. package/skills/borgee-agent/borgee-agent.mjs +0 -562
  74. package/skills/borgee-agent/borgee-agent.py +0 -469
@@ -1,10 +1,21 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { homedir } from 'node:os';
2
4
  import { fileURLToPath } from 'node:url';
3
- import { dirname, join, resolve } from 'node:path';
5
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
6
+ import { findTaskForThread } from '../task-thread-resolution.js';
7
+ import { buildClaudeFileBrief } from './claude-file-brief.js';
4
8
  const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
5
9
  const CHANNEL_CONTEXT_PAYLOAD_FILENAME = 'context.json';
6
- const CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME = '.localhost-gateway-auth.json';
7
- const TASK_WORKSPACE_ROOT_DIRNAME = '.borgee-task-workspaces';
10
+ const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
11
+ const CLAUDE_PROJECTED_BRIEF_FILENAME = 'CLAUDE.md';
12
+ const DEFAULT_HOME_BORGEE_DIRNAME = '.borgee';
13
+ const CHANNEL_WORKSPACE_COLLECTION_DIRNAME = 'channels';
14
+ const CHANNEL_WORKSPACE_DIRNAME = 'workspace';
15
+ const TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME = 'tasks';
16
+ const TASK_WORKSPACE_MODE_PROPERTY_KEY = 'workspace.mode';
17
+ const TASK_WORKSPACE_MODE_ISOLATED = 'isolated';
18
+ const DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS = 1_500;
8
19
  export class ChannelContextPreparationError extends Error {
9
20
  partialContext;
10
21
  constructor(message, partialContext, options) {
@@ -29,6 +40,9 @@ const DEFAULT_FILE_SYSTEM = {
29
40
  async unlink(path) {
30
41
  await fs.unlink(path);
31
42
  },
43
+ async rename(oldPath, newPath) {
44
+ await fs.rename(oldPath, newPath);
45
+ },
32
46
  async access(path) {
33
47
  await fs.access(path);
34
48
  },
@@ -45,7 +59,7 @@ function toSkillRuntimePayload(skillRuntime) {
45
59
  pythonCliPath: skillRuntime.pythonCliPath,
46
60
  };
47
61
  }
48
- function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
62
+ function buildBorgeeAgentGatewayCredential(channelId, localhostGateway) {
49
63
  if (!localhostGateway) {
50
64
  return undefined;
51
65
  }
@@ -53,23 +67,55 @@ function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
53
67
  schemaVersion: 1,
54
68
  channelId,
55
69
  localhostGateway: {
70
+ baseUrl: localhostGateway.baseUrl,
56
71
  token: localhostGateway.token,
57
72
  },
58
73
  };
59
74
  }
60
- function buildChannelContextPayload(channelId, collaborationOutcome, attentionSnapshot, taskThreadCollaborationContract, collaborationCapabilities, missedCollaborationDiagnostic, skillRuntime, localhostGateway, options) {
75
+ function buildChannelContextPayload(channelId, runtimeSurface, collaborationOutcome, attentionSnapshot, compactionSnapshot, taskThreadCollaborationContract, collaborationCapabilities, missedCollaborationDiagnostic, skillRuntime, localhostGateway, options) {
61
76
  return {
62
77
  schemaVersion: 1,
63
78
  channelId,
79
+ runtimeSurface,
64
80
  ...(collaborationOutcome ? { collaborationOutcome } : {}),
65
81
  ...(attentionSnapshot ? { attentionSnapshot } : {}),
82
+ ...(compactionSnapshot ? { compactionSnapshot } : {}),
66
83
  ...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
67
84
  ...(collaborationCapabilities ? { collaborationCapabilities } : {}),
68
85
  ...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
69
86
  ...(skillRuntime ? { skillRuntime: toSkillRuntimePayload(skillRuntime) } : {}),
70
87
  ...(localhostGateway ? { localhostGateway } : {}),
71
88
  ...(options?.taskAssignmentContext ? { taskAssignmentContext: options.taskAssignmentContext } : {}),
72
- ...(options?.taskWorkspace ? { taskWorkspace: options.taskWorkspace } : {}),
89
+ ...(options?.resolvedWorkspace ? { resolvedWorkspace: options.resolvedWorkspace } : {}),
90
+ };
91
+ }
92
+ function hashUtf8Content(content) {
93
+ return createHash('sha256').update(content).digest('hex');
94
+ }
95
+ async function unlinkIfPresent(fileSystem, path) {
96
+ await fileSystem.unlink(path).catch((error) => {
97
+ if (error.code !== 'ENOENT') {
98
+ throw error;
99
+ }
100
+ });
101
+ }
102
+ function buildPromptContextForClaudeProjectedBrief(preparedContext) {
103
+ return {
104
+ channelContextPayloadPath: preparedContext.payloadPath,
105
+ claudeProjectedBriefPath: preparedContext.claudeProjectedBriefPath,
106
+ claudeProjectedBriefHash: preparedContext.claudeProjectedBriefHash,
107
+ gatewayCredentialPath: preparedContext.gatewayCredentialPath,
108
+ skillRuntime: preparedContext.skillRuntime,
109
+ localhostGateway: preparedContext.localhostGateway,
110
+ taskAssignmentContext: preparedContext.payload.taskAssignmentContext,
111
+ resolvedWorkspace: preparedContext.resolvedWorkspace,
112
+ runtimeSurface: preparedContext.payload.runtimeSurface,
113
+ collaborationOutcome: preparedContext.payload.collaborationOutcome,
114
+ attentionSnapshot: preparedContext.payload.attentionSnapshot,
115
+ compactionSnapshot: preparedContext.payload.compactionSnapshot,
116
+ collaborationCapabilities: preparedContext.payload.collaborationCapabilities,
117
+ missedCollaborationDiagnostic: preparedContext.payload.missedCollaborationDiagnostic,
118
+ taskThreadCollaborationContract: preparedContext.payload.taskThreadCollaborationContract,
73
119
  };
74
120
  }
75
121
  function isRecord(value) {
@@ -91,6 +137,73 @@ function isUsableTaskId(value) {
91
137
  && !/[\u0000-\u001f\u007f\s]/u.test(value)
92
138
  && canRoundTripThroughUriComponentEncoding(value);
93
139
  }
140
+ function normalizeTaskAssignmentCurrentTaskId(taskAssignmentContext) {
141
+ const currentTaskId = typeof taskAssignmentContext?.currentTaskId === 'string'
142
+ ? taskAssignmentContext.currentTaskId.trim()
143
+ : '';
144
+ return currentTaskId && isUsableTaskId(currentTaskId)
145
+ ? currentTaskId
146
+ : undefined;
147
+ }
148
+ export function buildRuntimeSurfaceForTurn(options) {
149
+ const turnMode = options?.collaboration?.turnMode ?? 'ordinary';
150
+ const gatewayAvailable = options?.localhostGateway !== undefined;
151
+ const currentTaskId = normalizeTaskAssignmentCurrentTaskId(options?.taskAssignmentContext);
152
+ const taskAssignmentThreadActive = options?.taskAssignmentContext?.active === true;
153
+ const collaborationGatewayEnabled = gatewayAvailable && options.localhostGateway?.collaboration?.enabled === true;
154
+ const auxiliarySendAvailable = turnMode === 'ordinary'
155
+ && collaborationGatewayEnabled
156
+ && options?.collaboration?.sendRoutesAllowed === true;
157
+ return {
158
+ schemaVersion: 1,
159
+ turnMode,
160
+ gateway: {
161
+ available: gatewayAvailable,
162
+ readOnlyRoutesAuthorized: gatewayAvailable,
163
+ inspectBeforeAnswer: gatewayAvailable,
164
+ },
165
+ task: {
166
+ currentThread: taskAssignmentThreadActive ? 'task-assignment-thread' : 'parent-channel',
167
+ parentChannelTaskCollection: taskAssignmentThreadActive ? 'parent-channel-only' : 'available',
168
+ currentTaskShorthand: currentTaskId
169
+ ? 'persisted-current-task'
170
+ : taskAssignmentThreadActive
171
+ ? 'fallback-current-task'
172
+ : 'requires-task-id',
173
+ currentTaskIdPersisted: currentTaskId !== undefined,
174
+ currentTaskFallback: !currentTaskId && taskAssignmentThreadActive
175
+ ? 'visible-parent-channel-thread-scan'
176
+ : 'disallowed',
177
+ },
178
+ collaboration: gatewayAvailable
179
+ ? turnMode === 'ordinary'
180
+ ? {
181
+ participantLookup: collaborationGatewayEnabled ? 'available' : 'unavailable',
182
+ draftRead: auxiliarySendAvailable ? 'available' : 'unavailable',
183
+ auxiliarySend: auxiliarySendAvailable ? 'available' : 'unavailable',
184
+ }
185
+ : {
186
+ participantLookup: 'unavailable',
187
+ draftRead: 'unavailable',
188
+ auxiliarySend: 'unavailable',
189
+ }
190
+ : {
191
+ participantLookup: 'unavailable',
192
+ draftRead: 'unavailable',
193
+ auxiliarySend: 'unavailable',
194
+ },
195
+ response: {
196
+ mainVisibleReply: turnMode === 'ordinary'
197
+ ? 'model-visible-reply'
198
+ : turnMode === 'protocol-managed'
199
+ ? 'host-visible-protocol-reply'
200
+ : 'no-visible-reply',
201
+ auxiliaryMessageUsage: auxiliarySendAvailable
202
+ ? 'targeted-escalation-only'
203
+ : 'unavailable',
204
+ },
205
+ };
206
+ }
94
207
  function sanitizeTaskAssignmentContext(value) {
95
208
  if (!isRecord(value) || value.active !== true) {
96
209
  return undefined;
@@ -128,6 +241,128 @@ async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
128
241
  return undefined;
129
242
  }
130
243
  }
244
+ function sanitizeResolvedWorkspaceContext(value) {
245
+ if (!isRecord(value) || typeof value.rootPath !== 'string' || !isAbsolute(value.rootPath)) {
246
+ return undefined;
247
+ }
248
+ if (value.authority === 'channel'
249
+ && typeof value.owningChannelId === 'string'
250
+ && value.owningChannelId.length > 0) {
251
+ return {
252
+ authority: 'channel',
253
+ owningChannelId: value.owningChannelId,
254
+ rootPath: value.rootPath,
255
+ };
256
+ }
257
+ if (value.authority === 'task'
258
+ && typeof value.taskId === 'string'
259
+ && isUsableTaskId(value.taskId.trim())) {
260
+ return {
261
+ authority: 'task',
262
+ taskId: value.taskId.trim(),
263
+ rootPath: value.rootPath,
264
+ };
265
+ }
266
+ return undefined;
267
+ }
268
+ async function readExistingResolvedWorkspaceContext(payloadPath, fileSystem) {
269
+ try {
270
+ const raw = await fileSystem.readFile(payloadPath, { encoding: 'utf8' });
271
+ const payload = JSON.parse(raw);
272
+ return sanitizeResolvedWorkspaceContext(payload.resolvedWorkspace);
273
+ }
274
+ catch {
275
+ return undefined;
276
+ }
277
+ }
278
+ function buildChannelResolvedWorkspace(workspaceCollectionRootDir, channelId) {
279
+ return {
280
+ authority: 'channel',
281
+ owningChannelId: channelId,
282
+ rootPath: resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId),
283
+ };
284
+ }
285
+ function buildStickyChannelResolvedWorkspace(workspaceCollectionRootDir, resolvedWorkspace) {
286
+ if (resolvedWorkspace?.authority !== 'channel') {
287
+ return undefined;
288
+ }
289
+ const expectedRootPath = resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, resolvedWorkspace.owningChannelId);
290
+ if (resolvedWorkspace.rootPath !== expectedRootPath) {
291
+ return undefined;
292
+ }
293
+ return {
294
+ authority: 'channel',
295
+ owningChannelId: resolvedWorkspace.owningChannelId,
296
+ rootPath: expectedRootPath,
297
+ };
298
+ }
299
+ async function resolveTaskThreadWorkspaceContext(params) {
300
+ const channelWorkspace = buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
301
+ const taskAssignmentContext = params.taskAssignmentContext;
302
+ const currentTaskId = taskAssignmentContext?.currentTaskId?.trim();
303
+ if (taskAssignmentContext?.active !== true
304
+ || !currentTaskId
305
+ || !params.taskReader) {
306
+ return { taskAssignmentContext, resolvedWorkspace: channelWorkspace };
307
+ }
308
+ const stickyTaskWorkspace = params.existingResolvedWorkspace?.authority === 'task'
309
+ && params.existingResolvedWorkspace.taskId === currentTaskId
310
+ ? {
311
+ authority: 'task',
312
+ taskId: currentTaskId,
313
+ rootPath: resolveTaskWorkspaceDirectory(params.workspaceCollectionRootDir, params.channelId, currentTaskId),
314
+ }
315
+ : undefined;
316
+ const stickyChannelWorkspace = buildStickyChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.existingResolvedWorkspace);
317
+ try {
318
+ const task = await withDeadline(findTaskForThread(params.taskReader, params.channelId, currentTaskId), params.taskResolutionTimeoutMs, `workspace resolution timed out for task ${currentTaskId}`);
319
+ if (!task) {
320
+ return {
321
+ taskAssignmentContext,
322
+ resolvedWorkspace: channelWorkspace,
323
+ };
324
+ }
325
+ async function withDeadline(promise, timeoutMs, timeoutMessage) {
326
+ if (timeoutMs <= 0) {
327
+ return await promise;
328
+ }
329
+ let timer;
330
+ try {
331
+ return await Promise.race([
332
+ promise,
333
+ new Promise((_, reject) => {
334
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
335
+ }),
336
+ ]);
337
+ }
338
+ finally {
339
+ if (timer) {
340
+ clearTimeout(timer);
341
+ }
342
+ }
343
+ }
344
+ if (task.properties[TASK_WORKSPACE_MODE_PROPERTY_KEY] !== TASK_WORKSPACE_MODE_ISOLATED) {
345
+ return {
346
+ taskAssignmentContext,
347
+ resolvedWorkspace: buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, task.channelId),
348
+ };
349
+ }
350
+ return {
351
+ taskAssignmentContext,
352
+ resolvedWorkspace: {
353
+ authority: 'task',
354
+ taskId: task.id,
355
+ rootPath: resolveTaskWorkspaceDirectory(params.workspaceCollectionRootDir, params.channelId, task.id),
356
+ },
357
+ };
358
+ }
359
+ catch (error) {
360
+ return {
361
+ taskAssignmentContext,
362
+ resolvedWorkspace: stickyTaskWorkspace ?? stickyChannelWorkspace ?? channelWorkspace,
363
+ };
364
+ }
365
+ }
131
366
  export function encodeChannelPathSegment(channelId) {
132
367
  const encoded = Buffer.from(channelId, 'utf8').toString('hex');
133
368
  return encoded.length > 0 ? encoded : 'empty';
@@ -139,22 +374,54 @@ export function resolveChannelContextPayloadPath(stateRootDir, channelId) {
139
374
  return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_CONTEXT_PAYLOAD_FILENAME);
140
375
  }
141
376
  export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
142
- return join(resolve(startupWorkspaceRootDir), TASK_WORKSPACE_ROOT_DIRNAME);
377
+ return join(resolve(startupWorkspaceRootDir), TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME);
378
+ }
379
+ export function resolveManagedWorkspaceCollectionRoot(workspaceCollectionRootDir, env = process.env, resolvedHomeDir = homedir()) {
380
+ const explicitRoot = workspaceCollectionRootDir?.trim();
381
+ if (explicitRoot) {
382
+ return resolve(explicitRoot);
383
+ }
384
+ const home = env.HOME?.trim() || resolvedHomeDir.trim();
385
+ if (!home) {
386
+ throw new Error('Unable to resolve a user home directory for agents-host workspaces');
387
+ }
388
+ return join(resolve(home), DEFAULT_HOME_BORGEE_DIRNAME, CHANNEL_WORKSPACE_COLLECTION_DIRNAME);
389
+ }
390
+ export function resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId) {
391
+ return join(resolve(workspaceCollectionRootDir), encodeChannelPathSegment(channelId));
143
392
  }
144
- export function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir, channelId, taskId) {
145
- return join(resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir), encodeChannelPathSegment(channelId), encodeChannelPathSegment(taskId));
393
+ export function resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId) {
394
+ return join(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId), CHANNEL_WORKSPACE_DIRNAME);
146
395
  }
147
- export function resolveChannelContextGatewayAuthPath(stateRootDir, channelId, turnExecutionId) {
148
- const filename = turnExecutionId
149
- ? `.localhost-gateway-auth.${Buffer.from(turnExecutionId, 'utf8').toString('hex')}.json`
150
- : CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME;
151
- return join(resolveChannelContextDirectory(stateRootDir, channelId), filename);
396
+ export function resolveTaskIsolatedWorkspaceDirectory(workspaceCollectionRootDir, channelId, taskId) {
397
+ return join(resolveTaskWorkspaceRootDirectory(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId)), encodeChannelPathSegment(taskId), CHANNEL_WORKSPACE_DIRNAME);
152
398
  }
153
- export function resolveGatewayAuthPathFromPayloadPath(payloadPath) {
154
- return join(dirname(payloadPath), CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME);
399
+ export const resolveTaskWorkspaceDirectory = resolveTaskIsolatedWorkspaceDirectory;
400
+ export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
401
+ return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
155
402
  }
156
- const LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX = '.localhost-gateway-auth.';
157
- async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
403
+ export function resolveGatewayCredentialPathFromPayloadPath(payloadPath) {
404
+ return join(dirname(payloadPath), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
405
+ }
406
+ export function resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath) {
407
+ return join(dirname(payloadPath), CLAUDE_PROJECTED_BRIEF_FILENAME);
408
+ }
409
+ /**
410
+ * Every basename a gateway credential can carry in a channel context directory, including the ones
411
+ * this host no longer writes. The directory holds at most one live credential — the token is
412
+ * reissued every turn — and this sweep is the only thing that ever unlinks the others, so a name
413
+ * dropped from this list leaves a 0600 file behind that still looks live.
414
+ */
415
+ const GATEWAY_CREDENTIAL_SIDECAR_BASENAME_PREFIXES = [
416
+ '.borgee-agent-gateway.',
417
+ '.borgee-agent-bootstrap.',
418
+ '.localhost-gateway-auth.',
419
+ ];
420
+ export function isGatewayCredentialSidecarBasename(basename) {
421
+ return basename.endsWith('.json')
422
+ && GATEWAY_CREDENTIAL_SIDECAR_BASENAME_PREFIXES.some((prefix) => basename.startsWith(prefix));
423
+ }
424
+ async function pruneGatewayCredentialSidecars(fileSystem, directoryPath, keepBasename) {
158
425
  let entries;
159
426
  try {
160
427
  entries = await fileSystem.readdir(directoryPath);
@@ -167,8 +434,7 @@ async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
167
434
  throw error;
168
435
  }
169
436
  await Promise.all(entries
170
- .filter((entry) => entry.startsWith(LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX)
171
- && entry.endsWith('.json'))
437
+ .filter((entry) => isGatewayCredentialSidecarBasename(entry) && entry !== keepBasename)
172
438
  .map(async (entry) => {
173
439
  await fileSystem.unlink(join(directoryPath, entry)).catch((error) => {
174
440
  if (error.code !== 'ENOENT') {
@@ -196,8 +462,8 @@ export async function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl, fileSystem
196
462
  const packageRootPath = await resolvePackageRootFromModuleUrl(moduleUrl, fileSystem);
197
463
  const skillDirectoryPath = join(packageRootPath, 'skills', BORGEE_AGENT_SKILL_DIRNAME);
198
464
  const skillMarkdownPath = join(skillDirectoryPath, 'SKILL.md');
199
- const nodeCliPath = join(skillDirectoryPath, 'borgee-agent.mjs');
200
- const pythonCliPath = join(skillDirectoryPath, 'borgee-agent.py');
465
+ const nodeCliPath = join(skillDirectoryPath, 'scripts', 'borgee-agent.mjs');
466
+ const pythonCliPath = join(skillDirectoryPath, 'scripts', 'borgee-agent.py');
201
467
  await Promise.all([
202
468
  fileSystem.access(skillDirectoryPath),
203
469
  fileSystem.access(skillMarkdownPath),
@@ -230,14 +496,18 @@ export class FileChannelContextStore {
230
496
  skillRuntimeEnabled;
231
497
  skillAssetResolver;
232
498
  localhostGateway;
233
- startupWorkspaceRootDir;
499
+ workspaceCollectionRootDir;
500
+ taskReader;
501
+ taskResolutionTimeoutMs;
234
502
  constructor(stateRootDir, options = {}) {
235
503
  this.stateRootDir = stateRootDir;
236
504
  this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
237
505
  this.skillRuntimeEnabled = options.skillRuntimeEnabled ?? false;
238
506
  this.skillAssetResolver = options.skillAssetResolver ?? new PackageSkillAssetResolver(import.meta.url, this.fileSystem);
239
507
  this.localhostGateway = options.localhostGateway;
240
- this.startupWorkspaceRootDir = resolve(options.taskWorkspaceRootDir ?? process.cwd());
508
+ this.workspaceCollectionRootDir = resolveManagedWorkspaceCollectionRoot(options.workspaceCollectionRootDir, options.env, options.resolvedHomeDir);
509
+ this.taskReader = options.taskReader;
510
+ this.taskResolutionTimeoutMs = options.taskResolutionTimeoutMs ?? DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS;
241
511
  }
242
512
  async prepare(inputOrChannelId, options) {
243
513
  const input = typeof inputOrChannelId === 'string'
@@ -249,8 +519,10 @@ export class FileChannelContextStore {
249
519
  const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
250
520
  const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
251
521
  const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
252
- const gatewayAuthPath = resolveGatewayAuthPathFromPayloadPath(payloadPath);
522
+ const claudeProjectedBriefPath = resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath);
523
+ const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
253
524
  const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
525
+ const existingResolvedWorkspace = await readExistingResolvedWorkspaceContext(payloadPath, this.fileSystem);
254
526
  const skillRuntime = this.skillRuntimeEnabled
255
527
  ? await this.resolveSkillRuntimeBestEffort()
256
528
  : undefined;
@@ -280,66 +552,101 @@ export class FileChannelContextStore {
280
552
  incomingMessageType: input.incomingMessageType,
281
553
  incomingContent: input.incomingContent,
282
554
  }, existingTaskAssignmentContext);
283
- const taskWorkspace = taskAssignmentContext?.currentTaskId
284
- ? {
285
- currentTaskId: taskAssignmentContext.currentTaskId,
286
- rootPath: resolveTaskWorkspaceDirectory(this.startupWorkspaceRootDir, input.channelId, taskAssignmentContext.currentTaskId),
287
- }
288
- : undefined;
289
- const payload = buildChannelContextPayload(input.channelId, input.collaborationOutcome, input.attentionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, taskAssignmentContext || taskWorkspace
555
+ const workspaceResolution = await resolveTaskThreadWorkspaceContext({
556
+ channelId: input.channelId,
557
+ workspaceCollectionRootDir: this.workspaceCollectionRootDir,
558
+ taskAssignmentContext,
559
+ existingResolvedWorkspace,
560
+ taskReader: this.taskReader,
561
+ taskResolutionTimeoutMs: this.taskResolutionTimeoutMs,
562
+ });
563
+ const resolvedWorkspace = workspaceResolution.resolvedWorkspace;
564
+ const runtimeSurface = buildRuntimeSurfaceForTurn({
565
+ localhostGateway,
566
+ collaboration: input.collaboration,
567
+ taskAssignmentContext: workspaceResolution.taskAssignmentContext ?? taskAssignmentContext,
568
+ });
569
+ const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, workspaceResolution.taskAssignmentContext || resolvedWorkspace
290
570
  ? {
291
- ...(taskAssignmentContext ? { taskAssignmentContext } : {}),
292
- ...(taskWorkspace ? { taskWorkspace } : {}),
571
+ ...(workspaceResolution.taskAssignmentContext
572
+ ? { taskAssignmentContext: workspaceResolution.taskAssignmentContext }
573
+ : {}),
574
+ resolvedWorkspace,
293
575
  }
294
576
  : undefined);
295
- const gatewayAuthPayload = buildLocalhostGatewayAuthPayload(input.channelId, issuedLocalhostGateway);
296
- let taskWorkspaceMaterialized = false;
577
+ const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
578
+ let resolvedWorkspaceMaterialized = false;
579
+ let claudeProjectedBriefWritten = false;
297
580
  let payloadWritten = false;
298
- let gatewayAuthWritten = false;
581
+ let gatewayCredentialWritten = false;
299
582
  const preparedContext = {
300
583
  directoryPath,
301
584
  payload,
302
585
  payloadPath,
303
- gatewayAuthPath: gatewayAuthPayload ? gatewayAuthPath : undefined,
586
+ gatewayCredentialPath: gatewayCredential ? gatewayCredentialPath : undefined,
304
587
  skillRuntime,
305
588
  localhostGateway,
306
- taskWorkspace,
589
+ resolvedWorkspace,
307
590
  };
591
+ const shouldProjectClaudeBrief = input.provider === 'claude' && input.projectionStrategy === 'file-brief';
592
+ const claudeProjectedBriefContent = shouldProjectClaudeBrief
593
+ ? buildClaudeFileBrief(buildPromptContextForClaudeProjectedBrief({
594
+ ...preparedContext,
595
+ claudeProjectedBriefPath,
596
+ }))
597
+ : undefined;
598
+ if (claudeProjectedBriefContent) {
599
+ preparedContext.claudeProjectedBriefPath = claudeProjectedBriefPath;
600
+ preparedContext.claudeProjectedBriefHash = hashUtf8Content(claudeProjectedBriefContent);
601
+ }
308
602
  try {
309
603
  await this.fileSystem.mkdir(directoryPath, { recursive: true, mode: 0o700 });
310
- if (taskWorkspace) {
311
- await this.fileSystem.mkdir(taskWorkspace.rootPath, { recursive: true, mode: 0o700 });
312
- taskWorkspaceMaterialized = true;
604
+ await this.fileSystem.mkdir(resolvedWorkspace.rootPath, { recursive: true, mode: 0o700 });
605
+ resolvedWorkspaceMaterialized = true;
606
+ await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
607
+ if (!claudeProjectedBriefContent) {
608
+ await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
313
609
  }
314
- await pruneLegacyGatewayAuthSidecars(this.fileSystem, directoryPath);
315
610
  await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
316
611
  payloadWritten = true;
317
- if (gatewayAuthPayload) {
318
- await this.fileSystem.writeFile(gatewayAuthPath, `${JSON.stringify(gatewayAuthPayload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
319
- gatewayAuthWritten = true;
612
+ if (gatewayCredential) {
613
+ // Replaced atomically: a CLI invocation left over from an earlier turn must read either
614
+ // the old credential or the new one, never a missing or half-written file, because the
615
+ // documented correction for a rotated token is a 401 and not a usage error. The staging
616
+ // name is itself a credential sidecar, so a crashed run's leftover is swept above.
617
+ const stagingCredentialPath = join(directoryPath, `.borgee-agent-gateway.${randomUUID()}.json`);
618
+ await this.fileSystem.writeFile(stagingCredentialPath, `${JSON.stringify(gatewayCredential, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
619
+ await this.fileSystem.rename(stagingCredentialPath, gatewayCredentialPath);
620
+ gatewayCredentialWritten = true;
320
621
  }
321
622
  else {
322
- await this.fileSystem.unlink(gatewayAuthPath).catch((error) => {
323
- if (error.code !== 'ENOENT') {
324
- throw error;
325
- }
326
- });
327
623
  this.localhostGateway?.clearChannel(input.channelId);
328
624
  }
625
+ if (claudeProjectedBriefContent) {
626
+ const stagingProjectedBriefPath = join(directoryPath, `.claude-file-brief.${randomUUID()}.md`);
627
+ await this.fileSystem.writeFile(stagingProjectedBriefPath, claudeProjectedBriefContent, { encoding: 'utf8', mode: 0o600 });
628
+ await this.fileSystem.rename(stagingProjectedBriefPath, claudeProjectedBriefPath);
629
+ claudeProjectedBriefWritten = true;
630
+ }
329
631
  if (issuedLocalhostGateway) {
330
632
  this.localhostGateway?.publishPayload(input.channelId, payloadPath, payload);
331
633
  }
332
634
  return preparedContext;
333
635
  }
334
636
  catch (error) {
637
+ if (claudeProjectedBriefContent && !claudeProjectedBriefWritten) {
638
+ await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
639
+ }
335
640
  const partialContext = {
336
641
  directoryPath,
337
642
  payload,
643
+ claudeProjectedBriefPath: claudeProjectedBriefWritten ? claudeProjectedBriefPath : undefined,
644
+ claudeProjectedBriefHash: claudeProjectedBriefWritten ? preparedContext.claudeProjectedBriefHash : undefined,
338
645
  payloadPath: payloadWritten ? payloadPath : undefined,
339
- gatewayAuthPath: gatewayAuthWritten ? gatewayAuthPath : undefined,
646
+ gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
340
647
  skillRuntime: payloadWritten ? skillRuntime : undefined,
341
648
  localhostGateway: payloadWritten ? localhostGateway : undefined,
342
- taskWorkspace: taskWorkspaceMaterialized ? taskWorkspace : undefined,
649
+ resolvedWorkspace: resolvedWorkspaceMaterialized ? resolvedWorkspace : undefined,
343
650
  };
344
651
  throw new ChannelContextPreparationError('failed to persist channel context payload', partialContext, { cause: error });
345
652
  }
@@ -1 +1 @@
1
- export declare const MAIN_SESSION_DELEGATION_LINES: readonly string[];
1
+ export declare const MAIN_SESSION_DELEGATION_LINES: readonly ["This session is your coordination surface: read the channel, answer the human, break work down, and keep task state current here.", "Delegate the actual work to subagents — research, code edits, long builds and test runs, and broad repository sweeps belong in a delegated worker instead of running inline here.", "Delegation changes who executes, not who owns: the work still belongs to this thread, and you still report the result here yourself.", "Stay responsive while delegated work runs, so a new incoming message never waits behind a long local operation."];
@@ -0,0 +1,24 @@
1
+ import type { DebugLogger } from '../debug.js';
2
+ import type { ChatControlPlane } from '../chat/chat-control-plane.js';
3
+ import type { ProjectionStrategy } from '../types.js';
4
+ interface ProjectionStrategyResolverOptions {
5
+ baseUrl?: string;
6
+ agentApiKey?: string;
7
+ resolveStableAgentId?: () => string | undefined;
8
+ fetch?: typeof fetch;
9
+ fallback?: () => ProjectionStrategy;
10
+ logger?: Pick<DebugLogger, 'error'>;
11
+ taskReader?: Pick<ChatControlPlane, 'getTask' | 'listChannels' | 'listTasks'>;
12
+ }
13
+ export declare class ServerProjectionStrategyResolver {
14
+ private readonly options;
15
+ private readonly strategyCache;
16
+ private readonly channelAliases;
17
+ private readonly fetchImpl;
18
+ private readonly fallback;
19
+ constructor(options: ProjectionStrategyResolverOptions);
20
+ resolve(channelId: string): Promise<ProjectionStrategy>;
21
+ private resolveCachedStrategy;
22
+ private resolveEffectiveCacheKey;
23
+ }
24
+ export {};