agent-dealer 1.3.4 → 1.3.5

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.
@@ -358,13 +358,15 @@ function recordVisualEvidence(issueId, opts) {
358
358
  baseSha: "base1",
359
359
  uiAffecting: true,
360
360
  state: opts.state,
361
- runId: opts.state === "missing" ? null : 42,
361
+ runId: opts.state === "available" || opts.state === "failed" ? 42 : null,
362
362
  artifactNames: opts.state === "available" ? ["ui-screenshots", "ui-baseline", "ui-diff"] : [],
363
363
  reason: opts.state === "available"
364
364
  ? null
365
365
  : opts.state === "failed"
366
366
  ? "Visual run 42 for this head concluded failure — its captures are unusable"
367
- : "no Visual workflow run found for head abc123",
367
+ : opts.state === "not_applicable"
368
+ ? "repository has no Visual workflow (workflow visual.yml not found on the default branch) — nothing to capture"
369
+ : "no Visual workflow run found for head abc123",
368
370
  },
369
371
  author: "system",
370
372
  });
@@ -420,6 +422,22 @@ test("NOT-384: available visual evidence on a UI head merges normally", async ()
420
422
  assert.deepEqual(calls, [{ cwd: managedCwd, number: 42 }]);
421
423
  assert.equal(getIssue(issueId).status, "done");
422
424
  });
