@borgee/agents-host 0.2.44 → 0.2.62

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 (65) hide show
  1. package/README.md +24 -27
  2. package/dist/agents-host.d.ts +27 -5
  3. package/dist/agents-host.js +266 -201
  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 +3 -0
  7. package/dist/cli.js +1 -5
  8. package/dist/compatibility-gates.d.ts +4 -0
  9. package/dist/compatibility-gates.js +20 -1
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +21 -0
  12. package/dist/context/claude-file-brief.d.ts +2 -0
  13. package/dist/context/claude-file-brief.js +83 -0
  14. package/dist/context/compaction.d.ts +20 -0
  15. package/dist/context/compaction.js +59 -0
  16. package/dist/context/injection.d.ts +58 -7
  17. package/dist/context/injection.js +370 -36
  18. package/dist/context/main-session-delegation.d.ts +1 -1
  19. package/dist/context/projection-strategy.d.ts +24 -0
  20. package/dist/context/projection-strategy.js +90 -0
  21. package/dist/context/prompt.d.ts +16 -1
  22. package/dist/context/prompt.js +464 -26
  23. package/dist/context/resolved-workspace.d.ts +3 -0
  24. package/dist/context/resolved-workspace.js +106 -0
  25. package/dist/context/skill-manual.d.ts +1 -0
  26. package/dist/context/skill-manual.js +4 -1
  27. package/dist/context/turn-preparation.d.ts +8 -2
  28. package/dist/context/turn-preparation.js +64 -14
  29. package/dist/gateway/localhost-gateway.js +4 -5
  30. package/dist/local-config.js +11 -1
  31. package/dist/managed-daemon.js +127 -9
  32. package/dist/plugin-sdk.js +264 -364
  33. package/dist/plugin-sdk.js.map +4 -4
  34. package/dist/policy/copilot-permission.d.ts +1 -0
  35. package/dist/policy/copilot-permission.js +18 -0
  36. package/dist/policy/gateway-authorization.js +2 -2
  37. package/dist/progress-to-activity.d.ts +16 -0
  38. package/dist/progress-to-activity.js +24 -0
  39. package/dist/projection-strategy-values.d.ts +4 -0
  40. package/dist/projection-strategy-values.js +28 -0
  41. package/dist/providers/acp-progress-collector.d.ts +44 -0
  42. package/dist/providers/acp-progress-collector.js +130 -0
  43. package/dist/providers/awaiting-user.d.ts +2 -3
  44. package/dist/providers/awaiting-user.js +5 -7
  45. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  46. package/dist/providers/claude/activity-metadata.js +81 -0
  47. package/dist/providers/claude/cli-client.d.ts +2 -3
  48. package/dist/providers/claude/cli-client.js +220 -120
  49. package/dist/providers/codex/cli-client.d.ts +2 -0
  50. package/dist/providers/codex/cli-client.js +28 -104
  51. package/dist/providers/codex/project-doc.js +12 -11
  52. package/dist/providers/copilot/cli-client.d.ts +1 -1
  53. package/dist/providers/copilot/cli-client.js +12 -86
  54. package/dist/providers/create-provider.d.ts +2 -0
  55. package/dist/providers/create-provider.js +22 -4
  56. package/dist/state-paths.d.ts +1 -1
  57. package/dist/state-paths.js +3 -3
  58. package/dist/task-thread-resolution.d.ts +3 -2
  59. package/dist/types.d.ts +143 -6
  60. package/package.json +2 -2
  61. package/skills/borgee-agent/SKILL.md +12 -4
  62. package/skills/borgee-agent/references/errors.md +2 -2
  63. package/skills/borgee-agent/references/task-properties.md +6 -2
  64. package/dist/durable-cursor-store.d.ts +0 -5
  65. package/dist/durable-cursor-store.js +0 -7
