@app-vitals/agent-runtime 0.0.1 → 0.1.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 (160) hide show
  1. package/Dockerfile +265 -0
  2. package/agent-account-status-ref.ts +53 -0
  3. package/agent-account-status-ref.unit.test.ts +24 -0
  4. package/agent-extension.ts +144 -0
  5. package/agent-extension.unit.test.ts +139 -0
  6. package/agent-repos-ref.ts +63 -0
  7. package/agent-repos-ref.unit.test.ts +84 -0
  8. package/agent-slack-membership-ref.ts +94 -0
  9. package/agent-slack-membership-ref.unit.test.ts +201 -0
  10. package/agent-trial-expiry-ref.ts +63 -0
  11. package/agent-trial-expiry-ref.unit.test.ts +84 -0
  12. package/anthropic-federation-guard.ts +72 -0
  13. package/anthropic-federation-guard.unit.test.ts +127 -0
  14. package/base-image-contract.smoke.test.ts +149 -0
  15. package/boot.ts +845 -0
  16. package/browser.integration.test.ts +52 -0
  17. package/browser.ts +34 -0
  18. package/chat-poller.integration.test.ts +109 -0
  19. package/chat-poller.ts +401 -0
  20. package/chat-poller.unit.test.ts +1581 -0
  21. package/chat-token-reporter.integration.test.ts +282 -0
  22. package/chat-token-reporter.ts +136 -0
  23. package/claude-version.ts +55 -0
  24. package/claude-version.unit.test.ts +73 -0
  25. package/claude.ts +1054 -0
  26. package/claude.unit.test.ts +3042 -0
  27. package/cleanup-after-days-ref.ts +83 -0
  28. package/cleanup-after-days-ref.unit.test.ts +89 -0
  29. package/cleanup-merged-worktrees-ref.ts +75 -0
  30. package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
  31. package/cli-args.ts +36 -0
  32. package/cli-args.unit.test.ts +94 -0
  33. package/clock.ts +47 -0
  34. package/config.ts +99 -0
  35. package/config.unit.test.ts +356 -0
  36. package/context-stamp.ts +189 -0
  37. package/context-stamp.unit.test.ts +159 -0
  38. package/cron-failure-reporter.ts +107 -0
  39. package/cron-failure-reporter.unit.test.ts +244 -0
  40. package/cron-handler.integration.test.ts +614 -0
  41. package/cron-handler.smoke.test.ts +1371 -0
  42. package/cron-handler.ts +750 -0
  43. package/cron-handler.unit.test.ts +1177 -0
  44. package/cron-run-reporter.integration.test.ts +806 -0
  45. package/cron-run-reporter.ts +398 -0
  46. package/crypto.ts +73 -0
  47. package/cutover-validate.integration.test.ts +245 -0
  48. package/cutover-validate.ts +84 -0
  49. package/entrypoint-main.ts +79 -0
  50. package/entrypoint.integration.test.ts +315 -0
  51. package/entrypoint.ts +154 -0
  52. package/errors.ts +61 -0
  53. package/fixtures/chat-service/attachment.json +15 -0
  54. package/fixtures/chat-service/messages.json +86 -0
  55. package/fixtures/chat-service/threads.json +50 -0
  56. package/fixtures/github-installations/installations.json +1231 -0
  57. package/fixtures/slack-provision-cassette.json +6 -0
  58. package/fixtures/specialties/fixture/manifest.yaml +35 -0
  59. package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
  60. package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
  61. package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
  62. package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
  63. package/fixtures/stream-json/clean-multi-turn.ts +209 -0
  64. package/fixtures/stream-json/progress-phases.ts +203 -0
  65. package/fixtures/stream-json/skill-attribution.ts +251 -0
  66. package/fixtures/stream-json/truncated-no-result.ts +84 -0
  67. package/format.ts +283 -0
  68. package/format.unit.test.ts +558 -0
  69. package/gh-helpers.ts +139 -0
  70. package/gh-helpers.unit.test.ts +253 -0
  71. package/gh-token-files.integration.test.ts +143 -0
  72. package/gh-token-files.ts +92 -0
  73. package/gh-token-files.unit.test.ts +63 -0
  74. package/github-app-auth.integration.test.ts +419 -0
  75. package/github-app-auth.ts +193 -0
  76. package/github-auth-deps.ts +128 -0
  77. package/github-auth-startup.ts +231 -0
  78. package/github-auth-startup.unit.test.ts +325 -0
  79. package/github-installations-manager.integration.test.ts +323 -0
  80. package/github-installations-reporter.integration.test.ts +124 -0
  81. package/github-installations-reporter.ts +150 -0
  82. package/github-installations-reporter.unit.test.ts +166 -0
  83. package/github-installations.integration.test.ts +88 -0
  84. package/github-installations.ts +462 -0
  85. package/github-installations.unit.test.ts +98 -0
  86. package/health.smoke.test.ts +244 -0
  87. package/health.ts +193 -0
  88. package/health.unit.test.ts +40 -0
  89. package/http-chat-service-client.integration.test.ts +345 -0
  90. package/http-chat-service-client.ts +287 -0
  91. package/http-chat-service-client.unit.test.ts +500 -0
  92. package/log-prefix.ts +12 -0
  93. package/log-prefix.unit.test.ts +24 -0
  94. package/markers.ts +178 -0
  95. package/markers.unit.test.ts +374 -0
  96. package/package.json +38 -4
  97. package/piper-voice.ts +116 -0
  98. package/piper-voice.unit.test.ts +124 -0
  99. package/pricing.ts +7 -0
  100. package/pricing.unit.test.ts +149 -0
  101. package/process-tree-kill.integration.test.ts +133 -0
  102. package/process-tree-kill.ts +325 -0
  103. package/process-tree-kill.unit.test.ts +265 -0
  104. package/progress-milestones.ts +86 -0
  105. package/progress-milestones.unit.test.ts +188 -0
  106. package/run-telemetry.ts +241 -0
  107. package/run-telemetry.unit.test.ts +235 -0
  108. package/sessions.integration.test.ts +273 -0
  109. package/sessions.ts +191 -0
  110. package/sessions.unit.test.ts +94 -0
  111. package/setup-github-auth.integration.test.ts +858 -0
  112. package/setup-github-auth.ts +297 -0
  113. package/setup.fixture.integration.test.ts +109 -0
  114. package/setup.integration.test.ts +1495 -0
  115. package/setup.ts +853 -0
  116. package/setup.unit.test.ts +286 -0
  117. package/shipwright-config-client.ts +36 -0
  118. package/shipwright-runtime-client.integration.test.ts +336 -0
  119. package/shipwright-runtime-client.ts +118 -0
  120. package/slack-account-status.unit.test.ts +251 -0
  121. package/slack-bolt-client.integration.test.ts +76 -0
  122. package/slack-chat-conversation.integration.test.ts +333 -0
  123. package/slack-progress.ts +510 -0
  124. package/slack-progress.unit.test.ts +806 -0
  125. package/slack-socket-mode-ping.integration.test.ts +137 -0
  126. package/slack-startup.ts +170 -0
  127. package/slack-startup.unit.test.ts +300 -0
  128. package/slack-thread-status-tracker.ts +70 -0
  129. package/slack-thread-status-tracker.unit.test.ts +118 -0
  130. package/slack-trial-expiry.unit.test.ts +269 -0
  131. package/slack.integration.test.ts +3979 -0
  132. package/slack.ts +1286 -0
  133. package/slack.unit.test.ts +1070 -0
  134. package/spawn-env.ts +58 -0
  135. package/spawn-env.unit.test.ts +96 -0
  136. package/startup-dm.integration.test.ts +156 -0
  137. package/startup-dm.ts +29 -0
  138. package/sync-config-clone.ts +238 -0
  139. package/sync-config-clone.unit.test.ts +429 -0
  140. package/test-env.ts +27 -0
  141. package/users.ts +90 -0
  142. package/users.unit.test.ts +108 -0
  143. package/voice.integration.test.ts +669 -0
  144. package/voice.ts +315 -0
  145. package/voice.unit.test.ts +105 -0
  146. package/workspace/BOOTSTRAP.md.template +43 -0
  147. package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
  148. package/workspace/IDENTITY.md.template +8 -0
  149. package/workspace/RUNTIME.md.template +193 -0
  150. package/workspace/RUNTIME.md.template.content.test.ts +75 -0
  151. package/workspace/SOUL.md.template +45 -0
  152. package/workspace/SPECIALTY.md.template +37 -0
  153. package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
  154. package/workspace/VOICE.md.template +8 -0
  155. package/workspace/mise.toml.template +12 -0
  156. package/workspace/requirements.txt.template +1 -0
  157. package/workspace-paths.ts +37 -0
  158. package/worktree-reaper.ts +244 -0
  159. package/worktree-reaper.unit.test.ts +380 -0
  160. package/README.md +0 -3
