@stigmer/runner 3.1.4 → 3.1.6

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 (74) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
  3. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.js +40 -2
  5. package/dist/activities/execute-cursor/index.js.map +1 -1
  6. package/dist/activities/execute-cursor/skill-resolver.js +1 -1
  7. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  8. package/dist/activities/execute-cursor/workspace-provision.d.ts +23 -4
  9. package/dist/activities/execute-cursor/workspace-provision.js +17 -6
  10. package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/index.js +39 -6
  12. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  13. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +1 -1
  14. package/dist/activities/execute-deep-agent/post-stream.d.ts +1 -1
  15. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +1 -1
  16. package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +1 -1
  17. package/dist/activities/execute-deep-agent/streaming.d.ts +1 -1
  18. package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +1 -1
  19. package/dist/shared/execution-status-writer.d.ts +31 -0
  20. package/dist/shared/execution-status-writer.js +43 -0
  21. package/dist/shared/execution-status-writer.js.map +1 -0
  22. package/dist/shared/filereview/git-substrate.js +4 -0
  23. package/dist/shared/filereview/git-substrate.js.map +1 -1
  24. package/dist/shared/workspace/git-identity.d.ts +36 -0
  25. package/dist/shared/workspace/git-identity.js +39 -0
  26. package/dist/shared/workspace/git-identity.js.map +1 -0
  27. package/dist/shared/workspace/local-backend.d.ts +6 -2
  28. package/dist/shared/workspace/local-backend.js +6 -2
  29. package/dist/shared/workspace/local-backend.js.map +1 -1
  30. package/dist/shared/workspace/stigmer-link.d.ts +54 -0
  31. package/dist/shared/workspace/stigmer-link.js +92 -0
  32. package/dist/shared/workspace/stigmer-link.js.map +1 -0
  33. package/dist/shared/workspace/types.d.ts +6 -4
  34. package/dist/shared/workspace/writeback-coordinator.d.ts +120 -0
  35. package/dist/shared/workspace/writeback-coordinator.js +393 -0
  36. package/dist/shared/workspace/writeback-coordinator.js.map +1 -0
  37. package/package.json +2 -2
  38. package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
  39. package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
  40. package/src/activities/execute-cursor/index.ts +42 -2
  41. package/src/activities/execute-cursor/skill-resolver.ts +1 -1
  42. package/src/activities/execute-cursor/workspace-provision.ts +39 -7
  43. package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +1 -1
  44. package/src/activities/execute-deep-agent/index.ts +44 -6
  45. package/src/activities/execute-deep-agent/inline-publisher.ts +1 -1
  46. package/src/activities/execute-deep-agent/post-stream.ts +1 -1
  47. package/src/activities/execute-deep-agent/status-builder.ts +1 -1
  48. package/src/activities/execute-deep-agent/streaming-side-effects.ts +1 -1
  49. package/src/activities/execute-deep-agent/streaming-terminal.ts +1 -1
  50. package/src/activities/execute-deep-agent/streaming.ts +1 -1
  51. package/src/activities/execute-deep-agent/v3-status-builder.ts +1 -1
  52. package/src/shared/execution-status-writer.ts +56 -0
  53. package/src/shared/filereview/git-substrate.ts +4 -0
  54. package/src/shared/workspace/__tests__/git-identity.test.ts +124 -0
  55. package/src/shared/workspace/__tests__/stigmer-link.test.ts +173 -0
  56. package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +230 -0
  57. package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +477 -0
  58. package/src/shared/workspace/git-identity.ts +41 -0
  59. package/src/shared/workspace/local-backend.ts +6 -2
  60. package/src/shared/workspace/stigmer-link.ts +97 -0
  61. package/src/shared/workspace/types.ts +6 -4
  62. package/src/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +209 -93
  63. package/dist/activities/execute-cursor/stigmer-link.d.ts +0 -35
  64. package/dist/activities/execute-cursor/stigmer-link.js +0 -73
  65. package/dist/activities/execute-cursor/stigmer-link.js.map +0 -1
  66. package/dist/activities/execute-deep-agent/execution-status-writer.d.ts +0 -17
  67. package/dist/activities/execute-deep-agent/execution-status-writer.js +0 -9
  68. package/dist/activities/execute-deep-agent/execution-status-writer.js.map +0 -1
  69. package/dist/activities/execute-deep-agent/writeback-coordinator.d.ts +0 -71
  70. package/dist/activities/execute-deep-agent/writeback-coordinator.js +0 -295
  71. package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
  72. package/src/activities/execute-cursor/stigmer-link.ts +0 -78
  73. package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -399
  74. package/src/activities/execute-deep-agent/execution-status-writer.ts +0 -19
