Weekly tech debt audit: dispatch - 2026-06-17 #408

Description

@itsmiso-ai

Weekly tech debt audit: dispatch - 2026-06-17

Summary / Overall Risk Level

Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

Top Findings

P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

Evidence:

  • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
  • Route summary: auth=no verbs=POST(.
  • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
  • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
  • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

Evidence:

  • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
  • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
  • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

Evidence:

  • cat .npmrc shows omit= on a single line.
  • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
    npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
    npm warn invalid config Must be one or more of: dev, optional, peer
    
  • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
  • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

P1 — AGENTS.md does not document the four new agent-facing endpoints

The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

  • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
  • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
  • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
  • GET /api/agents/[agentName]/active-work (resume context)
  • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

Evidence:

  • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
  • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
  • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

Evidence:

  • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
  • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
  • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

Evidence:

  • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
  • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
  • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

P2 — lane-config.ts is added but not yet consumed by the rest of the system

src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

Evidence:

P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

Evidence:

  • find src/app/api -name 'route.test.ts' | wc -l → 31
  • find src/app/api -name 'route.ts' | wc -l → 48
  • Routes without co-located tests (selected):
    • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
    • /api/repos/route.ts (unauthenticated GET)
    • /api/automation/events/route.ts (unauthenticated GET)
    • /api/automation/workflows/route.ts (unauthenticated GET)
    • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
    • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
    • /api/pr-fix-queue/queued/route.ts (auth)
    • /api/pr-fix-queue/enqueue/route.ts (auth)
    • /api/pr-followup/sync/route.ts (auth)
    • /api/pr-followup/webhook/route.ts (auth, has signature verification)
    • /api/issues/reconcile/route.ts (auth)
    • /api/issues/untriaged/route.ts (unauthenticated GET)
    • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
    • /api/agent-runs/route.ts (auth)
    • /api/health/route.ts (unauthenticated)
    • /api/auth/logout/route.ts (intentionally no auth)
    • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

P2 — Repo label drift: typos and deprecated labels still on GitHub

labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

Evidence:

  • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
  • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

Evidence:

  • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
  • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

P3 — next-task test file is missing auth-coverage and a contract test for idle mode

The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

  • the idle task shape returned when neither queue nor PR-fix has work
  • the mode=groom groom-task-shape
  • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

Evidence:

  • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
  • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

Evidence:

  • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
  • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

Evidence:

  • src/mcp/server.ts:40-50 shows the agentName requirement handling.
  • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
  • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

Recommended Issue Breakdown

  1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
  2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
  3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
  4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
  5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
  6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
  7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
  8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
  9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
  10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
  11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
  12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
  13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
  14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

Not Worth Doing Yet

  • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
  • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
  • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
  • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
  • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
  • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
  • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
  • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

Audit Notes

Safe read-only checks performed:

  • Refreshed local main (already at 4c47335).
  • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
  • npm run test → 1308/1308 pass.
  • npm run typecheck → clean.
  • npm run lint → clean.
  • npm audit --omit=dev --json → 0 vulnerabilities.
  • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
  • Cross-checked gh pr list for the merged-in-this-window surface.
  • gh label list vs .github/labels.yaml to find unmanaged label drift.
  • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

Not done (would require write access or live state):

  • Did not exercise the live Dispatch instance.
  • Did not time the new next-task and queue routes against realistic data volumes.
  • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

Local repo state after audit: Clean. No files modified.

Decomposed into

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Weekly tech debt audit: dispatch - 2026-06-17 #408

      Description

      @itsmiso-ai

      Weekly tech debt audit: dispatch - 2026-06-17

      Summary / Overall Risk Level

      Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

      However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

      No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

      Top Findings

      P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

      A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

      Evidence:

      • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
      • Route summary: auth=no verbs=POST(.
      • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
      • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
      • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

      P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

      The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

      Evidence:

      • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
      • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
      • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

      P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

      The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

      Evidence:

      • cat .npmrc shows omit= on a single line.
      • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
        npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
        npm warn invalid config Must be one or more of: dev, optional, peer
        
      • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
      • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

      P1 — AGENTS.md does not document the four new agent-facing endpoints

      The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

      • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
      • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
      • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
      • GET /api/agents/[agentName]/active-work (resume context)
      • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

      This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

      Evidence:

      • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
      • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
      • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

      P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

      Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

      Evidence:

      • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
      • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
      • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

      P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

      The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

      Evidence:

      • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
      • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
      • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

      P2 — lane-config.ts is added but not yet consumed by the rest of the system

      src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

      Evidence:

      P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

      Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

      Evidence:

      • find src/app/api -name 'route.test.ts' | wc -l → 31
      • find src/app/api -name 'route.ts' | wc -l → 48
      • Routes without co-located tests (selected):
        • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
        • /api/repos/route.ts (unauthenticated GET)
        • /api/automation/events/route.ts (unauthenticated GET)
        • /api/automation/workflows/route.ts (unauthenticated GET)
        • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
        • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
        • /api/pr-fix-queue/queued/route.ts (auth)
        • /api/pr-fix-queue/enqueue/route.ts (auth)
        • /api/pr-followup/sync/route.ts (auth)
        • /api/pr-followup/webhook/route.ts (auth, has signature verification)
        • /api/issues/reconcile/route.ts (auth)
        • /api/issues/untriaged/route.ts (unauthenticated GET)
        • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
        • /api/agent-runs/route.ts (auth)
        • /api/health/route.ts (unauthenticated)
        • /api/auth/logout/route.ts (intentionally no auth)
        • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

      P2 — Repo label drift: typos and deprecated labels still on GitHub

      labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

      Evidence:

      • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
      • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

      P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

      The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

      Evidence:

      • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
      • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

      P3 — next-task test file is missing auth-coverage and a contract test for idle mode

      The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

      • the idle task shape returned when neither queue nor PR-fix has work
      • the mode=groom groom-task-shape
      • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

      The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

      Evidence:

      • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
      • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

      P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

      src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

      Evidence:

      • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
      • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

      P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

      The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

      Evidence:

      • src/mcp/server.ts:40-50 shows the agentName requirement handling.
      • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
      • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

      Recommended Issue Breakdown

      1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
      2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
      3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
      4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
      5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
      6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
      7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
      8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
      9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
      10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
      11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
      12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
      13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
      14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

      Not Worth Doing Yet

      • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
      • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
      • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
      • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
      • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
      • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
      • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
      • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

      Audit Notes

      Safe read-only checks performed:

      • Refreshed local main (already at 4c47335).
      • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
      • npm run test → 1308/1308 pass.
      • npm run typecheck → clean.
      • npm run lint → clean.
      • npm audit --omit=dev --json → 0 vulnerabilities.
      • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
      • Cross-checked gh pr list for the merged-in-this-window surface.
      • gh label list vs .github/labels.yaml to find unmanaged label drift.
      • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

      Not done (would require write access or live state):

      • Did not exercise the live Dispatch instance.
      • Did not time the new next-task and queue routes against realistic data volumes.
      • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

      Local repo state after audit: Clean. No files modified.

      Decomposed into

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Weekly tech debt audit: dispatch - 2026-06-17 #408

          Description

          @itsmiso-ai

          Weekly tech debt audit: dispatch - 2026-06-17

          Summary / Overall Risk Level

          Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

          However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

          No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

          Top Findings

          P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

          A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

          Evidence:

          • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
          • Route summary: auth=no verbs=POST(.
          • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
          • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
          • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

          P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

          The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

          Evidence:

          • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
          • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
          • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

          P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

          The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

          Evidence:

          • cat .npmrc shows omit= on a single line.
          • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
            npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
            npm warn invalid config Must be one or more of: dev, optional, peer
            
          • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
          • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

          P1 — AGENTS.md does not document the four new agent-facing endpoints

          The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

          • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
          • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
          • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
          • GET /api/agents/[agentName]/active-work (resume context)
          • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

          This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

          Evidence:

          • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
          • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
          • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

          P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

          Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

          Evidence:

          • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
          • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
          • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

          P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

          The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

          Evidence:

          • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
          • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
          • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

          P2 — lane-config.ts is added but not yet consumed by the rest of the system

          src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

          Evidence:

          P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

          Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

          Evidence:

          • find src/app/api -name 'route.test.ts' | wc -l → 31
          • find src/app/api -name 'route.ts' | wc -l → 48
          • Routes without co-located tests (selected):
            • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
            • /api/repos/route.ts (unauthenticated GET)
            • /api/automation/events/route.ts (unauthenticated GET)
            • /api/automation/workflows/route.ts (unauthenticated GET)
            • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
            • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
            • /api/pr-fix-queue/queued/route.ts (auth)
            • /api/pr-fix-queue/enqueue/route.ts (auth)
            • /api/pr-followup/sync/route.ts (auth)
            • /api/pr-followup/webhook/route.ts (auth, has signature verification)
            • /api/issues/reconcile/route.ts (auth)
            • /api/issues/untriaged/route.ts (unauthenticated GET)
            • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
            • /api/agent-runs/route.ts (auth)
            • /api/health/route.ts (unauthenticated)
            • /api/auth/logout/route.ts (intentionally no auth)
            • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

          P2 — Repo label drift: typos and deprecated labels still on GitHub

          labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

          Evidence:

          • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
          • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

          P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

          The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

          Evidence:

          • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
          • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

          P3 — next-task test file is missing auth-coverage and a contract test for idle mode

          The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

          • the idle task shape returned when neither queue nor PR-fix has work
          • the mode=groom groom-task-shape
          • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

          The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

          Evidence:

          • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
          • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

          P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

          src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

          Evidence:

          • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
          • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

          P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

          The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

          Evidence:

          • src/mcp/server.ts:40-50 shows the agentName requirement handling.
          • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
          • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

          Recommended Issue Breakdown

          1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
          2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
          3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
          4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
          5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
          6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
          7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
          8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
          9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
          10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
          11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
          12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
          13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
          14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

          Not Worth Doing Yet

          • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
          • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
          • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
          • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
          • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
          • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
          • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
          • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

          Audit Notes

          Safe read-only checks performed:

          • Refreshed local main (already at 4c47335).
          • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
          • npm run test → 1308/1308 pass.
          • npm run typecheck → clean.
          • npm run lint → clean.
          • npm audit --omit=dev --json → 0 vulnerabilities.
          • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
          • Cross-checked gh pr list for the merged-in-this-window surface.
          • gh label list vs .github/labels.yaml to find unmanaged label drift.
          • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

          Not done (would require write access or live state):

          • Did not exercise the live Dispatch instance.
          • Did not time the new next-task and queue routes against realistic data volumes.
          • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

          Local repo state after audit: Clean. No files modified.

          Decomposed into

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Weekly tech debt audit: dispatch - 2026-06-17 #408

              Description

              @itsmiso-ai

              Weekly tech debt audit: dispatch - 2026-06-17

              Summary / Overall Risk Level

              Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

              However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

              No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

              Top Findings

              P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

              A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

              Evidence:

              • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
              • Route summary: auth=no verbs=POST(.
              • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
              • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
              • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

              P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

              The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

              Evidence:

              • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
              • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
              • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

              P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

              The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

              Evidence:

              • cat .npmrc shows omit= on a single line.
              • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
                npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
                npm warn invalid config Must be one or more of: dev, optional, peer
                
              • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
              • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

              P1 — AGENTS.md does not document the four new agent-facing endpoints

              The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

              • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
              • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
              • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
              • GET /api/agents/[agentName]/active-work (resume context)
              • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

              This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

              Evidence:

              • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
              • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
              • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

              P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

              Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

              Evidence:

              • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
              • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
              • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

              P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

              The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

              Evidence:

              • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
              • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
              • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

              P2 — lane-config.ts is added but not yet consumed by the rest of the system

              src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

              Evidence:

              P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

              Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

              Evidence:

              • find src/app/api -name 'route.test.ts' | wc -l → 31
              • find src/app/api -name 'route.ts' | wc -l → 48
              • Routes without co-located tests (selected):
                • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
                • /api/repos/route.ts (unauthenticated GET)
                • /api/automation/events/route.ts (unauthenticated GET)
                • /api/automation/workflows/route.ts (unauthenticated GET)
                • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
                • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
                • /api/pr-fix-queue/queued/route.ts (auth)
                • /api/pr-fix-queue/enqueue/route.ts (auth)
                • /api/pr-followup/sync/route.ts (auth)
                • /api/pr-followup/webhook/route.ts (auth, has signature verification)
                • /api/issues/reconcile/route.ts (auth)
                • /api/issues/untriaged/route.ts (unauthenticated GET)
                • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
                • /api/agent-runs/route.ts (auth)
                • /api/health/route.ts (unauthenticated)
                • /api/auth/logout/route.ts (intentionally no auth)
                • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

              P2 — Repo label drift: typos and deprecated labels still on GitHub

              labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

              Evidence:

              • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
              • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

              P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

              The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

              Evidence:

              • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
              • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

              P3 — next-task test file is missing auth-coverage and a contract test for idle mode

              The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

              • the idle task shape returned when neither queue nor PR-fix has work
              • the mode=groom groom-task-shape
              • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

              The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

              Evidence:

              • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
              • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

              P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

              src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

              Evidence:

              • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
              • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

              P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

              The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

              Evidence:

              • src/mcp/server.ts:40-50 shows the agentName requirement handling.
              • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
              • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

              Recommended Issue Breakdown

              1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
              2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
              3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
              4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
              5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
              6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
              7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
              8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
              9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
              10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
              11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
              12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
              13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
              14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

              Not Worth Doing Yet

              • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
              • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
              • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
              • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
              • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
              • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
              • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
              • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

              Audit Notes

              Safe read-only checks performed:

              • Refreshed local main (already at 4c47335).
              • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
              • npm run test → 1308/1308 pass.
              • npm run typecheck → clean.
              • npm run lint → clean.
              • npm audit --omit=dev --json → 0 vulnerabilities.
              • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
              • Cross-checked gh pr list for the merged-in-this-window surface.
              • gh label list vs .github/labels.yaml to find unmanaged label drift.
              • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

              Not done (would require write access or live state):

              • Did not exercise the live Dispatch instance.
              • Did not time the new next-task and queue routes against realistic data volumes.
              • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

              Local repo state after audit: Clean. No files modified.

              Decomposed into

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Weekly tech debt audit: dispatch - 2026-06-17 #408

                  Description

                  @itsmiso-ai

                  Weekly tech debt audit: dispatch - 2026-06-17

                  Summary / Overall Risk Level

                  Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

                  However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

                  No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

                  Top Findings

                  P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

                  A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

                  Evidence:

                  • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
                  • Route summary: auth=no verbs=POST(.
                  • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
                  • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
                  • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

                  P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

                  The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

                  Evidence:

                  • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
                  • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
                  • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

                  P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

                  The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

                  Evidence:

                  • cat .npmrc shows omit= on a single line.
                  • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
                    npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
                    npm warn invalid config Must be one or more of: dev, optional, peer
                    
                  • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
                  • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

                  P1 — AGENTS.md does not document the four new agent-facing endpoints

                  The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

                  • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
                  • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
                  • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
                  • GET /api/agents/[agentName]/active-work (resume context)
                  • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

                  This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

                  Evidence:

                  • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
                  • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
                  • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

                  P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

                  Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

                  Evidence:

                  • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
                  • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
                  • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

                  P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

                  The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

                  Evidence:

                  • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
                  • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
                  • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

                  P2 — lane-config.ts is added but not yet consumed by the rest of the system

                  src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

                  Evidence:

                  P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

                  Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

                  Evidence:

                  • find src/app/api -name 'route.test.ts' | wc -l → 31
                  • find src/app/api -name 'route.ts' | wc -l → 48
                  • Routes without co-located tests (selected):
                    • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
                    • /api/repos/route.ts (unauthenticated GET)
                    • /api/automation/events/route.ts (unauthenticated GET)
                    • /api/automation/workflows/route.ts (unauthenticated GET)
                    • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
                    • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
                    • /api/pr-fix-queue/queued/route.ts (auth)
                    • /api/pr-fix-queue/enqueue/route.ts (auth)
                    • /api/pr-followup/sync/route.ts (auth)
                    • /api/pr-followup/webhook/route.ts (auth, has signature verification)
                    • /api/issues/reconcile/route.ts (auth)
                    • /api/issues/untriaged/route.ts (unauthenticated GET)
                    • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
                    • /api/agent-runs/route.ts (auth)
                    • /api/health/route.ts (unauthenticated)
                    • /api/auth/logout/route.ts (intentionally no auth)
                    • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

                  P2 — Repo label drift: typos and deprecated labels still on GitHub

                  labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

                  Evidence:

                  • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
                  • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

                  P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

                  The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

                  Evidence:

                  • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
                  • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

                  P3 — next-task test file is missing auth-coverage and a contract test for idle mode

                  The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

                  • the idle task shape returned when neither queue nor PR-fix has work
                  • the mode=groom groom-task-shape
                  • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

                  The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

                  Evidence:

                  • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
                  • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

                  P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

                  src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

                  Evidence:

                  • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
                  • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

                  P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

                  The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

                  Evidence:

                  • src/mcp/server.ts:40-50 shows the agentName requirement handling.
                  • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
                  • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

                  Recommended Issue Breakdown

                  1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
                  2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
                  3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
                  4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
                  5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
                  6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
                  7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
                  8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
                  9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
                  10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
                  11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
                  12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
                  13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
                  14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

                  Not Worth Doing Yet

                  • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
                  • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
                  • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
                  • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
                  • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
                  • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
                  • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
                  • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

                  Audit Notes

                  Safe read-only checks performed:

                  • Refreshed local main (already at 4c47335).
                  • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
                  • npm run test → 1308/1308 pass.
                  • npm run typecheck → clean.
                  • npm run lint → clean.
                  • npm audit --omit=dev --json → 0 vulnerabilities.
                  • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
                  • Cross-checked gh pr list for the merged-in-this-window surface.
                  • gh label list vs .github/labels.yaml to find unmanaged label drift.
                  • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

                  Not done (would require write access or live state):

                  • Did not exercise the live Dispatch instance.
                  • Did not time the new next-task and queue routes against realistic data volumes.
                  • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

                  Local repo state after audit: Clean. No files modified.

                  Decomposed into

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Weekly tech debt audit: dispatch - 2026-06-17 #408

                      Description

                      @itsmiso-ai

                      Weekly tech debt audit: dispatch - 2026-06-17

                      Summary / Overall Risk Level

                      Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

                      However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

                      No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

                      Top Findings

                      P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

                      A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

                      Evidence:

                      • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
                      • Route summary: auth=no verbs=POST(.
                      • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
                      • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
                      • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

                      P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

                      The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

                      Evidence:

                      • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
                      • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
                      • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

                      P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

                      The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

                      Evidence:

                      • cat .npmrc shows omit= on a single line.
                      • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
                        npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
                        npm warn invalid config Must be one or more of: dev, optional, peer
                        
                      • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
                      • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

                      P1 — AGENTS.md does not document the four new agent-facing endpoints

                      The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

                      • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
                      • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
                      • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
                      • GET /api/agents/[agentName]/active-work (resume context)
                      • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

                      This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

                      Evidence:

                      • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
                      • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
                      • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

                      P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

                      Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

                      Evidence:

                      • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
                      • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
                      • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

                      P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

                      The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

                      Evidence:

                      • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
                      • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
                      • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

                      P2 — lane-config.ts is added but not yet consumed by the rest of the system

                      src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

                      Evidence:

                      P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

                      Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

                      Evidence:

                      • find src/app/api -name 'route.test.ts' | wc -l → 31
                      • find src/app/api -name 'route.ts' | wc -l → 48
                      • Routes without co-located tests (selected):
                        • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
                        • /api/repos/route.ts (unauthenticated GET)
                        • /api/automation/events/route.ts (unauthenticated GET)
                        • /api/automation/workflows/route.ts (unauthenticated GET)
                        • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
                        • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
                        • /api/pr-fix-queue/queued/route.ts (auth)
                        • /api/pr-fix-queue/enqueue/route.ts (auth)
                        • /api/pr-followup/sync/route.ts (auth)
                        • /api/pr-followup/webhook/route.ts (auth, has signature verification)
                        • /api/issues/reconcile/route.ts (auth)
                        • /api/issues/untriaged/route.ts (unauthenticated GET)
                        • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
                        • /api/agent-runs/route.ts (auth)
                        • /api/health/route.ts (unauthenticated)
                        • /api/auth/logout/route.ts (intentionally no auth)
                        • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

                      P2 — Repo label drift: typos and deprecated labels still on GitHub

                      labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

                      Evidence:

                      • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
                      • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

                      P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

                      The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

                      Evidence:

                      • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
                      • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

                      P3 — next-task test file is missing auth-coverage and a contract test for idle mode

                      The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

                      • the idle task shape returned when neither queue nor PR-fix has work
                      • the mode=groom groom-task-shape
                      • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

                      The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

                      Evidence:

                      • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
                      • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

                      P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

                      src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

                      Evidence:

                      • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
                      • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

                      P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

                      The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

                      Evidence:

                      • src/mcp/server.ts:40-50 shows the agentName requirement handling.
                      • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
                      • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

                      Recommended Issue Breakdown

                      1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
                      2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
                      3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
                      4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
                      5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
                      6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
                      7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
                      8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
                      9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
                      10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
                      11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
                      12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
                      13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
                      14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

                      Not Worth Doing Yet

                      • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
                      • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
                      • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
                      • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
                      • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
                      • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
                      • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
                      • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

                      Audit Notes

                      Safe read-only checks performed:

                      • Refreshed local main (already at 4c47335).
                      • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
                      • npm run test → 1308/1308 pass.
                      • npm run typecheck → clean.
                      • npm run lint → clean.
                      • npm audit --omit=dev --json → 0 vulnerabilities.
                      • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
                      • Cross-checked gh pr list for the merged-in-this-window surface.
                      • gh label list vs .github/labels.yaml to find unmanaged label drift.
                      • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

                      Not done (would require write access or live state):

                      • Did not exercise the live Dispatch instance.
                      • Did not time the new next-task and queue routes against realistic data volumes.
                      • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

                      Local repo state after audit: Clean. No files modified.

                      Decomposed into

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Weekly tech debt audit: dispatch - 2026-06-17 #408

                          Description

                          @itsmiso-ai

                          Weekly tech debt audit: dispatch - 2026-06-17

                          Summary / Overall Risk Level

                          Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

                          However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

                          No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

                          Top Findings

                          P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

                          A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

                          Evidence:

                          • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
                          • Route summary: auth=no verbs=POST(.
                          • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
                          • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
                          • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

                          P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

                          The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

                          Evidence:

                          • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
                          • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
                          • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

                          P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

                          The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

                          Evidence:

                          • cat .npmrc shows omit= on a single line.
                          • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
                            npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
                            npm warn invalid config Must be one or more of: dev, optional, peer
                            
                          • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
                          • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

                          P1 — AGENTS.md does not document the four new agent-facing endpoints

                          The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

                          • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
                          • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
                          • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
                          • GET /api/agents/[agentName]/active-work (resume context)
                          • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

                          This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

                          Evidence:

                          • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
                          • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
                          • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

                          P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

                          Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

                          Evidence:

                          • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
                          • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
                          • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

                          P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

                          The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

                          Evidence:

                          • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
                          • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
                          • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

                          P2 — lane-config.ts is added but not yet consumed by the rest of the system

                          src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

                          Evidence:

                          P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

                          Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

                          Evidence:

                          • find src/app/api -name 'route.test.ts' | wc -l → 31
                          • find src/app/api -name 'route.ts' | wc -l → 48
                          • Routes without co-located tests (selected):
                            • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
                            • /api/repos/route.ts (unauthenticated GET)
                            • /api/automation/events/route.ts (unauthenticated GET)
                            • /api/automation/workflows/route.ts (unauthenticated GET)
                            • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
                            • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
                            • /api/pr-fix-queue/queued/route.ts (auth)
                            • /api/pr-fix-queue/enqueue/route.ts (auth)
                            • /api/pr-followup/sync/route.ts (auth)
                            • /api/pr-followup/webhook/route.ts (auth, has signature verification)
                            • /api/issues/reconcile/route.ts (auth)
                            • /api/issues/untriaged/route.ts (unauthenticated GET)
                            • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
                            • /api/agent-runs/route.ts (auth)
                            • /api/health/route.ts (unauthenticated)
                            • /api/auth/logout/route.ts (intentionally no auth)
                            • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

                          P2 — Repo label drift: typos and deprecated labels still on GitHub

                          labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

                          Evidence:

                          • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
                          • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

                          P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

                          The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

                          Evidence:

                          • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
                          • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

                          P3 — next-task test file is missing auth-coverage and a contract test for idle mode

                          The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

                          • the idle task shape returned when neither queue nor PR-fix has work
                          • the mode=groom groom-task-shape
                          • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

                          The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

                          Evidence:

                          • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
                          • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

                          P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

                          src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

                          Evidence:

                          • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
                          • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

                          P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

                          The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

                          Evidence:

                          • src/mcp/server.ts:40-50 shows the agentName requirement handling.
                          • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
                          • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

                          Recommended Issue Breakdown

                          1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
                          2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
                          3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
                          4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
                          5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
                          6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
                          7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
                          8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
                          9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
                          10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
                          11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
                          12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
                          13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
                          14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

                          Not Worth Doing Yet

                          • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
                          • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
                          • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
                          • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
                          • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
                          • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
                          • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
                          • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

                          Audit Notes

                          Safe read-only checks performed:

                          • Refreshed local main (already at 4c47335).
                          • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
                          • npm run test → 1308/1308 pass.
                          • npm run typecheck → clean.
                          • npm run lint → clean.
                          • npm audit --omit=dev --json → 0 vulnerabilities.
                          • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
                          • Cross-checked gh pr list for the merged-in-this-window surface.
                          • gh label list vs .github/labels.yaml to find unmanaged label drift.
                          • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

                          Not done (would require write access or live state):

                          • Did not exercise the live Dispatch instance.
                          • Did not time the new next-task and queue routes against realistic data volumes.
                          • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

                          Local repo state after audit: Clean. No files modified.

                          Decomposed into

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Weekly tech debt audit: dispatch - 2026-06-17 #408

                              Description

                              @itsmiso-ai

                              Weekly tech debt audit: dispatch - 2026-06-17

                              Summary / Overall Risk Level

                              Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

                              However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

                              No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

                              Top Findings

                              P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

                              A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

                              Evidence:

                              • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
                              • Route summary: auth=no verbs=POST(.
                              • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
                              • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
                              • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

                              P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

                              The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

                              Evidence:

                              • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
                              • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
                              • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

                              P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

                              The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

                              Evidence:

                              • cat .npmrc shows omit= on a single line.
                              • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
                                npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
                                npm warn invalid config Must be one or more of: dev, optional, peer
                                
                              • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
                              • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

                              P1 — AGENTS.md does not document the four new agent-facing endpoints

                              The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

                              • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
                              • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
                              • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
                              • GET /api/agents/[agentName]/active-work (resume context)
                              • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

                              This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

                              Evidence:

                              • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
                              • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
                              • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

                              P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

                              Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

                              Evidence:

                              • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
                              • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
                              • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

                              P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

                              The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

                              Evidence:

                              • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
                              • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
                              • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

                              P2 — lane-config.ts is added but not yet consumed by the rest of the system

                              src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

                              Evidence:

                              P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

                              Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

                              Evidence:

                              • find src/app/api -name 'route.test.ts' | wc -l → 31
                              • find src/app/api -name 'route.ts' | wc -l → 48
                              • Routes without co-located tests (selected):
                                • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
                                • /api/repos/route.ts (unauthenticated GET)
                                • /api/automation/events/route.ts (unauthenticated GET)
                                • /api/automation/workflows/route.ts (unauthenticated GET)
                                • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
                                • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
                                • /api/pr-fix-queue/queued/route.ts (auth)
                                • /api/pr-fix-queue/enqueue/route.ts (auth)
                                • /api/pr-followup/sync/route.ts (auth)
                                • /api/pr-followup/webhook/route.ts (auth, has signature verification)
                                • /api/issues/reconcile/route.ts (auth)
                                • /api/issues/untriaged/route.ts (unauthenticated GET)
                                • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
                                • /api/agent-runs/route.ts (auth)
                                • /api/health/route.ts (unauthenticated)
                                • /api/auth/logout/route.ts (intentionally no auth)
                                • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

                              P2 — Repo label drift: typos and deprecated labels still on GitHub

                              labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

                              Evidence:

                              • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
                              • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

                              P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

                              The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

                              Evidence:

                              • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
                              • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

                              P3 — next-task test file is missing auth-coverage and a contract test for idle mode

                              The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

                              • the idle task shape returned when neither queue nor PR-fix has work
                              • the mode=groom groom-task-shape
                              • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

                              The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

                              Evidence:

                              • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
                              • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

                              P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

                              src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

                              Evidence:

                              • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
                              • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

                              P3 — mc-client.tsresolveAgentName requires explicit agentName but the MCP server test environment may drift

                              The src/mcp/server.tsclaimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

                              Evidence:

                              • src/mcp/server.ts:40-50 shows the agentName requirement handling.
                              • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
                              • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

                              Recommended Issue Breakdown

                              1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
                              2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
                              3. P1 — Replace the .npmrcomit= line with include=dev (or delete the file) and verify CI is still clean.
                              4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
                              5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
                              6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
                              7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
                              8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
                              9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
                              10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
                              11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
                              12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
                              13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
                              14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

                              Not Worth Doing Yet

                              • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
                              • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
                              • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
                              • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
                              • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
                              • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
                              • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
                              • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

                              Audit Notes

                              Safe read-only checks performed:

                              • Refreshed local main (already at 4c47335).
                              • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
                              • npm run test → 1308/1308 pass.
                              • npm run typecheck → clean.
                              • npm run lint → clean.
                              • npm audit --omit=dev --json → 0 vulnerabilities.
                              • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
                              • Cross-checked gh pr list for the merged-in-this-window surface.
                              • gh label list vs .github/labels.yaml to find unmanaged label drift.
                              • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

                              Not done (would require write access or live state):

                              • Did not exercise the live Dispatch instance.
                              • Did not time the new next-task and queue routes against realistic data volumes.
                              • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

                              Local repo state after audit: Clean. No files modified.

                              Decomposed into

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions