@stigmer/runner 3.12.4 → 3.12.5

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 (146) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  3. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  4. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  5. package/dist/activities/execute-cursor/index.d.ts +10 -1
  6. package/dist/activities/execute-cursor/index.js +47 -29
  7. package/dist/activities/execute-cursor/index.js.map +1 -1
  8. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  9. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  10. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  11. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  12. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  13. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  14. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  15. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  16. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  17. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  18. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  19. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  20. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  21. package/dist/activities/execute-deep-agent/index.js +57 -5
  22. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  23. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  24. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  25. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/setup.js +17 -16
  27. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  29. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/middleware/path-normalization.d.ts +49 -49
  34. package/dist/middleware/path-normalization.js +73 -84
  35. package/dist/middleware/path-normalization.js.map +1 -1
  36. package/dist/middleware/types.d.ts +3 -3
  37. package/dist/runner-manager.d.ts +1 -1
  38. package/dist/runner-manager.js +22 -15
  39. package/dist/runner-manager.js.map +1 -1
  40. package/dist/runner.js +11 -0
  41. package/dist/runner.js.map +1 -1
  42. package/dist/shared/args-preview.d.ts +8 -0
  43. package/dist/shared/args-preview.js +14 -3
  44. package/dist/shared/args-preview.js.map +1 -1
  45. package/dist/shared/artifact-storage.d.ts +11 -1
  46. package/dist/shared/artifact-storage.js +10 -1
  47. package/dist/shared/artifact-storage.js.map +1 -1
  48. package/dist/shared/declared-preferences.d.ts +47 -0
  49. package/dist/shared/declared-preferences.js +64 -0
  50. package/dist/shared/declared-preferences.js.map +1 -0
  51. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  52. package/dist/shared/mcp-enabled-tools.js +2 -3
  53. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  54. package/dist/shared/mcp-resolver.d.ts +20 -15
  55. package/dist/shared/mcp-resolver.js +11 -12
  56. package/dist/shared/mcp-resolver.js.map +1 -1
  57. package/dist/shared/placeholder-resolver.d.ts +9 -2
  58. package/dist/shared/placeholder-resolver.js +9 -2
  59. package/dist/shared/placeholder-resolver.js.map +1 -1
  60. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  61. package/dist/shared/plan-mode-permissions.js +27 -55
  62. package/dist/shared/plan-mode-permissions.js.map +1 -1
  63. package/dist/shared/skill-mount.d.ts +89 -0
  64. package/dist/shared/skill-mount.js +142 -0
  65. package/dist/shared/skill-mount.js.map +1 -0
  66. package/dist/shared/skill-writer.d.ts +28 -26
  67. package/dist/shared/skill-writer.js +79 -102
  68. package/dist/shared/skill-writer.js.map +1 -1
  69. package/dist/shared/worker-shutdown.d.ts +67 -0
  70. package/dist/shared/worker-shutdown.js +79 -0
  71. package/dist/shared/worker-shutdown.js.map +1 -0
  72. package/dist/shared/workspace/types.d.ts +3 -2
  73. package/dist/workflow-engine/loader.js +38 -1
  74. package/dist/workflow-engine/loader.js.map +1 -1
  75. package/dist/workflow-engine/tasks/human-input.js +23 -2
  76. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  77. package/package.json +15 -11
  78. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  79. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  80. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  81. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  82. package/src/activities/execute-cursor/index.ts +63 -35
  83. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  84. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  85. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  86. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  87. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  88. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  89. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  90. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  91. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  92. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  93. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  94. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  95. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  96. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  97. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  98. package/src/activities/execute-deep-agent/index.ts +57 -5
  99. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  100. package/src/activities/execute-deep-agent/setup.ts +21 -19
  101. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  102. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  103. package/src/index.ts +1 -1
  104. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  105. package/src/middleware/path-normalization.ts +78 -90
  106. package/src/middleware/types.ts +3 -3
  107. package/src/runner-manager.ts +26 -16
  108. package/src/runner.ts +14 -0
  109. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  110. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  111. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  112. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  113. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  114. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  115. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  116. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  117. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  118. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  119. package/src/shared/args-preview.ts +17 -3
  120. package/src/shared/artifact-storage.ts +20 -1
  121. package/src/shared/declared-preferences.ts +84 -0
  122. package/src/shared/mcp-enabled-tools.ts +2 -3
  123. package/src/shared/mcp-resolver.ts +20 -20
  124. package/src/shared/placeholder-resolver.ts +9 -2
  125. package/src/shared/plan-mode-permissions.ts +27 -58
  126. package/src/shared/skill-mount.ts +179 -0
  127. package/src/shared/skill-writer.ts +96 -130
  128. package/src/shared/worker-shutdown.ts +99 -0
  129. package/src/shared/workspace/types.ts +3 -2
  130. package/src/workflow-engine/__tests__/loader.test.ts +51 -0
  131. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
  132. package/src/workflow-engine/loader.ts +46 -1
  133. package/src/workflow-engine/tasks/human-input.ts +33 -5
  134. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  135. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  136. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  137. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  138. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  139. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  140. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  141. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  142. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  143. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  144. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  145. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  146. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -1,17 +1,18 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { ConnectError, Code } from "@connectrpc/connect";
