@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
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Git write-back coordinator for workspace entries.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* commits to the same branch
|
|
4
|
+
* Commits the workspace's changes to the session's write-back branch, pushes,
|
|
5
|
+
* and keeps one pull request open per session. The branch and PR are
|
|
6
|
+
* SESSION-scoped (`stigmer/<session-id>`): a session is one workstream, so
|
|
7
|
+
* every approved turn appends commits to the same branch and the same PR —
|
|
8
|
+
* mirroring how Cursor cloud agents and Codex present a session's deliverable.
|
|
9
|
+
* Execution-scoped branches were a modeling bug: each turn branched off the
|
|
10
|
+
* previous turn's branch, leaving a trail of superseded PRs.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
12
|
+
* Because the branch outlives any single execution, every git/GitHub step is
|
|
13
|
+
* idempotent across coordinator instances: the branch is checked out if it
|
|
14
|
+
* already exists (locally, or on the remote after a sandbox re-provision), and
|
|
15
|
+
* an already-open PR for the branch is adopted instead of re-created.
|
|
10
16
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* When this runs depends on the harness's file-review mode:
|
|
18
|
+
* - Capture mode (apply-then-review — every git workspace): the streaming
|
|
19
|
+
* trigger is suppressed and `finalize()` runs exactly once on the APPROVED
|
|
20
|
+
* tree, after review decisions reconcile (see processCaptureWriteback in
|
|
21
|
+
* index.ts). Speculative mid-turn edits never reach GitHub.
|
|
22
|
+
* - Legacy non-capture turns: `onFileModified(path)` triggers an incremental
|
|
23
|
+
* commit/push per file-modifying tool call (DD-5), with `finalize()` as the
|
|
24
|
+
* post-stream safety net.
|
|
15
25
|
*
|
|
16
26
|
* Concurrency: one mutex per workspace entry serializes git operations.
|
|
17
27
|
*/
|
|
@@ -25,23 +35,25 @@ import {
|
|
|
25
35
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
26
36
|
import { GitWriteBackMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
27
37
|
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
28
|
-
import type { WorkspaceBackend, ProvisionResult } from "
|
|
29
|
-
import { SourceType } from "
|
|
30
|
-
import
|
|
38
|
+
import type { WorkspaceBackend, ProvisionResult } from "./types.js";
|
|
39
|
+
import { SourceType } from "./types.js";
|
|
40
|
+
import { gitCommitAsAgent } from "./git-identity.js";
|
|
41
|
+
import type { ExecutionStatusWriter } from "../execution-status-writer.js";
|
|
31
42
|
|
|
32
43
|
const WRITE_BACK_ENABLED_MODES = new Set([
|
|
33
44
|
GitWriteBackMode.GIT_WRITE_BACK_MODE_UNSPECIFIED,
|
|
34
45
|
GitWriteBackMode.GIT_WRITE_BACK_BRANCH_AND_PR,
|
|
35
46
|
]);
|
|
36
47
|
|
|
48
|
+
const GITHUB_API = "https://api.github.com";
|
|
49
|
+
|
|
37
50
|
interface EntryState {
|
|
38
|
-
|
|
51
|
+
branchReady: boolean;
|
|
39
52
|
prCreated: boolean;
|
|
40
53
|
prUrl: string;
|
|
41
54
|
prNumber: number;
|
|
42
55
|
commitCount: number;
|
|
43
56
|
lastCommitSha: string;
|
|
44
|
-
githubToken: string;
|
|
45
57
|
githubOwner: string;
|
|
46
58
|
githubRepo: string;
|
|
47
59
|
}
|
|
@@ -57,8 +69,15 @@ export class WriteBackCoordinator {
|
|
|
57
69
|
private readonly statusWriter: ExecutionStatusWriter;
|
|
58
70
|
private readonly executionId: string;
|
|
59
71
|
private readonly workspaceBackend: WorkspaceBackend;
|
|
60
|
-
private readonly shortId: string;
|
|
61
72
|
private readonly branchName: string;
|
|
73
|
+
/**
|
|
74
|
+
* Token for the GitHub PR API, plumbed explicitly from the resolved
|
|
75
|
+
* execution env (the same GITHUB_TOKEN that credentials the clone/push).
|
|
76
|
+
* Empty when the session has none — commit/push may still succeed via the
|
|
77
|
+
* repo-local credential store, so an empty token degrades to PUSHED with an
|
|
78
|
+
* actionable error rather than blocking the write-back.
|
|
79
|
+
*/
|
|
80
|
+
private readonly githubToken: string;
|
|
62
81
|
|
|
63
82
|
private readonly eligible = new Map<string, EligibleEntry>();
|
|
64
83
|
private readonly state = new Map<string, EntryState>();
|
|
@@ -67,6 +86,9 @@ export class WriteBackCoordinator {
|
|
|
67
86
|
constructor(opts: {
|
|
68
87
|
statusWriter: ExecutionStatusWriter;
|
|
69
88
|
executionId: string;
|
|
89
|
+
/** The owning session's id — the branch/PR are session-scoped. */
|
|
90
|
+
sessionId: string;
|
|
91
|
+
githubToken: string;
|
|
70
92
|
provisionResults: readonly ProvisionResult[];
|
|
71
93
|
workspaceEntries: readonly WorkspaceEntry[];
|
|
72
94
|
workspaceBackend: WorkspaceBackend;
|
|
@@ -74,8 +96,10 @@ export class WriteBackCoordinator {
|
|
|
74
96
|
this.statusWriter = opts.statusWriter;
|
|
75
97
|
this.executionId = opts.executionId;
|
|
76
98
|
this.workspaceBackend = opts.workspaceBackend;
|
|
77
|
-
this.
|
|
78
|
-
|
|
99
|
+
this.githubToken = opts.githubToken;
|
|
100
|
+
// The FULL session id: a truncated ULID is timestamp-dominated, so two
|
|
101
|
+
// sessions created near-simultaneously would collide on a short prefix.
|
|
102
|
+
this.branchName = `stigmer/${opts.sessionId}`;
|
|
79
103
|
|
|
80
104
|
this.initEligibleEntries(opts.provisionResults, opts.workspaceEntries);
|
|
81
105
|
}
|
|
@@ -85,9 +109,9 @@ export class WriteBackCoordinator {
|
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
/**
|
|
88
|
-
* Called after a file-modifying tool completes
|
|
89
|
-
* a workspace entry and runs an incremental
|
|
90
|
-
* Fire-and-forget: errors are logged, never thrown.
|
|
112
|
+
* Called after a file-modifying tool completes (legacy non-capture turns
|
|
113
|
+
* only). Resolves the path to a workspace entry and runs an incremental
|
|
114
|
+
* commit/push cycle. Fire-and-forget: errors are logged, never thrown.
|
|
91
115
|
*/
|
|
92
116
|
async onFileModified(path: string): Promise<void> {
|
|
93
117
|
try {
|
|
@@ -95,7 +119,7 @@ export class WriteBackCoordinator {
|
|
|
95
119
|
if (!entryName) return;
|
|
96
120
|
|
|
97
121
|
await this.withLock(entryName, () =>
|
|
98
|
-
this.
|
|
122
|
+
this.writeBackEntry(entryName),
|
|
99
123
|
);
|
|
100
124
|
} catch (err) {
|
|
101
125
|
console.warn(
|
|
@@ -106,14 +130,16 @@ export class WriteBackCoordinator {
|
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
133
|
+
* Commits and pushes every eligible workspace entry's remaining uncommitted
|
|
134
|
+
* changes. In capture mode this is THE write-back — invoked once on the
|
|
135
|
+
* approved tree after review reconcile; on legacy turns it is the
|
|
136
|
+
* post-stream safety net.
|
|
111
137
|
*/
|
|
112
138
|
async finalize(): Promise<void> {
|
|
113
139
|
for (const entryName of this.eligible.keys()) {
|
|
114
140
|
try {
|
|
115
141
|
await this.withLock(entryName, () =>
|
|
116
|
-
this.
|
|
142
|
+
this.writeBackEntry(entryName),
|
|
117
143
|
);
|
|
118
144
|
} catch (err) {
|
|
119
145
|
console.warn(
|
|
@@ -154,13 +180,12 @@ export class WriteBackCoordinator {
|
|
|
154
180
|
});
|
|
155
181
|
|
|
156
182
|
this.state.set(pr.entryName, {
|
|
157
|
-
|
|
183
|
+
branchReady: false,
|
|
158
184
|
prCreated: false,
|
|
159
185
|
prUrl: "",
|
|
160
186
|
prNumber: 0,
|
|
161
187
|
commitCount: 0,
|
|
162
188
|
lastCommitSha: "",
|
|
163
|
-
githubToken: "",
|
|
164
189
|
githubOwner: "",
|
|
165
190
|
githubRepo: "",
|
|
166
191
|
});
|
|
@@ -189,9 +214,19 @@ export class WriteBackCoordinator {
|
|
|
189
214
|
return null;
|
|
190
215
|
}
|
|
191
216
|
|
|
192
|
-
// ── Core
|
|
217
|
+
// ── Core Write-Back Cycle ───────────────────────────────────────────
|
|
193
218
|
|
|
194
|
-
|
|
219
|
+
/**
|
|
220
|
+
* One write-back cycle for an entry: branch → commit → push → PR → status.
|
|
221
|
+
*
|
|
222
|
+
* Failure semantics are split by what actually succeeded (the phases are
|
|
223
|
+
* facts, not a single verdict):
|
|
224
|
+
* - branch/commit/push failure → FAILED (the work did not reach GitHub);
|
|
225
|
+
* - PR failure after a successful push → PUSHED with the PR error carried
|
|
226
|
+
* in `error` — the branch is live and usable, and saying "failed" for a
|
|
227
|
+
* pushed branch would be dishonest (the original cloud regression).
|
|
228
|
+
*/
|
|
229
|
+
private async writeBackEntry(entryName: string): Promise<void> {
|
|
195
230
|
const entry = this.eligible.get(entryName)!;
|
|
196
231
|
const entryState = this.state.get(entryName)!;
|
|
197
232
|
const rootDir = entry.rootDir;
|
|
@@ -200,37 +235,23 @@ export class WriteBackCoordinator {
|
|
|
200
235
|
return this.workspaceBackend.execute(`cd ${rootDir} && ${cmd}`);
|
|
201
236
|
};
|
|
202
237
|
|
|
203
|
-
let mutationStarted = false;
|
|
204
238
|
try {
|
|
205
239
|
const hasChanges = await this.hasChanges(exec);
|
|
206
240
|
if (!hasChanges) return;
|
|
207
241
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!entryState.branchCreated) {
|
|
211
|
-
await this.createBranch(entryName, entryState, exec);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const commitMsg = `agent changes (${entryState.commitCount + 1})`;
|
|
215
|
-
await this.commitAndPush(entryName, entryState, exec, commitMsg);
|
|
216
|
-
|
|
217
|
-
if (!entryState.prCreated) {
|
|
218
|
-
await this.createPr(entryName, entryState, entry, exec);
|
|
242
|
+
if (!entryState.branchReady) {
|
|
243
|
+
await this.ensureBranch(entryName, entryState, exec);
|
|
219
244
|
}
|
|
220
|
-
|
|
221
|
-
await this.updateStatus(entryName, entryState, entry, exec);
|
|
222
|
-
|
|
245
|
+
await this.commitAndPush(entryName, entryState, exec);
|
|
223
246
|
} catch (err) {
|
|
224
247
|
console.warn(
|
|
225
248
|
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
226
|
-
`
|
|
249
|
+
`commit/push error: ${err}`,
|
|
227
250
|
);
|
|
228
|
-
if (!mutationStarted) return;
|
|
229
|
-
|
|
230
251
|
const wb = create(WorkspaceWriteBackSchema, {
|
|
231
252
|
workspaceEntryName: entryName,
|
|
232
253
|
baseBranch: entry.baseBranch,
|
|
233
|
-
branchName: entryState.
|
|
254
|
+
branchName: entryState.branchReady ? this.branchName : "",
|
|
234
255
|
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
235
256
|
error: String(err),
|
|
236
257
|
});
|
|
@@ -239,7 +260,23 @@ export class WriteBackCoordinator {
|
|
|
239
260
|
wb.pullRequestNumber = entryState.prNumber;
|
|
240
261
|
}
|
|
241
262
|
this.statusWriter.addWriteBack(wb);
|
|
263
|
+
return;
|
|
242
264
|
}
|
|
265
|
+
|
|
266
|
+
let prError = "";
|
|
267
|
+
if (!entryState.prCreated) {
|
|
268
|
+
try {
|
|
269
|
+
await this.ensurePr(entryName, entryState, entry);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.warn(
|
|
272
|
+
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
273
|
+
`PR error (branch is pushed): ${err}`,
|
|
274
|
+
);
|
|
275
|
+
prError = String(err);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await this.updateStatus(entryName, entryState, entry, exec, prError);
|
|
243
280
|
}
|
|
244
281
|
|
|
245
282
|
// ── Git Operations ──────────────────────────────────────────────────
|
|
@@ -253,16 +290,52 @@ export class WriteBackCoordinator {
|
|
|
253
290
|
return untracked.trim().length > 0;
|
|
254
291
|
}
|
|
255
292
|
|
|
256
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Put the working tree on the session branch, wherever the branch already
|
|
295
|
+
* lives. Three cases, in order:
|
|
296
|
+
* 1. HEAD is already on it — a later turn in the same workspace (the common
|
|
297
|
+
* multi-turn path; the previous turn's cycle left HEAD there).
|
|
298
|
+
* 2. It exists locally or on the remote — a re-provisioned workspace whose
|
|
299
|
+
* clone lost the local branch. Checked out (tracking the remote ref when
|
|
300
|
+
* that is the only copy). The checkout carries this turn's uncommitted
|
|
301
|
+
* changes along; if they collide with the branch's prior commits git
|
|
302
|
+
* refuses, and the error surfaces as an honest FAILED record — we never
|
|
303
|
+
* force (-B) over the session's pushed history.
|
|
304
|
+
* 3. Neither — the session's first write-back creates it.
|
|
305
|
+
*/
|
|
306
|
+
private async ensureBranch(
|
|
257
307
|
entryName: string,
|
|
258
308
|
entryState: EntryState,
|
|
259
309
|
exec: (cmd: string) => Promise<string>,
|
|
260
310
|
): Promise<void> {
|
|
261
|
-
await exec(
|
|
262
|
-
|
|
311
|
+
const current = (await exec("git branch --show-current").catch(() => "")).trim();
|
|
312
|
+
if (current === this.branchName) {
|
|
313
|
+
entryState.branchReady = true;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const localRef = await exec(
|
|
318
|
+
`git rev-parse --verify --quiet refs/heads/${this.branchName}`,
|
|
319
|
+
).then((out) => out.trim(), () => "");
|
|
320
|
+
|
|
321
|
+
if (localRef) {
|
|
322
|
+
await exec(`git checkout ${this.branchName}`);
|
|
323
|
+
} else {
|
|
324
|
+
const remoteRef = (
|
|
325
|
+
await exec(`git ls-remote --heads origin ${this.branchName}`).catch(() => "")
|
|
326
|
+
).trim();
|
|
327
|
+
if (remoteRef) {
|
|
328
|
+
await exec(`git fetch origin ${this.branchName}`);
|
|
329
|
+
await exec(`git checkout -b ${this.branchName} origin/${this.branchName}`);
|
|
330
|
+
} else {
|
|
331
|
+
await exec(`git checkout -b ${this.branchName}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
entryState.branchReady = true;
|
|
263
336
|
console.log(
|
|
264
337
|
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
265
|
-
`
|
|
338
|
+
`on branch ${this.branchName}`,
|
|
266
339
|
);
|
|
267
340
|
}
|
|
268
341
|
|
|
@@ -270,59 +343,82 @@ export class WriteBackCoordinator {
|
|
|
270
343
|
entryName: string,
|
|
271
344
|
entryState: EntryState,
|
|
272
345
|
exec: (cmd: string) => Promise<string>,
|
|
273
|
-
commitMsg: string,
|
|
274
346
|
): Promise<void> {
|
|
275
347
|
await exec("git add -A");
|
|
276
|
-
|
|
348
|
+
// Committed as the agent identity: the cloud sandbox has no git identity
|
|
349
|
+
// configured (a bare commit fails with "Author identity unknown"), and in
|
|
350
|
+
// local mode the agent's work should not be attributed to the host user.
|
|
351
|
+
// The execution id in the message links each commit back to its turn.
|
|
352
|
+
await exec(gitCommitAsAgent(`agent changes (${this.executionId})`));
|
|
277
353
|
|
|
278
354
|
entryState.commitCount++;
|
|
279
355
|
const shaOutput = await exec("git rev-parse HEAD");
|
|
280
356
|
entryState.lastCommitSha = shaOutput.trim();
|
|
281
357
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await exec("git push");
|
|
286
|
-
}
|
|
358
|
+
// Always -u: idempotent whether this push creates the remote branch or
|
|
359
|
+
// appends to it, and it (re-)establishes tracking after a re-provision.
|
|
360
|
+
await exec(`git push -u origin ${this.branchName}`);
|
|
287
361
|
|
|
288
362
|
console.log(
|
|
289
363
|
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
290
|
-
`commit
|
|
364
|
+
`commit pushed to ${this.branchName} (sha=${entryState.lastCommitSha.slice(0, 12)})`,
|
|
291
365
|
);
|
|
292
366
|
}
|
|
293
367
|
|
|
294
|
-
|
|
368
|
+
// ── GitHub PR ───────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Ensure one open PR exists for the session branch: adopt an already-open
|
|
372
|
+
* one (a prior turn — possibly a prior coordinator instance — created it),
|
|
373
|
+
* else create it. Runs only after a successful push; a thrown error here is
|
|
374
|
+
* reported as PUSHED + error, never FAILED.
|
|
375
|
+
*/
|
|
376
|
+
private async ensurePr(
|
|
295
377
|
entryName: string,
|
|
296
378
|
entryState: EntryState,
|
|
297
379
|
entry: EligibleEntry,
|
|
298
|
-
_exec: (cmd: string) => Promise<string>,
|
|
299
380
|
): Promise<void> {
|
|
300
381
|
const meta = entry.provisionResult.gitMetadata!;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!entryState.githubToken) {
|
|
304
|
-
entryState.githubToken = extractGithubToken(meta.repoUrl);
|
|
382
|
+
if (!entryState.githubOwner) {
|
|
383
|
+
const { owner, repo } = parseGithubRepo(meta.repoUrl);
|
|
305
384
|
entryState.githubOwner = owner;
|
|
306
385
|
entryState.githubRepo = repo;
|
|
307
386
|
}
|
|
308
387
|
|
|
309
|
-
|
|
388
|
+
if (!this.githubToken) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
"No GitHub token available to open a pull request. The branch " +
|
|
391
|
+
`'${this.branchName}' was pushed — open the PR manually, or configure ` +
|
|
392
|
+
"GITHUB_TOKEN for the session so PRs are created automatically.",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const existing = await this.findOpenPr(entryState);
|
|
397
|
+
if (existing) {
|
|
398
|
+
entryState.prCreated = true;
|
|
399
|
+
entryState.prUrl = existing.url;
|
|
400
|
+
entryState.prNumber = existing.number;
|
|
401
|
+
console.log(
|
|
402
|
+
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
403
|
+
`adopted open PR #${existing.number}: ${existing.url}`,
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const shortSessionId = this.branchName.replace(/^stigmer\//, "");
|
|
310
409
|
const prBody =
|
|
311
|
-
`Automated pull request from Stigmer agent
|
|
312
|
-
`**
|
|
313
|
-
`**Workspace:** \`${entryName}\`\n
|
|
410
|
+
`Automated pull request from a Stigmer agent session.\n\n` +
|
|
411
|
+
`**Session:** \`${shortSessionId}\`\n` +
|
|
412
|
+
`**Workspace:** \`${entryName}\`\n\n` +
|
|
413
|
+
`Each approved turn appends its commits to this pull request.\n`;
|
|
314
414
|
|
|
315
415
|
const resp = await fetch(
|
|
316
|
-
|
|
416
|
+
`${GITHUB_API}/repos/${entryState.githubOwner}/${entryState.githubRepo}/pulls`,
|
|
317
417
|
{
|
|
318
418
|
method: "POST",
|
|
319
|
-
headers:
|
|
320
|
-
"Authorization": `Bearer ${entryState.githubToken}`,
|
|
321
|
-
"Accept": "application/vnd.github+json",
|
|
322
|
-
"Content-Type": "application/json",
|
|
323
|
-
},
|
|
419
|
+
headers: this.githubHeaders(),
|
|
324
420
|
body: JSON.stringify({
|
|
325
|
-
title:
|
|
421
|
+
title: `Stigmer agent changes (${shortSessionId})`,
|
|
326
422
|
body: prBody,
|
|
327
423
|
head: this.branchName,
|
|
328
424
|
base: entry.baseBranch,
|
|
@@ -346,11 +442,44 @@ export class WriteBackCoordinator {
|
|
|
346
442
|
);
|
|
347
443
|
}
|
|
348
444
|
|
|
445
|
+
/** The open PR whose head is the session branch, if one exists. */
|
|
446
|
+
private async findOpenPr(
|
|
447
|
+
entryState: EntryState,
|
|
448
|
+
): Promise<{ url: string; number: number } | null> {
|
|
449
|
+
const head = `${entryState.githubOwner}:${this.branchName}`;
|
|
450
|
+
const resp = await fetch(
|
|
451
|
+
`${GITHUB_API}/repos/${entryState.githubOwner}/${entryState.githubRepo}` +
|
|
452
|
+
`/pulls?head=${encodeURIComponent(head)}&state=open`,
|
|
453
|
+
{ headers: this.githubHeaders() },
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (!resp.ok) {
|
|
457
|
+
const body = await resp.text();
|
|
458
|
+
throw new Error(`GitHub API error listing PRs (HTTP ${resp.status}): ${body}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const data = await resp.json() as Array<{ html_url?: string; number?: number }>;
|
|
462
|
+
const pr = data[0];
|
|
463
|
+
if (!pr) return null;
|
|
464
|
+
return { url: pr.html_url ?? "", number: pr.number ?? 0 };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private githubHeaders(): Record<string, string> {
|
|
468
|
+
return {
|
|
469
|
+
"Authorization": `Bearer ${this.githubToken}`,
|
|
470
|
+
"Accept": "application/vnd.github+json",
|
|
471
|
+
"Content-Type": "application/json",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Status Reporting ────────────────────────────────────────────────
|
|
476
|
+
|
|
349
477
|
private async updateStatus(
|
|
350
478
|
entryName: string,
|
|
351
479
|
entryState: EntryState,
|
|
352
480
|
entry: EligibleEntry,
|
|
353
481
|
exec: (cmd: string) => Promise<string>,
|
|
482
|
+
prError: string,
|
|
354
483
|
): Promise<void> {
|
|
355
484
|
const summaryOutput = await exec(
|
|
356
485
|
`git diff --stat ${entry.baseBranch}...HEAD`,
|
|
@@ -369,6 +498,9 @@ export class WriteBackCoordinator {
|
|
|
369
498
|
pullRequestNumber: entryState.prNumber,
|
|
370
499
|
diffSummary: summaryOutput.trim(),
|
|
371
500
|
phase,
|
|
501
|
+
// Non-empty only for a PUSHED record whose PR step failed: the branch
|
|
502
|
+
// is live, and the error tells the user why there is no PR link yet.
|
|
503
|
+
error: prError,
|
|
372
504
|
});
|
|
373
505
|
|
|
374
506
|
this.statusWriter.addWriteBack(wb);
|
|
@@ -402,19 +534,3 @@ export function parseGithubRepo(repoUrl: string): { owner: string; repo: string
|
|
|
402
534
|
}
|
|
403
535
|
throw new Error(`Cannot parse GitHub owner/repo from URL: ${repoUrl}`);
|
|
404
536
|
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Extract a GitHub token from an HTTPS clone URL that has credentials embedded.
|
|
408
|
-
* Format: https://{token}@github.com/...
|
|
409
|
-
*/
|
|
410
|
-
export function extractGithubToken(repoUrl: string): string {
|
|
411
|
-
const match = repoUrl.match(/https?:\/\/([^@]+)@github\.com/);
|
|
412
|
-
if (match) return match[1];
|
|
413
|
-
|
|
414
|
-
const envToken = process.env.GITHUB_TOKEN;
|
|
415
|
-
if (envToken) return envToken;
|
|
416
|
-
|
|
417
|
-
throw new Error(
|
|
418
|
-
"Cannot extract GitHub token from repo URL and GITHUB_TOKEN is not set",
|
|
419
|
-
);
|
|
420
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The workspace `.stigmer` symlink — the Cursor harness's bridge from the
|
|
3
|
-
* user's workspace to the session's platform-managed directory
|
|
4
|
-
* (~/.stigmer/sessions/{id}/platform/, see shared/workspace/platform-dir.ts).
|
|
5
|
-
*
|
|
6
|
-
* The Cursor SDK reads files from the workspace CWD, so platform-injected
|
|
7
|
-
* content (skills, attachment inputs) must be reachable there; the symlink is
|
|
8
|
-
* how, without ever writing platform files into the user's repo (issue #173).
|
|
9
|
-
* This mechanism is deliberately harness-local: the native harness routes
|
|
10
|
-
* `.stigmer/` paths through its WorkspaceBackend instead and never symlinks.
|
|
11
|
-
*
|
|
12
|
-
* Both resolvers that populate the platform dir (skills, attachments) call
|
|
13
|
-
* {@link ensureStigmerSymlink}; it is idempotent, so either may run first and
|
|
14
|
-
* an agent with only one kind of content still gets the link. The activity's
|
|
15
|
-
* cleanup calls {@link removeStigmerSymlink} so a real repo is left untouched
|
|
16
|
-
* once the turn ends; a multi-turn session recreates the link on the next turn.
|
|
17
|
-
*/
|
|
18
|
-
/** The workspace-visible name of the platform namespace. */
|
|
19
|
-
export declare const STIGMER_LOCAL_STATE_DIR = ".stigmer";
|
|
20
|
-
/**
|
|
21
|
-
* Ensure the workspace `.stigmer` symlink points to the platform dir.
|
|
22
|
-
* Idempotent: an existing correct link is kept; a stale link (or a non-link
|
|
23
|
-
* left behind by an older runner) is replaced.
|
|
24
|
-
*/
|
|
25
|
-
export declare function ensureStigmerSymlink(workspaceDir: string, platformDir: string): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* Remove the workspace `.stigmer` symlink created by
|
|
28
|
-
* {@link ensureStigmerSymlink}.
|
|
29
|
-
*
|
|
30
|
-
* Called in the activity's finally so attaching a real repo leaves no Stigmer
|
|
31
|
-
* symlink behind once the turn ends (issue #173). Only ever removes a
|
|
32
|
-
* SYMLINK — a real `.stigmer` directory (which would be the user's own, not
|
|
33
|
-
* ours) is left untouched. Best-effort.
|
|
34
|
-
*/
|
|
35
|
-
export declare function removeStigmerSymlink(workspaceDir: string): Promise<void>;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The workspace `.stigmer` symlink — the Cursor harness's bridge from the
|
|
3
|
-
* user's workspace to the session's platform-managed directory
|
|
4
|
-
* (~/.stigmer/sessions/{id}/platform/, see shared/workspace/platform-dir.ts).
|
|
5
|
-
*
|
|
6
|
-
* The Cursor SDK reads files from the workspace CWD, so platform-injected
|
|
7
|
-
* content (skills, attachment inputs) must be reachable there; the symlink is
|
|
8
|
-
* how, without ever writing platform files into the user's repo (issue #173).
|
|
9
|
-
* This mechanism is deliberately harness-local: the native harness routes
|
|
10
|
-
* `.stigmer/` paths through its WorkspaceBackend instead and never symlinks.
|
|
11
|
-
*
|
|
12
|
-
* Both resolvers that populate the platform dir (skills, attachments) call
|
|
13
|
-
* {@link ensureStigmerSymlink}; it is idempotent, so either may run first and
|
|
14
|
-
* an agent with only one kind of content still gets the link. The activity's
|
|
15
|
-
* cleanup calls {@link removeStigmerSymlink} so a real repo is left untouched
|
|
16
|
-
* once the turn ends; a multi-turn session recreates the link on the next turn.
|
|
17
|
-
*/
|
|
18
|
-
import { symlink, readlink, unlink, rm, lstat } from "node:fs/promises";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
/** The workspace-visible name of the platform namespace. */
|
|
21
|
-
export const STIGMER_LOCAL_STATE_DIR = ".stigmer";
|
|
22
|
-
/**
|
|
23
|
-
* Ensure the workspace `.stigmer` symlink points to the platform dir.
|
|
24
|
-
* Idempotent: an existing correct link is kept; a stale link (or a non-link
|
|
25
|
-
* left behind by an older runner) is replaced.
|
|
26
|
-
*/
|
|
27
|
-
export async function ensureStigmerSymlink(workspaceDir, platformDir) {
|
|
28
|
-
const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
|
|
29
|
-
try {
|
|
30
|
-
const existing = await readlink(linkPath);
|
|
31
|
-
if (existing === platformDir)
|
|
32
|
-
return;
|
|
33
|
-
await unlink(linkPath);
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
if (err.code === "ENOENT") {
|
|
37
|
-
// No existing symlink
|
|
38
|
-
}
|
|
39
|
-
else if (err.code === "EINVAL") {
|
|
40
|
-
// Exists but is not a symlink — remove the directory
|
|
41
|
-
await rm(linkPath, { recursive: true, force: true });
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
throw err;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
await symlink(platformDir, linkPath, "dir");
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Remove the workspace `.stigmer` symlink created by
|
|
51
|
-
* {@link ensureStigmerSymlink}.
|
|
52
|
-
*
|
|
53
|
-
* Called in the activity's finally so attaching a real repo leaves no Stigmer
|
|
54
|
-
* symlink behind once the turn ends (issue #173). Only ever removes a
|
|
55
|
-
* SYMLINK — a real `.stigmer` directory (which would be the user's own, not
|
|
56
|
-
* ours) is left untouched. Best-effort.
|
|
57
|
-
*/
|
|
58
|
-
export async function removeStigmerSymlink(workspaceDir) {
|
|
59
|
-
const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
|
|
60
|
-
try {
|
|
61
|
-
const stat = await lstat(linkPath);
|
|
62
|
-
if (stat.isSymbolicLink()) {
|
|
63
|
-
await unlink(linkPath);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
if (err?.code !== "ENOENT") {
|
|
68
|
-
console.warn(`removeStigmerSymlink: failed to remove ${linkPath} (non-fatal): ` +
|
|
69
|
-
`${err instanceof Error ? err.message : err}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
//# sourceMappingURL=stigmer-link.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stigmer-link.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/stigmer-link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,uBAAuB,GAAG,UAAU,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,YAAoB,EACpB,WAAmB;IAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAAC;IAE7D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,QAAQ,KAAK,WAAW;YAAE,OAAO;QACrC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1B,sBAAsB;QACxB,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACjC,qDAAqD;YACrD,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,MAAM,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,YAAoB;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAAC;IAC7D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YAC1B,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,IAAI,CACV,0CAA0C,QAAQ,gBAAgB;gBAClE,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAC9C,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared interface for proto mutation consumed by side-effect classes
|
|
3
|
-
* (InlinePublisher, WriteBackCoordinator) and streaming loops.
|
|
4
|
-
*
|
|
5
|
-
* Both v2 StatusBuilder and V3StatusBuilder implement this interface,
|
|
6
|
-
* decoupling side-effect classes from the specific builder version.
|
|
7
|
-
*/
|
|
8
|
-
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
9
|
-
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
10
|
-
import type { WorkspaceWriteBack } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
11
|
-
export interface ExecutionStatusWriter {
|
|
12
|
-
readonly currentStatus: AgentExecutionStatus;
|
|
13
|
-
readonly forceNextUpdate: boolean;
|
|
14
|
-
clearForceFlag(): void;
|
|
15
|
-
addArtifact(artifact: ExecutionArtifact): void;
|
|
16
|
-
addWriteBack(wb: WorkspaceWriteBack): void;
|
|
17
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared interface for proto mutation consumed by side-effect classes
|
|
3
|
-
* (InlinePublisher, WriteBackCoordinator) and streaming loops.
|
|
4
|
-
*
|
|
5
|
-
* Both v2 StatusBuilder and V3StatusBuilder implement this interface,
|
|
6
|
-
* decoupling side-effect classes from the specific builder version.
|
|
7
|
-
*/
|
|
8
|
-
export {};
|
|
9
|
-
//# sourceMappingURL=execution-status-writer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"execution-status-writer.js","sourceRoot":"","sources":["../../../src/activities/execute-deep-agent/execution-status-writer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|