mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 14:10:50 +02:00
## Thinking Path > - Paperclip manages work for AI agents. > - Legacy agents use the Paperclip skill to save task status and comments. > - The skill names a script relative to the task workspace. > - That script exists only in the Paperclip source repository. > - Agents in other workspaces can hit a missing command or search for it. > - This PR ships the helper inside the skill and uses the installed skill path. > - The repository command remains available through a forwarding wrapper. ## Linked Issues or Issue Description Fixes #9527. Refs #15548 for the preceding runtime checkout guidance. Related: #6052 addresses LF line endings for the repository helper; this change addresses helper delivery and path resolution. ## What Changed - Bundle the existing issue update helper with the Paperclip skill. Preserve its HTTP checks, echoed-status check and two-attempt limit. - Resolve the command from the installed skill directory. Use a verified PATCH when that path is unavailable, without searching the filesystem. - Keep the repository command as a wrapper that works from any directory. - Test shell execution and exact status/comment payloads through both provider skill-home layouts, including paths with spaces. - Add helper sources and existing verification tests to stock-harness admission. Record an absent historical helper explicitly. Add the missing declaration for the admission fingerprint export. ## Verification - Complete directly affected source suites: 30 tests pass. They cover skill delivery, preserved multiline comments and links, authentication headers, empty responses, mismatched status, transient retries and definitive rejections. - Product E2E typecheck passes. Support suites: 1,835 Vitest tests pass, one is skipped; 128 Node tests pass. - Full local build and workspace typecheck pass. - Full local repository tests are not claimed as passed. Embedded PostgreSQL was unavailable in this worktree during the preceding task; Linux CI will run the repository gates. - The authorized matched Codex/Claude comparison is pending. It uses the existing assigned-skill case and original oracle, one initial attempt per profile and variant. - CI and a fresh Greptile review are pending. Keep this PR in draft until readiness gates complete. ## Risks - Correct path resolution depends on the harness supplying the installed skill path. The instructions use verified PATCH when that path is unavailable. - The helper still requires Bash, curl and jq. Its existing retry and response-verification behavior is unchanged. - Tests use the shared skill-directory symlink mechanism and an HTTP fixture. Real provider completion behavior still requires the bounded live comparison. - This fix does not redesign native completion, legacy recovery or ambiguous transport handling. ## Model Used OpenAI Codex, GPT-6 family. The exact model build and context window are not exposed in this session. Used code editing, shell tools and test execution. ## 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
66 lines
4.7 KiB
TypeScript
66 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { sourceFingerprint } from "./stock-harness-checks.mjs";
|
|
import { stockHarnessSourceDigest, stockHarnessSkillSources } from "./stock-harness.js";
|
|
|
|
vi.mock("node:fs", async importOriginal => ({ ...await importOriginal<typeof import("node:fs")>(), readFileSync: vi.fn() }));
|
|
|
|
describe("stock harness instruction revision", () => {
|
|
it("invalidates the prerequisite fingerprint when checkout observation changes", () => {
|
|
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
|
|
const original = sourceFingerprint();
|
|
vi.mocked(readFileSync).mockImplementation(file => Buffer.from(
|
|
String(file).endsWith("tests/runner-e2e/checkout-activity.ts") ? "changed accounting" : "unchanged"));
|
|
const changed = sourceFingerprint();
|
|
expect(original.sourceErrors).toEqual([]);
|
|
expect(changed.sourceErrors).toEqual([]);
|
|
expect(changed.fingerprint).not.toBe(original.fingerprint);
|
|
});
|
|
|
|
it.each(["scripts/paperclip-issue-update.sh", "skills/paperclip/scripts/paperclip-issue-update.sh", "tests/runner-e2e/checkout-activity.ts", "server/src/onboarding-assets/default/AGENTS.md", "packages/adapter-utils/src/server-utils.ts", "packages/shared/src/connection-intent-guidance.ts", "skills/paperclip/SKILL.md", "skills/paperclip/references/issue-documents.md", "packages/paperclip-runner/generated/capability/capabilities.yaml", "packages/paperclip-runner/spec/capability/capabilities.yaml", "tests/runner-e2e/stock-harness-manifest.ts", "packages/adapter-utils/src/acpx-engine/execute.ts", "packages/adapter-utils/src/acpx-engine/ephemeral-session-environment.ts", "tests/runner-e2e/stock-harness-instruction-variant.mjs", "tests/runner-e2e/automatic-retry.ts", "tests/runner-e2e/catalog.ts"])(
|
|
"changes when the evaluated %s changes", source => {
|
|
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
|
|
const original = stockHarnessSourceDigest();
|
|
vi.mocked(readFileSync).mockImplementation(file => Buffer.from(String(file).endsWith(source) ? "changed instructions" : "unchanged"));
|
|
expect(stockHarnessSourceDigest()).not.toBe(original);
|
|
});
|
|
it.each(["scripts/paperclip-issue-update.sh", "skills/paperclip/scripts/paperclip-issue-update.sh"])(
|
|
"invalidates the prerequisite fingerprint when %s changes", source => {
|
|
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
|
|
const original = sourceFingerprint();
|
|
vi.mocked(readFileSync).mockImplementation(file => Buffer.from(
|
|
String(file) === resolve(import.meta.dirname, "../..", source) ? "changed helper" : "unchanged"));
|
|
expect(sourceFingerprint().fingerprint).not.toBe(original.fingerprint);
|
|
});
|
|
it("records a missing historical bundled helper without accepting unreadable helper files", () => {
|
|
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
|
|
const original = sourceFingerprint();
|
|
const digest = stockHarnessSourceDigest();
|
|
vi.mocked(readFileSync).mockImplementation(file => {
|
|
if (String(file).endsWith("skills/paperclip/scripts/paperclip-issue-update.sh"))
|
|
throw Object.assign(new Error("absent"), { code: "ENOENT" });
|
|
return Buffer.from("unchanged");
|
|
});
|
|
expect(sourceFingerprint().sourceErrors).toEqual([]);
|
|
expect(sourceFingerprint().fingerprint).not.toBe(original.fingerprint);
|
|
expect(stockHarnessSourceDigest()).not.toBe(digest);
|
|
expect(stockHarnessSkillSources()[3]).toMatchObject({ present: false, sha256: null });
|
|
vi.mocked(readFileSync).mockImplementation(() => { throw Object.assign(new Error("unreadable"), { code: "EACCES" }); });
|
|
expect(stockHarnessSourceDigest).toThrow("unreadable");
|
|
expect(sourceFingerprint().sourceErrors).toContain("skills/paperclip/scripts/paperclip-issue-update.sh");
|
|
});
|
|
it("records an absent historical recipe without introducing its content or hiding other read errors", () => {
|
|
vi.mocked(readFileSync).mockImplementation(() => Buffer.from("unchanged"));
|
|
const present = stockHarnessSourceDigest();
|
|
vi.mocked(readFileSync).mockImplementation(file => {
|
|
if (String(file).endsWith("references/issue-documents.md")) throw Object.assign(new Error("absent"), { code: "ENOENT" });
|
|
return Buffer.from("unchanged");
|
|
});
|
|
expect(stockHarnessSkillSources()[1]).toEqual({ path: "skills/paperclip/references/issue-documents.md", present: false, sha256: null });
|
|
expect(stockHarnessSourceDigest()).not.toBe(present);
|
|
vi.mocked(readFileSync).mockImplementation(() => { throw Object.assign(new Error("unreadable"), { code: "EACCES" }); });
|
|
expect(stockHarnessSourceDigest).toThrow("unreadable");
|
|
});
|
|
});
|