Skip to content

fix(copilot-local): implement instructions bundle + local agent JWT - #8

Open
asklar wants to merge 3 commits into
superbiche:mainfrom
asklar:fix/copilot-instructions-bundle
Open

fix(copilot-local): implement instructions bundle + local agent JWT#8
asklar wants to merge 3 commits into
superbiche:mainfrom
asklar:fix/copilot-instructions-bundle

Conversation

@asklar

@asklar asklar commented Jul 1, 2026

Copy link
Copy Markdown

Fixes #7.

What changed

  • copilot-local: execute() now loads the managed instructions bundle — the entry file (instructionsFilePath) plus sibling *.md files in instructionsRootPath — prepends it to the run prompt, and exposes the root to the CLI via --add-dir so relative references resolve. (loadCopilotInstructionsBundle() in packages/copilot-local/src/server/execute.ts.)
  • copilot-local: execute() now forwards the per-run ctx.authToken as PAPERCLIP_API_KEY (unless already set via config.env), matching qwen-local / cline-local.
  • The bundle loader caps injected content at 256 KB, enforced on a real byte boundary via StringDecoder (a char-based slice overshoots the cap and can split a surrogate pair for multibyte content).
  • Added a focused test suite (tests/instructions-bundle.test.ts, 6 tests) covering load order, HEARTBEAT/disposition content reaching the prompt, dedup, entry-only fallback, unreadable-files fallback, and the multibyte byte-cap.

Why

The manifest declared supportsInstructionsBundle: true and supportsLocalAgentJwt: true, but neither was implemented. As a result the Copilot agent ran with no AGENTS.md / HEARTBEAT.md operating contract (so it never recorded an issue disposition → Paperclip successful_run_missing_state / clear_next_step "Missing Disposition" recovery failures) and no API credential (every Paperclip API call returned HTTP 401). See #7 for full root-cause and evidence. qwen-local / cline-local already implement both; this brings copilot-local in line.

Verified live in a running Paperclip instance: the Chief-of-Staff agent went 30/30 successful runs, the first onboarding task ("Hire your first engineer") reached done, with zero "Missing Disposition" recoveries and zero 401s.

Verification

  • pnpm -r build clean
  • pnpm -r typecheck clean
  • pnpm -r test green (154 tests in copilot-local)
  • pnpm check:packages green
  • Added a changeset (pnpm exec changeset) for any user-visible package change

Summary by CodeRabbit

  • New Features

    • Copilot local runs now include managed instructions bundles, loading the entry file and related markdown files automatically.
    • Local agents can now receive a Paperclip API key from the current run context when one isn’t already set.
  • Bug Fixes

    • Improved support for expected agent operating guidance and recovery behavior.
    • Added safer handling for unreadable instruction files and large text content to avoid broken prompts.

asklar and others added 3 commits July 1, 2026 01:22
The adapter declared supportsInstructionsBundle: true but never read
instructionsFilePath/instructionsRootPath, so Copilot ran with no AGENTS.md/
HEARTBEAT.md operating contract and never recorded an issue disposition
(Paperclip 'Missing Disposition' / clear_next_step recovery failures).

execute() now loads the entry file plus sibling *.md bundle files, prepends
them to the prompt, and passes --add-dir <instructionsRootPath>. Adds a
focused test suite for the loader.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The adapter declared supportsLocalAgentJwt: true but never consumed
ctx.authToken, so the spawned Copilot agent had no credential for the
Paperclip API and every call returned HTTP 401 — blocking issue checkout
and the final disposition. Forward ctx.authToken as PAPERCLIP_API_KEY
(unless already set via config.env), matching qwen-local/cline-local.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Code review found the MAX_INSTRUCTIONS_BYTES cap was applied with a
character-based String.slice(), which overshoots the byte cap (~2-4x)
for multibyte content and can split a surrogate pair. Read the file as
bytes and truncate via StringDecoder so only complete characters within
the cap are kept. Adds a multibyte byte-cap regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements the previously-declared but missing Copilot adapter capabilities: loadCopilotInstructionsBundle reads and concatenates the entry instructions file and sibling *.md files (with byte-cap truncation) into the Copilot prompt, exposing --add-dir; execute() also forwards ctx.authToken as PAPERCLIP_API_KEY. Adds a changeset and tests.

