agent-dealer 1.3.1 → 1.3.3
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-health.test.js +52 -0
- package/bundle/server/dist/adapters/git-worktree.js +38 -1
- package/bundle/server/dist/adapters/github.js +50 -0
- package/bundle/server/dist/adapters/github.test.js +89 -1
- package/bundle/server/dist/capacity/service.js +66 -6
- package/bundle/server/dist/capacity/service.test.js +220 -0
- package/bundle/server/dist/coordinator/auto-merge.js +46 -9
- package/bundle/server/dist/coordinator/commands.js +89 -9
- package/bundle/server/dist/coordinator/commands.test.js +228 -2
- package/bundle/server/dist/coordinator/developer-effect.js +36 -0
- package/bundle/server/dist/coordinator/operator-verification.integration.test.js +102 -1
- package/bundle/server/dist/coordinator/product-scope-contract.test.js +249 -0
- package/bundle/server/dist/coordinator/prompts.js +57 -2
- package/bundle/server/dist/coordinator/prompts.test.js +236 -2
- package/bundle/server/dist/coordinator/reviewer-browser-probe.js +89 -10
- package/bundle/server/dist/coordinator/reviewer-browser-probe.test.js +100 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +30 -1
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +54 -0
- package/bundle/server/dist/coordinator/reviewer-visual-evidence.js +594 -0
- package/bundle/server/dist/coordinator/reviewer-visual-evidence.test.js +496 -0
- package/bundle/server/dist/coordinator/source-reload.js +44 -9
- package/bundle/server/dist/coordinator/visual-qa-collection.test.js +619 -0
- package/bundle/server/dist/coordinator/visual-qa.js +630 -0
- package/bundle/server/dist/coordinator/visual-qa.test.js +493 -0
- package/bundle/server/dist/db/index.js +8 -0
- package/bundle/server/dist/db/schema.sql +5 -1
- package/bundle/server/dist/paths.js +24 -0
- package/bundle/server/dist/repository/issues.js +8 -1
- package/bundle/server/dist/repository/issues.test.js +34 -0
- package/bundle/server/dist/repository/runtime-availability.js +29 -11
- package/bundle/server/dist/routes/issues-execution-contract.test.js +293 -0
- package/bundle/server/dist/routes/issues-source-attachments.test.js +277 -0
- package/bundle/server/dist/routes/issues.js +23 -8
- package/bundle/server/dist/routes/issues.test.js +80 -0
- package/bundle/server/dist/runners/usage-cap.js +66 -15
- package/bundle/server/dist/runners/usage-cap.test.js +96 -2
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-B_pNUDRY.js +64 -0
- package/bundle/server/static-ui/index.html +1 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +85 -0
- package/bundle/shared/dist/runtime-capacity.js +19 -0
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-Bz_c-UbO.js +0 -64
|
@@ -13,7 +13,8 @@ const { createIssue, getIssue, updateIssue, transitionIssue } = await import("..
|
|
|
13
13
|
const { listHumanActionsForIssue } = await import("../repository/human-actions.js");
|
|
14
14
|
const { listArtifactsForIssueByKind } = await import("../repository/artifacts-for-issue.js");
|
|
15
15
|
const { listWorkflowEventsForIssue, getActiveWorkflowInstance } = await import("../repository/workflow-events.js");
|
|
16
|
-
const { claimWorkItem, getWorkItem } = await import("../repository/work-items.js");
|
|
16
|
+
const { claimWorkItem, getWorkItem, listWorkItemsForIssue } = await import("../repository/work-items.js");
|
|
17
|
+
const { createIssueArtifact } = await import("../repository/artifacts.js");
|
|
17
18
|
const { startWorkflow, applyCompletion, resolveHumanActionAndAdvance, resolveHumanActionAndAdvanceAsync, responseOptionsFor, } = await import("./commands.js");
|
|
18
19
|
const { ReviewerResult } = await import("./reviewer-result.js");
|
|
19
20
|
const { setMergePrForTests, clearFinalizeInflightForTests, finalizeAutoMerge, gateOperatorVerification, OPERATOR_VERIFICATION_INTENT, } = await import("./auto-merge.js");
|
|
@@ -345,3 +346,103 @@ test("responseOptionsFor(operator_verification) matches the gate's stored option
|
|
|
345
346
|
{ choice: "repair", label: "Another repair round" },
|
|
346
347
|
]);
|
|
347
348
|
});
|
|
349
|
+
// NOT-384: a UI-affecting head whose recorded CI visual evidence is `missing`
|
|
350
|
+
// holds at the same gate — one operator_verification action naming the head
|
|
351
|
+
// SHA and the missing artifacts, no merge, and crucially no repair round.
|
|
352
|
+
function recordVisualEvidence(issueId, opts) {
|
|
353
|
+
createIssueArtifact({
|
|
354
|
+
issueId,
|
|
355
|
+
kind: "visual_evidence",
|
|
356
|
+
content: {
|
|
357
|
+
headSha: opts.headSha,
|
|
358
|
+
baseSha: "base1",
|
|
359
|
+
uiAffecting: true,
|
|
360
|
+
state: opts.state,
|
|
361
|
+
runId: opts.state === "missing" ? null : 42,
|
|
362
|
+
artifactNames: opts.state === "available" ? ["ui-screenshots", "ui-baseline", "ui-diff"] : [],
|
|
363
|
+
reason: opts.state === "available"
|
|
364
|
+
? null
|
|
365
|
+
: opts.state === "failed"
|
|
366
|
+
? "Visual run 42 for this head concluded failure — its captures are unusable"
|
|
367
|
+
: "no Visual workflow run found for head abc123",
|
|
368
|
+
},
|
|
369
|
+
author: "system",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function assertNoRepairWorkScheduled(issueId) {
|
|
373
|
+
const pending = listWorkItemsForIssue(issueId).filter((item) => item.status === "pending" || item.status === "leased");
|
|
374
|
+
assert.deepEqual(pending.map((item) => item.kind), [], "missing visual evidence must never schedule a repair round");
|
|
375
|
+
}
|
|
376
|
+
for (const state of ["missing", "failed"]) {
|
|
377
|
+
test(`NOT-384: ${state} visual evidence on a UI head raises operator_verification and never a repair round`, async () => {
|
|
378
|
+
let merges = 0;
|
|
379
|
+
setMergePrForTests(async () => {
|
|
380
|
+
merges += 1;
|
|
381
|
+
return { ok: true };
|
|
382
|
+
});
|
|
383
|
+
const issueId = newIssue("It works");
|
|
384
|
+
startWorkflow(issueId);
|
|
385
|
+
recordVisualEvidence(issueId, { headSha: "abc123", state });
|
|
386
|
+
const result = await approve(issueId);
|
|
387
|
+
assert.equal(result.applied, true);
|
|
388
|
+
if (result.applied) {
|
|
389
|
+
assert.equal(result.issueStatus, "needs_human");
|
|
390
|
+
assert.equal(result.nextWorkItemId, null, "the gate queues no repair round");
|
|
391
|
+
assert.ok(result.humanActionId);
|
|
392
|
+
}
|
|
393
|
+
assert.equal(merges, 0, "the merge adapter must not run behind the gate");
|
|
394
|
+
assertNoRepairWorkScheduled(issueId);
|
|
395
|
+
const action = openOperatorAction(issueId);
|
|
396
|
+
assert.match(action.question, /abc123/, "the action names the head SHA");
|
|
397
|
+
assert.match(action.question, /ui-screenshots/, "the action names the missing artifacts");
|
|
398
|
+
assert.match(action.question, new RegExp(state), "the action names the evidence state");
|
|
399
|
+
const evidence = JSON.parse(action.evidenceJson);
|
|
400
|
+
assert.equal(evidence.operatorVerification.headSha, "abc123");
|
|
401
|
+
assert.equal(evidence.operatorVerification.visualEvidence.state, state);
|
|
402
|
+
assert.ok(evidence.operatorVerification.visualEvidence.missingArtifacts.length > 0);
|
|
403
|
+
const options = JSON.parse(action.responseOptionsJson);
|
|
404
|
+
assert.deepEqual(options.map((o) => o.choice), ["verified", "waive", "repair"]);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
test("NOT-384: available visual evidence on a UI head merges normally", async () => {
|
|
408
|
+
const calls = [];
|
|
409
|
+
setMergePrForTests(async (opts) => {
|
|
410
|
+
calls.push(opts);
|
|
411
|
+
return { ok: true };
|
|
412
|
+
});
|
|
413
|
+
const issueId = newIssue("It works");
|
|
414
|
+
startWorkflow(issueId);
|
|
415
|
+
recordVisualEvidence(issueId, { headSha: "abc123", state: "available" });
|
|
416
|
+
const result = await approve(issueId);
|
|
417
|
+
assert.equal(result.applied, true);
|
|
418
|
+
if (result.applied)
|
|
419
|
+
assert.equal(result.issueStatus, "done");
|
|
420
|
+
assert.deepEqual(calls, [{ cwd: managedCwd, number: 42 }]);
|
|
421
|
+
assert.equal(getIssue(issueId).status, "done");
|
|
422
|
+
});
|
|
423
|
+
test("NOT-384: [operator] criteria plus a visual hold raise one action naming both", async () => {
|
|
424
|
+
let merges = 0;
|
|
425
|
+
setMergePrForTests(async () => {
|
|
426
|
+
merges += 1;
|
|
427
|
+
return { ok: true };
|
|
428
|
+
});
|
|
429
|
+
const issueId = newIssue();
|
|
430
|
+
startWorkflow(issueId);
|
|
431
|
+
recordVisualEvidence(issueId, { headSha: "abc123", state: "missing" });
|
|
432
|
+
await approve(issueId);
|
|
433
|
+
assert.equal(merges, 0);
|
|
434
|
+
const open = listHumanActionsForIssue(issueId).filter((a) => a.status === "open");
|
|
435
|
+
assert.equal(open.length, 1, "criteria and visual hold share one action for the head");
|
|
436
|
+
assert.equal(open[0].actionType, "operator_verification");
|
|
437
|
+
assert.match(open[0].question, /sign in with SSO/);
|
|
438
|
+
assert.match(open[0].question, /ui-screenshots/);
|
|
439
|
+
const evidence = JSON.parse(open[0].evidenceJson);
|
|
440
|
+
assert.equal(evidence.operatorVerification.criteria.length, 1);
|
|
441
|
+
assert.equal(evidence.operatorVerification.visualEvidence.state, "missing");
|
|
442
|
+
// One recorded result releases the combined action — a second finalize for
|
|
443
|
+
// the same head reuses it instead of raising another.
|
|
444
|
+
const again = await finalizeAutoMerge(issueId);
|
|
445
|
+
assert.equal(again.humanActionId, open[0].id);
|
|
446
|
+
assert.equal(listHumanActionsForIssue(issueId).filter((a) => a.status === "open").length, 1);
|
|
447
|
+
assert.equal(merges, 0);
|
|
448
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// NOT-422: the NOT-405-like product-scope sequence end to end — reviewer
|
|
2
|
+
// escalation → amended Linear ticket adopted at the pause → Resume → next
|
|
3
|
+
// developer and reviewer enforce only the amended snapshot, on the same
|
|
4
|
+
// workflow, PR, branch, and worktree.
|
|
5
|
+
import { test, before, beforeEach } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-scope-contract-"));
|
|
11
|
+
const { migrate, getDb } = await import("../db/index.js");
|
|
12
|
+
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
13
|
+
const { createIssue, getIssue } = await import("../repository/issues.js");
|
|
14
|
+
const { startWorkflow, applyCompletion, applyParkedEdit, resolveHumanActionAndAdvance, getTaskSnapshot, diffTaskSnapshotFromLive, TASK_SNAPSHOT_ARTIFACT_KIND, } = await import("./commands.js");
|
|
15
|
+
const { reloadIssueSourceFromLinear } = await import("./source-reload.js");
|
|
16
|
+
const { claimWorkItem, listWorkItemsForIssue } = await import("../repository/work-items.js");
|
|
17
|
+
const { listHumanActionsForIssue } = await import("../repository/human-actions.js");
|
|
18
|
+
const { getActiveWorkflowInstance, listWorkflowEventsForIssue } = await import("../repository/workflow-events.js");
|
|
19
|
+
const { listArtifactsForIssue } = await import("../repository/artifacts-for-issue.js");
|
|
20
|
+
const { listWorkerSessionsForIssue } = await import("../repository/worker-sessions.js");
|
|
21
|
+
const { ReviewerResult } = await import("./reviewer-result.js");
|
|
22
|
+
const { buildDeveloperPrompt, buildReviewerPrompt } = await import("./prompts.js");
|
|
23
|
+
const { stubManagedCloneForTests } = await import("../adapters/managed-repo.js");
|
|
24
|
+
before(() => migrate());
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
getDb().exec(`
|
|
27
|
+
DELETE FROM work_items;
|
|
28
|
+
DELETE FROM human_actions;
|
|
29
|
+
DELETE FROM workflow_events;
|
|
30
|
+
DELETE FROM worker_sessions;
|
|
31
|
+
DELETE FROM artifacts;
|
|
32
|
+
DELETE FROM workflow_instances;
|
|
33
|
+
DELETE FROM queue_entries;
|
|
34
|
+
DELETE FROM issues;
|
|
35
|
+
`);
|
|
36
|
+
stubManagedCloneForTests("acme/app");
|
|
37
|
+
});
|
|
38
|
+
// The ticket as frozen at workflow start: clean-machine proof is still an
|
|
39
|
+
// [operator] criterion the reviewer escalates on.
|
|
40
|
+
const CONTRACT_V1 = [
|
|
41
|
+
"Ship the friend-path run command.",
|
|
42
|
+
"",
|
|
43
|
+
"## Builder execution mode",
|
|
44
|
+
"feature",
|
|
45
|
+
"",
|
|
46
|
+
"## Non-goals",
|
|
47
|
+
"- Rewriting the installer",
|
|
48
|
+
"",
|
|
49
|
+
"## Exit predicate",
|
|
50
|
+
"The friend-path run command works on a clean machine with operator proof.",
|
|
51
|
+
"",
|
|
52
|
+
"## One-PR stopping point",
|
|
53
|
+
"Stop after the run command lands with its regression test.",
|
|
54
|
+
"",
|
|
55
|
+
"## Acceptance criteria",
|
|
56
|
+
"- [ ] Friend-path run works",
|
|
57
|
+
" Evidence: run fixtures | run the tests | green",
|
|
58
|
+
"- [ ] [operator] Clean-machine proof attached",
|
|
59
|
+
" Evidence: operator run | paste the output | reviewed",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n");
|
|
62
|
+
// The amended ticket: clean-machine proof moved out, simulator proof in.
|
|
63
|
+
const CONTRACT_V2 = [
|
|
64
|
+
"Ship the friend-path run command.",
|
|
65
|
+
"",
|
|
66
|
+
"## Builder execution mode",
|
|
67
|
+
"feature",
|
|
68
|
+
"",
|
|
69
|
+
"## Non-goals",
|
|
70
|
+
"- Rewriting the installer",
|
|
71
|
+
"",
|
|
72
|
+
"## Exit predicate",
|
|
73
|
+
"The friend-path run command works with simulator proof; clean-machine proof moved out.",
|
|
74
|
+
"",
|
|
75
|
+
"## One-PR stopping point",
|
|
76
|
+
"Stop after the run command lands with its regression test.",
|
|
77
|
+
"",
|
|
78
|
+
"## Acceptance criteria",
|
|
79
|
+
"- [ ] Friend-path run works",
|
|
80
|
+
" Evidence: run fixtures | run the tests | green",
|
|
81
|
+
"- [ ] Simulator proof attached",
|
|
82
|
+
" Evidence: simulator run | paste the output | reviewed",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
const REMOVED_CRITERION = "[operator] Clean-machine proof attached";
|
|
86
|
+
const AMENDED_EXIT = "The friend-path run command works with simulator proof; clean-machine proof moved out.";
|
|
87
|
+
async function adoptViaReload(issueId) {
|
|
88
|
+
// The operator reloads the amended Linear ticket at the pause (injected
|
|
89
|
+
// fetch — no network; the CommonJS fetch mock lives in the route suites).
|
|
90
|
+
const reloaded = await reloadIssueSourceFromLinear(issueId, {
|
|
91
|
+
fetchLinearIssue: async (externalId) => ({
|
|
92
|
+
id: externalId,
|
|
93
|
+
identifier: "NOT-405",
|
|
94
|
+
title: "Friend-path run, rescoped",
|
|
95
|
+
description: CONTRACT_V2,
|
|
96
|
+
url: "https://linear.app/not-so-fat/issue/NOT-405/x",
|
|
97
|
+
attachments: [],
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
assert.equal(reloaded.ok, true);
|
|
101
|
+
}
|
|
102
|
+
async function adoptViaLocalEdit(issueId) {
|
|
103
|
+
// The Dealer-local path: the same amended contract saved through the
|
|
104
|
+
// parked-edit surface instead of a Linear reload.
|
|
105
|
+
const { compileExecutionContract } = await import("@agent-dealer/shared");
|
|
106
|
+
const edited = applyParkedEdit(issueId, {
|
|
107
|
+
title: "NOT-405: friend-path run, rescoped",
|
|
108
|
+
description: CONTRACT_V2,
|
|
109
|
+
acceptanceCriteria: compileExecutionContract(CONTRACT_V2).acceptanceCriteria,
|
|
110
|
+
});
|
|
111
|
+
assert.equal(edited.ok, true);
|
|
112
|
+
}
|
|
113
|
+
async function runScopePauseAdoptionFlow(adopt) {
|
|
114
|
+
const issueId = createIssue({
|
|
115
|
+
title: "NOT-405: friend-path run",
|
|
116
|
+
description: CONTRACT_V1,
|
|
117
|
+
repo: "acme/app",
|
|
118
|
+
baseBranch: "main",
|
|
119
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
120
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
121
|
+
source: "linear",
|
|
122
|
+
externalId: "linear-uuid-405",
|
|
123
|
+
externalLabel: "NOT-405",
|
|
124
|
+
externalUrl: "https://linear.app/not-so-fat/issue/NOT-405/x",
|
|
125
|
+
}).id;
|
|
126
|
+
assert.equal(startWorkflow(issueId).ok, true);
|
|
127
|
+
// Round 1: developer hands off, reviewer escalates a product-scope question.
|
|
128
|
+
const dev1 = claimWorkItem("scope-contract-test", { leaseMs: 60_000 });
|
|
129
|
+
await applyCompletion(dev1.id, dev1.leaseToken, {
|
|
130
|
+
kind: "clean_handoff",
|
|
131
|
+
branch: "dealer/not-405",
|
|
132
|
+
headSha: "abc123",
|
|
133
|
+
baseSha: "base1",
|
|
134
|
+
prNumber: 42,
|
|
135
|
+
prUrl: "https://gh/pr/42",
|
|
136
|
+
});
|
|
137
|
+
const rev1 = claimWorkItem("scope-contract-test", { leaseMs: 60_000 });
|
|
138
|
+
await applyCompletion(rev1.id, rev1.leaseToken, {
|
|
139
|
+
kind: "verdict",
|
|
140
|
+
result: ReviewerResult.parse({
|
|
141
|
+
verdict: "escalated",
|
|
142
|
+
productScopeQuestion: "Is clean-machine operator proof still required?",
|
|
143
|
+
baseSha: "b",
|
|
144
|
+
headSha: "h",
|
|
145
|
+
acceptanceCriteriaAssessment: "ok",
|
|
146
|
+
evidenceAssessment: "ok",
|
|
147
|
+
findings: [],
|
|
148
|
+
risks: [],
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
const action = listHumanActionsForIssue(issueId).find((a) => a.actionType === "product_scope_decision");
|
|
152
|
+
const instanceBefore = getActiveWorkflowInstance(issueId);
|
|
153
|
+
const issueBefore = getIssue(issueId);
|
|
154
|
+
assert.equal(issueBefore.status, "needs_human");
|
|
155
|
+
// The frozen snapshot still enforces the old operator criterion.
|
|
156
|
+
assert.ok(getTaskSnapshot(issueBefore).acceptanceCriteria.includes(REMOVED_CRITERION));
|
|
157
|
+
// The operator adopts the amended contract at the pause — reload or local
|
|
158
|
+
// edit depending on the variant under test.
|
|
159
|
+
await adopt(issueId);
|
|
160
|
+
// The amended task is pending inspection: live rows changed, the open
|
|
161
|
+
// action and every workflow identity preserved, the snapshot untouched.
|
|
162
|
+
const parked = getIssue(issueId);
|
|
163
|
+
assert.equal(parked.description, CONTRACT_V2);
|
|
164
|
+
assert.equal(parked.executionContract?.exitPredicate, AMENDED_EXIT);
|
|
165
|
+
assert.equal(listHumanActionsForIssue(issueId).find((a) => a.id === action.id).status, "open");
|
|
166
|
+
assert.equal(getActiveWorkflowInstance(issueId).id, instanceBefore.id);
|
|
167
|
+
assert.equal(parked.prNumber, 42);
|
|
168
|
+
assert.equal(parked.branch, "dealer/not-405");
|
|
169
|
+
assert.ok(getTaskSnapshot(parked).acceptanceCriteria.includes(REMOVED_CRITERION));
|
|
170
|
+
assert.ok(diffTaskSnapshotFromLive(parked).length > 0);
|
|
171
|
+
// Resume freezes exactly one new snapshot before the next developer item.
|
|
172
|
+
const sessionsBeforeResume = listWorkerSessionsForIssue(issueId);
|
|
173
|
+
const snapshotsBefore = listArtifactsForIssue(issueId).filter((a) => a.kind === TASK_SNAPSHOT_ARTIFACT_KIND).length;
|
|
174
|
+
const resolved = resolveHumanActionAndAdvance(action.id, "operator", "resume", {
|
|
175
|
+
note: "Clean-machine proof moved to NOT-421; enforce simulator proof.",
|
|
176
|
+
});
|
|
177
|
+
assert.equal(resolved.ok, true);
|
|
178
|
+
const snapshotsAfter = listArtifactsForIssue(issueId).filter((a) => a.kind === TASK_SNAPSHOT_ARTIFACT_KIND);
|
|
179
|
+
assert.equal(snapshotsAfter.length, snapshotsBefore + 1);
|
|
180
|
+
const events = listWorkflowEventsForIssue(issueId);
|
|
181
|
+
const refreshed = events.filter((e) => e.type === "task_snapshot.refreshed");
|
|
182
|
+
assert.equal(refreshed.length, 1);
|
|
183
|
+
const types = events.map((e) => e.type);
|
|
184
|
+
assert.ok(types.indexOf("task_snapshot.refreshed") < types.lastIndexOf("repair.started"));
|
|
185
|
+
const resumed = getIssue(issueId);
|
|
186
|
+
assert.equal(getActiveWorkflowInstance(issueId).id, instanceBefore.id);
|
|
187
|
+
assert.equal(resumed.prNumber, issueBefore.prNumber);
|
|
188
|
+
assert.equal(resumed.prUrl, issueBefore.prUrl);
|
|
189
|
+
assert.equal(resumed.branch, issueBefore.branch);
|
|
190
|
+
assert.deepEqual(listWorkerSessionsForIssue(issueId), sessionsBeforeResume);
|
|
191
|
+
const pendingDev = listWorkItemsForIssue(issueId).filter((i) => i.status === "pending");
|
|
192
|
+
assert.equal(pendingDev.length, 1);
|
|
193
|
+
assert.equal(pendingDev[0].kind, "developer");
|
|
194
|
+
assert.equal(pendingDev[0].workflowInstanceId, instanceBefore.id);
|
|
195
|
+
const devPayload = JSON.parse(pendingDev[0].payloadJson);
|
|
196
|
+
assert.equal(devPayload.scopeDecisionNote, "Clean-machine proof moved to NOT-421; enforce simulator proof.");
|
|
197
|
+
// The next developer prompt — built the way the developer effect builds it,
|
|
198
|
+
// from this work item's own round and one-shot note over the frozen
|
|
199
|
+
// snapshot — enforces only the amended snapshot.
|
|
200
|
+
const developerPrompt = buildDeveloperPrompt({
|
|
201
|
+
taskSnapshot: getTaskSnapshot(resumed),
|
|
202
|
+
round: pendingDev[0].round,
|
|
203
|
+
scopeDecisionNote: devPayload.scopeDecisionNote,
|
|
204
|
+
});
|
|
205
|
+
assert.ok(developerPrompt.includes(AMENDED_EXIT));
|
|
206
|
+
assert.ok(developerPrompt.includes("Simulator proof attached"));
|
|
207
|
+
assert.ok(!developerPrompt.includes(REMOVED_CRITERION));
|
|
208
|
+
assert.ok(!developerPrompt.includes("clean machine with operator proof"));
|
|
209
|
+
// The developer completes; the following reviewer prompt enforces the same
|
|
210
|
+
// amended snapshot — no escalation for the removed criterion can recur.
|
|
211
|
+
const dev2 = claimWorkItem("scope-contract-test", { leaseMs: 60_000 });
|
|
212
|
+
assert.equal(dev2.id, pendingDev[0].id);
|
|
213
|
+
await applyCompletion(dev2.id, dev2.leaseToken, {
|
|
214
|
+
kind: "clean_handoff",
|
|
215
|
+
branch: "dealer/not-405",
|
|
216
|
+
headSha: "def456",
|
|
217
|
+
baseSha: "base1",
|
|
218
|
+
prNumber: 42,
|
|
219
|
+
prUrl: "https://gh/pr/42",
|
|
220
|
+
});
|
|
221
|
+
const reviewRound = getIssue(issueId);
|
|
222
|
+
assert.equal(reviewRound.status, "reviewing");
|
|
223
|
+
assert.equal(getActiveWorkflowInstance(issueId).id, instanceBefore.id);
|
|
224
|
+
assert.equal(reviewRound.branch, "dealer/not-405");
|
|
225
|
+
// The following reviewer work item — claimed the way the effect worker
|
|
226
|
+
// claims it — carries the same workflow instance, and its prompt inputs
|
|
227
|
+
// (this item's round over the frozen snapshot) enforce the amended
|
|
228
|
+
// snapshot with no escalation for the removed criterion recurring.
|
|
229
|
+
const rev2 = claimWorkItem("scope-contract-test", { leaseMs: 60_000 });
|
|
230
|
+
assert.equal(rev2.kind, "reviewer");
|
|
231
|
+
assert.equal(rev2.workflowInstanceId, instanceBefore.id);
|
|
232
|
+
const reviewerPrompt = buildReviewerPrompt({
|
|
233
|
+
taskSnapshot: getTaskSnapshot(reviewRound),
|
|
234
|
+
round: rev2.round,
|
|
235
|
+
baseSha: "a".repeat(40),
|
|
236
|
+
headSha: "b".repeat(40),
|
|
237
|
+
diff: "diff --git a/x b/x\n+added line\n",
|
|
238
|
+
});
|
|
239
|
+
assert.ok(reviewerPrompt.includes(AMENDED_EXIT));
|
|
240
|
+
assert.ok(reviewerPrompt.includes("Simulator proof attached"));
|
|
241
|
+
assert.ok(!reviewerPrompt.includes(REMOVED_CRITERION));
|
|
242
|
+
assert.ok(!reviewerPrompt.includes("clean machine with operator proof"));
|
|
243
|
+
}
|
|
244
|
+
test("NOT-422: reviewer escalation → scope-pause reload → Resume → both roles enforce only the amended snapshot", async () => {
|
|
245
|
+
await runScopePauseAdoptionFlow(adoptViaReload);
|
|
246
|
+
});
|
|
247
|
+
test("NOT-422: reviewer escalation → scope-pause local edit → Resume → both roles enforce only the amended snapshot", async () => {
|
|
248
|
+
await runScopePauseAdoptionFlow(adoptViaLocalEdit);
|
|
249
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sourceAttachmentsDeveloperSection, sourceAttachmentsReviewerSection, } from "./source-attachments.js";
|
|
2
2
|
import { MUSE_SANDBOX_CAPABILITIES } from "../runners/muse-code-args.js";
|
|
3
3
|
import { checkMuseVisualQa, museVisualQaPromptSection, } from "../adapters/muse-visual-qa.js";
|
|
4
|
+
import { capableDeveloperVisualQaPromptSection, developerVisualQaPolicy, visualQaReviewerSection, } from "./visual-qa.js";
|
|
4
5
|
import { formatVerificationReceiptSection, } from "./verification-receipt.js";
|
|
5
6
|
import { formatOperatorCriteria, } from "./operator-criteria.js";
|
|
6
7
|
/**
|
|
@@ -284,10 +285,15 @@ export function buildDeveloperPrompt(input) {
|
|
|
284
285
|
parts.push(...museEnvironmentLimitsSection());
|
|
285
286
|
// NOT-303: the screenshot-path preflight verdict is embedded before the session
|
|
286
287
|
// starts, so the worker reads it instead of discovering the Chrome.app abort
|
|
287
|
-
// mid-session.
|
|
288
|
-
|
|
288
|
+
// mid-session. NOT-381: non-Muse runtimes get the capable-runtime in-session
|
|
289
|
+
// attempt instead of the old blanket no-browser assumption.
|
|
290
|
+
const visualQaPolicy = input.museDeveloper ? "muse" : developerVisualQaPolicy(input.runtime);
|
|
291
|
+
if (visualQaPolicy === "muse") {
|
|
289
292
|
parts.push(...museVisualQaPromptSection(input.museVisualQa ?? checkMuseVisualQa()));
|
|
290
293
|
}
|
|
294
|
+
else {
|
|
295
|
+
parts.push(...capableDeveloperVisualQaPromptSection());
|
|
296
|
+
}
|
|
291
297
|
parts.push(`## Required`,
|
|
292
298
|
// NOT-315: the coordinator provisions dependencies before the session spawns —
|
|
293
299
|
// the sandbox has no network, so an in-session install can never work.
|
|
@@ -370,6 +376,49 @@ export function formatDiffForPrompt(diff) {
|
|
|
370
376
|
omittedPaths,
|
|
371
377
|
};
|
|
372
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* NOT-384: SHA-bound CI visual evidence for this head. `available` names the
|
|
381
|
+
* staged paths, both SHAs, and the diff summary, and instructs the reviewer to
|
|
382
|
+
* judge the screenshots and the base-vs-head diff at both viewports. `missing`
|
|
383
|
+
* and `failed` name the state and forbid a visual pass — and forbid a blocking
|
|
384
|
+
* finding about the missing evidence itself, since Dealer holds the head for a
|
|
385
|
+
* human operator automatically (a repair round must never start solely for
|
|
386
|
+
* missing visual evidence). Absent renders nothing.
|
|
387
|
+
*/
|
|
388
|
+
function visualEvidenceSection(evidence) {
|
|
389
|
+
if (!evidence)
|
|
390
|
+
return [];
|
|
391
|
+
const head8 = evidence.headSha.slice(0, 8);
|
|
392
|
+
if (evidence.state === "available") {
|
|
393
|
+
const staged = evidence.staged;
|
|
394
|
+
const lines = [
|
|
395
|
+
`## Visual evidence (CI-captured at ${head8})`,
|
|
396
|
+
`The \`Visual\` CI workflow captured this exact head (\`${evidence.headSha}\`, base \`${evidence.baseSha}\`${evidence.runId != null ? `, run ${evidence.runId}` : ""}) at the required viewports (1440x900 and 390x800), running each route's interaction steps before the screenshot. Judge the screenshots and the base-vs-head diff at both viewports — a nonzero pixel diff is evidence, never an automatic failure; decide whether the rendered changes match the task. A genuine rendered defect IS a coding finding; captures you cannot judge are a risk, never a pass.`,
|
|
397
|
+
``,
|
|
398
|
+
];
|
|
399
|
+
if (staged) {
|
|
400
|
+
lines.push(`- Head captures: \`${staged.screenshotsDir}/\``, `- Baseline captures: \`${staged.baselineDir}/\``, `- Diffs and summary: \`${staged.diffDir}/\``, ``);
|
|
401
|
+
}
|
|
402
|
+
if (evidence.files) {
|
|
403
|
+
for (const name of ["ui-screenshots", "ui-baseline", "ui-diff"]) {
|
|
404
|
+
const listing = evidence.files[name];
|
|
405
|
+
const extra = listing.total > listing.files.length ? ` (+${listing.total - listing.files.length} more)` : "";
|
|
406
|
+
lines.push(`\`${name}/\` (${listing.total} files${extra}): ${listing.files.map((f) => `\`${f}\``).join(", ") || "(empty)"}`);
|
|
407
|
+
}
|
|
408
|
+
lines.push(``);
|
|
409
|
+
}
|
|
410
|
+
lines.push(`### Diff summary (\`SUMMARY.md\`${staged?.summaryPath ? ` at \`${staged.summaryPath}\`` : ""})`, evidence.summary ?? "(the staged ui-diff directory has no SUMMARY.md — judge the diff PNGs directly)", ``, `Read the PNGs with your file tools — every path above is inside your read-only checkout. Record \`visual QA: verified (<n> shots at ${head8})\` in \`evidenceAssessment\`, naming the shots you judged.`, ``);
|
|
411
|
+
return lines;
|
|
412
|
+
}
|
|
413
|
+
const reason = evidence.reason ?? "no usable CI visual captures";
|
|
414
|
+
return [
|
|
415
|
+
`## Visual evidence: ${evidence.state} — no usable CI captures for this head`,
|
|
416
|
+
`The \`Visual\` CI workflow produced no usable screenshots for this exact head (\`${evidence.headSha}\`, base \`${evidence.baseSha}\`): ${reason}.`,
|
|
417
|
+
``,
|
|
418
|
+
`Do NOT report a visual pass: write \`visual QA: not run (${evidence.state})\` in \`evidenceAssessment\`. Do not raise a blocking finding about the missing evidence itself — Dealer holds this head for a human operator automatically, and a repair round must never start solely for missing visual evidence. Judge only the code in the diff.`,
|
|
419
|
+
``,
|
|
420
|
+
];
|
|
421
|
+
}
|
|
373
422
|
export function buildReviewerPrompt(input) {
|
|
374
423
|
const parts = [
|
|
375
424
|
input.round === 1
|
|
@@ -405,6 +454,12 @@ export function buildReviewerPrompt(input) {
|
|
|
405
454
|
if (input.checksSummary) {
|
|
406
455
|
parts.push(`## CI checks`, input.checksSummary, ``);
|
|
407
456
|
}
|
|
457
|
+
// NOT-384: SHA-bound CI captures for this head (or the missing/failed
|
|
458
|
+
// notice) — rendered only when the coordinator resolved visual evidence.
|
|
459
|
+
parts.push(...visualEvidenceSection(input.visualEvidence));
|
|
460
|
+
// NOT-381: the coordinator-validated visual receipt (or loud rejection),
|
|
461
|
+
// SHA-bound to the pinned head — the raw conclusion line above is unverified.
|
|
462
|
+
parts.push(...visualQaReviewerSection(input.visualQa?.record ?? null, input.visualQa?.pinnedHeadSha ?? input.headSha));
|
|
408
463
|
if (input.findings?.length) {
|
|
409
464
|
parts.push(`## Findings from prior rounds (verify each is actually resolved)`);
|
|
410
465
|
for (const f of input.findings) {
|