@stigmer/runner 3.1.4 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
  3. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.js +40 -2
  5. package/dist/activities/execute-cursor/index.js.map +1 -1
  6. package/dist/activities/execute-cursor/skill-resolver.js +1 -1
  7. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  8. package/dist/activities/execute-cursor/workspace-provision.d.ts +23 -4
  9. package/dist/activities/execute-cursor/workspace-provision.js +17 -6
  10. package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/index.js +39 -6
  12. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  13. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +1 -1
  14. package/dist/activities/execute-deep-agent/post-stream.d.ts +1 -1
  15. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +1 -1
  16. package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +1 -1
  17. package/dist/activities/execute-deep-agent/streaming.d.ts +1 -1
  18. package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +1 -1
  19. package/dist/shared/execution-status-writer.d.ts +31 -0
  20. package/dist/shared/execution-status-writer.js +43 -0
  21. package/dist/shared/execution-status-writer.js.map +1 -0
  22. package/dist/shared/filereview/git-substrate.js +4 -0
  23. package/dist/shared/filereview/git-substrate.js.map +1 -1
  24. package/dist/shared/workspace/git-identity.d.ts +36 -0
  25. package/dist/shared/workspace/git-identity.js +39 -0
  26. package/dist/shared/workspace/git-identity.js.map +1 -0
  27. package/dist/shared/workspace/local-backend.d.ts +6 -2
  28. package/dist/shared/workspace/local-backend.js +6 -2
  29. package/dist/shared/workspace/local-backend.js.map +1 -1
  30. package/dist/shared/workspace/stigmer-link.d.ts +54 -0
  31. package/dist/shared/workspace/stigmer-link.js +92 -0
  32. package/dist/shared/workspace/stigmer-link.js.map +1 -0
  33. package/dist/shared/workspace/types.d.ts +6 -4
  34. package/dist/shared/workspace/writeback-coordinator.d.ts +120 -0
  35. package/dist/shared/workspace/writeback-coordinator.js +393 -0
  36. package/dist/shared/workspace/writeback-coordinator.js.map +1 -0
  37. package/package.json +2 -2
  38. package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
  39. package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
  40. package/src/activities/execute-cursor/index.ts +42 -2
  41. package/src/activities/execute-cursor/skill-resolver.ts +1 -1
  42. package/src/activities/execute-cursor/workspace-provision.ts +39 -7
  43. package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +1 -1
  44. package/src/activities/execute-deep-agent/index.ts +44 -6
  45. package/src/activities/execute-deep-agent/inline-publisher.ts +1 -1
  46. package/src/activities/execute-deep-agent/post-stream.ts +1 -1
  47. package/src/activities/execute-deep-agent/status-builder.ts +1 -1
  48. package/src/activities/execute-deep-agent/streaming-side-effects.ts +1 -1
  49. package/src/activities/execute-deep-agent/streaming-terminal.ts +1 -1
  50. package/src/activities/execute-deep-agent/streaming.ts +1 -1
  51. package/src/activities/execute-deep-agent/v3-status-builder.ts +1 -1
  52. package/src/shared/execution-status-writer.ts +56 -0
  53. package/src/shared/filereview/git-substrate.ts +4 -0
  54. package/src/shared/workspace/__tests__/git-identity.test.ts +124 -0
  55. package/src/shared/workspace/__tests__/stigmer-link.test.ts +173 -0
  56. package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +230 -0
  57. package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +477 -0
  58. package/src/shared/workspace/git-identity.ts +41 -0
  59. package/src/shared/workspace/local-backend.ts +6 -2
  60. package/src/shared/workspace/stigmer-link.ts +97 -0
  61. package/src/shared/workspace/types.ts +6 -4
  62. package/src/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +209 -93
  63. package/dist/activities/execute-cursor/stigmer-link.d.ts +0 -35
  64. package/dist/activities/execute-cursor/stigmer-link.js +0 -73
  65. package/dist/activities/execute-cursor/stigmer-link.js.map +0 -1
  66. package/dist/activities/execute-deep-agent/execution-status-writer.d.ts +0 -17
  67. package/dist/activities/execute-deep-agent/execution-status-writer.js +0 -9
  68. package/dist/activities/execute-deep-agent/execution-status-writer.js.map +0 -1
  69. package/dist/activities/execute-deep-agent/writeback-coordinator.d.ts +0 -71
  70. package/dist/activities/execute-deep-agent/writeback-coordinator.js +0 -295
  71. package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
  72. package/src/activities/execute-cursor/stigmer-link.ts +0 -78
  73. package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -399
  74. package/src/activities/execute-deep-agent/execution-status-writer.ts +0 -19
@@ -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,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 "../../shared/workspace/types.js";
29
- import { SourceType } from "../../shared/workspace/types.js";
30
- 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";
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
- branchCreated: boolean;
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.shortId = opts.executionId.slice(0, 8);
78
- 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}`;
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. Resolves the path to
89
- * a workspace entry and runs an incremental commit/push cycle.
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.incrementalWriteBack(entryName),
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
- * Post-execution safety net. Checks every eligible workspace entry
110
- * 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.
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.incrementalWriteBack(entryName),
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
- branchCreated: false,
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 Incremental Write-Back ─────────────────────────────────────
217
+ // ── Core Write-Back Cycle ───────────────────────────────────────────
193
218
 
194
- 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> {
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
- mutationStarted = true;
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
- `incremental error: ${err}`,
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.branchCreated ? this.branchName : "",
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
- 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(
257
307
  entryName: string,
258
308
  entryState: EntryState,
259
309
  exec: (cmd: string) => Promise<string>,
260
310
  ): Promise<void> {
261
- await exec(`git checkout -b ${this.branchName}`);
262
- 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;
263
336
  console.log(
264
337
  `[WriteBack] execution=${this.executionId} entry=${entryName} — ` +
265
- `created branch ${this.branchName}`,
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
- await exec(`git commit -m "${commitMsg}"`);
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
- if (entryState.commitCount === 1) {
283
- await exec(`git push -u origin ${this.branchName}`);
284
- } else {
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 #${entryState.commitCount} pushed (sha=${entryState.lastCommitSha.slice(0, 12)})`,
364
+ `commit pushed to ${this.branchName} (sha=${entryState.lastCommitSha.slice(0, 12)})`,
291
365
  );
292
366
  }
293
367
 
294
- 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(
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
- const { owner, repo } = parseGithubRepo(meta.repoUrl);
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
- 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\//, "");
310
409
  const prBody =
311
- `Automated pull request from Stigmer agent execution.\n\n` +
312
- `**Execution:** \`${this.executionId}\`\n` +
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
- `https://api.github.com/repos/${entryState.githubOwner}/${entryState.githubRepo}/pulls`,
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: prTitle,
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"}