Changes

Instructions bundle and auth token forwarding

Layer / File(s) Summary
Instructions bundle loader implementation
packages/copilot-local/src/server/execute.ts
Adds StringDecoder import and implements CopilotInstructionsBundle/loadCopilotInstructionsBundle, reading the entry file and sorted sibling *.md files, enforcing a UTF-8 byte cap, and returning prompt content, root path, provenance, and notes.
PAPERCLIP_API_KEY forwarding in runtime config
packages/copilot-local/src/server/execute.ts
Extends buildCopilotRuntimeConfig's input with an optional authToken and injects it into env.PAPERCLIP_API_KEY unless already set.
Wiring bundle and auth token into execute()
packages/copilot-local/src/server/execute.ts, .changeset/copilot-instructions-bundle.md
Destructures authToken from ctx, loads the instructions bundle, prepends it to the prompt, adds --add-dir when applicable, extends onMeta with bundle provenance, and documents the fix in a changeset.
Instructions bundle loader test suite
packages/copilot-local/tests/instructions-bundle.test.ts
Adds Vitest tests covering empty bundles, sorted entry+sibling file loading, directory fallback, deduplication, unreadable-file notes, and UTF-8 byte-cap truncation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Paperclip
  participant execute
  participant loadCopilotInstructionsBundle
  participant CopilotCLI

  Paperclip->>execute: run(ctx.authToken, config)
  execute->>execute: buildCopilotRuntimeConfig(authToken)
  execute->>execute: set env.PAPERCLIP_API_KEY
  execute->>loadCopilotInstructionsBundle: read entry + sibling *.md files
  loadCopilotInstructionsBundle-->>execute: prompt, addDir, notes
  execute->>CopilotCLI: spawn with prepended prompt, --add-dir
  execute->>Paperclip: onMeta(instructionsBundle provenance)
Loading

Related issues: #7 — Implements the missing instructions bundle loading and local-agent JWT forwarding in packages/copilot-local/src/server/execute.ts, addressing the "Missing Disposition" and HTTP 401 issues described.

Suggested labels: bug, enhancement, copilot-local

Suggested reviewers: superbiche

🐰 A rabbit hops through markdown trees,
Bundling notes like autumn leaves,
A token slipped in, quiet and neat,
Now Copilot knows its final feat. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing the copilot-local instructions bundle and local agent JWT support.
Description check ✅ Passed The description matches the template with What changed, Why, and Verification sections filled in appropriately.
Linked Issues check ✅ Passed The PR implements the instructions bundle loading, add-dir exposure, and PAPERCLIP_API_KEY forwarding required by issue #7.
Out of Scope Changes check ✅ Passed The added byte-cap logic and test coverage are directly related to the linked issue and do not appear out of scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/copilot-local/tests/instructions-bundle.test.ts (1)

19-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression for ctx.authToken forwarding and override precedence.

This suite locks down the bundle loader well, but the other bug fixed in this PR—setting PAPERCLIP_API_KEY from ctx.authToken unless config.env already provides one—still has no coverage here. A small buildCopilotRuntimeConfig/execute() test would protect the exact 401 regression this change is meant to close.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/copilot-local/tests/instructions-bundle.test.ts` around lines 19 -
129, Add a regression test for `buildCopilotRuntimeConfig` or `execute()` that
verifies `ctx.authToken` is forwarded into `PAPERCLIP_API_KEY` when `config.env`
does not already define it. Also cover override precedence by asserting
`config.env.PAPERCLIP_API_KEY` wins over `ctx.authToken`, so the auth token
mapping and env override behavior stay locked down.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/copilot-local/src/server/execute.ts`:
- Around line 143-168: The fallback directory selection in execute() is still
using rootDir even when fs.readdir(rootDir) fails, which can send an unreadable
path to --add-dir. Update the logic around rootDir, entryFile, and the directory
scan so that when the configured root cannot be read, the fallback addDir is the
readable directory that contains instructionsFilePath (or otherwise a valid
directory for relative references), while still keeping the entry file in the
bundle. Apply the same fix in the other duplicated call sites noted by the
review.

