agent-dealer 1.2.13 → 1.2.15
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.
- package/bundle/server/dist/adapters/git-worktree.js +15 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +30 -0
- package/bundle/server/dist/adapters/linear-inbox.js +97 -0
- package/bundle/server/dist/adapters/linear-inbox.test.js +299 -1
- package/bundle/server/dist/adapters/linear-sync.js +33 -10
- package/bundle/server/dist/capacity/claude-events.js +25 -5
- package/bundle/server/dist/capacity/claude-local-cache.js +159 -13
- package/bundle/server/dist/capacity/claude-local-cache.test.js +191 -3
- package/bundle/server/dist/coordinator/auto-merge.js +10 -0
- package/bundle/server/dist/coordinator/branch-progress.js +5 -4
- package/bundle/server/dist/coordinator/commands.js +37 -3
- package/bundle/server/dist/coordinator/delete-issue.js +473 -0
- package/bundle/server/dist/coordinator/developer-effect.js +33 -6
- package/bundle/server/dist/coordinator/linear-merge-verify.js +382 -0
- package/bundle/server/dist/coordinator/linear-merge-verify.test.js +376 -0
- package/bundle/server/dist/coordinator/prompts.js +9 -1
- package/bundle/server/dist/coordinator/prompts.test.js +93 -0
- package/bundle/server/dist/coordinator/source-attachments.js +496 -0
- package/bundle/server/dist/coordinator/source-attachments.test.js +396 -0
- package/bundle/server/dist/coordinator/source-reload.js +178 -0
- package/bundle/server/dist/db/index.js +4 -0
- package/bundle/server/dist/db/schema.sql +32 -0
- package/bundle/server/dist/docs-linear-attachments.test.js +36 -0
- package/bundle/server/dist/paths.js +12 -0
- package/bundle/server/dist/repository/runtime-capacity.js +19 -3
- package/bundle/server/dist/repository/source-attachments.js +56 -0
- package/bundle/server/dist/routes/human-actions.js +17 -0
- package/bundle/server/dist/routes/issues-delete.test.js +666 -0
- package/bundle/server/dist/routes/issues-execution-contract.test.js +296 -0
- package/bundle/server/dist/routes/issues-source-attachments.test.js +447 -0
- package/bundle/server/dist/routes/issues.js +109 -1
- package/bundle/server/dist/routes/runtime-capacity.test.js +35 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-DVyqblie.js +64 -0
- package/bundle/server/static-ui/index.html +1 -1
- package/bundle/shared/dist/index.d.ts +108 -2
- package/bundle/shared/dist/index.js +12 -1
- package/bundle/shared/dist/issues.d.ts +43 -2
- package/bundle/shared/dist/issues.js +10 -0
- package/bundle/shared/dist/linear-intake.d.ts +85 -0
- package/bundle/shared/dist/linear-intake.js +56 -0
- package/bundle/shared/dist/linear-intake.test.js +55 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +147 -0
- package/bundle/shared/dist/runtime-capacity.js +15 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +7 -0
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-BzLjMBI9.js +0 -64
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// packages/server/src/coordinator/linear-merge-verify.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-362: when Dealer merges an approved PR for a `source = linear` issue, the
|
|
4
|
+
// Linear issue it came from must either advance, or a human must be told it did
|
|
5
|
+
// not. Linear's GitHub integration normally drives that (attaching the PR and
|
|
6
|
+
// moving the issue out of Backlog), but on repositories the integration does
|
|
7
|
+
// not cover nothing fires and Dealer recorded nothing — the issue sat stale
|
|
8
|
+
// with zero evidence anywhere.
|
|
9
|
+
//
|
|
10
|
+
// The check runs after the coordinator's terminal merged transition
|
|
11
|
+
// (finalizeAutoMerge's `done`, plus the external-merge close-as-`done` paths):
|
|
12
|
+
// re-read the Linear issue over a bounded retry window and treat it as advanced
|
|
13
|
+
// when its state type left `backlog`/`unstarted` or its attachments contain the
|
|
14
|
+
// merged PR URL. Every outcome is recorded as an issue artifact (both
|
|
15
|
+
// directions — a stale source issue appears in history instead of being
|
|
16
|
+
// inferred from absence). A stale issue with `linear.syncEnabled` gets exactly
|
|
17
|
+
// one fallback write to the team's completed state plus the Dealer comment,
|
|
18
|
+
// reusing linear-sync's writer; when the fallback is unavailable or fails, one
|
|
19
|
+
// open `policy_escalation` names the Linear identifier, the merged PR URL and
|
|
20
|
+
// the observed state (idempotent re-raise by stable request id).
|
|
21
|
+
//
|
|
22
|
+
// Safety: this module never touches the Dealer issue's status, never blocks
|
|
23
|
+
// the merge, and never throws — every `done` landing triggers it fire-and-forget
|
|
24
|
+
// (`triggerLinearPostMerge`), so the bounded retry window never holds the merge
|
|
25
|
+
// caller. The raised action is a notice, not a gate: it resolves through a
|
|
26
|
+
// dedicated acknowledge/re-check resolution (no active workflow to advance on a
|
|
27
|
+
// `done` issue), and advancing the Linear issue by hand then re-checking clears
|
|
28
|
+
// it when the re-read verifies.
|
|
29
|
+
import { createIssueArtifact } from "../repository/artifacts.js";
|
|
30
|
+
import { listArtifactsForIssueByKind } from "../repository/artifacts-for-issue.js";
|
|
31
|
+
import { createHumanAction, findOpenHumanActionByRequestId, getHumanAction, resolveHumanAction, } from "../repository/human-actions.js";
|
|
32
|
+
import { getIssue } from "../repository/issues.js";
|
|
33
|
+
import { getLinearIntakeConfig } from "../repository/intake-settings.js";
|
|
34
|
+
import { linearGraphqlRequest } from "../adapters/linear-graphql.js";
|
|
35
|
+
import { buildDoneComment, dealerIssueUrl, postLinearIssueComment, resolveCompletedStateId, setLinearIssueState, } from "../adapters/linear-sync.js";
|
|
36
|
+
/** Issue-artifact kind for every post-merge verification outcome. */
|
|
37
|
+
export const LINEAR_MERGE_VERIFY_ARTIFACT_KIND = "linear_merge_verify";
|
|
38
|
+
/** Stable request id for the stale-source human action — re-runs dedupe onto it. */
|
|
39
|
+
export function linearMergeStaleRequestId(issueId) {
|
|
40
|
+
return `linear-merge-stale:${issueId}`;
|
|
41
|
+
}
|
|
42
|
+
/** Bounded retry window so the GitHub integration has time to act. */
|
|
43
|
+
export const LINEAR_POST_MERGE_READ_DELAYS_MS = [0, 10_000, 30_000];
|
|
44
|
+
async function defaultPostMergeReader(externalId) {
|
|
45
|
+
const data = (await linearGraphqlRequest({
|
|
46
|
+
operation: "readLinearPostMergeState",
|
|
47
|
+
query: `query PostMergeState($id: String!) {
|
|
48
|
+
issue(id: $id) {
|
|
49
|
+
identifier
|
|
50
|
+
url
|
|
51
|
+
state { name type }
|
|
52
|
+
team { id }
|
|
53
|
+
attachments(first: 50) { nodes { url } }
|
|
54
|
+
}
|
|
55
|
+
}`,
|
|
56
|
+
variables: { id: externalId },
|
|
57
|
+
timeoutMs: 15_000,
|
|
58
|
+
}));
|
|
59
|
+
if (!data.issue)
|
|
60
|
+
throw new Error(`Linear issue not found: ${externalId}`);
|
|
61
|
+
const urls = (data.issue.attachments?.nodes ?? [])
|
|
62
|
+
.map((n) => n?.url)
|
|
63
|
+
.filter((u) => typeof u === "string" && u.length > 0);
|
|
64
|
+
return {
|
|
65
|
+
identifier: data.issue.identifier ?? null,
|
|
66
|
+
linearUrl: data.issue.url ?? null,
|
|
67
|
+
stateName: data.issue.state?.name ?? null,
|
|
68
|
+
stateType: data.issue.state?.type ?? null,
|
|
69
|
+
teamId: data.issue.team?.id ?? null,
|
|
70
|
+
attachmentUrls: urls,
|
|
71
|
+
prAttached: false,
|
|
72
|
+
advancedViaState: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let postMergeReader = defaultPostMergeReader;
|
|
76
|
+
/** Test hook — inject a fake Linear reader so tests never hit the network. */
|
|
77
|
+
export function setPostMergeReaderForTests(fn) {
|
|
78
|
+
postMergeReader = fn ?? defaultPostMergeReader;
|
|
79
|
+
}
|
|
80
|
+
let readDelaysMs = LINEAR_POST_MERGE_READ_DELAYS_MS;
|
|
81
|
+
/** Test hook — shrink the retry window (e.g. `[0, 0]`) so tests stay fast. */
|
|
82
|
+
export function setPostMergeDelaysForTests(delays) {
|
|
83
|
+
readDelaysMs = delays ?? LINEAR_POST_MERGE_READ_DELAYS_MS;
|
|
84
|
+
}
|
|
85
|
+
function sleep(ms) {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
// Unref'd: this check is fire-and-forget off every `done` landing and must
|
|
88
|
+
// never hold the merge caller — or a test runner's process — open on its own.
|
|
89
|
+
const timer = setTimeout(resolve, ms);
|
|
90
|
+
if (typeof timer.unref === "function")
|
|
91
|
+
timer.unref();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** State types that still mean "the integration has not acted". */
|
|
95
|
+
function isStaleStateType(stateType) {
|
|
96
|
+
const t = (stateType ?? "").toLowerCase();
|
|
97
|
+
return t === "" || t === "backlog" || t === "unstarted";
|
|
98
|
+
}
|
|
99
|
+
function fallbackAlreadyWritten(issueId) {
|
|
100
|
+
for (const artifact of listArtifactsForIssueByKind(issueId, LINEAR_MERGE_VERIFY_ARTIFACT_KIND)) {
|
|
101
|
+
try {
|
|
102
|
+
if (JSON.parse(artifact.contentJson ?? "null")?.fallback === "written") {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A row this module did not write — ignore, never treat as a guard.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function dismissOpenStaleAction(issueId, note) {
|
|
113
|
+
const open = findOpenHumanActionByRequestId(issueId, "policy_escalation", linearMergeStaleRequestId(issueId));
|
|
114
|
+
if (open) {
|
|
115
|
+
try {
|
|
116
|
+
resolveHumanAction(open.id, "system", { choice: "dismissed", note });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Resolving the notice must never fail the verification.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function staleReasonText(opts) {
|
|
124
|
+
const observed = opts.unreadable
|
|
125
|
+
? `could not be re-read (${opts.unreadable})`
|
|
126
|
+
: `still ${opts.stateName ?? "unknown state"}${opts.stateType ? ` (${opts.stateType})` : ""}`;
|
|
127
|
+
return (`Linear ${opts.identifier} ${observed} after the merged PR ${opts.prUrl ?? "(no PR URL recorded)"}. ` +
|
|
128
|
+
`Linear's GitHub integration did not advance it, so advance it by hand. ` +
|
|
129
|
+
`The Dealer issue itself is done and stays done — this action is a notice, not a gate.`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Re-read the Linear source issue after a merged terminal transition and either
|
|
133
|
+
* confirm it advanced, advance it once via the conditional fallback, or raise
|
|
134
|
+
* the one human action. Records an artifact in every direction. Never throws
|
|
135
|
+
* and never touches the Dealer issue's status.
|
|
136
|
+
*/
|
|
137
|
+
// NOT-362 round 3: per-issue in-flight guard. The stale fallback records
|
|
138
|
+
// `fallback: "written"` only via the artifact written *after* the Linear state
|
|
139
|
+
// + comment mutations, so two overlapping checks for one issue (finalize's
|
|
140
|
+
// done-already branch re-entered by recovery, or abort/resolve racing
|
|
141
|
+
// finalize) could both observe stale and each write. Concurrent checks for one
|
|
142
|
+
// issue join the in-flight run instead of running twice; sequential re-runs
|
|
143
|
+
// still execute and stay single-write via the artifact guard.
|
|
144
|
+
const verifyInflightByIssue = new Map();
|
|
145
|
+
export async function verifyLinearPostMerge(issueId) {
|
|
146
|
+
const inflight = verifyInflightByIssue.get(issueId);
|
|
147
|
+
if (inflight)
|
|
148
|
+
return inflight;
|
|
149
|
+
const pending = verifyLinearPostMergeInner(issueId).then((result) => result, (err) => ({ checked: false, reason: err instanceof Error ? err.message : String(err) }));
|
|
150
|
+
verifyInflightByIssue.set(issueId, pending);
|
|
151
|
+
try {
|
|
152
|
+
return await pending;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (verifyInflightByIssue.get(issueId) === pending)
|
|
156
|
+
verifyInflightByIssue.delete(issueId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function verifyLinearPostMergeInner(issueId) {
|
|
160
|
+
const issue = getIssue(issueId);
|
|
161
|
+
if (!issue || issue.source !== "linear" || !issue.externalId) {
|
|
162
|
+
return { checked: false, reason: "not a Linear-sourced issue" };
|
|
163
|
+
}
|
|
164
|
+
const linearId = issue.externalId;
|
|
165
|
+
const prUrl = issue.prUrl;
|
|
166
|
+
let observation = null;
|
|
167
|
+
let readError = null;
|
|
168
|
+
for (let attempt = 0; attempt < readDelaysMs.length; attempt += 1) {
|
|
169
|
+
if (readDelaysMs[attempt] > 0)
|
|
170
|
+
await sleep(readDelaysMs[attempt]);
|
|
171
|
+
try {
|
|
172
|
+
const read = await postMergeReader(linearId);
|
|
173
|
+
const advancedViaState = !isStaleStateType(read.stateType);
|
|
174
|
+
const prAttached = prUrl != null && read.attachmentUrls.includes(prUrl);
|
|
175
|
+
observation = { ...read, prAttached, advancedViaState };
|
|
176
|
+
if (advancedViaState || prAttached)
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
readError = err instanceof Error ? err.message : String(err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const identifier = observation?.identifier ?? issue.externalLabel ?? linearId;
|
|
184
|
+
const reads = observation
|
|
185
|
+
? {
|
|
186
|
+
stateName: observation.stateName,
|
|
187
|
+
stateType: observation.stateType,
|
|
188
|
+
prAttached: observation.prAttached,
|
|
189
|
+
}
|
|
190
|
+
: { error: readError };
|
|
191
|
+
if (observation && (observation.advancedViaState || observation.prAttached)) {
|
|
192
|
+
const advancedVia = observation.advancedViaState ? "state" : "attachment";
|
|
193
|
+
createIssueArtifact({
|
|
194
|
+
issueId,
|
|
195
|
+
kind: LINEAR_MERGE_VERIFY_ARTIFACT_KIND,
|
|
196
|
+
author: "system",
|
|
197
|
+
content: {
|
|
198
|
+
check: "post_merge",
|
|
199
|
+
linearId,
|
|
200
|
+
identifier,
|
|
201
|
+
prUrl,
|
|
202
|
+
reads,
|
|
203
|
+
outcome: "advanced",
|
|
204
|
+
advancedVia,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
dismissOpenStaleAction(issueId, `Linear ${identifier} advanced (${advancedVia}); notice cleared.`);
|
|
208
|
+
return { checked: true, advanced: true, advancedVia };
|
|
209
|
+
}
|
|
210
|
+
// Stale (or unreadable): the fallback fires only after the integration has
|
|
211
|
+
// demonstrably not acted — an unreadable issue never reaches the writer.
|
|
212
|
+
const syncEnabled = getLinearIntakeConfig().syncEnabled && Boolean(process.env.LINEAR_API_KEY);
|
|
213
|
+
const teamId = observation?.teamId ?? null;
|
|
214
|
+
let fallback = "skipped";
|
|
215
|
+
if (syncEnabled && teamId && !fallbackAlreadyWritten(issueId)) {
|
|
216
|
+
try {
|
|
217
|
+
const stateId = await resolveCompletedStateId(teamId);
|
|
218
|
+
if (!stateId) {
|
|
219
|
+
fallback = "unavailable:no-completed-state";
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// State first, comment second: a rejected state write then posts no
|
|
223
|
+
// comment, so re-runs do not stack "marked done" comments that were
|
|
224
|
+
// never true. (A comment failure after a successful state write still
|
|
225
|
+
// records `failed:…` and the re-run rewrites the same state —
|
|
226
|
+
// idempotent — before retrying the comment.)
|
|
227
|
+
const label = issue.externalLabel ?? identifier;
|
|
228
|
+
await setLinearIssueState(linearId, stateId);
|
|
229
|
+
await postLinearIssueComment(linearId, buildDoneComment(label, dealerIssueUrl(issueId), "View issue"));
|
|
230
|
+
fallback = "written";
|
|
231
|
+
dismissOpenStaleAction(issueId, `Dealer fallback advanced Linear ${identifier}; notice cleared.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
fallback = `failed:${err instanceof Error ? err.message : String(err)}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (fallbackAlreadyWritten(issueId)) {
|
|
239
|
+
fallback = "already_recorded";
|
|
240
|
+
}
|
|
241
|
+
else if (!syncEnabled) {
|
|
242
|
+
fallback = "unavailable:sync-disabled";
|
|
243
|
+
}
|
|
244
|
+
else if (!teamId) {
|
|
245
|
+
fallback = "unavailable:no-team";
|
|
246
|
+
}
|
|
247
|
+
let actionId = null;
|
|
248
|
+
if (fallback !== "written" && fallback !== "already_recorded") {
|
|
249
|
+
const reason = staleReasonText({
|
|
250
|
+
identifier,
|
|
251
|
+
prUrl,
|
|
252
|
+
stateName: observation?.stateName ?? null,
|
|
253
|
+
stateType: observation?.stateType ?? null,
|
|
254
|
+
unreadable: observation ? null : readError,
|
|
255
|
+
});
|
|
256
|
+
const requestId = linearMergeStaleRequestId(issueId);
|
|
257
|
+
const open = findOpenHumanActionByRequestId(issueId, "policy_escalation", requestId);
|
|
258
|
+
if (open) {
|
|
259
|
+
actionId = open.id;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const action = createHumanAction({
|
|
263
|
+
issueId,
|
|
264
|
+
workflowInstanceId: null,
|
|
265
|
+
actionType: "policy_escalation",
|
|
266
|
+
reason,
|
|
267
|
+
question: reason,
|
|
268
|
+
evidence: {
|
|
269
|
+
linearMergeStale: true,
|
|
270
|
+
linearId,
|
|
271
|
+
identifier,
|
|
272
|
+
linearUrl: observation?.linearUrl ?? null,
|
|
273
|
+
prUrl,
|
|
274
|
+
observedStateName: observation?.stateName ?? null,
|
|
275
|
+
observedStateType: observation?.stateType ?? null,
|
|
276
|
+
fallback,
|
|
277
|
+
},
|
|
278
|
+
// Acknowledge/re-check only: the Dealer issue is already `done`, so
|
|
279
|
+
// there is no workflow to resume — resolve through
|
|
280
|
+
// resolveLinearMergeStaleAction, never the workflow state machine.
|
|
281
|
+
responseOptions: [
|
|
282
|
+
{ choice: "acknowledge", label: "Acknowledge" },
|
|
283
|
+
{ choice: "recheck", label: "Re-check Linear" },
|
|
284
|
+
],
|
|
285
|
+
requestId,
|
|
286
|
+
});
|
|
287
|
+
actionId = action.id;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
createIssueArtifact({
|
|
291
|
+
issueId,
|
|
292
|
+
kind: LINEAR_MERGE_VERIFY_ARTIFACT_KIND,
|
|
293
|
+
author: "system",
|
|
294
|
+
content: {
|
|
295
|
+
check: "post_merge",
|
|
296
|
+
linearId,
|
|
297
|
+
identifier,
|
|
298
|
+
prUrl,
|
|
299
|
+
reads,
|
|
300
|
+
outcome: "stale",
|
|
301
|
+
fallback,
|
|
302
|
+
actionId,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
return { checked: true, advanced: false, fallback, actionId };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Fire-and-forget entry for every `done` landing (merge finalize, external-merge
|
|
309
|
+
* close, abort resolutions) — never throws, never holds the caller. The check's
|
|
310
|
+
* bounded retry window (tens of seconds plus read timeouts) must not hold the
|
|
311
|
+
* merge caller: finalizeAutoMerge is awaited while holding the reviewer session
|
|
312
|
+
* and slot, and the final_review:merge HTTP path must not wait on it either.
|
|
313
|
+
*/
|
|
314
|
+
const postMergeInflight = new Set();
|
|
315
|
+
export function triggerLinearPostMerge(issueId) {
|
|
316
|
+
const pending = verifyLinearPostMerge(issueId).catch((err) => {
|
|
317
|
+
console.error(`[linear-merge-verify] post-merge check for ${issueId} failed:`, err);
|
|
318
|
+
return { checked: false, reason: "trigger caught" };
|
|
319
|
+
});
|
|
320
|
+
postMergeInflight.add(pending);
|
|
321
|
+
void pending.finally(() => {
|
|
322
|
+
postMergeInflight.delete(pending);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Test hook — await every post-merge check a test triggered through the
|
|
327
|
+
* fire-and-forget entry, so assertions on artifacts/actions do not race it.
|
|
328
|
+
* Loops until none remain (a re-check resolution can trigger another).
|
|
329
|
+
*/
|
|
330
|
+
export async function drainLinearPostMergeForTests() {
|
|
331
|
+
for (let guard = 0; guard < 25 && postMergeInflight.size > 0; guard += 1) {
|
|
332
|
+
await Promise.all([...postMergeInflight]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** True when the action is this module's stale-source notice (any status). */
|
|
336
|
+
export function isLinearMergeStaleAction(action) {
|
|
337
|
+
if (action.actionType !== "policy_escalation" || !action.evidenceJson)
|
|
338
|
+
return false;
|
|
339
|
+
try {
|
|
340
|
+
return JSON.parse(action.evidenceJson).linearMergeStale === true;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Dedicated resolution for the stale-source notice. The Dealer issue is already
|
|
348
|
+
* `done` — there is no active workflow for the generic resolver to advance, so
|
|
349
|
+
* this resolves the notice directly (the reflection_interaction_required
|
|
350
|
+
* precedent in routes/human-actions.ts):
|
|
351
|
+
* - `acknowledge` (plus legacy `close`/`dismiss` labels): close the notice.
|
|
352
|
+
* - `recheck`: close the notice and re-run the post-merge verification
|
|
353
|
+
* fire-and-forget — a human who advanced the Linear issue by hand gets a
|
|
354
|
+
* fresh verdict (and a verification artifact) instead of a stuck notice. A
|
|
355
|
+
* still-stale issue raises one new action only when the fallback did not
|
|
356
|
+
* already write; an already-recorded fallback stays quiet.
|
|
357
|
+
*/
|
|
358
|
+
export function resolveLinearMergeStaleAction(actionId, resolvedBy, choice) {
|
|
359
|
+
const action = getHumanAction(actionId);
|
|
360
|
+
if (!action)
|
|
361
|
+
return { ok: false, code: 404, error: "Human action not found" };
|
|
362
|
+
if (!isLinearMergeStaleAction(action)) {
|
|
363
|
+
return { ok: false, code: 400, error: `Action ${actionId} is not a Linear post-merge notice` };
|
|
364
|
+
}
|
|
365
|
+
if (!action.issueId)
|
|
366
|
+
return { ok: false, code: 500, error: "Human action has no issue" };
|
|
367
|
+
if (action.status !== "open")
|
|
368
|
+
return { ok: false, code: 409, error: "Human action already resolved" };
|
|
369
|
+
if (choice !== "acknowledge" && choice !== "dismiss" && choice !== "close" && choice !== "recheck") {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
code: 400,
|
|
373
|
+
error: `Invalid choice "${choice}" for Linear post-merge notice (acknowledge or recheck)`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
resolveHumanAction(actionId, resolvedBy, { choice });
|
|
377
|
+
const rechecked = choice === "recheck";
|
|
378
|
+
if (rechecked)
|
|
379
|
+
triggerLinearPostMerge(action.issueId);
|
|
380
|
+
const issue = getIssue(action.issueId);
|
|
381
|
+
return { ok: true, issueStatus: issue?.status ?? "done", rechecked };
|
|
382
|
+
}
|