agent-dealer 1.3.1 → 1.3.2
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 +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/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/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/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/package.json +2 -2
- package/bundle/server/static-ui/assets/index-DQ5ivcyg.js +64 -0
- package/bundle/server/static-ui/index.html +1 -1
- 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
|
@@ -10,7 +10,7 @@ import { listUsageEventsForIssue, summarizeIssueUsage } from "../repository/usag
|
|
|
10
10
|
import { listWorkflowEventsForIssue, appendWorkflowEvent, listWorkflowInstancesForIssue, getActiveWorkflowInstance, } from "../repository/workflow-events.js";
|
|
11
11
|
import { listHumanActionsForIssue, listOpenHumanActions } from "../repository/human-actions.js";
|
|
12
12
|
import { listFindingsForIssue } from "../repository/findings.js";
|
|
13
|
-
import { abortIssueAsync, applyParkedEdit, canEditParkedIssue, capWaitForIssue, checkIssueReadiness, closeReadyIssue, parkCapWait,
|
|
13
|
+
import { abortIssueAsync, applyParkedEdit, canEditParkedIssue, capWaitForIssue, checkIssueReadiness, closeReadyIssue, diffTaskSnapshotFromLive, parkCapWait, parkedEditableFields, parkedEditableFieldsLabel } from "../coordinator/commands.js";
|
|
14
14
|
import { deleteDealerIssue } from "../coordinator/delete-issue.js";
|
|
15
15
|
import { getAgent } from "../repository/agents.js";
|
|
16
16
|
import { executeIssueNow, isStartable, queueStatusForIssue, refreshQueueWaitReasonForIssue, startIssueViaQueue, } from "../coordinator/admission.js";
|
|
@@ -82,6 +82,10 @@ export async function registerIssueRoutes(app) {
|
|
|
82
82
|
const statuses = requested.length > 0
|
|
83
83
|
? requested
|
|
84
84
|
: IssueStatus.options.filter((s) => s !== "closed");
|
|
85
|
+
// NOT-385: the direction is allowlisted — only an explicit `asc` reverses
|
|
86
|
+
// to oldest-first; an absent or unknown value renders newest-first
|
|
87
|
+
// instead of failing the list.
|
|
88
|
+
const direction = query.direction?.trim() === "asc" ? "asc" : "desc";
|
|
85
89
|
const result = queryIssues({
|
|
86
90
|
search: query.q,
|
|
87
91
|
status: statuses,
|
|
@@ -89,6 +93,7 @@ export async function registerIssueRoutes(app) {
|
|
|
89
93
|
needsAttention: query.needsAttention === "1" || query.needsAttention === "true",
|
|
90
94
|
page: query.page !== undefined ? Number(query.page) : undefined,
|
|
91
95
|
limit: query.limit !== undefined ? Number(query.limit) : undefined,
|
|
96
|
+
direction,
|
|
92
97
|
});
|
|
93
98
|
const openActionIssueIds = new Set(listOpenHumanActions().map((a) => a.issueId));
|
|
94
99
|
return {
|
|
@@ -154,6 +159,10 @@ export async function registerIssueRoutes(app) {
|
|
|
154
159
|
// NOT-364: durable Linear source attachments — files with safe name, size
|
|
155
160
|
// and checksum plus Dealer blob path; links as labeled metadata only.
|
|
156
161
|
sourceAttachments: listSourceAttachments(id),
|
|
162
|
+
// NOT-422: live-vs-frozen task diff — the fields a parked resume would
|
|
163
|
+
// re-freeze into a new task snapshot (empty = in sync, or pre-freeze).
|
|
164
|
+
// Issue Detail renders the pending-snapshot notice off this.
|
|
165
|
+
pendingSnapshotChangedFields: diffTaskSnapshotFromLive(issue),
|
|
157
166
|
};
|
|
158
167
|
});
|
|
159
168
|
/** NOT-173: stable issue-level execution read model. The existing
|
|
@@ -348,18 +357,21 @@ export async function registerIssueRoutes(app) {
|
|
|
348
357
|
if (getActiveWorkerSessionForIssue(id)) {
|
|
349
358
|
return "Cannot edit an issue with an actively running session";
|
|
350
359
|
}
|
|
351
|
-
// NOT-185: the
|
|
352
|
-
// or
|
|
353
|
-
//
|
|
360
|
+
// NOT-185: the active-workflow exception — parked at an open attempts_exhausted,
|
|
361
|
+
// policy_escalation, or (NOT-422) product_scope_decision action with nothing
|
|
362
|
+
// pending/leased. A resume re-freezes the snapshot (task fields) and, at an
|
|
363
|
+
// attempts/policy park, the next round's profile snapshot (agent fields).
|
|
354
364
|
if (getActiveWorkflowInstance(id)) {
|
|
355
365
|
if (!canEditParkedIssue(fresh)) {
|
|
356
366
|
return "Cannot edit an issue with an active workflow";
|
|
357
367
|
}
|
|
358
368
|
// Only the re-frozen inputs may change at the park: repo/base branch, the review
|
|
359
|
-
// budget and autoMerge stay as the running workflow saw them
|
|
360
|
-
|
|
369
|
+
// budget and autoMerge stay as the running workflow saw them — and at a pure
|
|
370
|
+
// product-scope pause the agents stay too (task text only).
|
|
371
|
+
const allowed = parkedEditableFields(id);
|
|
372
|
+
const blocked = Object.keys(parsed.data).filter((k) => !allowed.has(k));
|
|
361
373
|
if (blocked.length > 0) {
|
|
362
|
-
return `Only
|
|
374
|
+
return `Only ${parkedEditableFieldsLabel(id)} can be edited while parked (got: ${blocked.join(", ")})`;
|
|
363
375
|
}
|
|
364
376
|
}
|
|
365
377
|
return null;
|
|
@@ -453,7 +465,10 @@ export async function registerIssueRoutes(app) {
|
|
|
453
465
|
/**
|
|
454
466
|
* NOT-363: reload a Linear-sourced issue's task text from its linked ticket.
|
|
455
467
|
* Only while `ready` with no active workflow or running worker — a race
|
|
456
|
-
* with admission fails closed with 409.
|
|
468
|
+
* with admission fails closed with 409. NOT-422 adds the parked exception:
|
|
469
|
+
* a `needs_human` product-scope pause (open `product_scope_decision`, no
|
|
470
|
+
* running session, nothing pending/leased) reloads the same way and keeps
|
|
471
|
+
* the action open for an explicit Resume. Replaces title + description
|
|
457
472
|
* atomically, recompiles the execution contract, and swaps the acceptance
|
|
458
473
|
* criteria for the freshly derived ones; repo, agents, policy fields,
|
|
459
474
|
* auto-merge, queue position, and coordinator-owned fields are preserved.
|
|
@@ -735,6 +735,86 @@ test("NOT-228: limit clamps to 100 and out-of-range pages keep totals", async ()
|
|
|
735
735
|
assert.equal(pastEnd.page, 9);
|
|
736
736
|
await app.close();
|
|
737
737
|
});
|
|
738
|
+
// NOT-385: Latest with no direction renders newest-first; the ascending
|
|
739
|
+
// reverse renders the same cohort oldest-first, and an unknown direction
|
|
740
|
+
// falls back to newest-first instead of failing.
|
|
741
|
+
test("NOT-385: direction defaults to newest-first, asc reverses, unknown falls back", async () => {
|
|
742
|
+
const app = await buildApp();
|
|
743
|
+
const oldIssue = await r228Seed(app, "R385 order old");
|
|
744
|
+
const newIssue = await r228Seed(app, "R385 order new");
|
|
745
|
+
getDb().prepare("UPDATE issues SET updated_at = ? WHERE id = ?").run("2026-08-01T00:00:00.000Z", oldIssue.id);
|
|
746
|
+
getDb().prepare("UPDATE issues SET updated_at = ? WHERE id = ?").run("2026-09-01T00:00:00.000Z", newIssue.id);
|
|
747
|
+
const q = encodeURIComponent("R385 order");
|
|
748
|
+
const newest = await r228Get(app, `?q=${q}&page=1&limit=10`);
|
|
749
|
+
assert.deepEqual(newest.issues.map((i) => i.id), [newIssue.id, oldIssue.id]);
|
|
750
|
+
const explicitDesc = await r228Get(app, `?q=${q}&page=1&limit=10&direction=desc`);
|
|
751
|
+
assert.deepEqual(explicitDesc.issues.map((i) => i.id), [newIssue.id, oldIssue.id]);
|
|
752
|
+
const oldest = await r228Get(app, `?q=${q}&page=1&limit=10&direction=asc`);
|
|
753
|
+
assert.deepEqual(oldest.issues.map((i) => i.id), [oldIssue.id, newIssue.id]);
|
|
754
|
+
assert.equal(oldest.total, newest.total);
|
|
755
|
+
const fallback = await r228Get(app, `?q=${q}&page=1&limit=10&direction=sideways`);
|
|
756
|
+
assert.deepEqual(fallback.issues.map((i) => i.id), [newIssue.id, oldIssue.id]);
|
|
757
|
+
await app.close();
|
|
758
|
+
});
|
|
759
|
+
// NOT-385: equal `updated_at` rows paginate without duplicates or skips in
|
|
760
|
+
// either direction — the rowid tie-breaker follows the timestamp direction
|
|
761
|
+
// before LIMIT/OFFSET applies.
|
|
762
|
+
test("NOT-385: equal timestamps paginate repeatably in both directions", async () => {
|
|
763
|
+
const app = await buildApp();
|
|
764
|
+
const ids = [];
|
|
765
|
+
for (const title of ["R385 tie one", "R385 tie two", "R385 tie three"]) {
|
|
766
|
+
ids.push((await r228Seed(app, title)).id);
|
|
767
|
+
}
|
|
768
|
+
for (const id of ids) {
|
|
769
|
+
getDb().prepare("UPDATE issues SET updated_at = ? WHERE id = ?").run("2026-09-01T00:00:00.000Z", id);
|
|
770
|
+
}
|
|
771
|
+
const q = encodeURIComponent("R385 tie");
|
|
772
|
+
const descFirst = await r228Get(app, `?q=${q}&limit=2&page=1`);
|
|
773
|
+
const descSecond = await r228Get(app, `?q=${q}&limit=2&page=2`);
|
|
774
|
+
assert.deepEqual(descFirst.issues.map((i) => i.id), [ids[2], ids[1]]);
|
|
775
|
+
assert.deepEqual(descSecond.issues.map((i) => i.id), [ids[0]]);
|
|
776
|
+
const ascFirst = await r228Get(app, `?q=${q}&limit=2&page=1&direction=asc`);
|
|
777
|
+
const ascSecond = await r228Get(app, `?q=${q}&limit=2&page=2&direction=asc`);
|
|
778
|
+
assert.deepEqual(ascFirst.issues.map((i) => i.id), [ids[0], ids[1]]);
|
|
779
|
+
assert.deepEqual(ascSecond.issues.map((i) => i.id), [ids[2]]);
|
|
780
|
+
for (const [first, second] of [[descFirst, descSecond], [ascFirst, ascSecond]]) {
|
|
781
|
+
assert.equal(first.total, 3);
|
|
782
|
+
assert.equal(first.totalPages, 2);
|
|
783
|
+
assert.deepEqual(new Set([...first.issues, ...second.issues].map((i) => i.id)).size, 3, "pages neither duplicate nor skip rows");
|
|
784
|
+
}
|
|
785
|
+
await app.close();
|
|
786
|
+
});
|
|
787
|
+
// NOT-385: the direction composes with every filter — the reverse renders the
|
|
788
|
+
// same result set, only oldest-first.
|
|
789
|
+
test("NOT-385: direction coexists with the search, status, repo, and attention filters", async () => {
|
|
790
|
+
const app = await buildApp();
|
|
791
|
+
const repo = "github.com/r385/filtered";
|
|
792
|
+
const keep = await r228Seed(app, "R385 filter keep me", { repo });
|
|
793
|
+
const moved = await r228Seed(app, "R385 filter moved on", { repo });
|
|
794
|
+
await r228Seed(app, "R385 filter other repo", { repo: "github.com/r385/elsewhere" });
|
|
795
|
+
transitionIssue(moved.id, "developing");
|
|
796
|
+
createHumanAction({
|
|
797
|
+
issueId: moved.id,
|
|
798
|
+
actionType: "final_review",
|
|
799
|
+
reason: "Reviewer approved",
|
|
800
|
+
question: "Accept?",
|
|
801
|
+
responseOptions: ["complete"],
|
|
802
|
+
});
|
|
803
|
+
getDb().prepare("UPDATE issues SET updated_at = ? WHERE id = ?").run("2026-08-01T00:00:00.000Z", keep.id);
|
|
804
|
+
getDb().prepare("UPDATE issues SET updated_at = ? WHERE id = ?").run("2026-09-01T00:00:00.000Z", moved.id);
|
|
805
|
+
const q = encodeURIComponent("R385 filter");
|
|
806
|
+
const repoParam = encodeURIComponent(repo);
|
|
807
|
+
const newest = await r228Get(app, `?q=${q}&repo=${repoParam}&page=1&limit=10`);
|
|
808
|
+
assert.deepEqual(newest.issues.map((i) => i.id), [moved.id, keep.id]);
|
|
809
|
+
const oldest = await r228Get(app, `?q=${q}&repo=${repoParam}&page=1&limit=10&direction=asc`);
|
|
810
|
+
assert.deepEqual(oldest.issues.map((i) => i.id), [keep.id, moved.id]);
|
|
811
|
+
assert.equal(oldest.total, newest.total);
|
|
812
|
+
const byStatus = await r228Get(app, `?q=${q}&status=developing&page=1&limit=10&direction=asc`);
|
|
813
|
+
assert.deepEqual(byStatus.issues.map((i) => i.id), [moved.id]);
|
|
814
|
+
const attention = await r228Get(app, `?q=${q}&needsAttention=1&page=1&limit=10&direction=asc`);
|
|
815
|
+
assert.deepEqual(attention.issues.map((i) => i.id), [moved.id]);
|
|
816
|
+
await app.close();
|
|
817
|
+
});
|
|
738
818
|
test("NOT-217: PATCH reviewer-only edit records before/after reviewers without touching the developer", async () => {
|
|
739
819
|
const app = await buildApp();
|
|
740
820
|
const created = (await app.inject({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "AGENT_DEALER_SKIP_GITHUB_HEALTH=1 AGENT_DEALER_SKIP_AGENT_HEALTH=1 npm run build -w @agent-dealer/shared && AGENT_DEALER_SKIP_GITHUB_HEALTH=1 AGENT_DEALER_SKIP_AGENT_HEALTH=1 node ../../scripts/run-workspace-tests.mjs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@agent-dealer/shared": "1.3.
|
|
26
|
+
"@agent-dealer/shared": "1.3.2",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@fastify/static": "^8.2.0",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|