@straiffi/archon 1.7.7 → 1.7.9

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 (140) hide show
  1. package/dist/client/assets/ActivityView-CDLQOXZm.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-DYxLX2HS.js +1 -0
  3. package/dist/client/assets/VerificationDialog-DEKvZG75.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DVbmGv0b.js → architectureDiagram-3BPJPVTR-ByNXcCIO.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-BsxPuJKK.js → blockDiagram-GPEHLZMM-ByykLAaG.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-9Jjpu2R1.js → c4Diagram-AAUBKEIU-CmWA4Pli.js} +1 -1
  7. package/dist/client/assets/channel-BvcRv90i.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-Dzljq_5a.js → chunk-2J33WTMH-CTgbXRh4.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-CiBHaLdV.js → chunk-3OPIFGDE-D3FajVfq.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-DZFrI-_H.js → chunk-5ZQYHXKU-CUMg_Vfo.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-u519MeJ7.js → chunk-727SXJPM-Oc9MgxZD.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-DC91Nw9K.js → chunk-AQP2D5EJ-5JZboHeP.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-CR7cwH-D.js → chunk-CSCIHK7Q-BpkSMHjB.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-DJwQEfa_.js → chunk-KSCS5N6A-BPBMAswO.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-w6TWSOTT.js → chunk-L5ZTLDWV-BA2sHmu7.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Dfw_jEjR.js → chunk-LZXEDZCA-Blc9q-fQ.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DHdJgCA7.js → chunk-ND2GUHAM-GlVS74JP.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-GvOuUiUy.js → chunk-NZK2D7GU-fctBFnAm.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O--r1VQgDc.js → chunk-O5CBEL6O-eGs4DDLJ.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Xgc6shf5.js → chunk-WU5MYG2G-vxa50E7g.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-IuovJyxl.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BBJ3avX6.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-IcfTsQ6f.js → dagre-BM42HDAG-DXGXNVUp.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-D8Z4_NSQ.js → diagram-2AECGRRQ-BRYr1GvB.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CIjC8QzH.js → diagram-5GNKFQAL-BcwgMag4.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BBbYFygn.js → diagram-KO2AKTUF-Delk7lbr.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-lZstJdXv.js → diagram-LMA3HP47-BVtc_fjo.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-Cnqij60i.js → diagram-OG6HWLK6-weIPvUz5.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-1DAJwSbR.js → erDiagram-TEJ5UH35-DKde70dA.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-DIQkR95e.js → flowDiagram-I6XJVG4X-B7Jrk-6c.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-mDGbRM53.js → ganttDiagram-6RSMTGT7-CDzWDgnA.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BQirGZuE.js → gitGraphDiagram-PVQCEYII-DYZCryuN.js} +1 -1
  33. package/dist/client/assets/index-BlKOx9Vf.css +2 -0
  34. package/dist/client/assets/index-CmaBDvIB.js +273 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CljoXByt.js → infoDiagram-5YYISTIA-DmQOgVKK.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-ouY7AiSo.js → ishikawaDiagram-YF4QCWOH-DCpu-q-Z.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BiI9xI0r.js → journeyDiagram-JHISSGLW-DhBoglhI.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-CvuW7aYT.js → kanban-definition-UN3LZRKU-BSjvGFTY.js} +1 -1
  39. package/dist/client/assets/{line-CoE97hT8.js → line-ClKBZwDr.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BHswQsmF.js → mermaid-parser.core-BRA4mJ40.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-a9PzZlL1.js → mermaid.core-BZ7kSfEt.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CIkEQqLV.js → mindmap-definition-RKZ34NQL-DoUdLHgj.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-BIvcv2M5.js → pieDiagram-4H26LBE5-NVnzqltb.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BWCVll7J.js → quadrantDiagram-W4KKPZXB-DOITgkan.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-w5l0CZn2.js → requirementDiagram-4Y6WPE33-D0VPxrFE.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BuKReL9w.js → sankeyDiagram-5OEKKPKP-Bx0uU7xV.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Br5KpukA.js → sequenceDiagram-3UESZ5HK-B4lFL6cO.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-BfFCsPCI.js → stateDiagram-AJRCARHV-CsLVsMZ4.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CvrvaIdF.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BrDP2iO0.js → timeline-definition-PNZ67QCA-BkcYpPFI.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-C7lGYnr9.js → vennDiagram-CIIHVFJN-Xc5f2sRQ.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BRKJRuEF.js → wardleyDiagram-YWT4CUSO-n3Dpk02Q.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BjaB0hQ8.js → xychartDiagram-2RQKCTM6-BTMzF8zg.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +460 -2
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +542 -30
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/activeAutomationChild.js +31 -0
  60. package/dist/server/lib/activeAutomationChild.js.map +1 -0
  61. package/dist/server/lib/automationCheckGate.js +341 -0
  62. package/dist/server/lib/automationCheckGate.js.map +1 -0
  63. package/dist/server/lib/automationProgression.js +20 -21
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationReconciliation.js +35 -2
  66. package/dist/server/lib/automationReconciliation.js.map +1 -1
  67. package/dist/server/lib/automationResumeDecision.js +61 -1
  68. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  69. package/dist/server/lib/automationStageDetails.js +266 -41
  70. package/dist/server/lib/automationStageDetails.js.map +1 -1
  71. package/dist/server/lib/buildCompletionContinuation.js +109 -0
  72. package/dist/server/lib/buildCompletionContinuation.js.map +1 -0
  73. package/dist/server/lib/buildSequences.js +81 -0
  74. package/dist/server/lib/buildSequences.js.map +1 -1
  75. package/dist/server/lib/buildTicketCommit.js +219 -0
  76. package/dist/server/lib/buildTicketCommit.js.map +1 -0
  77. package/dist/server/lib/bundleAutomationReadiness.js +7 -0
  78. package/dist/server/lib/bundleAutomationReadiness.js.map +1 -0
  79. package/dist/server/lib/bundleReviewOperations.js +17 -5
  80. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  81. package/dist/server/lib/chatOperations.js +2 -2
  82. package/dist/server/lib/chatOperations.js.map +1 -1
  83. package/dist/server/lib/checkDefinitions.js +278 -0
  84. package/dist/server/lib/checkDefinitions.js.map +1 -0
  85. package/dist/server/lib/checkGateEvaluations.js +295 -0
  86. package/dist/server/lib/checkGateEvaluations.js.map +1 -0
  87. package/dist/server/lib/checkRunner.js +674 -0
  88. package/dist/server/lib/checkRunner.js.map +1 -0
  89. package/dist/server/lib/checkRuns.js +736 -0
  90. package/dist/server/lib/checkRuns.js.map +1 -0
  91. package/dist/server/lib/checkWorkspaceIdentity.js +207 -0
  92. package/dist/server/lib/checkWorkspaceIdentity.js.map +1 -0
  93. package/dist/server/lib/executionReconciliation.js +79 -2
  94. package/dist/server/lib/executionReconciliation.js.map +1 -1
  95. package/dist/server/lib/executionRetry.js +14 -6
  96. package/dist/server/lib/executionRetry.js.map +1 -1
  97. package/dist/server/lib/executionRetryContext.js +3 -3
  98. package/dist/server/lib/executionRetryContext.js.map +1 -1
  99. package/dist/server/lib/executionRunRetention.js +41 -3
  100. package/dist/server/lib/executionRunRetention.js.map +1 -1
  101. package/dist/server/lib/executionRuns.js +135 -2
  102. package/dist/server/lib/executionRuns.js.map +1 -1
  103. package/dist/server/lib/executionTimeline.js +52 -1
  104. package/dist/server/lib/executionTimeline.js.map +1 -1
  105. package/dist/server/lib/projectAutoConfig.js +47 -65
  106. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  107. package/dist/server/lib/projects.js +99 -7
  108. package/dist/server/lib/projects.js.map +1 -1
  109. package/dist/server/lib/requiredCheckPolicy.js +93 -0
  110. package/dist/server/lib/requiredCheckPolicy.js.map +1 -0
  111. package/dist/server/lib/shutdown.js +4 -4
  112. package/dist/server/lib/shutdown.js.map +1 -1
  113. package/dist/server/lib/testCommandRunner.js +3 -2
  114. package/dist/server/lib/testCommandRunner.js.map +1 -1
  115. package/dist/server/lib/testSessions.js +2 -6
  116. package/dist/server/lib/testSessions.js.map +1 -1
  117. package/dist/server/lib/ticketAutomationCommit.js +361 -108
  118. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  119. package/dist/server/lib/tickets.js +21 -1
  120. package/dist/server/lib/tickets.js.map +1 -1
  121. package/dist/server/lib/types.js +2 -0
  122. package/dist/server/lib/types.js.map +1 -1
  123. package/dist/server/workers/build.js +50 -81
  124. package/dist/server/workers/build.js.map +1 -1
  125. package/dist/server/workers/chat.js +13 -6
  126. package/dist/server/workers/chat.js.map +1 -1
  127. package/dist/server/workers/followUp.js +53 -30
  128. package/dist/server/workers/followUp.js.map +1 -1
  129. package/dist/server/workers/review.js +2 -10
  130. package/dist/server/workers/review.js.map +1 -1
  131. package/package.json +1 -1
  132. package/dist/client/assets/ActivityView-Btpsv-5U.js +0 -1
  133. package/dist/client/assets/GitHubActionsDialog-DRlIMgSB.js +0 -1
  134. package/dist/client/assets/TestsDialog-DSWDkKPV.js +0 -4
  135. package/dist/client/assets/channel-Lic13vb5.js +0 -1
  136. package/dist/client/assets/classDiagram-4FO5ZUOK-DACLwQIJ.js +0 -1
  137. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CKi6c1d9.js +0 -1
  138. package/dist/client/assets/index-DQt4dcwc.css +0 -2
  139. package/dist/client/assets/index-W9OCqkMU.js +0 -273
  140. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-lhIQ8FTe.js +0 -1
@@ -0,0 +1,674 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { spawn } from 'child_process';
3
+ import { existsSync, lstatSync, realpathSync } from 'fs';
4
+ import { relative, resolve } from 'path';
5
+ import { StringDecoder } from 'string_decoder';
6
+ import { appendCheckAttemptOutput, findActiveCheckBatchForTarget, getCheckBatchDetails, listCheckAttemptDetails, rerunCheckBatch, setCheckBatchStopRequested, } from './checkRuns.js';
7
+ import { getCheckDefinitionFingerprint } from './checkDefinitions.js';
8
+ import { captureCheckWorkspaceIdentity } from './checkWorkspaceIdentity.js';
9
+ import { getProjectById, getProjectCheckDefinitions } from './projects.js';
10
+ import { aggregateCheckBatch, completeCheckAttempt, startCheckAttempt, withExecutionRunTransaction, } from './executionRuns.js';
11
+ import { resolveCommandWorkspacePath } from './testCommandRunner.js';
12
+ const MAX_CONCURRENT_CHECK_ATTEMPTS = 2;
13
+ const OUTPUT_FLUSH_BYTES = 32 * 1024;
14
+ const OUTPUT_FLUSH_DELAY_MS = 25;
15
+ const OUTPUT_PERSIST_RETRY_DELAYS_MS = [50, 150, 500];
16
+ const STOP_GRACE_MS = 2_000;
17
+ const queue = [];
18
+ const processes = new Map();
19
+ const admittedEntries = new Map();
20
+ const invalidatedBatches = new Set();
21
+ const stoppedBatches = new Set();
22
+ const forcedBatchOutcomes = new Map();
23
+ const batchWaiters = new Map();
24
+ let admittedAttempts = 0;
25
+ let acceptingAdmissions = true;
26
+ let inFlightAdmissions = 0;
27
+ const admissionWaiters = new Set();
28
+ const assertAcceptingAdmissions = () => {
29
+ if (!acceptingAdmissions) {
30
+ throw new Error('Check runner is shutting down');
31
+ }
32
+ };
33
+ const withCheckAdmission = async (operation) => {
34
+ assertAcceptingAdmissions();
35
+ inFlightAdmissions += 1;
36
+ try {
37
+ return await operation();
38
+ }
39
+ finally {
40
+ inFlightAdmissions -= 1;
41
+ if (inFlightAdmissions === 0) {
42
+ for (const resolveWaiter of admissionWaiters) {
43
+ resolveWaiter();
44
+ }
45
+ admissionWaiters.clear();
46
+ }
47
+ }
48
+ };
49
+ const waitForCheckAdmissions = () => inFlightAdmissions === 0
50
+ ? Promise.resolve()
51
+ : new Promise(resolveWaiter => admissionWaiters.add(resolveWaiter));
52
+ export const closeCheckRunnerAdmissions = () => {
53
+ acceptingAdmissions = false;
54
+ };
55
+ export const openCheckRunnerAdmissions = () => {
56
+ acceptingAdmissions = true;
57
+ };
58
+ const resolveShell = () => process.env.SHELL?.trim() || '/bin/zsh';
59
+ const createShellEnvironment = () => {
60
+ const env = { ...process.env };
61
+ delete env.npm_config_prefix;
62
+ delete env.PORT;
63
+ return env;
64
+ };
65
+ const identitiesMatch = (left, right) => (left.state === 'available'
66
+ && right.state === 'available'
67
+ && left.fingerprint === right.fingerprint);
68
+ const isPathInside = (root, candidate) => {
69
+ const pathFromRoot = relative(root, candidate).replaceAll('\\', '/');
70
+ return pathFromRoot === '' || (!pathFromRoot.startsWith('..') && !pathFromRoot.includes('/../'));
71
+ };
72
+ const resolveDefinitionCwd = (workspaceCwd, definition) => {
73
+ const workspacePath = resolveCommandWorkspacePath(definition);
74
+ if (!workspacePath) {
75
+ return workspaceCwd;
76
+ }
77
+ const resolvedPath = resolve(workspaceCwd, workspacePath);
78
+ if (!isPathInside(workspaceCwd, resolvedPath)) {
79
+ throw new Error(`Check definition ${definition.id} working directory must stay inside the target workspace`);
80
+ }
81
+ let realWorkspacePath;
82
+ let realResolvedPath;
83
+ try {
84
+ realWorkspacePath = realpathSync(workspaceCwd);
85
+ realResolvedPath = realpathSync(resolvedPath);
86
+ }
87
+ catch {
88
+ throw new Error(`Check definition ${definition.id} working directory is unavailable`);
89
+ }
90
+ if (!isPathInside(realWorkspacePath, realResolvedPath)) {
91
+ throw new Error(`Check definition ${definition.id} working directory must stay inside the target workspace`);
92
+ }
93
+ if (!lstatSync(realResolvedPath).isDirectory()) {
94
+ throw new Error(`Check definition ${definition.id} working directory is unavailable`);
95
+ }
96
+ return definition.cwd?.trim() ? realResolvedPath : workspaceCwd;
97
+ };
98
+ const validateDefinitions = (selectedDefinitions, availableDefinitions) => {
99
+ if (selectedDefinitions.length === 0) {
100
+ throw new Error('At least one check definition must be selected');
101
+ }
102
+ const availableById = new Map(availableDefinitions.map(definition => [definition.id, definition]));
103
+ const selectedIds = new Set();
104
+ for (const definition of selectedDefinitions) {
105
+ if (selectedIds.has(definition.id)) {
106
+ throw new Error(`Check definition ${definition.id} was selected more than once`);
107
+ }
108
+ selectedIds.add(definition.id);
109
+ const available = availableById.get(definition.id);
110
+ if (!available || getCheckDefinitionFingerprint(available) !== getCheckDefinitionFingerprint(definition)) {
111
+ throw new Error(`Check definition ${definition.id} is stale or unavailable`);
112
+ }
113
+ }
114
+ };
115
+ const emitOutput = (entry, chunk) => {
116
+ const payload = appendCheckAttemptOutput({
117
+ projectId: entry.projectId,
118
+ runId: entry.attemptRunId,
119
+ chunk,
120
+ });
121
+ try {
122
+ entry.emitter?.emit('check-attempt:output', payload);
123
+ }
124
+ catch {
125
+ // Output remains durable when best-effort realtime delivery fails.
126
+ }
127
+ };
128
+ const isSqliteLockError = (error) => {
129
+ const code = error?.code;
130
+ return code === 'SQLITE_BUSY'
131
+ || code === 'SQLITE_LOCKED'
132
+ || (typeof code === 'string' && (code.startsWith('SQLITE_BUSY_') || code.startsWith('SQLITE_LOCKED_')));
133
+ };
134
+ const wait = (delayMs) => new Promise(resolveWaiter => setTimeout(resolveWaiter, delayMs));
135
+ const persistOutput = async (record, chunk) => {
136
+ for (let attempt = 0;; attempt += 1) {
137
+ try {
138
+ emitOutput(record, chunk);
139
+ return;
140
+ }
141
+ catch (error) {
142
+ const retryDelay = OUTPUT_PERSIST_RETRY_DELAYS_MS[attempt];
143
+ if (!isSqliteLockError(error) || retryDelay === undefined) {
144
+ throw error;
145
+ }
146
+ await wait(retryDelay);
147
+ }
148
+ }
149
+ };
150
+ const takeUtf8Prefix = (value, byteLimit) => {
151
+ if (Buffer.byteLength(value, 'utf8') <= byteLimit) {
152
+ return value.length;
153
+ }
154
+ let low = 0;
155
+ let high = value.length;
156
+ while (low < high) {
157
+ const midpoint = Math.ceil((low + high) / 2);
158
+ if (Buffer.byteLength(value.slice(0, midpoint), 'utf8') <= byteLimit) {
159
+ low = midpoint;
160
+ }
161
+ else {
162
+ high = midpoint - 1;
163
+ }
164
+ }
165
+ let end = low;
166
+ if (end > 0 && end < value.length) {
167
+ const previous = value.charCodeAt(end - 1);
168
+ const next = value.charCodeAt(end);
169
+ if (previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
170
+ end -= 1;
171
+ }
172
+ }
173
+ return end;
174
+ };
175
+ const flushOutput = (record) => {
176
+ if (record.flushTimer) {
177
+ clearTimeout(record.flushTimer);
178
+ record.flushTimer = null;
179
+ }
180
+ if (record.outputFlushPromise) {
181
+ return record.outputFlushPromise;
182
+ }
183
+ const flushPromise = (async () => {
184
+ while (record.pendingOutput) {
185
+ const end = takeUtf8Prefix(record.pendingOutput, OUTPUT_FLUSH_BYTES);
186
+ const chunk = record.pendingOutput.slice(0, end);
187
+ await persistOutput(record, chunk);
188
+ record.pendingOutput = record.pendingOutput.slice(end);
189
+ record.pendingOutputBytes -= Buffer.byteLength(chunk, 'utf8');
190
+ }
191
+ record.pendingOutputBytes = 0;
192
+ })();
193
+ record.outputFlushPromise = flushPromise;
194
+ void flushPromise.then(() => {
195
+ if (record.outputFlushPromise === flushPromise) {
196
+ record.outputFlushPromise = null;
197
+ }
198
+ }, () => {
199
+ if (record.outputFlushPromise === flushPromise) {
200
+ record.outputFlushPromise = null;
201
+ }
202
+ });
203
+ return flushPromise;
204
+ };
205
+ const requestOutputFlush = (record) => {
206
+ void flushOutput(record).catch(error => {
207
+ console.error('Check attempt output persistence failed', {
208
+ attempt_run_id: record.attemptRunId,
209
+ error: error instanceof Error ? error.message : String(error),
210
+ });
211
+ });
212
+ };
213
+ const appendProcessOutput = (record, text) => {
214
+ if (!text) {
215
+ return;
216
+ }
217
+ record.pendingOutput += text;
218
+ record.pendingOutputBytes += Buffer.byteLength(text, 'utf8');
219
+ if (record.pendingOutputBytes >= OUTPUT_FLUSH_BYTES) {
220
+ requestOutputFlush(record);
221
+ return;
222
+ }
223
+ if (!record.flushTimer) {
224
+ record.flushTimer = setTimeout(() => requestOutputFlush(record), OUTPUT_FLUSH_DELAY_MS);
225
+ }
226
+ };
227
+ const signalProcessGroup = (child, signal) => {
228
+ if (child.pid && process.platform !== 'win32') {
229
+ try {
230
+ process.kill(-child.pid, signal);
231
+ return;
232
+ }
233
+ catch (error) {
234
+ if (error.code === 'ESRCH') {
235
+ return;
236
+ }
237
+ }
238
+ }
239
+ try {
240
+ child.kill(signal);
241
+ }
242
+ catch {
243
+ // The process may have exited between the liveness check and the signal.
244
+ }
245
+ };
246
+ const terminateProcess = (record) => {
247
+ signalProcessGroup(record.child, 'SIGTERM');
248
+ if (!record.killTimer) {
249
+ record.killTimer = setTimeout(() => signalProcessGroup(record.child, 'SIGKILL'), STOP_GRACE_MS);
250
+ }
251
+ };
252
+ const notifyBatchWaiters = (projectId, batchRunId) => {
253
+ const details = getCheckBatchDetails(projectId, batchRunId);
254
+ if (!details || !['completed', 'failed', 'cancelled', 'interrupted'].includes(details.execution_status)) {
255
+ return;
256
+ }
257
+ const waiters = batchWaiters.get(batchRunId);
258
+ batchWaiters.delete(batchRunId);
259
+ for (const resolveWaiter of waiters ?? []) {
260
+ resolveWaiter(details);
261
+ }
262
+ };
263
+ const completeAttempt = async (entry, outcome, finalIdentity, options = {}) => {
264
+ completeCheckAttempt({
265
+ projectId: entry.projectId,
266
+ runId: entry.attemptRunId,
267
+ outcome,
268
+ finalWorkspaceIdentity: finalIdentity,
269
+ exitCode: options.exitCode,
270
+ exitSignal: options.exitSignal,
271
+ exitError: options.exitError,
272
+ }, entry.emitter);
273
+ aggregateCheckBatch(entry.projectId, entry.batchRunId, entry.emitter);
274
+ notifyBatchWaiters(entry.projectId, entry.batchRunId);
275
+ };
276
+ const finalizeProcess = (record, code, signal, error) => {
277
+ if (record.finalizationPromise) {
278
+ return record.finalizationPromise;
279
+ }
280
+ record.finalized = true;
281
+ if (record.timeoutTimer) {
282
+ clearTimeout(record.timeoutTimer);
283
+ }
284
+ if (record.killTimer) {
285
+ clearTimeout(record.killTimer);
286
+ }
287
+ record.finalizationPromise = (async () => {
288
+ appendProcessOutput(record, record.stdoutDecoder.end());
289
+ appendProcessOutput(record, record.stderrDecoder.end());
290
+ await flushOutput(record);
291
+ const outcome = record.terminalCause
292
+ ?? (error ? 'spawn_failure' : code === 0 ? 'passed' : 'command_failure');
293
+ const finalIdentity = await captureCheckWorkspaceIdentity(record.cwd);
294
+ await completeAttempt(record, outcome, finalIdentity, {
295
+ exitCode: code,
296
+ exitSignal: signal,
297
+ exitError: error?.message ?? (outcome === 'timeout' ? `Check timed out after ${record.definition.timeout_ms}ms` : null),
298
+ });
299
+ processes.delete(record.attemptRunId);
300
+ })();
301
+ return record.finalizationPromise;
302
+ };
303
+ const failQueuedBatch = async (batchRunId, outcome, identity) => {
304
+ const removed = queue.filter(entry => entry.batchRunId === batchRunId);
305
+ for (let index = queue.length - 1; index >= 0; index -= 1) {
306
+ if (queue[index]?.batchRunId === batchRunId) {
307
+ queue.splice(index, 1);
308
+ }
309
+ }
310
+ for (const entry of removed) {
311
+ await completeAttempt(entry, outcome, identity, {
312
+ exitError: outcome === 'workspace_changed_before_start'
313
+ ? 'Workspace identity changed before this check could start'
314
+ : null,
315
+ });
316
+ }
317
+ };
318
+ const runQueuedAttempt = async (entry) => {
319
+ const identity = await captureCheckWorkspaceIdentity(entry.cwd);
320
+ const forcedOutcome = forcedBatchOutcomes.get(entry.batchRunId);
321
+ if (forcedOutcome) {
322
+ await completeAttempt(entry, forcedOutcome, identity, {
323
+ exitError: forcedOutcome === 'shutdown_loss'
324
+ ? 'The server shut down before this check attempt completed'
325
+ : 'The server restarted before this check attempt completed',
326
+ });
327
+ return;
328
+ }
329
+ if (stoppedBatches.has(entry.batchRunId)) {
330
+ await completeAttempt(entry, 'user_stop', identity);
331
+ return;
332
+ }
333
+ if (invalidatedBatches.has(entry.batchRunId) || !identitiesMatch(entry.initialIdentity, identity)) {
334
+ invalidatedBatches.add(entry.batchRunId);
335
+ await completeAttempt(entry, 'workspace_changed_before_start', identity, {
336
+ exitError: 'Workspace identity changed before this check could start',
337
+ });
338
+ await failQueuedBatch(entry.batchRunId, 'workspace_changed_before_start', identity);
339
+ return;
340
+ }
341
+ const spawnCwd = resolveDefinitionCwd(entry.cwd, entry.definition);
342
+ startCheckAttempt({
343
+ projectId: entry.projectId,
344
+ runId: entry.attemptRunId,
345
+ }, entry.emitter);
346
+ await new Promise((resolveAttempt, rejectAttempt) => {
347
+ let child;
348
+ try {
349
+ child = spawn(resolveShell(), ['-i', '-c', entry.definition.command], {
350
+ cwd: spawnCwd,
351
+ detached: process.platform !== 'win32',
352
+ env: createShellEnvironment(),
353
+ stdio: ['ignore', 'pipe', 'pipe'],
354
+ });
355
+ }
356
+ catch (error) {
357
+ const spawnError = error instanceof Error ? error : new Error(String(error));
358
+ void captureCheckWorkspaceIdentity(entry.cwd)
359
+ .then(finalIdentity => completeAttempt(entry, 'spawn_failure', finalIdentity, { exitError: spawnError.message }))
360
+ .then(resolveAttempt, rejectAttempt);
361
+ return;
362
+ }
363
+ const record = {
364
+ ...entry,
365
+ child,
366
+ stdoutDecoder: new StringDecoder('utf8'),
367
+ stderrDecoder: new StringDecoder('utf8'),
368
+ pendingOutput: '',
369
+ pendingOutputBytes: 0,
370
+ flushTimer: null,
371
+ outputFlushPromise: null,
372
+ timeoutTimer: null,
373
+ killTimer: null,
374
+ terminalCause: null,
375
+ spawned: false,
376
+ finalized: false,
377
+ finalizationPromise: null,
378
+ };
379
+ processes.set(entry.attemptRunId, record);
380
+ child.once('spawn', () => {
381
+ record.spawned = true;
382
+ });
383
+ child.stdout?.on('data', chunk => appendProcessOutput(record, record.stdoutDecoder.write(Buffer.from(chunk))));
384
+ child.stderr?.on('data', chunk => appendProcessOutput(record, record.stderrDecoder.write(Buffer.from(chunk))));
385
+ child.on('error', error => {
386
+ if (!record.spawned) {
387
+ record.terminalCause ??= 'spawn_failure';
388
+ void finalizeProcess(record, null, null, error).then(resolveAttempt, rejectAttempt);
389
+ }
390
+ });
391
+ child.on('close', (code, signal) => {
392
+ void finalizeProcess(record, code, signal).then(resolveAttempt, rejectAttempt);
393
+ });
394
+ record.timeoutTimer = setTimeout(() => {
395
+ record.terminalCause ??= 'timeout';
396
+ terminateProcess(record);
397
+ }, entry.definition.timeout_ms);
398
+ });
399
+ };
400
+ const pumpScheduler = () => {
401
+ while (admittedAttempts < MAX_CONCURRENT_CHECK_ATTEMPTS && queue.length > 0) {
402
+ const entry = queue.shift();
403
+ if (!entry) {
404
+ return;
405
+ }
406
+ admittedAttempts += 1;
407
+ admittedEntries.set(entry.attemptRunId, entry);
408
+ void runQueuedAttempt(entry)
409
+ .catch(async (error) => {
410
+ console.error('Check attempt scheduler failure', {
411
+ attempt_run_id: entry.attemptRunId,
412
+ error: error instanceof Error ? error.message : String(error),
413
+ });
414
+ const attempt = listCheckAttemptDetails(entry.projectId, entry.batchRunId)
415
+ .find(candidate => candidate.execution_run_id === entry.attemptRunId);
416
+ if (!attempt?.outcome) {
417
+ const finalIdentity = await captureCheckWorkspaceIdentity(entry.cwd);
418
+ await completeAttempt(entry, 'spawn_failure', finalIdentity, {
419
+ exitError: error instanceof Error ? error.message : String(error),
420
+ });
421
+ }
422
+ else {
423
+ aggregateCheckBatch(entry.projectId, entry.batchRunId, entry.emitter);
424
+ notifyBatchWaiters(entry.projectId, entry.batchRunId);
425
+ }
426
+ processes.delete(entry.attemptRunId);
427
+ })
428
+ .catch(error => {
429
+ console.error('Check attempt scheduler recovery failed', {
430
+ attempt_run_id: entry.attemptRunId,
431
+ error: error instanceof Error ? error.message : String(error),
432
+ });
433
+ })
434
+ .finally(() => {
435
+ admittedEntries.delete(entry.attemptRunId);
436
+ admittedAttempts -= 1;
437
+ pumpScheduler();
438
+ });
439
+ }
440
+ };
441
+ const enqueueCheckAttempts = (input) => {
442
+ queue.push(...input.attempts.map(attempt => ({
443
+ projectId: input.projectId,
444
+ batchRunId: input.batchRunId,
445
+ attemptRunId: attempt.runId,
446
+ cwd: input.cwd,
447
+ definition: attempt.definition,
448
+ initialIdentity: input.initialIdentity,
449
+ emitter: input.emitter,
450
+ })));
451
+ pumpScheduler();
452
+ };
453
+ export const startCheckBatch = async (input, emitter) => withCheckAdmission(async () => {
454
+ const targetBundleId = input.targetKind === 'bundle' ? input.targetBundleId ?? null : null;
455
+ if (input.targetKind === 'bundle' && !targetBundleId) {
456
+ throw new Error('Bundle check batches require a target bundle ID');
457
+ }
458
+ if (!existsSync(input.cwd) || !lstatSync(input.cwd).isDirectory()) {
459
+ throw new Error('Check target workspace is unavailable');
460
+ }
461
+ validateDefinitions(input.selectedDefinitions, input.availableDefinitions ?? input.selectedDefinitions);
462
+ for (const definition of input.selectedDefinitions) {
463
+ resolveDefinitionCwd(input.cwd, definition);
464
+ }
465
+ const conflict = findActiveCheckBatchForTarget(input.projectId, input.targetKind, targetBundleId);
466
+ if (conflict) {
467
+ throw new Error(`Check batch ${conflict} is already active for this target`);
468
+ }
469
+ const initialIdentity = await captureCheckWorkspaceIdentity(input.cwd);
470
+ if (initialIdentity.state !== 'available') {
471
+ throw new Error(`Check workspace identity is unavailable: ${initialIdentity.message}`);
472
+ }
473
+ const currentDefinitions = getProjectCheckDefinitions(getProjectById(input.projectId));
474
+ validateDefinitions(input.selectedDefinitions, currentDefinitions);
475
+ for (const definition of input.selectedDefinitions) {
476
+ resolveDefinitionCwd(input.cwd, definition);
477
+ }
478
+ assertAcceptingAdmissions();
479
+ const batchRunId = randomUUID();
480
+ const attempts = input.selectedDefinitions.map(definition => ({ runId: randomUUID(), definition }));
481
+ const checkTarget = input.targetKind === 'bundle'
482
+ ? { targetKind: 'bundle', targetBundleId: targetBundleId }
483
+ : { targetKind: 'repo_root', targetBundleId: null };
484
+ const scopeLinks = targetBundleId
485
+ ? [{ entityType: 'bundle', entityId: targetBundleId, relationship: 'context' }]
486
+ : [];
487
+ withExecutionRunTransaction(transaction => {
488
+ const transactionConflict = findActiveCheckBatchForTarget(input.projectId, input.targetKind, targetBundleId);
489
+ if (transactionConflict) {
490
+ throw new Error(`Check batch ${transactionConflict} is already active for this target`);
491
+ }
492
+ transaction.createExecutionRun({
493
+ id: batchRunId,
494
+ projectId: input.projectId,
495
+ kind: 'check_batch',
496
+ initiatedBy: input.automationParentRunId ? 'automation' : 'user',
497
+ parentRunId: input.automationParentRunId ?? null,
498
+ domainStatus: 'queued',
499
+ sourceReference: input.sourceReference ?? null,
500
+ links: scopeLinks,
501
+ eventType: 'check_batch_created',
502
+ checkBatchDetails: {
503
+ ...checkTarget,
504
+ targetLabel: input.targetLabel,
505
+ selectedDefinitions: input.selectedDefinitions,
506
+ initialWorkspaceIdentity: initialIdentity,
507
+ rerunOfBatchRunId: input.rerunOfBatchRunId,
508
+ },
509
+ });
510
+ for (const attempt of attempts) {
511
+ transaction.createExecutionRun({
512
+ id: attempt.runId,
513
+ projectId: input.projectId,
514
+ kind: 'check_attempt',
515
+ parentRunId: batchRunId,
516
+ sourceReference: { kind: 'project', id: input.projectId },
517
+ links: scopeLinks,
518
+ domainStatus: 'queued',
519
+ eventType: 'check_attempt_created',
520
+ checkAttemptDetails: {
521
+ batchRunId,
522
+ definition: attempt.definition,
523
+ initialWorkspaceIdentity: initialIdentity,
524
+ rerunOfAttemptRunId: input.rerunAttemptRunIds?.[attempt.definition.id] ?? null,
525
+ },
526
+ });
527
+ }
528
+ transaction.transitionExecutionRun({
529
+ projectId: input.projectId,
530
+ runId: batchRunId,
531
+ status: 'running',
532
+ domainStatus: 'running',
533
+ eventType: 'check_batch_started',
534
+ });
535
+ }, emitter);
536
+ enqueueCheckAttempts({
537
+ projectId: input.projectId,
538
+ batchRunId,
539
+ cwd: input.cwd,
540
+ attempts,
541
+ initialIdentity,
542
+ emitter,
543
+ });
544
+ return getCheckBatchDetails(input.projectId, batchRunId);
545
+ });
546
+ export const rerunCheckBatchAndSchedule = async (input, emitter) => withCheckAdmission(async () => {
547
+ if (!existsSync(input.cwd) || !lstatSync(input.cwd).isDirectory()) {
548
+ throw new Error('Check target workspace is unavailable');
549
+ }
550
+ const sourceAttempts = listCheckAttemptDetails(input.projectId, input.sourceBatchRunId);
551
+ const selectedDefinitionIds = input.selection.mode === 'all'
552
+ ? sourceAttempts.map(attempt => attempt.definition_id)
553
+ : input.selection.mode === 'failed'
554
+ ? sourceAttempts.filter(attempt => attempt.outcome !== 'passed').map(attempt => attempt.definition_id)
555
+ : input.selection.definitionIds;
556
+ const selectedDefinitionIdSet = new Set(selectedDefinitionIds);
557
+ for (const definition of input.availableDefinitions.filter(candidate => selectedDefinitionIdSet.has(candidate.id))) {
558
+ resolveDefinitionCwd(input.cwd, definition);
559
+ }
560
+ const initialIdentity = await captureCheckWorkspaceIdentity(input.cwd);
561
+ if (initialIdentity.state !== 'available') {
562
+ throw new Error(`Check workspace identity is unavailable: ${initialIdentity.message}`);
563
+ }
564
+ const currentDefinitions = getProjectCheckDefinitions(getProjectById(input.projectId));
565
+ const currentById = new Map(currentDefinitions.map(definition => [definition.id, definition]));
566
+ for (const definitionId of selectedDefinitionIds) {
567
+ const definition = currentById.get(definitionId);
568
+ if (definition) {
569
+ resolveDefinitionCwd(input.cwd, definition);
570
+ }
571
+ }
572
+ assertAcceptingAdmissions();
573
+ const created = withExecutionRunTransaction(transaction => rerunCheckBatch({
574
+ transaction,
575
+ projectId: input.projectId,
576
+ sourceBatchRunId: input.sourceBatchRunId,
577
+ selection: input.selection,
578
+ availableDefinitions: currentDefinitions,
579
+ initialWorkspaceIdentity: initialIdentity,
580
+ }), emitter);
581
+ enqueueCheckAttempts({
582
+ projectId: input.projectId,
583
+ batchRunId: created.batchRunId,
584
+ cwd: input.cwd,
585
+ attempts: created.attempts,
586
+ initialIdentity,
587
+ emitter,
588
+ });
589
+ return {
590
+ batch: created.batch,
591
+ attempts: listCheckAttemptDetails(input.projectId, created.batchRunId),
592
+ };
593
+ });
594
+ export const stopCheckBatch = async (projectId, batchRunId, emitter) => {
595
+ const current = getCheckBatchDetails(projectId, batchRunId);
596
+ if (!current) {
597
+ throw new Error(`Check batch ${batchRunId} was not found in project ${projectId}`);
598
+ }
599
+ if (['completed', 'failed', 'cancelled', 'interrupted'].includes(current.execution_status)) {
600
+ return current;
601
+ }
602
+ const batch = setCheckBatchStopRequested(projectId, batchRunId);
603
+ stoppedBatches.add(batchRunId);
604
+ const identity = await captureCheckWorkspaceIdentity([...processes.values()].find(record => record.batchRunId === batchRunId)?.cwd
605
+ ?? queue.find(entry => entry.batchRunId === batchRunId)?.cwd
606
+ ?? '');
607
+ await failQueuedBatch(batchRunId, 'user_stop', identity);
608
+ for (const record of processes.values()) {
609
+ if (record.batchRunId === batchRunId && !record.finalized) {
610
+ record.terminalCause ??= 'user_stop';
611
+ terminateProcess(record);
612
+ }
613
+ }
614
+ aggregateCheckBatch(projectId, batchRunId, emitter);
615
+ notifyBatchWaiters(projectId, batchRunId);
616
+ return getCheckBatchDetails(projectId, batchRunId);
617
+ };
618
+ export const waitForCheckBatch = (projectId, batchRunId) => {
619
+ const details = getCheckBatchDetails(projectId, batchRunId);
620
+ if (!details) {
621
+ return Promise.reject(new Error(`Check batch ${batchRunId} was not found in project ${projectId}`));
622
+ }
623
+ if (['completed', 'failed', 'cancelled', 'interrupted'].includes(details.execution_status)) {
624
+ return Promise.resolve(details);
625
+ }
626
+ return new Promise(resolveWaiter => {
627
+ const waiters = batchWaiters.get(batchRunId) ?? new Set();
628
+ waiters.add(resolveWaiter);
629
+ batchWaiters.set(batchRunId, waiters);
630
+ });
631
+ };
632
+ export const getCheckRunnerState = () => ({
633
+ active_attempt_count: admittedAttempts,
634
+ queued_attempt_count: queue.length,
635
+ process_attempt_ids: [...processes.keys()],
636
+ });
637
+ export const shutdownCheckRunner = async () => {
638
+ closeCheckRunnerAdmissions();
639
+ await waitForCheckAdmissions();
640
+ const batchIds = new Set([
641
+ ...queue.map(entry => entry.batchRunId),
642
+ ...[...admittedEntries.values()].map(entry => entry.batchRunId),
643
+ ...[...processes.values()].map(record => record.batchRunId),
644
+ ]);
645
+ try {
646
+ for (const batchRunId of batchIds) {
647
+ forcedBatchOutcomes.set(batchRunId, 'shutdown_loss');
648
+ }
649
+ const queuedEntries = queue.splice(0);
650
+ for (const entry of queuedEntries) {
651
+ const finalIdentity = await captureCheckWorkspaceIdentity(entry.cwd);
652
+ await completeAttempt(entry, 'shutdown_loss', finalIdentity, {
653
+ exitError: 'The server shut down before this check attempt started',
654
+ });
655
+ }
656
+ for (const record of processes.values()) {
657
+ record.terminalCause ??= 'shutdown_loss';
658
+ terminateProcess(record);
659
+ }
660
+ await Promise.all([...batchIds].map(batchRunId => {
661
+ const projectId = [...admittedEntries.values()].find(entry => entry.batchRunId === batchRunId)?.projectId
662
+ ?? [...processes.values()].find(record => record.batchRunId === batchRunId)?.projectId
663
+ ?? queuedEntries.find(entry => entry.batchRunId === batchRunId)?.projectId;
664
+ return projectId ? waitForCheckBatch(projectId, batchRunId) : Promise.resolve(null);
665
+ }));
666
+ }
667
+ finally {
668
+ for (const batchRunId of batchIds) {
669
+ forcedBatchOutcomes.delete(batchRunId);
670
+ }
671
+ }
672
+ };
673
+ export const getCheckBatchAttempts = (projectId, batchRunId) => (listCheckAttemptDetails(projectId, batchRunId));
674
+ //# sourceMappingURL=checkRunner.js.map