@@ -138,12 +138,13 @@ describe("provisionCursorWorkspace", () => {
138
138
  const workspaceRoot = join(tmpRoot, "workspace");
139
139
  mkdirSync(workspaceRoot, { recursive: true });
140
140
 
141
- const dirs = await provisionCursorWorkspace(
142
- makeConfig(workspaceRoot),
143
- gitRepoSession(source),
144
- {},
145
- "test-session-clone",
146
- );
141
+ const { workspaceDirs: dirs, provisionResults, workspaceBackend } =
142
+ await provisionCursorWorkspace(
143
+ makeConfig(workspaceRoot),
144
+ gitRepoSession(source),
145
+ {},
146
+ "test-session-clone",
147
+ );
147
148
 
148
149
  expect(dirs).toHaveLength(1);
149
150
  const clonedFile = join(dirs[0], MARKER_FILE);
@@ -151,6 +152,12 @@ describe("provisionCursorWorkspace", () => {
151
152
  expect(readFileSync(clonedFile, "utf-8")).toBe(MARKER_CONTENT);
152
153
  // The clone must be a real git repo (so the agent can run git in it).
153
154
  expect(existsSync(join(dirs[0], ".git"))).toBe(true);
155
+
156
+ // The provision results carry the git metadata the write-back
157
+ // coordinator consumes (repo URL, base branch), through the same backend.
158
+ expect(provisionResults).toHaveLength(1);
159
+ expect(provisionResults[0].gitMetadata?.branch).toBe("main");
160
+ expect(workspaceBackend).toBeDefined();
154
161
  }, GIT_TEST_TIMEOUT_MS);
155
162
 
156
163
  it("clones into a workspace root that already contains lost+found (PVC simulation)", async () => {
@@ -161,7 +168,7 @@ describe("provisionCursorWorkspace", () => {
161
168
  mkdirSync(join(workspaceRoot, "lost+found"), { recursive: true });
162
169
  writeFileSync(join(workspaceRoot, "lost+found", "stray"), "fsck-artifact");
163
170
 
164
- const dirs = await provisionCursorWorkspace(
171
+ const { workspaceDirs: dirs } = await provisionCursorWorkspace(
165
172
  makeConfig(workspaceRoot),
166
173
  gitRepoSession(source),
167
174
  {},
@@ -196,7 +203,7 @@ describe("provisionCursorWorkspace", () => {
196
203
  const workspaceRoot = join(tmpRoot, "default-branch-workspace");
197
204
  mkdirSync(workspaceRoot, { recursive: true });
198
205
 
199
- const dirs = await provisionCursorWorkspace(
206
+ const { workspaceDirs: dirs } = await provisionCursorWorkspace(
200
207
  makeConfig(workspaceRoot),
201
208
  gitRepoSessionNoBranch(source),
202
209
  {},
@@ -221,7 +228,7 @@ describe("provisionCursorWorkspace", () => {
221
228
  // a worktree here, so it must be left untouched.
222
229
  mkdirSync(join(workspaceRoot, "lost+found"), { recursive: true });
223
230
 
224
- const dirs = await provisionCursorWorkspace(
231
+ const { workspaceDirs: dirs } = await provisionCursorWorkspace(
225
232
  makeConfig(workspaceRoot),
226
233
  multiGitRepoSession([
227
234
  { name: "frontend", url: frontend },
@@ -259,13 +266,14 @@ describe("provisionCursorWorkspace", () => {
259
266
  // Leftovers from other sessions at the shared root must not be visible.
260
267
  writeFileSync(join(workspaceRoot, "other-session-leftover.md"), "not yours");
261
268
 
262
- const dirs = await provisionCursorWorkspace(
269
+ const { workspaceDirs: dirs, provisionResults } = await provisionCursorWorkspace(
263
270
  makeConfig(workspaceRoot),
264
271
  emptySession(),
265
272
  {},
266
273
  "test-session-empty",
267
274
  );
268
275
 
276
+ expect(provisionResults).toEqual([]);
269
277
  expect(dirs).toEqual([join(workspaceRoot, "sessions", "test-session-empty")]);
270
278
  expect(existsSync(dirs[0])).toBe(true);
271
279
  expect(existsSync(join(dirs[0], "other-session-leftover.md"))).toBe(false);
@@ -276,9 +284,9 @@ describe("provisionCursorWorkspace", () => {
276
284
  mkdirSync(workspaceRoot, { recursive: true });
277
285
  const config = makeConfig(workspaceRoot);
278
286
 
279
- const turn1 = await provisionCursorWorkspace(config, emptySession(), {}, "stable-session");
287
+ const turn1 = (await provisionCursorWorkspace(config, emptySession(), {}, "stable-session")).workspaceDirs;
280
288
  writeFileSync(join(turn1[0], "notes.md"), "turn 1 output");
281
- const turn2 = await provisionCursorWorkspace(config, emptySession(), {}, "stable-session");
289
+ const turn2 = (await provisionCursorWorkspace(config, emptySession(), {}, "stable-session")).workspaceDirs;
282
290
 
283
291
  expect(turn2).toEqual(turn1);
284
292
  expect(readFileSync(join(turn2[0], "notes.md"), "utf-8")).toBe("turn 1 output");
@@ -289,8 +297,8 @@ describe("provisionCursorWorkspace", () => {
289
297
  mkdirSync(workspaceRoot, { recursive: true });
290
298
  const config = makeConfig(workspaceRoot);
291
299
 
292
- const [dirA] = await provisionCursorWorkspace(config, emptySession(), {}, "session-a");
293
- const [dirB] = await provisionCursorWorkspace(config, emptySession(), {}, "session-b");
300
+ const [dirA] = (await provisionCursorWorkspace(config, emptySession(), {}, "session-a")).workspaceDirs;
301
+ const [dirB] = (await provisionCursorWorkspace(config, emptySession(), {}, "session-b")).workspaceDirs;
294
302
 
295
303
  expect(dirA).not.toBe(dirB);
296
304
  writeFileSync(join(dirA, "a.md"), "session a");
@@ -304,7 +312,7 @@ describe("provisionCursorWorkspace", () => {
304
312
  const workspaceRoot = join(tmpRoot, "workspace-local");
305
313
  mkdirSync(workspaceRoot, { recursive: true });
306
314
 
307
- const dirs = await provisionCursorWorkspace(
315
+ const { workspaceDirs: dirs } = await provisionCursorWorkspace(
308
316
  makeConfig(workspaceRoot),
309
317
  localPathSession(projectDir),
310
318
  {},
@@ -34,7 +34,7 @@ import { join, basename } from "node:path";
34
34
  import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
35
35
  import type { ArtifactStorage } from "../../shared/artifact-storage.js";
36
36
  import { getPlatformDir } from "../../shared/workspace/platform-dir.js";
37
- import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "./stigmer-link.js";
37
+ import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
38
38
 
39
39
  const INPUTS_SUBDIR = "inputs";
40
40
 
@@ -61,7 +61,7 @@ import { resolveExecutionEnv } from "./env-resolver.js";
61
61
  import { resolveBlueprint } from "./blueprint-resolver.js";
62
62
  import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
63
63
  import { resolveSkills } from "./skill-resolver.js";
64
- import { removeStigmerSymlink } from "./stigmer-link.js";
64
+ import { removeStigmerSymlink } from "../../shared/workspace/stigmer-link.js";
65
65
  import { resolveAttachments } from "./attachment-resolver.js";
66
66
  import { buildEnhancedPrompt, buildReinvocationPrompt, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
67
67
  import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
@@ -99,6 +99,8 @@ import {
99
99
  import { deriveExecutionFingerprintKey } from "../../shared/approval-fingerprint.js";
100
100
  import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
101
101
  import { provisionCursorWorkspace } from "./workspace-provision.js";
102
+ import { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
103
+ import { statusProtoWriter } from "../../shared/execution-status-writer.js";
102
104
  import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-interceptor.js";
103
105
  import { closeProxySessions } from "./http2-interceptor.js";
104
106
  import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pricing.js";
@@ -277,11 +279,32 @@ async function executeCursorInner(
277
279
  // native harness. Git provisioning is idempotent across multi-turn and
278
280
  // HITL reinvocations.
279
281
  await reportSetupProgress(client, executionId, "Provisioning workspace");
280
- blueprint.workspaceDirs = await provisionCursorWorkspace(
282
+ const workspaceProvision = await provisionCursorWorkspace(
281
283
  config, session, envVars, sessionId ?? "",
282
284
  );
285
+ blueprint.workspaceDirs = workspaceProvision.workspaceDirs;
283
286
  heartbeat();
284
287
 
288
+ // Git write-back: pushes the session's APPROVED tree to the session
289
+ // branch (stigmer/<session-id>) and keeps one PR open — the same
290
+ // approval-gated model as the deep-agent harness (its
291
+ // processCaptureWriteback). Finalize runs at exactly two seams below:
292
+ // the pure-file-review resume (after decisions reconcile) and terminal
293
+ // completion. Never mid-turn: the working tree is speculative until
294
+ // reviewed. Non-eligible workspaces (local paths, no credentials) make
295
+ // this a no-op coordinator.
296
+ const writebackCoordinator = workspaceProvision.provisionResults.length > 0
297
+ ? new WriteBackCoordinator({
298
+ statusWriter: statusProtoWriter(status),
299
+ executionId,
300
+ sessionId: sessionId ?? "",
301
+ githubToken: envVars.GITHUB_TOKEN ?? "",
302
+ provisionResults: workspaceProvision.provisionResults,
303
+ workspaceEntries: session.spec?.workspaceEntries ?? [],
304
+ workspaceBackend: workspaceProvision.workspaceBackend,
305
+ })
306
+ : null;
307
+
285
308
  // Apply-then-review is the universal file-review model (Slice 2c). When the
286
309
  // primary workspace is a real git work tree, file edits flow during the turn
287
310
  // and are captured per-file from the git diff at the turn boundary
@@ -478,6 +501,14 @@ async function executeCursorInner(
478
501
  // (Cursor-like). The reconcile is done; the execution is complete.
479
502
  status.phase = ExecutionPhase.EXECUTION_COMPLETED;
480
503
  status.completedAt = utcTimestamp();
504
+ // Push the APPROVED tree — reconcile snapped rejected files back to
505
+ // baseline, so what finalize commits is exactly what the user kept.
506
+ // Mirrors the deep-agent's processCaptureWriteback: after reconcile,
507
+ // before persist, never on a failed reconcile (diverged bytes must
508
+ // not reach the remote).
509
+ if (!fileReviewFailed && writebackCoordinator) {
510
+ await writebackCoordinator.finalize();
511
+ }
481
512
  if (fileReviewFailed) {
482
513
  // What-you-approve-is-what-applies could not be honored (on-disk bytes
483
514
  // diverged from the approved digest). Surface it to the human; the
@@ -1566,6 +1597,15 @@ async function executeCursorInner(
1566
1597
  );
1567
1598
  }
1568
1599
 
1600
+ // Write-back safety net on terminal completion. A capture-mode turn with
1601
+ // captured changes always paused above (boundary.waiting), so reaching
1602
+ // here means no reviewable delta this turn and this is normally a no-op —
1603
+ // it exists for the same reason the deep-agent finalizes on completion:
1604
+ // stragglers outside the capture (and it never runs mid-turn).
1605
+ if (status.phase === ExecutionPhase.EXECUTION_COMPLETED && writebackCoordinator) {
1606
+ await writebackCoordinator.finalize();
1607
+ }
1608
+
1569
1609
  // NOW persist — subscriber sees COMPLETED + structured_output atomically
1570
1610
  await persist(status);
1571
1611
 
@@ -17,7 +17,7 @@ import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/ap
17
17
  import type { SkillMetadata } from "./prompt-builder.js";
18
18
  import { getPlatformDir } from "../../shared/workspace/platform-dir.js";
19
19
  import { extractZipFileEntries } from "../../shared/zip-extract.js";
20
- import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "./stigmer-link.js";
20
+ import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
21
21
 
22
22
  const SKILLS_SUBDIR = "skills";
23
23
 
@@ -10,18 +10,38 @@
10
10
  import type { Config } from "../../config.js";
11
11
  import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
12
12
  import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
13
+ import type { ProvisionResult, WorkspaceBackend } from "../../shared/workspace/types.js";
13
14
  import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
14
15
  import { ensurePlatformDir } from "../../shared/workspace/platform-dir.js";
15
16
  import { resolveSessionWorkspaceRoot } from "../../shared/workspace/session-root.js";
16
17
 
18
+ /** What {@link provisionCursorWorkspace} hands back to the harness. */
19
+ export interface CursorWorkspaceProvision {
20
+ /** The directories the agent should operate in (never empty). */
21
+ readonly workspaceDirs: string[];
22
+ /**
23
+ * Per-entry provision outcomes — carries the git metadata (repo URL, base
24
+ * branch, credential state) the write-back coordinator needs. Empty for a
25
+ * session with no workspace entries.
26
+ */
27
+ readonly provisionResults: ProvisionResult[];
28
+ /**
29
+ * The backend the entries were provisioned through — the write-back
30
+ * coordinator executes its git commands through it.
31
+ */
32
+ readonly workspaceBackend: WorkspaceBackend;
33
+ }
34
+
17
35
  /**
18
36
  * Provision the session's workspace entries for a LOCAL Cursor agent.
19
37
  *
20
38
  * Clones git-repo entries (using the user's GITHUB_TOKEN from the resolved
21
39
  * execution environment) and mounts local-path entries, then returns the
22
- * directories the agent should operate in. A session with no workspace
23
- * entries gets its own empty per-session directory (see session-root.ts)
24
- * never the shared root, which would leak other sessions' files into it.
40
+ * directories the agent should operate in along with the provision results
41
+ * and backend the git write-back coordinator needs. A session with no
42
+ * workspace entries gets its own empty per-session directory (see
43
+ * session-root.ts) — never the shared root, which would leak other sessions'
44
+ * files into it.
25
45
  *
26
46
  * provisionGit is idempotent (it reuses an existing clone), so this is safe
27
47
  * to call on every execution, including multi-turn and HITL reinvocations.
@@ -31,13 +51,21 @@ export async function provisionCursorWorkspace(
31
51
  session: Session,
32
52
  envVars: Record<string, string>,
33
53
  sessionId: string,
34
- ): Promise<string[]> {
54
+ ): Promise<CursorWorkspaceProvision> {
35
55
  const entries = session.spec?.workspaceEntries ?? [];
56
+ const platformDir = await ensurePlatformDir(sessionId);
57
+
36
58
  if (entries.length === 0) {
37
- return [await resolveSessionWorkspaceRoot(config.workspaceRootDir, entries, sessionId)];
59
+ const sessionRoot = await resolveSessionWorkspaceRoot(
60
+ config.workspaceRootDir, entries, sessionId,
61
+ );
62
+ return {
63
+ workspaceDirs: [sessionRoot],
64
+ provisionResults: [],
65
+ workspaceBackend: new LocalWorkspaceBackend(sessionRoot, platformDir),
66
+ };
38
67
  }
39
68
 
40
- const platformDir = await ensurePlatformDir(sessionId);
41
69
  const backend = new LocalWorkspaceBackend(config.workspaceRootDir, platformDir);
42
70
 
43
71
  const provisioner = new WorkspaceProvisioner();
@@ -53,5 +81,9 @@ export async function provisionCursorWorkspace(
53
81
  .map((result) => result.rootDir)
54
82
  .filter((dir): dir is string => Boolean(dir));
55
83
 
56
- return dirs.length > 0 ? dirs : [config.workspaceRootDir];
84
+ return {
85
+ workspaceDirs: dirs.length > 0 ? dirs : [config.workspaceRootDir],
86
+ provisionResults: results,
87
+ workspaceBackend: backend,
88
+ };
57
89
  }
@@ -3,7 +3,7 @@ import { create } from "@bufbuild/protobuf";
3
3
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
4
4
  import { processPostStream } from "../post-stream.js";
5
5
  import type { InlinePublisher } from "../inline-publisher.js";
6
- import type { WriteBackCoordinator } from "../writeback-coordinator.js";
6
+ import type { WriteBackCoordinator } from "../../../shared/workspace/writeback-coordinator.js";
7
7
 
8
8
  function mockInlinePublisher(): InlinePublisher {
9
9
  return {
@@ -26,6 +26,10 @@ import {
26
26
  WorkspaceLockTimeoutError,
27
27
  type ReleaseWorkspaceLock,
28
28
  } from "../../shared/workspace/workspace-lock.js";
29
+ import {
30
+ ensureStigmerSymlink,
31
+ removeStigmerSymlink,
32
+ } from "../../shared/workspace/stigmer-link.js";
29
33
  import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
30
34
  import { publishPlanArtifact } from "../../shared/plan-artifact.js";
31
35
  import { classifyTool } from "../../shared/tool-kind.js";
@@ -41,7 +45,7 @@ import { streamExecution, type StreamResult } from "./streaming.js";
41
45
  import { loadStreamingConfig } from "../../shared/streaming-scheduler.js";
42
46
  import { StatusBuilder } from "./status-builder.js";
43
47
  import { InlinePublisher } from "./inline-publisher.js";
44
- import { WriteBackCoordinator } from "./writeback-coordinator.js";
48
+ import { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
45
49
  import { processPostStream } from "./post-stream.js";
46
50
  import { resolveResumeInput, type GraphStateSnapshot } from "./hitl.js";
47
51
  import { captureApprovalArtifacts } from "./approval-file-change.js";
@@ -180,6 +184,13 @@ export function createDeepAgentActivities(config: Config) {
180
184
  ? new WriteBackCoordinator({
181
185
  statusWriter: statusBuilder,
182
186
  executionId,
187
+ // Branch/PR are session-scoped: every turn of this session
188
+ // appends commits to the same stigmer/<session-id> branch.
189
+ sessionId: setup.session.metadata?.id ?? "",
190
+ // The PR API token, plumbed from the resolved execution env —
191
+ // gitMetadata.repoUrl is token-stripped by construction, so the
192
+ // coordinator can never recover it from provisioning state.
193
+ githubToken: setup.mergedEnvVars.GITHUB_TOKEN ?? "",
183
194
  provisionResults: setup.provisionResults,
184
195
  workspaceEntries: workspaceEntries as any,
185
196
  workspaceBackend: setup.workspaceBackend,
@@ -229,6 +240,22 @@ export function createDeepAgentActivities(config: Config) {
229
240
  throw lockErr;
230
241
  }
231
242
 
243
+ // Bridge the workspace to the session's platform dir so the agent's
244
+ // file tools can read platform-mounted content (`.stigmer/inputs/…`
245
+ // attachments incl. the approved plan, `.stigmer/skills/…`) — the
246
+ // deepagents FilesystemBackend resolves paths against the workspace
247
+ // root and knows nothing of the LocalWorkspaceBackend's `.stigmer`
248
+ // routing. Two ordering invariants (see shared/workspace/stigmer-link.ts):
249
+ // - AFTER the lock: the link is a tree mutation; sessions sharing one
250
+ // attached directory must not re-point it under a running turn.
251
+ // - BEFORE the baseline capture below: present in both the baseline
252
+ // and candidate trees, the link cancels out of the file-review diff
253
+ // even when the workspace's git excludes do not list `.stigmer`.
254
+ // Removed in cleanup() (the finally below), before the lock releases.
255
+ if (setup.workspaceBackend.platformDir) {
256
+ await ensureStigmerSymlink(gitRoot, setup.workspaceBackend.platformDir);
257
+ }
258
+
232
259
  // (1) Capture-mode resume — reconcile any DECIDED change set FIRST (this
233
260
  // drops the per-execution refs), before the next baseline re-pins them
234
261
  // (refs are executionId-keyed). Then (2) short-circuit a pure file-review
@@ -315,8 +342,10 @@ export function createDeepAgentActivities(config: Config) {
315
342
  }
316
343
 
317
344
  // Turn start (capture mode): pin the pre-turn tree + author BASELINE. Taken
318
- // AFTER any reconcile above and AFTER performSetup's workspace writes, so
319
- // the baseline absorbs the reconciled state and the runner-owned files.
345
+ // AFTER any reconcile above and AFTER performSetup's workspace writes and
346
+ // the `.stigmer` symlink above, so the baseline absorbs the reconciled
347
+ // state and the runner-owned files (the symlink then cancels out of the
348
+ // baseline→candidate diff).
320
349
  let captureBaselineTree = "";
321
350
  // Snapshot the sub-agent tool-call ids that exist BEFORE this turn's
322
351
  // stream, so the turn-boundary stamp scopes itself to sub-agent rows
@@ -398,9 +427,11 @@ export function createDeepAgentActivities(config: Config) {
398
427
  // Mid-run live capture (DD-32 / DD-33): attach file_change_progress
399
428
  // before each scheduled persist, throttled by the floor inside
400
429
  // captureFileChangeProgress. The substrate (git / non-git CAS / hybrid)
401
- // was chosen for this turn above; deep-agent writes no runner-owned gate
402
- // files into the tree, so the git slice needs no excludePaths — matching
403
- // its turn-boundary candidate capture.
430
+ // was chosen for this turn above; the deep-agent's only runner-owned
431
+ // tree entry is the `.stigmer` symlink, created before the baseline
432
+ // so it appears identically in every capture and cancels out of the
433
+ // diff — the git slice therefore needs no excludePaths, matching its
434
+ // turn-boundary candidate capture.
404
435
  beforePersist: async (status) => {
405
436
  if (!progressSubstrate) return;
406
437
  await captureFileChangeProgress({
@@ -928,5 +959,12 @@ async function cleanup(setup: SetupResult | null): Promise<void> {
928
959
  console.warn("[ExecuteDeepAgent] MCP connection cleanup failed:", err);
929
960
  }
930
961
  }
962
+
963
+ // Drop the workspace→platform `.stigmer` symlink so an attached repo is
964
+ // left untouched between turns (issue #173 semantics; a multi-turn session
965
+ // recreates it next turn). Runs in the activity finally, BEFORE the
966
+ // workspace lock releases, so the next queued turn baselines a clean tree.
967
+ // Best-effort and symlink-only: a real `.stigmer` directory is never removed.
968
+ await removeStigmerSymlink(setup.workspaceBackend.rootDir);
931
969
  }
932
970
 
@@ -20,7 +20,7 @@ import {
20
20
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
21
  import type { ArtifactStorage } from "../../shared/artifact-storage.js";
22
22
  import type { WorkspaceBackend } from "../../shared/workspace/types.js";
23
- import type { ExecutionStatusWriter } from "./execution-status-writer.js";
23
+ import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
24
24
  import { utcTimestamp } from "../../shared/status.js";
25
25
  import { isSecretLikePath } from "../../shared/filereview/secret-paths.js";
26
26
 
@@ -13,7 +13,7 @@
13
13
  import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
14
14
  import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
15
15
  import type { InlinePublisher } from "./inline-publisher.js";
16
- import type { WriteBackCoordinator } from "./writeback-coordinator.js";
16
+ import type { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
17
17
  import { autoPublishWrittenFiles } from "./auto-publish.js";
18
18
 
19
19
  export interface PostStreamOptions {
@@ -31,7 +31,7 @@ import { classifyTool } from "../../shared/tool-kind.js";
31
31
  import { applyTodoUpdate } from "../../shared/todos.js";
32
32
  import { ExecutionState } from "./execution-state.js";
33
33
  import { utcTimestamp } from "../../shared/status.js";
34
- import type { ExecutionStatusWriter } from "./execution-status-writer.js";
34
+ import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
35
35
  import {
36
36
  UsageAccumulator,
37
37
  extractToolResult,
@@ -8,7 +8,7 @@
8
8
 
9
9
  import type { V3ProtocolEvent } from "./v3-event-recorder.js";
10
10
  import type { InlinePublisher } from "./inline-publisher.js";
11
- import type { WriteBackCoordinator } from "./writeback-coordinator.js";
11
+ import type { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
12
12
  import { extractFilePath, isFileModifyingTool } from "../../shared/file-tools.js";
13
13
 
14
14
  interface CachedToolInput {
@@ -8,7 +8,7 @@
8
8
  import { create } from "@bufbuild/protobuf";
9
9
  import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
10
10
  import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
11
- import type { ExecutionStatusWriter } from "./execution-status-writer.js";
11
+ import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
12
12
  import type { StreamResult } from "./streaming.js";
13
13
  import { slimStatus, utcTimestamp } from "../../shared/status.js";
14
14
 
@@ -39,7 +39,7 @@ import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
39
39
  import type { StigmerClient } from "../../client/stigmer-client.js";
40
40
  import type { GracefulStopMiddleware } from "../../middleware/index.js";
41
41
  import type { InlinePublisher } from "./inline-publisher.js";
42
- import type { WriteBackCoordinator } from "./writeback-coordinator.js";
42
+ import type { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
43
43
  import { createV2EventRecorder } from "./event-recorder.js";
44
44
  import { streamExecutionV3 } from "./streaming-v3.js";
45
45
  import { extractFilePath, isFileModifyingTool } from "../../shared/file-tools.js";
@@ -30,7 +30,7 @@ import { classifyTool } from "../../shared/tool-kind.js";
30
30
  import { applyTodoUpdate } from "../../shared/todos.js";
31
31
  import { ExecutionState } from "./execution-state.js";
32
32
  import { utcTimestamp } from "../../shared/status.js";
33
- import type { ExecutionStatusWriter } from "./execution-status-writer.js";
33
+ import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
34
34
  import type { ApprovalPolicyProvider } from "./status-builder.js";
35
35
  import type { StigmerRunEvent, V3UsagePayload } from "./v3-events.js";
36
36
  import { namespaceDepth } from "./v3-events.js";
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared interface for proto mutation consumed by side-effect classes
3
+ * (InlinePublisher, WriteBackCoordinator) and streaming loops.
4
+ *
5
+ * The deep-agent harness implements it on its v2 StatusBuilder and
6
+ * V3StatusBuilder; the Cursor harness — which mutates a bare status proto
7
+ * with no builder — uses {@link statusProtoWriter}. Side-effect classes
8
+ * depend only on this interface, never on a specific implementation.
9
+ */
10
+
11
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
12
+ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
13
+ import type { WorkspaceWriteBack } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
14
+
15
+ export interface ExecutionStatusWriter {
16
+ readonly currentStatus: AgentExecutionStatus;
17
+ readonly forceNextUpdate: boolean;
18
+ clearForceFlag(): void;
19
+ addArtifact(artifact: ExecutionArtifact): void;
20
+ addWriteBack(wb: WorkspaceWriteBack): void;
21
+ }
22
+
23
+ /**
24
+ * An {@link ExecutionStatusWriter} over a bare status proto, for harnesses
25
+ * that mutate the status directly instead of through a builder (the Cursor
26
+ * harness). Mutations apply synchronously to the given proto; the caller's
27
+ * own persist points pick them up — the force flag is a streaming-loop
28
+ * concern the direct-mutation style has no use for, so it stays `false`.
29
+ *
30
+ * Write-backs upsert by `workspaceEntryName` — the same contract as the
31
+ * deep-agent `StatusBuilder.addWriteBack`: each git-backed workspace entry
32
+ * carries at most one record, progressing through phases.
33
+ */
34
+ export function statusProtoWriter(status: AgentExecutionStatus): ExecutionStatusWriter {
35
+ return {
36
+ get currentStatus() {
37
+ return status;
38
+ },
39
+ forceNextUpdate: false,
40
+ clearForceFlag() {},
41
+ addArtifact(artifact: ExecutionArtifact) {
42
+ status.artifacts.push(artifact);
43
+ },
44
+ addWriteBack(wb: WorkspaceWriteBack) {
45
+ const backs = status.workspaceWriteBacks;
46
+ const idx = backs.findIndex(
47
+ (b) => b.workspaceEntryName === wb.workspaceEntryName,
48
+ );
49
+ if (idx >= 0) {
50
+ backs[idx] = wb;
51
+ } else {
52
+ backs.push(wb);
53
+ }
54
+ },
55
+ };
56
+ }
@@ -97,6 +97,10 @@ export function captureRef(executionId: string): string {
97
97
  * garbage collection — but `git commit-tree` still requires an author/committer,
98
98
  * and the repo may have no user.name/email configured. Supplied via env so we
99
99
  * never mutate the repo's config.
100
+ *
101
+ * Deliberately distinct from the public agent identity in
102
+ * `../workspace/git-identity.ts`, which authors the write-back commits that
103
+ * land on real branches and pull requests.
100
104
  */
101
105
  const SNAPSHOT_IDENTITY_ENV: Record<string, string> = {
102
106
  GIT_AUTHOR_NAME: "stigmer-runner",