2
6
  import {
3
7
  mergeSkillRefs,
4
8
  fetchSkillsByRefs,
5
- writeSkills,
6
- computeSkillPaths,
7
- checkSkillIntegrity,
9
+ mountSkills,
8
10
  generatePromptSection,
9
11
  generateAlsoAvailableSection,
10
- fetchSkillArtifacts,
11
12
  } from "../skill-writer.js";
12
13
  import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
13
14
  import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
14
- import type { WorkspaceBackend } from "../workspace/types.js";
15
+ import { buildZip } from "../../__test-utils__/zip-fixtures.js";
15
16
 
16
17
  // ─── Helpers ─────────────────────────────────────────────────────────────
17
18
 
@@ -19,16 +20,18 @@ function makeSkill(overrides: {
19
20
  id?: string;
20
21
  name?: string;
21
22
  slug?: string;
23
+ org?: string;
22
24
  description?: string;
23
25
  skillMd?: string;
24
26
  artifactStorageKey?: string;
27
+ versionHash?: string;
25
28
  } = {}): Skill {
26
29
  return {
27
30
  metadata: {
28
31
  id: overrides.id ?? "skill-id-1",
29
32
  name: overrides.name ?? overrides.slug ?? "test-skill",
30
33
  slug: overrides.slug ?? "test-skill",
31
- org: "test-org",
34
+ org: overrides.org ?? "test-org",
32
35
  },
33
36
  spec: {
34
37
  name: overrides.name ?? "test-skill",
@@ -38,7 +41,7 @@ function makeSkill(overrides: {
38
41
  },
39
42
  status: {
40
43
  artifactStorageKey: overrides.artifactStorageKey ?? "",
41
- versionHash: "abc123",
44
+ versionHash: overrides.versionHash ?? "abc123",
42
45
  state: "active",
43
46
  },
44
47
  } as any;
@@ -48,48 +51,21 @@ function makeRef(slug: string, org = "test-org"): ApiResourceReference {
48
51
  return { slug, org, kind: 43 } as any;
49
52
  }
50
53
 
54
+ /** A server that predates the transfer lane (#675) answers the mint RPC
55
+ * with UNIMPLEMENTED, pinning these tests to the unary fallback. */
51
56
  function makeMockClient(overrides: Record<string, unknown> = {}) {
52
57
  return {
53
58
  getSkillByReference: vi.fn().mockImplementation((ref: any) =>
54
59
  Promise.resolve(makeSkill({ slug: ref.slug, name: ref.slug })),
55
60
  ),
56
61
  getSkillArtifact: vi.fn().mockResolvedValue({ artifact: new Uint8Array(0) }),
62
+ getSkillArtifactDownloadUrl: vi.fn().mockRejectedValue(
63
+ new ConnectError("unimplemented", Code.Unimplemented),
64
+ ),
57
65
  ...overrides,
58
66
  } as any;
59
67
  }
60
68
 
61
- function makeMockBackend(): WorkspaceBackend & {
62
- writtenFiles: Map<string, string>;
63
- executedCommands: string[];
64
- existingFiles: Set<string>;
65
- } {
66
- const writtenFiles = new Map<string, string>();
67
- const executedCommands: string[] = [];
68
- const existingFiles = new Set<string>();
69
- return {
70
- rootDir: "/workspace",
71
- writtenFiles,
72
- executedCommands,
73
- existingFiles,
74
- async writeFile(path: string, content: string) {
75
- writtenFiles.set(path, content);
76
- },
77
- async writeFileBuffer(path: string, content: Buffer) {
78
- writtenFiles.set(path, content.toString("utf-8"));
79
- },
80
- async readFile(path: string) {
81
- return writtenFiles.get(path) ?? "";
82
- },
83
- async exists(path: string) {
84
- return writtenFiles.has(path) || existingFiles.has(path);
85
- },
86
- async execute(command: string) {
87
- executedCommands.push(command);
88
- return "";
89
- },
90
- };
91
- }
92
-
93
69
  // ─── mergeSkillRefs ──────────────────────────────────────────────────────
94
70
 
95
71
  describe("mergeSkillRefs", () => {
@@ -151,86 +127,171 @@ describe("fetchSkillsByRefs", () => {
151
127
  });
152
128
  });
153
129
 
154
- // ─── writeSkills ─────────────────────────────────────────────────────────
130
+ // ─── mountSkills ─────────────────────────────────────────────────────────
131
+
132
+ describe("mountSkills", () => {
133
+ let platformDir: string;
134
+
135
+ beforeEach(() => {
136
+ platformDir = mkdtempSync(join(tmpdir(), "skill-writer-platform-"));
137
+ });
155
138
 
156
- describe("writeSkills", () => {
157
- it("writes SKILL.md from spec when no artifact", async () => {
158
- const backend = makeMockBackend();
139
+ afterEach(() => {
140
+ rmSync(platformDir, { recursive: true, force: true });
141
+ });
142
+
143
+ it("writes SKILL.md and returns agent-visible paths keyed by skill id", async () => {
144
+ const client = makeMockClient();
159
145
  const skills = [makeSkill({ name: "calculator", skillMd: "# Calculator\n\nAdds numbers." })];
160
- const result = await writeSkills(skills, backend, new Map());
146
+ const result = await mountSkills(client, skills, platformDir);
161
147
 
162
148
  expect(result.paths.get("skill-id-1")).toBe(".stigmer/skills/calculator");
163
- expect(backend.writtenFiles.has(".stigmer/skills/calculator/SKILL.md")).toBe(true);
164
- expect(backend.writtenFiles.get(".stigmer/skills/calculator/SKILL.md"))
149
+ expect(readFileSync(join(platformDir, "skills", "calculator", "SKILL.md"), "utf-8"))
165
150
  .toBe("# Calculator\n\nAdds numbers.");
166
151
  });
167
152
 
168
- it("runs chmod on script extensions", async () => {
169
- const backend = makeMockBackend();
170
- const skills = [makeSkill({ name: "runner" })];
171
- await writeSkills(skills, backend, new Map());
172
- expect(backend.executedCommands.length).toBeGreaterThan(0);
173
- expect(backend.executedCommands[0]).toContain("chmod");
153
+ it("extracts artifact files alongside SKILL.md", async () => {
154
+ const artifact = buildZip([
155
+ { name: "SKILL.md", content: "# Stale zip copy" },
156
+ { name: "references/schema.md", content: "schema" },
157
+ ]);
158
+ const client = makeMockClient({
159
+ getSkillArtifact: vi.fn().mockResolvedValue({ artifact }),
160
+ });
161
+ const skills = [makeSkill({
162
+ name: "db-skill",
163
+ skillMd: "# Authoritative",
164
+ artifactStorageKey: "artifacts/db.zip",
165
+ })];
166
+ await mountSkills(client, skills, platformDir);
167
+
168
+ const skillDir = join(platformDir, "skills", "db-skill");
169
+ expect(readFileSync(join(skillDir, "SKILL.md"), "utf-8")).toBe("# Authoritative");
170
+ expect(readFileSync(join(skillDir, "references", "schema.md"), "utf-8")).toBe("schema");
174
171
  });
175
172
 
176
173
  it("handles multiple skills", async () => {
177
- const backend = makeMockBackend();
174
+ const client = makeMockClient();
178
175
  const skills = [
179
176
  makeSkill({ id: "id-1", name: "skill-a", skillMd: "# A" }),
180
177
  makeSkill({ id: "id-2", name: "skill-b", skillMd: "# B" }),
181
178
  ];
182
- const result = await writeSkills(skills, backend, new Map());
179
+ const result = await mountSkills(client, skills, platformDir);
183
180
  expect(result.paths.size).toBe(2);
184
181
  expect(result.paths.get("id-1")).toBe(".stigmer/skills/skill-a");
185
182
  expect(result.paths.get("id-2")).toBe(".stigmer/skills/skill-b");
183
+ expect(existsSync(join(platformDir, "skills", "skill-a", "SKILL.md"))).toBe(true);
184
+ expect(existsSync(join(platformDir, "skills", "skill-b", "SKILL.md"))).toBe(true);
186
185
  });
187
186
 
188
- it("skips write when skill has no skillMd and no artifact", async () => {
189
- const backend = makeMockBackend();
190
- const skills = [makeSkill({ skillMd: "" })];
191
- (skills[0] as any).spec.skillMd = "";
192
- await writeSkills(skills, backend, new Map());
193
- expect(backend.writtenFiles.size).toBe(0);
194
- });
195
- });
187
+ it("skips the artifact download when the mounted hash matches (cache hit)", async () => {
188
+ const artifact = buildZip([{ name: "references/guide.md", content: "guide" }]);
189
+ const client = makeMockClient({
190
+ getSkillArtifact: vi.fn().mockResolvedValue({ artifact }),
191
+ });
192
+ const skill = makeSkill({
193
+ name: "cached-skill",
194
+ artifactStorageKey: "artifacts/cached.zip",
195
+ versionHash: "hash-v1",
196
+ });
196
197
 
197
- // ─── computeSkillPaths ───────────────────────────────────────────────────
198
+ await mountSkills(client, [skill], platformDir);
199
+ expect(client.getSkillArtifact).toHaveBeenCalledTimes(1);
198
200
 
199
- describe("computeSkillPaths", () => {
200
- it("computes paths without side effects", () => {
201
- const skills = [
202
- makeSkill({ id: "id-1", name: "calculator" }),
203
- makeSkill({ id: "id-2", name: "web-scraper" }),
204
- ];
205
- const paths = computeSkillPaths(skills);
206
- expect(paths.get("id-1")).toBe(".stigmer/skills/calculator");
207
- expect(paths.get("id-2")).toBe(".stigmer/skills/web-scraper");
201
+ // Second execution of the same session: same hash, no transfer.
202
+ const second = await mountSkills(client, [skill], platformDir);
203
+ expect(client.getSkillArtifact).toHaveBeenCalledTimes(1);
204
+ expect(second.paths.get("skill-id-1")).toBe(".stigmer/skills/cached-skill");
205
+ expect(readFileSync(
206
+ join(platformDir, "skills", "cached-skill", "references", "guide.md"), "utf-8",
207
+ )).toBe("guide");
208
208
  });
209
209
 
210
- it("returns empty map for empty skills", () => {
211
- expect(computeSkillPaths([])).toEqual(new Map());
210
+ it("remounts on version change and clears stale files from the old mount", async () => {
211
+ const artifactV1 = buildZip([{ name: "references/removed-in-v2.md", content: "old" }]);
212
+ const artifactV2 = buildZip([{ name: "references/new-in-v2.md", content: "new" }]);
213
+ const client = makeMockClient({
214
+ getSkillArtifact: vi.fn()
215
+ .mockResolvedValueOnce({ artifact: artifactV1 })
216
+ .mockResolvedValueOnce({ artifact: artifactV2 }),
217
+ });
218
+
219
+ await mountSkills(client, [makeSkill({
220
+ name: "evolving-skill",
221
+ skillMd: "# V1",
222
+ artifactStorageKey: "artifacts/v1.zip",
223
+ versionHash: "hash-v1",
224
+ })], platformDir);
225
+
226
+ await mountSkills(client, [makeSkill({
227
+ name: "evolving-skill",
228
+ skillMd: "# V2",
229
+ artifactStorageKey: "artifacts/v2.zip",
230
+ versionHash: "hash-v2",
231
+ })], platformDir);
232
+
233
+ const skillDir = join(platformDir, "skills", "evolving-skill");
234
+ expect(client.getSkillArtifact).toHaveBeenCalledTimes(2);
235
+ expect(readFileSync(join(skillDir, "SKILL.md"), "utf-8")).toBe("# V2");
236
+ expect(readFileSync(join(skillDir, "references", "new-in-v2.md"), "utf-8")).toBe("new");
237
+ // The v1-only file must not linger in the v2 mount (the stale-file leak).
238
+ expect(existsSync(join(skillDir, "references", "removed-in-v2.md"))).toBe(false);
212
239
  });
213
- });
214
240
 
215
- // ─── checkSkillIntegrity ─────────────────────────────────────────────────
241
+ it("does not cache a degraded SKILL.md-only mount — the next execution retries", async () => {
242
+ const artifact = buildZip([{ name: "references/late.md", content: "finally" }]);
243
+ const client = makeMockClient({
244
+ getSkillArtifact: vi.fn()
245
+ .mockRejectedValueOnce(new Error("Network timeout"))
246
+ .mockResolvedValueOnce({ artifact }),
247
+ });
248
+ const skill = makeSkill({
249
+ name: "retry-skill",
250
+ artifactStorageKey: "artifacts/retry.zip",
251
+ versionHash: "hash-v1",
252
+ });
216
253
 
217
- describe("checkSkillIntegrity", () => {
218
- it("returns true when sentinel exists", async () => {
219
- const backend = makeMockBackend();
220
- backend.existingFiles.add(".stigmer/skills/calculator/SKILL.md");
221
- const skills = [makeSkill({ id: "id-1", name: "calculator" })];
222
- expect(await checkSkillIntegrity(skills, backend)).toBe(true);
254
+ // First pass degrades to SKILL.md only — never throws.
255
+ const first = await mountSkills(client, [skill], platformDir);
256
+ const skillDir = join(platformDir, "skills", "retry-skill");
257
+ expect(first.paths.get("skill-id-1")).toBe(".stigmer/skills/retry-skill");
258
+ expect(existsSync(join(skillDir, "SKILL.md"))).toBe(true);
259
+ expect(existsSync(join(skillDir, "references"))).toBe(false);
260
+
261
+ // Second pass retries and completes the mount.
262
+ await mountSkills(client, [skill], platformDir);
263
+ expect(client.getSkillArtifact).toHaveBeenCalledTimes(2);
264
+ expect(readFileSync(join(skillDir, "references", "late.md"), "utf-8")).toBe("finally");
265
+
266
+ // Third pass is a cache hit.
267
+ await mountSkills(client, [skill], platformDir);
268
+ expect(client.getSkillArtifact).toHaveBeenCalledTimes(2);
223
269
  });
224
270
 
225
- it("returns false when sentinel is missing", async () => {
226
- const backend = makeMockBackend();
227
- const skills = [makeSkill({ id: "id-1", name: "calculator" })];
228
- expect(await checkSkillIntegrity(skills, backend)).toBe(false);
271
+ it("mounts only the first claimant when two skills resolve to the same directory", async () => {
272
+ const client = makeMockClient();
273
+ const skills = [
274
+ makeSkill({ id: "id-1", org: "org-a", slug: "shared-a", name: "shared-name", skillMd: "# First" }),
275
+ makeSkill({ id: "id-2", org: "org-b", slug: "shared-b", name: "shared-name", skillMd: "# Second" }),
276
+ ];
277
+ const result = await mountSkills(client, skills, platformDir);
278
+
279
+ // Both ids resolve to the (single) directory; the first claimant's content wins.
280
+ expect(result.paths.get("id-1")).toBe(".stigmer/skills/shared-name");
281
+ expect(result.paths.get("id-2")).toBe(".stigmer/skills/shared-name");
282
+ expect(readFileSync(join(platformDir, "skills", "shared-name", "SKILL.md"), "utf-8"))
283
+ .toBe("# First");
229
284
  });
230
285
 
231
- it("returns true for empty skills list", async () => {
232
- const backend = makeMockBackend();
233
- expect(await checkSkillIntegrity([], backend)).toBe(true);
286
+ it("skips skills with no skillMd content", async () => {
287
+ const client = makeMockClient();
288
+ const skill = makeSkill({ name: "broken", skillMd: "" });
289
+ (skill as any).spec.skillMd = "";
290
+ const result = await mountSkills(client, [skill], platformDir);
291
+
292
+ // The path entry exists (naming is total) but nothing was mounted.
293
+ expect(result.paths.get("skill-id-1")).toBe(".stigmer/skills/broken");
294
+ expect(existsSync(join(platformDir, "skills", "broken"))).toBe(false);
234
295
  });
235
296
  });
236
297
 
@@ -309,41 +370,3 @@ describe("generateAlsoAvailableSection", () => {
309
370
  expect(section).toContain("relevant to your task");
310
371
  });
311
372
  });
312
-
313
- // ─── fetchSkillArtifacts ─────────────────────────────────────────────────
314
-
315
- describe("fetchSkillArtifacts", () => {
316
- it("returns empty map when no skills have artifacts", async () => {
317
- const client = makeMockClient();
318
- const skills = [makeSkill({ artifactStorageKey: "" })];
319
- const result = await fetchSkillArtifacts(client, skills);
320
- expect(result.size).toBe(0);
321
- expect(client.getSkillArtifact).not.toHaveBeenCalled();
322
- });
323
-
324
- it("fetches artifacts for skills with storage keys", async () => {
325
- const artifactData = new Uint8Array([80, 75, 3, 4]); // ZIP magic bytes
326
- const client = makeMockClient({
327
- getSkillArtifact: vi.fn().mockResolvedValue({ artifact: artifactData }),
328
- });
329
- const skills = [makeSkill({ id: "id-1", artifactStorageKey: "artifacts/key.zip" })];
330
- const result = await fetchSkillArtifacts(client, skills);
331
- expect(result.size).toBe(1);
332
- expect(result.get("id-1")).toEqual(artifactData);
333
- });
334
-
335
- it("handles partial failures gracefully", async () => {
336
- const client = makeMockClient({
337
- getSkillArtifact: vi.fn()
338
- .mockResolvedValueOnce({ artifact: new Uint8Array([1, 2, 3]) })
339
- .mockRejectedValueOnce(new Error("Network error")),
340
- });
341
- const skills = [
342
- makeSkill({ id: "id-1", name: "good", artifactStorageKey: "key1" }),
343
- makeSkill({ id: "id-2", name: "bad", artifactStorageKey: "key2" }),
344
- ];
345
- const result = await fetchSkillArtifacts(client, skills);
346
- expect(result.size).toBe(1);
347
- expect(result.has("id-1")).toBe(true);
348
- });
349
- });
@@ -123,9 +123,9 @@ describe("buildChatModel vertex adapter", () => {
123
123
  // default applies. That default prefix-matches the model string — and the
124
124
  // vertex branch hands ChatAnthropic the TRANSLATED string. This pins the
125
125
  // equality the zero-behavior-change criterion rests on (verified 16384 ==
126
- // 16384 for 4.5-generation ids at @langchain/anthropic 1.4.0); a future
127
- // bump that diverges the two forms fails here instead of silently capping
128
- // vertex deployments differently from public ones.
126
+ // 16384 for 4.5-generation ids at @langchain/anthropic 1.4.0, re-verified
127
+ // at 1.5.5); a future bump that diverges the two forms fails here instead
128
+ // of silently capping vertex deployments differently from public ones.
129
129
  for (const canonical of [
130
130
  "claude-sonnet-4-5-20250929",
131
131
  "claude-haiku-4-5-20251001",
@@ -137,6 +137,33 @@ describe("buildChatModel vertex adapter", () => {
137
137
  }
138
138
  });
139
139
 
140
+ it("model.profile goes empty exactly when translation rewrites the id", () => {
141
+ // ChatAnthropic.profile resolves per model name, and — unlike the
142
+ // maxTokens default above — its lookup does NOT prefix-match: a dated
143
+ // id rewritten to Vertex's `@` form resolves {} (verified at
144
+ // @langchain/anthropic 1.4.0 and again at 1.5.5), while undated ids
145
+ // pass through toVertexModelId unchanged and keep their full profile.
146
+ // Nothing in the runner consumes profile directly, but deepagents'
147
+ // SummarizationMiddleware reads profile.maxInputTokens when present, so
148
+ // vertex deployments pinned to DATED ids summarize on its fallback
149
+ // budget rather than the model's real window. This pins both halves of
150
+ // that contract: if a bump starts resolving `@`-form ids (a welcome
151
+ // improvement — those deployments' summarization budgets would change),
152
+ // or stops resolving canonical ids, this fails and the behavior gets
153
+ // re-decided consciously instead of shifting silently.
154
+ for (const dated of ["claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"]) {
155
+ const publicModel = new ChatAnthropic({ model: dated, apiKey: "probe" });
156
+ const vertexModel = new ChatAnthropic({ model: toVertexModelId(dated), apiKey: "probe" });
157
+ expect(publicModel.profile.maxInputTokens, dated).toBeGreaterThan(0);
158
+ expect(vertexModel.profile, dated).toEqual({});
159
+ }
160
+ const undated = "claude-sonnet-4-6";
161
+ expect(toVertexModelId(undated)).toBe(undated);
162
+ expect(
163
+ new ChatAnthropic({ model: undated, apiKey: "probe" }).profile.maxInputTokens,
164
+ ).toBeGreaterThan(0);
165
+ });
166
+
140
167
  it("forwards the request timeout to the SDK client (STIGMER_LLM_REQUEST_TIMEOUT_MS path)", async () => {
141
168
  // The timeout rides clientOptions -> factory options -> SDK constructor.
142
169
  // A factory that drops it silently unbounds the operator's timeout on
@@ -4,13 +4,13 @@
4
4
  *
5
5
  * This is NOT a unit test of our code (there is no production vertex code
6
6
  * yet). It pins the exact cross-package behavior production will rely on:
7
- * the REAL `ChatAnthropic` (@langchain/anthropic, bundling @anthropic-ai/sdk
8
- * 0.95.x) driving the REAL `AnthropicVertex` client (@anthropic-ai/vertex-sdk,
9
- * bundling its own nested @anthropic-ai/sdk >=0.115). If a future bump of
7
+ * the REAL `ChatAnthropic` (@langchain/anthropic) driving the REAL
8
+ * `AnthropicVertex` client (@anthropic-ai/vertex-sdk), both resolving the
9
+ * single override-pinned @anthropic-ai/sdk copy. If a future bump of
10
10
  * either side changes request shaping, streaming event handling, tool-call
11
11
  * assembly, or usage accounting across this seam, this suite fails in CI
12
- * instead of production. See scripts/check-langchain-deps.sh for why two
13
- * @anthropic-ai/sdk copies coexist and when they collapse to one.
12
+ * instead of production. See scripts/check-langchain-deps.sh for the
13
+ * override rationale and the single-copy invariant it guards.
14
14
  *
15
15
  * Determinism: zero credentials, zero network. Google auth is bypassed by
16
16
  * injecting a fake `authClient` (the SDK's supported constructor option —
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ classifyTurnInterruption,
4
+ getShutdownSignalForQueue,
5
+ registerWorkerShutdownSignal,
6
+ signalWorkerShutdown,
7
+ unregisterWorkerShutdownSignal,
8
+ } from "../worker-shutdown.js";
9
+
10
+ describe("worker-shutdown signal registry", () => {
11
+ it("registers a live signal an activity can resolve by queue name", () => {
12
+ const controller = registerWorkerShutdownSignal("session:reg-1");
13
+ const signal = getShutdownSignalForQueue("session:reg-1");
14
+ expect(signal).toBe(controller.signal);
15
+ expect(signal?.aborted).toBe(false);
16
+ unregisterWorkerShutdownSignal("session:reg-1");
17
+ });
18
+
19
+ it("aborting via signalWorkerShutdown marks the queue's signal (idempotent, unknown queues are no-ops)", () => {
20
+ registerWorkerShutdownSignal("session:abort-1");
21
+ signalWorkerShutdown("session:abort-1");
22
+ signalWorkerShutdown("session:abort-1");
23
+ expect(getShutdownSignalForQueue("session:abort-1")?.aborted).toBe(true);
24
+ expect(() => signalWorkerShutdown("session:never-registered")).not.toThrow();
25
+ unregisterWorkerShutdownSignal("session:abort-1");
26
+ });
27
+
28
+ it("re-registering a reused queue replaces an aborted signal with a fresh one", () => {
29
+ // A re-created worker on a reused queue (desktop re-opens a session) must
30
+ // not observe the previous worker's aborted signal as its own shutdown.
31
+ const first = registerWorkerShutdownSignal("session:reuse-1");
32
+ first.abort();
33
+ const second = registerWorkerShutdownSignal("session:reuse-1");
34
+ expect(getShutdownSignalForQueue("session:reuse-1")).toBe(second.signal);
35
+ expect(getShutdownSignalForQueue("session:reuse-1")?.aborted).toBe(false);
36
+ unregisterWorkerShutdownSignal("session:reuse-1");
37
+ });
38
+
39
+ it("unregistering removes the signal", () => {
40
+ registerWorkerShutdownSignal("session:gone-1");
41
+ unregisterWorkerShutdownSignal("session:gone-1");
42
+ expect(getShutdownSignalForQueue("session:gone-1")).toBeUndefined();
43
+ });
44
+ });
45
+
46
+ describe("classifyTurnInterruption", () => {
47
+ const none = {
48
+ heartbeatCancelled: false,
49
+ heartbeatWorkerShutdown: false,
50
+ cancellationSignalAborted: false,
51
+ shutdownSignalAborted: false,
52
+ };
53
+
54
+ it("an uninterrupted turn is none", () => {
55
+ expect(classifyTurnInterruption(none)).toBe("none");
56
+ });
57
+
58
+ it("GRACE-WINDOW GUARD (#776): an aborted shutdown signal alone stays none — a run that completed inside the drain grace window must not be failed", () => {
59
+ expect(
60
+ classifyTurnInterruption({ ...none, shutdownSignalAborted: true }),
61
+ ).toBe("none");
62
+ });
63
+
64
+ it("heartbeat cancellation without a shutdown signal is the orchestrator's pause", () => {
65
+ expect(
66
+ classifyTurnInterruption({ ...none, heartbeatCancelled: true }),
67
+ ).toBe("pause");
68
+ });
69
+
70
+ it("heartbeat cancellation WITH the shutdown signal aborted is a worker shutdown, not a pause (the #776 misclassification)", () => {
71
+ expect(
72
+ classifyTurnInterruption({
73
+ ...none,
74
+ heartbeatCancelled: true,
75
+ shutdownSignalAborted: true,
76
+ }),
77
+ ).toBe("worker-shutdown");
78
+ });
79
+
80
+ it("the heartbeat's own workerShutdown flag classifies directly", () => {
81
+ expect(
82
+ classifyTurnInterruption({ ...none, heartbeatWorkerShutdown: true }),
83
+ ).toBe("worker-shutdown");
84
+ });
85
+
86
+ it("a delivered cancellation with the shutdown signal aborted is a worker shutdown (signal races the heartbeat tick)", () => {
87
+ expect(
88
+ classifyTurnInterruption({
89
+ ...none,
90
+ cancellationSignalAborted: true,
91
+ shutdownSignalAborted: true,
92
+ }),
93
+ ).toBe("worker-shutdown");
94
+ });
95
+
96
+ it("a delivered cancellation alone (heartbeat timeout / infra cancel) is neither shutdown nor pause", () => {
97
+ expect(
98
+ classifyTurnInterruption({ ...none, cancellationSignalAborted: true }),
99
+ ).toBe("none");
100
+ });
101
+ });
@@ -34,11 +34,25 @@ export const MAX_ARGS_PREVIEW_LENGTH = 500;
34
34
  * Cursor gate path must instead use {@link buildElidedArgsPreview}, which keeps
35
35
  * the JSON valid and preserves the salient identity fields.
36
36
  */
37
- export function sanitizeArgsPreview(args: Record<string, unknown>): string {
38
- const sanitized: Record<string, unknown> = {};
37
+ /**
38
+ * Redact secret-keyed values (see {@link SENSITIVE_ARG_KEYS}), preserving every
39
+ * other entry verbatim. The shared first step of both preview builders, and the
40
+ * shape stamped as `args` on interrupt-placeholder tool calls (issue #754's
41
+ * header fix): full enough for the UI's path/primary-arg extraction, never
42
+ * carrying a secret value.
43
+ */
44
+ export function redactSensitiveArgs(
45
+ args: Record<string, unknown>,
46
+ ): Record<string, unknown> {
47
+ const redacted: Record<string, unknown> = {};
39
48
  for (const [key, value] of Object.entries(args)) {
40
- sanitized[key] = SENSITIVE_ARG_KEYS.has(key.toLowerCase()) ? "[REDACTED]" : value;
49
+ redacted[key] = SENSITIVE_ARG_KEYS.has(key.toLowerCase()) ? "[REDACTED]" : value;
41
50
  }
51
+ return redacted;
52
+ }
53
+
54
+ export function sanitizeArgsPreview(args: Record<string, unknown>): string {
55
+ const sanitized = redactSensitiveArgs(args);
42
56
  try {
43
57
  const json = JSON.stringify(sanitized);
44
58
  return json.length > MAX_ARGS_PREVIEW_LENGTH
@@ -56,7 +56,17 @@ export interface ArtifactStorage {
56
56
  exists(key: string): Promise<boolean>;
57
57
  }
58
58
 
59
- export type ArtifactStorageType = "local" | "proxy";
59
+ /**
60
+ * "none" is the explicit no-store posture: capture degrades to the deny-gate
61
+ * (file writes gated pre-execution, DD-22/DD-26) and offload is disabled —
62
+ * the same first-class absent-store state a proxy misconfig or unwritable
63
+ * local path resolves to, but chosen deliberately (ARTIFACT_STORAGE_TYPE=none)
64
+ * instead of reached by failure. The e2e file-gate stack boots with it, and
65
+ * an operator can use it to force gate-everything behavior. It is only valid
66
+ * where an absent store is: claimcheck (fail-hard by contract) refuses it at
67
+ * construction.
68
+ */
69
+ export type ArtifactStorageType = "local" | "proxy" | "none";
60
70
 
61
71
  // ── Local Backend ────────────────────────────────────────────────────
62
72
 
@@ -371,6 +381,7 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
371
381
  const type: ArtifactStorageType =
372
382
  envType === "proxy" ? "proxy" :
373
383
  envType === "local" ? "local" :
384
+ envType === "none" ? "none" :
374
385
  config.proxyEndpoint ? "proxy" : "local";
375
386
 
376
387
  return {
@@ -387,6 +398,14 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
387
398
  }
388
399
 
389
400
  export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStorage {
401
+ if (cfg.type === "none") {
402
+ // Deliberate no-store posture. Throwing keeps both consumers honest:
403
+ // resolveUsableArtifactStorage catches and degrades to the deny-gate
404
+ // (the intent of "none"), while claimcheck's direct fail-hard call
405
+ // surfaces the config contradiction at boot instead of silently
406
+ // dropping Temporal payloads.
407
+ throw new Error("ARTIFACT_STORAGE_TYPE=none: artifact storage deliberately disabled");
408
+ }
390
409
  if (cfg.type === "proxy") {
391
410
  if (!cfg.proxyEndpoint) {
392
411
  throw new Error("Proxy artifact storage requires STIGMER_PROXY_ENDPOINT");