@stigmer/cli 3.12.8 → 3.12.9
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/commands/agent-exec-flags.d.ts +1 -0
- package/commands/agent-exec-flags.d.ts.map +1 -1
- package/commands/agent-exec-flags.js +3 -1
- package/commands/agent-exec-flags.js.map +1 -1
- package/commands/run.js +3 -0
- package/commands/run.js.map +1 -1
- package/package.json +5 -5
- package/resources/run/agent-exec.d.ts.map +1 -1
- package/resources/run/agent-exec.js +2 -0
- package/resources/run/agent-exec.js.map +1 -1
- package/resources/run/create.d.ts +2 -1
- package/resources/run/create.d.ts.map +1 -1
- package/resources/run/create.js +21 -3
- package/resources/run/create.js.map +1 -1
- package/resources/run/header.d.ts +9 -1
- package/resources/run/header.d.ts.map +1 -1
- package/resources/run/header.js +3 -0
- package/resources/run/header.js.map +1 -1
- package/resources/run/prepare.d.ts +41 -3
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +53 -16
- package/resources/run/prepare.js.map +1 -1
- package/src/commands/agent-exec-flags.ts +5 -2
- package/src/commands/run.ts +3 -0
- package/src/resources/run/agent-exec.test.ts +25 -0
- package/src/resources/run/agent-exec.ts +2 -0
- package/src/resources/run/create.test.ts +92 -1
- package/src/resources/run/create.ts +25 -6
- package/src/resources/run/header.test.ts +18 -0
- package/src/resources/run/header.ts +11 -1
- package/src/resources/run/prepare.test.ts +247 -5
- package/src/resources/run/prepare.ts +99 -21
|
@@ -45,6 +45,7 @@ export async function executeResolvedAgent(input: ResolvedAgentExecInput): Promi
|
|
|
45
45
|
serviceTier: prepared.serviceTier,
|
|
46
46
|
thinking: prepared.thinking,
|
|
47
47
|
autoApproveAll: prepared.autoApproveAll,
|
|
48
|
+
harness: prepared.harness,
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
// The backend owns the canonical session id: it creates the session and
|
|
@@ -56,6 +57,7 @@ export async function executeResolvedAgent(input: ResolvedAgentExecInput): Promi
|
|
|
56
57
|
sessionId,
|
|
57
58
|
model: prepared.model,
|
|
58
59
|
mode: prepared.mode,
|
|
60
|
+
harness: prepared.harness,
|
|
59
61
|
workspaces: workspaceNames(prepared.workspaceEntries),
|
|
60
62
|
};
|
|
61
63
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
7
|
import { InteractionMode, ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
8
|
import { create } from "@bufbuild/protobuf";
|
|
9
|
+
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
9
10
|
import {
|
|
10
11
|
LocalPathSourceSchema,
|
|
11
12
|
WorkspaceEntrySchema,
|
|
@@ -43,6 +44,7 @@ describe("createAgentExecution", () => {
|
|
|
43
44
|
serviceTier: "",
|
|
44
45
|
thinking: "",
|
|
45
46
|
autoApproveAll: true,
|
|
47
|
+
harness: "",
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
expect(exec.kind).toBe("AgentExecution");
|
|
@@ -72,6 +74,7 @@ describe("createAgentExecution", () => {
|
|
|
72
74
|
serviceTier: "",
|
|
73
75
|
thinking: "",
|
|
74
76
|
autoApproveAll: false,
|
|
77
|
+
harness: "",
|
|
75
78
|
});
|
|
76
79
|
expect(exec.spec?.message).toBe("hi");
|
|
77
80
|
expect(exec.spec?.executionConfig).toBeUndefined();
|
|
@@ -92,6 +95,7 @@ describe("createAgentExecution", () => {
|
|
|
92
95
|
serviceTier: "fast",
|
|
93
96
|
thinking: "",
|
|
94
97
|
autoApproveAll: false,
|
|
98
|
+
harness: "",
|
|
95
99
|
});
|
|
96
100
|
expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.FAST);
|
|
97
101
|
});
|
|
@@ -113,6 +117,7 @@ describe("createAgentExecution", () => {
|
|
|
113
117
|
serviceTier: "standard",
|
|
114
118
|
thinking: "",
|
|
115
119
|
autoApproveAll: false,
|
|
120
|
+
harness: "",
|
|
116
121
|
});
|
|
117
122
|
expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.STANDARD);
|
|
118
123
|
});
|
|
@@ -132,6 +137,7 @@ describe("createAgentExecution", () => {
|
|
|
132
137
|
serviceTier: "",
|
|
133
138
|
thinking: "enabled",
|
|
134
139
|
autoApproveAll: false,
|
|
140
|
+
harness: "",
|
|
135
141
|
});
|
|
136
142
|
expect(exec.spec?.executionConfig?.thinkingMode).toBe(ThinkingMode.ENABLED);
|
|
137
143
|
});
|
|
@@ -153,6 +159,7 @@ describe("createAgentExecution", () => {
|
|
|
153
159
|
serviceTier: "",
|
|
154
160
|
thinking: "disabled",
|
|
155
161
|
autoApproveAll: false,
|
|
162
|
+
harness: "",
|
|
156
163
|
});
|
|
157
164
|
expect(exec.spec?.executionConfig?.thinkingMode).toBe(ThinkingMode.DISABLED);
|
|
158
165
|
});
|
|
@@ -172,6 +179,7 @@ describe("createAgentExecution", () => {
|
|
|
172
179
|
serviceTier: "",
|
|
173
180
|
thinking: "",
|
|
174
181
|
autoApproveAll: false,
|
|
182
|
+
harness: "",
|
|
175
183
|
});
|
|
176
184
|
expect(exec.spec?.sessionId).toBe("ses_1");
|
|
177
185
|
expect(exec.spec?.executionConfig?.interactionMode).toBe(InteractionMode.UNSPECIFIED);
|
|
@@ -199,6 +207,7 @@ describe("createAgentExecution", () => {
|
|
|
199
207
|
serviceTier: "",
|
|
200
208
|
thinking: "",
|
|
201
209
|
autoApproveAll: false,
|
|
210
|
+
harness: "",
|
|
202
211
|
});
|
|
203
212
|
|
|
204
213
|
expect(exec.spec?.sessionId).toBe("");
|
|
@@ -206,9 +215,16 @@ describe("createAgentExecution", () => {
|
|
|
206
215
|
// Subject stays empty so the server defaults its sentinel and the async
|
|
207
216
|
// title activity generates a real one.
|
|
208
217
|
expect(exec.spec?.sessionSpec?.subject).toBe("");
|
|
218
|
+
// No harness opinion: the field stays UNSPECIFIED so the server default
|
|
219
|
+
// (native) applies — a workspace-only run is byte-identical to before
|
|
220
|
+
// the --harness flag existed.
|
|
221
|
+
expect(exec.spec?.sessionSpec?.harness).toBe(Harness.UNSPECIFIED);
|
|
209
222
|
});
|
|
210
223
|
|
|
211
|
-
it("omits session_spec when there are no workspace entries", async () => {
|
|
224
|
+
it("omits session_spec when there are no workspace entries and no harness (wire-shape regression pin)", async () => {
|
|
225
|
+
// The pre---harness wire shape for a plain run: NO embedded session_spec
|
|
226
|
+
// at all. If this pin fails, plain runs have started carrying a spec they
|
|
227
|
+
// never carried before — a silent contract change, not a feature.
|
|
212
228
|
const { fn } = fakeController();
|
|
213
229
|
const exec = await createAgentExecution(fn, {
|
|
214
230
|
agentId: "agt_1",
|
|
@@ -223,9 +239,84 @@ describe("createAgentExecution", () => {
|
|
|
223
239
|
serviceTier: "",
|
|
224
240
|
thinking: "",
|
|
225
241
|
autoApproveAll: false,
|
|
242
|
+
harness: "",
|
|
226
243
|
});
|
|
227
244
|
expect(exec.spec?.sessionSpec).toBeUndefined();
|
|
228
245
|
});
|
|
246
|
+
|
|
247
|
+
it("stamps cursor harness on a session_spec created just for it (oss#293)", async () => {
|
|
248
|
+
const { fn } = fakeController();
|
|
249
|
+
const exec = await createAgentExecution(fn, {
|
|
250
|
+
agentId: "agt_1",
|
|
251
|
+
orgId: "acme",
|
|
252
|
+
message: "hi",
|
|
253
|
+
runtimeEnv: {},
|
|
254
|
+
attachments: [],
|
|
255
|
+
workspaceFileRefs: [],
|
|
256
|
+
workspaceEntries: [],
|
|
257
|
+
model: "",
|
|
258
|
+
mode: "",
|
|
259
|
+
serviceTier: "",
|
|
260
|
+
thinking: "",
|
|
261
|
+
autoApproveAll: false,
|
|
262
|
+
harness: "cursor",
|
|
263
|
+
});
|
|
264
|
+
// Harness alone justifies the embedded spec: the server clones it onto
|
|
265
|
+
// the auto-created session (the one-call bootstrap contract).
|
|
266
|
+
expect(exec.spec?.sessionSpec?.harness).toBe(Harness.CURSOR);
|
|
267
|
+
expect(exec.spec?.sessionSpec?.workspaceEntries).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("stamps an explicit native harness rather than leaving it unspecified", async () => {
|
|
271
|
+
// "native" may be a deliberate per-run escape from the account's
|
|
272
|
+
// default_harness preference — it must survive on the wire so it beats
|
|
273
|
+
// any future change to the server-side default (D3).
|
|
274
|
+
const { fn } = fakeController();
|
|
275
|
+
const exec = await createAgentExecution(fn, {
|
|
276
|
+
agentId: "agt_1",
|
|
277
|
+
orgId: "acme",
|
|
278
|
+
message: "hi",
|
|
279
|
+
runtimeEnv: {},
|
|
280
|
+
attachments: [],
|
|
281
|
+
workspaceFileRefs: [],
|
|
282
|
+
workspaceEntries: [],
|
|
283
|
+
model: "",
|
|
284
|
+
mode: "",
|
|
285
|
+
serviceTier: "",
|
|
286
|
+
thinking: "",
|
|
287
|
+
autoApproveAll: false,
|
|
288
|
+
harness: "native",
|
|
289
|
+
});
|
|
290
|
+
expect(exec.spec?.sessionSpec?.harness).toBe(Harness.NATIVE);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("carries harness and workspace entries together on one session_spec", async () => {
|
|
294
|
+
const entry = create(WorkspaceEntrySchema, {
|
|
295
|
+
name: "repo",
|
|
296
|
+
source: create(WorkspaceSourceSchema, {
|
|
297
|
+
source: { case: "localPath", value: create(LocalPathSourceSchema, { path: "/home/user/repo" }) },
|
|
298
|
+
}),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const { fn } = fakeController();
|
|
302
|
+
const exec = await createAgentExecution(fn, {
|
|
303
|
+
agentId: "agt_1",
|
|
304
|
+
orgId: "acme",
|
|
305
|
+
message: "hi",
|
|
306
|
+
runtimeEnv: {},
|
|
307
|
+
attachments: [],
|
|
308
|
+
workspaceFileRefs: [],
|
|
309
|
+
workspaceEntries: [entry],
|
|
310
|
+
model: "",
|
|
311
|
+
mode: "",
|
|
312
|
+
serviceTier: "",
|
|
313
|
+
thinking: "",
|
|
314
|
+
autoApproveAll: false,
|
|
315
|
+
harness: "cursor",
|
|
316
|
+
});
|
|
317
|
+
expect(exec.spec?.sessionSpec?.harness).toBe(Harness.CURSOR);
|
|
318
|
+
expect(exec.spec?.sessionSpec?.workspaceEntries).toEqual([entry]);
|
|
319
|
+
});
|
|
229
320
|
});
|
|
230
321
|
|
|
231
322
|
describe("createWorkflowExecution", () => {
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
ExecutionConfigSchema,
|
|
19
19
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
20
20
|
import type { ExecutionValue } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
|
|
21
|
-
import {
|
|
21
|
+
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
22
|
+
import { type SessionSpec, SessionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
|
|
22
23
|
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
23
24
|
import {
|
|
24
25
|
type WorkflowExecution,
|
|
@@ -29,7 +30,7 @@ import { WorkflowExecutionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/
|
|
|
29
30
|
import { ExecutionValueSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
|
|
30
31
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
31
32
|
import type { RuntimeEnv } from "./env.js";
|
|
32
|
-
import type { RunMode, ServiceTierFlag, ThinkingFlag } from "./prepare.js";
|
|
33
|
+
import type { HarnessFlag, RunMode, ServiceTierFlag, ThinkingFlag } from "./prepare.js";
|
|
33
34
|
|
|
34
35
|
const API_VERSION = "agentic.stigmer.ai/v1";
|
|
35
36
|
|
|
@@ -60,6 +61,7 @@ export interface CreateAgentExecutionInput {
|
|
|
60
61
|
readonly serviceTier: ServiceTierFlag;
|
|
61
62
|
readonly thinking: ThinkingFlag;
|
|
62
63
|
readonly autoApproveAll: boolean;
|
|
64
|
+
readonly harness: HarnessFlag;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/** Create an agent execution. Mirrors Go's createAgentExecution. */
|
|
@@ -81,10 +83,7 @@ export async function createAgentExecution(
|
|
|
81
83
|
agentId: input.agentId ?? "",
|
|
82
84
|
// Subject is left empty: the server defaults its sentinel and the async
|
|
83
85
|
// title activity replaces it, same as any auto-created session.
|
|
84
|
-
sessionSpec:
|
|
85
|
-
input.workspaceEntries.length > 0
|
|
86
|
-
? create(SessionSpecSchema, { workspaceEntries: [...input.workspaceEntries] })
|
|
87
|
-
: undefined,
|
|
86
|
+
sessionSpec: buildSessionSpec(input.workspaceEntries, input.harness),
|
|
88
87
|
executionConfig: buildExecutionConfig(input.model, input.mode, input.serviceTier, input.thinking),
|
|
89
88
|
}),
|
|
90
89
|
});
|
|
@@ -121,6 +120,26 @@ export async function createWorkflowExecution(
|
|
|
121
120
|
return controller(WorkflowExecutionCommandController).create(execution);
|
|
122
121
|
}
|
|
123
122
|
|
|
123
|
+
// Build the embedded session spec for the one-call bootstrap
|
|
124
|
+
// (stigmer/stigmer#249), or undefined when there is nothing to carry — the
|
|
125
|
+
// pre-existing wire shape for a plain run must stay byte-identical. A resolved
|
|
126
|
+
// harness is stamped explicitly, including "native": the value may be a
|
|
127
|
+
// deliberate per-run escape from the account's default_harness preference, so
|
|
128
|
+
// it must survive any future change to the server-side default. Empty means
|
|
129
|
+
// "no opinion" and stays off the wire (server defaults to native). The server
|
|
130
|
+
// clones this spec onto the auto-created session and then clears it from the
|
|
131
|
+
// persisted execution — the Session resource stays the single source of truth.
|
|
132
|
+
function buildSessionSpec(
|
|
133
|
+
workspaceEntries: readonly WorkspaceEntry[],
|
|
134
|
+
harness: HarnessFlag,
|
|
135
|
+
): SessionSpec | undefined {
|
|
136
|
+
if (workspaceEntries.length === 0 && harness === "") return undefined;
|
|
137
|
+
const spec = create(SessionSpecSchema, { workspaceEntries: [...workspaceEntries] });
|
|
138
|
+
if (harness === "cursor") spec.harness = Harness.CURSOR;
|
|
139
|
+
else if (harness === "native") spec.harness = Harness.NATIVE;
|
|
140
|
+
return spec;
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
// Build ExecutionConfig, or undefined when no flag is set so the backend
|
|
125
144
|
// applies its defaults. Mirrors Go's buildExecutionConfig (only "plan" maps to a
|
|
126
145
|
// non-default InteractionMode; "agent"/"" leave it unspecified). An explicit
|
|
@@ -31,6 +31,24 @@ describe("renderSessionHeader", () => {
|
|
|
31
31
|
expect(agent.text()).not.toContain("Mode:");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
it("surfaces the cursor harness but not native/default (oss#293 D2 visibility)", () => {
|
|
35
|
+
// A cursor session may have been selected by the account preference, not
|
|
36
|
+
// a flag — the header is the CLI's only pre-stream channel to say so.
|
|
37
|
+
const cursor = capture();
|
|
38
|
+
renderSessionHeader(cursor.out, { agentName: "", sessionId: "s", model: "", mode: "", harness: "cursor", workspaces: [] });
|
|
39
|
+
expect(cursor.text()).toContain("Harness:");
|
|
40
|
+
expect(cursor.text()).toContain("Cursor");
|
|
41
|
+
|
|
42
|
+
// Native (explicit or defaulted) stays implicit, like agent mode.
|
|
43
|
+
const native = capture();
|
|
44
|
+
renderSessionHeader(native.out, { agentName: "", sessionId: "s", model: "", mode: "", harness: "native", workspaces: [] });
|
|
45
|
+
expect(native.text()).not.toContain("Harness:");
|
|
46
|
+
|
|
47
|
+
const unset = capture();
|
|
48
|
+
renderSessionHeader(unset.out, { agentName: "", sessionId: "s", model: "", mode: "", workspaces: [] });
|
|
49
|
+
expect(unset.text()).not.toContain("Harness:");
|
|
50
|
+
});
|
|
51
|
+
|
|
34
52
|
it("indents extra workspaces under the first", () => {
|
|
35
53
|
const { out, text } = capture();
|
|
36
54
|
renderSessionHeader(out, {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
13
13
|
import { shouldColorize, styler } from "../../output/style.js";
|
|
14
|
-
import type { RunMode } from "./prepare.js";
|
|
14
|
+
import type { HarnessFlag, RunMode } from "./prepare.js";
|
|
15
15
|
|
|
16
16
|
export interface SessionHeaderInfo {
|
|
17
17
|
readonly agentName: string;
|
|
@@ -20,6 +20,14 @@ export interface SessionHeaderInfo {
|
|
|
20
20
|
readonly subject?: string;
|
|
21
21
|
readonly model: string;
|
|
22
22
|
readonly mode: RunMode;
|
|
23
|
+
/**
|
|
24
|
+
* Resolved harness for the session. Surfaced only when it is "cursor":
|
|
25
|
+
* a harness switch changes the toolset, conversation-state model, and
|
|
26
|
+
* billing tier, and it may come from the account preference rather than
|
|
27
|
+
* a flag — a cursor session must never start silently (the CLI
|
|
28
|
+
* counterpart of the web composer's harness selector).
|
|
29
|
+
*/
|
|
30
|
+
readonly harness?: HarnessFlag;
|
|
23
31
|
readonly workspaces: readonly string[];
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -39,6 +47,8 @@ export function renderSessionHeader(out: { write(chunk: string): unknown; isTTY?
|
|
|
39
47
|
if (info.sessionId !== "") lines.push(row(s, "Session", info.sessionId));
|
|
40
48
|
if ((info.subject ?? "") !== "") lines.push(row(s, "Subject", info.subject ?? ""));
|
|
41
49
|
if (info.model !== "") lines.push(row(s, "Model", info.model));
|
|
50
|
+
// Only "cursor" is surfaced; native/"" is the default and stays implicit.
|
|
51
|
+
if (info.harness === "cursor") lines.push(row(s, "Harness", "Cursor"));
|
|
42
52
|
// Only "plan" is surfaced; "agent"/"" is the default and stays implicit.
|
|
43
53
|
if (info.mode === "plan") lines.push(row(s, "Mode", "Plan (read-only)"));
|
|
44
54
|
if (info.workspaces.length > 0) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
7
|
import type { Stigmer } from "@stigmer/sdk";
|
|
8
|
-
import { type AgentExecFlags, parseApprovalAction, prepareAgentExec, validateMode, validateServiceTier, validateThinking } from "./prepare.js";
|
|
8
|
+
import { type AgentExecFlags, parseApprovalAction, prepareAgentExec, validateHarness, validateMode, validateServiceTier, validateThinking } from "./prepare.js";
|
|
9
9
|
|
|
10
10
|
describe("parseApprovalAction", () => {
|
|
11
11
|
it("maps each accepted value (case-insensitive)", () => {
|
|
@@ -59,6 +59,24 @@ describe("validateThinking", () => {
|
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
describe("validateHarness", () => {
|
|
63
|
+
it("accepts empty, native, and cursor", () => {
|
|
64
|
+
expect(() => validateHarness("")).not.toThrow();
|
|
65
|
+
expect(() => validateHarness("native")).not.toThrow();
|
|
66
|
+
expect(() => validateHarness("cursor")).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("rejects anything else before a network round trip", () => {
|
|
70
|
+
expect(() => validateHarness("turbo")).toThrow(/must be "native" or "cursor"/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rejects UI-only harness names that have no proto mapping", () => {
|
|
74
|
+
// copilot/codex/etc. exist in display metadata but not on the wire —
|
|
75
|
+
// accepting them here would stamp UNSPECIFIED and silently run native.
|
|
76
|
+
expect(() => validateHarness("copilot")).toThrow(/must be "native" or "cursor"/);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
62
80
|
const BASE_FLAGS: AgentExecFlags = {
|
|
63
81
|
message: "hi",
|
|
64
82
|
attach: [],
|
|
@@ -77,23 +95,35 @@ const BASE_FLAGS: AgentExecFlags = {
|
|
|
77
95
|
mode: "",
|
|
78
96
|
serviceTier: "",
|
|
79
97
|
thinking: "",
|
|
98
|
+
harness: "",
|
|
80
99
|
};
|
|
81
100
|
|
|
82
101
|
// prepareAgentExec only touches client.agentExecution for attachment uploads;
|
|
83
102
|
// with no attachments a bare stub suffices.
|
|
84
103
|
const STUB_CLIENT = { agentExecution: {} } as unknown as Stigmer;
|
|
85
104
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
105
|
+
/** The shape of IdentityAccountPreferences the stubs below expose. */
|
|
106
|
+
interface StubPreferences {
|
|
107
|
+
readonly defaultHarness?: string;
|
|
108
|
+
readonly defaultNativeModel?: string;
|
|
109
|
+
readonly defaultCursorModel?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Stub whose whoAmI resolves an account carrying the given preferences. */
|
|
113
|
+
function clientWithPreferences(preferences: StubPreferences): Stigmer {
|
|
88
114
|
return {
|
|
89
115
|
agentExecution: {},
|
|
90
116
|
identityAccount: {
|
|
91
|
-
whoAmI: () =>
|
|
92
|
-
Promise.resolve({ spec: { preferences: { defaultNativeModel } } }),
|
|
117
|
+
whoAmI: () => Promise.resolve({ spec: { preferences } }),
|
|
93
118
|
},
|
|
94
119
|
} as unknown as Stigmer;
|
|
95
120
|
}
|
|
96
121
|
|
|
122
|
+
/** Back-compat shorthand for the original native-model-only stub. */
|
|
123
|
+
function clientWithPreference(defaultNativeModel: string): Stigmer {
|
|
124
|
+
return clientWithPreferences({ defaultNativeModel });
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
/** Stub whose whoAmI rejects (auth failure, backend without the RPC, ...). */
|
|
98
128
|
function clientWithFailingWhoAmI(): Stigmer {
|
|
99
129
|
return {
|
|
@@ -104,6 +134,24 @@ function clientWithFailingWhoAmI(): Stigmer {
|
|
|
104
134
|
} as unknown as Stigmer;
|
|
105
135
|
}
|
|
106
136
|
|
|
137
|
+
/** Preference stub that also counts whoAmI invocations (round-trip pin). */
|
|
138
|
+
function countingClient(preferences: StubPreferences): { client: Stigmer; calls: () => number } {
|
|
139
|
+
let calls = 0;
|
|
140
|
+
const client = {
|
|
141
|
+
agentExecution: {},
|
|
142
|
+
identityAccount: {
|
|
143
|
+
whoAmI: () => {
|
|
144
|
+
calls += 1;
|
|
145
|
+
return Promise.resolve({ spec: { preferences } });
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
} as unknown as Stigmer;
|
|
149
|
+
return { client, calls: () => calls };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** prepareAgentExec options as `run` passes them: cloud + harness opt-in. */
|
|
153
|
+
const CLOUD_RUN_OPTIONS = { cloudBackend: true, applyAccountHarnessDefault: true } as const;
|
|
154
|
+
|
|
107
155
|
describe("prepareAgentExec env injection", () => {
|
|
108
156
|
it("injects STIGMER_ORG_ID when absent", async () => {
|
|
109
157
|
const prepared = await prepareAgentExec(BASE_FLAGS, STUB_CLIENT, "acme");
|
|
@@ -196,3 +244,197 @@ describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", (
|
|
|
196
244
|
expect(prepared.model).toBe("");
|
|
197
245
|
});
|
|
198
246
|
});
|
|
247
|
+
|
|
248
|
+
describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
249
|
+
it("fills an omitted --harness from the account preference on cloud when the caller opts in (run)", async () => {
|
|
250
|
+
const prepared = await prepareAgentExec(
|
|
251
|
+
BASE_FLAGS,
|
|
252
|
+
clientWithPreferences({ defaultHarness: "cursor" }),
|
|
253
|
+
"acme",
|
|
254
|
+
undefined,
|
|
255
|
+
CLOUD_RUN_OPTIONS,
|
|
256
|
+
);
|
|
257
|
+
expect(prepared.harness).toBe("cursor");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("an explicit --harness always outranks the account preference", async () => {
|
|
261
|
+
// The per-run escape hatch: a cursor-preference user can still force a
|
|
262
|
+
// native run without touching their settings.
|
|
263
|
+
const prepared = await prepareAgentExec(
|
|
264
|
+
{ ...BASE_FLAGS, harness: "native" },
|
|
265
|
+
clientWithPreferences({ defaultHarness: "cursor" }),
|
|
266
|
+
"acme",
|
|
267
|
+
undefined,
|
|
268
|
+
CLOUD_RUN_OPTIONS,
|
|
269
|
+
);
|
|
270
|
+
expect(prepared.harness).toBe("native");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("ignores the preference when the caller has not opted in (draft, D5)", async () => {
|
|
274
|
+
// draft runs the seedpack's system creator agents — a utility flow that
|
|
275
|
+
// must not be silently rerouted onto the cursor engine by a preference
|
|
276
|
+
// meant for the user's own sessions (owner-ratified D5).
|
|
277
|
+
const prepared = await prepareAgentExec(
|
|
278
|
+
BASE_FLAGS,
|
|
279
|
+
clientWithPreferences({ defaultHarness: "cursor" }),
|
|
280
|
+
"acme",
|
|
281
|
+
undefined,
|
|
282
|
+
{ cloudBackend: true },
|
|
283
|
+
);
|
|
284
|
+
expect(prepared.harness).toBe("");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("an explicit --harness still resolves for a non-opted-in caller (draft escape hatch)", async () => {
|
|
288
|
+
const prepared = await prepareAgentExec(
|
|
289
|
+
{ ...BASE_FLAGS, harness: "cursor" },
|
|
290
|
+
STUB_CLIENT,
|
|
291
|
+
"acme",
|
|
292
|
+
undefined,
|
|
293
|
+
{ cloudBackend: false },
|
|
294
|
+
);
|
|
295
|
+
expect(prepared.harness).toBe("cursor");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("treats a malformed persisted harness as undeclared", async () => {
|
|
299
|
+
// A client must not trust stored data it did not write: anything outside
|
|
300
|
+
// the shipped allowlist resolves as if no preference existed.
|
|
301
|
+
const prepared = await prepareAgentExec(
|
|
302
|
+
BASE_FLAGS,
|
|
303
|
+
clientWithPreferences({ defaultHarness: "devin" }),
|
|
304
|
+
"acme",
|
|
305
|
+
undefined,
|
|
306
|
+
CLOUD_RUN_OPTIONS,
|
|
307
|
+
);
|
|
308
|
+
expect(prepared.harness).toBe("");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("resolves empty when whoAmI fails — a missing preference never fails a run", async () => {
|
|
312
|
+
const prepared = await prepareAgentExec(
|
|
313
|
+
BASE_FLAGS,
|
|
314
|
+
clientWithFailingWhoAmI(),
|
|
315
|
+
"acme",
|
|
316
|
+
undefined,
|
|
317
|
+
CLOUD_RUN_OPTIONS,
|
|
318
|
+
);
|
|
319
|
+
expect(prepared.harness).toBe("");
|
|
320
|
+
expect(prepared.model).toBe("");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("never consults identity on a local backend, even with the opt-in", async () => {
|
|
324
|
+
// The failing stub doubles as a call detector, as in the model-fill suite.
|
|
325
|
+
const prepared = await prepareAgentExec(
|
|
326
|
+
BASE_FLAGS,
|
|
327
|
+
clientWithFailingWhoAmI(),
|
|
328
|
+
"acme",
|
|
329
|
+
undefined,
|
|
330
|
+
{ cloudBackend: false, applyAccountHarnessDefault: true },
|
|
331
|
+
);
|
|
332
|
+
expect(prepared.harness).toBe("");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("resolves empty when nothing is declared anywhere", async () => {
|
|
336
|
+
const prepared = await prepareAgentExec(
|
|
337
|
+
BASE_FLAGS,
|
|
338
|
+
clientWithPreferences({}),
|
|
339
|
+
"acme",
|
|
340
|
+
undefined,
|
|
341
|
+
CLOUD_RUN_OPTIONS,
|
|
342
|
+
);
|
|
343
|
+
expect(prepared.harness).toBe("");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
348
|
+
const BOTH_MODELS: StubPreferences = {
|
|
349
|
+
defaultNativeModel: "claude-sonnet-4.6",
|
|
350
|
+
defaultCursorModel: "composer-2.5",
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
it("fills the cursor model when the explicit harness is cursor", async () => {
|
|
354
|
+
const prepared = await prepareAgentExec(
|
|
355
|
+
{ ...BASE_FLAGS, harness: "cursor" },
|
|
356
|
+
clientWithPreferences(BOTH_MODELS),
|
|
357
|
+
"acme",
|
|
358
|
+
undefined,
|
|
359
|
+
CLOUD_RUN_OPTIONS,
|
|
360
|
+
);
|
|
361
|
+
expect(prepared.model).toBe("composer-2.5");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("fills the cursor model when the preference resolves the harness to cursor", async () => {
|
|
365
|
+
const prepared = await prepareAgentExec(
|
|
366
|
+
BASE_FLAGS,
|
|
367
|
+
clientWithPreferences({ ...BOTH_MODELS, defaultHarness: "cursor" }),
|
|
368
|
+
"acme",
|
|
369
|
+
undefined,
|
|
370
|
+
CLOUD_RUN_OPTIONS,
|
|
371
|
+
);
|
|
372
|
+
expect(prepared.harness).toBe("cursor");
|
|
373
|
+
expect(prepared.model).toBe("composer-2.5");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("fills the native model when the harness resolves native or empty", async () => {
|
|
377
|
+
const explicit = await prepareAgentExec(
|
|
378
|
+
{ ...BASE_FLAGS, harness: "native" },
|
|
379
|
+
clientWithPreferences(BOTH_MODELS),
|
|
380
|
+
"acme",
|
|
381
|
+
undefined,
|
|
382
|
+
CLOUD_RUN_OPTIONS,
|
|
383
|
+
);
|
|
384
|
+
expect(explicit.model).toBe("claude-sonnet-4.6");
|
|
385
|
+
|
|
386
|
+
const unset = await prepareAgentExec(
|
|
387
|
+
BASE_FLAGS,
|
|
388
|
+
clientWithPreferences(BOTH_MODELS),
|
|
389
|
+
"acme",
|
|
390
|
+
undefined,
|
|
391
|
+
CLOUD_RUN_OPTIONS,
|
|
392
|
+
);
|
|
393
|
+
expect(unset.model).toBe("claude-sonnet-4.6");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("an explicit --model outranks the harness-matched preference", async () => {
|
|
397
|
+
const prepared = await prepareAgentExec(
|
|
398
|
+
{ ...BASE_FLAGS, harness: "cursor", model: "gpt-5.3" },
|
|
399
|
+
clientWithPreferences(BOTH_MODELS),
|
|
400
|
+
"acme",
|
|
401
|
+
undefined,
|
|
402
|
+
CLOUD_RUN_OPTIONS,
|
|
403
|
+
);
|
|
404
|
+
expect(prepared.model).toBe("gpt-5.3");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("draft with an explicit --harness cursor fills the cursor model despite no harness opt-in (D5)", async () => {
|
|
408
|
+
// D5 scopes only the silent preference fill; the model fill follows
|
|
409
|
+
// whatever harness the user explicitly chose.
|
|
410
|
+
const prepared = await prepareAgentExec(
|
|
411
|
+
{ ...BASE_FLAGS, harness: "cursor" },
|
|
412
|
+
clientWithPreferences(BOTH_MODELS),
|
|
413
|
+
"acme",
|
|
414
|
+
undefined,
|
|
415
|
+
{ cloudBackend: true },
|
|
416
|
+
);
|
|
417
|
+
expect(prepared.harness).toBe("cursor");
|
|
418
|
+
expect(prepared.model).toBe("composer-2.5");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("serves both fills from one whoAmI round trip, and none when both flags are explicit", async () => {
|
|
422
|
+
const single = countingClient({ ...BOTH_MODELS, defaultHarness: "cursor" });
|
|
423
|
+
const filled = await prepareAgentExec(BASE_FLAGS, single.client, "acme", undefined, CLOUD_RUN_OPTIONS);
|
|
424
|
+
expect(filled.harness).toBe("cursor");
|
|
425
|
+
expect(filled.model).toBe("composer-2.5");
|
|
426
|
+
expect(single.calls(), "harness + model fills share one whoAmI").toBe(1);
|
|
427
|
+
|
|
428
|
+
const skipped = countingClient(BOTH_MODELS);
|
|
429
|
+
const explicit = await prepareAgentExec(
|
|
430
|
+
{ ...BASE_FLAGS, harness: "native", model: "gpt-5.3" },
|
|
431
|
+
skipped.client,
|
|
432
|
+
"acme",
|
|
433
|
+
undefined,
|
|
434
|
+
CLOUD_RUN_OPTIONS,
|
|
435
|
+
);
|
|
436
|
+
expect(explicit.harness).toBe("native");
|
|
437
|
+
expect(explicit.model).toBe("gpt-5.3");
|
|
438
|
+
expect(skipped.calls(), "explicit flags leave nothing to fill — no round trip").toBe(0);
|
|
439
|
+
});
|
|
440
|
+
});
|