@tickernelz/paperclip-pro-adapter-utils 2026.1006.0 → 2026.1006.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +13 -0
  2. package/dist/acpx-engine/ephemeral-session-environment.d.ts +4 -0
  3. package/dist/acpx-engine/ephemeral-session-environment.d.ts.map +1 -0
  4. package/dist/acpx-engine/ephemeral-session-environment.js +29 -0
  5. package/dist/acpx-engine/ephemeral-session-environment.js.map +1 -0
  6. package/dist/acpx-engine/ephemeral-session-environment.test.d.ts +2 -0
  7. package/dist/acpx-engine/ephemeral-session-environment.test.d.ts.map +1 -0
  8. package/dist/acpx-engine/ephemeral-session-environment.test.js +177 -0
  9. package/dist/acpx-engine/ephemeral-session-environment.test.js.map +1 -0
  10. package/dist/acpx-engine/execute.d.ts +2 -6
  11. package/dist/acpx-engine/execute.d.ts.map +1 -1
  12. package/dist/acpx-engine/execute.js +125 -62
  13. package/dist/acpx-engine/execute.js.map +1 -1
  14. package/dist/acpx-engine/execute.test.js +299 -3
  15. package/dist/acpx-engine/execute.test.js.map +1 -1
  16. package/dist/acpx-engine/session-codec.d.ts.map +1 -1
  17. package/dist/acpx-engine/session-codec.js +1 -0
  18. package/dist/acpx-engine/session-codec.js.map +1 -1
  19. package/dist/acpx-engine/settlement-characterization.test.js +105 -0
  20. package/dist/acpx-engine/settlement-characterization.test.js.map +1 -1
  21. package/dist/acpx-engine/spawn-smoke.test.js +16 -7
  22. package/dist/acpx-engine/spawn-smoke.test.js.map +1 -1
  23. package/dist/acpx-engine/startup-cancellation.d.ts +1 -0
  24. package/dist/acpx-engine/startup-cancellation.d.ts.map +1 -1
  25. package/dist/acpx-engine/startup-cancellation.js +7 -2
  26. package/dist/acpx-engine/startup-cancellation.js.map +1 -1
  27. package/dist/acpx-engine/terminal-session-failure.d.ts +18 -0
  28. package/dist/acpx-engine/terminal-session-failure.d.ts.map +1 -0
  29. package/dist/acpx-engine/terminal-session-failure.js +106 -0
  30. package/dist/acpx-engine/terminal-session-failure.js.map +1 -0
  31. package/dist/acpx-engine/terminal-session-failure.test.d.ts +2 -0
  32. package/dist/acpx-engine/terminal-session-failure.test.d.ts.map +1 -0
  33. package/dist/acpx-engine/terminal-session-failure.test.js +139 -0
  34. package/dist/acpx-engine/terminal-session-failure.test.js.map +1 -0
  35. package/dist/acpx-engine/turn-characterization.test.js +4 -3
  36. package/dist/acpx-engine/turn-characterization.test.js.map +1 -1
  37. package/dist/command-managed-runtime.d.ts +2 -0
  38. package/dist/command-managed-runtime.d.ts.map +1 -1
  39. package/dist/command-managed-runtime.js +2 -0
  40. package/dist/command-managed-runtime.js.map +1 -1
  41. package/dist/command-redaction.d.ts +2 -0
  42. package/dist/command-redaction.d.ts.map +1 -1
  43. package/dist/command-redaction.js +20 -10
  44. package/dist/command-redaction.js.map +1 -1
  45. package/dist/command-redaction.test.js +20 -1
  46. package/dist/command-redaction.test.js.map +1 -1
  47. package/dist/directory-merge-lock.test.d.ts +2 -0
  48. package/dist/directory-merge-lock.test.d.ts.map +1 -0
  49. package/dist/directory-merge-lock.test.js +166 -0
  50. package/dist/directory-merge-lock.test.js.map +1 -0
  51. package/dist/execution-phase.d.ts +10 -0
  52. package/dist/execution-phase.d.ts.map +1 -0
  53. package/dist/execution-phase.js +27 -0
  54. package/dist/execution-phase.js.map +1 -0
  55. package/dist/execution-phase.test.d.ts +2 -0
  56. package/dist/execution-phase.test.d.ts.map +1 -0
  57. package/dist/execution-phase.test.js +26 -0
  58. package/dist/execution-phase.test.js.map +1 -0
  59. package/dist/execution-target-sandbox.test.js +49 -7
  60. package/dist/execution-target-sandbox.test.js.map +1 -1
  61. package/dist/execution-target-stdin-race.test.js +245 -2
  62. package/dist/execution-target-stdin-race.test.js.map +1 -1
  63. package/dist/execution-target.d.ts +6 -0
  64. package/dist/execution-target.d.ts.map +1 -1
  65. package/dist/execution-target.js +74 -22
  66. package/dist/execution-target.js.map +1 -1
  67. package/dist/git-workspace-sync.d.ts +15 -5
  68. package/dist/git-workspace-sync.d.ts.map +1 -1
  69. package/dist/git-workspace-sync.js +267 -124
  70. package/dist/git-workspace-sync.js.map +1 -1
  71. package/dist/git-workspace-sync.test.js +296 -20
  72. package/dist/git-workspace-sync.test.js.map +1 -1
  73. package/dist/github-launcher.d.ts.map +1 -1
  74. package/dist/github-launcher.js +26 -11
  75. package/dist/github-launcher.js.map +1 -1
  76. package/dist/github-launcher.test.js +34 -1
  77. package/dist/github-launcher.test.js.map +1 -1
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/paperclip-runner-permissions.d.ts +28 -1
  83. package/dist/paperclip-runner-permissions.d.ts.map +1 -1
  84. package/dist/paperclip-runner-permissions.js +9 -1
  85. package/dist/paperclip-runner-permissions.js.map +1 -1
  86. package/dist/prompt-sections.test.d.ts +2 -0
  87. package/dist/prompt-sections.test.d.ts.map +1 -0
  88. package/dist/prompt-sections.test.js +53 -0
  89. package/dist/prompt-sections.test.js.map +1 -0
  90. package/dist/provider-stopped-boundary.d.ts +7 -0
  91. package/dist/provider-stopped-boundary.d.ts.map +1 -0
  92. package/dist/provider-stopped-boundary.js +22 -0
  93. package/dist/provider-stopped-boundary.js.map +1 -0
  94. package/dist/provider-stopped-boundary.test.d.ts +2 -0
  95. package/dist/provider-stopped-boundary.test.d.ts.map +1 -0
  96. package/dist/provider-stopped-boundary.test.js +76 -0
  97. package/dist/provider-stopped-boundary.test.js.map +1 -0
  98. package/dist/remote-managed-runtime.d.ts +2 -0
  99. package/dist/remote-managed-runtime.d.ts.map +1 -1
  100. package/dist/remote-managed-runtime.js +3 -1
  101. package/dist/remote-managed-runtime.js.map +1 -1
  102. package/dist/remote-managed-runtime.test.js +25 -0
  103. package/dist/remote-managed-runtime.test.js.map +1 -1
  104. package/dist/sandbox-callback-bridge.d.ts +2 -0
  105. package/dist/sandbox-callback-bridge.d.ts.map +1 -1
  106. package/dist/sandbox-callback-bridge.js +34 -10
  107. package/dist/sandbox-callback-bridge.js.map +1 -1
  108. package/dist/sandbox-callback-bridge.test.js +106 -3
  109. package/dist/sandbox-callback-bridge.test.js.map +1 -1
  110. package/dist/sandbox-managed-runtime.d.ts +3 -0
  111. package/dist/sandbox-managed-runtime.d.ts.map +1 -1
  112. package/dist/sandbox-managed-runtime.js +749 -718
  113. package/dist/sandbox-managed-runtime.js.map +1 -1
  114. package/dist/sandbox-managed-runtime.test.js +171 -2
  115. package/dist/sandbox-managed-runtime.test.js.map +1 -1
  116. package/dist/server-utils.d.ts +43 -1
  117. package/dist/server-utils.d.ts.map +1 -1
  118. package/dist/server-utils.js +103 -31
  119. package/dist/server-utils.js.map +1 -1
  120. package/dist/server-utils.test.js +235 -45
  121. package/dist/server-utils.test.js.map +1 -1
  122. package/dist/session-compaction.d.ts.map +1 -1
  123. package/dist/session-compaction.js +6 -0
  124. package/dist/session-compaction.js.map +1 -1
  125. package/dist/ssh-fixture.test.js +22 -0
  126. package/dist/ssh-fixture.test.js.map +1 -1
  127. package/dist/ssh.d.ts +2 -0
  128. package/dist/ssh.d.ts.map +1 -1
  129. package/dist/ssh.js +2 -2
  130. package/dist/ssh.js.map +1 -1
  131. package/dist/test-fixtures/prompt-context.d.ts +54 -0
  132. package/dist/test-fixtures/prompt-context.d.ts.map +1 -0
  133. package/dist/test-fixtures/prompt-context.js +55 -0
  134. package/dist/test-fixtures/prompt-context.js.map +1 -0
  135. package/dist/types.d.ts +12 -2
  136. package/dist/types.d.ts.map +1 -1
  137. package/dist/workspace-git-stream.d.ts +19 -0
  138. package/dist/workspace-git-stream.d.ts.map +1 -0
  139. package/dist/workspace-git-stream.js +99 -0
  140. package/dist/workspace-git-stream.js.map +1 -0
  141. package/dist/workspace-git-stream.test.d.ts +2 -0
  142. package/dist/workspace-git-stream.test.d.ts.map +1 -0
  143. package/dist/workspace-git-stream.test.js +71 -0
  144. package/dist/workspace-git-stream.test.js.map +1 -0
  145. package/dist/workspace-manifest.d.ts +61 -0
  146. package/dist/workspace-manifest.d.ts.map +1 -0
  147. package/dist/workspace-manifest.js +257 -0
  148. package/dist/workspace-manifest.js.map +1 -0
  149. package/dist/workspace-manifest.test.d.ts +2 -0
  150. package/dist/workspace-manifest.test.d.ts.map +1 -0
  151. package/dist/workspace-manifest.test.js +160 -0
  152. package/dist/workspace-manifest.test.js.map +1 -0
  153. package/dist/workspace-restore-diagnostics.d.ts +34 -0
  154. package/dist/workspace-restore-diagnostics.d.ts.map +1 -0
  155. package/dist/workspace-restore-diagnostics.js +238 -0
  156. package/dist/workspace-restore-diagnostics.js.map +1 -0
  157. package/dist/workspace-restore-diagnostics.test.d.ts +2 -0
  158. package/dist/workspace-restore-diagnostics.test.d.ts.map +1 -0
  159. package/dist/workspace-restore-diagnostics.test.js +258 -0
  160. package/dist/workspace-restore-diagnostics.test.js.map +1 -0
  161. package/dist/workspace-restore-merge.d.ts +47 -6
  162. package/dist/workspace-restore-merge.d.ts.map +1 -1
  163. package/dist/workspace-restore-merge.js +382 -114
  164. package/dist/workspace-restore-merge.js.map +1 -1
  165. package/dist/workspace-restore-merge.test.js +253 -4
  166. package/dist/workspace-restore-merge.test.js.map +1 -1
  167. package/dist/workspace-restore-result.d.ts +6 -0
  168. package/dist/workspace-restore-result.d.ts.map +1 -0
  169. package/dist/workspace-restore-result.js +72 -0
  170. package/dist/workspace-restore-result.js.map +1 -0
  171. package/dist/workspace-restore-result.test.d.ts +2 -0
  172. package/dist/workspace-restore-result.test.d.ts.map +1 -0
  173. package/dist/workspace-restore-result.test.js +74 -0
  174. package/dist/workspace-restore-result.test.js.map +1 -0
  175. package/dist/workspace-restore-teardown.d.ts.map +1 -1
  176. package/dist/workspace-restore-teardown.js +5 -3
  177. package/dist/workspace-restore-teardown.js.map +1 -1
  178. package/dist/workspace-restore-teardown.test.js +24 -0
  179. package/dist/workspace-restore-teardown.test.js.map +1 -1
  180. package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +4 -3
  181. package/package.json +2 -2
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { createRuntimeStore } from "acpx/runtime";
5
6
  import { DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC, prepareAdapterExecutionTargetRuntime, startAdapterExecutionTargetPaperclipBridge, startAdapterExecutionTargetProcessSessionBridge, } from "@tickernelz/paperclip-pro-adapter-utils/execution-target";
