@stigmer/runner 3.1.5 → 3.1.7
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/discover-mcp-server.js +23 -0
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +39 -1
- package/dist/activities/execute-cursor/index.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 +8 -1
- 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/mcp-oauth-detect.d.ts +53 -0
- package/dist/shared/mcp-oauth-detect.js +99 -0
- package/dist/shared/mcp-oauth-detect.js.map +1 -0
- 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/discover-mcp-server.ts +25 -0
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
- package/src/activities/execute-cursor/index.ts +41 -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 +8 -1
- 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/__tests__/mcp-oauth-detect.test.ts +147 -0
- package/src/shared/execution-status-writer.ts +56 -0
- package/src/shared/mcp-oauth-detect.ts +112 -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/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +206 -94
- 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 -299
- package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -426
- 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,24 +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 { gitCommitAsAgent } from "
|
|
31
|
-
import type { ExecutionStatusWriter } from "
|
|
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";
|
|
32
42
|
|
|
33
43
|
const WRITE_BACK_ENABLED_MODES = new Set([
|
|
34
44
|
GitWriteBackMode.GIT_WRITE_BACK_MODE_UNSPECIFIED,
|
|
35
45
|
GitWriteBackMode.GIT_WRITE_BACK_BRANCH_AND_PR,
|
|
36
46
|
]);
|
|
37
47
|
|
|
48
|
+
const GITHUB_API = "https://api.github.com";
|
|
49
|
+
|
|
38
50
|
interface EntryState {
|
|
39
|
-
|
|
51
|
+
branchReady: boolean;
|
|
40
52
|
prCreated: boolean;
|
|
41
53
|
prUrl: string;
|
|
42
54
|
prNumber: number;
|
|
43
55
|
commitCount: number;
|
|
44
56
|
lastCommitSha: string;
|
|
45
|
-
githubToken: string;
|
|
46
57
|
githubOwner: string;
|
|
47
58
|
githubRepo: string;
|
|
48
59
|
}
|
|
@@ -58,8 +69,15 @@ export class WriteBackCoordinator {
|
|
|
58
69
|
private readonly statusWriter: ExecutionStatusWriter;
|
|
59
70
|
private readonly executionId: string;
|
|
60
71
|
private readonly workspaceBackend: WorkspaceBackend;
|
|
61
|
-
private readonly shortId: string;
|
|
62
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;
|
|
63
81
|
|
|
64
82
|
private readonly eligible = new Map<string, EligibleEntry>();
|
|
65
83
|
private readonly state = new Map<string, EntryState>();
|
|
@@ -68,6 +86,9 @@ export class WriteBackCoordinator {
|
|
|
68
86
|
constructor(opts: {
|
|
69
87
|
statusWriter: ExecutionStatusWriter;
|
|
70
88
|
executionId: string;
|
|
89
|
+
/** The owning session's id — the branch/PR are session-scoped. */
|
|
90
|
+
sessionId: string;
|
|
91
|
+
githubToken: string;
|
|
71
92
|
provisionResults: readonly ProvisionResult[];
|
|
72
93
|
workspaceEntries: readonly WorkspaceEntry[];
|
|
73
94
|
workspaceBackend: WorkspaceBackend;
|
|
@@ -75,8 +96,10 @@ export class WriteBackCoordinator {
|
|
|
75
96
|
this.statusWriter = opts.statusWriter;
|
|
76
97
|
this.executionId = opts.executionId;
|
|
77
98
|
this.workspaceBackend = opts.workspaceBackend;
|
|
78
|
-
this.
|
|
79
|
-
|
|
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}`;
|
|
80
103
|
|
|
81
104
|
this.initEligibleEntries(opts.provisionResults, opts.workspaceEntries);
|
|
82
105
|
}
|
|
@@ -86,9 +109,9 @@ export class WriteBackCoordinator {
|
|
|
86
109
|
}
|
|
87
110
|
|
|
88
111
|
/**
|
|
89
|
-
* Called after a file-modifying tool completes
|
|
90
|
-
* a workspace entry and runs an incremental
|
|
91
|
-
* 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.
|
|
92
115
|
*/
|
|
93
116
|
async onFileModified(path: string): Promise<void> {
|
|
94
117
|
try {
|
|
@@ -96,7 +119,7 @@ export class WriteBackCoordinator {
|
|
|
96
119
|
if (!entryName) return;
|
|
97
120
|
|
|
98
121
|
await this.withLock(entryName, () =>
|
|
99
|
-
this.
|
|
122
|
+
this.writeBackEntry(entryName),
|
|
100
123
|
);
|
|
101
124
|
} catch (err) {
|
|
102
125
|
console.warn(
|
|
@@ -107,14 +130,16 @@ export class WriteBackCoordinator {
|
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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.
|
|
112
137
|
*/
|
|
113
138
|
async finalize(): Promise<void> {
|
|
114
139
|
for (const entryName of this.eligible.keys()) {
|
|
115
140
|
try {
|
|
116
141
|
await this.withLock(entryName, () =>
|
|
117
|
-
this.
|
|
142
|
+
this.writeBackEntry(entryName),
|
|
118
143
|
);
|
|
119
144
|
} catch (err) {
|
|
120
145
|
console.warn(
|
|
@@ -155,13 +180,12 @@ export class WriteBackCoordinator {
|
|
|
155
180
|
});
|
|
156
181
|
|
|
157
182
|
this.state.set(pr.entryName, {
|
|
158
|
-
|
|
183
|
+
branchReady: false,
|
|
159
184
|
prCreated: false,
|
|
160
185
|
prUrl: "",
|
|
161
186
|
prNumber: 0,
|
|
162
187
|
commitCount: 0,
|
|
163
188
|
lastCommitSha: "",
|
|
164
|
-
githubToken: "",
|
|
165
189
|
githubOwner: "",
|
|
166
190
|
githubRepo: "",
|
|
167
191
|
});
|
|
@@ -190,9 +214,19 @@ export class WriteBackCoordinator {
|
|
|
190
214
|
return null;
|
|
191
215
|
}
|
|
192
216
|
|
|
193
|
-
// ── Core
|
|
217
|
+
// ── Core Write-Back Cycle ───────────────────────────────────────────
|
|
194
218
|
|
|
195
|
-
|
|
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> {
|
|
196
230
|
const entry = this.eligible.get(entryName)!;
|
|
197
231
|
const entryState = this.state.get(entryName)!;
|
|
198
232
|
const rootDir = entry.rootDir;
|
|
@@ -201,37 +235,23 @@ export class WriteBackCoordinator {
|
|
|
201
235
|
return this.workspaceBackend.execute(`cd ${rootDir} && ${cmd}`);
|
|
202
236
|
};
|
|
203
237
|
|
|
204
|
-
let mutationStarted = false;
|
|
205
238
|
try {
|
|
206
239
|
const hasChanges = await this.hasChanges(exec);
|
|
207
240
|
if (!hasChanges) return;
|
|
208
241
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (!entryState.branchCreated) {
|
|
212
|
-
await this.createBranch(entryName, entryState, exec);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const commitMsg = `agent changes (${entryState.commitCount + 1})`;
|
|
216
|
-
await this.commitAndPush(entryName, entryState, exec, commitMsg);
|
|
217
|
-
|
|
218
|
-
if (!entryState.prCreated) {
|
|
219
|
-
await this.createPr(entryName, entryState, entry, exec);
|
|
242
|
+
if (!entryState.branchReady) {
|
|
243
|
+
await this.ensureBranch(entryName, entryState, exec);
|
|
220
244
|
}
|
|
221
|
-
|
|
222
|
-
await this.updateStatus(entryName, entryState, entry, exec);
|
|
223
|
-
|
|
245
|
+
await this.commitAndPush(entryName, entryState, exec);
|
|
224
246
|
} catch (err) {
|
|
225
247
|
console.warn(
|
|
226
248
|
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
227
|
-
`
|
|
249
|
+
`commit/push error: ${err}`,
|
|
228
250
|
);
|
|
229
|
-
if (!mutationStarted) return;
|
|
230
|
-
|
|
231
251
|
const wb = create(WorkspaceWriteBackSchema, {
|
|
232
252
|
workspaceEntryName: entryName,
|
|
233
253
|
baseBranch: entry.baseBranch,
|
|
234
|
-
branchName: entryState.
|
|
254
|
+
branchName: entryState.branchReady ? this.branchName : "",
|
|
235
255
|
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
236
256
|
error: String(err),
|
|
237
257
|
});
|
|
@@ -240,7 +260,23 @@ export class WriteBackCoordinator {
|
|
|
240
260
|
wb.pullRequestNumber = entryState.prNumber;
|
|
241
261
|
}
|
|
242
262
|
this.statusWriter.addWriteBack(wb);
|
|
263
|
+
return;
|
|
243
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);
|
|
244
280
|
}
|
|
245
281
|
|
|
246
282
|
// ── Git Operations ──────────────────────────────────────────────────
|
|
@@ -254,16 +290,52 @@ export class WriteBackCoordinator {
|
|
|
254
290
|
return untracked.trim().length > 0;
|
|
255
291
|
}
|
|
256
292
|
|
|
257
|
-
|
|
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(
|
|
258
307
|
entryName: string,
|
|
259
308
|
entryState: EntryState,
|
|
260
309
|
exec: (cmd: string) => Promise<string>,
|
|
261
310
|
): Promise<void> {
|
|
262
|
-
await exec(
|
|
263
|
-
|
|
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;
|
|
264
336
|
console.log(
|
|
265
337
|
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
266
|
-
`
|
|
338
|
+
`on branch ${this.branchName}`,
|
|
267
339
|
);
|
|
268
340
|
}
|
|
269
341
|
|
|
@@ -271,62 +343,82 @@ export class WriteBackCoordinator {
|
|
|
271
343
|
entryName: string,
|
|
272
344
|
entryState: EntryState,
|
|
273
345
|
exec: (cmd: string) => Promise<string>,
|
|
274
|
-
commitMsg: string,
|
|
275
346
|
): Promise<void> {
|
|
276
347
|
await exec("git add -A");
|
|
277
348
|
// Committed as the agent identity: the cloud sandbox has no git identity
|
|
278
349
|
// configured (a bare commit fails with "Author identity unknown"), and in
|
|
279
350
|
// local mode the agent's work should not be attributed to the host user.
|
|
280
|
-
|
|
351
|
+
// The execution id in the message links each commit back to its turn.
|
|
352
|
+
await exec(gitCommitAsAgent(`agent changes (${this.executionId})`));
|
|
281
353
|
|
|
282
354
|
entryState.commitCount++;
|
|
283
355
|
const shaOutput = await exec("git rev-parse HEAD");
|
|
284
356
|
entryState.lastCommitSha = shaOutput.trim();
|
|
285
357
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await exec("git push");
|
|
290
|
-
}
|
|
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}`);
|
|
291
361
|
|
|
292
362
|
console.log(
|
|
293
363
|
`[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
|
|
294
|
-
`commit
|
|
364
|
+
`commit pushed to ${this.branchName} (sha=${entryState.lastCommitSha.slice(0, 12)})`,
|
|
295
365
|
);
|
|
296
366
|
}
|
|
297
367
|
|
|
298
|
-
|
|
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(
|
|
299
377
|
entryName: string,
|
|
300
378
|
entryState: EntryState,
|
|
301
379
|
entry: EligibleEntry,
|
|
302
|
-
_exec: (cmd: string) => Promise<string>,
|
|
303
380
|
): Promise<void> {
|
|
304
381
|
const meta = entry.provisionResult.gitMetadata!;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (!entryState.githubToken) {
|
|
308
|
-
entryState.githubToken = extractGithubToken(meta.repoUrl);
|
|
382
|
+
if (!entryState.githubOwner) {
|
|
383
|
+
const { owner, repo } = parseGithubRepo(meta.repoUrl);
|
|
309
384
|
entryState.githubOwner = owner;
|
|
310
385
|
entryState.githubRepo = repo;
|
|
311
386
|
}
|
|
312
387
|
|
|
313
|
-
|
|
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\//, "");
|
|
314
409
|
const prBody =
|
|
315
|
-
`Automated pull request from Stigmer agent
|
|
316
|
-
`**
|
|
317
|
-
`**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`;
|
|
318
414
|
|
|
319
415
|
const resp = await fetch(
|
|
320
|
-
|
|
416
|
+
`${GITHUB_API}/repos/${entryState.githubOwner}/${entryState.githubRepo}/pulls`,
|
|
321
417
|
{
|
|
322
418
|
method: "POST",
|
|
323
|
-
headers:
|
|
324
|
-
"Authorization": `Bearer ${entryState.githubToken}`,
|
|
325
|
-
"Accept": "application/vnd.github+json",
|
|
326
|
-
"Content-Type": "application/json",
|
|
327
|
-
},
|
|
419
|
+
headers: this.githubHeaders(),
|
|
328
420
|
body: JSON.stringify({
|
|
329
|
-
title:
|
|
421
|
+
title: `Stigmer agent changes (${shortSessionId})`,
|
|
330
422
|
body: prBody,
|
|
331
423
|
head: this.branchName,
|
|
332
424
|
base: entry.baseBranch,
|
|
@@ -350,11 +442,44 @@ export class WriteBackCoordinator {
|
|
|
350
442
|
);
|
|
351
443
|
}
|
|
352
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
|
+
|
|
353
477
|
private async updateStatus(
|
|
354
478
|
entryName: string,
|
|
355
479
|
entryState: EntryState,
|
|
356
480
|
entry: EligibleEntry,
|
|
357
481
|
exec: (cmd: string) => Promise<string>,
|
|
482
|
+
prError: string,
|
|
358
483
|
): Promise<void> {
|
|
359
484
|
const summaryOutput = await exec(
|
|
360
485
|
`git diff --stat ${entry.baseBranch}...HEAD`,
|
|
@@ -373,6 +498,9 @@ export class WriteBackCoordinator {
|
|
|
373
498
|
pullRequestNumber: entryState.prNumber,
|
|
374
499
|
diffSummary: summaryOutput.trim(),
|
|
375
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,
|
|
376
504
|
});
|
|
377
505
|
|
|
378
506
|
this.statusWriter.addWriteBack(wb);
|
|
@@ -406,19 +534,3 @@ export function parseGithubRepo(repoUrl: string): { owner: string; repo: string
|
|
|
406
534
|
}
|
|
407
535
|
throw new Error(`Cannot parse GitHub owner/repo from URL: ${repoUrl}`);
|
|
408
536
|
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Extract a GitHub token from an HTTPS clone URL that has credentials embedded.
|
|
412
|
-
* Format: https://{token}@github.com/...
|
|
413
|
-
*/
|
|
414
|
-
export function extractGithubToken(repoUrl: string): string {
|
|
415
|
-
const match = repoUrl.match(/https?:\/\/([^@]+)@github\.com/);
|
|
416
|
-
if (match) return match[1];
|
|
417
|
-
|
|
418
|
-
const envToken = process.env.GITHUB_TOKEN;
|
|
419
|
-
if (envToken) return envToken;
|
|
420
|
-
|
|
421
|
-
throw new Error(
|
|
422
|
-
"Cannot extract GitHub token from repo URL and GITHUB_TOKEN is not set",
|
|
423
|
-
);
|
|
424
|
-
}
|
|
@@ -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"}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Incremental git write-back coordinator for workspace entries.
|
|
3
|
-
*
|
|
4
|
-
* During agent execution, each file-modifying tool call triggers an
|
|
5
|
-
* incremental commit-and-push cycle for the affected git workspace.
|
|
6
|
-
* The first cycle creates the branch and PR; subsequent cycles add
|
|
7
|
-
* commits to the same branch — the PR updates automatically on GitHub.
|
|
8
|
-
*
|
|
9
|
-
* DD-5: Incremental writeback, not batch.
|
|
10
|
-
*
|
|
11
|
-
* Lifecycle:
|
|
12
|
-
* 1. Created after workspace provisioning in index.ts.
|
|
13
|
-
* 2. `onFileModified(path)` called from streaming on each write/edit tool end.
|
|
14
|
-
* 3. `finalize()` called from post-stream as a safety net.
|
|
15
|
-
*
|
|
16
|
-
* Concurrency: one mutex per workspace entry serializes git operations.
|
|
17
|
-
*/
|
|
18
|
-
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
19
|
-
import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
|
|
20
|
-
import type { ExecutionStatusWriter } from "./execution-status-writer.js";
|
|
21
|
-
export declare class WriteBackCoordinator {
|
|
22
|
-
private readonly statusWriter;
|
|
23
|
-
private readonly executionId;
|
|
24
|
-
private readonly workspaceBackend;
|
|
25
|
-
private readonly shortId;
|
|
26
|
-
private readonly branchName;
|
|
27
|
-
private readonly eligible;
|
|
28
|
-
private readonly state;
|
|
29
|
-
private readonly locks;
|
|
30
|
-
constructor(opts: {
|
|
31
|
-
statusWriter: ExecutionStatusWriter;
|
|
32
|
-
executionId: string;
|
|
33
|
-
provisionResults: readonly ProvisionResult[];
|
|
34
|
-
workspaceEntries: readonly WorkspaceEntry[];
|
|
35
|
-
workspaceBackend: WorkspaceBackend;
|
|
36
|
-
});
|
|
37
|
-
get hasEligibleEntries(): boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Called after a file-modifying tool completes. Resolves the path to
|
|
40
|
-
* a workspace entry and runs an incremental commit/push cycle.
|
|
41
|
-
* Fire-and-forget: errors are logged, never thrown.
|
|
42
|
-
*/
|
|
43
|
-
onFileModified(path: string): Promise<void>;
|
|
44
|
-
/**
|
|
45
|
-
* Post-execution safety net. Checks every eligible workspace entry
|
|
46
|
-
* for remaining uncommitted changes and commits/pushes them.
|
|
47
|
-
*/
|
|
48
|
-
finalize(): Promise<void>;
|
|
49
|
-
private initEligibleEntries;
|
|
50
|
-
private resolveEntry;
|
|
51
|
-
private incrementalWriteBack;
|
|
52
|
-
private hasChanges;
|
|
53
|
-
private createBranch;
|
|
54
|
-
private commitAndPush;
|
|
55
|
-
private createPr;
|
|
56
|
-
private updateStatus;
|
|
57
|
-
private withLock;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Extract GitHub owner and repo from a clone URL.
|
|
61
|
-
* Supports HTTPS (with or without .git) and SSH formats.
|
|
62
|
-
*/
|
|
63
|
-
export declare function parseGithubRepo(repoUrl: string): {
|
|
64
|
-
owner: string;
|
|
65
|
-
repo: string;
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* Extract a GitHub token from an HTTPS clone URL that has credentials embedded.
|
|
69
|
-
* Format: https://{token}@github.com/...
|
|
70
|
-
*/
|
|
71
|
-
export declare function extractGithubToken(repoUrl: string): string;
|