Files
PaperClipAI/server/src/services/native-runtime/native-deliverable-feedback.ts
T
DottaandPaperclip cd12432947 fix(runner): recognize verified task document evidence (#18661)
## Thinking Path

> - Paperclip manages AI agents and keeps their results attached to
tasks.
> - Native tools return links to saved task documents.
> - Completion feedback asks agents to include those links.
> - The file-delivery validator treated every such relative link as a
workspace path.
> - A valid document and CSV result could therefore fail completion when
it cited the supplied document link.
> - This change derives document links from the same verified
publication projection for guidance and validation.
> - Requested files still need their own verified delivery evidence.

## Linked Issues or Issue Description

**What happened?**
The native document tool and completion feedback produce a link such as
`/EX/issues/EX-1#document-output`. When a task also requests a
downloadable CSV, citing that link causes completion to reject it as a
workspace-only file. This occurs even when the document is published and
the file has a valid current-run delivery receipt.

**Expected behavior**
Accept a server-derived document link only when it names the current
company/task and a currently attached revision with a matching native
publication receipt. Keep the separate requested-file requirement and
all foreign, stale, missing, and workspace-file guards.

**Steps to reproduce**
1. Save a task document through the native `write_document` tool.
2. Publish a CSV through `register_deliverable`.
3. Ask completion feedback for the final-response guidance.
4. Cite its returned document link alongside the file receipt in the
next completion report.
5. The old validator rejects the valid document link as a workspace-only
file.

**Paperclip version or commit**
Current source checkout. The regression exercises the real native
document producer and completion validator.

**Deployment mode**
Local source verification on macOS arm64 with Node 24.22.1, pinned pnpm
9.15.4, locked dependencies, temporary PostgreSQL, and a controlled
remote file-reader boundary. No live provider or hosted deployment is
included.

## What Changed

- Derive the canonical document href in the existing
company/task/current-revision publication query.
- Use that same verified href in completion guidance and reference
validation.
- Keep file registration and hash/size receipt requirements separate
from document evidence.
- Cover evidence, artifact, and criterion references; foreign
companies/tasks; bogus keys; local paths; and lost current-revision
publication proof.
- Exercise actual native document writes and CSV registration, including
attachment download query and fragment links.
- Update the execution contract and the document feature/access maps.

## Verification

- Red before the production edit: native completion generated a real
saved-document link, then rejected the same link alongside a valid
published file. The failure was the workspace-only-file guard.
- Green: 210 tests across native completion feedback, explicit output
validation, and the existing native file-handoff suite pass. These use
real PostgreSQL and native tool receipts; the remote file reader is
controlled.
- A saved document link alone still fails the explicit CSV requirement.
A document whose current revision lacks its publication receipt is
rejected. Foreign task/company links, missing keys, malformed links, and
workspace paths remain rejected.
- Full workspace typecheck, server typecheck, and production build pass.
- `node scripts/check-maps.mjs`, `node
scripts/check-module-boundaries.mjs`, and `git diff --check` pass. Map
checks establish structure only.
- Full `pnpm test:run` is running separately. There is no full-suite
verdict yet. CI and review are pending.
- Hosted acceptance and any recovery of existing tasks remain separate
steps. This PR does not change final-response recovery or scheduling.

## Risks

- An overly broad URL exception could admit foreign or unpublished
output. The validator accepts only exact hrefs from the authenticated
publication projection.
- Document evidence could hide a missing requested file. A separate
regression keeps that delivery gate effective.
- The shared projection adds current task/company joins. Existing
document feedback and publication tests cover those consumers.

## Model Used

OpenAI `gpt-6.1-sol` through Codex, with reasoning, code editing, shell
execution, and automated tests. The context-window size is not exposed
in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-02 15:02:00 -06:00

287 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { and, eq } from "drizzle-orm";
import { assets, companies, documents, documentRevisions, heartbeatRuns, issueDocuments, issueAttachments, issues, issueWorkProducts, mcpAttachmentUploads, dotRunnerAssignments, type Db } from "@paperclipai/db";
import type { PrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
function evidenceRefs(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.flatMap((entry) => {
if (typeof entry === "string") return [entry];
if (entry && typeof entry === "object" && typeof entry.ref === "string") return [entry.ref];
return [];
});
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown> : {};
}
async function hasCurrentPublicationReceipt(db: Db, companyId: string, receipts: unknown, attachment: {
id: string; originatingRunId: string | null; filename: string | null; byteSize: number; sha256: string;
}): Promise<boolean> {
for (const value of Object.values(record(receipts))) {
const receipt = record(value);
const input = record(receipt.input);
const result = record(receipt.result);
if ((result.disposition !== "applied" && result.disposition !== "duplicate") ||
!Array.isArray(result.entityRefs) || result.entityRefs[0] !== attachment.id) continue;
if (receipt.operationId === "publish_external_deliverable" && result.source === "authenticated_upload"
&& result.commandId === `deliverable-uploaded:${attachment.id}` && typeof input.uploadId === "string"
&& typeof input.workSessionId === "string" && attachment.originatingRunId && attachment.filename
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(input.uploadId)
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(input.workSessionId)
&& input.filename === attachment.filename
&& input.byteSize === attachment.byteSize && input.sha256 === attachment.sha256) {
const [upload] = await db.select({ id: mcpAttachmentUploads.id }).from(mcpAttachmentUploads)
.innerJoin(dotRunnerAssignments, and(eq(dotRunnerAssignments.id, mcpAttachmentUploads.workSessionId),
eq(dotRunnerAssignments.companyId, companyId)))
.where(and(eq(mcpAttachmentUploads.id, input.uploadId), eq(mcpAttachmentUploads.companyId, companyId),
eq(mcpAttachmentUploads.workSessionId, input.workSessionId), eq(mcpAttachmentUploads.attachmentId, attachment.id),
eq(dotRunnerAssignments.runId, attachment.originatingRunId), eq(mcpAttachmentUploads.originalFilename, attachment.filename),
eq(mcpAttachmentUploads.byteSize, attachment.byteSize), eq(mcpAttachmentUploads.sha256, attachment.sha256))).limit(1);
if (upload) return true;
}
if (receipt.operationId === "register_deliverable" &&
result.commandId === `deliverable-prepared:${attachment.id}` &&
typeof input.filename === "string" && input.filename.trim() === attachment.filename &&
input.byteSize === attachment.byteSize &&
typeof input.sha256 === "string" && input.sha256.trim().toLowerCase() === attachment.sha256.toLowerCase()) return true;
if (receipt.operationId !== "reuse_chat_attachment" ||
result.commandId !== `chat-attachment-reused:${attachment.id}`) continue;
const prepared = record(result.prepared);
const source = record(result.source);
if (prepared.attachmentId !== attachment.id ||
source.attachmentId !== input.attachmentId || source.commentId !== input.sourceCommentId ||
typeof prepared.sha256 !== "string" || prepared.sha256.toLowerCase() !== attachment.sha256.toLowerCase() ||
typeof source.sha256 !== "string" || source.sha256.toLowerCase() !== attachment.sha256.toLowerCase()) continue;
if ("filename" in prepared || "byteSize" in prepared) {
if (prepared.filename === attachment.filename && prepared.byteSize === attachment.byteSize) return true;
continue;
}
// Older committed reuse receipts contain the authenticated source and hash,
// but not its filename/size. Only an intact, matching source can supply those
// missing facts; this does not authorize a new reuse or bypass its tool gate.
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
if (typeof source.attachmentId !== "string" || !uuid.test(source.attachmentId) ||
typeof source.commentId !== "string" || !uuid.test(source.commentId)) continue;
const [original] = await db.select({ filename: assets.originalFilename, byteSize: assets.byteSize, sha256: assets.sha256 })
.from(issueAttachments).innerJoin(assets, and(eq(assets.id, issueAttachments.assetId), eq(assets.companyId, companyId)))
.where(and(eq(issueAttachments.id, source.attachmentId), eq(issueAttachments.companyId, companyId),
eq(issueAttachments.issueCommentId, source.commentId))).limit(1);
if (original?.filename === attachment.filename && original.byteSize === attachment.byteSize &&
original.sha256.toLowerCase() === attachment.sha256.toLowerCase()) return true;
}
return false;
}
/** Recognize explicit output requests, not incidental mentions of source files.
* The current server-bound objective is authoritative; summaries cannot invent
* an output requirement or erase a user's request for a file.
*/
export function explicitlyRequestsFileOutput(objective: string): boolean {
const sentences = objective.replaceAll("\\_", "_").split(/(?:[.!?](?:\s|$)|\n)/iu);
let precedingFileOutput = false;
return sentences.some((sentence, index) => {
const file = /\b(?:files?|attachments?|downloads?|pdf|spreadsheets?|workbooks?|slide decks?|powerpoints?|docx|xlsx|csv)\b|\b[^\s/]+\.(?:md|txt|json|pdf|docx?|xlsx?|csv|pptx?|png|jpe?g|svg|zip)\b/giu;
const outputs = sentence.split(/(?:[;,]|\bbut\b)/iu).flatMap(clause => {
const creates = [...clause.matchAll(/\b(?:create|make|write|save|export|attach|send|generate|produce|prepare|provide|give|return|build|publish)\b/giu)];
return creates.flatMap((create, createIndex) => {
const before = clause.slice(0, create.index);
if (/\b(?:do not|don['’]t|never|no need to)\s*$/iu.test(before)) return [];
const negation = /\b(?:do not|don['’]t|never|no need to)\b/iu.exec(before);
if (negation) {
const rest = before.slice(negation.index + negation[0].length);
if (!/\binstead\b/iu.test(rest) && /^(?:\s*|[\s\S]*\b(?:and|or|nor)\s*)$/iu.test(rest)) return [];
}
// Bind each object to its own verb. A denied write or "create no files"
// cannot suppress a separate requested report in this same clause.
const output = clause.slice(create.index + create[0].length, creates[createIndex + 1]?.index);
// An explicit attachment/export request can refer to the file by
// pronoun. It still requires publication when its name is omitted.
const referencesOutput = /^(?:attach|export|send|provide|give|return|publish)$/iu.test(create[0])
&& /^\s+(?:me\s+)?(?:it|them|this|that)\b/iu.test(output);
const referencedAttachment = referencesOutput && /^(?:attach|export)$/iu.test(create[0]);
const inlineDestination = /\b(?:inline|(?:in|within|inside|as|into)\s+(?:(?:a|an|the|my|your|our|final|plain|markdown|chat|fenced)\s+)*(?:chat|response|reply|message|comment|text|code block))\b/iu;
const inline = inlineDestination.test(output) || /\b(?:its|the) contents\b/iu.test(output);
const downloadable = referencedAttachment || (!/\b(?:no|without)\s+(?:downloadable|attached)/iu.test(output)
&& /\b(?:downloadable|attached)\s+(?:file|report|document|checklist|draft)\b/iu.test(output));
const inlineOnly = inlineDestination.test(output.split(/\s+(?:and|then)\s+/iu, 1)[0]) && !downloadable && !/^(?:attach|export|publish)$/iu.test(create[0])
&& !/\bas (?:an? )?attachment\b/iu.test(output);
const objects = [...output.matchAll(file)].filter(match => {
const prefix = output.slice(0, match.index);
const suffix = output.slice(match.index + match[0].length);
// "Create no files" is a prohibition, even though it contains a creation
// verb. Negate this object only; another explicit output can still count.
if (/\b(?:no|zero|without(?:\s+(?:any|a|an))?)\s+(?:(?:new|temporary|downloadable|attached|additional)\s+)*$/iu.test(prefix)) return false;
// "Write a summary of this PDF" names input, not a requested file.
// Explicit export destinations still count after such input references.
const json = /\.json$/iu.test(match[0]);
const inputReference = /\b(?:of|about|on|from|using|for|with)\b/iu;
const destinationMatch = (json
? /\b(as|into|to|named|called)\s+(?:(?:a|an|the|new|separate|markdown|word|excel)\s+)*["'`*]*(?:[^\s]*\/)?$/iu
: /\b(?:as|into|to)\s+(?:(?:a|an|the|new|separate|markdown|word|excel)\s+)*$/iu).exec(prefix);
const beforeDestination = prefix.slice(0, destinationMatch?.index ?? 0);
const destination = destinationMatch !== null && (!json || !/^(?:to|named|called)$/iu.test(destinationMatch[1])
|| !inputReference.test(beforeDestination));
if (json) {
const directObject = /^\s*(?:(?:a|an|the|my|your|our|their|his|her|its|this|that)\s+(?:(?:new|separate|downloadable|attached)\s+)*([\p{L}\p{N}_-]+\s+)?|(?:(?:new|separate|downloadable|attached)\s+)*)["'`*]*(?:[^\s]*\/)?$/iu.exec(prefix);
if (!destination && (!directObject || inputReference.test(directObject[1] ?? ""))) return false;
} else if (!destination && inputReference.test(prefix)) return false;
if (/^files?$/iu.test(match[0]) && /^\s+(?:tools?|permissions?|systems?|formats?|names?|paths?|types?|sizes?|descriptors?)\b/iu.test(suffix)) return false;
return true;
});
const publication = /\b(?:downloadable|attached|attach|export|send|provide|return|give|publish)\b/iu.test(create[0] + output);
const deniedAttempt = create[0].toLowerCase() === "write" && objects.length === 1
&& /\b(?:attempt|try)\s+(?:the\s+)?native\s*$/iu.test(before)
&& /\b(?:must be denied|denial is (?:the )?expected|(?:this|the) negative test)\b/iu.test(objective);
if (objects.length === 0 && !downloadable && /^\s+(?:no|zero|without)\b/iu.test(output)) return [];
return [{ namesFile: objects.length > 0, objects: objects.filter(match => !inlineOnly || !/\.json$/iu.test(match[0])).length, downloadable, publication, deniedAttempt,
referencesOutput, publicationReference: referencesOutput && !inline }];
});
});
// "This ..." qualifies a single requested file in the preceding sentence,
// even when a later clause checks it. Multiple files cannot share this
// exception and separate report requests still require publication.
const internal = outputs.reduce((count, output) => count + output.objects + Number(output.downloadable && output.objects === 0), 0) === 1
&& /^\s*This is (?:an? )?(?:personal memory|internal (?:assertion|verification) file)\b[^.!?]*\bnot a (?:task )?deliverable\b/iu.test(sentences[index + 1] ?? "");
return outputs.some(output => {
const publicationReference = output.publicationReference && precedingFileOutput;
if (!output.referencesOutput) precedingFileOutput = output.namesFile;
return (output.objects > 0 || output.downloadable || publicationReference)
&& (output.publication || (!internal && !output.deniedAttempt));
});
});
}
/** An explicitly requested document on the task must be published there. */
export function explicitlyRequestsTaskDocumentOutput(objective: string): boolean {
// Keep comma-separated conditions with their imperative. This is a narrow
// unconditional-output guard, not an interpreter of whether a condition held.
return objective.split(/(?:[.!?](?:\s|$)|\n|;)/iu).some(statement => {
// Check conditions before separating contrastive instructions: "create a
// document, but only if ..." must not become an unconditional requirement.
if (/\b(?:if|unless|when|once|otherwise|provided that|in case)\b/iu.test(statement)) return false;
return statement.split(/\bbut\b/iu).some(clause => {
if (/\boptionally\b/iu.test(clause)) return false;
const create = /\b(?:create|make|write|save|publish|prepare|provide|attach)\b/iu.exec(clause);
if (!create) return false;
const before = clause.slice(0, create.index);
if (/\b(?:do not|don['’]t|never|no need to|may|could|can)\b/iu.test(before)) return false;
if (/\b(?:explain|describe|discuss|review)\b/iu.test(before)) return false;
const output = clause.slice(create.index + create[0].length);
return [...output.matchAll(/\b(?:document|doc)\b/giu)].some(match => {
const prefix = output.slice(0, match.index);
if (/\b(?:of|about|from|using|for|with|without|no|zero)\b/iu.test(prefix)) return false;
return /^\s+(?:on|to|in|attached to)\s+(?:this|the|current)\s+(?:task|issue)\b/iu.test(output.slice(match.index + match[0].length));
});
});
});
}
/** Current attached revisions with server-owned publication proof. Joining the
* revision to its originating run preserves completed work across continuations
* without accepting stale, foreign-task, or provider-invented document refs.
*/
export async function publishedTaskDocuments(db: Db, binding: { companyId: string; issueId: string }) {
const saved = await db.select({ id: documents.id, revisionId: documents.latestRevisionId,
key: issueDocuments.key, resultJson: heartbeatRuns.resultJson,
issuePrefix: companies.issuePrefix, issueIdentifier: issues.identifier })
.from(issueDocuments)
.innerJoin(issues, and(eq(issues.id, issueDocuments.issueId), eq(issues.companyId, binding.companyId)))
.innerJoin(companies, eq(companies.id, issues.companyId))
.innerJoin(documents, and(eq(documents.id, issueDocuments.documentId), eq(documents.companyId, binding.companyId)))
.innerJoin(documentRevisions, and(eq(documentRevisions.id, documents.latestRevisionId),
eq(documentRevisions.documentId, documents.id), eq(documentRevisions.companyId, binding.companyId)))
.innerJoin(heartbeatRuns, and(eq(heartbeatRuns.id, documentRevisions.createdByRunId),
eq(heartbeatRuns.companyId, binding.companyId), eq(heartbeatRuns.nativeIssueId, binding.issueId),
eq(heartbeatRuns.runtimeMode, "native")))
.where(and(eq(issueDocuments.companyId, binding.companyId), eq(issueDocuments.issueId, binding.issueId)));
return saved.filter(document => Object.values(record(document.resultJson?.semanticToolReceipts)).some(value => {
const receipt = record(value), result = record(receipt.result), published = record(result.document);
return receipt.operationId === "write_document" && ["applied", "duplicate"].includes(String(result.disposition))
&& published.id === document.id && published.latestRevisionId === document.revisionId;
})).map(({ id, revisionId, key, issuePrefix, issueIdentifier }) => ({ id, revisionId, key,
href: `/${encodeURIComponent(issuePrefix)}/issues/${encodeURIComponent(issueIdentifier ?? binding.issueId)}#document-${encodeURIComponent(key)}` }));
}
/** Files cited as completed output must be reachable outside the agent workspace. */
export async function validateNativeDeliverableEvidence(
db: Db,
binding: { companyId: string; issueId: string; runId: string; objective: string; semanticToolReceipts: unknown },
result: PrpStructuredRunResult,
): Promise<void> {
if (result.reportedWorkDisposition !== "done") return;
const fileRequested = explicitlyRequestsFileOutput(binding.objective);
const taskDocumentRequested = explicitlyRequestsTaskDocumentOutput(binding.objective);
const artifactRefs = new Set(evidenceRefs(result.artifacts));
const refs = new Set([
...evidenceRefs(result.evidence),
...artifactRefs,
...result.completionClaim.criteria.flatMap(({ evidenceRefs }) => evidenceRefs),
]);
const citesTaskDocument = [...refs].some(ref => /^\/[^/?#]+\/issues\/[^/?#]+#document-[^/?#]+$/u.test(ref.trim()));
const publishedDocuments = taskDocumentRequested || citesTaskDocument ? await publishedTaskDocuments(db, binding) : [];
const publishedTaskDocument = taskDocumentRequested && publishedDocuments.length > 0;
const documentHrefs = new Set(publishedDocuments.map(document => document.href));
let registeredAttachment = false;
for (const value of refs) {
if (typeof value !== "string") continue;
const ref = value.trim();
if (documentHrefs.has(ref)) continue;
const attachmentPath = /^\/api\/attachments\/([^/?#]+)\/content(?:[?#].*)?$/u.exec(ref);
if (ref.startsWith("deliverable:") || attachmentPath) {
const id = attachmentPath?.[1] ?? ref.slice("deliverable:".length);
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
const [attachment] = uuid.test(id)
? await db.select({ id: issueAttachments.id, originatingRunId: issueAttachments.originatingRunId,
filename: assets.originalFilename, byteSize: assets.byteSize, sha256: assets.sha256 }).from(issueAttachments)
.innerJoin(assets, and(eq(assets.id, issueAttachments.assetId), eq(assets.companyId, binding.companyId)))
.where(and(eq(issueAttachments.id, id), eq(issueAttachments.companyId, binding.companyId), eq(issueAttachments.issueId, binding.issueId)))
.limit(1)
: [];
if (!attachment) {
throw new Error("Completion cites no registered attachment on this task. Use register_deliverable for the requested file and cite deliverable:<attachmentId> from its receipt. No human completion approval was created.");
}
// A prior output (or user input) can be useful context, but does not prove
// this run published the newly requested output. The receipt survives a
// controller restart of this run; a replacement can re-register preserved
// workspace bytes internally rather than asking the user to confirm them.
if ((fileRequested || taskDocumentRequested) && attachment.originatingRunId !== binding.runId) continue;
if ((fileRequested || taskDocumentRequested) && !await hasCurrentPublicationReceipt(db, binding.companyId, binding.semanticToolReceipts, attachment)) {
throw new Error("This attachment has no matching verified publication receipt for this run's requested output. Inspect any preserved file and use register_deliverable to verify its current filename, size, and SHA-256, then cite the new receipt. No human completion approval was created.");
}
registeredAttachment = true;
continue;
}
// URLs and typed durable refs are not workspace paths. Verification commands
// belong in verification; do not scan prose or upload files named by a model.
const localFile = /^(?:file:|\.{0,2}\/|[a-z]:[\\/])/iu.test(ref)
|| (!/^[a-z][a-z0-9+.-]*:/iu.test(ref) && /^[^\r\n]+\.[a-z0-9]{1,16}(?::\d+(?::\d+)?)?$/iu.test(ref));
if (localFile && taskDocumentRequested && !publishedTaskDocument) {
throw new Error("The requested task document is only a workspace file. Publish it on this task with write_document, or attach the verified file with register_deliverable and cite deliverable:<attachmentId>. Reuse completed work; do not request a new completion approval.");
}
if (localFile && (fileRequested || artifactRefs.has(value))) {
throw new Error("Completion cites a workspace-only file that the user cannot download. Before finishing, use register_deliverable for requested file outputs and cite deliverable:<attachmentId> from the receipt, with /api/attachments/<attachmentId>/content as the download link. For repository changes, cite an accessible PR or registered work product instead. No human completion approval was created.");
}
}
if (taskDocumentRequested && !publishedTaskDocument && !registeredAttachment) {
throw new Error("The requested document has not been published on this task. Use write_document, or register_deliverable with its verified attachment receipt. A workspace path or final message alone is not the requested task document. Reuse completed work without requesting a new completion approval.");
}
if (fileRequested && !registeredAttachment) {
const products = refs.size ? await db.select().from(issueWorkProducts).where(and(
eq(issueWorkProducts.companyId, binding.companyId), eq(issueWorkProducts.issueId, binding.issueId),
)) : [];
const accessibleProduct = products.some(product => {
if (product.createdByRunId !== binding.runId) return false;
if (["failed", "cancelled", "archived"].includes(product.status)) return false;
// A workspace_file resource is only a locator: registration neither checks
// its current bytes nor keeps them alive after workspace cleanup. Requested
// files need a published URL or the verified attachment receipt above.
const accessible = typeof product.url === "string" && /^https?:\/\//iu.test(product.url);
return accessible && [product.url, `work_product:${product.id}`, `work-product:${product.id}`, `artifact:${product.id}`]
.some(ref => typeof ref === "string" && refs.has(ref));
});
if (!accessibleProduct) throw new Error("The requested file has no accessible delivery evidence. Use register_deliverable and cite deliverable:<attachmentId>, or cite an accessible work product registered by this run for this task. Empty evidence, prior-run output, and a verification result cannot substitute for the requested file. Continue publishing or report a concrete blocker; no human completion approval was created.");
}
}