@stigmer/runner 3.1.1 → 3.1.3

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 (166) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.d.ts +49 -8
  3. package/dist/activities/execute-cursor/attachment-resolver.js +83 -26
  4. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  5. package/dist/activities/execute-cursor/capture-flow.d.ts +18 -0
  6. package/dist/activities/execute-cursor/capture-flow.js +21 -0
  7. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  8. package/dist/activities/execute-cursor/cas-observations.d.ts +13 -0
  9. package/dist/activities/execute-cursor/cas-observations.js +39 -5
  10. package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
  11. package/dist/activities/execute-cursor/hook-script.js +19 -1
  12. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  13. package/dist/activities/execute-cursor/index.d.ts +5 -0
  14. package/dist/activities/execute-cursor/index.js +222 -160
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/message-translator.js +10 -1
  17. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  18. package/dist/activities/execute-cursor/prompt-builder.d.ts +32 -7
  19. package/dist/activities/execute-cursor/prompt-builder.js +38 -20
  20. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  21. package/dist/activities/execute-cursor/skill-resolver.d.ts +1 -10
  22. package/dist/activities/execute-cursor/skill-resolver.js +3 -55
  23. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  24. package/dist/activities/execute-cursor/stigmer-link.d.ts +35 -0
  25. package/dist/activities/execute-cursor/stigmer-link.js +73 -0
  26. package/dist/activities/execute-cursor/stigmer-link.js.map +1 -0
  27. package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
  28. package/dist/activities/execute-cursor/turn-boundary.js +163 -0
  29. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
  30. package/dist/activities/execute-cursor/workspace-provision.d.ts +3 -2
  31. package/dist/activities/execute-cursor/workspace-provision.js +5 -3
  32. package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/index.js +89 -5
  34. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +17 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
  37. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/setup.js +22 -16
  39. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  40. package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
  41. package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
  43. package/dist/activities/execute-deep-agent/streaming.js +5 -1
  44. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  45. package/dist/config.d.ts +7 -0
  46. package/dist/config.js +9 -0
  47. package/dist/config.js.map +1 -1
  48. package/dist/middleware/approval-gate.js +36 -6
  49. package/dist/middleware/approval-gate.js.map +1 -1
  50. package/dist/runner-manager.d.ts +2 -0
  51. package/dist/runner-manager.js +2 -1
  52. package/dist/runner-manager.js.map +1 -1
  53. package/dist/runner.d.ts +2 -0
  54. package/dist/runner.js +2 -1
  55. package/dist/runner.js.map +1 -1
  56. package/dist/shared/artifact-storage.d.ts +32 -0
  57. package/dist/shared/artifact-storage.js +74 -1
  58. package/dist/shared/artifact-storage.js.map +1 -1
  59. package/dist/shared/filereview/capture.d.ts +18 -1
  60. package/dist/shared/filereview/capture.js +105 -28
  61. package/dist/shared/filereview/capture.js.map +1 -1
  62. package/dist/shared/filereview/cas-substrate.d.ts +9 -0
  63. package/dist/shared/filereview/cas-substrate.js +9 -1
  64. package/dist/shared/filereview/cas-substrate.js.map +1 -1
  65. package/dist/shared/filereview/events.d.ts +16 -0
  66. package/dist/shared/filereview/events.js +37 -0
  67. package/dist/shared/filereview/events.js.map +1 -1
  68. package/dist/shared/filereview/git-substrate.d.ts +42 -0
  69. package/dist/shared/filereview/git-substrate.js +78 -0
  70. package/dist/shared/filereview/git-substrate.js.map +1 -1
  71. package/dist/shared/filereview/index.d.ts +3 -1
  72. package/dist/shared/filereview/index.js +3 -1
  73. package/dist/shared/filereview/index.js.map +1 -1
  74. package/dist/shared/filereview/line-counts.d.ts +48 -0
  75. package/dist/shared/filereview/line-counts.js +72 -0
  76. package/dist/shared/filereview/line-counts.js.map +1 -0
  77. package/dist/shared/filereview/progress.d.ts +96 -0
  78. package/dist/shared/filereview/progress.js +134 -0
  79. package/dist/shared/filereview/progress.js.map +1 -0
  80. package/dist/shared/implement-plan-prompt.d.ts +44 -0
  81. package/dist/shared/implement-plan-prompt.js +98 -0
  82. package/dist/shared/implement-plan-prompt.js.map +1 -0
  83. package/dist/shared/plan-artifact.d.ts +59 -11
  84. package/dist/shared/plan-artifact.js +144 -16
  85. package/dist/shared/plan-artifact.js.map +1 -1
  86. package/dist/shared/plan-mode-prompt.d.ts +32 -0
  87. package/dist/shared/plan-mode-prompt.js +56 -0
  88. package/dist/shared/plan-mode-prompt.js.map +1 -0
  89. package/dist/shared/status.d.ts +11 -0
  90. package/dist/shared/status.js +12 -2
  91. package/dist/shared/status.js.map +1 -1
  92. package/dist/shared/tool-row.d.ts +30 -1
  93. package/dist/shared/tool-row.js +53 -4
  94. package/dist/shared/tool-row.js.map +1 -1
  95. package/dist/shared/workspace/session-root.d.ts +35 -0
  96. package/dist/shared/workspace/session-root.js +53 -0
  97. package/dist/shared/workspace/session-root.js.map +1 -0
  98. package/dist/shared/workspace/workspace-lock.d.ts +86 -0
  99. package/dist/shared/workspace/workspace-lock.js +187 -0
  100. package/dist/shared/workspace/workspace-lock.js.map +1 -0
  101. package/package.json +8 -5
  102. package/src/__tests__/config.test.ts +8 -0
  103. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  104. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  105. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +161 -0
  106. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +132 -2
  107. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +55 -0
  108. package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
  109. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +28 -0
  110. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +63 -4
  111. package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
  112. package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +36 -2
  113. package/src/activities/execute-cursor/attachment-resolver.ts +116 -34
  114. package/src/activities/execute-cursor/capture-flow.ts +32 -0
  115. package/src/activities/execute-cursor/cas-observations.ts +41 -5
  116. package/src/activities/execute-cursor/hook-script.ts +19 -1
  117. package/src/activities/execute-cursor/index.ts +243 -178
  118. package/src/activities/execute-cursor/message-translator.ts +9 -1
  119. package/src/activities/execute-cursor/prompt-builder.ts +60 -22
  120. package/src/activities/execute-cursor/skill-resolver.ts +3 -59
  121. package/src/activities/execute-cursor/stigmer-link.ts +78 -0
  122. package/src/activities/execute-cursor/turn-boundary.ts +274 -0
  123. package/src/activities/execute-cursor/workspace-provision.ts +5 -3
  124. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +7 -2
  125. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +7 -2
  126. package/src/activities/execute-deep-agent/__tests__/index.test.ts +4 -1
  127. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +99 -0
  128. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +7 -2
  129. package/src/activities/execute-deep-agent/index.ts +104 -5
  130. package/src/activities/execute-deep-agent/prompt-builder.ts +39 -0
  131. package/src/activities/execute-deep-agent/setup.ts +25 -17
  132. package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
  133. package/src/activities/execute-deep-agent/streaming.ts +12 -1
  134. package/src/config.ts +17 -0
  135. package/src/middleware/__tests__/approval-gate.test.ts +77 -12
  136. package/src/middleware/approval-gate.ts +39 -7
  137. package/src/runner-manager.ts +6 -1
  138. package/src/runner.ts +6 -1
  139. package/src/shared/__tests__/artifact-storage.test.ts +111 -1
  140. package/src/shared/__tests__/implement-plan-prompt.test.ts +86 -0
  141. package/src/shared/__tests__/plan-artifact.test.ts +142 -9
  142. package/src/shared/__tests__/secret-leak-scan.test.ts +105 -0
  143. package/src/shared/__tests__/status.test.ts +7 -2
  144. package/src/shared/__tests__/tool-row.test.ts +136 -1
  145. package/src/shared/artifact-storage.ts +83 -1
  146. package/src/shared/filereview/__tests__/capture.test.ts +182 -1
  147. package/src/shared/filereview/__tests__/cas-substrate.test.ts +34 -0
  148. package/src/shared/filereview/__tests__/events.test.ts +105 -0
  149. package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
  150. package/src/shared/filereview/__tests__/progress.test.ts +236 -0
  151. package/src/shared/filereview/capture.ts +134 -30
  152. package/src/shared/filereview/cas-substrate.ts +17 -1
  153. package/src/shared/filereview/events.ts +50 -0
  154. package/src/shared/filereview/git-substrate.ts +108 -0
  155. package/src/shared/filereview/index.ts +22 -1
  156. package/src/shared/filereview/line-counts.ts +83 -0
  157. package/src/shared/filereview/progress.ts +186 -0
  158. package/src/shared/implement-plan-prompt.ts +104 -0
  159. package/src/shared/plan-artifact.ts +152 -16
  160. package/src/shared/plan-mode-prompt.ts +56 -0
  161. package/src/shared/status.ts +11 -1
  162. package/src/shared/tool-row.ts +58 -5
  163. package/src/shared/workspace/__tests__/session-root.test.ts +42 -0
  164. package/src/shared/workspace/__tests__/workspace-lock.test.ts +200 -0
  165. package/src/shared/workspace/session-root.ts +60 -0
  166. package/src/shared/workspace/workspace-lock.ts +241 -0
