@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.
Files changed (53) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/discover-mcp-server.js +23 -0
  3. package/dist/activities/discover-mcp-server.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.js +39 -1
  5. package/dist/activities/execute-cursor/index.js.map +1 -1
  6. package/dist/activities/execute-cursor/workspace-provision.d.ts +23 -4
  7. package/dist/activities/execute-cursor/workspace-provision.js +17 -6
  8. package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
  9. package/dist/activities/execute-deep-agent/index.js +8 -1
  10. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +1 -1
  12. package/dist/activities/execute-deep-agent/post-stream.d.ts +1 -1
  13. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +1 -1
  14. package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +1 -1
  15. package/dist/activities/execute-deep-agent/streaming.d.ts +1 -1
  16. package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +1 -1
  17. package/dist/shared/execution-status-writer.d.ts +31 -0
  18. package/dist/shared/execution-status-writer.js +43 -0
  19. package/dist/shared/execution-status-writer.js.map +1 -0
  20. package/dist/shared/mcp-oauth-detect.d.ts +53 -0
  21. package/dist/shared/mcp-oauth-detect.js +99 -0
  22. package/dist/shared/mcp-oauth-detect.js.map +1 -0
  23. package/dist/shared/workspace/writeback-coordinator.d.ts +120 -0
  24. package/dist/shared/workspace/writeback-coordinator.js +393 -0
  25. package/dist/shared/workspace/writeback-coordinator.js.map +1 -0
  26. package/package.json +2 -2
  27. package/src/activities/discover-mcp-server.ts +25 -0
  28. package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
  29. package/src/activities/execute-cursor/index.ts +41 -1
  30. package/src/activities/execute-cursor/workspace-provision.ts +39 -7
  31. package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +1 -1
  32. package/src/activities/execute-deep-agent/index.ts +8 -1
  33. package/src/activities/execute-deep-agent/inline-publisher.ts +1 -1
  34. package/src/activities/execute-deep-agent/post-stream.ts +1 -1
  35. package/src/activities/execute-deep-agent/status-builder.ts +1 -1
  36. package/src/activities/execute-deep-agent/streaming-side-effects.ts +1 -1
  37. package/src/activities/execute-deep-agent/streaming-terminal.ts +1 -1
  38. package/src/activities/execute-deep-agent/streaming.ts +1 -1
  39. package/src/activities/execute-deep-agent/v3-status-builder.ts +1 -1
  40. package/src/shared/__tests__/mcp-oauth-detect.test.ts +147 -0
  41. package/src/shared/execution-status-writer.ts +56 -0
  42. package/src/shared/mcp-oauth-detect.ts +112 -0
  43. package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +230 -0
  44. package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +477 -0
  45. package/src/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +206 -94
  46. package/dist/activities/execute-deep-agent/execution-status-writer.d.ts +0 -17
  47. package/dist/activities/execute-deep-agent/execution-status-writer.js +0 -9
  48. package/dist/activities/execute-deep-agent/execution-status-writer.js.map +0 -1
  49. package/dist/activities/execute-deep-agent/writeback-coordinator.d.ts +0 -71
  50. package/dist/activities/execute-deep-agent/writeback-coordinator.js +0 -299
  51. package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
  52. package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -426
  53. package/src/activities/execute-deep-agent/execution-status-writer.ts +0 -19
@@ -1,17 +1,27 @@
1
1
  /**
2
- * Incremental git write-back coordinator for workspace entries.
2
+ * Git write-back coordinator for workspace entries.
3
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.
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
- * DD-5: Incremental writeback, not batch.
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
- * 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.
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 "../../shared/workspace/types.js";
29
- import { SourceType } from "../../shared/workspace/types.js";
30
- import { gitCommitAsAgent } from "../../shared/workspace/git-identity.js";
31
- import type { ExecutionStatusWriter } from "./execution-status-writer.js";
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
- branchCreated: boolean;
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.shortId = opts.executionId.slice(0, 8);
79
- this.branchName = `stigmer/${this.shortId}`;
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. Resolves the path to
90
- * a workspace entry and runs an incremental commit/push cycle.
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.incrementalWriteBack(entryName),
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
- * Post-execution safety net. Checks every eligible workspace entry
111
- * for remaining uncommitted changes and commits/pushes them.
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.incrementalWriteBack(entryName),
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
- branchCreated: false,
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 Incremental Write-Back ─────────────────────────────────────
217
+ // ── Core Write-Back Cycle ───────────────────────────────────────────
194
218
 
195
- private async incrementalWriteBack(entryName: string): Promise<void> {
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
- mutationStarted = true;
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
- `incremental error: ${err}`,
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.branchCreated ? this.branchName : "",
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
- private async createBranch(
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(`git checkout -b ${this.branchName}`);
263
- entryState.branchCreated = true;
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
- `created branch ${this.branchName}`,
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
- await exec(gitCommitAsAgent(commitMsg));
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
- if (entryState.commitCount === 1) {
287
- await exec(`git push -u origin ${this.branchName}`);
288
- } else {
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 #${entryState.commitCount} pushed (sha=${entryState.lastCommitSha.slice(0, 12)})`,
364
+ `commit pushed to ${this.branchName} (sha=${entryState.lastCommitSha.slice(0, 12)})`,
295
365
  );
296
366
  }
297
367
 
298
- private async createPr(
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
- const { owner, repo } = parseGithubRepo(meta.repoUrl);
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
- const prTitle = `Agent changes (${this.shortId})`;
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 execution.\n\n` +
316
- `**Execution:** \`${this.executionId}\`\n` +
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
- `https://api.github.com/repos/${entryState.githubOwner}/${entryState.githubRepo}/pulls`,
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: prTitle,
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;