@borgee/agents-host 0.2.33 → 0.2.44

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 (60) hide show
  1. package/README.md +28 -7
  2. package/dist/agents-host.d.ts +19 -0
  3. package/dist/agents-host.js +163 -44
  4. package/dist/chat/chat-control-plane.d.ts +9 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
  6. package/dist/chat/sdk-chat-control-plane.js +9 -0
  7. package/dist/cli-args.d.ts +1 -1
  8. package/dist/cli-args.js +5 -0
  9. package/dist/compatibility-gates.d.ts +1 -0
  10. package/dist/compatibility-gates.js +2 -0
  11. package/dist/config.d.ts +4 -1
  12. package/dist/config.js +21 -3
  13. package/dist/context/injection.d.ts +5 -3
  14. package/dist/context/injection.js +45 -29
  15. package/dist/context/main-session-delegation.d.ts +1 -0
  16. package/dist/context/main-session-delegation.js +6 -0
  17. package/dist/context/prompt.js +25 -35
  18. package/dist/context/skill-manual.d.ts +13 -0
  19. package/dist/context/skill-manual.js +18 -0
  20. package/dist/context/turn-preparation.js +13 -4
  21. package/dist/gateway/localhost-gateway.js +75 -1
  22. package/dist/hosted-turn-content.d.ts +15 -0
  23. package/dist/hosted-turn-content.js +50 -0
  24. package/dist/local-config.js +10 -1
  25. package/dist/managed-daemon.d.ts +3 -2
  26. package/dist/managed-daemon.js +82 -29
  27. package/dist/plugin-sdk.js +58 -1
  28. package/dist/plugin-sdk.js.map +2 -2
  29. package/dist/policy/gateway-authorization.d.ts +18 -3
  30. package/dist/policy/gateway-authorization.js +33 -1
  31. package/dist/providers/claude/adapter.d.ts +3 -1
  32. package/dist/providers/claude/adapter.js +13 -1
  33. package/dist/providers/claude/cli-client.d.ts +36 -3
  34. package/dist/providers/claude/cli-client.js +225 -37
  35. package/dist/providers/codex/adapter.d.ts +3 -1
  36. package/dist/providers/codex/adapter.js +13 -1
  37. package/dist/providers/codex/cli-client.d.ts +35 -3
  38. package/dist/providers/codex/cli-client.js +212 -31
  39. package/dist/providers/codex/project-doc.js +16 -29
  40. package/dist/providers/copilot/adapter.d.ts +3 -1
  41. package/dist/providers/copilot/adapter.js +13 -1
  42. package/dist/providers/copilot/cli-client.d.ts +34 -2
  43. package/dist/providers/copilot/cli-client.js +196 -18
  44. package/dist/providers/create-provider.d.ts +1 -1
  45. package/dist/providers/create-provider.js +30 -14
  46. package/dist/providers/idle-backend-shutdown.d.ts +16 -0
  47. package/dist/providers/idle-backend-shutdown.js +53 -0
  48. package/dist/providers/provider-adapter.d.ts +35 -0
  49. package/dist/providers/provider-adapter.js +44 -1
  50. package/dist/state-paths.d.ts +9 -1
  51. package/dist/state-paths.js +22 -3
  52. package/dist/types.d.ts +40 -2
  53. package/package.json +2 -2
  54. package/skills/borgee-agent/SKILL.md +133 -35
  55. package/skills/borgee-agent/references/errors.md +38 -0
  56. package/skills/borgee-agent/references/task-properties.md +30 -0
  57. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  58. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  59. package/skills/borgee-agent/borgee-agent.mjs +0 -507
  60. package/skills/borgee-agent/borgee-agent.py +0 -438
@@ -85,4 +85,4 @@ export declare function parseCleanupManagedArgs(argv: string[]): ParsedCleanupMa
85
85
  export declare function parseApplyManagedArgs(argv: string[]): ParsedApplyManagedArgs;
86
86
  /** `update` always installs the latest published release; it takes no options. */
87
87
  export declare function assertNoUpdateArgs(argv: string[]): void;