@@ -1,5 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
+ import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
2
3
  import { buildEnhancedSystemPrompt } from "../prompt-builder.js";
4
+ import { PLAN_MODE_DIRECTIVE } from "../../../shared/plan-mode-prompt.js";
3
5
  import { SourceType } from "../../../shared/workspace/types.js";
4
6
  import type { ProvisionResult } from "../../../shared/workspace/types.js";
5
7
 
@@ -205,4 +207,101 @@ describe("buildEnhancedSystemPrompt", () => {
205
207
 
206
208
  expect(prompt).not.toContain("## Workspace");
207
209
  });
210
+
211
+ describe("plan mode", () => {
212
+ const base = {
213
+ instructions: "Test",
214
+ provisionResults: [],
215
+ containerRoot: "",
216
+ skillsPromptSection: "",
217
+ workspaceFileRefs: [],
218
+ workspaceRoot: "/workspace",
219
+ injectedFiles: [],
220
+ };
221
+
222
+ it("appends the shared plan-mode directive as the final section", () => {
223
+ const prompt = buildEnhancedSystemPrompt({
224
+ ...base,
225
+ interactionMode: InteractionMode.PLAN,
226
+ });
227
+
228
+ expect(prompt).toContain("## Plan mode");
229
+ expect(prompt.endsWith(PLAN_MODE_DIRECTIVE)).toBe(true);
230
+ });
231
+
232
+ it("omits the directive for Agent mode", () => {
233
+ const prompt = buildEnhancedSystemPrompt({
234
+ ...base,
235
+ interactionMode: InteractionMode.AGENT,
236
+ });
237
+
238
+ expect(prompt).not.toContain("## Plan mode");
239
+ });
240
+
241
+ it("omits the directive when the mode is unset", () => {
242
+ const prompt = buildEnhancedSystemPrompt(base);
243
+
244
+ expect(prompt).not.toContain("## Plan mode");
245
+ });
246
+ });
247
+
248
+ describe("build from plan", () => {
249
+ const base = {
250
+ instructions: "Test",
251
+ provisionResults: [],
252
+ containerRoot: "",
253
+ skillsPromptSection: "",
254
+ workspaceFileRefs: [],
255
+ workspaceRoot: "/workspace",
256
+ injectedFiles: [],
257
+ };
258
+ const planFile = {
259
+ filename: "plan.md",
260
+ path: ".stigmer/inputs/plan.md",
261
+ size: 1024,
262
+ };
263
+
264
+ it("appends the implement-plan directive pointing at the injected plan", () => {
265
+ const prompt = buildEnhancedSystemPrompt({
266
+ ...base,
267
+ buildFromPlan: true,
268
+ injectedFiles: [planFile],
269
+ });
270
+
271
+ expect(prompt).toContain("## Implement the approved plan");
272
+ expect(prompt).toContain("`.stigmer/inputs/plan.md`");
273
+ expect(prompt).toContain("APPROVED");
274
+ });
275
+
276
+ it("falls back to the conversation-plan variant when the plan did not inject", () => {
277
+ const prompt = buildEnhancedSystemPrompt({
278
+ ...base,
279
+ buildFromPlan: true,
280
+ });
281
+
282
+ expect(prompt).toContain("## Implement the approved plan");
283
+ expect(prompt).toContain("conversation above");
284
+ expect(prompt).not.toContain("plan.md");
285
+ });
286
+
287
+ it("omits the directive for an ordinary execution", () => {
288
+ const prompt = buildEnhancedSystemPrompt({
289
+ ...base,
290
+ injectedFiles: [planFile],
291
+ });
292
+
293
+ expect(prompt).not.toContain("## Implement the approved plan");
294
+ });
295
+
296
+ it("carries the plan-derived progress-tracking instruction (Tier 3)", () => {
297
+ const prompt = buildEnhancedSystemPrompt({
298
+ ...base,
299
+ buildFromPlan: true,
300
+ injectedFiles: [planFile],
301
+ });
302
+
303
+ expect(prompt).toContain("to-do list");
304
+ expect(prompt).toContain("break the plan into");
305
+ });
306
+ });
208
307
  });