6
7
  // Wrap the staging seam + both sandbox bridges in call-recording spies that
7
8
  // still delegate to the real implementations (a runner-backed sandbox test
@@ -19,8 +20,12 @@ vi.mock("@tickernelz/paperclip-pro-adapter-utils/execution-target", async (impor
19
20
  });
20
21
  import { buildAcpxRunSummary, createAcpxEngineExecutor, findAncestorBin, geminiVersionSupportsNativeAcpFlag, parseGeminiVersionParts, referencedSourceContentSignature, rewriteGeminiAcpFlagForVersion, summarizeAcpxTurnUsage, } from "./execute.js";
21
22
  import { ACPX_HANDSHAKE_TIMEOUT_MS } from "./constants.js";
23
+ import { sessionCodec } from "./session-codec.js";
22
24
  import { runChildProcess } from "../server-utils.js";
25
+ import { createPromptContextFixture } from "../test-fixtures/prompt-context.js";
23
26
  import { setExpensiveWorkspaceGitExecutor } from "../git-workspace-sync.js";
27
+ import { createWorkspaceRestoreTeardown } from "../workspace-restore-teardown.js";
28
+ import { withWorkspaceRestoreDiagnostics, withWorkspaceRestoreStep } from "../workspace-restore-diagnostics.js";
24
29
  import { resolveReferencedSourceIgnore } from "../sandbox-managed-runtime.js";