---

Nitpick comments:
In `@packages/copilot-local/tests/instructions-bundle.test.ts`:
- Around line 19-129: Add a regression test for `buildCopilotRuntimeConfig` or
`execute()` that verifies `ctx.authToken` is forwarded into `PAPERCLIP_API_KEY`
when `config.env` does not already define it. Also cover override precedence by
asserting `config.env.PAPERCLIP_API_KEY` wins over `ctx.authToken`, so the auth
token mapping and env override behavior stay locked down.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 426e5e2d-4475-4635-be04-a3b6b814c6ed

📥 Commits

Reviewing files that changed from the base of the PR and between abeaea5 and dcf5a2e.

📒 Files selected for processing (3)
  • .changeset/copilot-instructions-bundle.md
  • packages/copilot-local/src/server/execute.ts
  • packages/copilot-local/tests/instructions-bundle.test.ts

Comment on lines +143 to +168
const rootDir = instructionsRootPath
? path.resolve(instructionsRootPath)
: path.dirname(path.resolve(instructionsFilePath));
const entryFile = instructionsFilePath ? path.resolve(instructionsFilePath) : "";

// Ordered, de-duplicated list of bundle files: entry first, then remaining
// top-level `*.md` files in the root (sorted for determinism).
const files: string[] = [];
const seen = new Set<string>();
const pushFile = (candidate: string) => {
const resolved = path.resolve(candidate);
if (seen.has(resolved)) return;
seen.add(resolved);
files.push(resolved);
};
if (entryFile) pushFile(entryFile);
try {
const dirEntries = await fs.readdir(rootDir, { withFileTypes: true });
for (const dirEntry of dirEntries
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".md"))
.sort((a, b) => a.name.localeCompare(b.name))) {
pushFile(path.join(rootDir, dirEntry.name));
}
} catch {
// Root unreadable (or only an entry file was supplied) — fall back to entry.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fallback addDir to the readable entry directory when the configured root cannot be read.

If instructionsFilePath loads but fs.readdir(rootDir) fails, this still returns addDir = rootDir, and execute() forwards that bad path to --add-dir. That breaks the intended unreadable-root fallback: the prompt contains the entry file, but relative references now resolve against the wrong directory (or Copilot rejects the arg entirely).

Proposed fix
-  const rootDir = instructionsRootPath
+  const rootDir = instructionsRootPath
     ? path.resolve(instructionsRootPath)
     : path.dirname(path.resolve(instructionsFilePath));
   const entryFile = instructionsFilePath ? path.resolve(instructionsFilePath) : "";
+  let addDir = rootDir;
@@
   try {
     const dirEntries = await fs.readdir(rootDir, { withFileTypes: true });
@@
   } catch {
     // Root unreadable (or only an entry file was supplied) — fall back to entry.
+    if (entryFile) addDir = path.dirname(entryFile);
   }
@@
   return {
     prompt: preamble,
-    addDir: rootDir,
+    addDir,
     notes,
     chars: preamble.length,
     loadedFiles,
   };

Also applies to: 232-235, 422-425

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/copilot-local/src/server/execute.ts` around lines 143 - 168, The
fallback directory selection in execute() is still using rootDir even when
fs.readdir(rootDir) fails, which can send an unreadable path to --add-dir.
Update the logic around rootDir, entryFile, and the directory scan so that when
the configured root cannot be read, the fallback addDir is the readable
directory that contains instructionsFilePath (or otherwise a valid directory for
relative references), while still keeping the entry file in the bundle. Apply
the same fix in the other duplicated call sites noted by the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

copilot-local: declared capabilities (instructions bundle + local agent JWT) are never implemented → "Missing Disposition" + HTTP 401

1 participant