agent-dealer 1.2.7 → 1.2.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.
- package/bundle/server/dist/adapters/agent-deck-bind.js +16 -6
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +74 -0
- package/bundle/server/dist/adapters/agent-health.js +14 -3
- package/bundle/server/dist/adapters/github.js +84 -5
- package/bundle/server/dist/adapters/github.test.js +83 -1
- package/bundle/server/dist/adapters/muse-capability.js +443 -24
- package/bundle/server/dist/adapters/muse-capability.test.js +469 -25
- package/bundle/server/dist/adapters/muse-visual-qa.js +114 -0
- package/bundle/server/dist/adapters/muse-visual-qa.test.js +68 -0
- package/bundle/server/dist/adapters/worktree-deps.js +96 -0
- package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
- package/bundle/server/dist/capacity/muse-probe.js +56 -9
- package/bundle/server/dist/capacity/muse-probe.test.js +188 -1
- package/bundle/server/dist/coordinator/admission.js +14 -1
- package/bundle/server/dist/coordinator/admission.test.js +197 -4
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +276 -0
- package/bundle/server/dist/coordinator/auto-merge.js +244 -4
- package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
- package/bundle/server/dist/coordinator/commands.js +284 -23
- package/bundle/server/dist/coordinator/commands.test.js +47 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +4 -2
- package/bundle/server/dist/coordinator/developer-effect.js +224 -4
- package/bundle/server/dist/coordinator/developer-effect.test.js +288 -3
- package/bundle/server/dist/coordinator/execution-report.js +6 -0
- package/bundle/server/dist/coordinator/execution-report.test.js +7 -0
- package/bundle/server/dist/coordinator/failure-cause.js +64 -1
- package/bundle/server/dist/coordinator/failure-cause.test.js +92 -1
- package/bundle/server/dist/coordinator/failure-reason.js +7 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +14 -0
- package/bundle/server/dist/coordinator/human-resolution.js +36 -1
- package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.js +647 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +121 -0
- package/bundle/server/dist/coordinator/muse-developer.integration.test.js +9 -4
- package/bundle/server/dist/coordinator/muse-spawn.js +122 -29
- package/bundle/server/dist/coordinator/muse-spawn.test.js +210 -0
- package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
- package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
- package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
- package/bundle/server/dist/coordinator/playbook-feedback.js +693 -0
- package/bundle/server/dist/coordinator/playbook-feedback.test.js +702 -0
- package/bundle/server/dist/coordinator/projection.js +10 -4
- package/bundle/server/dist/coordinator/projection.test.js +13 -0
- package/bundle/server/dist/coordinator/prompts-execution-contract.test.js +107 -0
- package/bundle/server/dist/coordinator/prompts.js +211 -2
- package/bundle/server/dist/coordinator/prompts.test.js +231 -0
- package/bundle/server/dist/coordinator/recovery.js +57 -3
- package/bundle/server/dist/coordinator/recovery.test.js +67 -8
- package/bundle/server/dist/coordinator/reflect-trigger.js +33 -169
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +149 -200
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +16 -1
- package/bundle/server/dist/coordinator/reviewer-result.js +6 -0
- package/bundle/server/dist/coordinator/routing.js +45 -6
- package/bundle/server/dist/coordinator/routing.test.js +76 -15
- package/bundle/server/dist/coordinator/session-timeouts.js +30 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +1 -1
- package/bundle/server/dist/coordinator/worker-loop.js +18 -4
- package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
- package/bundle/server/dist/db/index.js +18 -0
- package/bundle/server/dist/db/schema.sql +5 -0
- package/bundle/server/dist/docs-execution-analysis.test.js +1 -0
- package/bundle/server/dist/repository/artifacts-for-issue.js +3 -3
- package/bundle/server/dist/repository/human-actions.js +14 -0
- package/bundle/server/dist/repository/issues.js +67 -6
- package/bundle/server/dist/repository/work-items.js +7 -0
- package/bundle/server/dist/repository/worker-sessions.js +51 -2
- package/bundle/server/dist/routes/human-actions.js +10 -8
- package/bundle/server/dist/routes/issues-execution-contract.test.js +321 -0
- package/bundle/server/dist/routes/issues.js +19 -2
- package/bundle/server/dist/runners/muse-code-args.js +25 -0
- package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
- package/bundle/server/dist/runners/muse-code-jsonl.js +106 -11
- package/bundle/server/dist/runners/muse-config-core.js +18 -1
- package/bundle/server/dist/runners/muse-config.test.js +37 -0
- package/bundle/server/dist/runners/muse-serve-session.js +5 -0
- package/bundle/server/dist/runners/spawn-cli.js +116 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +124 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
- package/bundle/server/static-ui/assets/{index-DyAJNyfV.css → index-K_YcYkQU.css} +1 -1
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/execution-analysis.d.ts +22 -22
- package/bundle/shared/dist/execution-contract.d.ts +117 -0
- package/bundle/shared/dist/execution-contract.js +307 -0
- package/bundle/shared/dist/execution-contract.test.d.ts +2 -0
- package/bundle/shared/dist/execution-contract.test.js +499 -0
- package/bundle/shared/dist/execution-report.d.ts +9 -9
- package/bundle/shared/dist/execution-report.js +2 -0
- package/bundle/shared/dist/failure-cause.d.ts +4 -4
- package/bundle/shared/dist/failure-cause.js +2 -0
- package/bundle/shared/dist/human-actions.d.ts +4 -4
- package/bundle/shared/dist/human-actions.js +17 -0
- package/bundle/shared/dist/index.d.ts +85 -84
- package/bundle/shared/dist/index.js +3 -0
- package/bundle/shared/dist/issues.d.ts +176 -16
- package/bundle/shared/dist/issues.js +22 -0
- package/bundle/shared/dist/issues.test.js +3 -0
- package/bundle/shared/dist/outbound-draft.d.ts +12 -12
- package/bundle/shared/dist/worker-sessions.d.ts +10 -0
- package/bundle/shared/dist/worker-sessions.js +8 -0
- package/bundle/shared/dist/worker-sessions.test.js +1 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +16 -0
- package/bundle/shared/dist/workflow.test.js +3 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.js +4 -1
- package/dist/action.test.js +42 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-yLyxRd-7.js +0 -64
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// duplicate delivery is a no-op. The effect *work* itself runs elsewhere, through a leased
|
|
8
8
|
// worker whose structured result comes back into applyCompletion.
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
-
import { canTransitionIssue } from "@agent-dealer/shared";
|
|
10
|
+
import { canTransitionIssue, tryCompileContract } from "@agent-dealer/shared";
|
|
11
11
|
import { getDb } from "../db/index.js";
|
|
12
|
-
import { getIssue, incrementIssueRound, incrementIssueInfraAttempts, resetIssueInfraAttempts, grantReviewRetry, transitionIssue, } from "../repository/issues.js";
|
|
12
|
+
import { getIssue, incrementIssueRound, incrementIssueInfraAttempts, incrementIssueCiAttempts, resetIssueInfraAttempts, resetIssueCiAttempts, grantReviewRetry, transitionIssue, } from "../repository/issues.js";
|
|
13
13
|
import { appendWorkflowEvent, completeWorkflowInstance, getActiveWorkflowInstance, getWorkflowInstance, listWorkflowEventsForIssue, startWorkflowInstance, WorkflowAlreadyActiveError, } from "../repository/workflow-events.js";
|
|
14
14
|
import { createHumanAction, findOpenHumanAction, getHumanAction, listHumanActionsForIssue, reopenHumanAction, resolveHumanAction, updateOpenHumanAction, } from "../repository/human-actions.js";
|
|
15
15
|
import { pushLeaseToSha, readRemoteTip } from "../adapters/git-worktree.js";
|
|
@@ -17,9 +17,10 @@ import { ensureIssueRepoCheckout } from "../adapters/managed-repo.js";
|
|
|
17
17
|
import { reconcileFinding, resolveFindingsAbsentFromRound } from "../repository/findings.js";
|
|
18
18
|
import { normalizeReviewerResult } from "./reviewer-result.js";
|
|
19
19
|
import { getAgent } from "../repository/agents.js";
|
|
20
|
-
import { githubIssuesSync } from "../adapters/agent-health.js";
|
|
20
|
+
import { githubIssuesSync, invalidateMuseHealthCache } from "../adapters/agent-health.js";
|
|
21
|
+
import { recordMuseCapabilityOverride } from "../adapters/muse-capability.js";
|
|
21
22
|
import { createIssueArtifact, latestIssueArtifact } from "../repository/artifacts.js";
|
|
22
|
-
import { cancelWorkItem, enqueueWorkItem, finishWorkItem, getWorkItem, listWorkItemsForIssue, } from "../repository/work-items.js";
|
|
23
|
+
import { cancelWorkItem, enqueueWorkItem, finishWorkItem, getWorkItem, getWorkItemByIdempotencyKey, listWorkItemsForIssue, } from "../repository/work-items.js";
|
|
23
24
|
import { completeSession, getWorkerSession, listWorkerSessionsForIssue } from "../repository/worker-sessions.js";
|
|
24
25
|
import { killRunProcess } from "../runners/spawn-cli.js";
|
|
25
26
|
import { buildProfileSnapshot, serializeProfileSnapshot } from "./profile-snapshot.js";
|
|
@@ -30,9 +31,10 @@ import { routeDeveloperOutcome, routeReviewerOutcome, } from "./routing.js";
|
|
|
30
31
|
import { projectDeveloperRoute, projectReviewerRoute } from "./projection.js";
|
|
31
32
|
import { MERGE_FAILURE_EVIDENCE_KEY, MERGE_FAILURE_RESPONSE_OPTIONS, PUSH_DIVERGENCE_EVIDENCE_KEY, PUSH_DIVERGENCE_RESPONSE_OPTIONS, WORKTREE_BLOCKER_EVIDENCE_KEY, normalizeResolutionNote, parseHumanResolution, resolveHumanActionOutcome, } from "./human-resolution.js";
|
|
32
33
|
import { AUTO_MERGE_INTENT, finalizeAutoMerge } from "./auto-merge.js";
|
|
34
|
+
import { OPERATOR_VERIFICATION_ARTIFACT_KIND, OPERATOR_VERIFICATION_RESPONSE_OPTIONS, } from "./operator-criteria.js";
|
|
33
35
|
import { externalMergeStateForIssue } from "./external-merge.js";
|
|
34
36
|
import { detectNonConvergence, formatNonConvergenceReason, reviewHistoryFromEvents, } from "./non-convergence.js";
|
|
35
|
-
import { capEscalationEvents, deferLeasedWorkItemForBaseFetch, deferLeasedWorkItemForDeckOutage, deferLeasedWorkItemForUsageCap, formatCapEscalationReason, usageCapDeferralStartedAt, } from "./usage-cap-defer.js";
|
|
37
|
+
import { capEscalationEvents, checksWaitDeferralStartedAt, deferLeasedWorkItemForBaseFetch, deferLeasedWorkItemForChecksPending, deferLeasedWorkItemForDeckOutage, deferLeasedWorkItemForUsageCap, formatCapEscalationReason, formatChecksWaitEscalationReason, usageCapDeferralStartedAt, } from "./usage-cap-defer.js";
|
|
36
38
|
import { dequeueIssue, getQueuedEntryForIssue, markQueueEntryAdmitted, } from "../repository/queue-entries.js";
|
|
37
39
|
import { fetchResumeLiveHeadSha } from "./resume-live-head.js";
|
|
38
40
|
import { getWorkflow } from "./workflows/registry.js";
|
|
@@ -53,7 +55,13 @@ export function getTaskSnapshot(issue) {
|
|
|
53
55
|
const artifact = latestIssueArtifact(issue.id, TASK_SNAPSHOT_ARTIFACT_KIND);
|
|
54
56
|
if (artifact?.contentJson) {
|
|
55
57
|
try {
|
|
56
|
-
|
|
58
|
+
const parsed = JSON.parse(artifact.contentJson);
|
|
59
|
+
// Snapshots frozen before NOT-306 carry no contract — derive it from the
|
|
60
|
+
// frozen source description so old workflows still render what they ran.
|
|
61
|
+
if (parsed.executionContract === undefined) {
|
|
62
|
+
return { ...parsed, executionContract: tryCompileContract(parsed.description) };
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
57
65
|
}
|
|
58
66
|
catch {
|
|
59
67
|
// fall through to the live-field fallback below
|
|
@@ -66,6 +74,7 @@ export function getTaskSnapshot(issue) {
|
|
|
66
74
|
repo: issue.repo,
|
|
67
75
|
baseBranch: issue.baseBranch,
|
|
68
76
|
workflowVersion: WORKFLOW_VERSION,
|
|
77
|
+
executionContract: tryCompileContract(issue.description),
|
|
69
78
|
};
|
|
70
79
|
}
|
|
71
80
|
/**
|
|
@@ -83,9 +92,15 @@ export function canEditParkedIssue(issue) {
|
|
|
83
92
|
return false;
|
|
84
93
|
return !listWorkItemsForIssue(issue.id).some((w) => w.status === "pending" || w.status === "leased");
|
|
85
94
|
}
|
|
86
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Re-freezes the snapshot when the operator edited title/description/criteria
|
|
97
|
+
* (or the contract compiled from the description); returns the changed fields
|
|
98
|
+
* (empty = no-op). Source and compiled contract always re-freeze atomically —
|
|
99
|
+
* one artifact write carries both, so they can never diverge.
|
|
100
|
+
*/
|
|
87
101
|
function refreshTaskSnapshotIfEdited(issue) {
|
|
88
102
|
const frozen = getTaskSnapshot(issue);
|
|
103
|
+
const liveContract = tryCompileContract(issue.description);
|
|
89
104
|
const changed = [];
|
|
90
105
|
if (frozen.title !== issue.title)
|
|
91
106
|
changed.push("title");
|
|
@@ -93,6 +108,9 @@ function refreshTaskSnapshotIfEdited(issue) {
|
|
|
93
108
|
changed.push("description");
|
|
94
109
|
if (frozen.acceptanceCriteria !== (issue.acceptanceCriteria ?? ""))
|
|
95
110
|
changed.push("acceptanceCriteria");
|
|
111
|
+
if (JSON.stringify(frozen.executionContract ?? null) !== JSON.stringify(liveContract ?? null)) {
|
|
112
|
+
changed.push("executionContract");
|
|
113
|
+
}
|
|
96
114
|
if (changed.length > 0)
|
|
97
115
|
freezeTaskSnapshot(issue);
|
|
98
116
|
return changed;
|
|
@@ -109,6 +127,7 @@ function freezeTaskSnapshot(issue) {
|
|
|
109
127
|
repo: issue.repo,
|
|
110
128
|
baseBranch: issue.baseBranch,
|
|
111
129
|
workflowVersion: WORKFLOW_VERSION,
|
|
130
|
+
executionContract: tryCompileContract(issue.description),
|
|
112
131
|
},
|
|
113
132
|
});
|
|
114
133
|
}
|
|
@@ -118,7 +137,11 @@ function freezeTaskSnapshot(issue) {
|
|
|
118
137
|
* never changes the eventual session. The worker loop consumes this off the payload and
|
|
119
138
|
* only falls back to a live resolve for a legacy item queued before snapshots were carried.
|
|
120
139
|
*/
|
|
121
|
-
|
|
140
|
+
/**
|
|
141
|
+
* NOT-333: shared with recovery's stranded-reviewer re-enqueue so a recovered round
|
|
142
|
+
* carries the same frozen profile as a normally routed one.
|
|
143
|
+
*/
|
|
144
|
+
export function queuedProfileSnapshot(issue, kind) {
|
|
122
145
|
const agentId = kind === "developer" ? issue.developerAgentId : issue.reviewerAgentId;
|
|
123
146
|
const agent = agentId ? getAgent(agentId) : null;
|
|
124
147
|
return agent ? serializeProfileSnapshot(buildProfileSnapshot(agent, kind)) : null;
|
|
@@ -309,6 +332,12 @@ export async function applyCompletion(workItemId, leaseToken, outcome) {
|
|
|
309
332
|
if (outcome.kind === "base_fetch_failed") {
|
|
310
333
|
return applyBaseFetchDeferralCompletion(workItemId, leaseToken, outcome);
|
|
311
334
|
}
|
|
335
|
+
// NOT-311: CI still queued/running (or not yet registered) at the poll timeout —
|
|
336
|
+
// the session ended cleanly, so the item waits on backoff and re-polls instead of
|
|
337
|
+
// spending an infra attempt. Past the wait ceiling this escalates to a human.
|
|
338
|
+
if (outcome.kind === "checks_pending") {
|
|
339
|
+
return applyChecksPendingCompletion(workItemId, leaseToken, outcome);
|
|
340
|
+
}
|
|
312
341
|
const routed = getDb().transaction(() => {
|
|
313
342
|
const before = getWorkItem(workItemId);
|
|
314
343
|
if (!before)
|
|
@@ -397,6 +426,68 @@ function applyDeckOutageCompletion(workItemId, leaseToken, outage) {
|
|
|
397
426
|
function applyBaseFetchDeferralCompletion(workItemId, leaseToken, failure) {
|
|
398
427
|
return applyDeferralCompletion(workItemId, leaseToken, failure, (item, issue, instance) => deferLeasedWorkItemForBaseFetch(item, leaseToken, failure, issue, instance));
|
|
399
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* NOT-311: CI still pending at the poll timeout — defer with backoff, no attempt
|
|
431
|
+
* spent. Unlike a deck outage the wait has a ceiling, so the escalation arm raises a
|
|
432
|
+
* human action naming the stuck PR/SHA once CI has waited too long.
|
|
433
|
+
*/
|
|
434
|
+
function applyChecksPendingCompletion(workItemId, leaseToken, pending) {
|
|
435
|
+
return applyDeferralCompletion(workItemId, leaseToken, pending, (item, issue, instance) => deferLeasedWorkItemForChecksPending(item, leaseToken, pending, issue, instance), (issue, instance, item) => routeChecksWaitEscalation(issue, instance, item, pending));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* NOT-311: the CI wait passed its ceiling — hand to a human with the PR and head SHA
|
|
439
|
+
* CI never proved, without spending infra attempts. Mirrors the usage-cap ceiling
|
|
440
|
+
* escalation (worker.failed + needs_human + policy_escalation).
|
|
441
|
+
*/
|
|
442
|
+
export function routeChecksWaitEscalation(issue, instance, item, pending) {
|
|
443
|
+
const payload = parseWorkItemPayload(item.payloadJson);
|
|
444
|
+
const firstDeferredAt = checksWaitDeferralStartedAt(payload) ?? pending.waitStartedAt;
|
|
445
|
+
const reason = formatChecksWaitEscalationReason(pending, firstDeferredAt);
|
|
446
|
+
const role = item.kind === "developer" ? "developer" : "reviewer";
|
|
447
|
+
const ev = eventEmitter(issue, instance, item.workerSessionId, "needs_human", issue.currentRound);
|
|
448
|
+
for (const type of capEscalationEvents()) {
|
|
449
|
+
const session = item.workerSessionId ? getWorkerSession(item.workerSessionId) : null;
|
|
450
|
+
ev.emit(type, {
|
|
451
|
+
actorType: role,
|
|
452
|
+
payload: {
|
|
453
|
+
...workerSessionPayload({
|
|
454
|
+
runtime: session?.runtime ?? null,
|
|
455
|
+
model: session?.model ?? null,
|
|
456
|
+
sessionId: item.workerSessionId ?? "",
|
|
457
|
+
worktreePath: session?.worktreePath,
|
|
458
|
+
}),
|
|
459
|
+
outcome: "checks_pending",
|
|
460
|
+
reason: pending.reason,
|
|
461
|
+
branch: pending.branch,
|
|
462
|
+
prNumber: pending.prNumber,
|
|
463
|
+
headSha: pending.headSha,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
applyProjectionTransition(issue, {
|
|
468
|
+
issueStatus: "needs_human",
|
|
469
|
+
currentOwner: "human",
|
|
470
|
+
currentIntent: reason,
|
|
471
|
+
events: capEscalationEvents(),
|
|
472
|
+
}, {});
|
|
473
|
+
const action = createHumanAction({
|
|
474
|
+
issueId: issue.id,
|
|
475
|
+
workflowInstanceId: instance.id,
|
|
476
|
+
actionType: "policy_escalation",
|
|
477
|
+
reason,
|
|
478
|
+
question: questionFor("policy_escalation", reason),
|
|
479
|
+
evidence: { checksPending: { ...pending, firstDeferredAt } },
|
|
480
|
+
responseOptions: responseOptionsFor("policy_escalation"),
|
|
481
|
+
});
|
|
482
|
+
ev.emit("human_action.requested", { payload: { actionType: "policy_escalation", actionId: action.id } });
|
|
483
|
+
return {
|
|
484
|
+
applied: true,
|
|
485
|
+
issueStatus: "needs_human",
|
|
486
|
+
nextWorkItemId: null,
|
|
487
|
+
humanActionId: action.id,
|
|
488
|
+
instanceCompleted: false,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
400
491
|
/**
|
|
401
492
|
* Deferral ceiling exceeded — escalate with cap evidence without spending infra attempts.
|
|
402
493
|
* Used when applyCompletion or the worker loop cannot defer any longer.
|
|
@@ -507,6 +598,8 @@ function applyDeveloper(issue, instance, item, outcome) {
|
|
|
507
598
|
maxReviewRounds: issue.maxReviewRounds,
|
|
508
599
|
infraAttempts: issue.infraAttempts,
|
|
509
600
|
maxInfraAttempts: issue.maxInfraAttempts,
|
|
601
|
+
ciAttempts: issue.ciAttempts,
|
|
602
|
+
maxCiAttempts: issue.maxCiAttempts,
|
|
510
603
|
});
|
|
511
604
|
const { projection, effect, advance } = projectDeveloperRoute(route, issue.status, issue.currentRound);
|
|
512
605
|
const ev = eventEmitter(issue, instance, item.workerSessionId, projection.issueStatus, issue.currentRound);
|
|
@@ -589,6 +682,9 @@ function applyDeveloper(issue, instance, item, outcome) {
|
|
|
589
682
|
incrementIssueRound(issue.id);
|
|
590
683
|
else if (advance === "infra")
|
|
591
684
|
incrementIssueInfraAttempts(issue.id);
|
|
685
|
+
// NOT-313: a CI-repair retry spends ci_attempts, never infra_attempts.
|
|
686
|
+
else if (advance === "ci")
|
|
687
|
+
incrementIssueCiAttempts(issue.id);
|
|
592
688
|
const issueNow = getIssue(issue.id);
|
|
593
689
|
return applyEffect(issue, instance, effect, route, issueNow, ev, item.id);
|
|
594
690
|
}
|
|
@@ -722,9 +818,10 @@ function findNonConvergence(issue, instance, result) {
|
|
|
722
818
|
});
|
|
723
819
|
return detectNonConvergence(history, issue.currentRound, floorRound);
|
|
724
820
|
}
|
|
725
|
-
/** Every route that spends an infra attempt (projection.ts's advance === "infra"
|
|
726
|
-
* the same round/head, so its enqueue needs a suffix the plain
|
|
727
|
-
* provide to avoid colliding with the previous (terminal)
|
|
821
|
+
/** Every route that spends an infra or CI attempt (projection.ts's advance === "infra"
|
|
822
|
+
* or "ci") repeats the same round/head, so its enqueue needs a suffix the plain
|
|
823
|
+
* round/head key doesn't provide to avoid colliding with the previous (terminal)
|
|
824
|
+
* attempt's key.
|
|
728
825
|
* retry_reviewer_at_new_head belongs here too — a head that cycles A→B→A would otherwise
|
|
729
826
|
* re-derive attempt A's ORIGINAL key (same round, same head) and collide with that
|
|
730
827
|
* now-terminal work item, leaving the issue "reviewing" with no pending item. */
|
|
@@ -755,6 +852,21 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
755
852
|
const attemptSuffix = isInfraRetry(route) ? `:retry-of:${causativeItemId}` : "";
|
|
756
853
|
if (route.next === "retry_developer_with_findings")
|
|
757
854
|
ev.emit("repair.started");
|
|
855
|
+
let idempotencyKey = `${instance.id}:${kind}:${issueNow.currentRound}${headSuffix}${attemptSuffix}`;
|
|
856
|
+
// NOT-333: a developer round that pushes nothing (e.g. a resume round that only
|
|
857
|
+
// updates the PR description) re-derives the exact key of the already-terminal
|
|
858
|
+
// reviewer item it follows — same instance, same round, same head — so the enqueue
|
|
859
|
+
// dedupes against that done row and strands the issue in `reviewing` with no pending
|
|
860
|
+
// work. A terminal duplicate gets the same `:retry-of:<causative work item id>`
|
|
861
|
+
// shape the infra-retry path uses, so a new row is created; a still-active
|
|
862
|
+
// (pending/leased) duplicate keeps deduping.
|
|
863
|
+
if (kind === "reviewer" && attemptSuffix === "") {
|
|
864
|
+
const existing = getWorkItemByIdempotencyKey(idempotencyKey);
|
|
865
|
+
if (existing &&
|
|
866
|
+
(existing.status === "done" || existing.status === "dead" || existing.status === "cancelled")) {
|
|
867
|
+
idempotencyKey += `:retry-of:${causativeItemId}`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
758
870
|
const next = enqueueWorkItem({
|
|
759
871
|
issueId: issue.id,
|
|
760
872
|
workflowInstanceId: instance.id,
|
|
@@ -767,7 +879,7 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
767
879
|
...(effect.branch ? { branch: effect.branch } : {}),
|
|
768
880
|
profileSnapshot: queuedProfileSnapshot(issue, kind),
|
|
769
881
|
},
|
|
770
|
-
idempotencyKey
|
|
882
|
+
idempotencyKey,
|
|
771
883
|
});
|
|
772
884
|
return { ...base, nextWorkItemId: next.id };
|
|
773
885
|
}
|
|
@@ -973,6 +1085,17 @@ function questionFor(actionType, reason, resumeAsReviewer = false, mergeFailure
|
|
|
973
1085
|
// Run-scoped action type directly with its own question text (NOT-95). Case exists
|
|
974
1086
|
// only so this function stays total over HumanActionType.
|
|
975
1087
|
throw new Error("outbound_delivery_interaction_required is not raised through applyEffect");
|
|
1088
|
+
case "muse_capability":
|
|
1089
|
+
// Never raised through applyEffect — muse-capability.ts creates this action
|
|
1090
|
+
// directly with its own question text (NOT-308). Case exists only so this
|
|
1091
|
+
// function stays total over HumanActionType.
|
|
1092
|
+
throw new Error("muse_capability is not raised through applyEffect");
|
|
1093
|
+
case "operator_verification":
|
|
1094
|
+
// Never raised through applyEffect — auto-merge.ts's operator gate creates
|
|
1095
|
+
// this action directly with a question listing each criterion and its
|
|
1096
|
+
// command (NOT-314). Case exists only so this function stays total over
|
|
1097
|
+
// HumanActionType.
|
|
1098
|
+
throw new Error("operator_verification is not raised through applyEffect");
|
|
976
1099
|
}
|
|
977
1100
|
}
|
|
978
1101
|
/** Exported for db/migrate-to-issues.ts, which must populate the same response options on
|
|
@@ -1014,6 +1137,16 @@ export function responseOptionsFor(actionType, resumeAsReviewer = false, opts =
|
|
|
1014
1137
|
case "outbound_delivery_interaction_required":
|
|
1015
1138
|
// Never actually raised through applyEffect — see questionFor's identical case.
|
|
1016
1139
|
throw new Error("outbound_delivery_interaction_required is not raised through applyEffect");
|
|
1140
|
+
case "muse_capability":
|
|
1141
|
+
// NOT-308: created with its own stored options (muse-capability.ts) — acknowledge
|
|
1142
|
+
// admits on that Muse version (missing) or dismisses (unverified). Never raised
|
|
1143
|
+
// through applyEffect; case exists only so this function stays total.
|
|
1144
|
+
return [{ choice: "acknowledge", label: "Acknowledge" }];
|
|
1145
|
+
case "operator_verification":
|
|
1146
|
+
// Raised by auto-merge.ts's operator gate with these exact options (the
|
|
1147
|
+
// shared constant keeps the two from drifting); listed here so the table
|
|
1148
|
+
// stays total over HumanActionType.
|
|
1149
|
+
return [...OPERATOR_VERIFICATION_RESPONSE_OPTIONS];
|
|
1017
1150
|
}
|
|
1018
1151
|
}
|
|
1019
1152
|
function parseContinuationPreview(json) {
|
|
@@ -1052,18 +1185,88 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1052
1185
|
const normalizedNote = normalizeResolutionNote(opts?.note);
|
|
1053
1186
|
if (!normalizedNote.ok)
|
|
1054
1187
|
return { ok: false, code: 400, error: normalizedNote.error };
|
|
1188
|
+
// NOT-314: every operator_verification choice requires a non-empty note — the
|
|
1189
|
+
// recorded result (verified), the waiver reason (waive), or what to fix
|
|
1190
|
+
// (repair). parseHumanResolution below also rejects a missing note; this
|
|
1191
|
+
// names the requirement instead of misreporting a valid choice as invalid.
|
|
1192
|
+
if (action.actionType === "operator_verification" && !normalizedNote.note) {
|
|
1193
|
+
return {
|
|
1194
|
+
ok: false,
|
|
1195
|
+
code: 400,
|
|
1196
|
+
error: 'A non-empty note is required to resolve operator_verification (verified: the result, e.g. pasted output; waive: why it is waived; repair: what the next round must fix)',
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
// NOT-308: a Muse capability escalation resolves without touching the workflow — it
|
|
1200
|
+
// has no round to spend and no merge to park, and parseHumanResolution (below)
|
|
1201
|
+
// deliberately rejects it since there is no workflow outcome to map. Acknowledging a
|
|
1202
|
+
// `missing` verdict records a per-version override so the admission gate stops
|
|
1203
|
+
// blocking that version; acknowledging `unverified` just dismisses. Works with or
|
|
1204
|
+
// without an active instance (a blocked admission never has one).
|
|
1205
|
+
if (action.actionType === "muse_capability") {
|
|
1206
|
+
if (choice !== "acknowledge") {
|
|
1207
|
+
return { ok: false, code: 400, error: `Invalid choice "${choice}" for muse_capability` };
|
|
1208
|
+
}
|
|
1209
|
+
if (!action.issueId)
|
|
1210
|
+
return { ok: false, code: 500, error: "Human action has no issue" };
|
|
1211
|
+
const museIssue = getIssue(action.issueId);
|
|
1212
|
+
if (!museIssue)
|
|
1213
|
+
return { ok: false, code: 404, error: "Issue not found" };
|
|
1214
|
+
const museInstance = getActiveWorkflowInstance(action.issueId);
|
|
1215
|
+
let evidence = null;
|
|
1216
|
+
try {
|
|
1217
|
+
evidence = action.evidenceJson
|
|
1218
|
+
? JSON.parse(action.evidenceJson)
|
|
1219
|
+
: null;
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
evidence = null;
|
|
1223
|
+
}
|
|
1224
|
+
// File first, then the row: if the resolve below fails the action stays open and
|
|
1225
|
+
// the operator retries idempotently; the reverse order could leave a resolved
|
|
1226
|
+
// action whose version still blocks with no way to re-acknowledge.
|
|
1227
|
+
if (evidence?.kind === "missing" && typeof evidence.version === "string" && evidence.version) {
|
|
1228
|
+
recordMuseCapabilityOverride(evidence.version);
|
|
1229
|
+
}
|
|
1230
|
+
// The lifted block must be visible on the next health read, not after the cache TTL.
|
|
1231
|
+
invalidateMuseHealthCache();
|
|
1232
|
+
return getDb().transaction(() => {
|
|
1233
|
+
resolveHumanAction(actionId, resolvedBy, { choice });
|
|
1234
|
+
appendWorkflowEvent({
|
|
1235
|
+
issueId: museIssue.id,
|
|
1236
|
+
...(museInstance ? { workflowInstanceId: museInstance.id } : {}),
|
|
1237
|
+
type: "human_action.resolved",
|
|
1238
|
+
actorType: "human",
|
|
1239
|
+
actorRef: resolvedBy,
|
|
1240
|
+
stage: museIssue.status,
|
|
1241
|
+
round: museIssue.currentRound,
|
|
1242
|
+
payload: { actionType: action.actionType, choice },
|
|
1243
|
+
});
|
|
1244
|
+
return {
|
|
1245
|
+
ok: true,
|
|
1246
|
+
issueStatus: museIssue.status,
|
|
1247
|
+
nextWorkItemId: null,
|
|
1248
|
+
instanceCompleted: false,
|
|
1249
|
+
restarted: false,
|
|
1250
|
+
triggerReflect: false,
|
|
1251
|
+
};
|
|
1252
|
+
})();
|
|
1253
|
+
}
|
|
1055
1254
|
const resolution = parseHumanResolution(action.actionType, choice, normalizedNote.note);
|
|
1056
1255
|
if (!resolution) {
|
|
1057
1256
|
return { ok: false, code: 400, error: `Invalid choice "${choice}" for ${action.actionType}` };
|
|
1058
1257
|
}
|
|
1059
1258
|
// NOT-272: the typed resolution carries the note only for product_scope_decision
|
|
1060
|
-
// (parseHumanResolution drops it for every other action type
|
|
1259
|
+
// (parseHumanResolution drops it for every other action type — except
|
|
1260
|
+
// operator_verification, whose note is required and always carried).
|
|
1061
1261
|
const scopeDecisionNote = resolution.actionType === "product_scope_decision" && "note" in resolution
|
|
1062
1262
|
? resolution.note
|
|
1063
1263
|
: undefined;
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1264
|
+
// NOT-314: the repair instruction rides the next developer round's payload;
|
|
1265
|
+
// the verified/waived note is stored on the artifact / waiver event below.
|
|
1266
|
+
const operatorNote = resolution.actionType === "operator_verification" && "note" in resolution
|
|
1267
|
+
? resolution.note
|
|
1268
|
+
: undefined;
|
|
1269
|
+
const storedResolution = scopeDecisionNote ?? operatorNote ? { choice, note: scopeDecisionNote ?? operatorNote } : { choice };
|
|
1067
1270
|
// NOT-194: narrow policy_escalation choices per action. A merge-failure action offers
|
|
1068
1271
|
// retry_merge/repair/close only (resume would re-run development on approved work);
|
|
1069
1272
|
// every other policy_escalation keeps resume/close only. Pre-NOT-194 open merge-failure
|
|
@@ -1108,12 +1311,56 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1108
1311
|
// Park like auto-merge, then the async wrapper runs finalizeAutoMerge outside this txn.
|
|
1109
1312
|
// NOT-194: a merge-failure retry_merge parks the same way — the old action is resolved
|
|
1110
1313
|
// first, so a second failure escalates exactly one fresh action with the new reason.
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1314
|
+
// NOT-314: an operator_verification verified/waive parks the same way — the recorded
|
|
1315
|
+
// result satisfies the gate, so finalizeAutoMerge merges on the very next pass.
|
|
1316
|
+
// (repair takes the generic repair-round path below, never this park.)
|
|
1317
|
+
const parksForMerge = (resolution.actionType === "final_review" &&
|
|
1318
|
+
(resolution.choice === "merge" || resolution.choice === "complete")) ||
|
|
1319
|
+
(resolution.actionType === "policy_escalation" && resolution.choice === "retry_merge") ||
|
|
1320
|
+
(resolution.actionType === "operator_verification" &&
|
|
1321
|
+
(resolution.choice === "verified" || resolution.choice === "waive"));
|
|
1322
|
+
if (instance && parksForMerge) {
|
|
1115
1323
|
return getDb().transaction(() => {
|
|
1116
|
-
resolveHumanAction(actionId, resolvedBy,
|
|
1324
|
+
resolveHumanAction(actionId, resolvedBy, storedResolution);
|
|
1325
|
+
// NOT-314: durably record what the merge is authorized by, in the same txn as
|
|
1326
|
+
// the park — `verified` stores the result as an issue artifact (kind
|
|
1327
|
+
// operator_verification, author human) pinned to this head; `waive`
|
|
1328
|
+
// appends an operator_verification.waived workflow event. A later finalize
|
|
1329
|
+
// re-checks the record against the then-current head, so new commits
|
|
1330
|
+
// re-block the gate.
|
|
1331
|
+
if (resolution.actionType === "operator_verification" && operatorNote) {
|
|
1332
|
+
if (resolution.choice === "verified") {
|
|
1333
|
+
createIssueArtifact({
|
|
1334
|
+
issueId: issue.id,
|
|
1335
|
+
kind: OPERATOR_VERIFICATION_ARTIFACT_KIND,
|
|
1336
|
+
author: "human",
|
|
1337
|
+
content: {
|
|
1338
|
+
headSha: issue.headSha,
|
|
1339
|
+
note: operatorNote,
|
|
1340
|
+
verifiedBy: resolvedBy,
|
|
1341
|
+
verifiedAt: new Date().toISOString(),
|
|
1342
|
+
},
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
appendWorkflowEvent({
|
|
1347
|
+
issueId: issue.id,
|
|
1348
|
+
workflowInstanceId: instance.id,
|
|
1349
|
+
type: "operator_verification.waived",
|
|
1350
|
+
actorType: "human",
|
|
1351
|
+
actorRef: resolvedBy,
|
|
1352
|
+
stage: issue.status,
|
|
1353
|
+
round: issue.currentRound,
|
|
1354
|
+
payload: {
|
|
1355
|
+
actionType: "operator_verification",
|
|
1356
|
+
choice: "waive",
|
|
1357
|
+
headSha: issue.headSha,
|
|
1358
|
+
waivedBy: resolvedBy,
|
|
1359
|
+
note: operatorNote,
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1117
1364
|
appendWorkflowEvent({
|
|
1118
1365
|
issueId: issue.id,
|
|
1119
1366
|
workflowInstanceId: instance.id,
|
|
@@ -1122,7 +1369,12 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1122
1369
|
actorRef: resolvedBy,
|
|
1123
1370
|
stage: resolution.actionType === "final_review" ? "final_review" : issue.status,
|
|
1124
1371
|
round: issue.currentRound,
|
|
1125
|
-
payload: {
|
|
1372
|
+
payload: {
|
|
1373
|
+
actionType: action.actionType,
|
|
1374
|
+
choice: resolution.choice,
|
|
1375
|
+
pendingMerge: true,
|
|
1376
|
+
...(operatorNote ? { note: operatorNote } : {}),
|
|
1377
|
+
},
|
|
1126
1378
|
});
|
|
1127
1379
|
transitionIssue(issue.id, "final_review", {
|
|
1128
1380
|
currentOwner: "system",
|
|
@@ -1328,7 +1580,10 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1328
1580
|
incrementIssueRound(issue.id);
|
|
1329
1581
|
break;
|
|
1330
1582
|
case "infra":
|
|
1583
|
+
// NOT-313: a resume past an infra OR CI-checks escalation grants a fresh
|
|
1584
|
+
// budget of both kinds — the CI budget is not a review-round spend either.
|
|
1331
1585
|
resetIssueInfraAttempts(issue.id);
|
|
1586
|
+
resetIssueCiAttempts(issue.id);
|
|
1332
1587
|
if (advancesRound)
|
|
1333
1588
|
incrementIssueRound(issue.id);
|
|
1334
1589
|
break;
|
|
@@ -1372,6 +1627,10 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1372
1627
|
// NOT-272: a product_scope_decision note rides only this next round's
|
|
1373
1628
|
// payload — later rounds never see it.
|
|
1374
1629
|
...(scopeDecisionNote ? { scopeDecisionNote } : {}),
|
|
1630
|
+
// NOT-314: same one-shot ride for an operator_verification repair
|
|
1631
|
+
// note — only the repair choice reaches this path (verified/waive
|
|
1632
|
+
// park for merge above), and only this round reads it.
|
|
1633
|
+
...(operatorNote ? { operatorRepairNote: operatorNote } : {}),
|
|
1375
1634
|
},
|
|
1376
1635
|
idempotencyKey: resumeKey,
|
|
1377
1636
|
});
|
|
@@ -1446,7 +1705,9 @@ export async function resolveHumanActionAndAdvanceAsync(actionId, resolvedBy, ch
|
|
|
1446
1705
|
return {
|
|
1447
1706
|
ok: true,
|
|
1448
1707
|
issueStatus: merged.issueStatus,
|
|
1449
|
-
|
|
1708
|
+
// NOT-310: a retry_merge that hit a textual conflict queues a repair round —
|
|
1709
|
+
// report it like any other queued round instead of dropping it.
|
|
1710
|
+
nextWorkItemId: merged.nextWorkItemId,
|
|
1450
1711
|
instanceCompleted: merged.instanceCompleted,
|
|
1451
1712
|
restarted: false,
|
|
1452
1713
|
triggerReflect: merged.triggerReflect,
|
|
@@ -635,6 +635,53 @@ test("fail → retry → exhaust → resume → fail again does not collide with
|
|
|
635
635
|
assert.equal(pending.length, 1, "a fresh item must be enqueued — the old (round, infraAttempts)-keyed row must not be silently reused");
|
|
636
636
|
assert.notEqual(pending[0].id, firstRetryItem.id, "must be a NEW work item, not the pre-escalation retry's now-terminal row");
|
|
637
637
|
});
|
|
638
|
+
test("NOT-333: a resume round that pushes nothing still enqueues a fresh reviewer instead of deduping against the terminal one", async () => {
|
|
639
|
+
// The NOT-200 shape: reviewer round 1 at head H escalates with a product scope
|
|
640
|
+
// question, the human resumes, and the developer round changes nothing — so its
|
|
641
|
+
// handoff re-derives the exact idempotency key of the already-done reviewer item.
|
|
642
|
+
const issueId = newIssue();
|
|
643
|
+
startWorkflow(issueId);
|
|
644
|
+
await complete(issueId, cleanHandoff);
|
|
645
|
+
const firstReviewer = listWorkItemsForIssue(issueId).find((i) => i.kind === "reviewer");
|
|
646
|
+
await complete(issueId, {
|
|
647
|
+
kind: "verdict",
|
|
648
|
+
result: { ...okReview("escalated"), productScopeQuestion: "Should deleted users retain sessions?" },
|
|
649
|
+
});
|
|
650
|
+
const action = listHumanActionsForIssue(issueId).find((a) => a.actionType === "product_scope_decision");
|
|
651
|
+
const resolved = resolveHumanActionAndAdvance(action.id, "yusuke", "resume");
|
|
652
|
+
assert.equal(resolved.ok, true);
|
|
653
|
+
assert.equal(getIssue(issueId).status, "developing");
|
|
654
|
+
// The resume round pushes nothing: the handoff carries the very same head SHA.
|
|
655
|
+
const { result } = await complete(issueId, cleanHandoff);
|
|
656
|
+
assert.equal(result.applied, true);
|
|
657
|
+
assert.equal(getIssue(issueId).status, "reviewing");
|
|
658
|
+
assert.equal(getWorkItem(firstReviewer.id).status, "done");
|
|
659
|
+
const reviewers = listWorkItemsForIssue(issueId).filter((i) => i.kind === "reviewer");
|
|
660
|
+
const pending = reviewers.filter((i) => i.status === "pending");
|
|
661
|
+
assert.equal(pending.length, 1, "a fresh reviewer round must be queued, not dropped via a key collision");
|
|
662
|
+
assert.notEqual(pending[0].id, firstReviewer.id, "must be a NEW work item, not the escalated round's now-terminal row");
|
|
663
|
+
assert.match(pending[0].idempotencyKey, /:retry-of:/, "the terminal duplicate is disambiguated like an infra retry");
|
|
664
|
+
assert.equal(JSON.parse(pending[0].payloadJson).inputSha, cleanHandoff.headSha);
|
|
665
|
+
});
|
|
666
|
+
test("NOT-333: a duplicate developer completion while the reviewer is still pending or leased enqueues no second reviewer", async () => {
|
|
667
|
+
const issueId = newIssue();
|
|
668
|
+
startWorkflow(issueId);
|
|
669
|
+
const item = claim(issueId);
|
|
670
|
+
const first = await applyCompletion(item.id, item.leaseToken, cleanHandoff);
|
|
671
|
+
assert.equal(first.applied, true);
|
|
672
|
+
// A retried callback redelivers the same completion while the reviewer is pending.
|
|
673
|
+
const second = await applyCompletion(item.id, item.leaseToken, cleanHandoff);
|
|
674
|
+
assert.equal(second.applied, false);
|
|
675
|
+
// Same while the reviewer is leased — still exactly one active reviewer row.
|
|
676
|
+
const reviewer = claim(issueId);
|
|
677
|
+
assert.equal(reviewer.kind, "reviewer");
|
|
678
|
+
const third = await applyCompletion(item.id, item.leaseToken, cleanHandoff);
|
|
679
|
+
assert.equal(third.applied, false);
|
|
680
|
+
const active = listWorkItemsForIssue(issueId).filter((i) => i.kind === "reviewer" && (i.status === "pending" || i.status === "leased"));
|
|
681
|
+
assert.equal(active.length, 1);
|
|
682
|
+
assert.equal(active[0].id, reviewer.id);
|
|
683
|
+
assert.equal(listWorkItemsForIssue(issueId).filter((i) => i.kind === "reviewer").length, 1);
|
|
684
|
+
});
|
|
638
685
|
test("resolving product_scope_decision:resume after a reviewer's escalated+question resumes as the developer", async () => {
|
|
639
686
|
const issueId = newIssue();
|
|
640
687
|
startWorkflow(issueId);
|
|
@@ -31,15 +31,17 @@ const { deckOutageBackoffMs } = await import("./deck-outage-config.js");
|
|
|
31
31
|
const UNREACHABLE = "Agent Deck is unreachable — fetch failed";
|
|
32
32
|
before(() => migrate());
|
|
33
33
|
beforeEach(() => {
|
|
34
|
+
// NOT-305: session-linked playbook_use_receipt artifacts reference worker_sessions,
|
|
35
|
+
// so child rows (artifacts, usage_events) go before their parents.
|
|
34
36
|
getDb().exec(`
|
|
35
37
|
DELETE FROM review_publications;
|
|
38
|
+
DELETE FROM artifacts;
|
|
36
39
|
DELETE FROM work_items;
|
|
37
40
|
DELETE FROM human_actions;
|
|
38
41
|
DELETE FROM workflow_events;
|
|
39
42
|
DELETE FROM findings;
|
|
40
|
-
DELETE FROM worker_sessions;
|
|
41
|
-
DELETE FROM artifacts;
|
|
42
43
|
DELETE FROM usage_events;
|
|
44
|
+
DELETE FROM worker_sessions;
|
|
43
45
|
DELETE FROM workflow_instances;
|
|
44
46
|
DELETE FROM issues;
|
|
45
47
|
DELETE FROM runtime_availability;
|