@stigmer/runner 3.40.0 → 3.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/prompt-sections.test.ts +2 -54
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/sources/git.ts +14 -8
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Stigmer server's address for user-defined MCP servers — the reserved
|
|
3
|
+
* STIGMER_SERVER_ADDRESS key, and the one dial-target rule every writer of
|
|
4
|
+
* that key shares (stigmer/stigmer#1433, #1446).
|
|
5
|
+
*
|
|
6
|
+
* The runner is the one component that supplies it, because the runner is
|
|
7
|
+
* the one that knows where the consumer runs:
|
|
8
|
+
*
|
|
9
|
+
* - A remote HTTP server receives the key only through a templated header,
|
|
10
|
+
* so it needs the server's PUBLIC address: STIGMER_MCP_PUBLIC_ENDPOINT,
|
|
11
|
+
* which the server hands every sandbox it provisions from its own
|
|
12
|
+
* configuration (stigmer/stigmer#1447), or an operator names to a
|
|
13
|
+
* runner it starts by hand.
|
|
14
|
+
* - A stdio server is a child of this runner (only a local runner spawns
|
|
15
|
+
* one; mcp-transport-guard.ts), so the address the runner itself dials,
|
|
16
|
+
* STIGMER_BACKEND_ENDPOINT, is right for it by construction.
|
|
17
|
+
*
|
|
18
|
+
* No page supplies it: a value written into a run's runtime_env sits in the
|
|
19
|
+
* top merge layer, above every value the user saved (stigmer/stigmer#1446).
|
|
20
|
+
*
|
|
21
|
+
* The runner FILLS, it never overrides: a non-empty value already in the
|
|
22
|
+
* environment (one the user saved or typed to aim a server elsewhere)
|
|
23
|
+
* stands. A key nobody can supply stays missing, and a
|
|
24
|
+
* header that templates it fails with the resolver's named
|
|
25
|
+
* PlaceholderResolutionError instead of dialing a guess.
|
|
26
|
+
*
|
|
27
|
+
* Injection is opt-in exactly as for the caller-identity keys
|
|
28
|
+
* (caller-identity.ts): only a server whose spec.env declares the key
|
|
29
|
+
* receives it. Applied before filterEnvToDeclaredKeys at execution
|
|
30
|
+
* (mcp-resolver.ts) and before placeholder resolution at discovery
|
|
31
|
+
* (activities/discover-mcp-server.ts), so both phases share one rule.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
35
|
+
import type { Config } from "../config.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reserved env key: the Stigmer server's gRPC dial target, host:port. The
|
|
39
|
+
* name the mcp-server reads its target from (mcp-server/src/config.ts) and
|
|
40
|
+
* `@stigmer/react` keeps out of its setup prompts
|
|
41
|
+
* (sdk/react/src/environment/systemEnvVars.ts); a cross-package string,
|
|
42
|
+
* pinned on every side.
|
|
43
|
+
*/
|
|
44
|
+
export const SERVER_ADDRESS_ENV_KEY = "STIGMER_SERVER_ADDRESS";
|
|
45
|
+
|
|
46
|
+
/** The two runner-known endpoints an address can be filled from. */
|
|
47
|
+
export type PlatformEndpoints = Pick<Config, "mcpPublicEndpoint" | "stigmerBackendEndpoint">;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The gRPC dial target (host:port) for an endpoint, the shape
|
|
51
|
+
* STIGMER_SERVER_ADDRESS wants.
|
|
52
|
+
*
|
|
53
|
+
* An http(s) URL becomes its host with an explicit port: the URL's own, else
|
|
54
|
+
* 80 for http and 443 for https. The port is never left implicit, because
|
|
55
|
+
* the mcp-server derives TLS from it and assumes :443 with TLS for a
|
|
56
|
+
* non-loopback host that has none (mcp-server/src/config.ts). Any other
|
|
57
|
+
* input (a scheme-less host:port, a bare host) is already a dial target and
|
|
58
|
+
* passes through unchanged.
|
|
59
|
+
*/
|
|
60
|
+
export function grpcTarget(endpoint: string): string {
|
|
61
|
+
let url: URL;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(endpoint);
|
|
64
|
+
} catch {
|
|
65
|
+
return endpoint;
|
|
66
|
+
}
|
|
67
|
+
// `new URL("host:7234")` parses with `host:` as its scheme, so only a
|
|
68
|
+
// real http(s) URL is taken apart.
|
|
69
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
70
|
+
return endpoint;
|
|
71
|
+
}
|
|
72
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
73
|
+
return `${url.hostname}:${port}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The address the platform supplies for a server of this transport, or
|
|
78
|
+
* `null` when the runner knows none: the operator's public endpoint for any
|
|
79
|
+
* transport, else the runner's own backend endpoint for a stdio child.
|
|
80
|
+
*/
|
|
81
|
+
export function platformServerAddress(
|
|
82
|
+
transport: McpServerTransport | undefined,
|
|
83
|
+
endpoints: PlatformEndpoints,
|
|
84
|
+
): string | null {
|
|
85
|
+
if (endpoints.mcpPublicEndpoint) {
|
|
86
|
+
return grpcTarget(endpoints.mcpPublicEndpoint);
|
|
87
|
+
}
|
|
88
|
+
if (transport === "stdio") {
|
|
89
|
+
return grpcTarget(endpoints.stigmerBackendEndpoint);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return the env map with STIGMER_SERVER_ADDRESS filled for `server` when it
|
|
96
|
+
* declares the key and the map holds no non-empty value for it; otherwise the
|
|
97
|
+
* map itself, unchanged. Never mutates its input.
|
|
98
|
+
*/
|
|
99
|
+
export function fillPlatformServerAddress(
|
|
100
|
+
server: McpServer,
|
|
101
|
+
envVars: Record<string, string>,
|
|
102
|
+
endpoints: PlatformEndpoints,
|
|
103
|
+
): Record<string, string> {
|
|
104
|
+
const declared = server.spec?.env ?? {};
|
|
105
|
+
if (!(SERVER_ADDRESS_ENV_KEY in declared) || envVars[SERVER_ADDRESS_ENV_KEY]) {
|
|
106
|
+
return envVars;
|
|
107
|
+
}
|
|
108
|
+
const value = platformServerAddress(server.spec?.serverType.case, endpoints);
|
|
109
|
+
if (value === null) {
|
|
110
|
+
return envVars;
|
|
111
|
+
}
|
|
112
|
+
const source = endpoints.mcpPublicEndpoint
|
|
113
|
+
? "STIGMER_MCP_PUBLIC_ENDPOINT"
|
|
114
|
+
: "STIGMER_BACKEND_ENDPOINT";
|
|
115
|
+
console.info(
|
|
116
|
+
`MCP server '${server.metadata?.slug ?? ""}': ${SERVER_ADDRESS_ENV_KEY} ` +
|
|
117
|
+
`filled from ${source} (no value in the execution environment)`,
|
|
118
|
+
);
|
|
119
|
+
return { ...envVars, [SERVER_ADDRESS_ENV_KEY]: value };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The transports a McpServer spec declares (its `serverType` oneof). */
|
|
123
|
+
type McpServerTransport = NonNullable<NonNullable<McpServer["spec"]>["serverType"]["case"]>;
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* `attachment-download-urls.ts`). What the two prompt builders duplicated
|
|
10
10
|
* until #1096 was the glue around those bodies: the order
|
|
11
11
|
* the five standing sections read in, with the same doctrine comments written
|
|
12
|
-
* twice; the input-files bullet and disclosure lines
|
|
13
|
-
*
|
|
12
|
+
* twice; the input-files bullet and disclosure lines. This module is that
|
|
13
|
+
* glue, written once. It also chose which skills a prompt highlighted when
|
|
14
|
+
* there were many, until the native harness stopped choosing skills by the
|
|
15
|
+
* turn's message: a system prompt that follows the message misses the
|
|
16
|
+
* provider's prompt cache on every turn, so every harness now describes every
|
|
17
|
+
* mounted skill.
|
|
14
18
|
*
|
|
15
19
|
* What stays with each harness, on purpose:
|
|
16
20
|
* - Framing and placement. The native harness renders `## Heading` sections
|
|
@@ -43,9 +47,6 @@ import { formatDeclaredPreferencesText, type DeclaredPreferencesContent } from "
|
|
|
43
47
|
import { formatRecalledMemoriesText, type RecalledMemoriesContent } from "./recalled-memories.js";
|
|
44
48
|
import { formatSenderIdentityText, type SenderIdentity } from "./sender-identity.js";
|
|
45
49
|
import { formatSessionContextText } from "./session-context.js";
|
|
46
|
-
import { filterSkills } from "./skill-relevance.js";
|
|
47
|
-
import type { SkillMetadata } from "./skill-resolver.js";
|
|
48
|
-
import { STIGMER_LOCAL_STATE_DIR } from "./workspace/stigmer-link.js";
|
|
49
50
|
|
|
50
51
|
// ---------------------------------------------------------------------------
|
|
51
52
|
// The standing context: five sections, one order
|
|
@@ -204,53 +205,3 @@ export function inputFileLines(
|
|
|
204
205
|
vision: vision ? visionDisclosureLines(vision.inlineFilenames, vision.notViewable) : [],
|
|
205
206
|
};
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// The skills: which to highlight
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
/** Which mounted skills a prompt highlights, and which it only names. */
|
|
213
|
-
export interface SkillSelection {
|
|
214
|
-
/** The skills the prompt describes in full, in the filter's order. */
|
|
215
|
-
readonly highlighted: readonly SkillMetadata[];
|
|
216
|
-
/** The names of the skills left out of the highlighted set; empty below the threshold. */
|
|
217
|
-
readonly alsoAvailable: readonly string[];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* The relevance selection over the turn's message (`skill-relevance.ts`):
|
|
222
|
-
* below `SKILL_COUNT_THRESHOLD` every skill is highlighted; at and above it,
|
|
223
|
-
* the skills the message's terms reach are highlighted and the rest are
|
|
224
|
-
* named, so the agent can still activate one on its own judgement (the Agent
|
|
225
|
-
* Skills spec's progressive disclosure: metadata up front, instructions on
|
|
226
|
-
* demand). The native harness calls this on every turn; the Cursor harness
|
|
227
|
-
* lists every skill today; adopting the selection there is an open item.
|
|
228
|
-
*/
|
|
229
|
-
export function selectSkillsForPrompt(userMessage: string, skills: readonly SkillMetadata[]): SkillSelection {
|
|
230
|
-
const filter = filterSkills(
|
|
231
|
-
userMessage,
|
|
232
|
-
skills.map((s) => s.name),
|
|
233
|
-
skills.map((s) => s.description),
|
|
234
|
-
);
|
|
235
|
-
return {
|
|
236
|
-
highlighted: filter.includedIndices.map((i) => skills[i]),
|
|
237
|
-
alsoAvailable: filter.excludedNames,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* The tool-neutral sentence that names the skills a prompt did not highlight
|
|
243
|
-
* (`SkillSelection.alsoAvailable`), so the agent can activate one by reading
|
|
244
|
-
* its SKILL.md at the mounted path. The sentence names no tool, so it serves
|
|
245
|
-
* any harness; the heading above it is the builder's framing. Empty for no
|
|
246
|
-
* names.
|
|
247
|
-
*/
|
|
248
|
-
export function alsoAvailableSkillsNote(excludedNames: readonly string[]): string {
|
|
249
|
-
if (excludedNames.length === 0) return "";
|
|
250
|
-
const names = excludedNames.map((n) => `\`${n}\``).join(", ");
|
|
251
|
-
return (
|
|
252
|
-
`These skills are installed but were not highlighted above: ${names}. ` +
|
|
253
|
-
"If you determine one of them is relevant to your task, " +
|
|
254
|
-
`read its SKILL.md at \`${STIGMER_LOCAL_STATE_DIR}/skills/<name>/SKILL.md\` to activate it.`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
* into its prompt in its own placement. Moved from
|
|
15
15
|
* `activities/execute-cursor/` in #1070; the native harness's twin
|
|
16
16
|
* (`skill-writer.ts` `fetchSkillsByRefs` + `mountSkills`) retired in #1096,
|
|
17
|
-
* and the module itself with it
|
|
18
|
-
*
|
|
19
|
-
* skills path for every harness.
|
|
17
|
+
* and the module itself with it. This is the one skills path for every
|
|
18
|
+
* harness.
|
|
20
19
|
*
|
|
21
20
|
* The mount is cached by the skill's content-addressed version hash
|
|
22
21
|
* (stigmer/stigmer#672): metadata is fetched on every execution (that keeps
|
|
@@ -98,15 +98,3 @@ export function injectSynthesizedAttachment(
|
|
|
98
98
|
attachment,
|
|
99
99
|
];
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The gRPC dial target (host:port) for a backend endpoint URL — the
|
|
104
|
-
* shape STIGMER_SERVER_ADDRESS wants (the bridge warns on schemes).
|
|
105
|
-
*/
|
|
106
|
-
export function grpcTarget(endpoint: string): string {
|
|
107
|
-
try {
|
|
108
|
-
return new URL(endpoint).host;
|
|
109
|
-
} catch {
|
|
110
|
-
return endpoint;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ExecutionConfig.max_tool_rounds
|
|
2
|
+
* ExecutionConfig.max_tool_rounds: the round budget, its backstop, and the
|
|
3
|
+
* signal that the budget ended a turn.
|
|
3
4
|
*
|
|
4
5
|
* Implements the proto contract (agentexecution/v1/spec.proto):
|
|
5
|
-
* - 0 / unset = unlimited
|
|
6
|
-
*
|
|
6
|
+
* - 0 / unset = unlimited: no round limit and no recursionLimit, preserving
|
|
7
|
+
* the run-until-done + loop-detection posture.
|
|
7
8
|
* - When set, valid range is 10–1000 rounds; out-of-range values are
|
|
8
9
|
* clamped to the nearest bound with a warning log.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
* node; ×6 is the contract's floor estimate.
|
|
10
|
+
* - A round is one model response that proposes one or more tool calls,
|
|
11
|
+
* followed by their execution; parallel calls are one round.
|
|
12
12
|
*
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* The budget is COUNTED, not estimated. `middleware/execution-budget.ts`
|
|
14
|
+
* counts rounds where the model is called and throws
|
|
15
|
+
* {@link ToolRoundLimitError} before round N+1's model call, so N rounds
|
|
16
|
+
* run on every middleware stack. The count and the "~80% of budget"
|
|
17
|
+
* advisory live in that one middleware, so they can never disagree. Until
|
|
18
|
+
* #1113 the budget was LangGraph's recursion limit at rounds × 6 super-steps,
|
|
19
|
+
* and because every middleware hook is its own graph node, how many rounds
|
|
20
|
+
* fitted moved whenever the stack did.
|
|
21
|
+
*
|
|
22
|
+
* LangGraph's recursion limit stays only as a backstop
|
|
23
|
+
* ({@link backstopRecursionLimit}): generous enough never to bind before the
|
|
24
|
+
* round count on any stack, it halts a graph that loops without calling the
|
|
25
|
+
* model. Either stop is the same outcome ({@link isToolCallBudgetStop}).
|
|
16
26
|
*
|
|
17
27
|
* The knob and its terminal copy live together (the `cost-guard.ts` shape):
|
|
18
28
|
* when the budget is exhausted the adapter ends its turn `tool_call_limit`
|
|
@@ -21,26 +31,39 @@
|
|
|
21
31
|
* and the conversation continues on the next message.
|
|
22
32
|
*/
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
import { unwrapModelError } from "./model-error.js";
|
|
35
|
+
|
|
25
36
|
export const MIN_TOOL_ROUNDS = 10;
|
|
26
37
|
export const MAX_TOOL_ROUNDS = 1000;
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
40
|
+
* Super-steps per round the backstop allows: well above the graph nodes one
|
|
41
|
+
* round costs on any stack the runner builds (about five today), so the
|
|
42
|
+
* round count, never the backstop, is what a bounded run meets.
|
|
43
|
+
*/
|
|
44
|
+
export const SUPER_STEPS_BACKSTOP_PER_ROUND = 20;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The advisory-only round budget used when max_tool_rounds is unset: high
|
|
48
|
+
* enough to never fire on a healthy run. Deliberately NOT enforced — unset
|
|
49
|
+
* means unlimited per the proto contract.
|
|
32
50
|
*/
|
|
33
|
-
export const
|
|
51
|
+
export const UNBOUNDED_ADVISORY_TOOL_ROUNDS = MAX_TOOL_ROUNDS;
|
|
34
52
|
|
|
35
53
|
/**
|
|
36
|
-
* The
|
|
37
|
-
* unbounded (max_tool_rounds 0/unset — the
|
|
54
|
+
* The round budget for a turn, clamped to the valid range, or null when the
|
|
55
|
+
* execution is unbounded (max_tool_rounds 0/unset — the default posture).
|
|
38
56
|
*/
|
|
39
|
-
export function
|
|
57
|
+
export function resolveToolRoundLimit(maxToolRounds: number | undefined): number | null {
|
|
40
58
|
if (!maxToolRounds || maxToolRounds <= 0) {
|
|
41
59
|
return null;
|
|
42
60
|
}
|
|
43
|
-
return clampToolRounds(maxToolRounds)
|
|
61
|
+
return clampToolRounds(maxToolRounds);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The recursionLimit backstop for a bounded turn's invoke config; see {@link SUPER_STEPS_BACKSTOP_PER_ROUND}. */
|
|
65
|
+
export function backstopRecursionLimit(toolRoundLimit: number): number {
|
|
66
|
+
return toolRoundLimit * SUPER_STEPS_BACKSTOP_PER_ROUND;
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
function clampToolRounds(requested: number): number {
|
|
@@ -88,13 +111,36 @@ export const TOOL_CALL_LIMIT_USER_COPY =
|
|
|
88
111
|
"Send another message to continue where the agent left off.";
|
|
89
112
|
|
|
90
113
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
* The execution budget's stop: thrown by `middleware/execution-budget.ts` in
|
|
115
|
+
* place of round N+1's model call, so the graph ends on round N's tool
|
|
116
|
+
* results with no proposed call left unexecuted.
|
|
117
|
+
*/
|
|
118
|
+
export class ToolRoundLimitError extends Error {
|
|
119
|
+
constructor(readonly toolRoundLimit: number) {
|
|
120
|
+
super(`Tool-round limit reached: ${toolRoundLimit} rounds`);
|
|
121
|
+
this.name = "ToolRoundLimitError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether an error thrown out of a LangGraph run is the tool-call budget
|
|
127
|
+
* ending the turn — the budget's own signal, not a failure: the adapter ends
|
|
128
|
+
* the turn `tool_call_limit`. Two producers, one outcome: the round count's
|
|
129
|
+
* {@link ToolRoundLimitError}, which reaches the turn wrapped in LangChain's
|
|
130
|
+
* `MiddlewareError` once per `wrapModelCall` layer (so the cause chain is
|
|
131
|
+
* walked, as `unwrapModelError` does for a model error), and the backstop's
|
|
132
|
+
* `GraphRecursionError`.
|
|
133
|
+
*/
|
|
134
|
+
export function isToolCallBudgetStop(err: unknown): boolean {
|
|
135
|
+
return unwrapModelError(err) instanceof ToolRoundLimitError || isRecursionLimitError(err);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* LangGraph exhausting the backstop recursion limit. Matched by class name and
|
|
140
|
+
* message because the error class is not exported on a stable path across
|
|
141
|
+
* `@langchain/langgraph` versions.
|
|
96
142
|
*/
|
|
97
|
-
|
|
143
|
+
function isRecursionLimitError(err: unknown): boolean {
|
|
98
144
|
if (err instanceof Error) {
|
|
99
145
|
return err.constructor.name === "GraphRecursionError" ||
|
|
100
146
|
err.message.includes("GraphRecursionError") ||
|
|
@@ -73,14 +73,17 @@ describe("provisionGit", () => {
|
|
|
73
73
|
expect(cmds.some((c) => c.includes("git checkout 'develop'"))).toBe(true);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it("
|
|
76
|
+
it("describes the clone by repo URL and branch, never by its commit", async () => {
|
|
77
77
|
const backend = mockWorkspaceBackend({
|
|
78
78
|
execute: routingExecute({ branch: "main\n", sha: "sha789\n" }),
|
|
79
79
|
});
|
|
80
80
|
const result = await provisionGit(makeOptions({ backend }));
|
|
81
81
|
expect(result.workspaceDescription).toContain("github.com/org/repo.git");
|
|
82
|
-
expect(result.workspaceDescription).toContain("main");
|
|
83
|
-
|
|
82
|
+
expect(result.workspaceDescription).toContain("(branch: main)");
|
|
83
|
+
// The native system prompt carries this text on every turn; write-back
|
|
84
|
+
// commits move HEAD every approved turn, so a commit here would change it.
|
|
85
|
+
expect(result.workspaceDescription).not.toContain("sha789");
|
|
86
|
+
expect(result.gitMetadata?.baseCommit, "the commit stays in the metadata write-back reads").toBe("sha789");
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
// ── Default-branch resolution ─────────────────────────────────────
|
|
@@ -120,7 +123,10 @@ describe("provisionGit", () => {
|
|
|
120
123
|
|
|
121
124
|
const result = await provisionGit(makeOptions({ backend }));
|
|
122
125
|
expect(result.sourceType).toBe("git_repo");
|
|
123
|
-
|
|
126
|
+
const fresh = await provisionGit(
|
|
127
|
+
makeOptions({ backend: mockWorkspaceBackend({ execute: routingExecute() }) }),
|
|
128
|
+
);
|
|
129
|
+
expect(result.workspaceDescription, "a reused clone is described exactly as a fresh one").toBe(fresh.workspaceDescription);
|
|
124
130
|
|
|
125
131
|
expect(calls(backend).some((c) => c.includes("git init"))).toBe(false);
|
|
126
132
|
expect(calls(backend).some((c) => c.includes("git fetch"))).toBe(false);
|
|
@@ -395,7 +401,7 @@ describe("provisionGit", () => {
|
|
|
395
401
|
}));
|
|
396
402
|
|
|
397
403
|
expect(result.gitMetadata!.gitCredentialsConfigured).toBe(true);
|
|
398
|
-
expect(
|
|
404
|
+
expect(calls(backend).some((c) => c.includes("git init")), "the reuse path, not a fresh clone").toBe(false);
|
|
399
405
|
});
|
|
400
406
|
|
|
401
407
|
it("handles credential setup failure gracefully", async () => {
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, mkdir, rm, writeFile, readFile, lstat, readlink } from "node:fs/promises";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile, readFile, readdir, rename, lstat, readlink } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
// `rename` passes through to the real one; the cross-filesystem case rejects
|
|
7
|
+
// it once with EXDEV, as a move from a cloud workspace volume to `$HOME` does.
|
|
8
|
+
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
|
10
|
+
return { ...actual, rename: vi.fn(actual.rename) };
|
|
11
|
+
});
|
|
12
|
+
|
|
6
13
|
import {
|
|
7
14
|
ensureStigmerSymlink,
|
|
8
15
|
removeStigmerSymlink,
|
|
@@ -16,7 +23,9 @@ import { LocalWorkspaceBackend } from "../local-backend.js";
|
|
|
16
23
|
* (approved plan, skills, attachment inputs) readable at `.stigmer/…` by
|
|
17
24
|
* whatever reads from the workspace: the Cursor SDK and shell commands on
|
|
18
25
|
* both harnesses. The lifecycle tests pin the ownership contract (ensure
|
|
19
|
-
*
|
|
26
|
+
* moves a real entry out of the workspace and never deletes one, refuses a
|
|
27
|
+
* `.stigmer` that holds the platform dir, and remove is symlink-only); the
|
|
28
|
+
* convergence tests pin the physics:
|
|
20
29
|
* content written through the platform-routing LocalWorkspaceBackend is
|
|
21
30
|
* readable at the same `.stigmer/…` path from inside the workspace once the
|
|
22
31
|
* link exists, and not before (the "plan file doesn't exist" bug).
|
|
@@ -69,20 +78,108 @@ describe("stigmer-link", () => {
|
|
|
69
78
|
}
|
|
70
79
|
});
|
|
71
80
|
|
|
72
|
-
it("
|
|
73
|
-
|
|
74
|
-
// `.stigmer` (left behind by an older runner) must never shadow the
|
|
75
|
-
// platform mount. See the module header for why this is safe.
|
|
76
|
-
await mkdir(join(linkPath(), "old"), { recursive: true });
|
|
77
|
-
await writeFile(join(linkPath(), "old", "stale.txt"), "stale");
|
|
81
|
+
it("removes an empty real .stigmer directory, then links", async () => {
|
|
82
|
+
await mkdir(linkPath());
|
|
78
83
|
|
|
79
84
|
await ensureStigmerSymlink(workspaceDir, platformDir);
|
|
80
85
|
|
|
81
|
-
expect((await lstat(linkPath())).isSymbolicLink()).toBe(true);
|
|
82
86
|
expect(await readlink(linkPath())).toBe(platformDir);
|
|
83
87
|
});
|
|
84
88
|
});
|
|
85
89
|
|
|
90
|
+
// ── A real `.stigmer` in the link's place (#1123, #1424) ─────────
|
|
91
|
+
|
|
92
|
+
describe("ensureStigmerSymlink over a real .stigmer", () => {
|
|
93
|
+
// The platform dir sits in a session tree here, as `getPlatformDir` lays
|
|
94
|
+
// it out, so the displaced dir lands beside it and not in the shared tmp.
|
|
95
|
+
let sessionDir: string;
|
|
96
|
+
let sessionPlatformDir: string;
|
|
97
|
+
|
|
98
|
+
beforeEach(async () => {
|
|
99
|
+
sessionDir = await mkdtemp(join(tmpdir(), "stigmer-link-session-"));
|
|
100
|
+
sessionPlatformDir = join(sessionDir, "platform");
|
|
101
|
+
await mkdir(sessionPlatformDir);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
await rm(sessionDir, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** The one entry under the session's `displaced/` dir. */
|
|
109
|
+
async function displacedEntry(): Promise<string> {
|
|
110
|
+
const entries = await readdir(join(sessionDir, "displaced"));
|
|
111
|
+
expect(entries).toHaveLength(1);
|
|
112
|
+
expect(entries[0]).toMatch(/^\d{8}T\d{9}Z$/);
|
|
113
|
+
return join(sessionDir, "displaced", entries[0]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it("moves a non-empty directory to the session's displaced dir, then links", async () => {
|
|
117
|
+
// An agent write on a turn with no link, or an older runner's leftover:
|
|
118
|
+
// kept, never deleted.
|
|
119
|
+
await mkdir(join(linkPath(), "notes"), { recursive: true });
|
|
120
|
+
await writeFile(join(linkPath(), "notes", "draft.md"), "the agent's draft");
|
|
121
|
+
|
|
122
|
+
await ensureStigmerSymlink(workspaceDir, sessionPlatformDir);
|
|
123
|
+
|
|
124
|
+
expect(await readlink(linkPath())).toBe(sessionPlatformDir);
|
|
125
|
+
expect(await readFile(join(await displacedEntry(), "notes", "draft.md"), "utf-8")).toBe("the agent's draft");
|
|
126
|
+
expect(await readdir(workspaceDir), "nothing else is left in the workspace").toEqual([STIGMER_LOCAL_STATE_DIR]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("moves a real .stigmer file the same way", async () => {
|
|
130
|
+
await writeFile(linkPath(), "not a directory");
|
|
131
|
+
|
|
132
|
+
await ensureStigmerSymlink(workspaceDir, sessionPlatformDir);
|
|
133
|
+
|
|
134
|
+
expect(await readlink(linkPath())).toBe(sessionPlatformDir);
|
|
135
|
+
expect(await readFile(await displacedEntry(), "utf-8")).toBe("not a directory");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("copies then removes when the session dir is on another filesystem (EXDEV)", async () => {
|
|
139
|
+
await mkdir(join(linkPath(), "notes"), { recursive: true });
|
|
140
|
+
await writeFile(join(linkPath(), "notes", "draft.md"), "across mounts");
|
|
141
|
+
vi.mocked(rename).mockRejectedValueOnce(
|
|
142
|
+
Object.assign(new Error("EXDEV: cross-device link not permitted"), { code: "EXDEV" }),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await ensureStigmerSymlink(workspaceDir, sessionPlatformDir);
|
|
146
|
+
|
|
147
|
+
expect(vi.mocked(rename)).toHaveBeenCalled();
|
|
148
|
+
expect(await readlink(linkPath())).toBe(sessionPlatformDir);
|
|
149
|
+
expect(await readFile(join(await displacedEntry(), "notes", "draft.md"), "utf-8")).toBe("across mounts");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("refuses a .stigmer that holds the platform dir, touching nothing (a workspace at the home directory)", async () => {
|
|
153
|
+
// The workspace IS the home: its `.stigmer` is the Stigmer home, where
|
|
154
|
+
// the CLI's config and every session live, this one's platform dir too.
|
|
155
|
+
const home = await mkdtemp(join(tmpdir(), "stigmer-link-home-"));
|
|
156
|
+
try {
|
|
157
|
+
const stigmerHome = join(home, STIGMER_LOCAL_STATE_DIR);
|
|
158
|
+
const homePlatformDir = join(stigmerHome, "sessions", "s1", "platform");
|
|
159
|
+
await mkdir(homePlatformDir, { recursive: true });
|
|
160
|
+
await writeFile(join(stigmerHome, "config.yaml"), "context: local\n");
|
|
161
|
+
|
|
162
|
+
await expect(ensureStigmerSymlink(home, homePlatformDir)).rejects.toThrow(/is the Stigmer home/);
|
|
163
|
+
|
|
164
|
+
expect((await lstat(stigmerHome)).isDirectory(), "still the real directory").toBe(true);
|
|
165
|
+
expect(await readFile(join(stigmerHome, "config.yaml"), "utf-8")).toBe("context: local\n");
|
|
166
|
+
expect((await lstat(homePlatformDir)).isDirectory()).toBe(true);
|
|
167
|
+
await expect(lstat(join(stigmerHome, "sessions", "s1", "displaced"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
168
|
+
} finally {
|
|
169
|
+
await rm(home, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("refuses when the platform dir is the .stigmer itself", async () => {
|
|
174
|
+
await mkdir(linkPath());
|
|
175
|
+
await writeFile(join(linkPath(), "keep.txt"), "keep");
|
|
176
|
+
|
|
177
|
+
await expect(ensureStigmerSymlink(workspaceDir, linkPath())).rejects.toThrow(/is the Stigmer home/);
|
|
178
|
+
|
|
179
|
+
expect(await readFile(join(linkPath(), "keep.txt"), "utf-8")).toBe("keep");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
86
183
|
describe("removeStigmerSymlink", () => {
|
|
87
184
|
it("removes the symlink and leaves the platform dir intact", async () => {
|
|
88
185
|
await writeFile(join(platformDir, "keep.txt"), "keep");
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* a real repo never leaves Stigmer files behind in it (see issue #173); only
|
|
11
11
|
* a minimal, transient `.cursor/hooks.json` referencing the absolute script
|
|
12
12
|
* path is written into the workspace itself.
|
|
13
|
+
* - `displaced/` — a real `.stigmer` entry found in the workspace when the
|
|
14
|
+
* platform link takes the name, kept rather than deleted (stigmer-link.ts).
|
|
13
15
|
*
|
|
14
16
|
* This module replaces the duplicated `getPlatformDir` helpers in
|
|
15
17
|
* execute-cursor/skill-resolver.ts and shared/attachment-resolver.ts.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import { join } from "node:path";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
19
21
|
import { mkdir } from "node:fs/promises";
|
|
20
22
|
import { homedir } from "node:os";
|
|
21
23
|
import { createHash } from "node:crypto";
|
|
@@ -110,6 +112,22 @@ export async function ensurePlatformDir(sessionId: string): Promise<string> {
|
|
|
110
112
|
return dir;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Compute where a real `.stigmer` entry found in a workspace is moved when the
|
|
117
|
+
* platform link takes the name — `displaced/<stamp>` beside the session's
|
|
118
|
+
* `platform/` and `hitl/` trees (see stigmer-link.ts). Outside the workspace,
|
|
119
|
+
* so a real repo is never left holding it (issue #173) and a write-back never
|
|
120
|
+
* commits it; outside `platform/`, so the agent never reads it back as
|
|
121
|
+
* platform content.
|
|
122
|
+
*
|
|
123
|
+
* Takes the platform dir rather than a session id, unlike its siblings: the
|
|
124
|
+
* link step knows only the dir it links to, and the session dir is its parent.
|
|
125
|
+
* Pure function — performs no I/O.
|
|
126
|
+
*/
|
|
127
|
+
export function getDisplacedDir(platformDir: string, stamp: string): string {
|
|
128
|
+
return join(dirname(platformDir), "displaced", stamp);
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
/**
|
|
114
132
|
* Compute the HITL directory path for a session — the runner-owned home for the
|
|
115
133
|
* approval gate's hook script, approval-state file, and denial ledger.
|
|
@@ -83,15 +83,24 @@ export async function provisionGit(options: GitProvisionOptions): Promise<Provis
|
|
|
83
83
|
rootDir: cloneDir,
|
|
84
84
|
sourceType: "git_repo",
|
|
85
85
|
consumedKeys,
|
|
86
|
-
workspaceDescription:
|
|
87
|
-
`Your workspace is a git clone of ${url}` +
|
|
88
|
-
(metadata.branch ? ` (branch: ${metadata.branch})` : "") +
|
|
89
|
-
`.\nBase commit: ${metadata.baseCommit}`,
|
|
86
|
+
workspaceDescription: describeClone(url, metadata.branch),
|
|
90
87
|
gitMetadata: metadata,
|
|
91
88
|
entryName: "",
|
|
92
89
|
};
|
|
93
90
|
}
|
|
94
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The model's description of a cloned workspace, the same on the turn that
|
|
94
|
+
* clones and on every turn that reuses the clone: the native system prompt
|
|
95
|
+
* carries it and must not change from one message to the next. So it names
|
|
96
|
+
* the repository and the branch, never the commit (write-back commits move
|
|
97
|
+
* `HEAD` on every approved turn) and never whether the clone was reused. The
|
|
98
|
+
* branch changes only when a branch really changes, and the text says so.
|
|
99
|
+
*/
|
|
100
|
+
function describeClone(url: string, branch: string): string {
|
|
101
|
+
return `Your workspace is a git clone of ${url}` + (branch ? ` (branch: ${branch})` : "") + ".";
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
async function reuseExistingRepo(
|
|
96
105
|
cloneDir: string,
|
|
97
106
|
url: string,
|
|
@@ -114,10 +123,7 @@ async function reuseExistingRepo(
|
|
|
114
123
|
rootDir: cloneDir,
|
|
115
124
|
sourceType: "git_repo",
|
|
116
125
|
consumedKeys: [],
|
|
117
|
-
workspaceDescription:
|
|
118
|
-
`Your workspace is a git clone of ${url}` +
|
|
119
|
-
(metadata.branch ? ` (branch: ${metadata.branch})` : "") +
|
|
120
|
-
` (existing repo detected).\nBase commit: ${metadata.baseCommit}`,
|
|
126
|
+
workspaceDescription: describeClone(url, metadata.branch),
|
|
121
127
|
gitMetadata: metadata,
|
|
122
128
|
entryName: "",
|
|
123
129
|
};
|