88
- export declare const USAGE = "Usage:\n agents-host start <serverUrl> <apiKey> [options]\n agents-host start-managed <serverUrl>\n agents-host start-managed <serverUrl> <apiKey> [agent-options]\n agents-host describe-managed <serverUrl>\n agents-host cleanup-managed <serverUrl> [--purge]\n agents-host apply-managed <serverUrl> --stdin\n agents-host apply-managed <serverUrl> --spec-json <json>\n agents-host log <serverUrl> [--lines <n>] [--follow]\n agents-host log <serverUrl> --path\n agents-host start --config <path-to-host-config> [--debug]\n agents-host validate --config <path-to-host-config>\n agents-host describe --config <path-to-host-config>\n agents-host print-layout --root <dir>\n agents-host generate-config --root <dir> --stdin\n agents-host generate-config --root <dir> --spec-json <json>\n agents-host update\n\nForeground start options:\n --debug Enable host/provider debug logs (or set AGENTS_HOST_DEBUG=1)\n\nSingle-agent agent options:\n --name <name> Display name (default: Assistant)\n --provider <claude|codex|copilot>\n Runtime provider (default: claude)\n --claude-command <cmd> Local Claude ACP adapter command (default: claude-agent-acp)\n --claude-args <args> Local Claude ACP adapter args (empty on the shipped bundled path)\n --codex-command <cmd> Local Codex ACP adapter command (default: codex-acp)\n --codex-args <args> Local Codex ACP adapter args\n --copilot-command <cmd> Local Copilot CLI command (default: copilot)\n --copilot-args <args> Ignored by the Copilot ACP prototype\n --copilot-session-ttl-minutes <minutes>\n Idle session TTL for Copilot ACP sessions (default: 2880)\n\nCommand summary:\n start <serverUrl> <apiKey> Start one foreground hosted agent (legacy-compatible behavior)\n start --config <path> Start agents + supervisor + watchers from a host config file\n start-managed <serverUrl> Restart or resume an existing per-serverUrl managed daemon\n start-managed <serverUrl> <apiKey>\n Upsert one agent into the per-serverUrl local daemon and exit after reconcile\n describe-managed <serverUrl> Print managed-runtime spec JSON for machine callers\n cleanup-managed <serverUrl> Stop one managed-runtime daemon locally; --purge also removes its runtime root\n apply-managed <serverUrl> Apply one managed-runtime full-set spec for machine callers\n log <serverUrl> Print the managed daemon log tail for one server runtime\n validate --config <path> Validate local-config files without starting agents or watchers\n describe --config <path> Print the current managed full-set spec as JSON\n print-layout --root <dir> Print the canonical default local-config layout as JSON\n generate-config --root <dir> --stdin Materialize canonical local-config files from stdin\n generate-config --root <dir> --spec-json <json>\n Compatibility input; JSON is exposed in process arguments\n update Update the globally installed @borgee/agents-host to the latest release\n\nStartup update check:\n Startup commands print an advisory notice on stderr when a newer release is published.\n Set AGENTS_HOST_DISABLE_UPDATE_CHECK=1 to turn that check off.\n\nExamples:\n agents-host start https://borgee.example.com bgr_xxxxxxxx --provider copilot --debug\n agents-host start-managed https://borgee.example.com\n agents-host start-managed https://borgee.example.com bgr_xxxxxxxx --provider copilot\n agents-host describe-managed https://borgee.example.com\n agents-host cleanup-managed https://borgee.example.com --purge\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host apply-managed https://borgee.example.com --stdin\n agents-host log https://borgee.example.com --lines 200 --follow\n agents-host log https://borgee.example.com --path\n agents-host start --config ./agents-host.yaml --debug\n agents-host validate --config ./agents-host.yaml\n agents-host describe --config ./agents-host.yaml\n agents-host print-layout --root ./runtime-root\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host generate-config --root ./runtime-root --stdin\n agents-host update\n";
88
+ export declare const USAGE = "Usage:\n agents-host start <serverUrl> <apiKey> [options]\n agents-host start-managed <serverUrl>\n agents-host start-managed <serverUrl> <apiKey> [agent-options]\n agents-host describe-managed <serverUrl>\n agents-host cleanup-managed <serverUrl> [--purge]\n agents-host apply-managed <serverUrl> --stdin\n agents-host apply-managed <serverUrl> --spec-json <json>\n agents-host log <serverUrl> [--lines <n>] [--follow]\n agents-host log <serverUrl> --path\n agents-host start --config <path-to-host-config> [--debug]\n agents-host validate --config <path-to-host-config>\n agents-host describe --config <path-to-host-config>\n agents-host print-layout --root <dir>\n agents-host generate-config --root <dir> --stdin\n agents-host generate-config --root <dir> --spec-json <json>\n agents-host update\n\nForeground start options:\n --debug Enable host/provider debug logs (or set AGENTS_HOST_DEBUG=1)\n\nSingle-agent agent options:\n --name <name> Display name (default: Assistant)\n --provider <claude|codex|copilot>\n Runtime provider (default: claude)\n --claude-command <cmd> Local Claude ACP adapter command (default: claude-agent-acp)\n --claude-args <args> Local Claude ACP adapter args (empty on the shipped bundled path)\n --codex-command <cmd> Local Codex ACP adapter command (default: codex-acp)\n --codex-args <args> Local Codex ACP adapter args\n --copilot-command <cmd> Local Copilot CLI command (default: copilot)\n --copilot-args <args> Ignored by the Copilot ACP prototype\n --copilot-session-ttl-minutes <minutes>\n Idle session TTL for Copilot ACP sessions (default: 2880)\n --provider-idle-shutdown-minutes <minutes>\n Shut the shared ACP adapter process down after this much idle\n time and re-spawn it on the next turn; 0 keeps it resident\n (default: 10)\n\nCommand summary:\n start <serverUrl> <apiKey> Start one foreground hosted agent (legacy-compatible behavior)\n start --config <path> Start agents + supervisor + watchers from a host config file\n start-managed <serverUrl> Restart or resume an existing per-serverUrl managed daemon\n start-managed <serverUrl> <apiKey>\n Upsert one agent into the per-serverUrl local daemon and exit after reconcile\n describe-managed <serverUrl> Print managed-runtime spec JSON for machine callers\n cleanup-managed <serverUrl> Stop one managed-runtime daemon locally; --purge also removes its runtime root\n apply-managed <serverUrl> Apply one managed-runtime full-set spec for machine callers\n log <serverUrl> Print the managed daemon log tail for one server runtime\n validate --config <path> Validate local-config files without starting agents or watchers\n describe --config <path> Print the current managed full-set spec as JSON\n print-layout --root <dir> Print the canonical default local-config layout as JSON\n generate-config --root <dir> --stdin Materialize canonical local-config files from stdin\n generate-config --root <dir> --spec-json <json>\n Compatibility input; JSON is exposed in process arguments\n update Update the globally installed @borgee/agents-host to the latest release\n\nStartup update check:\n Startup commands print an advisory notice on stderr when a newer release is published.\n Set AGENTS_HOST_DISABLE_UPDATE_CHECK=1 to turn that check off.\n\nExamples:\n agents-host start https://borgee.example.com bgr_xxxxxxxx --provider copilot --debug\n agents-host start-managed https://borgee.example.com\n agents-host start-managed https://borgee.example.com bgr_xxxxxxxx --provider copilot\n agents-host describe-managed https://borgee.example.com\n agents-host cleanup-managed https://borgee.example.com --purge\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host apply-managed https://borgee.example.com --stdin\n agents-host log https://borgee.example.com --lines 200 --follow\n agents-host log https://borgee.example.com --path\n agents-host start --config ./agents-host.yaml --debug\n agents-host validate --config ./agents-host.yaml\n agents-host describe --config ./agents-host.yaml\n agents-host print-layout --root ./runtime-root\n printf '%s' '{\"host\":{\"borgeeBaseUrl\":\"https://borgee.example.com\"},\"agents\":[{\"key\":\"cp1\",\"name\":\"Copilot\",\"apiKey\":\"bgr_xxx\",\"provider\":\"copilot\"}]}' | agents-host generate-config --root ./runtime-root --stdin\n agents-host update\n";
package/dist/cli-args.js CHANGED
@@ -15,6 +15,7 @@ export const CLI_FLAG_TO_ENV = {
15
15
  'copilot-command': 'COPILOT_COMMAND',
16
16
  'copilot-args': 'COPILOT_ARGS',
17
17
  'copilot-session-ttl-minutes': 'COPILOT_SESSION_TTL_MINUTES',
18
+ 'provider-idle-shutdown-minutes': 'PROVIDER_IDLE_SHUTDOWN_MINUTES',
18
19
  };