@@ -0,0 +1,245 @@
1
+ /**
2
+ * agent/src/runtime/cutover-validate.integration.test.ts
3
+ * Integration tests for cutover-validate using in-memory doubles.
4
+ * No database or network required — runs unconditionally.
5
+ */
6
+
7
+ import { describe, expect, it } from "bun:test";
8
+ import type {
9
+ AgentConfigResponse,
10
+ AgentCronJob,
11
+ } from "@app-vitals/agent-admin";
12
+ import {
13
+ type ShipwrightConfigClient,
14
+ validateCutover,
15
+ } from "./cutover-validate.ts";
16
+
17
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
18
+
19
+ const AGENT_ID = "agent-cutover-test";
20
+
21
+ function makeCron(id: string): AgentCronJob {
22
+ return {
23
+ id,
24
+ agentId: AGENT_ID,
25
+ schedule: "0 9 * * *",
26
+ prompt: "/shipwright:dev-task",
27
+ channel: "C123456",
28
+ user: null,
29
+ silent: false,
30
+ enabled: true,
31
+ preCheck: null,
32
+ name: "dev-task",
33
+ createdAt: new Date("2026-01-01"),
34
+ updatedAt: new Date("2026-01-01"),
35
+ system: false,
36
+ parentCronId: null,
37
+ lockdownDisabledAt: null,
38
+ };
39
+ }
40
+
41
+ function makeConfig(env: Record<string, string>): AgentConfigResponse {
42
+ return {
43
+ env,
44
+ allowedTools: [],
45
+ plugins: [],
46
+ repos: [],
47
+ reviewAuthorAllowlist: [],
48
+ patchAuthorAllowlist: [],
49
+ restrictSlackToMembers: false,
50
+ memberEmails: [],
51
+ autoPostReviews: true,
52
+ allowSelfReview: false,
53
+ minConfidence: 75,
54
+ maxFindings: 5,
55
+ cleanupMergedWorktrees: true,
56
+ cleanupAfterDays: 14,
57
+ };
58
+ }
59
+
60
+ // ─── Recorded double ──────────────────────────────────────────────────────────
61
+
62
+ class RecordedShipwrightConfigClient implements ShipwrightConfigClient {
63
+ constructor(
64
+ private readonly config: AgentConfigResponse,
65
+ private readonly crons: AgentCronJob[],
66
+ ) {}
67
+
68
+ async getConfig(_agentId: string): Promise<AgentConfigResponse> {
69
+ return this.config;
70
+ }
71
+
72
+ async getCrons(_agentId: string): Promise<AgentCronJob[]> {
73
+ return this.crons;
74
+ }
75
+ }
76
+
77
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
78
+
79
+ function makeAppAuthEnv(): Record<string, string> {
80
+ return {
81
+ SLACK_BOT_TOKEN: "xoxb-test-token",
82
+ GH_APP_ID: "12345",
83
+ GH_APP_PRIVATE_KEY:
84
+ "-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----",
85
+ GH_APP_INSTALLATION_ID: "67890",
86
+ };
87
+ }
88
+
89
+ function makePatEnv(): Record<string, string> {
90
+ return {
91
+ SLACK_BOT_TOKEN: "xoxb-test-token",
92
+ GH_TOKEN: "ghp_faketoken123",
93
+ };
94
+ }
95
+
96
+ // ─── Tests ────────────────────────────────────────────────────────────────────
97
+
98
+ describe("validateCutover", () => {
99
+ it("returns all passing when SLACK_BOT_TOKEN + GitHub App creds + crons present", async () => {
100
+ const client = new RecordedShipwrightConfigClient(
101
+ makeConfig(makeAppAuthEnv()),
102
+ [makeCron("cron-1")],
103
+ );
104
+ const results = await validateCutover(client, AGENT_ID);
105
+ expect(results.every((r) => r.passed)).toBe(true);
106
+ });
107
+
108
+ it("returns all passing when SLACK_BOT_TOKEN + GH_TOKEN (PAT) + crons present", async () => {
109
+ const client = new RecordedShipwrightConfigClient(
110
+ makeConfig(makePatEnv()),
111
+ [makeCron("cron-1")],
112
+ );
113
+ const results = await validateCutover(client, AGENT_ID);
114
+ expect(results.every((r) => r.passed)).toBe(true);
115
+ });
116
+
117
+ it("does NOT fail when SLACK_BOT_TOKEN is missing — a chat-only agent is a supported configuration", async () => {
118
+ const { SLACK_BOT_TOKEN: _s, ...env } = makeAppAuthEnv();
119
+ const client = new RecordedShipwrightConfigClient(makeConfig(env), [
120
+ makeCron("cron-1"),
121
+ ]);
122
+ const results = await validateCutover(client, AGENT_ID);
123
+ const slackCheck = results.find((r) => r.name === "SLACK_BOT_TOKEN");
124
+ expect(slackCheck).toBeDefined();
125
+ expect(slackCheck?.passed).toBe(true);
126
+ expect(slackCheck?.message).toContain("chat-only");
127
+ // The whole run still passes — nothing else depended on Slack.
128
+ expect(results.every((r) => r.passed)).toBe(true);
129
+ });
130
+
131
+ it("reports SLACK_BOT_TOKEN as present when it is set", async () => {
132
+ const client = new RecordedShipwrightConfigClient(
133
+ makeConfig(makeAppAuthEnv()),
134
+ [makeCron("cron-1")],
135
+ );
136
+ const results = await validateCutover(client, AGENT_ID);
137
+ const slackCheck = results.find((r) => r.name === "SLACK_BOT_TOKEN");
138
+ expect(slackCheck?.passed).toBe(true);
139
+ expect(slackCheck?.message).toBe("SLACK_BOT_TOKEN is present");
140
+ });
141
+
142
+ it("names no failing check when only SLACK_BOT_TOKEN is missing", async () => {
143
+ const { SLACK_BOT_TOKEN: _s, ...env } = makeAppAuthEnv();
144
+ const client = new RecordedShipwrightConfigClient(makeConfig(env), [
145
+ makeCron("cron-1"),
146
+ ]);
147
+ const results = await validateCutover(client, AGENT_ID);
148
+ expect(results.filter((r) => !r.passed)).toEqual([]);
149
+ });
150
+
151
+ it("fails when GitHub auth credentials are missing", async () => {
152
+ const client = new RecordedShipwrightConfigClient(
153
+ makeConfig({ SLACK_BOT_TOKEN: "xoxb-token" }),
154
+ [makeCron("cron-1")],
155
+ );
156
+ const results = await validateCutover(client, AGENT_ID);
157
+ const githubCheck = results.find((r) => r.name === "github_auth");
158
+ expect(githubCheck).toBeDefined();
159
+ expect(githubCheck?.passed).toBe(false);
160
+ });
161
+
162
+ it("fails when no crons are present", async () => {
163
+ const client = new RecordedShipwrightConfigClient(
164
+ makeConfig(makeAppAuthEnv()),
165
+ [],
166
+ );
167
+ const results = await validateCutover(client, AGENT_ID);
168
+ const cronCheck = results.find((r) => r.name === "crons");
169
+ expect(cronCheck).toBeDefined();
170
+ expect(cronCheck?.passed).toBe(false);
171
+ });
172
+
173
+ it("reports all failing checks when multiple checks fail", async () => {
174
+ const client = new RecordedShipwrightConfigClient(makeConfig({}), []);
175
+ const results = await validateCutover(client, AGENT_ID);
176
+ const failed = results.filter((r) => !r.passed);
177
+ expect(failed.length).toBeGreaterThanOrEqual(2);
178
+ const names = failed.map((r) => r.name);
179
+ expect(names).toContain("github_auth");
180
+ expect(names).toContain("crons");
181
+ // Slack is informational — an empty env does not fail it.
182
+ expect(names).not.toContain("SLACK_BOT_TOKEN");
183
+ });
184
+
185
+ it("passes with App id + key and no GH_APP_INSTALLATION_ID (the installation id is an optional pin)", async () => {
186
+ const env: Record<string, string> = {
187
+ SLACK_BOT_TOKEN: "xoxb-test-token",
188
+ GH_APP_ID: "12345",
189
+ GH_APP_PRIVATE_KEY: "some-key",
190
+ // GH_APP_INSTALLATION_ID intentionally omitted — discovered at runtime
191
+ };
192
+ const client = new RecordedShipwrightConfigClient(makeConfig(env), [
193
+ makeCron("cron-1"),
194
+ ]);
195
+ const results = await validateCutover(client, AGENT_ID);
196
+ const githubCheck = results.find((r) => r.name === "github_auth");
197
+ expect(githubCheck).toBeDefined();
198
+ expect(githubCheck?.passed).toBe(true);
199
+ });
200
+
201
+ it("fails when GH_APP_PRIVATE_KEY is missing (partial App creds)", async () => {
202
+ const env: Record<string, string> = {
203
+ SLACK_BOT_TOKEN: "xoxb-test-token",
204
+ GH_APP_ID: "12345",
205
+ GH_APP_INSTALLATION_ID: "67890",
206
+ };
207
+ const client = new RecordedShipwrightConfigClient(makeConfig(env), [
208
+ makeCron("cron-1"),
209
+ ]);
210
+ const results = await validateCutover(client, AGENT_ID);
211
+ const githubCheck = results.find((r) => r.name === "github_auth");
212
+ expect(githubCheck?.passed).toBe(false);
213
+ expect(githubCheck?.message).toContain("GH_APP_ID + GH_APP_PRIVATE_KEY");
214
+ expect(githubCheck?.message).not.toContain("+ GH_APP_INSTALLATION_ID");
215
+ });
216
+
217
+ it("fails when only disabled crons are present", async () => {
218
+ const disabledCron: AgentCronJob = {
219
+ ...makeCron("cron-disabled"),
220
+ enabled: false,
221
+ };
222
+ const client = new RecordedShipwrightConfigClient(
223
+ makeConfig(makeAppAuthEnv()),
224
+ [disabledCron],
225
+ );
226
+ const results = await validateCutover(client, AGENT_ID);
227
+ const cronCheck = results.find((r) => r.name === "crons");
228
+ expect(cronCheck).toBeDefined();
229
+ expect(cronCheck?.passed).toBe(false);
230
+ });
231
+
232
+ it("accepts GitHub App creds with only the three required fields", async () => {
233
+ const env: Record<string, string> = {
234
+ SLACK_BOT_TOKEN: "xoxb-test-token",
235
+ GH_APP_ID: "99999",
236
+ GH_APP_PRIVATE_KEY: "some-key",
237
+ GH_APP_INSTALLATION_ID: "11111",
238
+ };
239
+ const client = new RecordedShipwrightConfigClient(makeConfig(env), [
240
+ makeCron("cron-1"),
241
+ ]);
242
+ const results = await validateCutover(client, AGENT_ID);
243
+ expect(results.every((r) => r.passed)).toBe(true);
244
+ });
245
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * agent/src/runtime/cutover-validate.ts
3
+ * Validates that a shipwright agent is ready for cutover by checking the
4
+ * runtime API for required credentials and crons.
5
+ *
6
+ * Checks:
7
+ * - SLACK_BOT_TOKEN reported (informational — absence means a chat-only agent,
8
+ * which is a supported configuration, so this check never fails)
9
+ * - GitHub auth credentials present (App: GH_APP_ID + GH_APP_PRIVATE_KEY,
10
+ * with GH_APP_INSTALLATION_ID as an optional pin; or PAT: GH_TOKEN)
11
+ * - At least one cron job configured
12
+ */
13
+
14
+ import type {
15
+ AgentConfigResponse,
16
+ AgentCronJob,
17
+ } from "@app-vitals/agent-admin";
18
+
19
+ // ─── Types ────────────────────────────────────────────────────────────────────
20
+
21
+ export interface ShipwrightConfigClient {
22
+ getConfig(agentId: string): Promise<AgentConfigResponse>;
23
+ getCrons(agentId: string): Promise<AgentCronJob[]>;
24
+ }
25
+
26
+ export interface CheckResult {
27
+ name: string;
28
+ passed: boolean;
29
+ message: string;
30
+ }
31
+
32
+ // ─── Validation ───────────────────────────────────────────────────────────────
33
+
34
+ function hasGitHubAuth(env: Record<string, string>): boolean {
35
+ // GH_APP_INSTALLATION_ID is an optional pin — installations are discovered.
36
+ const hasAppCreds = Boolean(env.GH_APP_ID) && Boolean(env.GH_APP_PRIVATE_KEY);
37
+ const hasPat = Boolean(env.GH_TOKEN);
38
+ return hasAppCreds || hasPat;
39
+ }
40
+
41
+ export async function validateCutover(
42
+ client: ShipwrightConfigClient,
43
+ agentId: string,
44
+ ): Promise<CheckResult[]> {
45
+ const [config, crons] = await Promise.all([
46
+ client.getConfig(agentId),
47
+ client.getCrons(agentId),
48
+ ]);
49
+
50
+ const { env } = config;
51
+
52
+ const hasSlack = Boolean(env.SLACK_BOT_TOKEN);
53
+ const hasGithub = hasGitHubAuth(env);
54
+ const enabledCrons = crons.filter((c) => c.enabled);
55
+ const hasCrons = enabledCrons.length > 0;
56
+
57
+ return [
58
+ {
59
+ // Slack is an optional interaction surface, not a requirement: an agent
60
+ // with no Slack credentials runs in offline mode (agent/src/index.ts) and
61
+ // is driven from the admin chat UI instead. Absence is reported, not
62
+ // failed — otherwise every chat-only agent fails cutover by design.
63
+ name: "SLACK_BOT_TOKEN",
64
+ passed: true,
65
+ message: hasSlack
66
+ ? "SLACK_BOT_TOKEN is present"
67
+ : "SLACK_BOT_TOKEN is not set — chat-only agent (interact via /admin/chat)",
68
+ },
69
+ {
70
+ name: "github_auth",
71
+ passed: hasGithub,
72
+ message: hasGithub
73
+ ? "GitHub auth credentials are present"
74
+ : "GitHub auth credentials missing — need GH_TOKEN or GH_APP_ID + GH_APP_PRIVATE_KEY (GH_APP_INSTALLATION_ID optional pin)",
75
+ },
76
+ {
77
+ name: "crons",
78
+ passed: hasCrons,
79
+ message: hasCrons
80
+ ? `${enabledCrons.length} enabled cron job(s) configured`
81
+ : "No enabled cron jobs configured — at least one is required",
82
+ },
83
+ ];
84
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * agent/src/entrypoint-main.ts
3
+ *
4
+ * Production CLI entry point — invoked by the Dockerfile ENTRYPOINT.
5
+ *
6
+ * Wires all real dependencies and calls runEntrypoint().
7
+ * Run via: bun run agent/src/entrypoint-main.ts [--agent-id X] [--api-url Y] [--api-key Z]
8
+ *
9
+ * The agent server (index.ts) runs as a subprocess (spawnAgentServer) and owns
10
+ * the single health server. Do NOT start a health server here too — index.ts
11
+ * binds the same port, so a second listener would fail with EADDRINUSE and crash
12
+ * the agent on startup.
13
+ */
14
+
15
+ import { join } from "node:path";
16
+ import { applyEnvToProcess } from "@app-vitals/agent-runtime/anthropic-federation-guard";
17
+ import { parseCliArgs } from "@app-vitals/agent-runtime/cli-args";
18
+ import { runEntrypoint } from "@app-vitals/agent-runtime/entrypoint";
19
+ import { buildGitHubAuthDeps } from "@app-vitals/agent-runtime/github-auth-deps";
20
+ import {
21
+ ensureDotClaudeSymlink,
22
+ installPlugins,
23
+ runMiseStartup,
24
+ } from "@app-vitals/agent-runtime/setup";
25
+ import { setupGitHubAuth } from "@app-vitals/agent-runtime/setup-github-auth";
26
+ import { HttpShipwrightRuntimeClient } from "@app-vitals/agent-runtime/shipwright-runtime-client";
27
+
28
+ const { agentId, apiUrl, apiKey } = parseCliArgs(
29
+ process.argv.slice(2),
30
+ process.env as Record<string, string | undefined>,
31
+ );
32
+
33
+ const agentHome =
34
+ process.env.AGENT_HOME ??
35
+ join(process.env.HOME ?? "/root", ".shipwright-agent");
36
+
37
+ const runtimeClient = new HttpShipwrightRuntimeClient({
38
+ apiUrl: apiUrl ?? "",
39
+ apiKey: apiKey ?? "",
40
+ });
41
+ const configClient = {
42
+ getConfig: (id: string) => runtimeClient.getAgentConfigBundle(id),
43
+ };
44
+
45
+ const SCRIPTS_BIN = join(import.meta.dir, "..", "scripts", "bin");
46
+
47
+ await runEntrypoint({
48
+ agentId,
49
+ apiUrl,
50
+ apiKey,
51
+ agentHome,
52
+ configClient,
53
+ // WIF-2.1: skips the Anthropic credential keys when federation is active.
54
+ applyEnv: (env: Record<string, string>) => applyEnvToProcess(env),
55
+ symlinkDotClaude: ensureDotClaudeSymlink,
56
+ setupGitHubAuth: async () => {
57
+ await setupGitHubAuth(buildGitHubAuthDeps(agentHome, SCRIPTS_BIN));
58
+ },
59
+ runMiseStartup,
60
+ installPlugins,
61
+ spawnAgentServer: (cmd: string, args: string[]) => {
62
+ // Detach — let the agent server run as the main process
63
+ const proc = Bun.spawn([cmd, ...args], {
64
+ stdio: ["inherit", "inherit", "inherit"],
65
+ env: process.env as Record<string, string>,
66
+ });
67
+ // Transfer control — wait for the server process to exit
68
+ proc.exited.then((code) => {
69
+ process.exit(code ?? 0);
70
+ });
71
+ },
72
+ exit: (code: number) => {
73
+ process.exit(code);
74
+ },
75
+ startupTimeoutMs: (() => {
76
+ const ms = Number(process.env.SHIPWRIGHT_STARTUP_TIMEOUT_MS);
77
+ return Number.isFinite(ms) ? ms : undefined;
78
+ })(),
79
+ });