@stigmer/runner 3.40.0 → 3.41.0

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 (278) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  34. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  35. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  37. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  38. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  40. package/dist/activities/execute-deep-agent/translator.js +5 -4
  41. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  43. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  45. package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
  46. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  48. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  49. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  51. package/dist/activities/execute-deep-agent/turn.js +15 -6
  52. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  53. package/dist/activities/workflow-event-activities.js +2 -0
  54. package/dist/activities/workflow-event-activities.js.map +1 -1
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +2 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/exit-backstop.d.ts +64 -0
  59. package/dist/exit-backstop.js +79 -0
  60. package/dist/exit-backstop.js.map +1 -0
  61. package/dist/harness/terminal-table.d.ts +12 -5
  62. package/dist/harness/terminal-table.js +13 -13
  63. package/dist/harness/terminal-table.js.map +1 -1
  64. package/dist/harness/transcript/builder.d.ts +21 -0
  65. package/dist/harness/transcript/builder.js +53 -31
  66. package/dist/harness/transcript/builder.js.map +1 -1
  67. package/dist/harness/transcript/events.d.ts +22 -26
  68. package/dist/harness/transcript/events.js +6 -5
  69. package/dist/harness/transcript/events.js.map +1 -1
  70. package/dist/harness/turn-context.d.ts +3 -1
  71. package/dist/harness/turn-context.js +5 -3
  72. package/dist/harness/turn-context.js.map +1 -1
  73. package/dist/main.js +10 -4
  74. package/dist/main.js.map +1 -1
  75. package/dist/middleware/cost-advisory.d.ts +6 -3
  76. package/dist/middleware/cost-advisory.js +6 -3
  77. package/dist/middleware/cost-advisory.js.map +1 -1
  78. package/dist/middleware/execution-budget.d.ts +22 -7
  79. package/dist/middleware/execution-budget.js +57 -35
  80. package/dist/middleware/execution-budget.js.map +1 -1
  81. package/dist/middleware/index.d.ts +10 -7
  82. package/dist/middleware/index.js +10 -7
  83. package/dist/middleware/index.js.map +1 -1
  84. package/dist/middleware/types.d.ts +6 -1
  85. package/dist/runner-manager.js +3 -2
  86. package/dist/runner-manager.js.map +1 -1
  87. package/dist/runner.d.ts +25 -0
  88. package/dist/runner.js +41 -14
  89. package/dist/runner.js.map +1 -1
  90. package/dist/shared/approval-policy.d.ts +23 -13
  91. package/dist/shared/approval-policy.js +47 -14
  92. package/dist/shared/approval-policy.js.map +1 -1
  93. package/dist/shared/caller-identity.d.ts +4 -5
  94. package/dist/shared/caller-identity.js +4 -5
  95. package/dist/shared/caller-identity.js.map +1 -1
  96. package/dist/shared/channel-attachment.js +3 -2
  97. package/dist/shared/channel-attachment.js.map +1 -1
  98. package/dist/shared/connect-backfill.d.ts +2 -1
  99. package/dist/shared/connect-backfill.js +2 -2
  100. package/dist/shared/connect-backfill.js.map +1 -1
  101. package/dist/shared/mcp-resolver.d.ts +10 -1
  102. package/dist/shared/mcp-resolver.js +11 -2
  103. package/dist/shared/mcp-resolver.js.map +1 -1
  104. package/dist/shared/memory-attachment.js +3 -2
  105. package/dist/shared/memory-attachment.js.map +1 -1
  106. package/dist/shared/model-error.d.ts +25 -3
  107. package/dist/shared/model-error.js +78 -10
  108. package/dist/shared/model-error.js.map +1 -1
  109. package/dist/shared/platform-server-address.d.ts +71 -0
  110. package/dist/shared/platform-server-address.js +103 -0
  111. package/dist/shared/platform-server-address.js.map +1 -0
  112. package/dist/shared/prompt-sections.d.ts +6 -28
  113. package/dist/shared/prompt-sections.js +6 -35
  114. package/dist/shared/prompt-sections.js.map +1 -1
  115. package/dist/shared/skill-resolver.d.ts +2 -3
  116. package/dist/shared/skill-resolver.js +2 -3
  117. package/dist/shared/skill-resolver.js.map +1 -1
  118. package/dist/shared/synthesized-attachment.d.ts +0 -5
  119. package/dist/shared/synthesized-attachment.js +0 -12
  120. package/dist/shared/synthesized-attachment.js.map +1 -1
  121. package/dist/shared/tool-rounds.d.ts +51 -23
  122. package/dist/shared/tool-rounds.js +67 -24
  123. package/dist/shared/tool-rounds.js.map +1 -1
  124. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  125. package/dist/shared/workspace/platform-dir.js +18 -1
  126. package/dist/shared/workspace/platform-dir.js.map +1 -1
  127. package/dist/shared/workspace/sources/git.js +13 -6
  128. package/dist/shared/workspace/sources/git.js.map +1 -1
  129. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  130. package/dist/shared/workspace/stigmer-link.js +91 -29
  131. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  132. package/dist/tools/url-guard.d.ts +5 -1
  133. package/dist/tools/url-guard.js +9 -2
  134. package/dist/tools/url-guard.js.map +1 -1
  135. package/dist/worker.d.ts +11 -2
  136. package/dist/worker.js +1 -1
  137. package/dist/worker.js.map +1 -1
  138. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  139. package/dist/workflow-engine/tasks/human-input.js +79 -37
  140. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +14 -4
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  144. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  145. package/dist/workflows/engine-core.d.ts +25 -0
  146. package/dist/workflows/engine-core.js.map +1 -1
  147. package/dist/workflows/execute-from-execution.d.ts +3 -0
  148. package/dist/workflows/execute-from-execution.js.map +1 -1
  149. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  150. package/dist/workflows/human-input-orchestrator.js +12 -44
  151. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  152. package/package.json +5 -5
  153. package/src/__test-utils__/config-fixture.ts +1 -0
  154. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  155. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  156. package/src/__test-utils__/test-gate.ts +8 -0
  157. package/src/__tests__/exit-backstop.test.ts +91 -0
  158. package/src/__tests__/golden-e2e.test.ts +80 -62
  159. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  160. package/src/__tests__/runner.test.ts +60 -2
  161. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  162. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  163. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  164. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  165. package/src/activities/call-agent.ts +1 -1
  166. package/src/activities/discover-mcp-server.ts +27 -37
  167. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  168. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  169. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  170. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  171. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  172. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  173. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  174. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  175. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  176. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  177. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  178. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  179. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  180. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  181. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  182. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  183. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  184. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  185. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  186. package/src/activities/execute-cursor/translator.ts +15 -32
  187. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  188. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  189. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  190. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  191. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  192. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  193. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  194. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  195. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  196. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  197. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  198. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  199. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  200. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  201. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  202. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  203. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  204. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  205. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  206. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  207. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  208. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  209. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  210. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  211. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  212. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  213. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  214. package/src/activities/execute-deep-agent/translator.ts +5 -4
  215. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  216. package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
  217. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  218. package/src/activities/execute-deep-agent/turn.ts +16 -7
  219. package/src/activities/workflow-event-activities.ts +2 -0
  220. package/src/config.ts +11 -0
  221. package/src/exit-backstop.ts +97 -0
  222. package/src/harness/README.md +5 -5
  223. package/src/harness/terminal-table.ts +13 -13
  224. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  225. package/src/harness/transcript/builder.ts +53 -30
  226. package/src/harness/transcript/events.ts +22 -27
  227. package/src/harness/turn-context.ts +5 -3
  228. package/src/main.ts +9 -5
  229. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  230. package/src/middleware/cost-advisory.ts +6 -3
  231. package/src/middleware/execution-budget.ts +60 -34
  232. package/src/middleware/index.ts +10 -7
  233. package/src/middleware/types.ts +6 -1
  234. package/src/runner-manager.ts +3 -2
  235. package/src/runner.ts +57 -15
  236. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  237. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  238. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  239. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  240. package/src/shared/__tests__/model-error.test.ts +71 -3
  241. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  242. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  243. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  244. package/src/shared/approval-policy.ts +57 -14
  245. package/src/shared/caller-identity.ts +4 -5
  246. package/src/shared/channel-attachment.ts +2 -2
  247. package/src/shared/connect-backfill.ts +5 -1
  248. package/src/shared/mcp-resolver.ts +11 -1
  249. package/src/shared/memory-attachment.ts +2 -2
  250. package/src/shared/model-error.ts +87 -9
  251. package/src/shared/platform-server-address.ts +123 -0
  252. package/src/shared/prompt-sections.ts +6 -55
  253. package/src/shared/skill-resolver.ts +2 -3
  254. package/src/shared/synthesized-attachment.ts +0 -12
  255. package/src/shared/tool-rounds.ts +70 -24
  256. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  257. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  258. package/src/shared/workspace/platform-dir.ts +19 -1
  259. package/src/shared/workspace/sources/git.ts +14 -8
  260. package/src/shared/workspace/stigmer-link.ts +99 -29
  261. package/src/tools/__tests__/url-guard.test.ts +19 -1
  262. package/src/tools/url-guard.ts +9 -1
  263. package/src/worker.ts +12 -2
  264. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  265. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  266. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  267. package/src/workflow-engine/tasks/human-input.ts +92 -42
  268. package/src/workflow-engine/types.ts +16 -4
  269. package/src/workflows/call-agent-orchestrator.ts +7 -0
  270. package/src/workflows/engine-core.ts +18 -0
  271. package/src/workflows/execute-from-execution.ts +2 -1
  272. package/src/workflows/human-input-orchestrator.ts +15 -56
  273. package/dist/shared/skill-relevance.d.ts +0 -65
  274. package/dist/shared/skill-relevance.js +0 -175
  275. package/dist/shared/skill-relevance.js.map +0 -1
  276. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  277. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  278. package/src/shared/skill-relevance.ts +0 -222