19
20
  const SINGLE_AGENT_FLAG_NAMES = new Set(Object.keys(CLI_FLAG_TO_ENV));
20
21
  const FLAGS_ALLOWING_DASH_PREFIX_VALUE = new Set(['claude-args', 'codex-args', 'copilot-args']);
@@ -512,6 +513,10 @@ Single-agent agent options:
512
513
  --copilot-args <args> Ignored by the Copilot ACP prototype
513
514
  --copilot-session-ttl-minutes <minutes>
514
515
  Idle session TTL for Copilot ACP sessions (default: 2880)
516
+ --provider-idle-shutdown-minutes <minutes>
517
+ Shut the shared ACP adapter process down after this much idle
518
+ time and re-spawn it on the next turn; 0 keeps it resident
519
+ (default: 10)
515
520
 
516
521
  Command summary:
517
522
  start <serverUrl> <apiKey> Start one foreground hosted agent (legacy-compatible behavior)
@@ -13,6 +13,7 @@ export declare const COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE =
13
13
  export declare const TOKEN_BINDING_COMPATIBILITY_GATE = "token-binding";
14
14
  export declare const POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE = "policy-audit-enforcement";
15
15
  export declare const MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE = "managed-runtime-convergence";
16
+ export declare const PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE = "provider-idle-shutdown";
16
17
  export declare const COMPATIBILITY_GATES_ENV = "AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES";
17
18
  export declare const INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV = "AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES";
18
19
  export declare const INTERNAL_POLICY_MODE_ENV = "AGENTS_HOST_INTERNAL_POLICY_MODE";
@@ -14,6 +14,7 @@ export const COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE = 'collab
14
14
  export const TOKEN_BINDING_COMPATIBILITY_GATE = 'token-binding';
15
15
  export const POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE = 'policy-audit-enforcement';
16
16
  export const MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE = 'managed-runtime-convergence';
17
+ export const PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE = 'provider-idle-shutdown';
17
18
  export const COMPATIBILITY_GATES_ENV = 'AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES';
18
19
  export const INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV = 'AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES';
19
20
  export const INTERNAL_POLICY_MODE_ENV = 'AGENTS_HOST_INTERNAL_POLICY_MODE';