25
30
  import { getActiveStepContext, runWithRuntimeParent, SANDBOX_STARTUP_SPAN_ATTRS, } from "./startup-timing.js";
26
31
  const tempRoots = [];
@@ -265,6 +270,43 @@ const ALLOWED_TURN_SPAN_ATTRIBUTE_KEYS = new Set([
265
270
  "paperclip.agent.turn.wall_ms",
266
271
  ]);
267
272
  describe("shared ACPX engine runtime behavior", () => {
273
+ it("integrates the env-free store while delivering current credentials and options to the provider", async () => {
274
+ const root = await makeTempRoot();
275
+ const stateDir = path.join(root, "state");
276
+ const first = await runExecutor({ agent: "claude", agentCommand: "node ./fake-acp.js", cwd: root, stateDir,
277
+ env: { ANTHROPIC_API_KEY: "first-fixture-credential" } });
278
+ const liveEnvironment = first.sessionInputs[0].sessionOptions.env;
279
+ expect(liveEnvironment.ANTHROPIC_API_KEY).toBe("first-fixture-credential");
280
+ const store = first.runtimeOptions[0].sessionStore;
281
+ const record = {
282
+ schema: "acpx.session.v1", acpxRecordId: "integration-record", acpSessionId: "integration-session",
283
+ agentCommand: "fixture-provider", cwd: root, name: "fixture", createdAt: "2026-10-03T00:00:00.000Z",
284
+ lastUsedAt: "2026-10-03T00:00:01.000Z", lastSeq: 0,
285
+ eventLog: { active_path: path.join(stateDir, "fixture.jsonl"), segment_count: 1, max_segment_bytes: 1024, max_segments: 2 },
286
+ messages: [], updated_at: "2026-10-03T00:00:01.000Z", cumulative_token_usage: {}, request_token_usage: {},
287
+ acpx: { session_options: { env: { ...liveEnvironment }, model: "retained-model", max_turns: 4 } },
288
+ };
289
+ await store.save(record);
290
+ const file = path.join(stateDir, "sessions", "integration-record.json");
291
+ const saved = await fs.readFile(file, "utf8");
292
+ expect(saved).not.toContain("first-fixture-credential");
293
+ expect(JSON.parse(saved).acpx.session_options).toEqual({ model: "retained-model", max_turns: 4 });
294
+ expect(record.acpx?.session_options?.env).toEqual(liveEnvironment);
295
+ // A file written by the prior engine is loaded with the current run's env,
296
+ // without altering the provider launch or deleting unrelated options.
297
+ record.acpx.session_options.env = { ANTHROPIC_API_KEY: "legacy-fixture-credential" };
298
+ await createRuntimeStore({ stateDir }).save(record);
299
+ const second = await runExecutor({ agent: "claude", agentCommand: "node ./fake-acp.js", cwd: root, stateDir,
300
+ env: { ANTHROPIC_API_KEY: "rotated-fixture-credential" } }, { runtime: { sessionParams: first.result.sessionParams } });
301
+ expect(second.sessionInputs[0].sessionOptions.env.ANTHROPIC_API_KEY).toBe("rotated-fixture-credential");
302
+ const rotatedStore = second.runtimeOptions[0].sessionStore;
303
+ const loaded = await rotatedStore.load(record.acpxRecordId);
304
+ expect(loaded?.acpx?.session_options).toMatchObject({ env: { ANTHROPIC_API_KEY: "rotated-fixture-credential" }, model: "retained-model", max_turns: 4 });
305
+ await rotatedStore.save(loaded);
306
+ const resaved = await fs.readFile(file, "utf8");
307
+ for (const value of ["first-fixture-credential", "legacy-fixture-credential", "rotated-fixture-credential"])
308
+ expect(resaved).not.toContain(value);
309
+ });
268
310
  it.each(["claude", "codex", "gemini", "kimi", "custom"])("defaults the legacy %s engine to full auto on fresh and resumed runs", async (agent) => {
269
311
  const root = await makeTempRoot();
270
312
  const config = {
@@ -477,6 +519,42 @@ describe("shared ACPX engine runtime behavior", () => {
477
519
  expect((meta[0]?.env).CODEX_CONFIG).toBeUndefined();
478
520
  expect(configOptions).toEqual([]);
479
521
  });
522
+ it.each(["claude", "codex", "gemini", "kimi", "custom"])("delivers the shared owned sections at the %s ACP turn boundary", async (agent) => {
523
+ const root = await makeTempRoot();
524
+ const context = createPromptContextFixture();
525
+ const config = { agent, agentCommand: "node ./fixture-acp.js", cwd: root, stateDir: path.join(root, "state"), mode: "persistent" };
526
+ const fresh = await runExecutor(config, { context });
527
+ expect(fresh.turnInputs).toHaveLength(1);
528
+ const prompt = String(fresh.turnInputs[0]?.text ?? "");
529
+ expect(prompt).toContain("You are agent agent-1");
530
+ expect(prompt).toContain("Connection tools:");
531
+ expect(prompt).toContain("Execution contract:");
532
+ expect(prompt).toContain(context.paperclipTaskMarkdownAssignment);
533
+ expect(prompt).toContain(context.paperclipTaskCommunicationGuidance);
534
+ expect(prompt).not.toContain('"objective":');
535
+ expect(prompt).not.toContain("### Issue description");
536
+ expect(prompt).toContain('"id":"comment-first"');
537
+ expect(prompt).toContain('"id":"comment-second"');
538
+ expect(prompt).toContain('"id":"comment-scope"');
539
+ expect(prompt.indexOf('"id":"comment-first"')).toBeLessThan(prompt.indexOf('"id":"comment-second"'));
540
+ expect(prompt).toContain("Untrusted continuation evidence");
541
+ expect(prompt).toContain("receipt-1");
542
+ const resumed = await runExecutor(config, { context, runtime: { sessionParams: fresh.result.sessionParams } });
543
+ expect(resumed.sessionInputs[0]?.resumeSessionId).toBe(fresh.result.sessionId);
544
+ const resumedPrompt = String(resumed.turnInputs[0]?.text ?? "");
545
+ expect(resumedPrompt).not.toContain("Execution contract:");
546
+ expect(resumedPrompt).toContain(context.paperclipTaskMarkdownAssignmentCompact);
547
+ expect(resumedPrompt).not.toContain(context.paperclipTaskCommunicationGuidance);
548
+ expect(resumedPrompt).not.toContain('"id":"comment-first"');
549
+ expect(resumedPrompt).toContain('"id":"comment-second"');
550
+ const reset = await runExecutor(config, { context });
551
+ expect(reset.sessionInputs[0]?.resumeSessionId).toBeUndefined();
552
+ const resetPrompt = String(reset.turnInputs[0]?.text ?? "");
553
+ expect(resetPrompt).toContain("You are agent agent-1");
554
+ expect(resetPrompt).toContain("Connection tools:");
555
+ expect(resetPrompt).toContain(context.paperclipTaskMarkdownAssignment);
556
+ expect(resetPrompt).toContain("Execution contract:");
557
+ });
480
558
  it("includes Paperclip env and API access notes in the ACPX prompt without leaking the token", async () => {
481
559
  const { meta } = await runExecutor({ agent: "custom", agentCommand: "node ./fake-acp.js" }, {
482
560
  authToken: "runtime-secret-token",
@@ -603,8 +681,8 @@ describe("shared ACPX engine runtime behavior", () => {
603
681
  expect(prompt).not.toContain("Create child issues");
604
682
  expect(prompt).not.toContain("Use child issues");
605
683
  }
606
- expect(String(fresh.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "Continue your Paperclip conversation");
607
- expect(String(reset.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "Continue your Paperclip conversation");
684
+ expect(String(fresh.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "You are agent agent-1");
685
+ expect(String(reset.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "You are agent agent-1");
608
686
  const ordinary = await runExecutor({ ...config, promptTemplate: "" }, { context: { ...context, conversationMode: false } });
609
687
  expect(String(ordinary.meta[0]?.prompt)).toContain("Execution contract:");
610
688
  expect(String(ordinary.meta[0]?.prompt)).toContain("Create child issues from the approved plan");
@@ -2298,6 +2376,23 @@ describe("shared ACPX engine runtime behavior", () => {
2298
2376
  expect(second.result.sessionParams?.configFingerprint).toBeTypeOf("string");
2299
2377
  expect(first.result.sessionParams?.configFingerprint).not.toBe(second.result.sessionParams?.configFingerprint);
2300
2378
  });
2379
+ it.each(["claude", "codex", "grok", "gemini", "kimi"])("resumes %s with added and removed MCP servers and current credentials", async (agent) => {
2380
+ const root = await makeTempRoot();
2381
+ const config = { agent, cwd: root, stateDir: path.join(root, "state"), paperclipRuntimeSkills: [], paperclipSkillSync: { desiredSkills: [] } };
2382
+ const first = await runExecutor(config);
2383
+ const server = { name: "github", url: "https://example.test/github/mcp", connectionId: "github", token: "current-token" };
2384
+ const second = await runExecutor(config, {
2385
+ runtime: { sessionParams: sessionCodec.deserialize(first.result.sessionParams), taskKey: "default" },
2386
+ runtimeMcp: { getServers: () => [server] }, context: { refreshTools: true },
2387
+ });
2388
+ expect(second.sessionInputs[0]?.resumeSessionId).toBe("backend-session");
2389
+ expect(second.result.sessionParams?.configFingerprint).toBe(first.result.sessionParams?.configFingerprint);
2390
+ expect(second.runtimeOptions[0]?.mcpServers).toEqual([{ type: "http", name: "github", url: server.url, headers: [{ name: "Authorization", value: "Bearer current-token" }] }]);
2391
+ expect(JSON.stringify(sessionCodec.serialize(second.result.sessionParams ?? null))).not.toContain("current-token");
2392
+ const third = await runExecutor(config, { runtime: { sessionParams: sessionCodec.deserialize(second.result.sessionParams) }, context: { refreshTools: true } });
2393
+ expect(third.sessionInputs[0]?.resumeSessionId).toBe("backend-session");
2394
+ expect(third.runtimeOptions[0]?.mcpServers).toEqual([]);
2395
+ });
2301
2396
  it("injects runtime MCP servers and fingerprints their identity without persisting bearer tokens", async () => {
2302
2397
  const root = await makeTempRoot();
2303
2398
  const baseConfig = {
@@ -2487,10 +2582,14 @@ describe("gemini ACP flag selection", () => {
2487
2582
  // an explicit 0 cannot mean "no timeout" — it keeps the 4h backstop.
2488
2583
  const explicitZero = await runExecutor({ agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd, timeoutSec: 0 }, sandboxContext);
2489
2584
  expect(explicitZero.runtimeOptions[0]?.timeoutMs).toBe(DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC * 1000);
2585
+ expect(explicitZero.result.resultJson?.adapterExecutionTimeout).toEqual({
2586
+ timeoutSec: DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC, source: "sandbox_default",
2587
+ });
2490
2588
  // A negative timeoutSec is the documented opt-out from any adapter
2491
2589
  // wall-clock timeout, sandbox targets included.
2492
2590
  const negativeOptOut = await runExecutor({ agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd, timeoutSec: -1 }, sandboxContext);
2493
2591
  expect(negativeOptOut.runtimeOptions[0]?.timeoutMs).toBeUndefined();
2592
+ expect(negativeOptOut.result.resultJson?.adapterExecutionTimeout).toEqual({ timeoutSec: 0, source: "configured" });
2494
2593
  const startLine = negativeOptOut.logs.find((entry) => entry.stream === "stderr" && entry.text.includes("Adapter execution timeout:"));
2495
2594
  expect(startLine.text).toContain("Adapter execution timeout: none (explicitly disabled via adapterConfig.timeoutSec; " +
2496
2595
  "set it to a positive value to add one).");
@@ -2546,10 +2645,89 @@ describe("gemini ACP flag selection", () => {
2546
2645
  "Set adapterConfig.timeoutSec to raise it.";
2547
2646
  expect(result.timedOut).toBe(true);
2548
2647
  expect(result.errorCode).toBe("acpx_timeout");
2648
+ expect(result.resultJson?.adapterExecutionTimeout).toEqual({ timeoutSec: 1, source: "configured" });
2549
2649
  expect(result.errorMessage).toBe(expectedMessage);
2550
2650
  expect(cancelReasons).toContain(expectedMessage);
2651
+ expect(result.resultJson).toMatchObject({ acpObservedEventCount: 0, acpPendingToolCount: 0, acpToolInventoryComplete: true });
2652
+ expect(result.resultJson).not.toHaveProperty("acpLastEventAgeMs");
2551
2653
  }, 15_000);
2552
2654
  });
2655
+ describe("ACP activity diagnostics", () => {
2656
+ it("classifies rejected definitions before redaction and overrides a transient adapter label", async () => {
2657
+ const root = await makeTempRoot(), cwd = path.join(root, "worktree");
2658
+ await fs.mkdir(cwd, { recursive: true });
2659
+ const execute = createAcpxEngineExecutor({
2660
+ classifyTerminalSessionFailure: () => ({ errorCode: "claude_transient_upstream", errorFamily: "transient_upstream" }),
2661
+ createRuntime: () => ({ ...buildRuntime(), startTurn: (options) => {
2662
+ options.onTerminalSessionFailure({ category: "service", details: "API Error: 400 tools.17.custom.name: String should have at most 128 characters" });
2663
+ return { events: (async function* () { })(), result: Promise.resolve({ status: "failed", error: new Error("turn failed") }), cancel: async () => { } };
2664
+ } }),
2665
+ });
2666
+ const result = await execute({ runId: "invalid-definition", agent: { id: "agent-1", companyId: "company-1" }, runtime: {},
2667
+ config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state"), cwd, env: { SECRET: "128" } },
2668
+ context: {}, onMeta: async () => { }, onLog: async () => { } });
2669
+ expect(result).toMatchObject({ errorCode: "provider_tool_definition_invalid", errorFamily: "configuration" });
2670
+ expect(JSON.stringify(result.resultJson?.terminalSessionFailure)).not.toContain("128");
2671
+ });
2672
+ it.each(["terminal", "relay_error", "no_events"])("snapshots %s activity before usage reads, failure logging, and cleanup", async (outcome) => {
2673
+ const root = await makeTempRoot();
2674
+ const cwd = path.join(root, "worktree");
2675
+ await fs.mkdir(cwd, { recursive: true });
2676
+ let currentNow = 1000;
2677
+ let statusReads = 0;
2678
+ const execute = createAcpxEngineExecutor({
2679
+ now: () => currentNow,
2680
+ createRuntime: () => ({
2681
+ ...buildRuntime(),
2682
+ getStatus: async () => {
2683
+ if (++statusReads > 1)
2684
+ currentNow += 100_000;
2685
+ return null;
2686
+ },
2687
+ startTurn: () => ({
2688
+ events: (async function* () {
2689
+ if (outcome !== "no_events") {
2690
+ yield { type: "tool_call", toolCallId: "private-pending-id", title: "private command", kind: "execute", status: "in_progress" };
2691
+ yield { type: "tool_call", toolCallId: "private-done-id", title: "private read", kind: "read", status: "in_progress" };
2692
+ yield { type: "tool_call", toolCallId: "private-done-id", status: "completed" };
2693
+ yield { type: "tool_call", toolCallId: "private-cancelled-id", status: "cancelled" };
2694
+ yield { type: "tool_call", toolCallId: "private-failed-id", status: "failed" };
2695
+ yield { type: "status", text: "terminal/create private receipt" };
2696
+ }
2697
+ currentNow = 10_000;
2698
+ if (outcome === "relay_error")
2699
+ throw new Error("relay failed");
2700
+ })(),
2701
+ result: Promise.resolve({ status: "failed", error: new Error("turn failed") }),
2702
+ cancel: async () => { },
2703
+ }),
2704
+ close: async () => { currentNow += 100_000; },
2705
+ }),
2706
+ });
2707
+ const result = await execute({
2708
+ runId: "activity-run", agent: { id: "agent-1", companyId: "company-1" }, runtime: {},
2709
+ config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state"), cwd },
2710
+ context: {}, onMeta: async () => { },
2711
+ onLog: async (_stream, text) => {
2712
+ if (text.includes('"type":"acpx.error"'))
2713
+ currentNow += 100_000;
2714
+ },
2715
+ });
2716
+ expect(result.exitCode).toBe(1);
2717
+ expect(result.resultJson).toMatchObject({
2718
+ acpObservedEventCount: outcome === "no_events" ? 0 : 6,
2719
+ acpPendingToolCount: outcome === "no_events" ? 0 : 1,
2720
+ acpToolInventoryComplete: outcome === "no_events",
2721
+ });
2722
+ if (outcome === "no_events")
2723
+ expect(result.resultJson).not.toHaveProperty("acpLastEventAgeMs");
2724
+ else
2725
+ expect(result.resultJson?.acpLastEventAgeMs).toBe(9000);
2726
+ const diagnostics = Object.fromEntries(Object.entries(result.resultJson ?? {}).filter(([key]) => key.startsWith("acp")));
2727
+ expect(JSON.stringify(diagnostics)).not.toContain("private");
2728
+ expect(currentNow).toBeGreaterThan(10_000);
2729
+ });
2730
+ });
2553
2731
  describe("summarizeAcpxTurnUsage", () => {
2554
2732
  it("uses the post-turn amount alone when the cumulative cost counter reset", () => {
2555
2733
  const summary = summarizeAcpxTurnUsage({
@@ -3023,6 +3201,28 @@ describe("ACPX engine remote managed-home seam (PR 2: per-adapter home seed)", (
3023
3201
  expect(remoteAssetDir).not.toBe(managedHomeDir);
3024
3202
  expect(path.isAbsolute(remoteAssetDir)).toBe(true);
3025
3203
  });
3204
+ it("retains bounded restore diagnostics through real settlement and result reproduction", async () => {
3205
+ const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
3206
+ const failure = Object.assign(new Error("private-restore-path and command"), { code: 1, stderr: "private-restore-stderr" });
3207
+ const { result } = await runExecutor({ agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, {
3208
+ authToken: "real-run-jwt", executionTarget,
3209
+ prepareRemoteManagedHome: async (input) => {
3210
+ const stagedRuntime = await input.stage([]);
3211
+ return {
3212
+ stagedRuntime,
3213
+ teardown: createWorkspaceRestoreTeardown({
3214
+ stagedRuntime: { restoreWorkspace: () => withWorkspaceRestoreDiagnostics("workspace", () => withWorkspaceRestoreStep("git_integration", async () => { throw failure; })) },
3215
+ onLog: async () => { }, startMessage: "Restoring workspace", failurePrefix: "Restore failed",
3216
+ }),
3217
+ };
3218
+ },
3219
+ });
3220
+ expect(result.resultJson).toMatchObject({
3221
+ workspaceRestoreFailure: "restore_failed",
3222
+ workspaceRestoreDiagnostic: { phase: "workspace", step: "git_integration", errorCode: "unknown", exitCode: 1 },
3223
+ });
3224
+ expect(JSON.stringify(result)).not.toContain("private-restore-");
3225
+ });
3026
3226
  it("test_remote_seam_teardown_fires_once_on_exit", async () => {
3027
3227
  const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
3028
3228
  let teardownCalls = 0;
@@ -3116,9 +3316,53 @@ describe("ACPX engine Claude skill bundle staging (remote ACP lane)", () => {
3116
3316
  return { stagedRuntime };
3117
3317
  };
3118
3318
  }
3319
+ it("advertises folded routing metadata and exact staged paths without injecting skill bodies", async () => {
3320
+ const root = await makeTempRoot();
3321
+ const cwd = path.join(root, "worktree");
3322
+ const stateDir = path.join(root, "state");
3323
+ await fs.mkdir(cwd, { recursive: true });
3324
+ const skill = await createSkill(path.join(root, "sources"), "paperclip--versioned", [
3325
+ "---", "description: >-", " Use for Paperclip-managed tasks", " and durable task documents.",
3326
+ "---", "PRIVATE_SKILL_BODY_RECIPE", "",
3327
+ ].join("\n"));
3328
+ const { meta, result } = await runExecutor({
3329
+ agent: "claude", agentCommand: "node ./fake-acp.js", stateDir, cwd,
3330
+ paperclipRuntimeSkills: [skill], paperclipSkillSync: { desiredSkills: [skill.key] },
3331
+ });
3332
+ const bundle = await onlyChildDir(path.join(stateDir, "runtime-skills", "claude"));
3333
+ const stagedFile = path.join(bundle, ".claude", "skills", skill.runtimeName, "SKILL.md");
3334
+ const prompt = String(meta[0]?.prompt ?? "");
3335
+ expect(prompt).toContain(`- ${skill.runtimeName}: Use for Paperclip-managed tasks and durable task documents. (file: ${stagedFile})`);
3336
+ expect(prompt).not.toContain("PRIVATE_SKILL_BODY_RECIPE");
3337
+ expect(prompt).not.toContain(skill.source);
3338
+ expect(result.sessionParams?.skills).toMatchObject({ selectedSkills: [skill.runtimeName] });
3339
+ await expect(fs.readFile(stagedFile, "utf8")).resolves.toContain("PRIVATE_SKILL_BODY_RECIPE");
3340
+ });
3341
+ it("bounds routing descriptions and keeps files discoverable when metadata is absent or malformed", async () => {
3342
+ const root = await makeTempRoot();
3343
+ const cwd = path.join(root, "worktree");
3344
+ const stateDir = path.join(root, "state");
3345
+ await fs.mkdir(cwd, { recursive: true });
3346
+ const sources = path.join(root, "sources");
3347
+ const bounded = await createSkill(sources, "bounded", `---\ndescription: ${"x".repeat(600)}\n---\nBOUNDED_BODY\n`);
3348
+ const absent = await createSkill(sources, "absent", "# ABSENT_BODY\n");
3349
+ const malformed = await createSkill(sources, "malformed", "---\ndescription: incomplete header\nMALFORMED_BODY\n");
3350
+ const skills = [bounded, absent, malformed];
3351
+ const { meta } = await runExecutor({
3352
+ agent: "claude", agentCommand: "node ./fake-acp.js", stateDir, cwd,
3353
+ paperclipRuntimeSkills: skills, paperclipSkillSync: { desiredSkills: skills.map((skill) => skill.key) },
3354
+ });
3355
+ const prompt = String(meta[0]?.prompt ?? "");
3356
+ expect(prompt).toContain(`- bounded: ${"x".repeat(512)} (file: `);
3357
+ expect(prompt).not.toContain("x".repeat(513));
3358
+ expect(prompt).toMatch(/- absent \(file: .*\/absent\/SKILL\.md\)/);
3359
+ expect(prompt).toMatch(/- malformed \(file: .*\/malformed\/SKILL\.md\)/);
3360
+ for (const body of ["BOUNDED_BODY", "ABSENT_BODY", "MALFORMED_BODY"])
3361
+ expect(prompt).not.toContain(body);
3362
+ });
3119
3363
  it("rewrites the prompt and skill identity onto the in-sandbox skill root once the bundle is staged", async () => {
3120
3364
  const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
3121
- const skill = await createSkill(path.join(localCwd, "skills"), "review");
3365
+ const skill = await createSkill(path.join(localCwd, "skills"), "review", "---\ndescription: Review requested task documents.\n---\n# review\nREMOTE_BODY_ONLY\n");
3122
3366
  const { meta, result } = await runExecutor({
3123
3367
  agent: "claude",
3124
3368
  agentCommand: "node ./fake-acp.js",
@@ -3134,6 +3378,8 @@ describe("ACPX engine Claude skill bundle staging (remote ACP lane)", () => {
3134
3378
  expect(prompt).not.toContain(hostSkillsHome);
3135
3379
  const inSandboxSkillsRoot = prompt.match(/Skill root: (\S+)/)[1];
3136
3380
  expect(inSandboxSkillsRoot).not.toBe(hostSkillsHome);
3381
+ expect(prompt).toContain(`- review: Review requested task documents. (file: ${path.join(inSandboxSkillsRoot, "review", "SKILL.md")})`);
3382
+ expect(prompt).not.toContain("REMOTE_BODY_ONLY");
3137
3383
  await expect(fs.readFile(path.join(inSandboxSkillsRoot, skill.runtimeName, "SKILL.md"), "utf8")).resolves.toContain("# review");
3138
3384
  const skillsIdentity = result.sessionParams?.skills;
3139
3385
  expect(skillsIdentity?.skillRoot).toBe(inSandboxSkillsRoot);
@@ -5508,10 +5754,29 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)"
5508
5754
  ...overrides,
5509
5755
  };
5510
5756
  }
5757
+ it("collects instructions after confirmed close on a thrown provider turn, before workspace restore", async () => {
5758
+ const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
5759
+ const { paperclipStops, anyStopped } = stubBridges();
5760
+ const order = [];
5761
+ const execute = createAcpxEngineExecutor({
5762
+ stagingLocks: new Map(), warmHandles: new Map(), stagedRuntimes: new Map(),
5763
+ createRuntime: () => ({ ensureSession: async () => okHandle, startTurn: () => throwingTurn(),
5764
+ close: async () => { order.push("close"); } }),
5765
+ });
5766
+ const result = await execute({ runId: "instruction-stop-failure",
5767
+ ...remoteArgs(stateDir, localCwd, executionTarget, { onProviderStopped: async () => {
5768
+ expect(anyStopped(paperclipStops)).toBe(true);
5769
+ order.push("collect");
5770
+ } }),
5771
+ });
5772
+ expect(result.exitCode).toBe(1);
5773
+ expect(order).toEqual(["close", "collect"]);
5774
+ });
5511
5775
  it("test_teardown_continues_after_one_teardown_step_fails", async () => {
5512
5776
  const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
5513
5777
  const { paperclipStops, processStops, anyStopped } = stubBridges();
5514
5778
  const stagingLocks = new Map();
5779
+ const collectInstructions = vi.fn(async () => { });
5515
5780
  const logs = [];
5516
5781
  const execute = createAcpxEngineExecutor({
5517
5782
  stagingLocks,
@@ -5529,12 +5794,14 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)"
5529
5794
  const result = await execute({
5530
5795
  runId: "td-continue",
5531
5796
  ...remoteArgs(stateDir, localCwd, executionTarget, {
5797
+ onProviderStopped: collectInstructions,
5532
5798
  onLog: async (stream, text) => {
5533
5799
  logs.push({ stream, text });
5534
5800
  },
5535
5801
  }),
5536
5802
  });
5537
5803
  expect(result.exitCode).toBe(1);
5804
+ expect(collectInstructions).not.toHaveBeenCalled();
5538
5805
  // The close failure did not stop the bridge stops or the lease release.
5539
5806
  expect(anyStopped(paperclipStops)).toBe(true);
5540
5807
  expect(anyStopped(processStops)).toBe(true);
@@ -5542,6 +5809,35 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)"
5542
5809
  // The close failure was recorded, not silently dropped.
5543
5810
  expect(logs.some((entry) => entry.stream === "stderr" && entry.text.includes('teardown step "runtime-close" failed'))).toBe(true);
5544
5811
  });
5812
+ it("restores managed home and releases the lease when instruction collection rejects", async () => {
5813
+ const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
5814
+ const { paperclipStops, processStops, anyStopped } = stubBridges();
5815
+ const stagingLocks = new Map();
5816
+ const order = [];
5817
+ const logs = [];
5818
+ const execute = createAcpxEngineExecutor({
5819
+ stagingLocks, warmHandles: new Map(), stagedRuntimes: new Map(),
5820
+ prepareRemoteManagedHome: async (input) => ({
5821
+ stagedRuntime: await input.stage([]),
5822
+ teardown: async () => { order.push("restore"); return { ok: true }; },
5823
+ }),
5824
+ createRuntime: () => ({ ensureSession: async () => okHandle, startTurn: () => throwingTurn(),
5825
+ close: async () => { order.push("close"); } }),
5826
+ });
5827
+ const result = await execute({ runId: "instruction-collection-reject",
5828
+ ...remoteArgs(stateDir, localCwd, executionTarget, {
5829
+ onProviderStopped: async () => { order.push("collect"); throw new Error("collector failed reading /private/test-instructions/AGENTS.md"); },
5830
+ onLog: async (_stream, text) => { logs.push(text); },
5831
+ }),
5832
+ });
5833
+ expect(result.exitCode).toBe(1);
5834
+ expect(order).toEqual(["close", "collect", "restore"]);
5835
+ expect(anyStopped(paperclipStops)).toBe(true);
5836
+ expect(anyStopped(processStops)).toBe(true);
5837
+ expect(stagingLocks.size).toBe(0);
5838
+ expect(logs.some(text => text.includes('teardown step "instruction-collection" failed: Instruction collection failed'))).toBe(true);
5839
+ expect(logs.some(text => text.includes("/private/test-instructions"))).toBe(false);
5840
+ });
5545
5841
  it("test_staging_lease_releases_in_finally_on_every_exit_path", async () => {
5546
5842
  const scenarios = [
5547
5843
  {