Files
PaperClipAI/server/src/services/native-runtime/native-deliverable-feedback.test.ts
T
DottaandClaude Opus 5.5 2b461b2f58 fix(native): require publication for files named under a Deliverable label (#20231)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native agents finish a task with a completion report, and a server
gate checks that requested files are published to the task
> - The gate finds a file request only from a creation verb, such as
"Write report.pdf"
> - Delegated briefs often list the output under a "Deliverable:" label
instead, and the gate did not know `.html` or `.css` names
> - So a worker can finish a website task with `index.html` only in its
own workspace, and the person cannot open it
> - This pull request makes a file named under a Deliverable label a
requested file output
> - The benefit is that the worker gets the existing "use
register_deliverable" feedback before the task closes

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened**
A person asked the CEO: "Make me a simple website introducing our
business." The CEO delegated a task. Its brief said "Deliverable: - A
self-contained static website in this task's workspace: index.html plus
a stylesheet (for example styles.css)". The worker wrote `index.html`
and `styles.css` in its workspace. It did not call
`register_deliverable`. It cited `index.html` and `styles.css` as
completion evidence, and the gate accepted the report. The task was
marked done with no attachment. The CEO then told the person that the
files were "in the task workspace".

**Expected behavior**
A brief that names a file as its deliverable is a file request. The
completion gate must reject a workspace-only file and tell the worker to
publish it with `register_deliverable`.

**Steps to reproduce**
1. Call `explicitlyRequestsFileOutput` with the brief above.
2. See that it returns `false`, so `validateNativeDeliverableEvidence`
accepts the bare `index.html` evidence ref.

## What Changed

- `explicitlyRequestsFileOutput` now also returns true when a
`Deliverable:` / `Deliverables` label line (plain, bold or heading), or
a bullet or numbered item in the list below it (blank lines between
items allowed), names a file. Relative paths such as `dist/index.html`
count.
- Under the label, web page files (`.html`, `.htm`, `.css`) count with
the existing file types.
- Items that name an input ("a summary of input.csv", "a comparison of
input.csv and baseline.csv"), describe a repository change, pull
request, commit or branch, forbid the named file (`do not create
report.pdf`), name a file to edit in place (`update styles.css`) without
asking to publish it, send the file inline (`index.html and styles.css
inline in the reply`), or forbid creating files, do not count. An inline
or no-upload instruction about another output in the same item
(`index.html and a short summary in the reply`, `index.html; do not
upload it externally`) does not cancel the named file. The
repository-change check ignores file names, so `branch-report.pdf` still
counts.
- The sentence-based detector is unchanged.
- `doc/AGENT-ARTIFACTS.md` describes the label rule.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-deliverable-feedback.test.ts
src/services/native-runtime/native-runner-file-handoff.test.ts` (197
pass), then after the review fixes
`native-deliverable-feedback.test.ts`,
`native-completion-format-feedback.test.ts` and
`chat-attachment-reuse.test.ts` (211 pass). The 13 first-round review
cases fail on the original head.
- New cases: the real delegated brief, inline labels, heading lists,
numbered lists and bold labels require publication. Unlabelled text,
inline-chat deliverables, input references, repository changes, text
after the list ends, and "Deliverable quality ..." prose do not.
- The chat eval `onboarding.first-work` exercises this path end to end
on its candidate branch.

## Risks

- Low risk. The change only adds a true result for labelled file
outputs; an inline-only or no-file label item stays false, as the
sentence detector already decided.
- A labelled brief that names a file which is not a repository change
now needs `register_deliverable` or an accessible work product. That is
the documented delivery rule.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## 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
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 07:15:11 -06:00

244 lines
15 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 { describe, expect, it } from "vitest";
import { explicitlyRequestsFileOutput, explicitlyRequestsTaskDocumentOutput } from "./native-deliverable-feedback.js";
describe("explicit file output requirements", () => {
it.each([
"Prepare a requested file",
"Make a Markdown file named checklist.md with three items.",
"Write connection-proof.json with the computed result.",
"Write a new result.json.",
"Write a detailed result.json.",
"Write a summary of input.json as output.json.",
"Write a summary of input.json to a file named output.json.",
"Write result.json.md inline in your response.",
"Write the verified report named result.json.",
"Write the result to reports/output.json.",
"Write result.json with the contents of the verified report.",
"Create report.pdf and include the JSON inline in chat.",
"Create a PDF report and include the JSON inline in chat.",
"Write result.json inline and attach it as a download.",
"Write result.json inline and include a download link.",
"Write result.json and describe it inline in chat.",
"Write notes-and-results.json and describe it inline in chat.",
"Write result.json and explain the result inline in chat.",
"Write result.json and summarize it inline in your response.",
"Write the result to `output.json`.",
"Write result.json inline and publish it.",
"Write result.json inline. Publish it on this task.",
"Write result.json and update package.json.",
"Publish connection-proof.json on this child task with register_deliverable using the workspace-relative path, correct byte size, and correct SHA-256.",
"Write internal-proof.json. This is an internal verification file, not a deliverable. Publish it on this task.",
"Export the results as a CSV.",
"Do not modify repository files, but create an attachment.",
"Do not create attachments; export the results as CSV.",
"Do not create attachments and instead export a CSV.",
"Give me a downloadable report.",
"Please create out/answer.pdf and attach it.",
"Do not use external services. Create a file with the results.",
"Make a file but do not send it to anyone else.",
"Export a summary of this PDF as CSV.",
"Create no temporary files; export the results as CSV.",
"Write report.pdf. This is an internal verification file, not a deliverable. Also export the results as CSV.",
"Write a downloadable report.pdf. This is personal memory, not a task deliverable.",
"Attempt native write to report.txt with content requested.",
"Write report.pdf and checklist.md. This is an internal assertion file, not a deliverable.",
"Write report.pdf; write internal-proof.txt. This is an internal verification file, not a deliverable.",
"Write report.pdf and attempt native write to /outside/probe.txt; this negative test must be denied.",
"Attempt native write to /outside/probe.json and write report.pdf; this negative test must be denied.",
"Write report.txt and attach it. This is an internal verification file, not a deliverable.",
"Attach it.",
"Write report.txt and return it as an attachment. This is an internal verification file, not a deliverable.",
"Write report.txt and send it to me. This is an internal verification file, not a deliverable.",
"Write report.txt. This is an internal verification file, not a deliverable. Send it to me.",
"Write report.txt and provide it to me. This is an internal verification file, not a deliverable.",
"Write report.txt and give me it. This is an internal verification file, not a deliverable.",
"Write report.txt and return it. This is an internal verification file, not a deliverable.",
"Write report.txt and send it as an attachment in your response. This is an internal verification file, not a deliverable.",
"Write report.txt. This is an internal verification file, not a deliverable. Send it as a download link in your response.",
])("recognizes an explicit output request: %s", objective => {
expect(explicitlyRequestsFileOutput(objective)).toBe(true);
});
it.each([
"Explain how a newsletter works",
"Read the file and explain what it does.",
"Review the PDF and answer in the chat.",
"Do not create a file; answer inline.",
"Run hostname, id, and pwd on your assigned computer. Report their output in a short completion comment, followed by CONTROL_PLANE_CHECK_OK. Do not modify repository files or create attachments. Mark the task done when finished.",
"Do not modify repository files or create attachments.",
"Do not edit files and do not create attachments.",
"Do not create files; instead, don't create attachments either.",
"Do not create files and attach them.",
"Don’t edit files or create attachments.",
"Never edit files nor create attachments.",
"Don't attach a file. Reply with three bullets.",
"Fix a crash in parser.ts.",
"Update config.json to fix the parser.",
"Write the fix and update package.json.",
"Write the migration then edit package-lock.json.",
"Write a summary and read result.json.",
"Write a function that reads config.json.",
"Write code to parse tsconfig.json.",
"Write the route and wire it in routes.json.",
"Write a function reading config.json.",
"Write a short summary after reading input.json.",
"Write a short summary explaining config.json.",
"Write a short summary of changes to config.json.",
"Write a summary of the file named input.json.",
"Write about config.json.",
"Write concerning config.json.",
"Write a comparison of input.json to baseline.json.",
"Write a summary of input.json to output.json.",
"Write a wrapper around config.json.",
"Write tests covering package.json.",
"Write a migration touching package-lock.json.",
"Return the answer inline with no download link.",
"Return the results inline without a download link.",
"Return the answer without a download link.",
"Write result.json inline in your response as a code block; do not create any files.",
"Write notes-and-results.json inline in your response.",
"Return result.json as a code block in the reply.",
"Read connection-input.json and explain its contents in chat.",
"Write a summary of connection-input.json inline in your response.",
"Write the contents of connection-proof.json inline in your response.",
"Do not publish connection-proof.json; reply inline.",
"Write internal-proof.json. This is an internal verification file, not a deliverable. Do not publish it.",
"Read the file and write a short explanation inline.",
"No downloadable file is needed.",
"Write a summary of this PDF in chat.",
"Create a review of README.md; reply inline.",
"Give me advice on file permissions.",
"Post exactly one durable progress comment whose entire body is TRACKED, then finish this child task. Create no files and do not delegate or create any further tasks.",
"Create no files.",
"Generate no attachments and answer in chat.",
"Write a reply without any files.",
"Use native write/read file tools for this task, not bash or the instructions API.",
"Write a memory entry to memory/pi-native.txt inside the registered AGENT_HOME. This is personal memory, not a task deliverable.",
"Use native write to copy those exact bytes into pi-agent-memory-proof.txt in the task workspace. This is an internal assertion file, not a deliverable.",
"Before finishing, attempt native write exactly once to /outside/pi-unassigned.txt with content forbidden. This intentionally unassigned root must be denied.",
"Write internal-proof.txt; then check it exists. This is an internal verification file, not a deliverable.",
"Attempt native write to /outside/probe.txt and create no files. This negative test must be denied.",
"Write internal-proof.txt; do not attach it. This is an internal verification file, not a deliverable.",
"Write a reply and return it in chat.",
"Write internal-proof.txt and return it in chat. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and send it in chat. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt. This is an internal verification file, not a deliverable. Return it inline.",
"Write internal-proof.txt and send it as a code block in your response. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and return it in my reply. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and provide it as plain text. This is an internal verification file, not a deliverable.",
])("does not require a file for a text or source-review request: %s", objective => {
expect(explicitlyRequestsFileOutput(objective)).toBe(false);
});
});
describe("explicit task-document output", () => {
it.each([
"Use the connected page service to find recent pages and create a short Markdown briefing document on this task. Include the titles and verification code returned by the service.",
"Save a document on this task.",
"Write a report document attached to the issue.",
"Do not call HubSpot, but create a document on this task.",
"You may skip HubSpot, but create a document on this task.",
"Explain the lookup error, but save a document on this task.",
])("requires a published task document: %s", objective => {
expect(explicitlyRequestsTaskDocumentOutput(objective)).toBe(true);
});
it.each([
"Explain the document on this task.",
"Write a summary of the document on this task in chat.",
"Do not create a document on this task; reply inline.",
"Create no document on this task.",
"Write a response without a document on this task.",
"Create a document about this task in the repository.",
"Explain how to create a document on this task.",
"Create briefing.md in the workspace.",
"If the lookup succeeds, create a document on this task.",
"Create a document on this task if the lookup succeeds.",
"Only create a document on this task when the lookup succeeds.",
"Create a document on this task, but only if the lookup succeeds.",
"If the lookup succeeds, do not call HubSpot, but create a document on this task.",
"Read the task, but do not yet create a document on this task.",
"Unless I decline, create a document on this task.",
"Once approved, create a document on this task.",
"Do not yet create a document on this task.",
"Don’t ever create a document on this task.",
"You may optionally create a document on this task.",
"Create a document on this task only if useful; otherwise answer inline.",
"Connect HubSpot so you can read my recent contacts. If the contacts are unavailable, a brief explanation is enough instead of the contact list.",
])("preserves other output scopes: %s", objective => {
expect(explicitlyRequestsTaskDocumentOutput(objective)).toBe(false);
});
});
describe("deliverable labels that name files", () => {
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",
"Deliverable: dist/index.html",
"Deliverable: reports/summary.csv",
"Deliverables:\n- src/styles.css\n- ./site/index.html",
"## Deliverables\n\n- index.html\n- styles.css",
"Deliverables:\n\n- A landing page\n\n- index.html with the hero section",
"Deliverable: branch-report.pdf",
"Deliverable: commit-history.csv covering the last release",
"Deliverable: a comparison of input.csv and baseline.csv, saved as comparison.csv",
"Deliverable: report.pdf, with a short summary in the reply",
"Deliverable: index.html and a short summary in the reply",
"Deliverable: index.html plus a summary inline in chat",
"Deliverable: index.html; do not upload it externally",
"Deliverable: an updated report.pdf",
"Deliverable: update report.pdf and publish it",
"Deliverable: update report.pdf, then attach the updated report.pdf",
"Deliverable: index.html with a short summary in the reply",
"Deliverable: a summary in the reply and index.html",
"Deliverable: a short explanation; do not create notes.md, but attach report.pdf",
"Deliverables:\n- report.pdf, do not attach it to an email",
])("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.",
"Deliverable: Write result.json inline in your response as a code block; do not create any files.",
"Deliverable: result.json as a code block in the reply",
"Deliverables:\n- notes.md content inline in the chat",
"Deliverable: a comparison of input.csv and baseline.csv",
"Deliverable: a summary of a.csv, b.csv and c.csv",
"Deliverable: differences between old.json and new.json",
"Deliverable: a pull request on the feature branch that updates dist/index.html",
"Deliverables:\n\n- A summary in chat\n\nNotes:\n- index.html is the current page",
"Deliverable: a review of https://example.com/index.html",
"Deliverable: summary.md in the reply; do not create any new files",
"Deliverable: notes.txt without creating any files",
"Deliverable: update styles.css so the header is blue",
"Deliverables:\n- Edit the existing `index.html` to add a footer",
"Deliverable: a short explanation; do not create report.pdf",
"Deliverable: never write a separate summary.md",
"Deliverable: index.html with its contents inline in the reply",
"Deliverables: index.html and styles.css inline in the reply",
"Deliverable: update styles.css; do not publish it",
"Deliverable: update styles.css and send a short summary in the reply",
"Deliverable: index.html with the full markup inline in the reply",
])("does not treat this as a labelled file output: %s", objective => {
expect(explicitlyRequestsFileOutput(objective)).toBe(false);
});
});