@@ -30,6 +31,7 @@ export const DEFAULT_INTERNAL_COMPATIBILITY_GATES = Object.freeze([
30
31
  LOCALHOST_GATEWAY_COMPATIBILITY_GATE,
31
32
  MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE,
32
33
  POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE,
34
+ PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE,
33
35
  SKILL_RUNTIME_COMPATIBILITY_GATE,
34
36
  TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE,
35
37
  TOKEN_BINDING_COMPATIBILITY_GATE,
package/dist/config.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { AgentsHostConfig, ProviderCommandConfig, ProviderKind } from './types.js';
2
- export declare const MAX_COPILOT_SESSION_TTL_MINUTES: number;
2
+ export declare const MAX_TIMER_DELAY_MINUTES: number;
3
3
  export declare const DEFAULT_COPILOT_SESSION_TTL_MINUTES: number;
4
+ export declare const PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES = 0;
5
+ export declare const DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES = 10;
4
6
  export declare const DEFAULT_CLAUDE_COMMAND = "claude-agent-acp";
5
7
  export declare const LEGACY_CLAUDE_COMMAND = "claude";
6
8
  export declare const LEGACY_CLAUDE_ONE_SHOT_ARGS: readonly ["--print", "--permission-mode", "bypassPermissions"];
@@ -10,6 +12,7 @@ export declare function optionalNonEmptyString(value: unknown, fieldName: string
10
12
  export declare function parseArgs(value: string): string[];
11
13
  export declare function optionalStringArray(value: unknown, fieldName: string, sourceLabel: string): string[] | undefined;
12
14
  export declare function parseCopilotSessionTtlMinutesValue(value: unknown, sourceLabel: string): number;
15
+ export declare function parseProviderIdleShutdownMinutesValue(value: unknown, sourceLabel: string): number;
13
16
  export declare function resolveProvider(rawValue: string, sourceLabel: string): ProviderKind;
14
17
  export declare function assertProviderCompatibility(provider: ProviderKind, sourceLabel: string, env?: NodeJS.ProcessEnv): void;
15
18
  export declare function isLegacyClaudeCompatibilityAlias(command: string, args: string[]): boolean;
package/dist/config.js CHANGED
@@ -2,8 +2,10 @@ import { basename } from 'node:path';
2
2
  import { resolveSingleAgentStateRoot } from './state-paths.js';
3
3
  import { CODEX_PROVIDER_COMPATIBILITY_GATE, resolveInternalCompatibilityGates, } from './compatibility-gates.js';
4
4
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
5
- export const MAX_COPILOT_SESSION_TTL_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
5
+ export const MAX_TIMER_DELAY_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
6
6
  export const DEFAULT_COPILOT_SESSION_TTL_MINUTES = 2 * 24 * 60;
7
+ export const PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES = 0;
8
+ export const DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES = 10;
7
9
  export const DEFAULT_CLAUDE_COMMAND = 'claude-agent-acp';
8
10
  export const LEGACY_CLAUDE_COMMAND = 'claude';
9
11
  export const LEGACY_CLAUDE_ONE_SHOT_ARGS = ['--print', '--permission-mode', 'bypassPermissions'];
@@ -15,6 +17,7 @@ export const DEFAULT_PROVIDER_COMMAND_CONFIG = {
15
17
  copilotCommand: 'copilot',
16
18
  copilotArgs: ['-s', '--no-color', '--allow-all-tools', '--output-format', 'text'],
17
19
  copilotSessionTtlMinutes: DEFAULT_COPILOT_SESSION_TTL_MINUTES,
20
+ providerIdleShutdownMinutes: DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES,
18
21
  };
19
22
  function requireEnv(name, env) {
20
23
  return requireNonEmptyString(env[name], `Missing required environment variable: ${name}`);
@@ -57,8 +60,18 @@ export function parseCopilotSessionTtlMinutesValue(value, sourceLabel) {
57
60
  if (!Number.isFinite(parsed) || parsed <= 0) {
58
61
  throw new Error(`Invalid COPILOT_SESSION_TTL_MINUTES in ${sourceLabel}: expected a positive number`);
59
62
  }
60
- if (parsed > MAX_COPILOT_SESSION_TTL_MINUTES) {
61
- throw new Error(`Invalid COPILOT_SESSION_TTL_MINUTES in ${sourceLabel}: must be <= ${MAX_COPILOT_SESSION_TTL_MINUTES.toFixed(2)} minutes to fit within the Node.js timer limit`);
63
+ if (parsed > MAX_TIMER_DELAY_MINUTES) {
64
+ throw new Error(`Invalid COPILOT_SESSION_TTL_MINUTES in ${sourceLabel}: must be <= ${MAX_TIMER_DELAY_MINUTES.toFixed(2)} minutes to fit within the Node.js timer limit`);
65
+ }
66
+ return parsed;
67
+ }
68
+ export function parseProviderIdleShutdownMinutesValue(value, sourceLabel) {
69
+ const parsed = typeof value === 'number' ? value : Number(String(value).trim());
70
+ if (!Number.isFinite(parsed) || parsed < PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES) {
71
+ throw new Error(`Invalid PROVIDER_IDLE_SHUTDOWN_MINUTES in ${sourceLabel}: expected a non-negative number, where ${PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES} keeps the shared adapter process resident`);
72
+ }
73
+ if (parsed > MAX_TIMER_DELAY_MINUTES) {
74
+ throw new Error(`Invalid PROVIDER_IDLE_SHUTDOWN_MINUTES in ${sourceLabel}: must be <= ${MAX_TIMER_DELAY_MINUTES.toFixed(2)} minutes to fit within the Node.js timer limit`);
62
75
  }
63
76
  return parsed;
64
77
  }
@@ -119,6 +132,8 @@ export function resolveProviderCommandConfig(overrides = {}) {
119
132
  copilotCommand: overrides.copilotCommand ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotCommand,
120
133
  copilotArgs: [...(overrides.copilotArgs ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotArgs)],
121
134
  copilotSessionTtlMinutes: overrides.copilotSessionTtlMinutes ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotSessionTtlMinutes,
135
+ providerIdleShutdownMinutes: overrides.providerIdleShutdownMinutes
136
+ ?? DEFAULT_PROVIDER_COMMAND_CONFIG.providerIdleShutdownMinutes,
122
137
  };
123
138
  }
124
139
  export function loadConfigFromEnv(env = process.env) {
@@ -135,6 +150,9 @@ export function loadConfigFromEnv(env = process.env) {
135
150
  copilotSessionTtlMinutes: env.COPILOT_SESSION_TTL_MINUTES && env.COPILOT_SESSION_TTL_MINUTES.trim().length > 0
136
151
  ? parseCopilotSessionTtlMinutesValue(env.COPILOT_SESSION_TTL_MINUTES, 'environment variable COPILOT_SESSION_TTL_MINUTES')
137
152
  : DEFAULT_PROVIDER_COMMAND_CONFIG.copilotSessionTtlMinutes,
153
+ providerIdleShutdownMinutes: env.PROVIDER_IDLE_SHUTDOWN_MINUTES && env.PROVIDER_IDLE_SHUTDOWN_MINUTES.trim().length > 0
154
+ ? parseProviderIdleShutdownMinutesValue(env.PROVIDER_IDLE_SHUTDOWN_MINUTES, 'environment variable PROVIDER_IDLE_SHUTDOWN_MINUTES')
155
+ : DEFAULT_PROVIDER_COMMAND_CONFIG.providerIdleShutdownMinutes,
138
156
  });
139
157
  assertProviderCommandCompatibility(provider, providerConfig, 'environment variables');
140
158
  return {
@@ -21,7 +21,7 @@ export interface PreparedChannelContext {
21
21
  directoryPath: string;
22
22
  payload: ChannelContextPayload;
23
23
  payloadPath?: string;
24
- gatewayAuthPath?: string;
24
+ gatewayCredentialPath?: string;
25
25
  skillRuntime?: SkillRuntimeBootstrapMetadata;
26
26
  localhostGateway?: LocalhostGatewayBootstrapMetadata;
27
27
  taskWorkspace?: TaskWorkspaceContext;
@@ -83,6 +83,7 @@ interface ContextFileSystem {
83
83
  mode?: number;
84
84
  }): Promise<void>;
85
85
  unlink(path: string): Promise<void>;
86
+ rename(oldPath: string, newPath: string): Promise<void>;
86
87
  access(path: string): Promise<void>;
87
88
  }
88
89
  interface FileChannelContextStoreOptions {
@@ -98,8 +99,9 @@ export declare function resolveChannelContextDirectory(stateRootDir: string, cha
98
99
  export declare function resolveChannelContextPayloadPath(stateRootDir: string, channelId: string): string;
99
100
  export declare function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir: string): string;
100
101
  export declare function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir: string, channelId: string, taskId: string): string;
101
- export declare function resolveChannelContextGatewayAuthPath(stateRootDir: string, channelId: string, turnExecutionId?: string): string;
102
- export declare function resolveGatewayAuthPathFromPayloadPath(payloadPath: string): string;
102
+ export declare function resolveChannelGatewayCredentialPath(stateRootDir: string, channelId: string): string;
103
+ export declare function resolveGatewayCredentialPathFromPayloadPath(payloadPath: string): string;
104
+ export declare function isGatewayCredentialSidecarBasename(basename: string): boolean;
103
105
  export declare function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl: string, fileSystem?: Pick<ContextFileSystem, 'access'>): Promise<SkillRuntimeBootstrapMetadata>;
104
106
  export declare class FileChannelContextStore implements ChannelContextStore {
105
107
  private readonly stateRootDir;
@@ -1,9 +1,10 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
5
6
  const CHANNEL_CONTEXT_PAYLOAD_FILENAME = 'context.json';
6
- const CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME = '.localhost-gateway-auth.json';
7
+ const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
7
8
  const TASK_WORKSPACE_ROOT_DIRNAME = '.borgee-task-workspaces';
8
9
  export class ChannelContextPreparationError extends Error {
9
10
  partialContext;
@@ -29,6 +30,9 @@ const DEFAULT_FILE_SYSTEM = {
29
30
  async unlink(path) {
30
31
  await fs.unlink(path);
31
32
  },
33
+ async rename(oldPath, newPath) {
34
+ await fs.rename(oldPath, newPath);
35
+ },
32
36
  async access(path) {
33
37
  await fs.access(path);
34
38
  },
@@ -45,7 +49,7 @@ function toSkillRuntimePayload(skillRuntime) {
45
49
  pythonCliPath: skillRuntime.pythonCliPath,
46
50
  };
47
51
  }
48
- function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
52
+ function buildBorgeeAgentGatewayCredential(channelId, localhostGateway) {
49
53
  if (!localhostGateway) {
50
54
  return undefined;
51
55
  }
@@ -53,6 +57,7 @@ function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
53
57
  schemaVersion: 1,
54
58
  channelId,
55
59
  localhostGateway: {
60
+ baseUrl: localhostGateway.baseUrl,
56
61
  token: localhostGateway.token,
57
62
  },
58
63
  };
@@ -144,17 +149,28 @@ export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
144
149
  export function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir, channelId, taskId) {
145
150
  return join(resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir), encodeChannelPathSegment(channelId), encodeChannelPathSegment(taskId));
146
151
  }
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);
152
+ export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
153
+ return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
154
+ }
155
+ export function resolveGatewayCredentialPathFromPayloadPath(payloadPath) {
156
+ return join(dirname(payloadPath), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
152
157
  }
153
- export function resolveGatewayAuthPathFromPayloadPath(payloadPath) {
154
- return join(dirname(payloadPath), CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME);
158
+ /**
159
+ * Every basename a gateway credential can carry in a channel context directory, including the ones
160
+ * this host no longer writes. The directory holds at most one live credential — the token is
161
+ * reissued every turn — and this sweep is the only thing that ever unlinks the others, so a name
162
+ * dropped from this list leaves a 0600 file behind that still looks live.
163
+ */
164
+ const GATEWAY_CREDENTIAL_SIDECAR_BASENAME_PREFIXES = [
165
+ '.borgee-agent-gateway.',
166
+ '.borgee-agent-bootstrap.',
167
+ '.localhost-gateway-auth.',
168
+ ];
169
+ export function isGatewayCredentialSidecarBasename(basename) {
170
+ return basename.endsWith('.json')
171
+ && GATEWAY_CREDENTIAL_SIDECAR_BASENAME_PREFIXES.some((prefix) => basename.startsWith(prefix));
155
172
  }
156
- const LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX = '.localhost-gateway-auth.';
157
- async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
173
+ async function pruneGatewayCredentialSidecars(fileSystem, directoryPath, keepBasename) {
158
174
  let entries;
159
175
  try {
160
176
  entries = await fileSystem.readdir(directoryPath);
@@ -167,8 +183,7 @@ async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
167
183
  throw error;
168
184
  }
169
185
  await Promise.all(entries
170
- .filter((entry) => entry.startsWith(LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX)
171
- && entry.endsWith('.json'))
186
+ .filter((entry) => isGatewayCredentialSidecarBasename(entry) && entry !== keepBasename)
172
187
  .map(async (entry) => {
173
188
  await fileSystem.unlink(join(directoryPath, entry)).catch((error) => {
174
189
  if (error.code !== 'ENOENT') {
@@ -196,8 +211,8 @@ export async function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl, fileSystem
196
211
  const packageRootPath = await resolvePackageRootFromModuleUrl(moduleUrl, fileSystem);
197
212
  const skillDirectoryPath = join(packageRootPath, 'skills', BORGEE_AGENT_SKILL_DIRNAME);
198
213
  const skillMarkdownPath = join(skillDirectoryPath, 'SKILL.md');
199
- const nodeCliPath = join(skillDirectoryPath, 'borgee-agent.mjs');
200
- const pythonCliPath = join(skillDirectoryPath, 'borgee-agent.py');
214
+ const nodeCliPath = join(skillDirectoryPath, 'scripts', 'borgee-agent.mjs');
215
+ const pythonCliPath = join(skillDirectoryPath, 'scripts', 'borgee-agent.py');
201
216
  await Promise.all([
202
217
  fileSystem.access(skillDirectoryPath),
203
218
  fileSystem.access(skillMarkdownPath),
@@ -249,7 +264,7 @@ export class FileChannelContextStore {
249
264
  const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
250
265
  const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
251
266
  const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
252
- const gatewayAuthPath = resolveGatewayAuthPathFromPayloadPath(payloadPath);
267
+ const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
253
268
  const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
254
269
  const skillRuntime = this.skillRuntimeEnabled
255
270
  ? await this.resolveSkillRuntimeBestEffort()
@@ -292,15 +307,15 @@ export class FileChannelContextStore {
292
307
  ...(taskWorkspace ? { taskWorkspace } : {}),
293
308
  }
294
309
  : undefined);
295
- const gatewayAuthPayload = buildLocalhostGatewayAuthPayload(input.channelId, issuedLocalhostGateway);
310
+ const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
296
311
  let taskWorkspaceMaterialized = false;
297
312
  let payloadWritten = false;
298
- let gatewayAuthWritten = false;
313
+ let gatewayCredentialWritten = false;
299
314
  const preparedContext = {
300
315
  directoryPath,
301
316
  payload,
302
317
  payloadPath,
303
- gatewayAuthPath: gatewayAuthPayload ? gatewayAuthPath : undefined,
318
+ gatewayCredentialPath: gatewayCredential ? gatewayCredentialPath : undefined,
304
319
  skillRuntime,
305
320
  localhostGateway,
306
321
  taskWorkspace,
@@ -311,19 +326,20 @@ export class FileChannelContextStore {
311
326
  await this.fileSystem.mkdir(taskWorkspace.rootPath, { recursive: true, mode: 0o700 });
312
327
  taskWorkspaceMaterialized = true;
313
328
  }
314
- await pruneLegacyGatewayAuthSidecars(this.fileSystem, directoryPath);
329
+ await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
315
330
  await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
316
331
  payloadWritten = true;
317
- if (gatewayAuthPayload) {
318
- await this.fileSystem.writeFile(gatewayAuthPath, `${JSON.stringify(gatewayAuthPayload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
319
- gatewayAuthWritten = true;
332
+ if (gatewayCredential) {
333
+ // Replaced atomically: a CLI invocation left over from an earlier turn must read either
334
+ // the old credential or the new one, never a missing or half-written file, because the
335
+ // documented correction for a rotated token is a 401 and not a usage error. The staging
336
+ // name is itself a credential sidecar, so a crashed run's leftover is swept above.
337
+ const stagingCredentialPath = join(directoryPath, `.borgee-agent-gateway.${randomUUID()}.json`);
338
+ await this.fileSystem.writeFile(stagingCredentialPath, `${JSON.stringify(gatewayCredential, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
339
+ await this.fileSystem.rename(stagingCredentialPath, gatewayCredentialPath);
340
+ gatewayCredentialWritten = true;
320
341
  }
321
342
  else {
322
- await this.fileSystem.unlink(gatewayAuthPath).catch((error) => {
323
- if (error.code !== 'ENOENT') {
324
- throw error;
325
- }
326
- });
327
343
  this.localhostGateway?.clearChannel(input.channelId);
328
344
  }
329
345
  if (issuedLocalhostGateway) {
@@ -336,7 +352,7 @@ export class FileChannelContextStore {
336
352
  directoryPath,
337
353
  payload,
338
354
  payloadPath: payloadWritten ? payloadPath : undefined,
339
- gatewayAuthPath: gatewayAuthWritten ? gatewayAuthPath : undefined,
355
+ gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
340
356
  skillRuntime: payloadWritten ? skillRuntime : undefined,
341
357
  localhostGateway: payloadWritten ? localhostGateway : undefined,
342
358
  taskWorkspace: taskWorkspaceMaterialized ? taskWorkspace : undefined,
@@ -0,0 +1 @@
1
+ export declare const MAIN_SESSION_DELEGATION_LINES: readonly string[];
@@ -0,0 +1,6 @@
1
+ export const MAIN_SESSION_DELEGATION_LINES = [
2
+ 'This session is your coordination surface: read the channel, answer the human, break work down, and keep task state current here.',
3
+ '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.',
4
+ 'Delegation changes who executes, not who owns: the work still belongs to this thread, and you still report the result here yourself.',
5
+ 'Stay responsive while delegated work runs, so a new incoming message never waits behind a long local operation.',
6
+ ];
@@ -2,6 +2,8 @@ import { AWAITING_USER_CONTROL_PREFIX } from '../providers/awaiting-user.js';
2
2
  import { buildAttentionSummaryLines } from './attention.js';
3
3
  import { buildCollaborationCapabilityDeclarationSummaryLines, buildMissedCollaborationDiagnosticSummaryLines, } from './collaboration-capabilities-diagnostics.js';
4
4
  import { buildCollaborationOutcomeSummaryLines } from './collaboration-outcome.js';
5
+ import { MAIN_SESSION_DELEGATION_LINES } from './main-session-delegation.js';
6
+ import { buildSkillManualLines } from './skill-manual.js';
5
7
  import { buildTaskThreadCollaborationSummaryLines } from './task-thread-collaboration.js';
6
8
  function providerLabel(provider) {
7
9
  if (provider === 'copilot') {
@@ -12,50 +14,39 @@ function providerLabel(provider) {
12
14
  }
13
15
  return 'Claude';
14
16
  }
15
- function buildSkillRuntimePromptLines(context) {
16
- if (!context?.skillRuntime) {
17
+ // Claude and Codex name the manual once on their session-scoped surfaces. Copilot has no
18
+ // session-scoped surface — this prompt is its only model-facing text — so for Copilot alone the
19
+ // pointer rides every turn.
20
+ function buildSkillManualPromptLines(provider, context) {
21
+ if (provider !== 'copilot' || !context?.skillRuntime || !context.gatewayCredentialPath) {
17
22
  return [];
18
23
  }
19
- return [
20
- '',
21
- 'Read-only local skill runtime bootstrap is available for this turn.',
22
- `Channel context payload: ${context.channelContextPayloadPath}`,
23
- `Skill guide: ${context.skillRuntime.skillMarkdownPath}`,
24
- `Node CLI: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --print-bootstrap`,
25
- `Python CLI: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --print-bootstrap`,
26
- 'These CLIs are local-only, may only access the loopback gateway described below, must not mutate files or the local environment, and may only send auxiliary collaboration messages when that gateway surface is explicitly advertised below.',
27
- ];
24
+ return ['', ...buildSkillManualLines(context.skillRuntime)];
28
25
  }
26
+ // Only the facts that change from turn to turn: which credential file authorizes this turn, which
27
+ // task grammar the thread shape licenses, and whether the collaboration commands can be reached at
28
+ // all. Everything else a model needs to invoke the CLI is in the manual, which does not change.
29
29
  function buildLocalhostGatewayPromptLines(context) {
30
30
  if (!context?.localhostGateway) {
31
31
  return [];
32
32
  }
33
- if (!context.gatewayAuthPath) {
33
+ if (!context.gatewayCredentialPath) {
34
34
  return [];
35
35
  }
36
- const lines = [
37
- '',
38
- 'A loopback-only localhost gateway is available for this turn.',
39
- 'Use the packaged local CLI with the existing --context payload to access the documented gateway surface.',
40
- `Gateway auth sidecar for this turn: ${context.gatewayAuthPath}`,
41
- ];
42
- const isTaskAssignmentThread = context.taskAssignmentContext?.active === true;
36
+ const lines = ['', `Gateway credential file for this turn: ${context.gatewayCredentialPath}`];
43
37
  if (!context.skillRuntime) {
44
38
  return lines;
45
39
  }
46
- lines.push('The read-only gateway commands listed in this prompt are already authorized for this turn and may be executed directly.', 'Do not ask the user for permission before using the read-only gateway commands listed below.', 'If the user asks about channel history, visible participants, or your current agent identity, run the relevant read command first and answer from its result instead of speculating about authorization.', `Node gateway checks: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --health`, `Node channel bootstrap: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-bootstrap`, `Node agent identity: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-me`, `Node channel history: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-history --limit 20`, `Python gateway checks: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --health`, `Python channel bootstrap: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-bootstrap`, `Python agent identity: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-me`, `Python channel history: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-history --limit 20`);
47
- if (isTaskAssignmentThread) {
48
- lines.push('Task-collection commands remain disabled inside this task thread; use the parent channel for create/list task operations.', `Node get current thread task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task`, `Node update current thread task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`, `Python get current thread task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task`, `Python update current thread task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`);
49
- }
50
- else {
51
- lines.push(`Node create task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --create-task --title "<title>" [--description "<description>"] [--assignee-id "<assignee-id>"]`, `Node list tasks: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-tasks`, `Node get task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task --task-id "<task-id>"`, `Node read task thread history: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-task-history --task-id "<task-id>" --limit 20`, `Node update task: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task --task-id "<task-id>" [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`, `Python create task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --create-task --title "<title>" [--description "<description>"] [--assignee-id "<assignee-id>"]`, `Python list tasks: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-tasks`, `Python get task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --get-task --task-id "<task-id>"`, `Python read task thread history: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --read-task-history --task-id "<task-id>" --limit 20`, `Python update task: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --update-task --task-id "<task-id>" [--status "<status>"] [--assignee-id "<assignee-id>"] [--title "<title>"]`);
40
+ lines.push(context.taskAssignmentContext?.active === true
41
+ ? 'This turn runs inside a task assignment thread.'
42
+ : 'This turn runs in a parent channel, not a task thread.');
43
+ const collaborationLive = context.localhostGateway.collaboration?.enabled === true
44
+ && (context.collaborationTurnMode ?? 'ordinary') === 'ordinary';
45
+ if (!collaborationLive) {
46
+ lines.push('Collaboration commands are not available this turn.');
52
47
  }
53
- if (context.localhostGateway.collaboration?.enabled &&
54
- (context.collaborationTurnMode ?? 'ordinary') === 'ordinary') {
55
- const turnExecutionArgument = context.collaborationTurnExecutionId
56
- ? ` --turn-execution-id ${context.collaborationTurnExecutionId}`
57
- : '';
58
- lines.push('Auxiliary collaboration commands are available for this turn. Use them only for short targeted escalation, mention, or reply notices, never for the main final answer body.', 'When you need the host-private in-flight draft for this live collaboration turn, use the dedicated read-draft command rather than inspecting public channel messages.', 'When --list-users is listed below, it is a read-only command for this turn and may be executed directly without asking for permission.', 'When the user explicitly asks you to mention, ping, notify, or send a short note to another visible participant, use the dedicated send-mention command directly and include a visible <@targetId> token in the body.', 'If the user asks you to @ another agent now, send the short auxiliary mention first, then confirm what you sent.', 'If the user says "the other agent" and only one other visible agent is present, resolve that target from --list-users and then use --send-mention with that participant id.', 'If collaboration with another visible agent would help, use these auxiliary commands to send a short targeted mention or reply-thread note yourself rather than asking AgentsHost to orchestrate a special protocol.', `Node private draft snapshot: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --read-draft`, `Node user directory: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-users`, `Node auxiliary mention: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-mention user-id --body "Need review from <@user-id>"`, `Node reply thread notice: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-message --body "Following up here" --reply-to message-id`, `Python private draft snapshot: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --read-draft`, `Python user directory: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath} --list-users`, `Python auxiliary mention: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-mention user-id --body "Need review from <@user-id>"`, `Python reply thread notice: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --auth-path ${context.gatewayAuthPath}${turnExecutionArgument} --send-message --body "Following up here" --reply-to message-id`);
48
+ else if (context.collaborationTurnExecutionId) {
49
+ lines.push(`Pass --turn-execution-id ${context.collaborationTurnExecutionId} on send, mention and draft.`);
59
50
  }
60
51
  return lines;
61
52
  }
@@ -76,7 +67,6 @@ function buildTaskAssignmentPromptLines(params) {
76
67
  ];
77
68
  if (taskAssignmentContext?.currentTaskId) {
78
69
  lines.push(`Current assigned task id: ${taskAssignmentContext.currentTaskId}.`);
79
- lines.push('Inside this task thread, the packaged Node and Python CLIs may omit --task-id for --get-task and --update-task because the current task id is already persisted in the injected thread context.');
80
70
  if (params.promptContext?.taskWorkspace) {
81
71
  lines.push(`Task-scoped writable workspace root: ${params.promptContext.taskWorkspace.rootPath}.`);
82
72
  lines.push('This writable workspace is local to agents-host for the current task thread. It does not imply that the target repository has already been checked out there.');
@@ -84,9 +74,8 @@ function buildTaskAssignmentPromptLines(params) {
84
74
  }
85
75
  else if (taskAssignmentContext?.active === true) {
86
76
  lines.push('No current task id is persisted in the injected thread context for this task thread.');
87
- lines.push('Inside this task thread, shorthand --get-task/--update-task calls without --task-id use an agents-host local fallback that scans visible parent-channel tasks for this thread; pass --task-id explicitly if that fallback cannot resolve a unique task.');
88
77
  }
89
- lines.push('Set the task status to in_progress when you start.', 'When you finish, set the task status to in_review and return your normal final response in this thread.', 'Do not use an auxiliary message command for the main completion report; reserve it for intentional targeted escalation or cross-channel notification.', 'Do not use create/list task operations inside this task thread; those remain parent-channel operations.', 'Do not move the main work back to the parent channel.');
78
+ lines.push('Return your normal final response in this thread, and do not move the main work back to the parent channel.');
90
79
  return lines;
91
80
  }
92
81
  function buildInboundMetadataLines(params) {
@@ -221,6 +210,7 @@ export function buildPrompt(params) {
221
210
  'Be concise, helpful, and honest about uncertainty.',
222
211
  'Do not claim to have performed actions you did not actually perform.',
223
212
  'Keep the visible reply text concise.',
213
+ ...MAIN_SESSION_DELEGATION_LINES,
224
214
  ...buildTurnControlPromptLines(params.promptContext),
225
215
  `If the user asks who you are, what powers you, or which backend/provider you use, mention that you are currently running on ${providerLabel(params.provider)}.`,
226
216
  `Channel: ${params.channelId}`,
@@ -245,7 +235,7 @@ export function buildPrompt(params) {
245
235
  const lines = buildMissedCollaborationDiagnosticSummaryLines(params.promptContext?.missedCollaborationDiagnostic);
246
236
  return lines.length > 0 ? ['', ...lines] : [];
247
237
  })(),
248
- ...buildSkillRuntimePromptLines(params.promptContext),
238
+ ...buildSkillManualPromptLines(params.provider, params.promptContext),
249
239
  ...buildLocalhostGatewayPromptLines(params.promptContext),
250
240
  '',
251
241
  `New message from ${params.incomingAuthorId}:`,
@@ -0,0 +1,13 @@
1
+ import type { SkillRuntimeBootstrapMetadata } from '../types.js';
2
+ /**
3
+ * What every model-facing surface says about the packaged CLI: what it is, where its manual is, and
4
+ * that the manual has to be opened before acting. Nothing else about the CLI is stated on a host
5
+ * surface — the manual is self-contained, so the invocation grammar, the command inventory, the
6
+ * limits and the credential-file rules have one home that cannot drift from the implementations.
7
+ *
8
+ * The authorization sentence is written against the credential file rather than against "this turn"
9
+ * because the surfaces disagree about scope: the per-turn prompt is turn-scoped while Claude's
10
+ * session append and Codex's projected document are session-scoped, and only the per-turn prompt
11
+ * knows whether a credential file exists.
12
+ */
13
+ export declare function buildSkillManualLines(skillRuntime: SkillRuntimeBootstrapMetadata): string[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What every model-facing surface says about the packaged CLI: what it is, where its manual is, and
3
+ * that the manual has to be opened before acting. Nothing else about the CLI is stated on a host
4
+ * surface — the manual is self-contained, so the invocation grammar, the command inventory, the
5
+ * limits and the credential-file rules have one home that cannot drift from the implementations.
6
+ *
7
+ * The authorization sentence is written against the credential file rather than against "this turn"
8
+ * because the surfaces disagree about scope: the per-turn prompt is turn-scoped while Claude's
9
+ * session append and Codex's projected document are session-scoped, and only the per-turn prompt
10
+ * knows whether a credential file exists.
11
+ */
12
+ export function buildSkillManualLines(skillRuntime) {
13
+ return [
14
+ "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.`,
16
+ '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
+ ];
18
+ }
@@ -1,4 +1,5 @@
1
1
  import { HostLogger, summarizeError } from '../debug.js';
2
+ import { buildHostedIncomingContentText, buildHostedTurnContentParts } from '../hosted-turn-content.js';
2
3
  import { ChannelContextPreparationError, } from './injection.js';
3
4
  import { buildPrompt } from './prompt.js';
4
5
  function providerLabel(provider) {
@@ -24,7 +25,7 @@ function toPromptContext(channelContext, input) {
24
25
  ...(channelContext
25
26
  ? {
26
27
  channelContextPayloadPath: channelContext.payloadPath,
27
- gatewayAuthPath: channelContext.gatewayAuthPath,
28
+ gatewayCredentialPath: channelContext.gatewayCredentialPath,
28
29
  skillRuntime: channelContext.skillRuntime,
29
30
  localhostGateway: channelContext.localhostGateway,
30
31
  taskAssignmentContext: channelContext.payload.taskAssignmentContext,
@@ -75,6 +76,12 @@ export class ProviderTurnPreparer {
75
76
  this.logger = logger;
76
77
  }
77
78
  async prepare(input) {
79
+ const incomingParts = input.incomingParts ?? buildHostedTurnContentParts({
80
+ text: input.incomingContent,
81
+ });
82
+ const incomingContent = incomingParts.length > 0
83
+ ? buildHostedIncomingContentText(incomingParts)
84
+ : input.incomingContent;
78
85
  let channelContext;
79
86
  if (this.channelContextStore) {
80
87
  try {
@@ -97,8 +104,8 @@ export class ProviderTurnPreparer {
97
104
  ...(input.incomingMessageType !== undefined
98
105
  ? { incomingMessageType: input.incomingMessageType }
99
106
  : {}),
100
- ...(input.incomingMessageType !== undefined && input.incomingContent !== undefined
101
- ? { incomingContent: input.incomingContent }
107
+ ...(input.incomingMessageType !== undefined && incomingContent !== undefined
108
+ ? { incomingContent }
102
109
  : {}),
103
110
  };
104
111
  channelContext = await this.channelContextStore.prepare(prepareInput);
@@ -116,6 +123,8 @@ export class ProviderTurnPreparer {
116
123
  const promptContext = toPromptContext(channelContext, input);
117
124
  return {
118
125
  channelId: input.channelId,
126
+ incomingContent,
127
+ incomingParts,
119
128
  incomingEventKind: input.incomingEventKind,
120
129
  incomingMessageType: input.incomingMessageType,
121
130
  prompt: buildPrompt({
@@ -123,7 +132,7 @@ export class ProviderTurnPreparer {
123
132
  provider: input.provider,
124
133
  channelId: input.channelId,
125
134
  incomingAuthorId: input.incomingAuthorId,
126
- incomingContent: input.incomingContent,
135
+ incomingContent,
127
136
  incomingEventKind: input.incomingEventKind,
128
137
  incomingMessageType: input.incomingMessageType,
129
138
  promptContext,