mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
fix(skills): ship the completion helper with the installed skill (#15554)
## 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>
This commit is contained in:
1 parent
71cd0a2621
commit
2f0c485dec
9 files changed
+271
-176
No files matched your search
@@ -1,15 +1,19 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import http from "node:http";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { ensurePaperclipSkillSymlink } from "@paperclipai/adapter-utils/server-utils";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
// End-to-end coverage for scripts/paperclip-issue-update.sh: the helper must
|
||||
// End-to-end coverage for the skill-bundled issue update helper: the helper must
|
||||
// only exit 0 when the server confirms the write by echoing the update, must
|
||||
// classify failures (retry connection-level faults and 5xx, never retry a
|
||||
// definitive 4xx), and must stop at two attempts total to honor the shared
|
||||
// bounded-write-retry rule.
|
||||
const HELPER_PATH = path.resolve("scripts/paperclip-issue-update.sh");
|
||||
const HELPER_PATH = path.resolve("skills/paperclip/scripts/paperclip-issue-update.sh");
|
||||
const REPO_HELPER_PATH = path.resolve("scripts/paperclip-issue-update.sh");
|
||||
|
||||
interface HelperResult {
|
||||
code: number | null;
|
||||
@@ -21,6 +25,8 @@ interface RecordedRequest {
|
||||
method: string;
|
||||
url: string;
|
||||
body: string;
|
||||
authorization: string | undefined;
|
||||
runId: string | string[] | undefined;
|
||||
}
|
||||
|
||||
describe("paperclip issue update helper", () => {
|
||||
@@ -48,6 +54,8 @@ describe("paperclip issue update helper", () => {
|
||||
method: req.method ?? "",
|
||||
url: req.url ?? "",
|
||||
body,
|
||||
authorization: req.headers.authorization,
|
||||
runId: req.headers["x-paperclip-run-id"],
|
||||
};
|
||||
requests.push(recorded);
|
||||
respond(recorded, requests.length, res);
|
||||
@@ -66,17 +74,19 @@ describe("paperclip issue update helper", () => {
|
||||
return { baseUrl: `http://127.0.0.1:${port}`, requests };
|
||||
}
|
||||
|
||||
function runHelper(apiUrl: string, args: string[]): Promise<HelperResult> {
|
||||
function runHelper(apiUrl: string, args: string[], options: { helper?: string; cwd?: string; input?: string } = {}): Promise<HelperResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("bash", [HELPER_PATH, ...args], {
|
||||
const child = spawn("bash", [options.helper ?? HELPER_PATH, ...args], {
|
||||
cwd: options.cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
PAPERCLIP_API_URL: apiUrl,
|
||||
PAPERCLIP_API_KEY: "test-key",
|
||||
PAPERCLIP_RUN_ID: "test-run",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
child.stdin.end(options.input);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
@@ -109,6 +119,44 @@ describe("paperclip issue update helper", () => {
|
||||
expect(JSON.parse(requests[0]?.body ?? "{}")).toEqual({ status: "done", comment: "closing note" });
|
||||
});
|
||||
|
||||
it.each([".claude/skills", "codex-home/skills"])(
|
||||
"delivers the bundled helper through %s from an unrelated workspace", async skillsHome => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip completion helper "));
|
||||
cleanupFns.push(() => fs.rm(root, { recursive: true, force: true }));
|
||||
const source = path.join(root, "bundle", "paperclip");
|
||||
const installed = path.join(root, skillsHome, "paperclip");
|
||||
const cwd = path.join(root, "unrelated project");
|
||||
await fs.cp(path.dirname(path.dirname(HELPER_PATH)), source, { recursive: true });
|
||||
await fs.mkdir(path.dirname(installed), { recursive: true });
|
||||
await fs.mkdir(cwd);
|
||||
await ensurePaperclipSkillSymlink(source, installed);
|
||||
await expect(fs.access(path.join(cwd, "scripts/paperclip-issue-update.sh"))).rejects.toThrow();
|
||||
const savedComments: string[] = [];
|
||||
const { baseUrl, requests } = await startServer((request, _attempt, res) => {
|
||||
const payload = JSON.parse(request.body);
|
||||
savedComments.push(payload.comment);
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ id: "issue-1", status: payload.status, comment: { body: payload.comment } }));
|
||||
});
|
||||
const comment = "Done\n\n- Saved [report](/TST/issues/TST-1#document-report)\n- Literal `code`, $HOME and $(not-a-command)";
|
||||
const result = await runHelper(baseUrl, ["--issue-id", "issue-1", "--status", "done"], {
|
||||
helper: path.join(installed, "scripts/paperclip-issue-update.sh"), cwd, input: comment,
|
||||
});
|
||||
expect(result.code).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({ status: "done", comment: { body: comment } });
|
||||
expect(savedComments).toEqual([comment]);
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]).toMatchObject({ method: "PATCH", url: "/api/issues/issue-1", authorization: "Bearer test-key", runId: "test-run" });
|
||||
});
|
||||
|
||||
it("keeps the repository entrypoint working outside the repository", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-helper-wrapper-"));
|
||||
cleanupFns.push(() => fs.rm(root, { recursive: true, force: true }));
|
||||
const result = await runHelper("", [...doneArgs, "--dry-run"], { helper: REPO_HELPER_PATH, cwd: root });
|
||||
expect(result.code).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toEqual({ status: "done", comment: "closing note" });
|
||||
});
|
||||
|
||||
it("fails an empty 2xx body instead of treating it as success", async () => {
|
||||
const { baseUrl } = await startServer((_request, _attempt, res) => {
|
||||
res.writeHead(200, { "content-length": "0" });
|
||||
|
||||
@@ -547,6 +547,10 @@ describe("paperclip skill utils", () => {
|
||||
expect(skillBody).toContain("Verify writes — never infer them");
|
||||
expect(skillBody).toContain("An empty response body means the write FAILED");
|
||||
expect(skillBody).toContain("Never pipe a disposition write through `head`/`tail`");
|
||||
expect(skillBody).toContain("resolved relative to this installed `SKILL.md`, not the task workspace");
|
||||
expect(skillBody).toContain("do not search the filesystem for it");
|
||||
expect(skillBody).toContain('bash "$paperclip_skill_dir/scripts/paperclip-issue-update.sh"');
|
||||
expect(skillBody).not.toMatch(/^scripts\/paperclip-issue-update\.sh/m);
|
||||
// The helper's verification behavior (HTTP status parsing, retry
|
||||
// classification, attempt bound, exit codes) is exercised end-to-end in
|
||||
// paperclip-issue-update-helper.test.ts against a live local server.
|
||||
|
||||
Reference in new issue
Block a user