425
+ test("NOT-538: not_applicable visual evidence (no Visual workflow) on a UI head merges normally", async () => {
426
+ const calls = [];
427
+ setMergePrForTests(async (opts) => {
428
+ calls.push(opts);
429
+ return { ok: true };
430
+ });
431
+ const issueId = newIssue("It works");
432
+ startWorkflow(issueId);
433
+ recordVisualEvidence(issueId, { headSha: "abc123", state: "not_applicable" });
434
+ const result = await approve(issueId);
435
+ assert.equal(result.applied, true);
436
+ if (result.applied)
437
+ assert.equal(result.issueStatus, "done");
438
+ assert.deepEqual(calls, [{ cwd: managedCwd, number: 42 }]);
439
+ assert.equal(getIssue(issueId).status, "done");
440
+ });
423
441
  test("NOT-384: [operator] criteria plus a visual hold raise one action naming both", async () => {
424
442
  let merges = 0;
425
443
  setMergePrForTests(async () => {
@@ -22,8 +22,10 @@
22
22
  // Dealer's issue-artifact storage, stage a read-only copy inside the
23
23
  // reviewer's worktree (outside tracked paths, ignored by `isWorktreeClean`),
24
24
  // and record one `visual_evidence` artifact row. It never throws: every
25
- // failure mode resolves to `missing` with a reason, and a missing or
26
- // failed capture never counts as a visual pass.
25
+ // failure mode resolves to `missing` with a reason — except a lookup 404
26
+ // proving the repository has no `visual.yml`, which resolves to
27
+ // `not_applicable` (nothing to capture) — and a missing or failed capture
28
+ // never counts as a visual pass.
27
29
  // 4. `readVisualEvidenceHold` — the merge-gate reader (auto-merge.ts): a
28
30
  // UI-affecting head whose recorded evidence is `missing` or `failed` holds
29
31
  // for `operator_verification` instead of merging or queuing a repair round.
@@ -334,6 +336,24 @@ function truncateReason(text, maxChars = 300) {
334
336
  const oneLine = text.replace(/\s+/g, " ").trim();
335
337
  return oneLine.length > maxChars ? `${oneLine.slice(0, maxChars).trimEnd()}…` : oneLine;
336
338
  }
339
+ /**
340
+ * NOT-538: true when a Visual run lookup failure means the repository's
341
+ * default branch has no `visual.yml` (`gh run list --workflow visual.yml`
342
+ * answers HTTP 404: "workflow visual.yml not found on the default branch"),
343
+ * rather than a transient or auth failure. Deliberately narrow — all three
344
+ * markers must be present — so auth, network, 5xx, and rate-limit failures
345
+ * keep failing closed to `missing`.
346
+ */
347
+ export function isVisualWorkflowNotFoundError(message) {
348
+ const text = message.toLowerCase();
349
+ return text.includes("404") && text.includes("visual.yml") && text.includes("not found");
350
+ }
351
+ /**
352
+ * NOT-538: the recorded reason when the lookup proves the repository has no
353
+ * Visual workflow. The head still matches the UI paths (`uiAffecting: true`),
354
+ * but there is nothing to capture, so the merge gate must not hold.
355
+ */
356
+ export const VISUAL_WORKFLOW_ABSENT_REASON = "repository has no Visual workflow (workflow visual.yml not found on the default branch) — nothing to capture";
337
357
  function noEvidence(uiAffecting, base, reason) {
338
358
  return {
339
359
  ...base,
@@ -357,7 +377,9 @@ function noEvidence(uiAffecting, base, reason) {
357
377
  *
358
378
  * Never throws and never returns usable evidence it does not have: unknown
359
379
  * changed files fail closed to UI-affecting + `missing`, and every lookup,
360
- * download, stage, or record failure degrades to `missing` with a reason.
380
+ * download, stage, or record failure degrades to `missing` with a reason —
381
+ * except a lookup 404 proving the repository has no Visual workflow, which
382
+ * records `not_applicable` instead (NOT-538).
361
383
  */
362
384
  export async function resolveReviewerVisualEvidence(opts) {
363
385
  const base = { headSha: opts.headSha, baseSha: opts.baseSha };
@@ -411,7 +433,25 @@ export async function resolveReviewerVisualEvidence(opts) {
411
433
  run = await fetcher.findRunForHead({ cwd: opts.worktreePath, headSha: opts.headSha });
412
434
  }
413
435
  catch (err) {
414
- const resolved = noEvidence(true, base, `Visual run lookup failed: ${truncateReason(err instanceof Error ? err.message : String(err))}`);
436
+ const message = err instanceof Error ? err.message : String(err);
437
+ if (isVisualWorkflowNotFoundError(message)) {
438
+ const resolved = {
439
+ ...base,
440
+ uiAffecting: true,
441
+ state: "not_applicable",
442
+ runId: null,
443
+ artifactNames: [],
444
+ stagedDir: null,
445
+ summaryChars: null,
446
+ reason: VISUAL_WORKFLOW_ABSENT_REASON,
447
+ staged: null,
448
+ summary: null,
449
+ files: null,
450
+ };
451
+ record(resolved, null);
452
+ return resolved;
453
+ }
454
+ const resolved = noEvidence(true, base, `Visual run lookup failed: ${truncateReason(message)}`);
415
455
  record(resolved, null);
416
456
  return resolved;
417
457
  }
@@ -17,7 +17,7 @@ const { createIssue } = await import("../repository/issues.js");
17
17
  const { createIssueArtifact } = await import("../repository/artifacts.js");
18
18
  const { listArtifactsForIssueByKind } = await import("../repository/artifacts-for-issue.js");
19
19
  const { isWorktreeClean, REVIEWER_VISUAL_STAGING_DIR_NAME } = await import("../adapters/git-worktree.js");
20
- const { VISUAL_WORKFLOW_PATHS, VISUAL_ARTIFACT_NAMES, VISUAL_EVIDENCE_ARTIFACT_KIND, isUiAffectingChange, classifyVisualEvidence, githubVisualEvidenceFetcher, resolveReviewerVisualEvidence, stageVisualArtifacts, cleanupStagedVisualArtifacts, readVisualDiffSummary, reviewerVisualStagingDir, toReviewerVisualEvidenceInput, readVisualEvidenceHold, } = await import("./reviewer-visual-evidence.js");
20
+ const { VISUAL_WORKFLOW_PATHS, VISUAL_ARTIFACT_NAMES, VISUAL_EVIDENCE_ARTIFACT_KIND, VISUAL_WORKFLOW_ABSENT_REASON, isUiAffectingChange, classifyVisualEvidence, githubVisualEvidenceFetcher, resolveReviewerVisualEvidence, stageVisualArtifacts, cleanupStagedVisualArtifacts, readVisualDiffSummary, reviewerVisualStagingDir, toReviewerVisualEvidenceInput, readVisualEvidenceHold, } = await import("./reviewer-visual-evidence.js");
21
21
  before(() => migrate());
22
22
  beforeEach(() => {
23
23
  getDb().exec(`
@@ -285,6 +285,8 @@ function fixtureFetcher(opts) {
285
285
  return {
286
286
  calls,
287
287
  async findRunForHead() {
288
+ if (opts.failLookupError !== undefined)
289
+ throw new Error(opts.failLookupError);
288
290
  if (opts.failLookup)
289
291
  throw new Error("gh exploded");
290
292
  return opts.run === undefined
@@ -494,3 +496,129 @@ test("NOT-384: gate reader holds only UI heads with missing/failed evidence at t
494
496
  });
495
497
  assert.equal(readVisualEvidenceHold(nonUiIssue, HEAD), null);
496
498
  });
499
+ // NOT-538: a repository whose default branch has no visual.yml has nothing to
500
+ // capture — the lookup's "workflow not found" 404 resolves to not_applicable
501
+ // (the head still matches the UI paths) instead of holding the PR.
502
+ test("NOT-538: workflow-not-found 404 records not_applicable, never missing", async () => {
503
+ const issueId = makeIssue();
504
+ const worktree = initRepo();
505
+ try {
506
+ const resolved = await resolveReviewerVisualEvidence({
507
+ issueId,
508
+ worktreePath: worktree,
509
+ baseSha: "c".repeat(40),
510
+ headSha: HEAD,
511
+ changedFiles: ["packages/shared/src/types.ts"],
512
+ github: {},
513
+ fetcher: fixtureFetcher({
514
+ failLookupError: "gh run list failed: HTTP 404: workflow visual.yml not found on the default branch",
515
+ }),
516
+ });
517
+ assert.equal(resolved.state, "not_applicable");
518
+ assert.equal(resolved.uiAffecting, true);
519
+ assert.equal(resolved.runId, null);
520
+ assert.equal(resolved.staged, null);
521
+ assert.equal(resolved.reason, VISUAL_WORKFLOW_ABSENT_REASON);
522
+ assert.match(resolved.reason, /repository has no Visual workflow/);
523
+ assert.equal(toReviewerVisualEvidenceInput(resolved), undefined);
524
+ const rows = listArtifactsForIssueByKind(issueId, VISUAL_EVIDENCE_ARTIFACT_KIND);
525
+ assert.equal(rows.length, 1);
526
+ const content = JSON.parse(rows[0].contentJson);
527
+ assert.equal(content.state, "not_applicable");
528
+ assert.equal(content.uiAffecting, true);
529
+ // The recorded workflow-absent record releases the merge gate.
530
+ assert.equal(readVisualEvidenceHold(issueId, HEAD), null);
531
+ }
532
+ finally {
533
+ fs.rmSync(worktree, { recursive: true, force: true });
534
+ }
535
+ });
536
+ // NOT-538: every other lookup failure still fails closed to missing with the
537
+ // existing reason — only the workflow-not-found 404 skips the gate.
538
+ test("NOT-538: auth, network, 5xx, and rate-limit lookup failures still record missing", async () => {
539
+ const worktree = initRepo();
540
+ try {
541
+ const failures = {
542
+ auth: "gh run list failed: HTTP 401: Bad credentials",
543
+ network: "gh run list failed: Could not resolve host: api.github.com",
544
+ server: "gh run list failed: HTTP 502: Bad Gateway",
545
+ rateLimit: "gh run list failed: HTTP 403: API rate limit exceeded for user ID 123",
546
+ };
547
+ for (const [kind, message] of Object.entries(failures)) {
548
+ const resolved = await resolveReviewerVisualEvidence({
549
+ issueId: makeIssue(),
550
+ worktreePath: worktree,
551
+ baseSha: "c".repeat(40),
552
+ headSha: HEAD,
553
+ changedFiles: ["packages/shared/src/types.ts"],
554
+ github: {},
555
+ fetcher: fixtureFetcher({ failLookupError: message }),
556
+ });
557
+ assert.equal(resolved.state, "missing", `${kind} failure must stay missing`);
558
+ assert.equal(resolved.uiAffecting, true);
559
+ assert.match(resolved.reason, /^Visual run lookup failed: /, `${kind} failure keeps the lookup-failed reason`);
560
+ }
561
+ // Near-miss 404s that do not name the missing workflow also stay missing.
562
+ for (const message of [
563
+ "gh run list failed: HTTP 404: Not Found",
564
+ "gh run list failed: HTTP 404: repository not found",
565
+ ]) {
566
+ const resolved = await resolveReviewerVisualEvidence({
567
+ issueId: makeIssue(),
568
+ worktreePath: worktree,
569
+ baseSha: "c".repeat(40),
570
+ headSha: HEAD,
571
+ changedFiles: ["packages/shared/src/types.ts"],
572
+ github: {},
573
+ fetcher: fixtureFetcher({ failLookupError: message }),
574
+ });
575
+ assert.equal(resolved.state, "missing", `${message} must stay missing`);
576
+ }
577
+ }
578
+ finally {
579
+ fs.rmSync(worktree, { recursive: true, force: true });
580
+ }
581
+ });
582
+ // NOT-538: the gate reader releases a UI-affecting head whose record is
583
+ // not_applicable because the repository has no Visual workflow, and still
584
+ // holds missing and failed.
585
+ test("NOT-538: gate reader releases workflow-absent not_applicable, holds missing and failed", () => {
586
+ const absentIssue = makeIssue();
587
+ createIssueArtifact({
588
+ issueId: absentIssue,
589
+ kind: VISUAL_EVIDENCE_ARTIFACT_KIND,
590
+ content: {
591
+ headSha: HEAD,
592
+ uiAffecting: true,
593
+ state: "not_applicable",
594
+ reason: VISUAL_WORKFLOW_ABSENT_REASON,
595
+ runId: null,
596
+ artifactNames: [],
597
+ },
598
+ author: "system",
599
+ });
600
+ assert.equal(readVisualEvidenceHold(absentIssue, HEAD), null);
601
+ const missingIssue = makeIssue();
602
+ createIssueArtifact({
603
+ issueId: missingIssue,
604
+ kind: VISUAL_EVIDENCE_ARTIFACT_KIND,
605
+ content: {
606
+ headSha: HEAD,
607
+ uiAffecting: true,
608
+ state: "missing",
609
+ reason: "Visual run lookup failed: HTTP 502: Bad Gateway",
610
+ runId: null,
611
+ artifactNames: [],
612
+ },
613
+ author: "system",
614
+ });
615
+ assert.equal(readVisualEvidenceHold(missingIssue, HEAD).state, "missing");
616
+ const failedIssue = makeIssue();
617
+ createIssueArtifact({
618
+ issueId: failedIssue,
619
+ kind: VISUAL_EVIDENCE_ARTIFACT_KIND,
620
+ content: { headSha: HEAD, uiAffecting: true, state: "failed", reason: "boom", runId: 9, artifactNames: [] },
621
+ author: "system",
622
+ });
623
+ assert.equal(readVisualEvidenceHold(failedIssue, HEAD).state, "failed");
624
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/server",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
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.4",
26
+ "@agent-dealer/shared": "1.3.5",
27
27
  "@fastify/cors": "^11.0.1",
28
28
  "@fastify/static": "^8.2.0",
29
29
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/shared",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dealer",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Human control plane for agent execution — queue, plan approval, audit",
5
5
  "license": "MIT",
6
6
  "type": "module",