@@ -51,7 +51,9 @@ import type { SetupResult } from "../setup.js";
51
51
  vi.mock("@temporalio/activity", () => ({
52
52
  Context: {
53
53
  current: () => ({
54
- cancellationSignal: { aborted: false },
54
+ // A real AbortSignal: the workspace-lock wait registers abort listeners
55
+ // on it, which a bare `{ aborted: false }` stub cannot satisfy.
56
+ cancellationSignal: new AbortController().signal,
55
57
  heartbeat: vi.fn(),
56
58
  }),
57
59
  },
@@ -187,7 +189,9 @@ function fakeMemoryGateSetup(opts: {
187
189
  agent: {},
188
190
  session: { spec: { workspaceEntries: [] } },
189
191
  workspaceBackend: {
190
- rootDir: "/tmp/ws",
192
+ // Unique per test file: the workspace turn lock keys on this path, and a
193
+ // path shared across files would serialize parallel test workers for real.
194
+ rootDir: "/tmp/stigmer-test-ws-seq-gate",
191
195
  exists: vi.fn(async () => false),
192
196
  readFile: vi.fn(async () => ""),
193
197
  },
@@ -252,6 +256,7 @@ const memoryConfig: Config = {
252
256
  checkpointerProxyEndpoint: null,
253
257
  primaryModel: "claude-sonnet",
254
258
  cursorStreamStallTimeoutMs: 180000,
259
+ workspaceLockTimeoutMs: 900000,
255
260
  };
256
261
 
257
262
  function toolCallIds(status: AgentExecutionStatus): string[] {
@@ -19,7 +19,13 @@ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/a
19
19
  import { ExecutionPhase, FileCaptureClass, FileChangeSetStatus, InteractionMode, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
20
20
  import { activityStarted, activityFinished } from "../../idle-watchdog.js";
21
21
  import { normalizeActivityInput, type ExecuteActivityInput } from "../../shared/activity-input.js";
22
- import { persistStatus, slimStatus, utcTimestamp } from "../../shared/status.js";
22
+ import { persistStatus, reportSetupProgress, slimStatus, utcTimestamp } from "../../shared/status.js";
23
+ import {
24
+ acquireWorkspaceLock,
25
+ WorkspaceLockCancelledError,
26
+ WorkspaceLockTimeoutError,
27
+ type ReleaseWorkspaceLock,
28
+ } from "../../shared/workspace/workspace-lock.js";
23
29
  import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
24
30
  import { publishPlanArtifact } from "../../shared/plan-artifact.js";
25
31
  import { classifyTool } from "../../shared/tool-kind.js";
@@ -44,11 +50,15 @@ import {
44
50
  captureBaselineToLedger,
45
51
  captureCandidateToLedger,
46
52
  } from "../../shared/filereview/capture.js";
53
+ import {
54
+ captureFileChangeProgress,
55
+ newProgressCaptureState,
56
+ } from "../../shared/filereview/progress.js";
47
57
  import { hasCandidateCaptured } from "../../shared/filereview/events.js";
48
58
  import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
49
59
  import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
50
60
  import type { CasCaptureObserver } from "./cas-capture-observer.js";
51
- import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
61
+ import { collectSubAgentToolCallIds, withholdSecretContentFromMessages } from "../../shared/tool-row.js";
52
62
  import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stamp-flowed-rows.js";
53
63
 
54
64
  /** The harness id stamped on the deep-agent's file-review ledger events. */
@@ -75,6 +85,11 @@ export function createDeepAgentActivities(config: Config) {
75
85
  const { executionId, threadId, turnSeq } = normalizeActivityInput(arg0, arg1);
76
86
  activityStarted();
77
87
  let setup: SetupResult | null = null;
88
+ // Exclusive turn lock on the workspace working tree — held across the
89
+ // entire tree-mutating window (decision reconcile, agent writes,
90
+ // candidate capture, write-back) and released in the finally. Mirrors
91
+ // the Cursor harness; see shared/workspace/workspace-lock.ts.
92
+ let releaseWorkspaceLock: ReleaseWorkspaceLock | undefined;
78
93
 
79
94
  try {
80
95
  console.log(`[ExecuteDeepAgent] Started for execution ${executionId}`);
@@ -163,6 +178,45 @@ export function createDeepAgentActivities(config: Config) {
163
178
  const gitRoot = setup.workspaceBackend.rootDir;
164
179
  const changeSetId = `${executionId}:${turnSeq}`;
165
180
 
181
+ // Serialize this turn against every other execution sharing this
182
+ // working tree — an unserialized concurrent write lands inside this
183
+ // turn's baseline→candidate window and gets misattributed to this
184
+ // turn's review. Acquired before ANY tree mutation below (decision
185
+ // reconcile, agent writes, candidate capture, write-back); waiting
186
+ // surfaces a visible state and heartbeats, and a cancel aborts the
187
+ // wait immediately. Mirrors the Cursor harness wiring exactly.
188
+ try {
189
+ releaseWorkspaceLock = await acquireWorkspaceLock(gitRoot, {
190
+ onWaiting: () => reportSetupProgress(
191
+ client, executionId, "Waiting for workspace — in use by another session",
192
+ ),
193
+ heartbeat: () => Context.current().heartbeat(),
194
+ signal: Context.current().cancellationSignal,
195
+ timeoutMs: config.workspaceLockTimeoutMs,
196
+ });
197
+ } catch (lockErr) {
198
+ if (lockErr instanceof WorkspaceLockCancelledError) {
199
+ throw new CancelledFailure("Activity cancelled while waiting for the workspace lock");
200
+ }
201
+ if (lockErr instanceof WorkspaceLockTimeoutError) {
202
+ const failedStatus = create(AgentExecutionStatusSchema, {
203
+ phase: ExecutionPhase.EXECUTION_FAILED,
204
+ error: lockErr.message,
205
+ completedAt: utcTimestamp(),
206
+ messages: [
207
+ create(AgentMessageSchema, {
208
+ type: MessageType.MESSAGE_SYSTEM,
209
+ content: `Execution failed: ${lockErr.message}`,
210
+ timestamp: utcTimestamp(),
211
+ }),
212
+ ],
213
+ });
214
+ await persistStatus(client, executionId, failedStatus, { offload: statusOffload });
215
+ return slimStatus(failedStatus);
216
+ }
217
+ throw lockErr;
218
+ }
219
+
166
220
  // (1) Capture-mode resume — reconcile any DECIDED change set FIRST (this
167
221
  // drops the per-execution refs), before the next baseline re-pins them
168
222
  // (refs are executionId-keyed). Then (2) short-circuit a pure file-review
@@ -269,6 +323,15 @@ export function createDeepAgentActivities(config: Config) {
269
323
  });
270
324
  }
271
325
 
326
+ // Per-turn state for mid-run live capture (DD-32): the last progress tree
327
+ // sha (short-circuit) + last capture time (floor), threaded across the
328
+ // streaming loop's persists via the beforePersist hook below. The capture
329
+ // flags are hoisted so the deferred hook does not depend on `setup`
330
+ // (non-null here, but not narrowable inside a later-invoked closure).
331
+ const progressState = newProgressCaptureState();
332
+ const progressCaptureMode = setup.captureMode;
333
+ const progressGitWorkspace = setup.gitWorkspace;
334
+
272
335
  const cancellationSignal = Context.current().cancellationSignal;
273
336
 
274
337
  const result: StreamResult = await streamExecution({
@@ -294,6 +357,23 @@ export function createDeepAgentActivities(config: Config) {
294
357
  globalBypass: setup.globalBypass,
295
358
  },
296
359
  streamVersion: setup.streamVersion,
360
+ // Mid-run live capture (DD-32): attach file_change_progress before each
361
+ // scheduled persist. Git capture mode only (a pinned baseline exists);
362
+ // deep-agent writes no runner-owned gate files into the tree, so no
363
+ // excludePaths — matching its turn-boundary candidate capture.
364
+ beforePersist: async (status) => {
365
+ if (!progressCaptureMode || !progressGitWorkspace || !captureBaselineTree) {
366
+ return;
367
+ }
368
+ await captureFileChangeProgress({
369
+ status,
370
+ gitRoot,
371
+ executionId,
372
+ changeSetId,
373
+ baselineTree: captureBaselineTree,
374
+ state: progressState,
375
+ });
376
+ },
297
377
  });
298
378
 
299
379
  await processPostStream({
@@ -308,6 +388,20 @@ export function createDeepAgentActivities(config: Config) {
308
388
  executionId,
309
389
  });
310
390
 
391
+ // Never-persist-secret backstop (DD-26 #2): withhold content from any
392
+ // built-in write row targeting a secret-like path, across the top-level
393
+ // and sub-agent transcripts, before ANY persist below. On the deny-gate a
394
+ // secret write is hard-blocked (a COMPLETED row) or — under
395
+ // auto_approve_all, where no gate is installed — flowed; either way the
396
+ // streamed row still carries `args` from handleToolStarted, and the
397
+ // capture-mode stamping pass (which performs the same scrub) does not run
398
+ // here. Idempotent / a no-op in capture mode. This single post-stream
399
+ // sweep is the one choke point that precedes every downstream persist.
400
+ withholdSecretContentFromMessages(
401
+ initialStatus.messages,
402
+ initialStatus.subAgentExecutions,
403
+ );
404
+
311
405
  // Turn boundary (capture mode): capture the candidate change set from the
312
406
  // git diff and author CANDIDATE_CAPTURED, then stamp the flowed file-edit
313
407
  // rows with the change set id — they stay visible in place as
@@ -492,9 +586,10 @@ export function createDeepAgentActivities(config: Config) {
492
586
  }
493
587
 
494
588
  // Plan mode: the agent's final message is the plan. Publish it as a
495
- // first-class plan.md artifact so the UI can render a reviewable Plan
496
- // card and a follow-up Implement run can reference it. Read-only mode
497
- // produces no file to auto-publish, so this is the only artifact path.
589
+ // first-class plan artifact (named from the plan's title) so the UI can
590
+ // render a reviewable Plan card and a follow-up Implement run can
591
+ // reference it. Read-only mode produces no file to auto-publish, so
592
+ // this is the only artifact path.
498
593
  // Requires artifact storage; with none (proxy misconfig) the plan text
499
594
  // still lives in the final message — only the reviewable artifact is
500
595
  // skipped (mirrors Cursor's storage-guarded plan publish).
@@ -590,6 +685,10 @@ export function createDeepAgentActivities(config: Config) {
590
685
 
591
686
  } finally {
592
687
  await cleanup(setup);
688
+ // Release the workspace turn lock LAST — the next queued turn must
689
+ // not baseline until every mutation of this one has landed.
690
+ // Idempotent and non-throwing (see workspace-lock.ts).
691
+ await releaseWorkspaceLock?.();
593
692
  activityFinished();
594
693
  }
595
694
  },
@@ -7,8 +7,14 @@
7
7
  */
8
8
 
9
9
  import { relative } from "node:path";
10
+ import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
10
11
  import type { ProvisionResult, GitMetadata } from "../../shared/workspace/types.js";
11
12
  import { SourceType } from "../../shared/workspace/types.js";
13
+ import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
14
+ import {
15
+ buildImplementPlanDirective,
16
+ findApprovedPlanPath,
17
+ } from "../../shared/implement-plan-prompt.js";
12
18
 
13
19
  const RESPONSE_RULES = `
14
20
 
@@ -84,6 +90,22 @@ export interface PromptBuilderInput {
84
90
  workspaceFileRefs: string[];
85
91
  workspaceRoot: string;
86
92
  injectedFiles: InjectedFile[];
93
+ /**
94
+ * The execution's interaction mode. PLAN appends the shared plan-mode
95
+ * directive so the model knows the turn's deliverable is a plan document.
96
+ * Tool-level write enforcement is separate (see setup.ts permissions) —
97
+ * without this directive the model is silently read-only but never told
98
+ * to produce a plan.
99
+ */
100
+ interactionMode?: InteractionMode;
101
+ /**
102
+ * The execution is a Build-from-plan turn (spec.execution_config
103
+ * .build_from_plan): appends the shared implement-plan directive, pointing
104
+ * the model at the injected approved plan document (or, when the plan
105
+ * attachment did not materialize, at the conversation's plan). The user
106
+ * message itself is just a short label ("Build from plan").
107
+ */
108
+ buildFromPlan?: boolean;
87
109
  }
88
110
 
89
111
  export interface InjectedFile {
@@ -128,6 +150,23 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
128
150
  prompt += RESPONSE_RULES;
129
151
  prompt += SUB_AGENT_RULES;
130
152
 
153
+ // Last sections on purpose: these per-execution directives redefine the
154
+ // turn's deliverable, so they must be the freshest instruction the model
155
+ // reads. (PLAN and build_from_plan are mutually exclusive in practice —
156
+ // the build turn is always an Agent-mode execution.)
157
+ if (input.interactionMode === InteractionMode.PLAN) {
158
+ prompt += "\n\n## Plan mode\n\n" + PLAN_MODE_DIRECTIVE;
159
+ }
160
+
161
+ if (input.buildFromPlan) {
162
+ const planPath = findApprovedPlanPath(
163
+ input.injectedFiles.map((f) => f.path),
164
+ );
165
+ prompt +=
166
+ "\n\n## Implement the approved plan\n\n" +
167
+ buildImplementPlanDirective(planPath);
168
+ }
169
+
131
170
  return prompt;
132
171
  }
133
172
 
@@ -35,6 +35,7 @@ import { isGitWorkTree, isPathCapturable } from "../../shared/filereview/git-sub
35
35
  import { deriveCaptureMode } from "../../shared/filereview/capture.js";
36
36
  import { resolveWorkspacePath } from "../../shared/file-change.js";
37
37
  import { ensurePlatformDir } from "../../shared/workspace/platform-dir.js";
38
+ import { resolveSessionWorkspaceRoot } from "../../shared/workspace/session-root.js";
38
39
  import { buildWorkspaceFileTree } from "../../shared/workspace/file-tree.js";
39
40
  import { reportSetupProgress } from "../../shared/status.js";
40
41
  import { resolveEnvironment, type EnvironmentResult } from "./environment.js";
@@ -49,7 +50,7 @@ import { getDefaultModel } from "../../shared/model-registry.js";
49
50
  import { buildChatModel } from "../../shared/model-client.js";
50
51
  import {
51
52
  loadArtifactStorageConfig,
52
- createArtifactStorage,
53
+ resolveUsableArtifactStorage,
53
54
  type ArtifactStorage,
54
55
  } from "../../shared/artifact-storage.js";
55
56
  import {
@@ -201,20 +202,14 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
201
202
  await reportSetupProgress(client, executionId, "Resolving environment…");
202
203
  const envResult: EnvironmentResult = await resolveEnvironment(client, executionId);
203
204
 
204
- // Step 6: Create artifact storage. Best-effort, mirroring the Cursor harness:
205
- // `createArtifactStorage` throws only for proxy transport with a missing
206
- // token/endpoint (a misconfiguration; local storage never throws). We degrade
207
- // rather than crash an absent store flips capture mode off (deny-gate
208
- // fallback below) and disables offload, instead of failing the whole execution.
209
- let artifactStorage: ArtifactStorage | undefined;
210
- try {
211
- artifactStorage = createArtifactStorage(loadArtifactStorageConfig(config));
212
- } catch (storageErr) {
213
- console.warn(
214
- `[setup] artifact storage unavailable — capture degrades to the deny-gate ` +
215
- `and tool-output offload is disabled: execution=${executionId}, error=${storageErr}`,
216
- );
217
- }
205
+ // Step 6: Resolve a usable artifact store (shared with the Cursor harness so
206
+ // both degrade identically). Returns `undefined` never throws when there
207
+ // is no working substrate: a proxy misconfig OR an unwritable local path
208
+ // (which `createArtifactStorage` cannot detect, since it just holds a path).
209
+ // An absent store flips capture mode off (deny-gate fallback below) and
210
+ // disables offload, instead of flowing writes then crashing at the boundary.
211
+ const artifactStorage: ArtifactStorage | undefined =
212
+ await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
218
213
 
219
214
  // Step 7: Provision workspace
220
215
  await reportSetupProgress(client, executionId, "Initializing workspace…");
@@ -372,6 +367,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
372
367
  workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
373
368
  workspaceRoot: workspaceBackend.rootDir,
374
369
  injectedFiles,
370
+ interactionMode: execution.spec!.executionConfig?.interactionMode,
371
+ buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
375
372
  });
376
373
 
377
374
  // Step 9: Construct the LLM model. Resolution to the provider API id
@@ -631,6 +628,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
631
628
 
632
629
  /**
633
630
  * Provision workspace from session workspace entries.
631
+ *
632
+ * A session with no entries gets its own empty per-session directory (see
633
+ * shared/workspace/session-root.ts) — never the shared root, which would
634
+ * leak other sessions' files into it. Mirrors the Cursor harness's
635
+ * provisionCursorWorkspace exactly.
634
636
  */
635
637
  async function provisionWorkspace(
636
638
  config: Config,
@@ -639,13 +641,19 @@ async function provisionWorkspace(
639
641
  sessionId: string,
640
642
  ): Promise<{ workspaceBackend: WorkspaceBackend; provisionResults: ProvisionResult[] }> {
641
643
  const platformDir = await ensurePlatformDir(sessionId);
642
- const workspaceBackend = new LocalWorkspaceBackend(config.workspaceRootDir, platformDir);
643
644
 
644
645
  const workspaceEntries = session.spec!.workspaceEntries || [];
645
646
  if (workspaceEntries.length === 0) {
646
- return { workspaceBackend, provisionResults: [] };
647
+ const sessionRoot = await resolveSessionWorkspaceRoot(
648
+ config.workspaceRootDir, workspaceEntries, sessionId,
649
+ );
650
+ return {
651
+ workspaceBackend: new LocalWorkspaceBackend(sessionRoot, platformDir),
652
+ provisionResults: [],
653
+ };
647
654
  }
648
655
 
656
+ const workspaceBackend = new LocalWorkspaceBackend(config.workspaceRootDir, platformDir);
649
657
  const provisioner = new WorkspaceProvisioner();
650
658
  const provisionResults = await provisioner.provisionAll(
651
659
  workspaceEntries.map(entry => ({
@@ -118,10 +118,15 @@ export async function streamExecutionV3(
118
118
  }
119
119
 
120
120
  statusBuilder.syncSubAgentExecutions();
121
+ // Mid-run live capture (DD-32): attach file_change_progress to the live
122
+ // status before it is persisted. Injected so this loop stays ignorant of
123
+ // file-review specifics; a no-op when the hook is absent or nothing changed.
124
+ const statusToPersist = statusBuilder.currentStatus;
125
+ await deps.beforePersist?.(statusToPersist);
121
126
  const signal = await persistStatus(
122
127
  client,
123
128
  executionId,
124
- statusBuilder.currentStatus,
129
+ statusToPersist,
125
130
  { offload, retry: retryOptions },
126
131
  );
127
132
  scheduler.markUpdateSent(eventsProcessed);
@@ -83,6 +83,13 @@ export interface StreamDependencies {
83
83
  readonly approvalProvider?: ApprovalPolicyProvider;
84
84
  /** Streaming protocol version. Defaults to "v2" if unset. */
85
85
  readonly streamVersion?: "v2" | "v3";
86
+ /**
87
+ * Optional hook invoked with the live status just before each scheduled
88
+ * persist. Used by mid-run live capture (DD-32) to attach file_change_progress.
89
+ * Kept as an injected callback (closing over the turn's baseline tree / change
90
+ * set id in index.ts) so this loop stays ignorant of file-review specifics.
91
+ */
92
+ readonly beforePersist?: (status: AgentExecutionStatus) => Promise<void>;
86
93
  }
87
94
 
88
95
  export interface StreamResult {
@@ -189,10 +196,14 @@ async function streamExecutionV2(
189
196
  statusBuilder.clearForceFlag();
190
197
  }
191
198
 
199
+ // Mid-run live capture (DD-32): attach file_change_progress to the live
200
+ // status before persisting (see streaming-v3.ts for the rationale).
201
+ const statusToPersist = statusBuilder.currentStatus;
202
+ await deps.beforePersist?.(statusToPersist);
192
203
  const signal = await persistStatus(
193
204
  client,
194
205
  executionId,
195
- statusBuilder.currentStatus,
206
+ statusToPersist,
196
207
  { offload, retry: retryOptions },
197
208
  );
198
209
  scheduler.markUpdateSent(eventsProcessed);
package/src/config.ts CHANGED
@@ -37,6 +37,12 @@ import { homedir, tmpdir } from "node:os";
37
37
  */
38
38
  export const DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS = 180_000;
39
39
 
40
+ // Re-exported so the runner/manager options mappers default the lock-wait
41
+ // bound from the same constant the lock module owns (no drift across the
42
+ // three construction sites — mirrors DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS).
43
+ export { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
44
+ import { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
45
+
40
46
  export interface Config {
41
47
  readonly taskQueue: string;
42
48
  readonly temporalAddress: string;
@@ -83,6 +89,12 @@ export interface Config {
83
89
  * heartbeatTimeout (process liveness) and the 30s keep-alive heartbeat.
84
90
  */
85
91
  readonly cursorStreamStallTimeoutMs: number;
92
+ /**
93
+ * Max wait (milliseconds) for the per-workspace turn lock before failing
94
+ * the execution with a "workspace is in use by another session" error.
95
+ * See shared/workspace/workspace-lock.ts for the serialization model.
96
+ */
97
+ readonly workspaceLockTimeoutMs: number;
86
98
  /** Shared mutable token reference for dynamic token updates (manager mode). */
87
99
  readonly stigmerTokenRef?: { current: string | null };
88
100
  }
@@ -145,6 +157,10 @@ export function loadConfig(): Config {
145
157
  ? parseInt(process.env.CURSOR_STREAM_STALL_TIMEOUT_MS, 10)
146
158
  : DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS;
147
159
 
160
+ const workspaceLockTimeoutMs = process.env.WORKSPACE_LOCK_TIMEOUT_MS
161
+ ? parseInt(process.env.WORKSPACE_LOCK_TIMEOUT_MS, 10)
162
+ : DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS;
163
+
148
164
  return {
149
165
  taskQueue,
150
166
  temporalAddress,
@@ -162,6 +178,7 @@ export function loadConfig(): Config {
162
178
  checkpointerProxyEndpoint,
163
179
  primaryModel,
164
180
  cursorStreamStallTimeoutMs,
181
+ workspaceLockTimeoutMs,
165
182
  };
166
183
  }
167
184