@stigmer/runner 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +49 -8
- package/dist/activities/execute-cursor/attachment-resolver.js +83 -26
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/cas-observations.d.ts +13 -0
- package/dist/activities/execute-cursor/cas-observations.js +39 -5
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +19 -1
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -0
- package/dist/activities/execute-cursor/index.js +102 -22
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.js +10 -1
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +32 -7
- package/dist/activities/execute-cursor/prompt-builder.js +38 -20
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +1 -10
- package/dist/activities/execute-cursor/skill-resolver.js +3 -55
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/stigmer-link.d.ts +35 -0
- package/dist/activities/execute-cursor/stigmer-link.js +73 -0
- package/dist/activities/execute-cursor/stigmer-link.js.map +1 -0
- package/dist/activities/execute-cursor/workspace-provision.d.ts +3 -2
- package/dist/activities/execute-cursor/workspace-provision.js +5 -3
- package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +59 -2
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +17 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +22 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.js +36 -6
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +2 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +32 -0
- package/dist/shared/artifact-storage.js +74 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/implement-plan-prompt.d.ts +42 -0
- package/dist/shared/implement-plan-prompt.js +66 -0
- package/dist/shared/implement-plan-prompt.js.map +1 -0
- package/dist/shared/plan-mode-prompt.d.ts +30 -0
- package/dist/shared/plan-mode-prompt.js +51 -0
- package/dist/shared/plan-mode-prompt.js.map +1 -0
- package/dist/shared/status.d.ts +11 -0
- package/dist/shared/status.js +12 -2
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-row.d.ts +30 -1
- package/dist/shared/tool-row.js +53 -4
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/session-root.d.ts +35 -0
- package/dist/shared/workspace/session-root.js +53 -0
- package/dist/shared/workspace/session-root.js.map +1 -0
- package/dist/shared/workspace/workspace-lock.d.ts +86 -0
- package/dist/shared/workspace/workspace-lock.js +187 -0
- package/dist/shared/workspace/workspace-lock.js.map +1 -0
- package/package.json +7 -5
- package/src/__tests__/config.test.ts +8 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +161 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +125 -2
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +55 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +28 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +63 -4
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +36 -2
- package/src/activities/execute-cursor/attachment-resolver.ts +116 -34
- package/src/activities/execute-cursor/cas-observations.ts +41 -5
- package/src/activities/execute-cursor/hook-script.ts +19 -1
- package/src/activities/execute-cursor/index.ts +114 -26
- package/src/activities/execute-cursor/message-translator.ts +9 -1
- package/src/activities/execute-cursor/prompt-builder.ts +60 -22
- package/src/activities/execute-cursor/skill-resolver.ts +3 -59
- package/src/activities/execute-cursor/stigmer-link.ts +78 -0
- package/src/activities/execute-cursor/workspace-provision.ts +5 -3
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +4 -1
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +88 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +7 -2
- package/src/activities/execute-deep-agent/index.ts +70 -2
- package/src/activities/execute-deep-agent/prompt-builder.ts +39 -0
- package/src/activities/execute-deep-agent/setup.ts +25 -17
- package/src/config.ts +17 -0
- package/src/middleware/__tests__/approval-gate.test.ts +77 -12
- package/src/middleware/approval-gate.ts +39 -7
- package/src/runner-manager.ts +6 -1
- package/src/runner.ts +6 -1
- package/src/shared/__tests__/artifact-storage.test.ts +111 -1
- package/src/shared/__tests__/implement-plan-prompt.test.ts +48 -0
- package/src/shared/__tests__/secret-leak-scan.test.ts +105 -0
- package/src/shared/__tests__/status.test.ts +7 -2
- package/src/shared/__tests__/tool-row.test.ts +136 -1
- package/src/shared/artifact-storage.ts +83 -1
- package/src/shared/implement-plan-prompt.ts +73 -0
- package/src/shared/plan-mode-prompt.ts +51 -0
- package/src/shared/status.ts +11 -1
- package/src/shared/tool-row.ts +58 -5
- package/src/shared/workspace/__tests__/session-root.test.ts +42 -0
- package/src/shared/workspace/__tests__/workspace-lock.test.ts +200 -0
- package/src/shared/workspace/session-root.ts +60 -0
- package/src/shared/workspace/workspace-lock.ts +241 -0
|
@@ -45,9 +45,9 @@ import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_p
|
|
|
45
45
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
46
46
|
import { MessageAccumulator, reconcileDeniedToolCalls, clearProvisionalPostDenialNarration, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
|
|
47
47
|
import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
|
|
48
|
-
import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
|
|
48
|
+
import { collectSubAgentToolCallIds, withholdSecretContentFromMessages } from "../../shared/tool-row.js";
|
|
49
49
|
import { startStallWatchdog, StallTimeoutError, formatStallFailure, type StallWatchdog } from "../../shared/stall-watchdog.js";
|
|
50
|
-
import {
|
|
50
|
+
import { resolveUsableArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
51
51
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
52
52
|
import { DeltaEnricher } from "./delta-enricher.js";
|
|
53
53
|
import { TodoTracker } from "./todo-tracker.js";
|
|
@@ -61,11 +61,18 @@ import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
|
|
|
61
61
|
import { resolveExecutionEnv } from "./env-resolver.js";
|
|
62
62
|
import { resolveBlueprint } from "./blueprint-resolver.js";
|
|
63
63
|
import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
|
|
64
|
-
import { resolveSkills
|
|
64
|
+
import { resolveSkills } from "./skill-resolver.js";
|
|
65
|
+
import { removeStigmerSymlink } from "./stigmer-link.js";
|
|
65
66
|
import { resolveAttachments } from "./attachment-resolver.js";
|
|
66
|
-
import { buildEnhancedPrompt, buildReinvocationPrompt } from "./prompt-builder.js";
|
|
67
|
+
import { buildEnhancedPrompt, buildReinvocationPrompt, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
67
68
|
import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
|
|
68
69
|
import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
|
|
70
|
+
import {
|
|
71
|
+
acquireWorkspaceLock,
|
|
72
|
+
WorkspaceLockCancelledError,
|
|
73
|
+
WorkspaceLockTimeoutError,
|
|
74
|
+
type ReleaseWorkspaceLock,
|
|
75
|
+
} from "../../shared/workspace/workspace-lock.js";
|
|
69
76
|
import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
70
77
|
import { buildApprovalState, buildApprovalGrants, emitCursorGrantReceipts, primaryToken, readDenialLedger, reconstructAdjudicatedApprovals, watchDenialLedger } from "./approval-state.js";
|
|
71
78
|
import { deriveTurnCommandProvenance } from "./command-provenance.js";
|
|
@@ -167,27 +174,31 @@ async function executeCursorInner(
|
|
|
167
174
|
|
|
168
175
|
// Artifact storage for offloading oversized tool outputs (screenshots, giant
|
|
169
176
|
// dumps) out of the persisted status, and for publishing the plan artifact.
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} catch (storageErr) {
|
|
178
|
-
console.warn(
|
|
179
|
-
`ExecuteCursor artifact storage unavailable — tool-output offload disabled ` +
|
|
180
|
-
`(aggregate size guard still active): execution=${executionId}, error=${storageErr}`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
177
|
+
// Resolved once here so it is available to EVERY persist below. Best-effort via
|
|
178
|
+
// the shared resolver (identical to the deep-agent harness): `undefined` — never
|
|
179
|
+
// a throw — when there is no working substrate (proxy misconfig OR an unwritable
|
|
180
|
+
// local path). An absent store disables offload (persistStatus still enforces
|
|
181
|
+
// the aggregate size cap) and flips capture mode off (deny-gate fallback).
|
|
182
|
+
const artifactStorage: ArtifactStorage | undefined =
|
|
183
|
+
await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
|
|
183
184
|
const statusOffload = artifactStorage
|
|
184
185
|
? { artifactStorage, executionId }
|
|
185
186
|
: undefined;
|
|
186
187
|
// ALL status persistence in this activity flows through `persist`, so the
|
|
187
188
|
// single size-bounding guard (offload + aggregate elision) is unforgeable and
|
|
188
189
|
// a future call site cannot accidentally skip it.
|
|
189
|
-
const persist = (s: AgentExecutionStatus = status) =>
|
|
190
|
-
|
|
190
|
+
const persist = (s: AgentExecutionStatus = status) => {
|
|
191
|
+
// Never-persist-secret backstop (DD-26 #2): before EVERY persist, withhold
|
|
192
|
+
// content from any built-in write row targeting a secret-like path (top-level
|
|
193
|
+
// + sub-agent). This is the single airtight choke point for the Cursor harness
|
|
194
|
+
// — the deny-gate analog of capture mode's stamping scrub, and the only
|
|
195
|
+
// guarantee under auto_approve_all (where the hook installs no gate). Safe on
|
|
196
|
+
// every call: Cursor sets tool args atomically from the SDK tool_call event
|
|
197
|
+
// (buildToolCallProto), so there is no mid-stream partial-args hazard, and the
|
|
198
|
+
// pass only ever touches secret-like write rows (idempotent, else a no-op).
|
|
199
|
+
withholdSecretContentFromMessages(s.messages, s.subAgentExecutions);
|
|
200
|
+
return persistStatus(client, executionId, s, { offload: statusOffload });
|
|
201
|
+
};
|
|
191
202
|
|
|
192
203
|
let sessionId: string | undefined;
|
|
193
204
|
let session: import("@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb").Session | undefined;
|
|
@@ -234,6 +245,14 @@ async function executeCursorInner(
|
|
|
234
245
|
// (issue #173). Runs in the finally, covering every success/error/approval
|
|
235
246
|
// exit path. Undefined until the gate is installed.
|
|
236
247
|
let hitlCleanup: (() => Promise<void>) | undefined;
|
|
248
|
+
// Exclusive turn lock on the primary workspace working tree. Held across the
|
|
249
|
+
// ENTIRE tree-mutating window (decision reconcile, HITL gate install, the
|
|
250
|
+
// agent's own writes, candidate capture) so a concurrent execution sharing
|
|
251
|
+
// this directory can never write between this turn's baseline and candidate
|
|
252
|
+
// snapshots — the misattribution that showed another session's file as this
|
|
253
|
+
// turn's change. Released in the finally AFTER hitlCleanup (which still
|
|
254
|
+
// mutates the tree). See shared/workspace/workspace-lock.ts.
|
|
255
|
+
let releaseWorkspaceLock: ReleaseWorkspaceLock | undefined;
|
|
237
256
|
// Carries model/mode/agentId out to the outer catch so a thrown CursorSdkError
|
|
238
257
|
// can be classified with the same context as the run.wait() error path.
|
|
239
258
|
let errorContext = { model: "default", mode: "local", agentId: "" };
|
|
@@ -294,6 +313,46 @@ async function executeCursorInner(
|
|
|
294
313
|
const changeSetId = `${executionId}:${turnSeq}`;
|
|
295
314
|
heartbeat();
|
|
296
315
|
|
|
316
|
+
// Serialize this turn against every other execution sharing this working
|
|
317
|
+
// tree — sessions declaring the same localPath (or the shared runner root)
|
|
318
|
+
// resolve to ONE directory, and an unserialized concurrent write lands
|
|
319
|
+
// inside this turn's baseline→candidate window, misattributing another
|
|
320
|
+
// session's file to this turn's review. Acquired before ANY tree mutation
|
|
321
|
+
// below (decision reconcile, gate install, agent writes, capture). While
|
|
322
|
+
// another turn holds the lock this surfaces a visible waiting state and
|
|
323
|
+
// heartbeats; a cancel aborts the wait immediately.
|
|
324
|
+
if (primaryWorkspaceDir) {
|
|
325
|
+
try {
|
|
326
|
+
releaseWorkspaceLock = await acquireWorkspaceLock(primaryWorkspaceDir, {
|
|
327
|
+
onWaiting: () => reportSetupProgress(
|
|
328
|
+
client, executionId, "Waiting for workspace — in use by another session",
|
|
329
|
+
),
|
|
330
|
+
heartbeat,
|
|
331
|
+
signal: Context.current().cancellationSignal,
|
|
332
|
+
timeoutMs: config.workspaceLockTimeoutMs,
|
|
333
|
+
});
|
|
334
|
+
} catch (lockErr) {
|
|
335
|
+
if (lockErr instanceof WorkspaceLockCancelledError) {
|
|
336
|
+
throw new CancelledFailure("Activity cancelled while waiting for the workspace lock");
|
|
337
|
+
}
|
|
338
|
+
if (lockErr instanceof WorkspaceLockTimeoutError) {
|
|
339
|
+
status.phase = ExecutionPhase.EXECUTION_FAILED;
|
|
340
|
+
status.error = lockErr.message;
|
|
341
|
+
status.completedAt = utcTimestamp();
|
|
342
|
+
status.messages.push(create(AgentMessageSchema, {
|
|
343
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
344
|
+
content: `Execution failed: ${lockErr.message}`,
|
|
345
|
+
timestamp: utcTimestamp(),
|
|
346
|
+
}));
|
|
347
|
+
await persist(status);
|
|
348
|
+
console.warn(`ExecuteCursor workspace lock timeout: execution=${executionId}`);
|
|
349
|
+
return slimStatus(status);
|
|
350
|
+
}
|
|
351
|
+
throw lockErr;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
heartbeat();
|
|
355
|
+
|
|
297
356
|
// Set OTel baggage so downstream calls carry execution context.
|
|
298
357
|
try {
|
|
299
358
|
const { setBaggage, BAGGAGE_EXECUTION_ID, BAGGAGE_SESSION_ID, BAGGAGE_ORG_ID } = await import("../../otel.js");
|
|
@@ -511,13 +570,15 @@ async function executeCursorInner(
|
|
|
511
570
|
});
|
|
512
571
|
heartbeat();
|
|
513
572
|
|
|
514
|
-
// Phase 5b: Resolve attachments
|
|
515
|
-
|
|
516
|
-
|
|
573
|
+
// Phase 5b: Resolve attachments (fail-hard — explicit user inputs; see
|
|
574
|
+
// attachment-resolver.ts). Downloads by storage key through the same
|
|
575
|
+
// artifactStorage resolved for status offload above.
|
|
576
|
+
const attachmentResults = await resolveAttachments(spec.attachments, {
|
|
517
577
|
sessionId,
|
|
518
578
|
primaryWorkspaceDir,
|
|
519
|
-
config.mode,
|
|
520
|
-
|
|
579
|
+
mode: config.mode,
|
|
580
|
+
storage: artifactStorage,
|
|
581
|
+
});
|
|
521
582
|
const attachmentPaths = attachmentResults.map((a) => a.relativePath);
|
|
522
583
|
|
|
523
584
|
// Phase 5b3: Exact-apply approved whole-file writes (HITL "what you approve
|
|
@@ -741,6 +802,7 @@ async function executeCursorInner(
|
|
|
741
802
|
// Phase 10: Build the prompt
|
|
742
803
|
const interactionMode = spec.executionConfig?.interactionMode
|
|
743
804
|
?? InteractionMode.UNSPECIFIED;
|
|
805
|
+
const buildFromPlan = spec.executionConfig?.buildFromPlan ?? false;
|
|
744
806
|
|
|
745
807
|
const prompt = buildPrompt({
|
|
746
808
|
resolution,
|
|
@@ -755,6 +817,7 @@ async function executeCursorInner(
|
|
|
755
817
|
pendingApprovals: adjudicatedApprovals,
|
|
756
818
|
appliedToolCallIds,
|
|
757
819
|
interactionMode,
|
|
820
|
+
buildFromPlan,
|
|
758
821
|
});
|
|
759
822
|
|
|
760
823
|
// Phase 10a: Inject structured output instruction for Cursor harness
|
|
@@ -1885,6 +1948,13 @@ async function executeCursorInner(
|
|
|
1885
1948
|
);
|
|
1886
1949
|
}
|
|
1887
1950
|
}
|
|
1951
|
+
|
|
1952
|
+
// Release the workspace turn lock LAST — hitlCleanup above still mutates
|
|
1953
|
+
// the tree (restores .cursor/hooks.json), and the next queued turn must
|
|
1954
|
+
// not baseline until every mutation of this one has landed. Idempotent
|
|
1955
|
+
// and non-throwing (see workspace-lock.ts), so it can never mask the
|
|
1956
|
+
// turn's real outcome.
|
|
1957
|
+
await releaseWorkspaceLock?.();
|
|
1888
1958
|
}
|
|
1889
1959
|
}
|
|
1890
1960
|
|
|
@@ -1996,6 +2066,11 @@ export interface BuildPromptInput {
|
|
|
1996
2066
|
*/
|
|
1997
2067
|
appliedToolCallIds?: ReadonlySet<string>;
|
|
1998
2068
|
interactionMode?: InteractionMode;
|
|
2069
|
+
/**
|
|
2070
|
+
* The execution is a Build-from-plan turn (spec.execution_config
|
|
2071
|
+
* .build_from_plan): both prompt paths carry the implement-plan directive.
|
|
2072
|
+
*/
|
|
2073
|
+
buildFromPlan?: boolean;
|
|
1999
2074
|
}
|
|
2000
2075
|
|
|
2001
2076
|
/**
|
|
@@ -2026,6 +2101,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2026
2101
|
workspaceFileRefs,
|
|
2027
2102
|
attachmentPaths,
|
|
2028
2103
|
interactionMode,
|
|
2104
|
+
buildFromPlan,
|
|
2029
2105
|
} = input;
|
|
2030
2106
|
|
|
2031
2107
|
const isHitlReinvocation = approvalDecisions !== undefined && approvalDecisions.size > 0;
|
|
@@ -2042,9 +2118,20 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2042
2118
|
}
|
|
2043
2119
|
|
|
2044
2120
|
// A successfully resumed agent carries its own conversation context via the
|
|
2045
|
-
// SDK's native store — send the raw user message with no preamble.
|
|
2121
|
+
// SDK's native store — send the raw user message with no preamble. The
|
|
2122
|
+
// exceptions are the per-EXECUTION directives, which never inherit from the
|
|
2123
|
+
// session's first turn: the interaction-mode prefix (a follow-up can switch
|
|
2124
|
+
// Agent→Plan mid-session, and for Cursor the prompt is the only plan-mode
|
|
2125
|
+
// enforcement) and the implement-plan directive (the build turn is usually
|
|
2126
|
+
// a follow-up on a resumed agent).
|
|
2046
2127
|
if (resolution.reason === "resumed_successfully") {
|
|
2047
|
-
|
|
2128
|
+
const prefixes = [
|
|
2129
|
+
formatInteractionModePrefix(interactionMode),
|
|
2130
|
+
formatImplementPlanSection(buildFromPlan, attachmentPaths),
|
|
2131
|
+
].filter((p): p is string => p !== undefined);
|
|
2132
|
+
return prefixes.length > 0
|
|
2133
|
+
? [...prefixes, userMessage].join("\n\n")
|
|
2134
|
+
: userMessage;
|
|
2048
2135
|
}
|
|
2049
2136
|
|
|
2050
2137
|
// First execution, or a fresh agent created after a resume failure: there is
|
|
@@ -2058,6 +2145,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2058
2145
|
workspaceFileRefs,
|
|
2059
2146
|
attachmentPaths,
|
|
2060
2147
|
interactionMode,
|
|
2148
|
+
buildFromPlan,
|
|
2061
2149
|
});
|
|
2062
2150
|
}
|
|
2063
2151
|
|
|
@@ -50,7 +50,8 @@ import { utcTimestamp } from "../../shared/status.js";
|
|
|
50
50
|
import { hideToolCallRow, isToolCallRowHidden } from "../../shared/tool-row.js";
|
|
51
51
|
import { classifyTool, toolApprovalCategory, type ToolApprovalCategory } from "../../shared/tool-kind.js";
|
|
52
52
|
import { resolveWorkspacePath } from "../../shared/file-change.js";
|
|
53
|
-
import { contentDigest } from "../../shared/file-tools.js";
|
|
53
|
+
import { contentDigest, extractFilePath } from "../../shared/file-tools.js";
|
|
54
|
+
import { isSecretLikePath } from "../../shared/filereview/secret-paths.js";
|
|
54
55
|
import { buildElidedArgsPreview } from "../../shared/args-preview.js";
|
|
55
56
|
import type { WorkspaceBackend } from "../../shared/workspace/types.js";
|
|
56
57
|
|
|
@@ -1631,6 +1632,13 @@ function applyGateInput(
|
|
|
1631
1632
|
input: Record<string, unknown> | undefined,
|
|
1632
1633
|
): void {
|
|
1633
1634
|
if (!input) return;
|
|
1635
|
+
// Defense-in-depth (DD-26 #2): never overlay a secret-like write's content into
|
|
1636
|
+
// the persisted approval preview. Normally unreachable — the hook hard-blocks a
|
|
1637
|
+
// secret write and records no ledger input — but if a hook classify failure fell
|
|
1638
|
+
// one through, its content must still never reach args/args_preview. The
|
|
1639
|
+
// Invariant-A backstop is the final net; this closes the path at the source.
|
|
1640
|
+
const gatePath = extractFilePath(input);
|
|
1641
|
+
if (gatePath !== null && isSecretLikePath(gatePath)) return;
|
|
1634
1642
|
tc.args = input as JsonObject;
|
|
1635
1643
|
tc.argsPreview = buildElidedArgsPreview(input, SALIENT_ARG_FIELDS);
|
|
1636
1644
|
// Stamp the content digest from the AUTHORITATIVE captured input, so the
|
|
@@ -6,19 +6,25 @@
|
|
|
6
6
|
* parameter). On reinvocation, only the approval decisions are sent.
|
|
7
7
|
*
|
|
8
8
|
* Sections (in order, conditionally included):
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
* 5.
|
|
14
|
-
* 6.
|
|
15
|
-
* 7.
|
|
9
|
+
* 1. Interaction-mode / implement-plan directives
|
|
10
|
+
* 2. Agent instructions (persona/character)
|
|
11
|
+
* 3. Available skills metadata
|
|
12
|
+
* 4. Sub-agent delegation guidance
|
|
13
|
+
* 5. Workspace context (multi-root only; single-dir is redundant with SDK cwd)
|
|
14
|
+
* 6. Input files / referenced files
|
|
15
|
+
* 7. Response rules (only when agent has no custom instructions)
|
|
16
|
+
* 8. User's actual message
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { resolve } from "node:path";
|
|
19
20
|
import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
20
21
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
21
22
|
import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
23
|
+
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
24
|
+
import {
|
|
25
|
+
buildImplementPlanDirective,
|
|
26
|
+
findApprovedPlanPath,
|
|
27
|
+
} from "../../shared/implement-plan-prompt.js";
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Marker path segments that identify runner-internal directories. A workspace
|
|
@@ -47,6 +53,13 @@ export interface EnhancedPromptOptions {
|
|
|
47
53
|
workspaceFileRefs: string[];
|
|
48
54
|
attachmentPaths: string[];
|
|
49
55
|
interactionMode?: InteractionMode;
|
|
56
|
+
/**
|
|
57
|
+
* The execution is a Build-from-plan turn (spec.execution_config
|
|
58
|
+
* .build_from_plan): inject the implement-plan directive so the model reads
|
|
59
|
+
* the attached approved plan (or falls back to the conversation's plan).
|
|
60
|
+
* The user message itself is just a short label ("Build from plan").
|
|
61
|
+
*/
|
|
62
|
+
buildFromPlan?: boolean;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/**
|
|
@@ -63,6 +76,14 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
63
76
|
sections.push(modePrefix);
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
const implementPlan = formatImplementPlanSection(
|
|
80
|
+
options.buildFromPlan,
|
|
81
|
+
options.attachmentPaths,
|
|
82
|
+
);
|
|
83
|
+
if (implementPlan) {
|
|
84
|
+
sections.push(implementPlan);
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
if (options.instructions) {
|
|
67
88
|
sections.push(formatInstructions(options.instructions));
|
|
68
89
|
}
|
|
@@ -348,30 +369,47 @@ export function formatReferencedFiles(refs: string[]): string {
|
|
|
348
369
|
/**
|
|
349
370
|
* Returns a system-level directive when the execution is in Plan mode.
|
|
350
371
|
* Returns `undefined` for Agent mode (default) since no prefix is needed.
|
|
372
|
+
*
|
|
373
|
+
* The directive body is the harness-agnostic {@link PLAN_MODE_DIRECTIVE}
|
|
374
|
+
* (shared with the native harness) wrapped in this harness's XML-tag section
|
|
375
|
+
* framing. For Cursor this prompt IS the plan-mode enforcement — the Cursor
|
|
376
|
+
* SDK exposes no mode parameter — see the shared module's doc comment.
|
|
351
377
|
*/
|
|
352
378
|
export function formatInteractionModePrefix(
|
|
353
379
|
mode: InteractionMode | undefined,
|
|
354
380
|
): string | undefined {
|
|
355
|
-
if (
|
|
356
|
-
mode == null ||
|
|
357
|
-
mode === InteractionMode.UNSPECIFIED ||
|
|
358
|
-
mode === InteractionMode.AGENT
|
|
359
|
-
) {
|
|
381
|
+
if (mode !== InteractionMode.PLAN) {
|
|
360
382
|
return undefined;
|
|
361
383
|
}
|
|
362
384
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
385
|
+
return ["<interaction_mode>", PLAN_MODE_DIRECTIVE, "</interaction_mode>"].join(
|
|
386
|
+
"\n",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Returns the implement-plan directive section for a Build-from-plan
|
|
392
|
+
* execution, or `undefined` otherwise. The directive body is the shared
|
|
393
|
+
* {@link buildImplementPlanDirective} (same words as the native harness)
|
|
394
|
+
* wrapped in this harness's XML-tag framing; the variant is picked by whether
|
|
395
|
+
* the approved plan document is among the resolved attachment paths.
|
|
396
|
+
*
|
|
397
|
+
* Like the interaction-mode prefix, this rides BOTH prompt paths — the fresh
|
|
398
|
+
* enhanced prompt and a resumed turn's prefix — because build_from_plan is
|
|
399
|
+
* per-execution and the build turn is usually a follow-up on a resumed agent.
|
|
400
|
+
*/
|
|
401
|
+
export function formatImplementPlanSection(
|
|
402
|
+
buildFromPlan: boolean | undefined,
|
|
403
|
+
attachmentPaths: string[],
|
|
404
|
+
): string | undefined {
|
|
405
|
+
if (!buildFromPlan) {
|
|
406
|
+
return undefined;
|
|
372
407
|
}
|
|
373
408
|
|
|
374
|
-
|
|
409
|
+
const directive = buildImplementPlanDirective(
|
|
410
|
+
findApprovedPlanPath(attachmentPaths),
|
|
411
|
+
);
|
|
412
|
+
return ["<implement_plan>", directive, "</implement_plan>"].join("\n");
|
|
375
413
|
}
|
|
376
414
|
|
|
377
415
|
export function formatResponseRules(): string {
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* - Writes SKILL.md to .stigmer/skills/{name}/SKILL.md
|
|
6
6
|
* - Downloads and extracts ZIP artifacts (references/, scripts/, etc.)
|
|
7
7
|
* - Uses a platform-managed directory outside the workspace
|
|
8
|
-
* -
|
|
8
|
+
* - Ensures the workspace `.stigmer` symlink (see stigmer-link.ts)
|
|
9
9
|
* - Returns metadata for prompt injection
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { mkdir, writeFile,
|
|
12
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
13
13
|
import { join, dirname } from "node:path";
|
|
14
14
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
15
15
|
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
@@ -17,8 +17,8 @@ import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/ap
|
|
|
17
17
|
import type { SkillMetadata } from "./prompt-builder.js";
|
|
18
18
|
import { getPlatformDir } from "../../shared/workspace/platform-dir.js";
|
|
19
19
|
import { extractZipFileEntries } from "../../shared/zip-extract.js";
|
|
20
|
+
import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "./stigmer-link.js";
|
|
20
21
|
|
|
21
|
-
const STIGMER_LOCAL_STATE_DIR = ".stigmer";
|
|
22
22
|
const SKILLS_SUBDIR = "skills";
|
|
23
23
|
|
|
24
24
|
export interface SkillResolverOptions {
|
|
@@ -132,62 +132,6 @@ async function writeSkill(
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/**
|
|
136
|
-
* Ensure .stigmer symlink in the workspace points to the platform dir.
|
|
137
|
-
*
|
|
138
|
-
* The Cursor SDK reads files from the workspace CWD, so we need the
|
|
139
|
-
* .stigmer directory to be accessible there. Unlike the Python runner
|
|
140
|
-
* which intercepts file paths, Cursor reads directly from the filesystem.
|
|
141
|
-
*/
|
|
142
|
-
async function ensureStigmerSymlink(
|
|
143
|
-
workspaceDir: string,
|
|
144
|
-
platformDir: string,
|
|
145
|
-
): Promise<void> {
|
|
146
|
-
const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const existing = await readlink(linkPath);
|
|
150
|
-
if (existing === platformDir) return;
|
|
151
|
-
await unlink(linkPath);
|
|
152
|
-
} catch (err: any) {
|
|
153
|
-
if (err.code === "ENOENT") {
|
|
154
|
-
// No existing symlink
|
|
155
|
-
} else if (err.code === "EINVAL") {
|
|
156
|
-
// Exists but is not a symlink — remove the directory
|
|
157
|
-
await rm(linkPath, { recursive: true, force: true });
|
|
158
|
-
} else {
|
|
159
|
-
throw err;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
await symlink(platformDir, linkPath, "dir");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Remove the workspace `.stigmer` symlink created by {@link resolveSkills}.
|
|
168
|
-
*
|
|
169
|
-
* Called in the activity's finally so attaching a real repo leaves no Stigmer
|
|
170
|
-
* symlink behind once the turn ends (issue #173); a multi-turn session recreates
|
|
171
|
-
* it on the next turn. Only ever removes a SYMLINK — a real `.stigmer` directory
|
|
172
|
-
* (which would be the user's own, not ours) is left untouched. Best-effort.
|
|
173
|
-
*/
|
|
174
|
-
export async function removeStigmerSymlink(workspaceDir: string): Promise<void> {
|
|
175
|
-
const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
|
|
176
|
-
try {
|
|
177
|
-
const stat = await lstat(linkPath);
|
|
178
|
-
if (stat.isSymbolicLink()) {
|
|
179
|
-
await unlink(linkPath);
|
|
180
|
-
}
|
|
181
|
-
} catch (err: any) {
|
|
182
|
-
if (err?.code !== "ENOENT") {
|
|
183
|
-
console.warn(
|
|
184
|
-
`removeStigmerSymlink: failed to remove ${linkPath} (non-fatal): ` +
|
|
185
|
-
`${err instanceof Error ? err.message : err}`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
135
|
/**
|
|
192
136
|
* Clean up platform-managed skill directory for a session.
|
|
193
137
|
*/
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
|
@@ -12,14 +12,16 @@ import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_
|
|
|
12
12
|
import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
|
|
13
13
|
import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
14
14
|
import { ensurePlatformDir } from "../../shared/workspace/platform-dir.js";
|
|
15
|
+
import { resolveSessionWorkspaceRoot } from "../../shared/workspace/session-root.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Provision the session's workspace entries for a LOCAL Cursor agent.
|
|
18
19
|
*
|
|
19
20
|
* Clones git-repo entries (using the user's GITHUB_TOKEN from the resolved
|
|
20
21
|
* execution environment) and mounts local-path entries, then returns the
|
|
21
|
-
* directories the agent should operate in.
|
|
22
|
-
*
|
|
22
|
+
* directories the agent should operate in. A session with no workspace
|
|
23
|
+
* entries gets its own empty per-session directory (see session-root.ts) —
|
|
24
|
+
* never the shared root, which would leak other sessions' files into it.
|
|
23
25
|
*
|
|
24
26
|
* provisionGit is idempotent (it reuses an existing clone), so this is safe
|
|
25
27
|
* to call on every execution, including multi-turn and HITL reinvocations.
|
|
@@ -32,7 +34,7 @@ export async function provisionCursorWorkspace(
|
|
|
32
34
|
): Promise<string[]> {
|
|
33
35
|
const entries = session.spec?.workspaceEntries ?? [];
|
|
34
36
|
if (entries.length === 0) {
|
|
35
|
-
return [config.workspaceRootDir];
|
|
37
|
+
return [await resolveSessionWorkspaceRoot(config.workspaceRootDir, entries, sessionId)];
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const platformDir = await ensurePlatformDir(sessionId);
|
|
@@ -49,7 +49,9 @@ import type { SetupResult } from "../setup.js";
|
|
|
49
49
|
vi.mock("@temporalio/activity", () => ({
|
|
50
50
|
Context: {
|
|
51
51
|
current: () => ({
|
|
52
|
-
|
|
52
|
+
// A real AbortSignal: the workspace-lock wait registers abort listeners
|
|
53
|
+
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
54
|
+
cancellationSignal: new AbortController().signal,
|
|
53
55
|
heartbeat: vi.fn(),
|
|
54
56
|
}),
|
|
55
57
|
},
|
|
@@ -258,7 +260,9 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
258
260
|
},
|
|
259
261
|
agent: {},
|
|
260
262
|
session: { spec: { workspaceEntries: [] } },
|
|
261
|
-
|
|
263
|
+
// Unique per test file: the workspace turn lock keys on this path, and a
|
|
264
|
+
// path shared across files would serialize parallel test workers for real.
|
|
265
|
+
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-approve-all" },
|
|
262
266
|
mcpConnection: null,
|
|
263
267
|
mergedEnvVars: {},
|
|
264
268
|
secretKeys: new Set<string>(),
|
|
@@ -292,6 +296,7 @@ const baseConfig: Config = {
|
|
|
292
296
|
checkpointerProxyEndpoint: "http://localhost:7234",
|
|
293
297
|
primaryModel: "claude-sonnet",
|
|
294
298
|
cursorStreamStallTimeoutMs: 180000,
|
|
299
|
+
workspaceLockTimeoutMs: 900000,
|
|
295
300
|
};
|
|
296
301
|
|
|
297
302
|
describe("ExecuteDeepAgent — APPROVE_ALL durable resume preserves leading thinking + first tool call", () => {
|
|
@@ -42,7 +42,9 @@ import type { SetupResult } from "../setup.js";
|
|
|
42
42
|
vi.mock("@temporalio/activity", () => ({
|
|
43
43
|
Context: {
|
|
44
44
|
current: () => ({
|
|
45
|
-
|
|
45
|
+
// A real AbortSignal: the workspace-lock wait registers abort listeners
|
|
46
|
+
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
47
|
+
cancellationSignal: new AbortController().signal,
|
|
46
48
|
heartbeat: vi.fn(),
|
|
47
49
|
}),
|
|
48
50
|
},
|
|
@@ -213,7 +215,9 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
213
215
|
},
|
|
214
216
|
agent: {},
|
|
215
217
|
session: { spec: { workspaceEntries: [] } },
|
|
216
|
-
|
|
218
|
+
// Unique per test file: the workspace turn lock keys on this path, and a
|
|
219
|
+
// path shared across files would serialize parallel test workers for real.
|
|
220
|
+
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-history" },
|
|
217
221
|
mcpConnection: null,
|
|
218
222
|
mergedEnvVars: {},
|
|
219
223
|
secretKeys: new Set<string>(),
|
|
@@ -246,6 +250,7 @@ const baseConfig: Config = {
|
|
|
246
250
|
checkpointerProxyEndpoint: "http://localhost:7234",
|
|
247
251
|
primaryModel: "claude-sonnet",
|
|
248
252
|
cursorStreamStallTimeoutMs: 180000,
|
|
253
|
+
workspaceLockTimeoutMs: 900000,
|
|
249
254
|
};
|
|
250
255
|
|
|
251
256
|
describe("ExecuteDeepAgent — durable-checkpoint resume preserves history", () => {
|
|
@@ -8,7 +8,9 @@ import type { Config } from "../../../config.js";
|
|
|
8
8
|
vi.mock("@temporalio/activity", () => ({
|
|
9
9
|
Context: {
|
|
10
10
|
current: () => ({
|
|
11
|
-
|
|
11
|
+
// A real AbortSignal: the workspace-lock wait registers abort listeners
|
|
12
|
+
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
13
|
+
cancellationSignal: new AbortController().signal,
|
|
12
14
|
heartbeat: vi.fn(),
|
|
13
15
|
}),
|
|
14
16
|
},
|
|
@@ -45,6 +47,7 @@ describe("ExecuteDeepAgent activity", () => {
|
|
|
45
47
|
checkpointerProxyEndpoint: null,
|
|
46
48
|
primaryModel: "gpt-4.1",
|
|
47
49
|
cursorStreamStallTimeoutMs: 180000,
|
|
50
|
+
workspaceLockTimeoutMs: 900000,
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
let activities: ReturnType<typeof createDeepAgentActivities>;
|