@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,107 @@
1
+ /**
2
+ * agent/src/runtime/cron-failure-reporter.ts
3
+ *
4
+ * Extracted from index.ts's cron-sync scheduled callback so a thrown dispatch
5
+ * error (either the `dispatch === "loop"` path or the regular
6
+ * handleCronRequest path) surfaces the same way any other failed cron run
7
+ * does: an AgentCronRun row (visible in the admin run-history UI / Slack),
8
+ * not just raw pod stdout.
9
+ *
10
+ * Mirrors loop-orchestrator.ts's dispatch() catch block: createRun() is
11
+ * called for the tick, then on a thrown error completeRun() is called with
12
+ * outcome "failed" and { error: <message> }. Also calls
13
+ * sentryClient.captureException(err) — this is a genuinely unhandled failure
14
+ * (the whole tick aborted), not a swallowed/handled one, so it's captured as
15
+ * a proper Sentry Issue rather than relying on consoleLoggingIntegration to
16
+ * turn a console.error call into a lower-signal Sentry log line.
17
+ *
18
+ * `cronRunReporter` and `sentryClient` are both optional-by-convention at the
19
+ * call site (NoopCronRunReporter / undefined respectively) — this function
20
+ * itself requires a CronRunReporter (callers pass the Noop fallback) and
21
+ * treats sentryClient as optional so it behaves identically whether or not
22
+ * SENTRY_DSN is set.
23
+ *
24
+ * index.ts's catch wraps BOTH the loop-dispatch path (which calls into
25
+ * loop-orchestrator.ts's dispatch()) and the handleCronRequest path
26
+ * (cron-handler.ts) — both of those already call
27
+ * cronRunReporter.completeRun(..., "failed", ...) on their own runId before
28
+ * rethrowing. To avoid a duplicate AgentCronRun "failed" row per real
29
+ * failure, those callers tag the rethrown error via
30
+ * markCronRunFailureReported() first; reportCronFailure() always logs and
31
+ * captures to Sentry, but skips its own createRun/completeRun pair when the
32
+ * error is already tagged.
33
+ */
34
+
35
+ import type { ErrorCapturingClient } from "@app-vitals/agent-lib/sentry";
36
+ import { reportClaudeError } from "./claude.ts";
37
+ import type { Clock } from "./clock.ts";
38
+ import type { CronRunReporter } from "./cron-run-reporter.ts";
39
+
40
+ interface ReportCronFailureDeps {
41
+ cronRunReporter: CronRunReporter;
42
+ sentryClient?: ErrorCapturingClient;
43
+ clock: Clock;
44
+ }
45
+
46
+ /**
47
+ * Symbol tag used to mark a thrown error as "an AgentCronRun failure row has
48
+ * already been recorded for this error" — set by callers (cron-handler.ts's
49
+ * runner-scope catch, loop-orchestrator.ts's dispatch() catch) that already
50
+ * called cronRunReporter.completeRun(..., "failed", ...) on their own runId
51
+ * before rethrowing. Symbol.for() (not Symbol()) so the tag survives across
52
+ * module instances if the error crosses a boundary.
53
+ */
54
+ const CRON_RUN_ALREADY_REPORTED = Symbol.for(
55
+ "shipwright.cronRunAlreadyReported",
56
+ );
57
+
58
+ /**
59
+ * Marks a thrown error as already reported to CronRunReporter, so a later,
60
+ * broader catch (e.g. index.ts's cron-sync scheduled callback) can call
61
+ * reportCronFailure() for logging/Sentry purposes without also creating a
62
+ * duplicate AgentCronRun "failed" row.
63
+ */
64
+ export function markCronRunFailureReported(err: unknown): void {
65
+ if (err && typeof err === "object") {
66
+ Reflect.set(err as object, CRON_RUN_ALREADY_REPORTED, true);
67
+ }
68
+ }
69
+
70
+ function isCronRunFailureReported(err: unknown): boolean {
71
+ return Boolean(
72
+ err &&
73
+ typeof err === "object" &&
74
+ Reflect.get(err as object, CRON_RUN_ALREADY_REPORTED),
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Reports a cron tick's dispatch failure: logs locally (console.error),
80
+ * captures the error in Sentry (when a sentryClient is wired), and records a
81
+ * failed AgentCronRun via createRun/completeRun so the failure shows up in
82
+ * the admin run-history UI and metrics dashboard, not just raw pod logs.
83
+ */
84
+ export async function reportCronFailure(
85
+ cronId: string,
86
+ err: unknown,
87
+ deps: ReportCronFailureDeps,
88
+ ): Promise<void> {
89
+ const { cronRunReporter, sentryClient, clock } = deps;
90
+ const message = err instanceof Error ? err.message : String(err);
91
+
92
+ console.error(`[cron] job ${cronId} failed:`, message);
93
+
94
+ reportClaudeError(sentryClient, err);
95
+
96
+ if (isCronRunFailureReported(err)) {
97
+ // A more specific layer (cron-handler.ts / loop-orchestrator.ts) already
98
+ // recorded a failed AgentCronRun on its own runId before rethrowing —
99
+ // skip creating a second, untagged row for the same failure.
100
+ return;
101
+ }
102
+
103
+ const runId = await cronRunReporter.createRun(cronId, clock.now());
104
+ await cronRunReporter.completeRun(cronId, runId, clock.now(), "failed", {
105
+ error: message,
106
+ });
107
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * agent/src/runtime/cron-failure-reporter.unit.test.ts
3
+ *
4
+ * Unit tests for reportCronFailure() — pure logic, no I/O. Verifies a thrown
5
+ * cron dispatch error results in a createRun/completeRun("failed") pair on
6
+ * the injected CronRunReporter and a captureException call on the injected
7
+ * ErrorCapturingClient, mirroring loop-orchestrator.ts's dispatch() pattern.
8
+ */
9
+
10
+ import { describe, expect, it } from "bun:test";
11
+ import { ClaudeTimeoutError } from "./claude.ts";
12
+ import { FixedClock } from "./clock.ts";
13
+ import {
14
+ markCronRunFailureReported,
15
+ reportCronFailure,
16
+ } from "./cron-failure-reporter.ts";
17
+ import type {
18
+ CronRunReporter,
19
+ ModelBreakdownEntry,
20
+ } from "./cron-run-reporter.ts";
21
+
22
+ const FIXED_TIME = new Date("2024-01-01T00:00:00.000Z");
23
+
24
+ interface CreateRunCall {
25
+ cronId: string;
26
+ startedAt: Date;
27
+ phase?: string;
28
+ }
29
+
30
+ interface CompleteRunCall {
31
+ cronId: string;
32
+ runId: string | null;
33
+ completedAt: Date;
34
+ outcome: "completed" | "failed";
35
+ opts?: {
36
+ error?: string;
37
+ inputTokens?: number;
38
+ outputTokens?: number;
39
+ cacheReadTokens?: number;
40
+ cacheCreationTokens?: number;
41
+ modelBreakdown?: ModelBreakdownEntry[];
42
+ };
43
+ phase?: string;
44
+ }
45
+
46
+ function createFakeCronRunReporter(runId: string | null = "run-1") {
47
+ const createRunCalls: CreateRunCall[] = [];
48
+ const completeRunCalls: CompleteRunCall[] = [];
49
+
50
+ const reporter: CronRunReporter = {
51
+ async createRun(cronId, startedAt, phase) {
52
+ createRunCalls.push({ cronId, startedAt, phase });
53
+ return runId;
54
+ },
55
+ async completeRun(cronId, callRunId, completedAt, outcome, opts, phase) {
56
+ completeRunCalls.push({
57
+ cronId,
58
+ runId: callRunId,
59
+ completedAt,
60
+ outcome,
61
+ opts,
62
+ phase,
63
+ });
64
+ },
65
+ async skipRun() {
66
+ throw new Error("skipRun should not be called by reportCronFailure");
67
+ },
68
+ async recordProgress() {},
69
+ async recordSessionId() {},
70
+ };
71
+
72
+ return { reporter, createRunCalls, completeRunCalls };
73
+ }
74
+
75
+ function createFakeErrorCapturingClient() {
76
+ const capturedErrors: unknown[] = [];
77
+ const capturedMessages: string[] = [];
78
+ return {
79
+ client: {
80
+ captureException: (err: unknown) => {
81
+ capturedErrors.push(err);
82
+ },
83
+ captureMessage: (message: string) => {
84
+ capturedMessages.push(message);
85
+ },
86
+ },
87
+ capturedErrors,
88
+ capturedMessages,
89
+ };
90
+ }
91
+
92
+ describe("reportCronFailure", () => {
93
+ it("creates a run and completes it as failed with the error message", async () => {
94
+ const { reporter, createRunCalls, completeRunCalls } =
95
+ createFakeCronRunReporter();
96
+ const clock = FixedClock(FIXED_TIME);
97
+ const err = new Error("dispatch exploded");
98
+
99
+ await reportCronFailure("my-cron-id", err, {
100
+ cronRunReporter: reporter,
101
+ clock,
102
+ });
103
+
104
+ expect(createRunCalls).toHaveLength(1);
105
+ expect(createRunCalls[0]?.cronId).toBe("my-cron-id");
106
+ expect(createRunCalls[0]?.startedAt).toEqual(FIXED_TIME);
107
+
108
+ expect(completeRunCalls).toHaveLength(1);
109
+ expect(completeRunCalls[0]?.cronId).toBe("my-cron-id");
110
+ expect(completeRunCalls[0]?.runId).toBe("run-1");
111
+ expect(completeRunCalls[0]?.completedAt).toEqual(FIXED_TIME);
112
+ expect(completeRunCalls[0]?.outcome).toBe("failed");
113
+ expect(completeRunCalls[0]?.opts?.error).toBe("dispatch exploded");
114
+ });
115
+
116
+ it("stringifies a non-Error thrown value for the error field", async () => {
117
+ const { reporter, completeRunCalls } = createFakeCronRunReporter();
118
+ const clock = FixedClock(FIXED_TIME);
119
+
120
+ await reportCronFailure("my-cron-id", "just a string failure", {
121
+ cronRunReporter: reporter,
122
+ clock,
123
+ });
124
+
125
+ expect(completeRunCalls[0]?.outcome).toBe("failed");
126
+ expect(completeRunCalls[0]?.opts?.error).toBe("just a string failure");
127
+ });
128
+
129
+ it("calls captureException on the injected sentry client with the original error", async () => {
130
+ const { reporter } = createFakeCronRunReporter();
131
+ const { client, capturedErrors } = createFakeErrorCapturingClient();
132
+ const clock = FixedClock(FIXED_TIME);
133
+ const err = new Error("boom");
134
+
135
+ await reportCronFailure("my-cron-id", err, {
136
+ cronRunReporter: reporter,
137
+ sentryClient: client,
138
+ clock,
139
+ });
140
+
141
+ expect(capturedErrors).toHaveLength(1);
142
+ expect(capturedErrors[0]).toBe(err);
143
+ });
144
+
145
+ it("SENTRY-46: reports a ceiling-reason ClaudeTimeoutError via captureMessage, not captureException", async () => {
146
+ const { reporter } = createFakeCronRunReporter();
147
+ const { client, capturedErrors, capturedMessages } =
148
+ createFakeErrorCapturingClient();
149
+ const clock = FixedClock(FIXED_TIME);
150
+ const err = new ClaudeTimeoutError(3_600_000, "ceiling");
151
+
152
+ await reportCronFailure("my-cron-id", err, {
153
+ cronRunReporter: reporter,
154
+ sentryClient: client,
155
+ clock,
156
+ });
157
+
158
+ expect(capturedErrors).toHaveLength(0);
159
+ expect(capturedMessages).toHaveLength(1);
160
+ expect(capturedMessages[0]).toContain("3600s");
161
+ });
162
+
163
+ it("still reports an idle-reason ClaudeTimeoutError via captureException", async () => {
164
+ const { reporter } = createFakeCronRunReporter();
165
+ const { client, capturedErrors, capturedMessages } =
166
+ createFakeErrorCapturingClient();
167
+ const clock = FixedClock(FIXED_TIME);
168
+ const err = new ClaudeTimeoutError(1_500_000, "idle");
169
+
170
+ await reportCronFailure("my-cron-id", err, {
171
+ cronRunReporter: reporter,
172
+ sentryClient: client,
173
+ clock,
174
+ });
175
+
176
+ expect(capturedMessages).toHaveLength(0);
177
+ expect(capturedErrors).toHaveLength(1);
178
+ expect(capturedErrors[0]).toBe(err);
179
+ });
180
+
181
+ it("does not throw when no sentryClient is provided", async () => {
182
+ const { reporter } = createFakeCronRunReporter();
183
+ const clock = FixedClock(FIXED_TIME);
184
+
185
+ await expect(
186
+ reportCronFailure("my-cron-id", new Error("boom"), {
187
+ cronRunReporter: reporter,
188
+ clock,
189
+ }),
190
+ ).resolves.toBeUndefined();
191
+ });
192
+
193
+ it("still creates/completes a run for an error that was not marked as already-reported", async () => {
194
+ const { reporter, createRunCalls, completeRunCalls } =
195
+ createFakeCronRunReporter();
196
+ const clock = FixedClock(FIXED_TIME);
197
+ const err = new Error("unmarked failure");
198
+
199
+ await reportCronFailure("my-cron-id", err, {
200
+ cronRunReporter: reporter,
201
+ clock,
202
+ });
203
+
204
+ expect(createRunCalls).toHaveLength(1);
205
+ expect(completeRunCalls).toHaveLength(1);
206
+ });
207
+
208
+ it("skips createRun/completeRun but still logs and captures to Sentry when the error was already reported", async () => {
209
+ const { reporter, createRunCalls, completeRunCalls } =
210
+ createFakeCronRunReporter();
211
+ const { client, capturedErrors } = createFakeErrorCapturingClient();
212
+ const clock = FixedClock(FIXED_TIME);
213
+ const err = new Error("already reported by inner catch");
214
+ markCronRunFailureReported(err);
215
+
216
+ await reportCronFailure("my-cron-id", err, {
217
+ cronRunReporter: reporter,
218
+ sentryClient: client,
219
+ clock,
220
+ });
221
+
222
+ expect(createRunCalls).toHaveLength(0);
223
+ expect(completeRunCalls).toHaveLength(0);
224
+ expect(capturedErrors).toHaveLength(1);
225
+ expect(capturedErrors[0]).toBe(err);
226
+ });
227
+
228
+ it("does not mark non-object thrown values (e.g. strings) but still reports normally", async () => {
229
+ const { reporter, createRunCalls, completeRunCalls } =
230
+ createFakeCronRunReporter();
231
+
232
+ // markCronRunFailureReported on a primitive is a no-op; should not throw.
233
+ expect(() => markCronRunFailureReported("just a string")).not.toThrow();
234
+
235
+ const clock = FixedClock(FIXED_TIME);
236
+ await reportCronFailure("my-cron-id", "just a string", {
237
+ cronRunReporter: reporter,
238
+ clock,
239
+ });
240
+
241
+ expect(createRunCalls).toHaveLength(1);
242
+ expect(completeRunCalls).toHaveLength(1);
243
+ });
244
+ });