@@ -0,0 +1,106 @@
1
+ function buildWorkspaceRootLine(context) {
2
+ const resolvedWorkspace = context.resolvedWorkspace;
3
+ if (!resolvedWorkspace) {
4
+ return undefined;
5
+ }
6
+ if (resolvedWorkspace.authority === 'task-execution-target') {
7
+ return `Explicit execution local directory for this task thread: ${resolvedWorkspace.rootPath}.`;
8
+ }
9
+ if (resolvedWorkspace.authority === 'task-thread-scratch') {
10
+ return `Discussion-only scratch workspace for this task thread: ${resolvedWorkspace.rootPath}.`;
11
+ }
12
+ if (context.taskAssignmentContext?.active === true) {
13
+ return `Writable workspace root inherited from this task thread's channel: ${resolvedWorkspace.rootPath}.`;
14
+ }
15
+ return `Writable workspace root for this channel: ${resolvedWorkspace.rootPath}.`;
16
+ }
17
+ function buildWorkspaceModeLine(context) {
18
+ if (context.taskAssignmentContext?.active !== true || !context.resolvedWorkspace) {
19
+ return undefined;
20
+ }
21
+ if (context.resolvedWorkspace.authority === 'task-execution-target') {
22
+ return 'This task thread has a valid explicit execution.local_directory target, so execution is bound to that exact existing local directory.';
23
+ }
24
+ if (context.resolvedWorkspace.authority === 'task-thread-scratch') {
25
+ return context.resolvedWorkspace.reason === 'cross-agent-independent-workspace-disabled'
26
+ ? 'This task thread has an explicit execution.local_directory target, but this agents-host currently disables cross-agent independent-workspace handoff, so it is discussion-only.'
27
+ : 'This task thread has no valid explicit execution.local_directory target, so it is discussion-only.';
28
+ }
29
+ return 'This task thread inherits the channel workspace rather than using a task-isolated workspace.';
30
+ }
31
+ function buildWorkspaceLocalityLine(context) {
32
+ if (!context.resolvedWorkspace) {
33
+ return undefined;
34
+ }
35
+ if (context.resolvedWorkspace.authority === 'task-execution-target') {
36
+ return 'This is a human-selected existing local directory. The host validates it before use and never creates it.';
37
+ }
38
+ if (context.resolvedWorkspace.authority === 'task-thread-scratch') {
39
+ return 'This scratch workspace is host-managed only for discussion turns. It is not a bound project workspace and does not imply that any target repository is checked out there.';
40
+ }
41
+ if (context.taskAssignmentContext?.active === true) {
42
+ return 'This writable workspace is local to agents-host for this task thread because the task inherits its channel workspace. It does not imply that the target repository has already been checked out there.';
43
+ }
44
+ return 'This writable workspace is local to agents-host for this channel. It does not imply that the target repository has already been checked out there.';
45
+ }
46
+ function buildCopilotCwdLine(context) {
47
+ if (!context.resolvedWorkspace) {
48
+ return undefined;
49
+ }
50
+ if (context.resolvedWorkspace.authority === 'task-execution-target') {
51
+ return 'GitHub Copilot runs this turn with that exact local directory as its real cwd.';
52
+ }
53
+ if (context.resolvedWorkspace.authority === 'task-thread-scratch') {
54
+ return 'GitHub Copilot runs this turn with that host-managed scratch workspace as its real cwd.';
55
+ }
56
+ if (context.taskAssignmentContext?.active === true) {
57
+ return 'GitHub Copilot runs this turn with that inherited channel workspace as its real cwd.';
58
+ }
59
+ return 'GitHub Copilot runs this turn with that workspace as its real cwd.';
60
+ }
61
+ function buildDiscussionOnlyPermissionLine(context) {
62
+ if (context.resolvedWorkspace?.authority !== 'task-thread-scratch') {
63
+ return undefined;
64
+ }
65
+ return context.resolvedWorkspace.reason === 'cross-agent-independent-workspace-disabled'
66
+ ? 'Filesystem and shell tool permissions stay denied in this discussion-only task thread while this agents-host keeps cross-agent independent-workspace handoff disabled.'
67
+ : 'Filesystem and shell tool permissions stay denied in this discussion-only task thread until a human sets a valid execution.local_directory target.';
68
+ }
69
+ function buildProjectEntryPrecheckLine(context) {
70
+ if (context.taskAssignmentContext?.active !== true) {
71
+ return undefined;
72
+ }
73
+ if (context.resolvedWorkspace?.authority === 'task-execution-target') {
74
+ return 'Precheck before switching this task thread into your own local workspace or project directory: the task\'s parent channel must have exactly one human user. Cross-agent independent-workspace handoff is also governed by this agents-host\'s local policy switch.';
75
+ }
76
+ return 'Before a human switches this task thread into their own local workspace or project directory, verify that the task\'s parent channel has exactly one human user; otherwise keep the thread discussion-only. Cross-agent independent-workspace handoff is also governed by this agents-host\'s local policy switch.';
77
+ }
78
+ export function buildResolvedWorkspaceGuidanceLines(context, provider) {
79
+ if (!context?.resolvedWorkspace) {
80
+ return [];
81
+ }
82
+ const lines = [buildWorkspaceRootLine(context), buildWorkspaceModeLine(context)]
83
+ .filter((line) => line != null);
84
+ if (provider === 'copilot') {
85
+ const copilotCwdLine = buildCopilotCwdLine(context);
86
+ if (copilotCwdLine) {
87
+ lines.push(copilotCwdLine);
88
+ }
89
+ }
90
+ const localityLine = buildWorkspaceLocalityLine(context);
91
+ if (localityLine) {
92
+ lines.push(localityLine);
93
+ }
94
+ const permissionLine = buildDiscussionOnlyPermissionLine(context);
95
+ if (permissionLine) {
96
+ lines.push(permissionLine);
97
+ }
98
+ const precheckLine = buildProjectEntryPrecheckLine(context);
99
+ if (precheckLine) {
100
+ lines.push(precheckLine);
101
+ }
102
+ return lines;
103
+ }
104
+ export function isDiscussionOnlyResolvedWorkspace(context) {
105
+ return context?.resolvedWorkspace?.authority === 'task-thread-scratch';
106
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SkillRuntimeBootstrapMetadata } from '../types.js';
2
+ export declare function buildSkillManualReadLine(skillRuntime: SkillRuntimeBootstrapMetadata): string;
2
3
  /**
3
4
  * What every model-facing surface says about the packaged CLI: what it is, where its manual is, and
4
5
  * that the manual has to be opened before acting. Nothing else about the CLI is stated on a host
@@ -1,3 +1,6 @@
1
+ export function buildSkillManualReadLine(skillRuntime) {
2
+ return `Read its manual at ${skillRuntime.skillMarkdownPath} before you act on this channel; it defines both the session-level coordination and delegation contract and the CLI invocation.`;
3
+ }
1
4
  /**
2
5
  * What every model-facing surface says about the packaged CLI: what it is, where its manual is, and
3
6
  * that the manual has to be opened before acting. Nothing else about the CLI is stated on a host
@@ -12,7 +15,7 @@
12
15
  export function buildSkillManualLines(skillRuntime) {
13
16
  return [
14
17
  "A packaged local CLI reads this Borgee channel and acts on its tasks: channel history, visible participants, this channel's tasks and their properties, and short auxiliary mentions.",
15
- `Read its manual at ${skillRuntime.skillMarkdownPath} before you act on this channel; you cannot form a working invocation without it.`,
18
+ buildSkillManualReadLine(skillRuntime),
16
19
  'Every command the manual documents is already authorized on a turn whose prompt names a gateway credential file. Run them directly and do not ask the user for permission first.',
17
20
  ];
18
21
  }
@@ -1,9 +1,15 @@
1
1
  import { type DebugLogger } from '../debug.js';
2
- import type { PreparedProviderTurnInput, ProviderInput } from '../types.js';
2
+ import type { ProjectionStrategy, PreparedProviderTurnInput, ProviderInput } from '../types.js';
3
3
  import { type ChannelContextStore } from './injection.js';
4
+ type ResolveProjectionStrategy = (input: ProviderInput) => Promise<ProjectionStrategy>;
4
5
  export declare class ProviderTurnPreparer {
5
6
  private readonly channelContextStore?;
6
7
  private readonly logger;
7
- constructor(channelContextStore?: ChannelContextStore | undefined, logger?: DebugLogger);
8
+ private readonly resolveProjectionStrategy;
9
+ constructor(channelContextStore?: ChannelContextStore | undefined, logger?: DebugLogger, options?: {
10
+ projectionStrategy?: ProjectionStrategy;
11
+ resolveProjectionStrategy?: ResolveProjectionStrategy;
12
+ });
8
13
  prepare(input: ProviderInput): Promise<PreparedProviderTurnInput>;
9
14
  }
15
+ export {};
@@ -1,7 +1,8 @@
1
1
  import { HostLogger, summarizeError } from '../debug.js';
2
+ import { DEFAULT_PROJECTION_STRATEGY } from '../projection-strategy-values.js';
2
3
  import { buildHostedIncomingContentText, buildHostedTurnContentParts } from '../hosted-turn-content.js';
3
- import { ChannelContextPreparationError, } from './injection.js';
4
- import { buildPrompt } from './prompt.js';
4
+ import { buildRuntimeSurfaceForTurn, ChannelContextPreparationError, } from './injection.js';
5
+ import { buildClaudeSessionPromptAppend, buildClaudeTurnPrompt, buildPrompt } from './prompt.js';
5
6
  function providerLabel(provider) {
6
7
  if (provider === 'copilot') {
7
8
  return 'Copilot';
@@ -16,26 +17,36 @@ function toPromptContext(channelContext, input) {
16
17
  && !input.collaboration
17
18
  && !input.collaborationOutcome
18
19
  && !input.attentionSnapshot
20
+ && !input.compactionSnapshot
19
21
  && !input.collaborationCapabilities
20
22
  && !input.missedCollaborationDiagnostic
21
23
  && !input.taskThreadCollaborationContract) {
22
24
  return undefined;
23
25
  }
26
+ const runtimeSurface = channelContext?.payload.runtimeSurface ?? buildRuntimeSurfaceForTurn({
27
+ localhostGateway: channelContext?.payload.localhostGateway ?? channelContext?.localhostGateway,
28
+ collaboration: input.collaboration,
29
+ taskAssignmentContext: channelContext?.payload.taskAssignmentContext,
30
+ });
24
31
  return {
25
32
  ...(channelContext
26
33
  ? {
27
34
  channelContextPayloadPath: channelContext.payloadPath,
35
+ claudeProjectedBriefPath: channelContext.claudeProjectedBriefPath,
36
+ claudeProjectedBriefHash: channelContext.claudeProjectedBriefHash,
28
37
  gatewayCredentialPath: channelContext.gatewayCredentialPath,
29
38
  skillRuntime: channelContext.skillRuntime,
30
39
  localhostGateway: channelContext.localhostGateway,
31
40
  taskAssignmentContext: channelContext.payload.taskAssignmentContext,
32
- taskWorkspace: channelContext.taskWorkspace,
41
+ resolvedWorkspace: channelContext.resolvedWorkspace,
33
42
  }
34
43
  : {}),
44
+ runtimeSurface,
35
45
  collaborationTurnExecutionId: input.collaboration?.turnExecutionId,
36
46
  collaborationTurnMode: input.collaboration?.turnMode,
37
47
  collaborationOutcome: input.collaborationOutcome ?? channelContext?.payload.collaborationOutcome,
38
48
  attentionSnapshot: input.attentionSnapshot ?? channelContext?.payload.attentionSnapshot,
49
+ compactionSnapshot: input.compactionSnapshot ?? channelContext?.payload.compactionSnapshot,
39
50
  ...(() => {
40
51
  const collaborationCapabilities = input.collaborationCapabilities ?? channelContext?.payload.collaborationCapabilities;
41
52
  return collaborationCapabilities ? { collaborationCapabilities } : {};
@@ -71,9 +82,12 @@ function resolveProviderSessionRouting(input) {
71
82
  export class ProviderTurnPreparer {
72
83
  channelContextStore;
73
84
  logger;
74
- constructor(channelContextStore, logger = new HostLogger()) {
85
+ resolveProjectionStrategy;
86
+ constructor(channelContextStore, logger = new HostLogger(), options = {}) {
75
87
  this.channelContextStore = channelContextStore;
76
88
  this.logger = logger;
89
+ this.resolveProjectionStrategy = options.resolveProjectionStrategy
90
+ ?? (async () => options.projectionStrategy ?? DEFAULT_PROJECTION_STRATEGY);
77
91
  }
78
92
  async prepare(input) {
79
93
  const incomingParts = input.incomingParts ?? buildHostedTurnContentParts({
@@ -82,11 +96,16 @@ export class ProviderTurnPreparer {
82
96
  const incomingContent = incomingParts.length > 0
83
97
  ? buildHostedIncomingContentText(incomingParts)
84
98
  : input.incomingContent;
99
+ const projectionStrategy = input.provider === 'claude'
100
+ ? await this.resolveProjectionStrategy(input)
101
+ : undefined;
85
102
  let channelContext;
86
103
  if (this.channelContextStore) {
87
104
  try {
88
105
  const prepareInput = {
89
106
  channelId: input.channelId,
107
+ provider: input.provider,
108
+ projectionStrategy,
90
109
  collaboration: input.collaboration
91
110
  ? {
92
111
  ...input.collaboration,
@@ -96,6 +115,7 @@ export class ProviderTurnPreparer {
96
115
  : undefined,
97
116
  collaborationOutcome: input.collaborationOutcome,
98
117
  attentionSnapshot: input.attentionSnapshot,
118
+ compactionSnapshot: input.compactionSnapshot,
99
119
  collaborationCapabilities: input.collaborationCapabilities,
100
120
  missedCollaborationDiagnostic: input.missedCollaborationDiagnostic,
101
121
  ...(input.taskThreadCollaborationContract
@@ -107,6 +127,14 @@ export class ProviderTurnPreparer {
107
127
  ...(input.incomingMessageType !== undefined && incomingContent !== undefined
108
128
  ? { incomingContent }
109
129
  : {}),
130
+ ...(input.taskAssignmentContextOverride
131
+ ? {
132
+ taskAssignmentContextOverride: input.taskAssignmentContextOverride,
133
+ ...(input.taskAssignmentContextOverridePersistence
134
+ ? { taskAssignmentContextOverridePersistence: input.taskAssignmentContextOverridePersistence }
135
+ : {}),
136
+ }
137
+ : {}),
110
138
  };
111
139
  channelContext = await this.channelContextStore.prepare(prepareInput);
112
140
  }
@@ -122,21 +150,43 @@ export class ProviderTurnPreparer {
122
150
  }
123
151
  const promptContext = toPromptContext(channelContext, input);
124
152
  return {
153
+ agentName: input.agentName,
125
154
  channelId: input.channelId,
126
155
  incomingContent,
127
156
  incomingParts,
128
157
  incomingEventKind: input.incomingEventKind,
129
158
  incomingMessageType: input.incomingMessageType,
130
- prompt: buildPrompt({
131
- agentName: input.agentName,
132
- provider: input.provider,
133
- channelId: input.channelId,
134
- incomingAuthorId: input.incomingAuthorId,
135
- incomingContent,
136
- incomingEventKind: input.incomingEventKind,
137
- incomingMessageType: input.incomingMessageType,
138
- promptContext,
139
- }),
159
+ prompt: input.provider === 'claude'
160
+ ? buildClaudeTurnPrompt({
161
+ agentName: input.agentName,
162
+ channelId: input.channelId,
163
+ incomingAuthorId: input.incomingAuthorId,
164
+ incomingContent,
165
+ incomingEventKind: input.incomingEventKind,
166
+ incomingMessageType: input.incomingMessageType,
167
+ promptContext,
168
+ promptStrategy: projectionStrategy,
169
+ })
170
+ : buildPrompt({
171
+ agentName: input.agentName,
172
+ provider: input.provider,
173
+ channelId: input.channelId,
174
+ incomingAuthorId: input.incomingAuthorId,
175
+ incomingContent,
176
+ incomingEventKind: input.incomingEventKind,
177
+ incomingMessageType: input.incomingMessageType,
178
+ promptContext,
179
+ }),
180
+ ...(input.provider === 'claude'
181
+ ? {
182
+ claudeSessionPromptAppend: buildClaudeSessionPromptAppend({
183
+ agentName: input.agentName,
184
+ promptContext,
185
+ promptStrategy: projectionStrategy,
186
+ }),
187
+ projectionStrategy,
188
+ }
189
+ : {}),
140
190
  promptContext,
141
191
  providerSessionRouting: resolveProviderSessionRouting(input),
142
192
  };
@@ -550,11 +550,6 @@ class LoopbackLocalhostGatewayController {
550
550
  return;
551
551
  }
552
552
  case 'users': {
553
- if (!binding.payload?.localhostGateway?.collaboration?.enabled) {
554
- this.sendJson(response, 404, { error: 'not_found' });
555
- this.recordAudit('not-found', 404, decision.path, request.method ?? 'GET', decision.binding);
556
- return;
557
- }
558
553
  this.sendJson(response, 200, {
559
554
  users: await this.controlPlane.listUsers(),
560
555
  });
@@ -910,6 +905,10 @@ function mapGatewayControlPlaneError(error, fallbackReason) {
910
905
  // is broken, and it would retry the same bad call instead of correcting it.
911
906
  case 'bpp.task_property_key_unknown':
912
907
  return new GatewayHttpError(400, { error: 'unknown_property_key' }, 'bad-request');
908
+ case 'bpp.task_property_write_forbidden':
909
+ return new GatewayHttpError(403, { error: 'forbidden' }, 'forbidden');
910
+ case 'bpp.task_property_value_invalid':
911
+ return new GatewayHttpError(400, { error: 'invalid_property_value' }, 'bad-request');
913
912
  case 'bpp.task_property_value_too_long':
914
913
  return new GatewayHttpError(400, { error: 'property_value_too_long' }, 'bad-request');
915
914
  case 'bpp.task_invalid_status':
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { parseDocument, stringify } from 'yaml';
5
- import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, parseProviderIdleShutdownMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
5
+ import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, parseBooleanValue, parseProviderIdleShutdownMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
6
6
  import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
7
7
  const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
8
8
  export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
@@ -127,6 +127,10 @@ function parseProviderCommandOverrides(value, sourceLabel) {
127
127
  if (value.providerIdleShutdownMinutes !== undefined && value.providerIdleShutdownMinutes !== null) {
128
128
  overrides.providerIdleShutdownMinutes = parseProviderIdleShutdownMinutesValue(value.providerIdleShutdownMinutes, sourceLabel);
129
129
  }
130
+ if (value.allowCrossAgentIndependentWorkspaceHandoff !== undefined
131
+ && value.allowCrossAgentIndependentWorkspaceHandoff !== null) {
132
+ overrides.allowCrossAgentIndependentWorkspaceHandoff = parseBooleanValue(value.allowCrossAgentIndependentWorkspaceHandoff, 'allowCrossAgentIndependentWorkspaceHandoff', sourceLabel);
133
+ }
130
134
  return overrides;
131
135
  }
132
136
  function toAgentsDir(hostConfigPath, rawAgentsDir) {
@@ -599,6 +603,9 @@ function renderAgentConfigYaml(agent) {
599
603
  if (agent.providerIdleShutdownMinutes !== undefined) {
600
604
  config.providerIdleShutdownMinutes = agent.providerIdleShutdownMinutes;
601
605
  }
606
+ if (agent.allowCrossAgentIndependentWorkspaceHandoff !== undefined) {
607
+ config.allowCrossAgentIndependentWorkspaceHandoff = agent.allowCrossAgentIndependentWorkspaceHandoff;
608
+ }
602
609
  return stringify(config);
603
610
  }
604
611
  function buildManagedAgentSnapshot(host, stateRootBaseDir, sourcePath, agent) {
@@ -638,6 +645,9 @@ function resolveAgentProviderCommandConfig(hostDefaults, agent) {
638
645
  ...(agent.providerIdleShutdownMinutes !== undefined
639
646
  ? { providerIdleShutdownMinutes: agent.providerIdleShutdownMinutes }
640
647
  : {}),
648
+ ...(agent.allowCrossAgentIndependentWorkspaceHandoff !== undefined
649
+ ? { allowCrossAgentIndependentWorkspaceHandoff: agent.allowCrossAgentIndependentWorkspaceHandoff }
650
+ : {}),
641
651
  });
642
652
  }
643
653
  export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
@@ -2,13 +2,15 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { createConnection, createServer } from 'node:net';
5
- import { dirname, join, resolve } from 'node:path';
5
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { AgentsHostSupervisor } from './agents-host-supervisor.js';
8
- import { CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE, COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveDisabledDefaultCompatibilityGates, resolveManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
8
+ import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from './projection-strategy-values.js';
9
+ import { resolveChannelWorkspaceDirectory, resolveChannelWorkspaceRootDirectory, resolveManagedWorkspaceCollectionRoot, resolveTaskThreadScratchWorkspaceDirectory, } from './context/injection.js';
10
+ import { CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE, COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_CLAUDE_PROMPT_STRATEGY_ENV, INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveDisabledDefaultCompatibilityGates, resolveManagedRuntimeSettingsSnapshot, serializeManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
9
11
  import { DEFAULT_PROVIDER_COMMAND_CONFIG, hasLegacyClaudeOneShotArgs, loadConfigFromEnv, } from './config.js';
10
12
  import { loadLocalConfigGenerateSpec, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
11
- import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonEndpoint, resolveManagedDaemonLogPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
13
+ import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonEndpoint, resolveManagedDaemonLogPath, resolveManagedRuntimeRoot, resolveManagedStateRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
12
14
  const MANAGED_ROOT_MODE = 0o700;
13
15
  const MANAGED_CONFIG_MODE = 0o600;
14
16
  const BOOTSTRAP_LOCK_WAIT_MS = 10_000;
@@ -48,6 +50,9 @@ function isAlreadyExistsError(error) {
48
50
  function isSocketBusyError(error) {
49
51
  return isNodeErrorWithCode(error, 'EADDRINUSE');
50
52
  }
53
+ function isRecord(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
51
56
  function delay(milliseconds) {
52
57
  return new Promise((resolveDelay) => {
53
58
  setTimeout(resolveDelay, milliseconds);
@@ -157,6 +162,7 @@ function normalizeManagedRuntimeSettingsSnapshot(snapshot) {
157
162
  compatibilityGates,
158
163
  providerImplementationOverrides,
159
164
  internalPolicyMode: snapshot.internalPolicyMode,
165
+ projectionStrategy: snapshot.projectionStrategy ?? DEFAULT_PROJECTION_STRATEGY,
160
166
  };
161
167
  }
162
168
  function parseManagedRuntimeSettingsSnapshotRecord(value, sourceLabel) {
@@ -178,11 +184,19 @@ function parseManagedRuntimeSettingsSnapshotRecord(value, sourceLabel) {
178
184
  if (record.internalPolicyMode !== 'audit-only' && record.internalPolicyMode !== 'enforce') {
179
185
  throw new Error(`Invalid ${sourceLabel}: expected internalPolicyMode to be "audit-only" or "enforce"`);
180
186
  }
187
+ const rawProjectionStrategy = typeof record.projectionStrategy === 'string'
188
+ ? record.projectionStrategy
189
+ : record.claudePromptStrategy;
190
+ const projectionStrategy = normalizeProjectionStrategy(rawProjectionStrategy);
191
+ if (rawProjectionStrategy !== undefined && !projectionStrategy) {
192
+ throw new Error(`Invalid ${sourceLabel}: expected projectionStrategy to be "session-brief", "turn-full", "message-only", "turn-thin", or "file-brief"`);
193
+ }
181
194
  return normalizeManagedRuntimeSettingsSnapshot({
182
195
  schemaVersion: MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION,
183
196
  compatibilityGates: [...record.compatibilityGates],
184
197
  providerImplementationOverrides: [...record.providerImplementationOverrides],
185
198
  internalPolicyMode: record.internalPolicyMode,
199
+ projectionStrategy: projectionStrategy ?? DEFAULT_PROJECTION_STRATEGY,
186
200
  });
187
201
  }
188
202
  async function loadManagedRuntimeSettingsSnapshot(rootPath) {
@@ -205,7 +219,7 @@ async function writeManagedRuntimeSettingsSnapshot(rootPath, snapshot) {
205
219
  await ensureDirectory(rootPath, MANAGED_ROOT_MODE);
206
220
  const settingsPath = resolveManagedRuntimeSettingsPath(rootPath);
207
221
  const normalizedSnapshot = normalizeManagedRuntimeSettingsSnapshot(snapshot);
208
- await fs.writeFile(settingsPath, `${JSON.stringify(normalizedSnapshot)}\n`, {
222
+ await fs.writeFile(settingsPath, `${serializeManagedRuntimeSettingsSnapshot(normalizedSnapshot)}\n`, {
209
223
  encoding: 'utf8',
210
224
  mode: MANAGED_CONFIG_MODE,
211
225
  });
@@ -226,6 +240,7 @@ function buildManagedRuntimeSettingsEnv(baseEnv, snapshot) {
226
240
  [INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV]: resolveDisabledDefaultCompatibilityGates(snapshot.compatibilityGates).join(','),
227
241
  [INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV]: snapshot.providerImplementationOverrides.join(','),
228
242
  [INTERNAL_POLICY_MODE_ENV]: snapshot.internalPolicyMode,
243
+ [INTERNAL_CLAUDE_PROMPT_STRATEGY_ENV]: snapshot.projectionStrategy,
229
244
  };
230
245
  }
231
246
  function buildDesiredManagedRuntimeSettingsFromSnapshot(snapshot) {
@@ -340,6 +355,7 @@ export function buildManagedLocalAgentConfig(options) {
340
355
  copilotArgs: [...config.copilotArgs],
341
356
  copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
342
357
  providerIdleShutdownMinutes: config.providerIdleShutdownMinutes,
358
+ allowCrossAgentIndependentWorkspaceHandoff: config.allowCrossAgentIndependentWorkspaceHandoff,
343
359
  },
344
360
  };
345
361
  }
@@ -386,6 +402,9 @@ function mergeManagedLocalAgentConfig(hostDefaults, existingAgent, generatedAgen
386
402
  providerIdleShutdownMinutes: hasOverride('PROVIDER_IDLE_SHUTDOWN_MINUTES')
387
403
  ? generatedAgent.providerIdleShutdownMinutes
388
404
  : existingAgent.providerIdleShutdownMinutes,
405
+ allowCrossAgentIndependentWorkspaceHandoff: hasOverride('ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF')
406
+ ? generatedAgent.allowCrossAgentIndependentWorkspaceHandoff
407
+ : existingAgent.allowCrossAgentIndependentWorkspaceHandoff,
389
408
  };
390
409
  }
391
410
  function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
@@ -401,6 +420,7 @@ function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
401
420
  'COPILOT_ARGS',
402
421
  'COPILOT_SESSION_TTL_MINUTES',
403
422
  'PROVIDER_IDLE_SHUTDOWN_MINUTES',
423
+ 'ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF',
404
424
  ]) {
405
425
  if (processEnv[key] !== undefined) {
406
426
  explicitOverrides[key] = processEnv[key];
@@ -819,6 +839,101 @@ async function shutdownManagedDaemon(rootPath, sendRequest, waitForShutdown) {
819
839
  }
820
840
  await waitForShutdown(rootPath);
821
841
  }
842
+ function resolveTrustedPersistedChannelWorkspaceRoot(workspaceCollectionRoot, owningChannelId, rootPath) {
843
+ if (!owningChannelId || !isAbsolute(rootPath)) {
844
+ return null;
845
+ }
846
+ const expectedWorkspacePath = resolveChannelWorkspaceDirectory(workspaceCollectionRoot, owningChannelId);
847
+ if (rootPath !== expectedWorkspacePath) {
848
+ return null;
849
+ }
850
+ return resolveChannelWorkspaceRootDirectory(workspaceCollectionRoot, owningChannelId);
851
+ }
852
+ async function discoverManagedWorkspaceRootsForPurge(rootPath, processEnv) {
853
+ const trustedWorkspaceCollectionRoots = new Set([
854
+ resolveManagedWorkspaceCollectionRoot(undefined, processEnv),
855
+ ]);
856
+ const managedStateRoot = resolveManagedStateRoot(rootPath);
857
+ const agentStateEntries = await fs.readdir(managedStateRoot, { withFileTypes: true }).catch((error) => {
858
+ if (isNotFoundError(error)) {
859
+ return [];
860
+ }
861
+ throw error;
862
+ });
863
+ const workspaceRoots = new Set();
864
+ for (const agentStateEntry of agentStateEntries) {
865
+ if (!agentStateEntry.isDirectory()) {
866
+ continue;
867
+ }
868
+ const channelContextRoot = join(managedStateRoot, agentStateEntry.name, 'channel-context');
869
+ const channelEntries = await fs.readdir(channelContextRoot, { withFileTypes: true }).catch((error) => {
870
+ if (isNotFoundError(error)) {
871
+ return [];
872
+ }
873
+ throw error;
874
+ });
875
+ for (const channelEntry of channelEntries) {
876
+ if (!channelEntry.isDirectory()) {
877
+ continue;
878
+ }
879
+ const payloadPath = join(channelContextRoot, channelEntry.name, 'context.json');
880
+ const payload = await fs.readFile(payloadPath, 'utf8').catch((error) => {
881
+ if (isNotFoundError(error)) {
882
+ return undefined;
883
+ }
884
+ throw error;
885
+ });
886
+ if (!payload) {
887
+ continue;
888
+ }
889
+ let parsed;
890
+ try {
891
+ parsed = JSON.parse(payload);
892
+ }
893
+ catch {
894
+ continue;
895
+ }
896
+ if (!isRecord(parsed) || typeof parsed.channelId !== 'string' || !isRecord(parsed.resolvedWorkspace)) {
897
+ continue;
898
+ }
899
+ const resolvedWorkspace = parsed.resolvedWorkspace;
900
+ if ((resolvedWorkspace.authority !== 'channel'
901
+ && resolvedWorkspace.authority !== 'task-thread-scratch'
902
+ && resolvedWorkspace.authority !== 'task-execution-target')
903
+ || typeof resolvedWorkspace.rootPath !== 'string'
904
+ || !isAbsolute(resolvedWorkspace.rootPath)) {
905
+ continue;
906
+ }
907
+ for (const workspaceCollectionRoot of trustedWorkspaceCollectionRoots) {
908
+ if (resolvedWorkspace.authority === 'channel') {
909
+ if (typeof resolvedWorkspace.owningChannelId !== 'string'
910
+ || resolvedWorkspace.owningChannelId.length === 0) {
911
+ continue;
912
+ }
913
+ const trustedChannelWorkspaceRoot = resolveTrustedPersistedChannelWorkspaceRoot(workspaceCollectionRoot, resolvedWorkspace.owningChannelId, resolvedWorkspace.rootPath);
914
+ if (!trustedChannelWorkspaceRoot) {
915
+ continue;
916
+ }
917
+ workspaceRoots.add(trustedChannelWorkspaceRoot);
918
+ break;
919
+ }
920
+ if (resolvedWorkspace.authority === 'task-thread-scratch') {
921
+ const expectedWorkspacePath = resolveTaskThreadScratchWorkspaceDirectory(workspaceCollectionRoot, parsed.channelId);
922
+ if (resolvedWorkspace.rootPath !== expectedWorkspacePath) {
923
+ continue;
924
+ }
925
+ workspaceRoots.add(resolveChannelWorkspaceRootDirectory(workspaceCollectionRoot, parsed.channelId));
926
+ break;
927
+ }
928
+ if (resolvedWorkspace.authority !== 'task-execution-target') {
929
+ continue;
930
+ }
931
+ continue;
932
+ }
933
+ }
934
+ }
935
+ return [...workspaceRoots].sort((left, right) => left.localeCompare(right));
936
+ }
822
937
  export async function cleanupManagedRuntime(options, deps = {}) {
823
938
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
824
939
  const removeRuntimeRoot = deps.removeRuntimeRoot ??
@@ -855,6 +970,9 @@ export async function cleanupManagedRuntime(options, deps = {}) {
855
970
  await waitForShutdown(resolved.rootPath);
856
971
  }
857
972
  if (options.purge === true) {
973
+ for (const workspaceRoot of await discoverManagedWorkspaceRootsForPurge(resolved.rootPath, options.processEnv ?? process.env)) {
974
+ await fs.rm(workspaceRoot, { recursive: true, force: true });
975
+ }
858
976
  await removeRuntimeRoot(resolved.rootPath);
859
977
  }
860
978
  }
@@ -900,16 +1018,16 @@ export async function spawnManagedDaemonProcess(options, deps = {}) {
900
1018
  }
901
1019
  const thisPath = fileURLToPath(import.meta.url);
902
1020
  const cliEntrypointPath = join(dirname(thisPath), thisPath.endsWith('.ts') ? 'cli.ts' : 'cli.js');
1021
+ const baseEnv = {
1022
+ ...process.env,
1023
+ ...(options.processEnv ?? {}),
1024
+ };
903
1025
  return spawnDetachedProcess({
904
1026
  rootPath: options.rootPath,
905
1027
  command: process.execPath,
906
1028
  env: {
907
- ...buildManagedRuntimeSettingsEnv({
908
- ...process.env,
909
- ...(options.processEnv ?? {}),
910
- }, managedRuntimeSettingsSnapshot),
1029
+ ...buildManagedRuntimeSettingsEnv(baseEnv, managedRuntimeSettingsSnapshot),
911
1030
  AGENTS_HOST_MANAGED_DAEMON_LOG_PATH: resolveManagedDaemonLogPath(options.rootPath),
912
- AGENTS_HOST_INTERNAL_TASK_WORKSPACE_ROOT_DIR: options.rootPath,
913
1031
  },
914
1032
  args: [
915
1033
  ...process.execArgv,