@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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +40 -2
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js +1 -1
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-provision.d.ts +23 -4
- package/dist/activities/execute-cursor/workspace-provision.js +17 -6
- package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +39 -6
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +1 -1
- package/dist/activities/execute-deep-agent/post-stream.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +1 -1
- package/dist/shared/execution-status-writer.d.ts +31 -0
- package/dist/shared/execution-status-writer.js +43 -0
- package/dist/shared/execution-status-writer.js.map +1 -0
- package/dist/shared/filereview/git-substrate.js +4 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/workspace/git-identity.d.ts +36 -0
- package/dist/shared/workspace/git-identity.js +39 -0
- package/dist/shared/workspace/git-identity.js.map +1 -0
- package/dist/shared/workspace/local-backend.d.ts +6 -2
- package/dist/shared/workspace/local-backend.js +6 -2
- package/dist/shared/workspace/local-backend.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +54 -0
- package/dist/shared/workspace/stigmer-link.js +92 -0
- package/dist/shared/workspace/stigmer-link.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +6 -4
- package/dist/shared/workspace/writeback-coordinator.d.ts +120 -0
- package/dist/shared/workspace/writeback-coordinator.js +393 -0
- package/dist/shared/workspace/writeback-coordinator.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
- package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
- package/src/activities/execute-cursor/index.ts +42 -2
- package/src/activities/execute-cursor/skill-resolver.ts +1 -1
- package/src/activities/execute-cursor/workspace-provision.ts +39 -7
- package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +1 -1
- package/src/activities/execute-deep-agent/index.ts +44 -6
- package/src/activities/execute-deep-agent/inline-publisher.ts +1 -1
- package/src/activities/execute-deep-agent/post-stream.ts +1 -1
- package/src/activities/execute-deep-agent/status-builder.ts +1 -1
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +1 -1
- package/src/activities/execute-deep-agent/streaming-terminal.ts +1 -1
- package/src/activities/execute-deep-agent/streaming.ts +1 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +1 -1
- package/src/shared/execution-status-writer.ts +56 -0
- package/src/shared/filereview/git-substrate.ts +4 -0
- package/src/shared/workspace/__tests__/git-identity.test.ts +124 -0
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +173 -0
- package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +230 -0
- package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +477 -0
- package/src/shared/workspace/git-identity.ts +41 -0
- package/src/shared/workspace/local-backend.ts +6 -2
- package/src/shared/workspace/stigmer-link.ts +97 -0
- package/src/shared/workspace/types.ts +6 -4
- package/src/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +209 -93
- package/dist/activities/execute-cursor/stigmer-link.d.ts +0 -35
- package/dist/activities/execute-cursor/stigmer-link.js +0 -73
- package/dist/activities/execute-cursor/stigmer-link.js.map +0 -1
- package/dist/activities/execute-deep-agent/execution-status-writer.d.ts +0 -17
- package/dist/activities/execute-deep-agent/execution-status-writer.js +0 -9
- package/dist/activities/execute-deep-agent/execution-status-writer.js.map +0 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.d.ts +0 -71
- package/dist/activities/execute-deep-agent/writeback-coordinator.js +0 -295
- package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
- package/src/activities/execute-cursor/stigmer-link.ts +0 -78
- package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -399
- 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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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<
|
|
54
|
+
): Promise<CursorWorkspaceProvision> {
|
|
35
55
|
const entries = session.spec?.workspaceEntries ?? [];
|
|
56
|
+
const platformDir = await ensurePlatformDir(sessionId);
|
|
57
|
+
|
|
36
58
|
if (entries.length === 0) {
|
|
37
|
-
|
|
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
|
|
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 "
|
|
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 "
|
|
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
|
|
319
|
-
// the
|
|
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
|
|
402
|
-
//
|
|
403
|
-
//
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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",
|