diff --git a/doc/AGENT-ARTIFACTS.md b/doc/AGENT-ARTIFACTS.md index e380f34c3c5..91edffb52ef 100644 --- a/doc/AGENT-ARTIFACTS.md +++ b/doc/AGENT-ARTIFACTS.md @@ -34,6 +34,10 @@ their contents inline does not by itself require a file attachment. JSON output intent is recognized from a direct filename or an explicit output destination. Ambiguous descriptions of input files joined by `to` remain text requests; use `as`, `into`, or an explicit output file when an artifact is required. +A `Deliverable:` or `Deliverables` label also states the output. A file named +on that line, or in the list directly below it, is a requested file. This +includes web page files such as `index.html` and `styles.css`. Items that +describe a repository change, pull request, commit, or branch do not count. The completion gate permits a narrow sentence-bound exception for one requested file when the following sentence explicitly qualifies it, for example: diff --git a/server/src/services/native-runtime/native-deliverable-feedback.test.ts b/server/src/services/native-runtime/native-deliverable-feedback.test.ts index d3637c080bc..3b28bf1e9f6 100644 --- a/server/src/services/native-runtime/native-deliverable-feedback.test.ts +++ b/server/src/services/native-runtime/native-deliverable-feedback.test.ts @@ -168,3 +168,40 @@ describe("explicit task-document output", () => { expect(explicitlyRequestsTaskDocumentOutput(objective)).toBe(false); }); }); + +describe("deliverable labels that name files", () => { + // The delegated brief from a real first-website run: the worker finished with + // index.html and styles.css only in its workspace and the gate did not ask it + // to publish them. + const delegatedBrief = [ + "Build a simple, polished static marketing website that introduces the business, Acorn Notes.", + "", + "Deliverable:", + "- A self-contained static website in this task's workspace: index.html plus a stylesheet (for example styles.css). Plain HTML/CSS, no build step.", + "- Sections: a hero with the value proposition and a contact call to action.", + "", + "Constraints:", + "- Do not publish or deploy externally; no hosting, DNS, analytics, or third-party services.", + ].join("\n"); + it.each([ + delegatedBrief, + "Deliverables: index.html and styles.css", + "## Deliverables\n- `index.html`\n- `styles.css`", + "**Deliverable:** report.pdf", + "Requested deliverable:\n1. summary.csv with one row per team", + ])("requires publication for a labelled file output: %s", objective => { + expect(explicitlyRequestsFileOutput(objective)).toBe(true); + }); + it.each([ + "Build a simple website introducing the business.", + "A self-contained static website in this task's workspace: index.html plus a stylesheet.", + "Deliverable: a short answer in chat.", + "Deliverables:\n- A summary of input.csv in the reply", + "Deliverable: a pull request that updates index.html", + "Deliverables:\n- Repository change to styles.css", + "Deliverable:\n\nindex.html is mentioned after the list ended.", + "Deliverable quality matters more than speed for index.html.", + ])("does not treat this as a labelled file output: %s", objective => { + expect(explicitlyRequestsFileOutput(objective)).toBe(false); + }); +}); diff --git a/server/src/services/native-runtime/native-deliverable-feedback.ts b/server/src/services/native-runtime/native-deliverable-feedback.ts index 3cba6e59d5c..d7d45694b02 100644 --- a/server/src/services/native-runtime/native-deliverable-feedback.ts +++ b/server/src/services/native-runtime/native-deliverable-feedback.ts @@ -99,6 +99,36 @@ async function hasCurrentPublicationReceipt(db: Db, companyId: string, receipts: * an output requirement or erase a user's request for a file. */ export function explicitlyRequestsFileOutput(objective: string): boolean { + return deliverableLabelNamesFile(objective) || explicitlyRequestsFileOutputBySentence(objective); +} + +/** A brief that labels its output ("Deliverable:", "## Deliverables") and names + * a file there has stated the requested result as plainly as a creation verb. + * Delegated briefs often take this form, so a worker cannot finish with that + * file left only in its workspace. Web page files count here: the label makes + * them the output, not a repository file being edited in passing. + */ +function deliverableLabelNamesFile(objective: string): boolean { + const lines = objective.replaceAll("\\_", "_").split(/\r?\n/u); + const named = /(?:^|[\s(`'"*:])([^\s/()`'"*:]+\.(?:html?|css|md|txt|json|pdf|docx?|xlsx?|csv|pptx?|png|jpe?g|svg|zip))\b/giu; + // A repository change names files it edits; it is delivered as a PR or commit. + const namesFile = (text: string) => !/\b(?:[Rr]epo(?:sitory)?|[Pp]ull [Rr]equest|PR|[Cc]ommit|[Bb]ranch)\b/u.test(text) && [...text.matchAll(named)].some(match => + !/\b(?:of|about|from|using|reading|no|without)\s+(?:(?:the|a|an|this|that)\s+)?[`'"*]*$/iu.test(text.slice(0, match.index! + match[0].indexOf(match[1]!)))); + for (let index = 0; index < lines.length; index += 1) { + const label = /^\s*(?:#{1,6}\s*)?(?:\*\*|__)?(?:requested\s+)?deliverables?(?:\*\*|__)?\s*(?::(?:\*\*|__)?\s*(.*)|(?:\*\*|__)?\s*)$/iu.exec(lines[index]!); + if (!label) continue; + if (namesFile(label[1] ?? "")) return true; + // The label's own list: consecutive bullet or numbered items below it. + for (let item = index + 1; item < lines.length; item += 1) { + const bullet = /^\s*(?:[-*+]|\d+[.)])\s+(.*)$/u.exec(lines[item]!); + if (!bullet) break; + if (namesFile(bullet[1]!)) return true; + } + } + return false; +} + +function explicitlyRequestsFileOutputBySentence(objective: string): boolean { const sentences = objective.replaceAll("\\_", "_").split(/(?:[.!?](?:\s|$)|\n)/iu); let precedingFileOutput = false; return sentences.some((sentence, index) => {