@stigmer/runner 3.1.1 → 3.1.3
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/capture-flow.d.ts +18 -0
- package/dist/activities/execute-cursor/capture-flow.js +21 -0
- package/dist/activities/execute-cursor/capture-flow.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 +222 -160
- 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/turn-boundary.d.ts +108 -0
- package/dist/activities/execute-cursor/turn-boundary.js +163 -0
- package/dist/activities/execute-cursor/turn-boundary.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 +89 -5
- 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/activities/execute-deep-agent/streaming-v3.js +6 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
- package/dist/activities/execute-deep-agent/streaming.js +5 -1
- package/dist/activities/execute-deep-agent/streaming.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/filereview/capture.d.ts +18 -1
- package/dist/shared/filereview/capture.js +105 -28
- package/dist/shared/filereview/capture.js.map +1 -1
- package/dist/shared/filereview/cas-substrate.d.ts +9 -0
- package/dist/shared/filereview/cas-substrate.js +9 -1
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/events.d.ts +16 -0
- package/dist/shared/filereview/events.js +37 -0
- package/dist/shared/filereview/events.js.map +1 -1
- package/dist/shared/filereview/git-substrate.d.ts +42 -0
- package/dist/shared/filereview/git-substrate.js +78 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +3 -1
- package/dist/shared/filereview/index.js +3 -1
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/line-counts.d.ts +48 -0
- package/dist/shared/filereview/line-counts.js +72 -0
- package/dist/shared/filereview/line-counts.js.map +1 -0
- package/dist/shared/filereview/progress.d.ts +96 -0
- package/dist/shared/filereview/progress.js +134 -0
- package/dist/shared/filereview/progress.js.map +1 -0
- package/dist/shared/implement-plan-prompt.d.ts +44 -0
- package/dist/shared/implement-plan-prompt.js +98 -0
- package/dist/shared/implement-plan-prompt.js.map +1 -0
- package/dist/shared/plan-artifact.d.ts +59 -11
- package/dist/shared/plan-artifact.js +144 -16
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +32 -0
- package/dist/shared/plan-mode-prompt.js +56 -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 +8 -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 +132 -2
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +55 -0
- package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -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__/turn-boundary.test.ts +280 -0
- 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/capture-flow.ts +32 -0
- 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 +243 -178
- 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/turn-boundary.ts +274 -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 +99 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +7 -2
- package/src/activities/execute-deep-agent/index.ts +104 -5
- package/src/activities/execute-deep-agent/prompt-builder.ts +39 -0
- package/src/activities/execute-deep-agent/setup.ts +25 -17
- package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
- package/src/activities/execute-deep-agent/streaming.ts +12 -1
- 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 +86 -0
- package/src/shared/__tests__/plan-artifact.test.ts +142 -9
- 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/filereview/__tests__/capture.test.ts +182 -1
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +34 -0
- package/src/shared/filereview/__tests__/events.test.ts +105 -0
- package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
- package/src/shared/filereview/__tests__/progress.test.ts +236 -0
- package/src/shared/filereview/capture.ts +134 -30
- package/src/shared/filereview/cas-substrate.ts +17 -1
- package/src/shared/filereview/events.ts +50 -0
- package/src/shared/filereview/git-substrate.ts +108 -0
- package/src/shared/filereview/index.ts +22 -1
- package/src/shared/filereview/line-counts.ts +83 -0
- package/src/shared/filereview/progress.ts +186 -0
- package/src/shared/implement-plan-prompt.ts +104 -0
- package/src/shared/plan-artifact.ts +152 -16
- package/src/shared/plan-mode-prompt.ts +56 -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
|
@@ -398,7 +398,10 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
398
398
|
expect(mockedInterrupt).not.toHaveBeenCalled();
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
-
it("KEEPS GATING a gitignored write (it cannot be captured or reverted)", async () => {
|
|
401
|
+
it("KEEPS GATING a non-secret gitignored write (it cannot be captured or reverted)", async () => {
|
|
402
|
+
// A NON-secret gitignored path: no CAS routing (captureIgnored unset) and not
|
|
403
|
+
// secret-like, so it stays on the interrupt gate. (A secret-like gitignored
|
|
404
|
+
// write is hard-blocked instead — see the DD-26 #2 deny-gate cases below.)
|
|
402
405
|
const mw = createApprovalGateMiddleware(makeConfig({
|
|
403
406
|
fileCaptureMode: true,
|
|
404
407
|
isCapturablePath: async () => false, // gitignored
|
|
@@ -406,7 +409,7 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
406
409
|
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
407
410
|
|
|
408
411
|
await mw.wrapToolCall!(
|
|
409
|
-
makeRequest({ name: "write", args: { path: ".
|
|
412
|
+
makeRequest({ name: "write", args: { path: "dist/bundle.js" } }),
|
|
410
413
|
passthrough,
|
|
411
414
|
);
|
|
412
415
|
|
|
@@ -650,12 +653,12 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
650
653
|
expect(mockedInterrupt).toHaveBeenCalledTimes(1);
|
|
651
654
|
});
|
|
652
655
|
|
|
653
|
-
it("
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
656
|
+
it("HARD-BLOCKS a secret-like write when capture mode is off (DD-26 #2): never gated, never applied", async () => {
|
|
657
|
+
// DD-26 follow-up #2 supersedes the earlier "gated, not hard-blocked" parity:
|
|
658
|
+
// a secret-like write must NEVER surface its content for approval, in ANY
|
|
659
|
+
// mode. On the deny-gate it is hard-blocked exactly like the capture-mode
|
|
660
|
+
// secret block — never interrupted, never applied. The content never reaches
|
|
661
|
+
// a pending approval or the persisted transcript.
|
|
659
662
|
const handler = vi.fn(passthrough);
|
|
660
663
|
const recordBlockedSecret = vi.fn();
|
|
661
664
|
const mw = createApprovalGateMiddleware(makeConfig({
|
|
@@ -665,14 +668,76 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
665
668
|
}));
|
|
666
669
|
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
667
670
|
|
|
671
|
+
const result = await mw.wrapToolCall!(
|
|
672
|
+
makeRequest({ name: "write", args: { path: ".env", content: "API_KEY=xyz" } }),
|
|
673
|
+
handler,
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect(mockedInterrupt).not.toHaveBeenCalled(); // hard-block, not a pause
|
|
677
|
+
expect(handler).not.toHaveBeenCalled(); // never applied
|
|
678
|
+
expect(recordBlockedSecret).toHaveBeenCalledWith(".env");
|
|
679
|
+
expect((result as ToolMessage).content).toContain("blocked for security");
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("hard-blocks a secret-like edit too (id_rsa via path fragment)", async () => {
|
|
683
|
+
const handler = vi.fn(passthrough);
|
|
684
|
+
const mw = createApprovalGateMiddleware(makeConfig({
|
|
685
|
+
fileCaptureMode: false,
|
|
686
|
+
captureIgnored: false,
|
|
687
|
+
}));
|
|
688
|
+
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
689
|
+
|
|
690
|
+
const result = await mw.wrapToolCall!(
|
|
691
|
+
makeRequest({ name: "edit", args: { path: ".ssh/id_rsa", old_string: "a", new_string: "b" } }),
|
|
692
|
+
handler,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
expect(mockedInterrupt).not.toHaveBeenCalled();
|
|
696
|
+
expect(handler).not.toHaveBeenCalled();
|
|
697
|
+
expect((result as ToolMessage).content).toContain("blocked for security");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("hard-blocks a secret write in a git workspace with no storage (captureMode on, captureIgnored off)", async () => {
|
|
701
|
+
// captureMode true + captureIgnored false = a git workspace with no artifact
|
|
702
|
+
// storage. A gitignored secret write skips the captureIgnored arm and would
|
|
703
|
+
// otherwise have reached the deny-gate (a leak); it is now hard-blocked too,
|
|
704
|
+
// and the recorded path lets the turn boundary author a content-less entry.
|
|
705
|
+
const handler = vi.fn(passthrough);
|
|
706
|
+
const recordBlockedSecret = vi.fn();
|
|
707
|
+
const mw = createApprovalGateMiddleware(makeConfig({
|
|
708
|
+
fileCaptureMode: true,
|
|
709
|
+
isCapturablePath: async () => false, // gitignored
|
|
710
|
+
captureIgnored: false, // no artifact storage
|
|
711
|
+
recordBlockedSecret,
|
|
712
|
+
}));
|
|
713
|
+
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
714
|
+
|
|
715
|
+
const result = await mw.wrapToolCall!(
|
|
716
|
+
makeRequest({ name: "write", args: { path: "secrets.yaml", content: "token: t" } }),
|
|
717
|
+
handler,
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
expect(mockedInterrupt).not.toHaveBeenCalled();
|
|
721
|
+
expect(handler).not.toHaveBeenCalled();
|
|
722
|
+
expect(recordBlockedSecret).toHaveBeenCalledWith("secrets.yaml");
|
|
723
|
+
expect((result as ToolMessage).content).toContain("blocked for security");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("still GATES a NON-secret write when capture mode is off (deny-gate unchanged)", async () => {
|
|
727
|
+
const handler = vi.fn(passthrough);
|
|
728
|
+
const mw = createApprovalGateMiddleware(makeConfig({
|
|
729
|
+
fileCaptureMode: false,
|
|
730
|
+
captureIgnored: false,
|
|
731
|
+
}));
|
|
732
|
+
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
733
|
+
|
|
668
734
|
await mw.wrapToolCall!(
|
|
669
|
-
makeRequest({ name: "write", args: { path: ".
|
|
735
|
+
makeRequest({ name: "write", args: { path: "notes.md", content: "hi" } }),
|
|
670
736
|
handler,
|
|
671
737
|
);
|
|
672
738
|
|
|
673
|
-
expect(mockedInterrupt).toHaveBeenCalledTimes(1); //
|
|
674
|
-
expect(
|
|
675
|
-
expect(handler).toHaveBeenCalledTimes(1); // flows only after the user approves
|
|
739
|
+
expect(mockedInterrupt).toHaveBeenCalledTimes(1); // a non-secret write still gates
|
|
740
|
+
expect(handler).toHaveBeenCalledTimes(1); // flows after approve
|
|
676
741
|
});
|
|
677
742
|
|
|
678
743
|
it("still auto-approves read-only built-ins when capture mode is off", async () => {
|
|
@@ -138,6 +138,24 @@ const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
|
|
|
138
138
|
shell: "Execute command: {{args.command}}",
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The ToolMessage returned when a secret-like write is hard-blocked (DD-E /
|
|
143
|
+
* DD-26 #2): the write is NEVER applied and the graph continues (this replaces
|
|
144
|
+
* the tool call's side effect, so the model moves on rather than waiting). The
|
|
145
|
+
* path is named (a filename is not itself the secret); the CONTENT is not echoed.
|
|
146
|
+
* Shared by the capture-mode secret block and the deny-gate secret block so both
|
|
147
|
+
* speak with one voice.
|
|
148
|
+
*/
|
|
149
|
+
function secretBlockToolMessage(toolName: string, path: string, toolCallId: string): ToolMessage {
|
|
150
|
+
return new ToolMessage({
|
|
151
|
+
content:
|
|
152
|
+
`Tool '${toolName}' was blocked for security: '${path}' matches a ` +
|
|
153
|
+
`secret-like path Stigmer will not capture for review. Nothing was written.`,
|
|
154
|
+
tool_call_id: toolCallId,
|
|
155
|
+
name: toolName,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
export function createApprovalGateMiddleware(
|
|
142
160
|
config: ApprovalGateConfig,
|
|
143
161
|
): StigmerMiddleware {
|
|
@@ -183,13 +201,7 @@ export function createApprovalGateMiddleware(
|
|
|
183
201
|
// and NEVER captured. Record it so the turn boundary authors a
|
|
184
202
|
// DIFF_UNREVIEWABLE entry (blocking approval); nothing is written.
|
|
185
203
|
config.recordBlockedSecret?.(path);
|
|
186
|
-
return
|
|
187
|
-
content:
|
|
188
|
-
`Tool '${toolName}' was blocked for security: '${path}' matches a ` +
|
|
189
|
-
`secret-like path Stigmer will not capture for review. Nothing was written.`,
|
|
190
|
-
tool_call_id: toolCall.id,
|
|
191
|
-
name: toolName,
|
|
192
|
-
});
|
|
204
|
+
return secretBlockToolMessage(toolName, path, toolCall.id);
|
|
193
205
|
}
|
|
194
206
|
if (category === "write") {
|
|
195
207
|
// Non-secret gitignored write/edit: flows (apply-then-review). The
|
|
@@ -203,6 +215,26 @@ export function createApprovalGateMiddleware(
|
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
// Deny-gate secret hard-block (DD-26 #2): a built-in file WRITE to a
|
|
219
|
+
// secret-like path that reaches here has no capture substrate for it — the
|
|
220
|
+
// classic no-storage deny-gate, or a git workspace with no artifact storage
|
|
221
|
+
// whose gitignored write skipped the captureIgnored arm above. It must NOT
|
|
222
|
+
// surface its content for approval, so hard-block it (never applied, graph
|
|
223
|
+
// continues) exactly like the capture-mode secret block — a secret write is
|
|
224
|
+
// never applied or persisted in ANY mode. Placed AFTER the capture block so
|
|
225
|
+
// capture-mode paths stay byte-identical (a capturable write already flowed;
|
|
226
|
+
// a captureIgnored gitignored secret is already blocked). Deletes carry no
|
|
227
|
+
// content and stay on the deny-gate. recordBlockedSecret is a no-op unless a
|
|
228
|
+
// turn boundary reads it (the git-no-storage case, where it authors a
|
|
229
|
+
// content-less DIFF_UNREVIEWABLE).
|
|
230
|
+
if (!serverSlug && category === "write") {
|
|
231
|
+
const path = extractFilePath(toolCall.args);
|
|
232
|
+
if (path !== null && isSecretLikePath(path)) {
|
|
233
|
+
config.recordBlockedSecret?.(path);
|
|
234
|
+
return secretBlockToolMessage(toolName, path, toolCall.id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
206
238
|
const requirement = resolveToolApproval(
|
|
207
239
|
toolName,
|
|
208
240
|
serverSlug,
|
package/src/runner-manager.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "@temporalio/worker";
|
|
25
25
|
import type { PayloadCodec } from "@temporalio/common";
|
|
26
26
|
import type { Config } from "./config.js";
|
|
27
|
-
import { DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS } from "./config.js";
|
|
27
|
+
import { DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
|
|
28
28
|
import type { WorkerActivities } from "./worker.js";
|
|
29
29
|
import { resolveWorkflowSource, OTEL_WORKFLOW_INTERCEPTOR_MODULE } from "./workflow-source.js";
|
|
30
30
|
import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
|
|
@@ -91,6 +91,9 @@ export interface RunnerManagerOptions {
|
|
|
91
91
|
/** No-progress bound for the Cursor harness stream (ms). @default 180000 */
|
|
92
92
|
readonly cursorStreamStallTimeoutMs?: number;
|
|
93
93
|
|
|
94
|
+
/** Max wait for the per-workspace turn lock (ms). @default 900000 */
|
|
95
|
+
readonly workspaceLockTimeoutMs?: number;
|
|
96
|
+
|
|
94
97
|
/** Checkpointer type for LangGraph agent state. @default "memory" (or "http" if proxyEndpoint is set) */
|
|
95
98
|
readonly checkpointerType?: "memory" | "http";
|
|
96
99
|
|
|
@@ -576,6 +579,8 @@ export function mapManagerOptionsToConfig(
|
|
|
576
579
|
primaryModel: options.primaryModel ?? "gpt-4.1",
|
|
577
580
|
cursorStreamStallTimeoutMs:
|
|
578
581
|
options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
|
|
582
|
+
workspaceLockTimeoutMs:
|
|
583
|
+
options.workspaceLockTimeoutMs ?? DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
579
584
|
};
|
|
580
585
|
}
|
|
581
586
|
|
package/src/runner.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import { homedir, tmpdir } from "node:os";
|
|
18
18
|
import type { PayloadCodec } from "@temporalio/common";
|
|
19
19
|
import type { Config } from "./config.js";
|
|
20
|
-
import { DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS } from "./config.js";
|
|
20
|
+
import { DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
|
|
21
21
|
import type { WorkerActivities } from "./worker.js";
|
|
22
22
|
import { resolveRunnerBootstrap } from "./bootstrap.js";
|
|
23
23
|
|
|
@@ -69,6 +69,9 @@ export interface StigmerRunnerOptions {
|
|
|
69
69
|
/** No-progress bound for the Cursor harness stream (ms). @default 180000 */
|
|
70
70
|
readonly cursorStreamStallTimeoutMs?: number;
|
|
71
71
|
|
|
72
|
+
/** Max wait for the per-workspace turn lock (ms). @default 900000 */
|
|
73
|
+
readonly workspaceLockTimeoutMs?: number;
|
|
74
|
+
|
|
72
75
|
/** Checkpointer type for LangGraph agent state. @default "memory" (or "http" if proxyEndpoint is set) */
|
|
73
76
|
readonly checkpointerType?: "memory" | "http";
|
|
74
77
|
|
|
@@ -274,6 +277,8 @@ export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
|
|
|
274
277
|
primaryModel: options.primaryModel ?? "gpt-4.1",
|
|
275
278
|
cursorStreamStallTimeoutMs:
|
|
276
279
|
options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
|
|
280
|
+
workspaceLockTimeoutMs:
|
|
281
|
+
options.workspaceLockTimeoutMs ?? DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
277
282
|
};
|
|
278
283
|
}
|
|
279
284
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import {
|
|
6
6
|
LocalArtifactStorage,
|
|
7
7
|
ProxyArtifactStorage,
|
|
8
8
|
createArtifactStorage,
|
|
9
|
+
resolveUsableArtifactStorage,
|
|
9
10
|
loadArtifactStorageConfig,
|
|
10
11
|
type ArtifactStorageConfig,
|
|
11
12
|
} from "../artifact-storage.js";
|
|
13
|
+
import { deriveCaptureMode } from "../filereview/capture.js";
|
|
12
14
|
|
|
13
15
|
// ── LocalArtifactStorage ─────────────────────────────────────────────
|
|
14
16
|
|
|
@@ -428,6 +430,7 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
428
430
|
checkpointerProxyEndpoint: null,
|
|
429
431
|
primaryModel: "gpt-4.1",
|
|
430
432
|
cursorStreamStallTimeoutMs: 180000,
|
|
433
|
+
workspaceLockTimeoutMs: 900000,
|
|
431
434
|
};
|
|
432
435
|
|
|
433
436
|
afterEach(() => {
|
|
@@ -476,3 +479,110 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
476
479
|
expect(cfg.type).toBe("proxy");
|
|
477
480
|
});
|
|
478
481
|
});
|
|
482
|
+
|
|
483
|
+
// ── resolveUsableArtifactStorage (DD-26 follow-up #1) ─────────────────
|
|
484
|
+
//
|
|
485
|
+
// The shared construct-or-degrade seam: it must return `undefined` (never throw)
|
|
486
|
+
// for every "no working substrate" condition so both harnesses fall to the
|
|
487
|
+
// deny-gate up front, instead of flowing file writes then crashing at the
|
|
488
|
+
// turn-boundary upload.
|
|
489
|
+
|
|
490
|
+
describe("resolveUsableArtifactStorage", () => {
|
|
491
|
+
const ctx = { executionId: "exec-test" };
|
|
492
|
+
let tempDir: string;
|
|
493
|
+
|
|
494
|
+
beforeEach(async () => {
|
|
495
|
+
tempDir = await mkdtemp(join(tmpdir(), "resolve-artifact-test-"));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
afterEach(async () => {
|
|
499
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
500
|
+
vi.restoreAllMocks();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const localCfg = (localPath: string): ArtifactStorageConfig => ({
|
|
504
|
+
type: "local",
|
|
505
|
+
localPath,
|
|
506
|
+
localServeUrl: "http://localhost:7235",
|
|
507
|
+
proxyEndpoint: null,
|
|
508
|
+
proxyAuthToken: null,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("returns a LocalArtifactStorage for a writable path and leaves no residue", async () => {
|
|
512
|
+
const base = join(tempDir, "artifacts");
|
|
513
|
+
const storage = await resolveUsableArtifactStorage(localCfg(base), ctx);
|
|
514
|
+
|
|
515
|
+
expect(storage).toBeInstanceOf(LocalArtifactStorage);
|
|
516
|
+
// The write-probe file must be cleaned up; the base dir may be created.
|
|
517
|
+
const entries = await readdir(base);
|
|
518
|
+
expect(entries).toEqual([]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("returns undefined for an unwritable local path (mkdir ENOTDIR)", async () => {
|
|
522
|
+
// Deterministic, cross-platform: point localPath at a child of a regular
|
|
523
|
+
// FILE, so the resolver's recursive mkdir fails with ENOTDIR.
|
|
524
|
+
const filePath = join(tempDir, "not-a-dir");
|
|
525
|
+
await writeFile(filePath, "");
|
|
526
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
527
|
+
|
|
528
|
+
const storage = await resolveUsableArtifactStorage(
|
|
529
|
+
localCfg(join(filePath, "sub")),
|
|
530
|
+
ctx,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
expect(storage).toBeUndefined();
|
|
534
|
+
expect(warn).toHaveBeenCalledWith(
|
|
535
|
+
expect.stringContaining("local path not writable"),
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("returns undefined when proxy config is a misconfig (construct throws)", async () => {
|
|
540
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
541
|
+
const cfg: ArtifactStorageConfig = {
|
|
542
|
+
type: "proxy",
|
|
543
|
+
localPath: "/tmp/artifacts",
|
|
544
|
+
localServeUrl: "http://localhost:7235",
|
|
545
|
+
proxyEndpoint: "https://proxy.example.com",
|
|
546
|
+
proxyAuthToken: null, // missing token
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const storage = await resolveUsableArtifactStorage(cfg, ctx);
|
|
550
|
+
|
|
551
|
+
expect(storage).toBeUndefined();
|
|
552
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("unavailable"));
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("returns a ProxyArtifactStorage for a valid proxy config without any network call", async () => {
|
|
556
|
+
// Proxy is never probed at the network layer — assert no fetch happens.
|
|
557
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
558
|
+
const cfg: ArtifactStorageConfig = {
|
|
559
|
+
type: "proxy",
|
|
560
|
+
localPath: "/tmp/artifacts",
|
|
561
|
+
localServeUrl: "http://localhost:7235",
|
|
562
|
+
proxyEndpoint: "https://proxy.example.com",
|
|
563
|
+
proxyAuthToken: "token-123",
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const storage = await resolveUsableArtifactStorage(cfg, ctx);
|
|
567
|
+
|
|
568
|
+
expect(storage).toBeInstanceOf(ProxyArtifactStorage);
|
|
569
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("wires through to deriveCaptureMode: an unwritable local store degrades a non-git turn to the deny-gate", async () => {
|
|
573
|
+
const filePath = join(tempDir, "not-a-dir");
|
|
574
|
+
await writeFile(filePath, "");
|
|
575
|
+
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
576
|
+
|
|
577
|
+
const storage = await resolveUsableArtifactStorage(
|
|
578
|
+
localCfg(join(filePath, "sub")),
|
|
579
|
+
ctx,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// Non-git workspace + no usable storage => no capture substrate => deny-gate.
|
|
583
|
+
expect(deriveCaptureMode("/some/workspace", false, !!storage)).toBe(false);
|
|
584
|
+
// A git workspace still captures (git substrate needs no storage), but its
|
|
585
|
+
// gitignored->CAS edits and offload are off because the store is absent.
|
|
586
|
+
expect(deriveCaptureMode("/some/workspace", true, !!storage)).toBe(true);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildImplementPlanDirective,
|
|
4
|
+
findApprovedPlanPath,
|
|
5
|
+
} from "../implement-plan-prompt.js";
|
|
6
|
+
|
|
7
|
+
describe("findApprovedPlanPath", () => {
|
|
8
|
+
it("finds the plan by the legacy filename among attachment paths", () => {
|
|
9
|
+
expect(
|
|
10
|
+
findApprovedPlanPath([
|
|
11
|
+
".stigmer/inputs/data.csv",
|
|
12
|
+
".stigmer/inputs/plan.md",
|
|
13
|
+
]),
|
|
14
|
+
).toBe(".stigmer/inputs/plan.md");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("finds a title-named <slug>_<id>.plan.md plan", () => {
|
|
18
|
+
expect(
|
|
19
|
+
findApprovedPlanPath([
|
|
20
|
+
".stigmer/inputs/data.csv",
|
|
21
|
+
".stigmer/inputs/plan-card-ux-cleanup_a1b2c3d4.plan.md",
|
|
22
|
+
]),
|
|
23
|
+
).toBe(".stigmer/inputs/plan-card-ux-cleanup_a1b2c3d4.plan.md");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns undefined when no plan attachment resolved", () => {
|
|
27
|
+
expect(findApprovedPlanPath([".stigmer/inputs/data.csv"])).toBeUndefined();
|
|
28
|
+
expect(findApprovedPlanPath([])).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("matches the plan convention only — a plan-adjacent filename is not the plan", () => {
|
|
32
|
+
expect(
|
|
33
|
+
findApprovedPlanPath([".stigmer/inputs/my-plan.md.bak"]),
|
|
34
|
+
).toBeUndefined();
|
|
35
|
+
// A file merely containing "plan" is not a plan artifact.
|
|
36
|
+
expect(
|
|
37
|
+
findApprovedPlanPath([".stigmer/inputs/myplan.md"]),
|
|
38
|
+
).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("buildImplementPlanDirective", () => {
|
|
43
|
+
it("points the model at the attached plan and names it authoritative", () => {
|
|
44
|
+
const directive = buildImplementPlanDirective(".stigmer/inputs/plan.md");
|
|
45
|
+
|
|
46
|
+
expect(directive).toContain("APPROVED");
|
|
47
|
+
expect(directive).toContain("`.stigmer/inputs/plan.md`");
|
|
48
|
+
expect(directive).toContain("Read it FIRST");
|
|
49
|
+
// The user may have edited the document after the plan turn — the file,
|
|
50
|
+
// not the conversation, is the approved text.
|
|
51
|
+
expect(directive).toContain("authoritative");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("falls back to the conversation's plan when nothing is attached", () => {
|
|
55
|
+
const directive = buildImplementPlanDirective(undefined);
|
|
56
|
+
|
|
57
|
+
expect(directive).toContain("APPROVED");
|
|
58
|
+
expect(directive).toContain("conversation above");
|
|
59
|
+
expect(directive).not.toContain("plan.md");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Tier 3 (plan-driven build progress): both variants instruct the agent to
|
|
63
|
+
// derive its to-do list from the plan and keep it current — the agent's todo
|
|
64
|
+
// tool is the single writer of status.todos, so this instruction is the
|
|
65
|
+
// entire plan→progress linkage.
|
|
66
|
+
it("instructs plan-derived progress tracking in the attached variant", () => {
|
|
67
|
+
const directive = buildImplementPlanDirective(".stigmer/inputs/plan.md");
|
|
68
|
+
|
|
69
|
+
expect(directive).toContain("to-do list");
|
|
70
|
+
expect(directive).toContain("break the plan into");
|
|
71
|
+
expect(directive).toContain("in progress");
|
|
72
|
+
expect(directive).toContain("completed");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("instructs plan-derived progress tracking in the conversation-only variant", () => {
|
|
76
|
+
const directive = buildImplementPlanDirective(undefined);
|
|
77
|
+
|
|
78
|
+
expect(directive).toContain("to-do list");
|
|
79
|
+
expect(directive).toContain("break the plan into");
|
|
80
|
+
expect(directive).toContain("in progress");
|
|
81
|
+
expect(directive).toContain("completed");
|
|
82
|
+
// The progress block must never name the file — this variant exists
|
|
83
|
+
// because no plan file materialized.
|
|
84
|
+
expect(directive).not.toContain("plan.md");
|
|
85
|
+
});
|
|
86
|
+
});
|