@stigmer/runner 3.1.3 → 3.1.5

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 (114) 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/capture-flow.d.ts +26 -15
  5. package/dist/activities/execute-cursor/capture-flow.js +56 -18
  6. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  7. package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
  8. package/dist/activities/execute-cursor/command-provenance.js +25 -115
  9. package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
  10. package/dist/activities/execute-cursor/index.js +297 -478
  11. package/dist/activities/execute-cursor/index.js.map +1 -1
  12. package/dist/activities/execute-cursor/skill-resolver.js +1 -1
  13. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  14. package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
  15. package/dist/activities/execute-cursor/todo-tracker.js +15 -43
  16. package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
  17. package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
  18. package/dist/activities/execute-cursor/turn-stream.js +249 -0
  19. package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
  20. package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
  21. package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
  22. package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
  23. package/dist/activities/execute-deep-agent/index.js +88 -20
  24. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  25. package/dist/activities/execute-deep-agent/status-builder.js +8 -1
  26. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
  28. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  29. package/dist/activities/execute-deep-agent/writeback-coordinator.js +5 -1
  30. package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +1 -1
  31. package/dist/otel.js +10 -0
  32. package/dist/otel.js.map +1 -1
  33. package/dist/shared/filereview/cas-progress.d.ts +63 -0
  34. package/dist/shared/filereview/cas-progress.js +128 -0
  35. package/dist/shared/filereview/cas-progress.js.map +1 -0
  36. package/dist/shared/filereview/cas-substrate.d.ts +29 -0
  37. package/dist/shared/filereview/cas-substrate.js +46 -21
  38. package/dist/shared/filereview/cas-substrate.js.map +1 -1
  39. package/dist/shared/filereview/command-provenance.d.ts +93 -0
  40. package/dist/shared/filereview/command-provenance.js +132 -0
  41. package/dist/shared/filereview/command-provenance.js.map +1 -0
  42. package/dist/shared/filereview/git-substrate.d.ts +9 -3
  43. package/dist/shared/filereview/git-substrate.js +4 -0
  44. package/dist/shared/filereview/git-substrate.js.map +1 -1
  45. package/dist/shared/filereview/index.d.ts +4 -3
  46. package/dist/shared/filereview/index.js +3 -2
  47. package/dist/shared/filereview/index.js.map +1 -1
  48. package/dist/shared/filereview/progress.d.ts +105 -34
  49. package/dist/shared/filereview/progress.js +96 -34
  50. package/dist/shared/filereview/progress.js.map +1 -1
  51. package/dist/shared/plan-mode-prompt.d.ts +9 -0
  52. package/dist/shared/plan-mode-prompt.js +18 -0
  53. package/dist/shared/plan-mode-prompt.js.map +1 -1
  54. package/dist/shared/todos.d.ts +56 -0
  55. package/dist/shared/todos.js +98 -0
  56. package/dist/shared/todos.js.map +1 -0
  57. package/dist/shared/tool-row.d.ts +16 -0
  58. package/dist/shared/tool-row.js +31 -0
  59. package/dist/shared/tool-row.js.map +1 -1
  60. package/dist/shared/workspace/git-identity.d.ts +36 -0
  61. package/dist/shared/workspace/git-identity.js +39 -0
  62. package/dist/shared/workspace/git-identity.js.map +1 -0
  63. package/dist/shared/workspace/local-backend.d.ts +6 -2
  64. package/dist/shared/workspace/local-backend.js +6 -2
  65. package/dist/shared/workspace/local-backend.js.map +1 -1
  66. package/dist/shared/workspace/stigmer-link.d.ts +54 -0
  67. package/dist/shared/workspace/stigmer-link.js +92 -0
  68. package/dist/shared/workspace/stigmer-link.js.map +1 -0
  69. package/dist/shared/workspace/types.d.ts +6 -4
  70. package/package.json +2 -2
  71. package/src/__tests__/otel-turn-span.test.ts +61 -0
  72. package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
  73. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
  74. package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
  75. package/src/activities/execute-cursor/capture-flow.ts +71 -25
  76. package/src/activities/execute-cursor/command-provenance.ts +25 -120
  77. package/src/activities/execute-cursor/index.ts +345 -505
  78. package/src/activities/execute-cursor/skill-resolver.ts +1 -1
  79. package/src/activities/execute-cursor/todo-tracker.ts +17 -59
  80. package/src/activities/execute-cursor/turn-stream.ts +418 -0
  81. package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
  82. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
  83. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
  84. package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +30 -3
  85. package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
  86. package/src/activities/execute-deep-agent/index.ts +107 -20
  87. package/src/activities/execute-deep-agent/status-builder.ts +9 -0
  88. package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
  89. package/src/activities/execute-deep-agent/writeback-coordinator.ts +5 -1
  90. package/src/otel.ts +8 -0
  91. package/src/shared/__tests__/todos.test.ts +216 -0
  92. package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
  93. package/src/shared/filereview/__tests__/cas-substrate.test.ts +66 -0
  94. package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
  95. package/src/shared/filereview/__tests__/progress.test.ts +112 -10
  96. package/src/shared/filereview/cas-progress.ts +170 -0
  97. package/src/shared/filereview/cas-substrate.ts +69 -24
  98. package/src/shared/filereview/command-provenance.ts +180 -0
  99. package/src/shared/filereview/git-substrate.ts +13 -3
  100. package/src/shared/filereview/index.ts +15 -1
  101. package/src/shared/filereview/progress.ts +171 -47
  102. package/src/shared/plan-mode-prompt.ts +18 -0
  103. package/src/shared/todos.ts +126 -0
  104. package/src/shared/tool-row.ts +34 -0
  105. package/src/shared/workspace/__tests__/git-identity.test.ts +124 -0
  106. package/src/shared/workspace/__tests__/stigmer-link.test.ts +173 -0
  107. package/src/shared/workspace/git-identity.ts +41 -0
  108. package/src/shared/workspace/local-backend.ts +6 -2
  109. package/src/shared/workspace/stigmer-link.ts +97 -0
  110. package/src/shared/workspace/types.ts +6 -4
  111. package/dist/activities/execute-cursor/stigmer-link.d.ts +0 -35
  112. package/dist/activities/execute-cursor/stigmer-link.js +0 -73
  113. package/dist/activities/execute-cursor/stigmer-link.js.map +0 -1
  114. package/src/activities/execute-cursor/stigmer-link.ts +0 -78
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import {
7
+ AGENT_GIT_AUTHOR_NAME,
8
+ AGENT_GIT_AUTHOR_EMAIL,
9
+ AGENT_GIT_IDENTITY_FLAGS,
10
+ gitCommitAsAgent,
11
+ } from "../git-identity.js";
12
+
13
+ /**
14
+ * Environment that approximates the cloud sandbox: no global/system gitconfig,
15
+ * no GIT_AUTHOR / GIT_COMMITTER variables. In the real sandbox a bare
16
+ * `git commit` here fails with "Author identity unknown" (hostname-based
17
+ * auto-detection yields an invalid ident inside the pod); on a developer
18
+ * machine git may still auto-detect from username + hostname, so these tests
19
+ * assert the fix's guarantee — the pinned identity always wins — rather than
20
+ * the environment-dependent failure mode.
21
+ */
22
+ function identitylessEnv(): NodeJS.ProcessEnv {
23
+ const env = { ...process.env };
24
+ delete env.GIT_AUTHOR_NAME;
25
+ delete env.GIT_AUTHOR_EMAIL;
26
+ delete env.GIT_COMMITTER_NAME;
27
+ delete env.GIT_COMMITTER_EMAIL;
28
+ env.GIT_CONFIG_GLOBAL = "/dev/null";
29
+ env.GIT_CONFIG_SYSTEM = "/dev/null";
30
+ return env;
31
+ }
32
+
33
+ function sh(
34
+ cwd: string,
35
+ command: string,
36
+ envOverrides: Record<string, string> = {},
37
+ ): Promise<string> {
38
+ return new Promise((resolve, reject) => {
39
+ execFile(
40
+ "sh",
41
+ ["-c", command],
42
+ { cwd, env: { ...identitylessEnv(), ...envOverrides } },
43
+ (err, stdout, stderr) => {
44
+ if (err) reject(new Error(`Command failed: ${command}\n${stderr || err.message}`));
45
+ else resolve(stdout);
46
+ },
47
+ );
48
+ });
49
+ }
50
+
51
+ describe("gitCommitAsAgent (real repo, no ambient identity)", () => {
52
+ let repo: string;
53
+
54
+ beforeEach(async () => {
55
+ repo = mkdtempSync(join(tmpdir(), "git-identity-test-"));
56
+ await sh(repo, "git init -q");
57
+ writeFileSync(join(repo, "notes.md"), "# Project Notes\n");
58
+ await sh(repo, "git add -A");
59
+ });
60
+
61
+ afterEach(() => {
62
+ rmSync(repo, { recursive: true, force: true });
63
+ });
64
+
65
+ /**
66
+ * `user.useConfigOnly=true` (injected via git's config env) forbids the
67
+ * username+hostname auto-detection that a developer machine would otherwise
68
+ * fall back to — deterministically reproducing the sandbox condition where
69
+ * git has no usable identity.
70
+ */
71
+ const NO_AUTODETECT_ENV = {
72
+ GIT_CONFIG_COUNT: "1",
73
+ GIT_CONFIG_KEY_0: "user.useConfigOnly",
74
+ GIT_CONFIG_VALUE_0: "true",
75
+ };
76
+
77
+ it("reproduces the cloud failure: a bare commit has no identity", async () => {
78
+ await expect(
79
+ sh(repo, 'git commit -m "agent changes (1)"', NO_AUTODETECT_ENV),
80
+ ).rejects.toThrow(/user\.email|user\.name|Author identity unknown|empty ident/);
81
+ });
82
+
83
+ it("the agent identity satisfies git even when auto-detection is forbidden", async () => {
84
+ await sh(repo, gitCommitAsAgent("agent changes (1)"), NO_AUTODETECT_ENV);
85
+
86
+ const author = await sh(repo, "git log -1 --format='%an <%ae>'");
87
+ expect(author.trim()).toBe(
88
+ `${AGENT_GIT_AUTHOR_NAME} <${AGENT_GIT_AUTHOR_EMAIL}>`,
89
+ );
90
+ });
91
+
92
+ it("commits successfully with the agent identity", async () => {
93
+ await sh(repo, gitCommitAsAgent("agent changes (1)"));
94
+
95
+ const author = await sh(repo, "git log -1 --format='%an <%ae>'");
96
+ expect(author.trim()).toBe(
97
+ `${AGENT_GIT_AUTHOR_NAME} <${AGENT_GIT_AUTHOR_EMAIL}>`,
98
+ );
99
+
100
+ const committer = await sh(repo, "git log -1 --format='%cn <%ce>'");
101
+ expect(committer.trim()).toBe(
102
+ `${AGENT_GIT_AUTHOR_NAME} <${AGENT_GIT_AUTHOR_EMAIL}>`,
103
+ );
104
+
105
+ const message = await sh(repo, "git log -1 --format=%s");
106
+ expect(message.trim()).toBe("agent changes (1)");
107
+ });
108
+
109
+ it("never mutates the repository's git config", async () => {
110
+ await sh(repo, gitCommitAsAgent("agent changes (1)"));
111
+
112
+ // `git config --local` exits non-zero when the key is unset.
113
+ await expect(sh(repo, "git config --local user.name")).rejects.toThrow();
114
+ await expect(sh(repo, "git config --local user.email")).rejects.toThrow();
115
+ });
116
+ });
117
+
118
+ describe("AGENT_GIT_IDENTITY_FLAGS", () => {
119
+ it("pins both name and email via per-command -c flags", () => {
120
+ expect(AGENT_GIT_IDENTITY_FLAGS).toBe(
121
+ `-c user.name='${AGENT_GIT_AUTHOR_NAME}' -c user.email='${AGENT_GIT_AUTHOR_EMAIL}'`,
122
+ );
123
+ });
124
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtemp, mkdir, rm, writeFile, readFile, lstat, readlink } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { FilesystemBackend } from "deepagents";
6
+
7
+ import {
8
+ ensureStigmerSymlink,
9
+ removeStigmerSymlink,
10
+ STIGMER_LOCAL_STATE_DIR,
11
+ } from "../stigmer-link.js";
12
+ import { LocalWorkspaceBackend } from "../local-backend.js";
13
+ import { injectAttachments } from "../../../activities/execute-deep-agent/attachment-injector.js";
14
+
15
+ /**
16
+ * The `.stigmer` symlink is the bridge that makes platform-mounted content
17
+ * (approved plan, skills, attachment inputs) readable by the agent's file
18
+ * tools in BOTH harnesses. The lifecycle tests pin the ownership contract
19
+ * (ensure replaces, remove is symlink-only); the convergence tests pin the
20
+ * end-to-end physics the native harness depends on: content written through
21
+ * the platform-routing LocalWorkspaceBackend must be readable at the same
22
+ * `.stigmer/…` path through a stock deepagents FilesystemBackend rooted at
23
+ * the workspace — which is exactly what broke when the native harness had no
24
+ * symlink (the "plan file doesn't exist" bug).
25
+ */
26
+ describe("stigmer-link", () => {
27
+ let workspaceDir: string;
28
+ let platformDir: string;
29
+
30
+ beforeEach(async () => {
31
+ workspaceDir = await mkdtemp(join(tmpdir(), "stigmer-link-ws-"));
32
+ platformDir = await mkdtemp(join(tmpdir(), "stigmer-link-platform-"));
33
+ });
34
+
35
+ afterEach(async () => {
36
+ await rm(workspaceDir, { recursive: true, force: true });
37
+ await rm(platformDir, { recursive: true, force: true });
38
+ });
39
+
40
+ const linkPath = () => join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
41
+
42
+ describe("ensureStigmerSymlink", () => {
43
+ it("creates the workspace symlink pointing at the platform dir", async () => {
44
+ await ensureStigmerSymlink(workspaceDir, platformDir);
45
+
46
+ expect((await lstat(linkPath())).isSymbolicLink()).toBe(true);
47
+ expect(await readlink(linkPath())).toBe(platformDir);
48
+ });
49
+
50
+ it("is idempotent for a correct existing link", async () => {
51
+ await ensureStigmerSymlink(workspaceDir, platformDir);
52
+ await ensureStigmerSymlink(workspaceDir, platformDir);
53
+
54
+ expect(await readlink(linkPath())).toBe(platformDir);
55
+ });
56
+
57
+ it("re-points a stale link at the current platform dir", async () => {
58
+ const stalePlatform = await mkdtemp(join(tmpdir(), "stigmer-link-stale-"));
59
+ try {
60
+ await ensureStigmerSymlink(workspaceDir, stalePlatform);
61
+ await ensureStigmerSymlink(workspaceDir, platformDir);
62
+
63
+ expect(await readlink(linkPath())).toBe(platformDir);
64
+ } finally {
65
+ await rm(stalePlatform, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ it("replaces a real .stigmer directory (platform owns the name)", async () => {
70
+ // The ownership sharp edge, pinned deliberately: a non-symlink
71
+ // `.stigmer` (left behind by an older runner) must never shadow the
72
+ // platform mount. See the module header for why this is safe.
73
+ await mkdir(join(linkPath(), "old"), { recursive: true });
74
+ await writeFile(join(linkPath(), "old", "stale.txt"), "stale");
75
+
76
+ await ensureStigmerSymlink(workspaceDir, platformDir);
77
+
78
+ expect((await lstat(linkPath())).isSymbolicLink()).toBe(true);
79
+ expect(await readlink(linkPath())).toBe(platformDir);
80
+ });
81
+ });
82
+
83
+ describe("removeStigmerSymlink", () => {
84
+ it("removes the symlink and leaves the platform dir intact", async () => {
85
+ await writeFile(join(platformDir, "keep.txt"), "keep");
86
+ await ensureStigmerSymlink(workspaceDir, platformDir);
87
+
88
+ await removeStigmerSymlink(workspaceDir);
89
+
90
+ await expect(lstat(linkPath())).rejects.toMatchObject({ code: "ENOENT" });
91
+ expect(await readFile(join(platformDir, "keep.txt"), "utf-8")).toBe("keep");
92
+ });
93
+
94
+ it("leaves a real .stigmer directory untouched (symlink-only removal)", async () => {
95
+ await mkdir(linkPath(), { recursive: true });
96
+ await writeFile(join(linkPath(), "user-owned.txt"), "mine");
97
+
98
+ await removeStigmerSymlink(workspaceDir);
99
+
100
+ expect(await readFile(join(linkPath(), "user-owned.txt"), "utf-8")).toBe("mine");
101
+ });
102
+
103
+ it("is a no-op when no link exists", async () => {
104
+ await expect(removeStigmerSymlink(workspaceDir)).resolves.toBeUndefined();
105
+ });
106
+ });
107
+
108
+ // ── Write/read convergence (the native-harness bug, pinned) ─────────
109
+
110
+ describe("platform write / agent read convergence", () => {
111
+ /** Read through the agent's actual file-tool backend. */
112
+ function agentRead(path: string): Promise<{ content?: string; error?: string }> {
113
+ const agentBackend = new FilesystemBackend({ rootDir: workspaceDir });
114
+ return agentBackend.read(path) as Promise<{ content?: string; error?: string }>;
115
+ }
116
+
117
+ it("approved plan injected via injectAttachments is readable by the deepagents backend", async () => {
118
+ const planFileName = "notes_ab12cd34.plan.md";
119
+ const planText = "# The Approved Plan\n\nStep 1: do the thing.\n";
120
+ const uploadPath = join(platformDir, "upload-src.md");
121
+ await writeFile(uploadPath, planText);
122
+
123
+ // Write path: exactly what the build turn does — the attachment is
124
+ // materialized through the platform-routing backend at the mount path
125
+ // the implement-plan directive will tell the model to read.
126
+ const workspaceBackend = new LocalWorkspaceBackend(workspaceDir, platformDir);
127
+ const injected = await injectAttachments({
128
+ backend: workspaceBackend,
129
+ attachments: [{
130
+ filename: planFileName,
131
+ storageKey: "",
132
+ mountPath: `.stigmer/inputs/${planFileName}`,
133
+ contentType: "text/markdown",
134
+ extract: false,
135
+ localPath: uploadPath,
136
+ $typeName: "ai.stigmer.agentic.agentexecution.v1.Attachment",
137
+ } as any],
138
+ storage: undefined,
139
+ isLocalMode: true,
140
+ });
141
+ expect(injected.map((f) => f.path)).toEqual([`.stigmer/inputs/${planFileName}`]);
142
+
143
+ // Without the symlink the agent cannot see the plan — the bug this
144
+ // module fixes for the native harness. Pinned so the link stays
145
+ // load-bearing in reviewers' and agents' mental models.
146
+ const before = await agentRead(`.stigmer/inputs/${planFileName}`);
147
+ expect(before.content ?? "").not.toContain("The Approved Plan");
148
+
149
+ await ensureStigmerSymlink(workspaceDir, platformDir);
150
+
151
+ const after = await agentRead(`.stigmer/inputs/${planFileName}`);
152
+ expect(after.error).toBeUndefined();
153
+ expect(after.content).toContain("The Approved Plan");
154
+ });
155
+
156
+ it("skill written through the platform-routing backend is readable at its prompt location", async () => {
157
+ // Same physical write the skill-writer performs for
158
+ // `.stigmer/skills/{name}/SKILL.md` — the path the system prompt tells
159
+ // the model to read to activate the skill.
160
+ const workspaceBackend = new LocalWorkspaceBackend(workspaceDir, platformDir);
161
+ await workspaceBackend.writeFile(
162
+ ".stigmer/skills/my-skill/SKILL.md",
163
+ "# My Skill\n\nDo skillful things.\n",
164
+ );
165
+
166
+ await ensureStigmerSymlink(workspaceDir, platformDir);
167
+
168
+ const result = await agentRead(".stigmer/skills/my-skill/SKILL.md");
169
+ expect(result.error).toBeUndefined();
170
+ expect(result.content).toContain("My Skill");
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The public git identity for commits the agent authors on the user's behalf.
3
+ *
4
+ * Write-back commits (branch/commit/push/PR — see WriteBackCoordinator) land
5
+ * on real branches in the user's repository, so they carry a dedicated,
6
+ * recognizable agent identity in every mode — local and cloud alike. This is
7
+ * deliberate: a commit authored by the agent should never be silently
8
+ * attributed to whatever identity happens to live in the host's `~/.gitconfig`
9
+ * (local), and the cloud sandbox has no identity at all, which makes a bare
10
+ * `git commit` fail with "Author identity unknown".
11
+ *
12
+ * Distinct from the *internal* snapshot identity in
13
+ * `../filereview/git-substrate.ts` (`stigmer-runner <runner@stigmer.local>`):
14
+ * those commit objects are never on a branch and never pushed — they exist
15
+ * only to pin trees against GC. This identity is the public-facing one that
16
+ * appears in `git log`, on GitHub, and in pull requests.
17
+ */
18
+ export const AGENT_GIT_AUTHOR_NAME = "Stigmer Agent";
19
+ export const AGENT_GIT_AUTHOR_EMAIL = "noreply@stigmer.ai";
20
+
21
+ /**
22
+ * `git -c` config flags that pin the agent identity for a single command.
23
+ *
24
+ * Per-command `-c` flags (rather than repo-local or global `git config`) keep
25
+ * the identity deterministic in every environment without mutating the user's
26
+ * repository config — the same never-mutate reasoning documented on
27
+ * `SNAPSHOT_IDENTITY_ENV` in `git-substrate.ts`. Env-var injection is not an
28
+ * option here: `WorkspaceBackend.execute()` takes no env parameter, and
29
+ * widening that contract for one caller would be a larger change than the
30
+ * problem deserves.
31
+ */
32
+ export const AGENT_GIT_IDENTITY_FLAGS =
33
+ `-c user.name='${AGENT_GIT_AUTHOR_NAME}' -c user.email='${AGENT_GIT_AUTHOR_EMAIL}'`;
34
+
35
+ /**
36
+ * Build a `git commit` command that is always authored by the agent identity,
37
+ * regardless of what identity (if any) the surrounding environment provides.
38
+ */
39
+ export function gitCommitAsAgent(commitMsg: string): string {
40
+ return `git ${AGENT_GIT_IDENTITY_FLAGS} commit -m "${commitMsg}"`;
41
+ }
@@ -6,8 +6,12 @@
6
6
  * sandbox) backend will be added in Phase 3.
7
7
  *
8
8
  * When `platformDir` is provided, paths under `.stigmer/` are
9
- * transparently routed to the platform directory. The agent sees a
10
- * unified namespace; the workspace directory is never polluted.
9
+ * transparently routed to the platform directory, keeping platform files
10
+ * out of the workspace tree. This routing serves the RUNNER's own I/O
11
+ * (skill/attachment materialization, `execute` command rewriting) — the
12
+ * agent's file tools do not go through this backend. Agent-visible reads
13
+ * of `.stigmer/…` reach the same physical files through the per-turn
14
+ * workspace symlink instead (see shared/workspace/stigmer-link.ts).
11
15
  */
12
16
 
13
17
  import { execFile } from "node:child_process";
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The workspace `.stigmer` symlink — the bridge from the session's workspace
3
+ * to its platform-managed directory (~/.stigmer/sessions/{id}/platform/, see
4
+ * platform-dir.ts), shared by BOTH harnesses.
5
+ *
6
+ * Platform-injected content (skills, attachment inputs, the approved plan)
7
+ * physically lives in the platform dir, outside the workspace, so a real repo
8
+ * is never polluted with Stigmer files (issue #173). But the agent reads from
9
+ * the workspace: the Cursor SDK resolves paths against the workspace CWD, and
10
+ * the native (deepagents) harness's file tools resolve against a
11
+ * `FilesystemBackend` rooted at the workspace. The symlink is what makes the
12
+ * `.stigmer/…` paths those agents are prompted with (skill locations,
13
+ * `.stigmer/inputs/…` attachments) actually resolve.
14
+ *
15
+ * Lifecycle contract (same for both harnesses):
16
+ * - Created per turn, under the workspace turn lock — the link is a
17
+ * working-tree mutation, and re-pointing it while another session's turn is
18
+ * running on a shared tree would redirect that turn's reads mid-flight.
19
+ * Cursor creates it in its skill/attachment resolvers (which run after lock
20
+ * acquisition); the native harness creates it right after acquiring the
21
+ * lock (execute-deep-agent/index.ts).
22
+ * - Removed at turn end ({@link removeStigmerSymlink} in the activity's
23
+ * cleanup), so a real repo is left untouched once the turn ends; a
24
+ * multi-turn session recreates the link on the next turn.
25
+ * - {@link ensureStigmerSymlink} is idempotent, so multiple populaters may
26
+ * run in any order within a turn.
27
+ *
28
+ * OWNERSHIP SHARP EDGE: the platform owns the `.stigmer` name inside a
29
+ * workspace. {@link ensureStigmerSymlink} REPLACES a real (non-symlink)
30
+ * `.stigmer` directory it finds there — deliberate, so a directory left
31
+ * behind by an older runner can never shadow the platform mount. (A user's
32
+ * own `.stigmer` content is unreachable anyway: the runner's
33
+ * `LocalWorkspaceBackend` routes every `.stigmer/…` path to the platform
34
+ * dir.) Turn-end removal is the conservative half: it only ever removes a
35
+ * SYMLINK.
36
+ */
37
+
38
+ import { symlink, readlink, unlink, rm, lstat } from "node:fs/promises";
39
+ import { join } from "node:path";
40
+
41
+ /** The workspace-visible name of the platform namespace. */
42
+ export const STIGMER_LOCAL_STATE_DIR = ".stigmer";
43
+
44
+ /**
45
+ * Ensure the workspace `.stigmer` symlink points to the platform dir.
46
+ * Idempotent: an existing correct link is kept; a stale link (or a non-link
47
+ * left behind by an older runner) is replaced.
48
+ */
49
+ export async function ensureStigmerSymlink(
50
+ workspaceDir: string,
51
+ platformDir: string,
52
+ ): Promise<void> {
53
+ const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
54
+
55
+ try {
56
+ const existing = await readlink(linkPath);
57
+ if (existing === platformDir) return;
58
+ await unlink(linkPath);
59
+ } catch (err: any) {
60
+ if (err.code === "ENOENT") {
61
+ // No existing symlink
62
+ } else if (err.code === "EINVAL") {
63
+ // Exists but is not a symlink — remove the directory
64
+ await rm(linkPath, { recursive: true, force: true });
65
+ } else {
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ await symlink(platformDir, linkPath, "dir");
71
+ }
72
+
73
+ /**
74
+ * Remove the workspace `.stigmer` symlink created by
75
+ * {@link ensureStigmerSymlink}.
76
+ *
77
+ * Called in the activity's cleanup so attaching a real repo leaves no Stigmer
78
+ * symlink behind once the turn ends (issue #173). Only ever removes a
79
+ * SYMLINK — a real `.stigmer` directory (which would be the user's own, not
80
+ * ours) is left untouched. Best-effort.
81
+ */
82
+ export async function removeStigmerSymlink(workspaceDir: string): Promise<void> {
83
+ const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
84
+ try {
85
+ const stat = await lstat(linkPath);
86
+ if (stat.isSymbolicLink()) {
87
+ await unlink(linkPath);
88
+ }
89
+ } catch (err: any) {
90
+ if (err?.code !== "ENOENT") {
91
+ console.warn(
92
+ `removeStigmerSymlink: failed to remove ${linkPath} (non-fatal): ` +
93
+ `${err instanceof Error ? err.message : err}`,
94
+ );
95
+ }
96
+ }
97
+ }
@@ -53,10 +53,12 @@ export class WorkspaceProvisionError extends Error {
53
53
  * commands route through the Daytona sandbox proxy.
54
54
  *
55
55
  * When `platformDir` is set, paths under `.stigmer/` are transparently
56
- * routed to the platform directory instead of the workspace root. This
57
- * keeps platform files (skills, inputs) physically separate from the
58
- * user's workspace while presenting a unified `.stigmer/` namespace to
59
- * the agent.
56
+ * routed to the platform directory instead of the workspace root, keeping
57
+ * platform files (skills, inputs) physically separate from the user's
58
+ * workspace. The routing covers the RUNNER's own reads/writes through
59
+ * this interface; the agent's file tools resolve against the workspace
60
+ * root and see the same files through the per-turn `.stigmer` symlink
61
+ * (see stigmer-link.ts).
60
62
  */
61
63
  export interface WorkspaceBackend {
62
64
  readonly rootDir: string;
@@ -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,78 +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
-
19
- import { symlink, readlink, unlink, rm, lstat } from "node:fs/promises";
20
- import { join } from "node:path";
21
-
22
- /** The workspace-visible name of the platform namespace. */
23
- export const STIGMER_LOCAL_STATE_DIR = ".stigmer";
24
-
25
- /**
26
- * Ensure the workspace `.stigmer` symlink points to the platform dir.
27
- * Idempotent: an existing correct link is kept; a stale link (or a non-link
28
- * left behind by an older runner) is replaced.
29
- */
30
- export async function ensureStigmerSymlink(
31
- workspaceDir: string,
32
- platformDir: string,
33
- ): Promise<void> {
34
- const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
35
-
36
- try {
37
- const existing = await readlink(linkPath);
38
- if (existing === platformDir) return;
39
- await unlink(linkPath);
40
- } catch (err: any) {
41
- if (err.code === "ENOENT") {
42
- // No existing symlink
43
- } else if (err.code === "EINVAL") {
44
- // Exists but is not a symlink — remove the directory
45
- await rm(linkPath, { recursive: true, force: true });
46
- } else {
47
- throw err;
48
- }
49
- }
50
-
51
- await symlink(platformDir, linkPath, "dir");
52
- }
53
-
54
- /**
55
- * Remove the workspace `.stigmer` symlink created by
56
- * {@link ensureStigmerSymlink}.
57
- *
58
- * Called in the activity's finally so attaching a real repo leaves no Stigmer
59
- * symlink behind once the turn ends (issue #173). Only ever removes a
60
- * SYMLINK — a real `.stigmer` directory (which would be the user's own, not
61
- * ours) is left untouched. Best-effort.
62
- */
63
- export async function removeStigmerSymlink(workspaceDir: string): Promise<void> {
64
- const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
65
- try {
66
- const stat = await lstat(linkPath);
67
- if (stat.isSymbolicLink()) {
68
- await unlink(linkPath);
69
- }
70
- } catch (err: any) {
71
- if (err?.code !== "ENOENT") {
72
- console.warn(
73
- `removeStigmerSymlink: failed to remove ${linkPath} (non-fatal): ` +
74
- `${err instanceof Error ? err.message : err}`,
75
- );
76
- }
77
- }
78
- }