@@ -45,7 +45,7 @@ import type { WorkspaceBackend } from "../../../shared/workspace/types.js";
45
45
  import type { ModelPricing } from "../../../shared/model-pricing.js";
46
46
  import { CasCaptureObserver } from "../cas-capture-observer.js";
47
47
  import type { DeepAgentEngine, DeepAgentGateState, DeepAgentGraphInput, DeepAgentWorkspace } from "../turn-setup.js";
48
- import { consumeDeepAgentStream, createDeepAgentTranscript, type StreamableRun } from "../turn-stream.js";
48
+ import { consumeDeepAgentStream, createDeepAgentTranscript, type DeepAgentTranscript, type StreamableRun } from "../turn-stream.js";
49
49
  import type { V3ProtocolEvent } from "../v3-event-recorder.js";
50
50
  import {
51
51
  makeMessageFinish,
@@ -129,6 +129,8 @@ interface Harness {
129
129
  readonly engine: DeepAgentEngine;
130
130
  readonly streamEvents: ReturnType<typeof vi.fn>;
131
131
  readonly blobs: Map<string, Buffer>;
132
+ /** The transcript the last `run` streamed into; its side effects hold the turn's publishes. */
133
+ readonly transcript: () => DeepAgentTranscript;
132
134
  run(run: StreamableRun): ReturnType<typeof consumeDeepAgentStream>;
133
135
  }
134
136
 
@@ -150,14 +152,19 @@ function harness(options: { gate?: DeepAgentGateState; files?: Record<string, st
150
152
  };
151
153
  const workspace = workspaceOver(options.files ?? {});
152
154
  const graphInput: DeepAgentGraphInput = { kind: "message", input: { messages: [{ role: "user", content: "Say hello." }] } };
155
+ let transcript: DeepAgentTranscript | undefined;
153
156
  return {
154
157
  sink,
155
158
  engine,
156
159
  streamEvents,
157
160
  blobs,
161
+ transcript: () => {
162
+ if (transcript === undefined) throw new Error("harness: no run yet");
163
+ return transcript;
164
+ },
158
165
  run(run) {
159
166
  streamEvents.mockResolvedValue(run);
160
- const transcript = createDeepAgentTranscript(input, sink, engine, workspace);
167
+ transcript = createDeepAgentTranscript(input, sink, engine, workspace);
161
168
  return consumeDeepAgentStream({ input, sink, engine, graphInput, transcript });
162
169
  },
163
170
  };
@@ -245,7 +252,7 @@ describe("consumeDeepAgentStream — what each event becomes", () => {
245
252
 
246
253
  it("publishes a file-modifying tool's target as an artifact on the status when the call finishes", async () => {
247
254
  const h = harness({ files: { "src/main.ts": "console.log('hi');" } });
248
- const result = await h.run(
255
+ await h.run(
249
256
  scriptedRun([
250
257
  ...textTurn("run-1", "Writing."),
251
258
  makeToolStarted("toolu_w", "write_file", { file_path: "src/main.ts", content: "console.log('hi');" }),
@@ -253,8 +260,9 @@ describe("consumeDeepAgentStream — what each event becomes", () => {
253
260
  ]),
254
261
  );
255
262
 
256
- expect(result.pendingPublishPromises, "one publish fired mid-turn, for the settle to drain").toHaveLength(1);
257
- await Promise.all(result.pendingPublishPromises);
263
+ const { sideEffects } = h.transcript();
264
+ expect(sideEffects.pendingPublishPromises, "one publish fired mid-turn, held by the transcript for the drain").toHaveLength(1);
265
+ await sideEffects.drainPublishes();
258
266
  expect(h.sink.status.artifacts.map((a) => a.sandboxPath)).toEqual(["src/main.ts"]);
259
267
  expect([...h.blobs.keys()], "the bytes were uploaded under the execution's artifact key").toEqual(["artifacts/aex_fixture_0001/main.ts"]);
260
268
  });
@@ -8,6 +8,15 @@
8
8
  * Designed as a fire-and-forget callback: exceptions are logged and
9
9
  * swallowed so the streaming loop is never interrupted.
10
10
  *
11
+ * A file is uploaded only when its bytes differ from the row the execution's
12
+ * status already lists for its path. The status is the one home of that fact:
13
+ * the builder writes every row into it, and a reinvocation's seed carries the
14
+ * earlier turns' rows onto it before this publisher exists. So a completed
15
+ * turn after a gate, whose safety net walks the seeded history again, re-reads
16
+ * each earlier file but uploads none that did not change (stigmer/stigmer#1448).
17
+ * The re-read is kept on purpose: a shell command may have rewritten an earlier
18
+ * turn's file, and only its bytes can say so.
19
+ *
11
20
  * DD-7: No skill-aware directory publishing. Individual files only.
12
21
  */
13
22
 
@@ -35,6 +44,16 @@ export interface ArtifactSink {
35
44
  addArtifact(artifact: ExecutionArtifact): void;
36
45
  }
37
46
 
47
+ /**
48
+ * The execution status's artifact rows, read at every publish: the status the
49
+ * {@link ArtifactSink} builds into (`TurnSink.status` in production), so a row
50
+ * registered here or seeded by a reinvocation is visible on the next check.
51
+ * Read, never copied, because the builder keeps writing it.
52
+ */
53
+ export interface ArtifactRecord {
54
+ readonly artifacts: readonly ExecutionArtifact[];
55
+ }
56
+
38
57
  export class InlinePublisher {
39
58
  private readonly workspaceBackend: WorkspaceBackend;
40
59
  /**
@@ -45,33 +64,46 @@ export class InlinePublisher {
45
64
  */
46
65
  private readonly artifactStorage: ArtifactStorage | undefined;
47
66
  private readonly artifacts: ArtifactSink;
67
+ private readonly record: ArtifactRecord;
48
68
  private readonly executionId: string;
49
69
 
50
- /** Tracks (sandboxPath -> contentHash) for deduplication. */
51
- private readonly published = new Map<string, string>();
70
+ /**
71
+ * Paths this publisher brought up to date in this turn, uploaded or found
72
+ * unchanged, so the post-stream safety net does not read them again. Per
73
+ * turn by design: an earlier turn's path is absent until this turn checks
74
+ * it, so a file rewritten outside a write tool is still caught.
75
+ */
76
+ private readonly published = new Set<string>();
52
77
 
53
78
  constructor(opts: {
54
79
  workspaceBackend: WorkspaceBackend;
55
80
  artifactStorage: ArtifactStorage | undefined;
56
81
  artifacts: ArtifactSink;
82
+ record: ArtifactRecord;
57
83
  executionId: string;
58
84
  }) {
59
85
  this.workspaceBackend = opts.workspaceBackend;
60
86
  this.artifactStorage = opts.artifactStorage;
61
87
  this.artifacts = opts.artifacts;
88
+ this.record = opts.record;
62
89
  this.executionId = opts.executionId;
63
90
  }
64
91
 
65
- /** Set of sandbox paths that have been published (for auto-publish dedup). */
92
+ /** Set of sandbox paths brought up to date this turn (for auto-publish dedup). */
66
93
  get publishedPaths(): ReadonlySet<string> {
67
- return new Set(this.published.keys());
94
+ return new Set(this.published);
68
95
  }
69
96
 
70
97
  /**
71
98
  * Upload the file at `path` to artifact storage and register it on the
72
99
  * status builder. Fire-and-forget: errors are logged and swallowed.
100
+ *
101
+ * The artifact's `createdAt` is read here, before the first await: the
102
+ * stream asks for the publish in the step that folds the write's finish, so
103
+ * the stamp is the write's, never the moment a slow upload returned.
73
104
  */
74
105
  async publish(path: string): Promise<void> {
106
+ const createdAt = utcTimestamp();
75
107
  // No artifact store (proxy misconfig): nothing to upload to. Skip silently —
76
108
  // this is a best-effort UI publisher and the operator was warned at setup.
77
109
  if (!this.artifactStorage) return;
@@ -97,7 +129,8 @@ export class InlinePublisher {
97
129
  const contentBuffer = Buffer.from(content, "utf-8");
98
130
  const contentHash = sha256(contentBuffer);
99
131
 
100
- if (this.published.get(sandboxPath) === contentHash) {
132
+ if (this.isRecorded(sandboxPath, contentHash)) {
133
+ this.published.add(sandboxPath);
101
134
  return;
102
135
  }
103
136
 
@@ -112,12 +145,12 @@ export class InlinePublisher {
112
145
  kind: ExecutionArtifactKind.FILE,
113
146
  sizeBytes: BigInt(contentBuffer.length),
114
147
  storageKey,
115
- createdAt: utcTimestamp(),
148
+ createdAt,
116
149
  contentHash,
117
150
  });
118
151
 
119
152
  this.artifacts.addArtifact(artifact);
120
- this.published.set(sandboxPath, contentHash);
153
+ this.published.add(sandboxPath);
121
154
 
122
155
  console.log(
123
156
  `[InlinePublisher] execution=${this.executionId} — published '${sandboxPath}' ` +
@@ -130,6 +163,13 @@ export class InlinePublisher {
130
163
  );
131
164
  }
132
165
  }
166
+
167
+ /** Whether the status already lists these exact bytes at this path. */
168
+ private isRecorded(sandboxPath: string, contentHash: string): boolean {
169
+ return this.record.artifacts.some(
170
+ (a) => a.sandboxPath === sandboxPath && a.contentHash === contentHash,
171
+ );
172
+ }
133
173
  }
134
174
 
135
175
  function normalizePath(path: string): string {
@@ -30,9 +30,18 @@
30
30
  *
31
31
  * WHEN IT IS MOUNTED
32
32
  * ------------------
33
- * On the turns where the workspace link points at the platform dir — the
34
- * turns whose skill or attachment phase made it — so what the agent can
35
- * reach is exactly what the link exposes to the shell on the same turn.
33
+ * On every turn: `.stigmer/` is the platform's name whether or not the turn
34
+ * mounted anything, so a write there never reaches the workspace. Before
35
+ * #1123 the route existed only on linked turns, and a write under `.stigmer/`
36
+ * on any other turn made a real directory in the user's tree that the next
37
+ * link displaced. What the route exposes follows the link, so the file tools
38
+ * reach exactly what the shell reaches on the same turn:
39
+ * - on a turn whose skill or attachment phase made the link, the platform dir
40
+ * ({@link PlatformContentBackend});
41
+ * - on any other turn, nothing ({@link NoPlatformContentBackend}): the dir
42
+ * still holds earlier turns' skills and inputs, which this turn's shell
43
+ * cannot reach, so the file tools must not either. The root listing then
44
+ * leaves `.stigmer/` out, as the tree does.
36
45
  * The link itself stays: shell commands (a skill's own scripts among them)
37
46
  * run in the workspace and resolve `.stigmer/…` through it.
38
47
  */
@@ -40,10 +49,16 @@
40
49
  import { CompositeBackend, FilesystemBackend } from "deepagents";
41
50
  import type {
42
51
  AnyBackendProtocol,
52
+ BackendProtocolV2,
43
53
  DeleteResult,
44
54
  EditResult,
55
+ FileDownloadResponse,
45
56
  FileUploadResponse,
57
+ GlobResult,
58
+ GrepResult,
46
59
  LsResult,
60
+ ReadRawResult,
61
+ ReadResult,
47
62
  WriteResult,
48
63
  } from "deepagents";
49
64
  import { STIGMER_LOCAL_STATE_DIR, stigmerSymlinkPointsAt } from "../../shared/workspace/stigmer-link.js";
@@ -51,14 +66,25 @@ import { STIGMER_LOCAL_STATE_DIR, stigmerSymlinkPointsAt } from "../../shared/wo
51
66
  /** The route prefix, with the trailing slash `CompositeBackend` needs to match `/.stigmer/x` but never `/.stigmerx`. */
52
67
  export const PLATFORM_ROUTE_PREFIX = `/${STIGMER_LOCAL_STATE_DIR}/`;
53
68
 
69
+ /** The path as the agent named it: the route hands its backend the path with the prefix stripped. */
70
+ function agentPath(routedPath: string): string {
71
+ return `${PLATFORM_ROUTE_PREFIX.slice(0, -1)}${routedPath}`;
72
+ }
73
+
54
74
  function readOnly(routedPath: string): string {
55
- const agentPath = `${PLATFORM_ROUTE_PREFIX.slice(0, -1)}${routedPath}`;
56
75
  return (
57
- `Error: '${agentPath}' is platform content (skills, attached inputs, the approved plan) ` +
76
+ `Error: '${agentPath(routedPath)}' is platform content (skills, attached inputs, the approved plan) ` +
58
77
  "and is read-only; write your own files in the workspace"
59
78
  );
60
79
  }
61
80
 
81
+ function noPlatformContent(routedPath: string): string {
82
+ return (
83
+ `Error: '${agentPath(routedPath)}' does not exist: this turn has no platform content ` +
84
+ "(no skills or attached inputs)"
85
+ );
86
+ }
87
+
62
88
  /** The platform dir, virtual-rooted like every native backend, refusing every mutation. */
63
89
  export class PlatformContentBackend extends FilesystemBackend {
64
90
  constructor(platformDir: string) {
@@ -83,17 +109,80 @@ export class PlatformContentBackend extends FilesystemBackend {
83
109
  }
84
110
 
85
111
  /**
86
- * The workspace backend with the platform mounted at {@link PLATFORM_ROUTE_PREFIX}.
112
+ * The `.stigmer/` route on a turn that exposes no platform content: nothing
113
+ * to read, list or find, and every mutation refused with the same message
114
+ * the platform dir's route gives. It stands on no filesystem, so it cannot
115
+ * create or reveal anything, and `ls` answers "does not exist" rather than
116
+ * an empty listing because the root listing leaves `.stigmer/` out.
117
+ */
118
+ export class NoPlatformContentBackend implements BackendProtocolV2 {
119
+ ls(path: string): LsResult {
120
+ return { error: noPlatformContent(path) };
121
+ }
122
+
123
+ read(filePath: string): ReadResult {
124
+ return { error: noPlatformContent(filePath) };
125
+ }
126
+
127
+ readRaw(filePath: string): ReadRawResult {
128
+ return { error: noPlatformContent(filePath) };
129
+ }
130
+
131
+ grep(): GrepResult {
132
+ return { matches: [] };
133
+ }
134
+
135
+ glob(): GlobResult {
136
+ return { files: [] };
137
+ }
138
+
139
+ write(filePath: string): WriteResult {
140
+ return { error: readOnly(filePath) };
141
+ }
142
+
143
+ edit(filePath: string): EditResult {
144
+ return { error: readOnly(filePath) };
145
+ }
146
+
147
+ delete(filePath: string): DeleteResult {
148
+ return { error: readOnly(filePath) };
149
+ }
150
+
151
+ uploadFiles(files: Array<[string, Uint8Array]>): FileUploadResponse[] {
152
+ return files.map(([path]) => ({ path, error: "permission_denied" }));
153
+ }
154
+
155
+ downloadFiles(paths: string[]): FileDownloadResponse[] {
156
+ return paths.map((path) => ({ path, content: null, error: "file_not_found" }));
157
+ }
158
+ }
159
+
160
+ /**
161
+ * The workspace backend with `.stigmer/` routed to {@link PLATFORM_ROUTE_PREFIX}'s
162
+ * backend for this turn.
87
163
  *
88
164
  * One behaviour differs from the stock router: listing `/`. The composite
89
- * adds each route to the root listing, and the workspace backend already
90
- * lists `.stigmer/` by following the link, so the stock listing names the
91
- * same directory twice; the root listing is de-duplicated by path.
165
+ * adds each route to the root listing whatever the route holds. With the
166
+ * platform exposed, the workspace backend also lists `.stigmer/` by following
167
+ * the link, so the stock listing names the same directory twice and is
168
+ * de-duplicated by path. With nothing exposed, `.stigmer/` is left out of the
169
+ * root listing altogether: the route has nothing to show, and a real
170
+ * `.stigmer` the workspace may hold is unreachable behind it.
92
171
  */
93
172
  export class PlatformRoutedBackend extends CompositeBackend {
173
+ private readonly exposesPlatform: boolean;
174
+
175
+ constructor(backend: AnyBackendProtocol, platform: PlatformContentBackend | NoPlatformContentBackend) {
176
+ super(backend, { [PLATFORM_ROUTE_PREFIX]: platform });
177
+ this.exposesPlatform = platform instanceof PlatformContentBackend;
178
+ }
179
+
94
180
  override async ls(path: string): Promise<LsResult> {
95
181
  const result = await super.ls(path);
96
182
  if (path !== "/" || !result.files) return result;
183
+ if (!this.exposesPlatform) {
184
+ return { ...result, files: result.files.filter((entry) => entry.path !== PLATFORM_ROUTE_PREFIX) };
185
+ }
97
186
  const seen = new Set<string>();
98
187
  return {
99
188
  ...result,
@@ -114,21 +203,21 @@ export interface PlatformRouteOptions {
114
203
  }
115
204
 
116
205
  /**
117
- * Mount the platform route over `backend` when this turn exposes platform
118
- * content; otherwise hand `backend` back unchanged. `execute` and the
119
- * sandbox identity stay the workspace backend's (`CompositeBackend` delegates
120
- * both to its default), so a shell-capable graph keeps its `execute` tool and
121
- * a plan-mode graph stays without one.
206
+ * Mount the `.stigmer/` route over `backend` for this turn: the platform dir
207
+ * when the workspace link points at it, the empty route otherwise. Only a
208
+ * session without a platform dir gets `backend` back unchanged. `execute`
209
+ * and the sandbox identity stay the workspace backend's (`CompositeBackend`
210
+ * delegates both to its default), so a shell-capable graph keeps its
211
+ * `execute` tool and a plan-mode graph stays without one.
122
212
  */
123
213
  export async function mountPlatformRoute(
124
214
  backend: AnyBackendProtocol,
125
215
  options: PlatformRouteOptions,
126
216
  ): Promise<AnyBackendProtocol> {
127
217
  const { workspaceDir, platformDir } = options;
128
- if (platformDir === undefined || !(await stigmerSymlinkPointsAt(workspaceDir, platformDir))) {
129
- return backend;
130
- }
131
- return new PlatformRoutedBackend(backend, {
132
- [PLATFORM_ROUTE_PREFIX]: new PlatformContentBackend(platformDir),
133
- });
218
+ if (platformDir === undefined) return backend;
219
+ const platform = (await stigmerSymlinkPointsAt(workspaceDir, platformDir))
220
+ ? new PlatformContentBackend(platformDir)
221
+ : new NoPlatformContentBackend();
222
+ return new PlatformRoutedBackend(backend, platform);
134
223
  }
@@ -1,19 +1,32 @@
1
1
  /**
2
- * The native harness's system prompt: what `createDeepAgent` receives as
3
- * `systemPrompt`, rebuilt on EVERY invocation (never checkpointed with the
4
- * message history), so every standing fact is injected on every turn by
5
- * design — a first-turn-only injection would vanish from turn 2 onward.
2
+ * The native harness's prompt: the system prompt `createDeepAgent` receives
3
+ * as `systemPrompt`, and the turn's user message.
4
+ *
5
+ * The system prompt is rebuilt on EVERY invocation (never checkpointed with
6
+ * the message history), so every standing fact is injected on every turn by
7
+ * design — a first-turn-only injection would vanish from turn 2 onward. It is
8
+ * a function of the session and of the execution's settings (its mode), never
9
+ * of the turn. The provider caches tools, system and messages as one prefix,
10
+ * so a system byte that follows the message re-writes the cached prompt and
11
+ * the whole conversation after it, on every turn, at the cache-write price.
12
+ * What belongs to one turn — its input files, the workspace paths it
13
+ * references, the conversation catchup — rides that turn's user message
14
+ * (`composeUserMessage`), which enters the checkpointer with the turn and
15
+ * stays in history. That is the Cursor harness's resumed-turn placement
16
+ * (`execute-cursor/prompt-builder.ts` `buildPrompt`) and the runner's rule
17
+ * that user content goes in user messages, never the system prompt.
6
18
  *
7
19
  * Pure functions: no side effects, no I/O. `turn-setup.ts` gathers the
8
20
  * inputs from the runtime's resolved record and this module renders them.
9
21
  *
10
22
  * What is this harness's and what is shared (since #1096):
11
23
  * - Shared through `shared/prompt-sections.ts`: WHICH standing sections
12
- * render and in WHAT order (`standingContextSections`), the input-files
13
- * bullet and disclosure lines (`inputFileLines`), which skills a prompt
14
- * highlights (`selectSkillsForPrompt`, called from `turn-setup.ts`) and
15
- * the also-available sentence. Their WORDS come from each fact's own
16
- * module; this builder only frames them.
24
+ * render and in WHAT order (`standingContextSections`) and the
25
+ * input-files bullet and disclosure lines (`inputFileLines`). Their WORDS
26
+ * come from each fact's own module; this builder only frames them. Every
27
+ * mounted skill is described on every turn, as the Cursor harness does:
28
+ * choosing skills by the message made the system prompt follow the
29
+ * message.
17
30
  * - This harness's: the framing — `## Heading` markdown sections, the house
18
31
  * style for a system prompt — and every text that quotes this engine's
19
32
  * tools: the `## Skills` activation protocol, the response rules, the
@@ -24,7 +37,8 @@
24
37
  *
25
38
  * The rendered bytes are load-bearing for the hermetic tests: `ScriptedModel`
26
39
  * tells its scripted roles apart by the system prompt's text, so this
27
- * module's whole output is pinned by `__tests__/prompt-goldens.test.ts`.
40
+ * module's whole output — the system prompt and the turn's message — is
41
+ * pinned by `__tests__/prompt-goldens.test.ts`.
28
42
  */
29
43
 
30
44
  import { relative } from "node:path";
@@ -43,10 +57,7 @@ import {
43
57
  } from "../../shared/prompt-sections.js";
44
58
  import { effectiveInstructions } from "../../shared/builtin-assistant-prompt.js";
45
59
  import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
46
- import {
47
- buildImplementPlanDirective,
48
- findApprovedPlanPath,
49
- } from "../../shared/implement-plan-prompt.js";
60
+ import { buildImplementPlanDirective } from "../../shared/implement-plan-prompt.js";
50
61
  import type { ResolvedAttachment } from "../../shared/attachment-resolver.js";
51
62
  import type { SkillMetadata } from "../../shared/skill-resolver.js";
52
63
  import { STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
@@ -131,22 +142,6 @@ export interface PromptBuilderInput {
131
142
  * when the agent serves no proactive channel or nothing is sendable.
132
143
  */
133
144
  channelTemplatesPromptSection?: string;
134
- workspaceFileRefs: string[];
135
- workspaceRoot: string;
136
- /** The turn's resolved input files, as the runtime's attachment phase returns them. */
137
- inputFiles: readonly ResolvedAttachment[];
138
- /**
139
- * Vision facts about this turn's attachments (T04): which images the model
140
- * sees inline in the user message and which degraded to path-only.
141
- * Rendered inside the Input Files section.
142
- */
143
- vision?: VisionPromptInfo;
144
- /**
145
- * What kind of URL the turn's storage backend mints (issue #532) — keys
146
- * the Input Files section's hand-off wording (attachment-download-urls.ts).
147
- * One turn-level fact: all attachments ride the one configured storage.
148
- */
149
- downloadUrlKind?: DownloadUrlKind;
150
145
  /**
151
146
  * The execution's interaction mode. PLAN appends the shared plan-mode
152
147
  * directive so the model knows the turn's deliverable is a plan document.
@@ -163,6 +158,14 @@ export interface PromptBuilderInput {
163
158
  * message itself is just a short label ("Build from plan").
164
159
  */
165
160
  buildFromPlan?: boolean;
161
+ /**
162
+ * The approved plan's path among the turn's input files
163
+ * (`implement-plan-prompt.ts` `findApprovedPlanPath`), read only under
164
+ * `buildFromPlan`. Absent when the plan did not materialize, which selects
165
+ * the directive's conversation-plan variant. The Input Files section
166
+ * itself rides the turn's message (`composeUserMessage`).
167
+ */
168
+ approvedPlanPath?: string;
166
169
  /**
167
170
  * Rollover context bridge (cloud DD-013): a digest of the previous
168
171
  * session's conversation, read from `SessionSpec.metadata`. Injected on
@@ -252,22 +255,6 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
252
255
  prompt += "\n\n" + input.channelTemplatesPromptSection;
253
256
  }
254
257
 
255
- if (input.workspaceFileRefs.length > 0) {
256
- const refSection = buildReferencedFilesSection(
257
- input.workspaceFileRefs,
258
- input.workspaceRoot,
259
- );
260
- if (refSection) {
261
- prompt += refSection;
262
- }
263
- }
264
-
265
- if (input.inputFiles.length > 0) {
266
- prompt += buildInputFilesSection(
267
- input.inputFiles, input.vision, input.downloadUrlKind,
268
- );
269
- }
270
-
271
258
  // The standing context, in the shared order (the doctrine is stated once,
272
259
  // on `standingContextSections`); this harness only frames each section.
273
260
  for (const section of standingContextSections(input)) {
@@ -296,34 +283,67 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
296
283
  }
297
284
 
298
285
  if (input.buildFromPlan) {
299
- const planPath = findApprovedPlanPath(
300
- input.inputFiles.map((f) => f.relativePath),
301
- );
302
286
  prompt +=
303
287
  "\n\n## Implement the approved plan\n\n" +
304
- buildImplementPlanDirective(planPath);
288
+ buildImplementPlanDirective(input.approvedPlanPath);
305
289
  }
306
290
 
307
291
  return prompt;
308
292
  }
309
293
 
294
+ /** The turn's own content, composed around what the user typed. */
295
+ export interface TurnMessageInput {
296
+ /** What the user typed (`spec.message`); never mutated. */
297
+ readonly message: string;
298
+ /** The conversation catchup digest (cloud DD-006), when the turn carries one. */
299
+ readonly conversationCatchup?: string;
300
+ /** This turn's resolved input files, as the runtime's attachment phase returns them. */
301
+ readonly inputFiles: readonly ResolvedAttachment[];
302
+ /**
303
+ * Vision facts about this turn's attachments (T04): which images the model
304
+ * sees inline in this same message and which degraded to path-only.
305
+ * Rendered inside the Input Files section.
306
+ */
307
+ readonly vision?: VisionPromptInfo;
308
+ /**
309
+ * What kind of URL the turn's storage backend mints (issue #532) — keys
310
+ * the Input Files section's hand-off wording (attachment-download-urls.ts).
311
+ * One turn-level fact: all attachments ride the one configured storage.
312
+ */
313
+ readonly downloadUrlKind?: DownloadUrlKind;
314
+ /** The workspace paths the user highlighted for this message (`spec.workspace_file_refs`). */
315
+ readonly workspaceFileRefs: readonly string[];
316
+ }
317
+
310
318
  /**
311
- * Compose the turn's USER MESSAGE for the graph invocation: the framed
312
- * conversation catchup (cloud DD-006), when present, prepended to the
313
- * customer's message. In the user message and never the system prompt (A27):
314
- * the system prompt is rebuilt per invocation and would forget the digest one
315
- * turn later, while a message enters the checkpointer with the turn and
316
- * persists in history — the same durability the cursor harness gets from its
317
- * prompt prefix. The caller's `spec.message` is never mutated; the prepend
318
- * exists only in the graph input.
319
+ * Compose the turn's USER MESSAGE for the graph invocation: this turn's
320
+ * payload (its input files, then the workspace paths it references), then the
321
+ * framed conversation catchup (cloud DD-006), then what the user typed, each
322
+ * set off by a horizontal rule. Payload first and context closest to the task
323
+ * is the Cursor harness's resumed-turn order.
324
+ *
325
+ * In the user message and never the system prompt (A27, and the module
326
+ * header): the system prompt is rebuilt per invocation, so a turn's content
327
+ * there would vanish one turn later and, while it lasted, change the cached
328
+ * prefix; a message enters the checkpointer with the turn and persists in
329
+ * history, so a later turn still knows which files an earlier one carried.
330
+ * An approval resume sends no new message and needs none: the payload is
331
+ * already in the checkpointed one. The caller's `spec.message` is never
332
+ * mutated; the composition exists only in the graph input.
319
333
  */
320
- export function composeUserMessage(
321
- message: string,
322
- conversationCatchup: string | undefined,
323
- ): string {
324
- return conversationCatchup
325
- ? `${formatConversationCatchupText(conversationCatchup)}\n\n---\n\n${message}`
326
- : message;
334
+ export function composeUserMessage(input: TurnMessageInput): string {
335
+ const parts: string[] = [];
336
+ if (input.inputFiles.length > 0) {
337
+ parts.push(buildInputFilesSection(input.inputFiles, input.vision, input.downloadUrlKind));
338
+ }
339
+ if (input.workspaceFileRefs.length > 0) {
340
+ parts.push(buildReferencedFilesSection(input.workspaceFileRefs));
341
+ }
342
+ if (input.conversationCatchup) {
343
+ parts.push(formatConversationCatchupText(input.conversationCatchup));
344
+ }
345
+ parts.push(input.message);
346
+ return parts.join("\n\n---\n\n");
327
347
  }
328
348
 
329
349
  /**
@@ -454,14 +474,14 @@ function formatEntryDescription(result: ProvisionResult): string {
454
474
  );
455
475
  }
456
476
 
477
+ // No commit: the clone's HEAD moves with every write-back commit, and a
478
+ // system prompt that names it would change on every such turn
479
+ // (`shared/workspace/sources/git.ts` says the same for one entry).
457
480
  if (result.sourceType === "git_repo" && result.gitMetadata) {
458
481
  const meta: GitMetadata = result.gitMetadata;
459
- const shortSha = meta.baseCommit.length >= 7
460
- ? meta.baseCommit.slice(0, 7)
461
- : meta.baseCommit;
482
+ const branch = meta.branch ? ` (branch: ${meta.branch})` : "";
462
483
  return (
463
- `Workspace entry **${name}** was initialized from ` +
464
- `${meta.repoUrl} (branch: ${meta.branch}, commit: ${shortSha}).\n` +
484
+ `Workspace entry **${name}** was initialized from ${meta.repoUrl}${branch}.\n` +
465
485
  "Changes you make will be captured as artifacts when execution completes."
466
486
  );
467
487
  }
@@ -476,36 +496,29 @@ function formatEntryDescription(result: ProvisionResult): string {
476
496
  return result.workspaceDescription;
477
497
  }
478
498
 
479
- function buildReferencedFilesSection(
480
- workspaceFileRefs: string[],
481
- _workspaceRoot: string,
482
- ): string {
483
- if (workspaceFileRefs.length === 0) return "";
484
-
485
- let section =
486
- "\n\n## Referenced Files\n\n" +
499
+ /** The `## Referenced Files` part of the turn's message: the paths the user highlighted, one bullet each. */
500
+ function buildReferencedFilesSection(workspaceFileRefs: readonly string[]): string {
501
+ return (
502
+ "## Referenced Files\n\n" +
487
503
  "The user has highlighted the following workspace paths for your " +
488
- `attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n`;
489
-
490
- for (const refPath of workspaceFileRefs) {
491
- section += `- \`${refPath}\`\n`;
492
- }
493
-
494
- return section;
504
+ `attention. Use \`${ENGINE_TOOL.readFile}\` to access file contents.\n\n` +
505
+ workspaceFileRefs.map((refPath) => `- \`${refPath}\``).join("\n")
506
+ );
495
507
  }
496
508
 
497
509
  /**
498
- * The `## Input Files` section: this harness's intro (it names this engine's
499
- * file-reading tool) around the shared lines (`prompt-sections.ts` `inputFileLines`
500
- * says what each bullet and disclosure carries and why). Each disclosure
501
- * group is set off by a blank line, the markdown idiom.
510
+ * The `## Input Files` part of the turn's message: this harness's intro (it
511
+ * names this engine's file-reading tool) around the shared lines
512
+ * (`prompt-sections.ts` `inputFileLines` says what each bullet and disclosure
513
+ * carries and why). Each disclosure group is set off by a blank line, the
514
+ * markdown idiom.
502
515
  */
503
516
  function buildInputFilesSection(
504
517
  files: readonly ResolvedAttachment[],
505
518
  vision?: VisionPromptInfo,
506
519
  downloadUrlKind?: DownloadUrlKind,
507
520
  ): string {
508
- let section = "\n\n## Input Files\n\n";
521
+ let section = "## Input Files\n\n";
509
522
  section +=
510
523
  "The following files have been provided as read-only reference " +
511
524
  "material for your task. They live under `.stigmer/inputs/` and " +
@@ -526,7 +539,6 @@ function buildInputFilesSection(
526
539
  section += "\n" + lines.vision.join("\n") + "\n";
527
540
  }
528
541
 
529
- // Every section opens with its own blank line ("\n\n## ..."), so this one
530
- // ends on its last line rather than adding a second blank (#1127).
542
+ // The part ends on its last line; the message's rule sets it off (#1127).
531
543
  return section.replace(/\n+$/, "");
532
544
  }