@virtengine/openfleet 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,827 @@
1
+ /**
2
+ * copilot-shell.mjs — Persistent Copilot SDK agent for openfleet.
3
+ *
4
+ * Uses the GitHub Copilot SDK (@github/copilot-sdk) to maintain a persistent
5
+ * session with multi-turn conversation, tool use (shell, file I/O, MCP), and
6
+ * streaming. Designed as a drop-in primary agent when Copilot is configured
7
+ * as the primary executor.
8
+ */
9
+
10
+ import { existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
11
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
12
+ import { resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { execSync } from "node:child_process";
15
+ import { resolveRepoRoot } from "./repo-root.mjs";
16
+
17
+ const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
18
+
19
+ // ── Configuration ────────────────────────────────────────────────────────────
20
+
21
+ const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000; // 60 min for agentic tasks
22
+ const STATE_FILE = resolve(__dirname, "logs", "copilot-shell-state.json");
23
+ const SESSION_LOG_DIR = resolve(__dirname, "logs", "copilot-sessions");
24
+ const REPO_ROOT = resolveRepoRoot();
25
+
26
+ // Valid reasoning effort levels for models that support it
27
+ const VALID_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
28
+
29
+ // ── State ────────────────────────────────────────────────────────────────────
30
+
31
+ let CopilotClientClass = null; // CopilotClient class from SDK
32
+ let copilotClient = null;
33
+ let clientStarted = false;
34
+ let activeSession = null;
35
+ let activeSessionId = null;
36
+ let activeTurn = false;
37
+ let turnCount = 0;
38
+ let workspacePath = null;
39
+
40
+ function envFlagEnabled(value) {
41
+ const raw = String(value ?? "")
42
+ .trim()
43
+ .toLowerCase();
44
+ return ["1", "true", "yes", "on", "y"].includes(raw);
45
+ }
46
+
47
+ function resolveCopilotTransport() {
48
+ const raw = String(process.env.COPILOT_TRANSPORT || "auto")
49
+ .trim()
50
+ .toLowerCase();
51
+ if (["auto", "sdk", "cli", "url"].includes(raw)) {
52
+ return raw;
53
+ }
54
+ console.warn(
55
+ `[copilot-shell] invalid COPILOT_TRANSPORT='${raw}', defaulting to 'auto'`,
56
+ );
57
+ return "auto";
58
+ }
59
+
60
+ function normalizeProfileKey(name) {
61
+ return String(name || "")
62
+ .trim()
63
+ .toUpperCase()
64
+ .replace(/[^A-Z0-9]/g, "_");
65
+ }
66
+
67
+ function parseMcpServersValue(raw) {
68
+ if (!raw) return null;
69
+ const parsed = safeJsonParse(raw);
70
+ if (!parsed) return null;
71
+ if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
72
+ return parsed.mcpServers;
73
+ }
74
+ if (parsed["github.copilot.mcpServers"]) {
75
+ return parsed["github.copilot.mcpServers"];
76
+ }
77
+ if (parsed.mcp && parsed.mcp.servers && typeof parsed.mcp.servers === "object") {
78
+ return parsed.mcp.servers;
79
+ }
80
+ if (typeof parsed === "object") return parsed;
81
+ return null;
82
+ }
83
+
84
+ function resolveCopilotProfile(env = process.env) {
85
+ const name = String(env.COPILOT_PROFILE || "").trim();
86
+ if (!name) {
87
+ return { name: "", model: "", reasoningEffort: "", mcpConfig: "", mcpServers: null };
88
+ }
89
+ const key = normalizeProfileKey(name);
90
+ const prefix = `COPILOT_PROFILE_${key}_`;
91
+ const model = String(env[`${prefix}MODEL`] || "").trim();
92
+ const reasoningEffort = String(env[`${prefix}REASONING_EFFORT`] || "").trim();
93
+ const mcpConfig = String(env[`${prefix}MCP_CONFIG`] || "").trim();
94
+ const mcpServers = parseMcpServersValue(env[`${prefix}MCP_SERVERS`]);
95
+ return {
96
+ name,
97
+ model,
98
+ reasoningEffort,
99
+ mcpConfig,
100
+ mcpServers,
101
+ };
102
+ }
103
+
104
+ // ── Helpers ──────────────────────────────────────────────────────────────────
105
+
106
+ function timestamp() {
107
+ return new Date().toISOString();
108
+ }
109
+
110
+ function safeJsonParse(raw) {
111
+ try {
112
+ return JSON.parse(raw);
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function safeStringify(value, maxLen = 8000) {
119
+ let text = "";
120
+ try {
121
+ text = JSON.stringify(value);
122
+ } catch {
123
+ text = String(value);
124
+ }
125
+ if (text.length > maxLen) {
126
+ text = text.slice(0, maxLen) + "...";
127
+ }
128
+ return text;
129
+ }
130
+
131
+ function initSessionLog(sessionId, prompt, timeoutMs) {
132
+ if (!sessionId) return null;
133
+ try {
134
+ mkdirSync(SESSION_LOG_DIR, { recursive: true });
135
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
136
+ const shortId = sessionId.slice(0, 8);
137
+ const logPath = resolve(
138
+ SESSION_LOG_DIR,
139
+ `copilot-session-${stamp}-${shortId}.log`,
140
+ );
141
+ const header = [
142
+ "# Copilot Session Log",
143
+ `# Timestamp: ${timestamp()}`,
144
+ `# Session ID: ${sessionId}`,
145
+ `# Timeout: ${timeoutMs}ms`,
146
+ `# Prompt: ${(prompt || "").slice(0, 500)}`,
147
+ "",
148
+ ].join("\n");
149
+ appendFileSync(logPath, header + "\n");
150
+ return logPath;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ function logSessionEvent(logPath, event) {
157
+ if (!logPath || !event) return;
158
+ try {
159
+ const payload = safeStringify(event);
160
+ appendFileSync(
161
+ logPath,
162
+ `${timestamp()} ${event.type || "event"} ${payload}\n`,
163
+ );
164
+ } catch {
165
+ /* best effort */
166
+ }
167
+ }
168
+
169
+ // ── CLI Args & Handlers ──────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Build CLI arguments for the Copilot subprocess.
173
+ * Enables experimental features (fleet, autopilot), auto-permissions,
174
+ * sub-agents, and autonomy.
175
+ */
176
+ function buildCliArgs() {
177
+ const args = [];
178
+
179
+ // Always enable experimental features (fleet, autopilot, persisted permissions, etc.)
180
+ if (!envFlagEnabled(process.env.COPILOT_NO_EXPERIMENTAL)) {
181
+ args.push("--experimental");
182
+ }
183
+
184
+ // Auto-approve all permissions (tools, paths, URLs) — equivalent to /allow-all or /yolo
185
+ if (!envFlagEnabled(process.env.COPILOT_NO_ALLOW_ALL)) {
186
+ args.push("--allow-all");
187
+ }
188
+
189
+ // Disable ask_user tool — agent works fully autonomously
190
+ if (!envFlagEnabled(process.env.COPILOT_ENABLE_ASK_USER)) {
191
+ args.push("--no-ask-user");
192
+ }
193
+
194
+ // Prevent auto-update during SDK sessions (avoid mid-task restarts)
195
+ args.push("--no-auto-update");
196
+
197
+ // Enable all GitHub MCP tools if requested
198
+ if (envFlagEnabled(process.env.COPILOT_ENABLE_ALL_GITHUB_MCP_TOOLS)) {
199
+ args.push("--enable-all-github-mcp-tools");
200
+ }
201
+
202
+ // Disable built-in MCPs if custom ones are provided exclusively
203
+ if (envFlagEnabled(process.env.COPILOT_DISABLE_BUILTIN_MCPS)) {
204
+ args.push("--disable-builtin-mcps");
205
+ }
206
+
207
+ // Enable parallel tool execution for sub-agent-like concurrency
208
+ if (!envFlagEnabled(process.env.COPILOT_DISABLE_PARALLEL_TOOLS)) {
209
+ // Parallel is on by default; only disable if explicitly set
210
+ } else {
211
+ args.push("--disable-parallel-tools-execution");
212
+ }
213
+
214
+ // Add additional MCP config if available (for fleet/task sub-agent MCP servers)
215
+ const mcpConfigPath = process.env.COPILOT_ADDITIONAL_MCP_CONFIG;
216
+ if (mcpConfigPath) {
217
+ args.push("--additional-mcp-config", mcpConfigPath);
218
+ }
219
+
220
+ if (args.length > 0) {
221
+ console.log(`[copilot-shell] cliArgs: ${args.join(" ")}`);
222
+ }
223
+ return args;
224
+ }
225
+
226
+ /**
227
+ * Auto-approve all permission requests (shell, write, read, mcp, url).
228
+ * This is the SDK equivalent of --allow-all / --yolo.
229
+ */
230
+ function autoApprovePermissions(request, _invocation) {
231
+ return { kind: "approved", rules: [] };
232
+ }
233
+
234
+ /**
235
+ * Auto-respond to agent user-input requests.
236
+ * Provides sensible defaults so the agent never blocks waiting for human input.
237
+ */
238
+ function autoRespondToUserInput(request, _invocation) {
239
+ // If choices are provided, pick the first one
240
+ if (request.choices && request.choices.length > 0) {
241
+ console.log(
242
+ `[copilot-shell] auto-responding to "${request.question}" with choice: "${request.choices[0]}"`,
243
+ );
244
+ return { answer: request.choices[0], wasFreeform: false };
245
+ }
246
+ // For y/n style questions, default to "yes"
247
+ const question = (request.question || "").toLowerCase();
248
+ if (/\b(y\/n|yes\/no|confirm|proceed|continue)\b/i.test(question)) {
249
+ console.log(
250
+ `[copilot-shell] auto-responding "yes" to: "${request.question}"`,
251
+ );
252
+ return { answer: "yes", wasFreeform: true };
253
+ }
254
+ // Generic: tell the agent to proceed with its best judgment
255
+ console.log(
256
+ `[copilot-shell] auto-responding to: "${request.question}"`,
257
+ );
258
+ return {
259
+ answer: "Proceed with your best judgment. Do not wait for human input.",
260
+ wasFreeform: true,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Session lifecycle hooks for observability and logging.
266
+ */
267
+ function buildSessionHooks() {
268
+ return {
269
+ onPreToolUse: async (input, _invocation) => {
270
+ // Auto-allow all tool executions
271
+ return { permissionDecision: "allow" };
272
+ },
273
+ onSessionStart: async (input, _invocation) => {
274
+ console.log(`[copilot-shell] session hook: started (source=${input.source})`);
275
+ return {};
276
+ },
277
+ onSessionEnd: async (input, _invocation) => {
278
+ console.log(
279
+ `[copilot-shell] session hook: ended (reason=${input.reason})`,
280
+ );
281
+ return {};
282
+ },
283
+ onErrorOccurred: async (input, _invocation) => {
284
+ console.error(
285
+ `[copilot-shell] session hook: error in ${input.errorContext}: ${input.error} (recoverable=${input.recoverable})`,
286
+ );
287
+ // Auto-retry recoverable errors
288
+ if (input.recoverable) {
289
+ return { errorHandling: "retry", retryCount: 2 };
290
+ }
291
+ return { errorHandling: "skip" };
292
+ },
293
+ };
294
+ }
295
+
296
+ // ── SDK Loading ──────────────────────────────────────────────────────────────
297
+
298
+ async function loadCopilotSdk() {
299
+ if (CopilotClientClass) return CopilotClientClass;
300
+ if (envFlagEnabled(process.env.COPILOT_SDK_DISABLED)) {
301
+ console.warn("[copilot-shell] SDK disabled via COPILOT_SDK_DISABLED");
302
+ return null;
303
+ }
304
+ try {
305
+ const mod = await import("@github/copilot-sdk");
306
+ CopilotClientClass =
307
+ mod.CopilotClient || mod.default?.CopilotClient || null;
308
+ if (!CopilotClientClass) {
309
+ throw new Error("CopilotClient export not found");
310
+ }
311
+ console.log("[copilot-shell] SDK loaded successfully");
312
+ return CopilotClientClass;
313
+ } catch (err) {
314
+ console.error(`[copilot-shell] failed to load SDK: ${err.message}`);
315
+ return null;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Detect GitHub token from multiple sources (auth passthrough).
321
+ * Priority: ENV > gh CLI > undefined (SDK will use default auth).
322
+ */
323
+ function detectGitHubToken() {
324
+ // 1. Direct token env vars (highest priority)
325
+ const envToken =
326
+ process.env.COPILOT_CLI_TOKEN ||
327
+ process.env.GITHUB_TOKEN ||
328
+ process.env.GH_TOKEN ||
329
+ process.env.GITHUB_PAT;
330
+ if (envToken) {
331
+ console.log("[copilot-shell] using token from environment");
332
+ return envToken;
333
+ }
334
+
335
+ // 2. Try to read from gh CLI auth
336
+ try {
337
+ execSync("gh auth status", { stdio: "pipe", encoding: "utf8" });
338
+ console.log("[copilot-shell] detected gh CLI authentication");
339
+ // gh CLI is authenticated - SDK will use it automatically
340
+ return undefined;
341
+ } catch {
342
+ // gh not authenticated or not installed
343
+ }
344
+
345
+ // 3. VS Code auth detection could be added here
346
+ // For now, return undefined to let SDK use default auth flow
347
+ console.log("[copilot-shell] no pre-auth detected, using SDK default auth");
348
+ return undefined;
349
+ }
350
+
351
+ const OPENAI_ENV_KEYS = [
352
+ "OPENAI_API_KEY",
353
+ "OPENAI_BASE_URL",
354
+ "OPENAI_ORGANIZATION",
355
+ "OPENAI_PROJECT",
356
+ ];
357
+
358
+ async function withSanitizedOpenAiEnv(fn) {
359
+ const saved = {};
360
+ for (const key of OPENAI_ENV_KEYS) {
361
+ if (Object.prototype.hasOwnProperty.call(process.env, key)) {
362
+ saved[key] = process.env[key];
363
+ delete process.env[key];
364
+ }
365
+ }
366
+ try {
367
+ return await fn();
368
+ } finally {
369
+ for (const [key, value] of Object.entries(saved)) {
370
+ if (value !== undefined) process.env[key] = value;
371
+ }
372
+ }
373
+ }
374
+
375
+ async function ensureClientStarted() {
376
+ if (clientStarted && copilotClient) return true;
377
+ const Cls = await loadCopilotSdk();
378
+ if (!Cls) return false;
379
+
380
+ // Auth passthrough: detect from multiple sources
381
+ const cliPath =
382
+ process.env.COPILOT_CLI_PATH ||
383
+ process.env.GITHUB_COPILOT_CLI_PATH ||
384
+ undefined;
385
+ const cliUrl = process.env.COPILOT_CLI_URL || undefined;
386
+ const token = detectGitHubToken();
387
+ const transport = resolveCopilotTransport();
388
+
389
+ // Session mode: "local" (default) uses stdio for full model access + MCP + sub-agents.
390
+ // "auto" lets the SDK decide. "url" connects to remote server (potentially limited).
391
+ const sessionMode = (process.env.COPILOT_SESSION_MODE || "local").trim().toLowerCase();
392
+
393
+ // Build cliArgs for experimental features, permissions, and autonomy
394
+ const cliArgs = buildCliArgs();
395
+
396
+ let clientOptions;
397
+ if (transport === "url") {
398
+ if (!cliUrl) {
399
+ console.warn(
400
+ "[copilot-shell] COPILOT_TRANSPORT=url requested but COPILOT_CLI_URL is unset; falling back to local",
401
+ );
402
+ clientOptions = { cliPath, token, cliArgs, useStdio: true, cwd: REPO_ROOT };
403
+ } else {
404
+ clientOptions = { cliUrl };
405
+ }
406
+ } else if (transport === "cli") {
407
+ clientOptions = { cliPath: cliPath || "copilot", token, cliArgs, useStdio: true, cwd: REPO_ROOT };
408
+ } else if (transport === "sdk") {
409
+ clientOptions = token
410
+ ? { token, cliArgs, useStdio: true, cwd: REPO_ROOT }
411
+ : { cliArgs, useStdio: true, cwd: REPO_ROOT };
412
+ } else {
413
+ // "auto" transport — use cliUrl if provided, otherwise local stdio
414
+ if (cliUrl && sessionMode !== "local") {
415
+ clientOptions = { cliUrl };
416
+ } else {
417
+ clientOptions = {
418
+ cliPath,
419
+ token,
420
+ cliArgs,
421
+ useStdio: true,
422
+ cwd: REPO_ROOT,
423
+ };
424
+ }
425
+ }
426
+
427
+ const modeLabel = clientOptions.cliUrl ? "remote" : "local (stdio)";
428
+ console.log(`[copilot-shell] starting client in ${modeLabel} mode`);
429
+
430
+ await withSanitizedOpenAiEnv(async () => {
431
+ copilotClient = new Cls(clientOptions);
432
+ await copilotClient.start();
433
+ });
434
+ clientStarted = true;
435
+ console.log("[copilot-shell] client started");
436
+ return true;
437
+ }
438
+
439
+ // ── State Persistence ────────────────────────────────────────────────────────
440
+
441
+ async function loadState() {
442
+ try {
443
+ const raw = await readFile(STATE_FILE, "utf8");
444
+ const data = JSON.parse(raw);
445
+ activeSessionId = data.sessionId || null;
446
+ turnCount = data.turnCount || 0;
447
+ workspacePath = data.workspacePath || null;
448
+ console.log(
449
+ `[copilot-shell] loaded state: sessionId=${activeSessionId}, turns=${turnCount}`,
450
+ );
451
+ } catch {
452
+ activeSessionId = null;
453
+ turnCount = 0;
454
+ workspacePath = null;
455
+ }
456
+ }
457
+
458
+ async function saveState() {
459
+ try {
460
+ await mkdir(resolve(__dirname, "logs"), { recursive: true });
461
+ await writeFile(
462
+ STATE_FILE,
463
+ JSON.stringify(
464
+ {
465
+ sessionId: activeSessionId,
466
+ turnCount,
467
+ workspacePath,
468
+ updatedAt: timestamp(),
469
+ },
470
+ null,
471
+ 2,
472
+ ),
473
+ "utf8",
474
+ );
475
+ } catch (err) {
476
+ console.warn(`[copilot-shell] failed to save state: ${err.message}`);
477
+ }
478
+ }
479
+
480
+ // ── System Prompt ────────────────────────────────────────────────────────────
481
+
482
+ const SYSTEM_PROMPT = `# AGENT DIRECTIVE — EXECUTE IMMEDIATELY
483
+
484
+ You are an autonomous AI coding agent deployed inside openfleet.
485
+ You are NOT a chatbot. You are NOT waiting for input. You EXECUTE tasks.
486
+
487
+ CRITICAL RULES:
488
+ 1. NEVER respond with "Ready" or "What would you like me to do?" — you already have your task below.
489
+ 2. NEVER ask clarifying questions — infer intent and take action.
490
+ 3. DO the work. Read files, run commands, analyze code, write output.
491
+ 4. Show your work as you go — print what you're reading, what you found, what you're doing next.
492
+ 5. Produce DETAILED, STRUCTURED output with your findings and actions taken.
493
+ 6. If the task involves analysis, actually READ the files and show what you found.
494
+ 7. If the task involves code changes, actually MAKE the changes.
495
+ 8. Think step-by-step, show your reasoning, then act.
496
+
497
+ You have FULL ACCESS to:
498
+ - The target repository checked out for this openfleet instance
499
+ - Shell: git, gh, node, go, make, and all system commands (pwsh optional)
500
+ - File read/write: read any file, create/edit any file
501
+ - MCP servers configured in this environment (availability varies)
502
+ - Subagents and VS Code tools when available
503
+
504
+ Key files:
505
+ ${REPO_ROOT} — Repository root
506
+ .cache/ve-orchestrator-status.json — Live status data (if enabled)
507
+ scripts/openfleet/logs/ — Monitor logs (if available)
508
+ AGENTS.md — Repo guide for agents
509
+ `;
510
+
511
+ // ── MCP / Tool Config ────────────────────────────────────────────────────────
512
+
513
+ function loadMcpServersFromFile(path) {
514
+ if (!path || !existsSync(path)) return null;
515
+ const raw = readFileSync(path, "utf8");
516
+ const parsed = safeJsonParse(raw);
517
+ if (!parsed) return null;
518
+ if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
519
+ return parsed.mcpServers;
520
+ }
521
+ if (
522
+ parsed.mcp &&
523
+ parsed.mcp.servers &&
524
+ typeof parsed.mcp.servers === "object"
525
+ ) {
526
+ return parsed.mcp.servers;
527
+ }
528
+ if (parsed["github.copilot.mcpServers"]) {
529
+ return parsed["github.copilot.mcpServers"];
530
+ }
531
+ return null;
532
+ }
533
+
534
+ function loadMcpServers(profile = null) {
535
+ if (profile?.mcpServers) return profile.mcpServers;
536
+ if (profile?.mcpConfig) {
537
+ return loadMcpServersFromFile(profile.mcpConfig);
538
+ }
539
+ if (process.env.COPILOT_MCP_SERVERS) {
540
+ const parsed = parseMcpServersValue(process.env.COPILOT_MCP_SERVERS);
541
+ if (parsed) return parsed;
542
+ }
543
+ const configPath =
544
+ process.env.COPILOT_MCP_CONFIG || resolve(REPO_ROOT, ".vscode", "mcp.json");
545
+ return loadMcpServersFromFile(configPath);
546
+ }
547
+
548
+ function buildSessionConfig() {
549
+ const profile = resolveCopilotProfile();
550
+ const config = {
551
+ streaming: true,
552
+ systemMessage: {
553
+ mode: "replace",
554
+ content: SYSTEM_PROMPT,
555
+ },
556
+ infiniteSessions: { enabled: true },
557
+ // Auto-approve all permissions (belt-and-suspenders with --allow-all cliArg)
558
+ onPermissionRequest: autoApprovePermissions,
559
+ // Auto-respond to agent questions (belt-and-suspenders with --no-ask-user)
560
+ onUserInputRequest: autoRespondToUserInput,
561
+ // Session lifecycle hooks for observability
562
+ hooks: buildSessionHooks(),
563
+ // Set working directory to repo root
564
+ workingDirectory: REPO_ROOT,
565
+ };
566
+ const model =
567
+ profile.model ||
568
+ process.env.COPILOT_MODEL ||
569
+ process.env.COPILOT_SDK_MODEL ||
570
+ "";
571
+ if (model) config.model = model;
572
+
573
+ // Reasoning effort: low | medium | high | xhigh
574
+ const effort =
575
+ profile.reasoningEffort ||
576
+ process.env.COPILOT_REASONING_EFFORT ||
577
+ process.env.COPILOT_SDK_REASONING_EFFORT ||
578
+ "";
579
+ if (effort && VALID_REASONING_EFFORTS.includes(effort.toLowerCase())) {
580
+ config.reasoningEffort = effort.toLowerCase();
581
+ }
582
+
583
+ const mcpServers = loadMcpServers(profile);
584
+ if (mcpServers) config.mcpServers = mcpServers;
585
+ return config;
586
+ }
587
+
588
+ // ── Session Management ───────────────────────────────────────────────────────
589
+
590
+ async function getSession() {
591
+ if (activeSession) return activeSession;
592
+ const started = await ensureClientStarted();
593
+ if (!started) throw new Error("Copilot SDK not available");
594
+
595
+ const config = buildSessionConfig();
596
+
597
+ if (activeSessionId && typeof copilotClient?.resumeSession === "function") {
598
+ try {
599
+ activeSession = await copilotClient.resumeSession(
600
+ activeSessionId,
601
+ config,
602
+ );
603
+ workspacePath = activeSession?.workspacePath || workspacePath;
604
+ console.log(`[copilot-shell] resumed session ${activeSessionId}`);
605
+ return activeSession;
606
+ } catch (err) {
607
+ console.warn(
608
+ `[copilot-shell] failed to resume session ${activeSessionId}: ${err.message} — starting fresh`,
609
+ );
610
+ activeSessionId = null;
611
+ }
612
+ }
613
+
614
+ activeSession = await copilotClient.createSession(config);
615
+ activeSessionId =
616
+ activeSession?.sessionId || activeSession?.id || activeSessionId;
617
+ workspacePath = activeSession?.workspacePath || workspacePath;
618
+ await saveState();
619
+ console.log(`[copilot-shell] new session started: ${activeSessionId}`);
620
+ return activeSession;
621
+ }
622
+
623
+ // ── Main Execution ───────────────────────────────────────────────────────────
624
+
625
+ export async function execCopilotPrompt(userMessage, options = {}) {
626
+ const {
627
+ onEvent = null,
628
+ statusData = null,
629
+ timeoutMs = DEFAULT_TIMEOUT_MS,
630
+ sendRawEvents = false,
631
+ abortController = null,
632
+ persistent = false,
633
+ } = options;
634
+
635
+ if (activeTurn) {
636
+ return {
637
+ finalResponse:
638
+ "⏳ Agent is still executing a previous task. Please wait.",
639
+ items: [],
640
+ usage: null,
641
+ };
642
+ }
643
+
644
+ activeTurn = true;
645
+
646
+ if (!persistent) {
647
+ // Task executor path — fresh session each call
648
+ activeSession = null;
649
+ }
650
+
651
+ let unsubscribe = null;
652
+ const session = await getSession();
653
+ const logPath = initSessionLog(activeSessionId, userMessage, timeoutMs);
654
+ const items = [];
655
+ let finalResponse = "";
656
+ let responseFromMessage = false;
657
+
658
+ const handleEvent = async (event) => {
659
+ if (!event) return;
660
+ logSessionEvent(logPath, event);
661
+ items.push(event);
662
+ if (event.type === "assistant.message" && event.data?.content) {
663
+ finalResponse = event.data.content;
664
+ responseFromMessage = true;
665
+ }
666
+ if (
667
+ !responseFromMessage &&
668
+ event.type === "assistant.message_delta" &&
669
+ event.data?.deltaContent
670
+ ) {
671
+ finalResponse += event.data.deltaContent;
672
+ }
673
+ if (event.type === "session.idle") {
674
+ turnCount += 1;
675
+ await saveState();
676
+ }
677
+ if (onEvent) {
678
+ try {
679
+ if (sendRawEvents) {
680
+ await onEvent(null, event);
681
+ } else {
682
+ await onEvent(null);
683
+ }
684
+ } catch {
685
+ /* best effort */
686
+ }
687
+ }
688
+ };
689
+
690
+ try {
691
+ if (typeof session.on === "function") {
692
+ unsubscribe = session.on(handleEvent);
693
+ }
694
+
695
+ const controller = abortController || new AbortController();
696
+ const timer = setTimeout(() => controller.abort("timeout"), timeoutMs);
697
+
698
+ const onAbort = () => {
699
+ const reason = controller.signal.reason || "user_stop";
700
+ if (typeof session.abort === "function") session.abort(reason);
701
+ if (typeof session.cancel === "function") session.cancel(reason);
702
+ if (typeof session.stop === "function") session.stop(reason);
703
+ };
704
+ if (controller?.signal) {
705
+ controller.signal.addEventListener("abort", onAbort, { once: true });
706
+ }
707
+
708
+ // Build prompt with optional orchestrator status
709
+ let prompt = userMessage;
710
+ if (statusData) {
711
+ const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
712
+ prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
713
+ } else {
714
+ prompt = `# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
715
+ }
716
+
717
+ const sendFn = session.sendAndWait || session.send;
718
+ if (typeof sendFn !== "function") {
719
+ throw new Error("Copilot SDK session does not support send");
720
+ }
721
+
722
+ // Pass timeout parameter to sendAndWait to override 60s SDK default
723
+ const sendPromise = session.sendAndWait
724
+ ? sendFn.call(session, { prompt }, timeoutMs)
725
+ : sendFn.call(session, { prompt });
726
+
727
+ // If send() returns before idle, wait for session.idle if available
728
+ if (!session.sendAndWait) {
729
+ await new Promise((resolve, reject) => {
730
+ const idleHandler = (event) => {
731
+ if (!event) return;
732
+ if (event.type === "session.idle") resolve();
733
+ if (event.type === "session.error") {
734
+ reject(new Error(event.data?.message || "session error"));
735
+ }
736
+ };
737
+ const off = session.on ? session.on(idleHandler) : null;
738
+ Promise.resolve(sendPromise).catch(reject);
739
+ setTimeout(resolve, timeoutMs + 1000);
740
+ if (typeof off === "function") {
741
+ setTimeout(() => off(), timeoutMs + 2000);
742
+ }
743
+ });
744
+ } else {
745
+ await sendPromise;
746
+ }
747
+
748
+ clearTimeout(timer);
749
+ controller.signal?.removeEventListener("abort", onAbort);
750
+
751
+ return {
752
+ finalResponse:
753
+ finalResponse.trim() || "(Agent completed with no text output)",
754
+ items,
755
+ usage: null,
756
+ };
757
+ } catch (err) {
758
+ if (err?.name === "AbortError" || /abort|timeout/i.test(err?.message)) {
759
+ const reason = abortController?.signal?.reason || "timeout";
760
+ const msg =
761
+ reason === "user_stop"
762
+ ? "🛑 Agent stopped by user."
763
+ : `⏱️ Agent timed out after ${timeoutMs / 1000}s`;
764
+ return { finalResponse: msg, items: [], usage: null };
765
+ }
766
+ throw err;
767
+ } finally {
768
+ if (typeof unsubscribe === "function") {
769
+ try {
770
+ unsubscribe();
771
+ } catch {
772
+ /* best effort */
773
+ }
774
+ } else if (typeof session.off === "function") {
775
+ try {
776
+ session.off(handleEvent);
777
+ } catch {
778
+ /* best effort */
779
+ }
780
+ }
781
+ activeTurn = false;
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Copilot SDK does not currently expose steering APIs. We return unsupported.
787
+ */
788
+ export async function steerCopilotPrompt() {
789
+ return { ok: false, reason: "unsupported" };
790
+ }
791
+
792
+ export function isCopilotBusy() {
793
+ return !!activeTurn;
794
+ }
795
+
796
+ export function getSessionInfo() {
797
+ return {
798
+ sessionId: activeSessionId,
799
+ turnCount,
800
+ isActive: !!activeSession,
801
+ isBusy: !!activeTurn,
802
+ workspacePath,
803
+ };
804
+ }
805
+
806
+ export async function resetSession() {
807
+ activeSession = null;
808
+ activeSessionId = null;
809
+ workspacePath = null;
810
+ turnCount = 0;
811
+ activeTurn = false;
812
+ await saveState();
813
+ console.log("[copilot-shell] session reset");
814
+ }
815
+
816
+ export async function initCopilotShell() {
817
+ await loadState();
818
+ const started = await ensureClientStarted();
819
+ if (started) {
820
+ console.log("[copilot-shell] initialised with Copilot SDK");
821
+ return true;
822
+ }
823
+ console.warn(
824
+ "[copilot-shell] initialised WITHOUT Copilot SDK — agent will not work",
825
+ );
826
+ return false;
827
+ }