@stigmer/cli 3.12.8 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/commands/up.d.ts.map +1 -1
- package/commands/up.js +22 -6
- package/commands/up.js.map +1 -1
- package/config/resolve.d.ts +5 -2
- package/config/resolve.d.ts.map +1 -1
- package/config/resolve.js +7 -4
- package/config/resolve.js.map +1 -1
- package/local/constants.d.ts +8 -3
- package/local/constants.d.ts.map +1 -1
- package/local/constants.js +8 -3
- package/local/constants.js.map +1 -1
- package/local/daemon/components.d.ts.map +1 -1
- package/local/daemon/components.js +4 -2
- package/local/daemon/components.js.map +1 -1
- package/local/daemon/env.d.ts +16 -3
- package/local/daemon/env.d.ts.map +1 -1
- package/local/daemon/env.js +16 -3
- package/local/daemon/env.js.map +1 -1
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +5 -4
- package/local/daemon/launch.js.map +1 -1
- package/local/daemon/process.d.ts.map +1 -1
- package/local/daemon/process.js +8 -5
- package/local/daemon/process.js.map +1 -1
- package/local/runtime/index.d.ts +2 -2
- package/local/runtime/index.d.ts.map +1 -1
- package/local/runtime/index.js +4 -2
- package/local/runtime/index.js.map +1 -1
- package/local/runtime/node.d.ts +8 -0
- package/local/runtime/node.d.ts.map +1 -1
- package/local/runtime/node.js +74 -23
- package/local/runtime/node.js.map +1 -1
- package/local/runtime/runner.d.ts +2 -1
- package/local/runtime/runner.d.ts.map +1 -1
- package/local/runtime/runner.js +4 -31
- package/local/runtime/runner.js.map +1 -1
- package/local/runtime/runtimes-install.d.ts +13 -0
- package/local/runtime/runtimes-install.d.ts.map +1 -0
- package/local/runtime/runtimes-install.js +60 -0
- package/local/runtime/runtimes-install.js.map +1 -0
- package/local/runtime/server.d.ts +26 -30
- package/local/runtime/server.d.ts.map +1 -1
- package/local/runtime/server.js +106 -108
- package/local/runtime/server.js.map +1 -1
- package/local/status.d.ts.map +1 -1
- package/local/status.js +4 -2
- package/local/status.js.map +1 -1
- package/local/webconsole/index.d.ts +2 -3
- package/local/webconsole/index.d.ts.map +1 -1
- package/local/webconsole/index.js +24 -10
- package/local/webconsole/index.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/commands/up.ts +24 -6
- package/src/config/resolve.test.ts +1 -1
- package/src/config/resolve.ts +7 -4
- package/src/local/constants.ts +8 -4
- package/src/local/daemon/components.test.ts +9 -1
- package/src/local/daemon/components.ts +4 -2
- package/src/local/daemon/daemon.integration.test.ts +1 -1
- package/src/local/daemon/env.test.ts +15 -4
- package/src/local/daemon/env.ts +33 -5
- package/src/local/daemon/launch.ts +5 -4
- package/src/local/daemon/process.ts +7 -4
- package/src/local/runtime/index.ts +6 -5
- package/src/local/runtime/node.ts +99 -23
- package/src/local/runtime/runner.ts +5 -36
- package/src/local/runtime/runtime.test.ts +58 -14
- package/src/local/runtime/runtimes-install.test.ts +56 -0
- package/src/local/runtime/runtimes-install.ts +72 -0
- package/src/local/runtime/server.test.ts +178 -114
- package/src/local/runtime/server.ts +149 -129
- package/src/local/status.ts +4 -2
- package/src/local/webconsole/index.ts +27 -10
- package/src/resources/connect/oauth.test.ts +1 -1
- 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
- package/src/resources/share.test.ts +2 -2
|
@@ -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
|
+
});
|
|
@@ -32,6 +32,15 @@ export type ServiceTierFlag = "" | "standard" | "fast";
|
|
|
32
32
|
*/
|
|
33
33
|
export type ThinkingFlag = "" | "disabled" | "enabled";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The `--harness` flag's value space (oss#293): empty means unset — the
|
|
37
|
+
* account preference may fill it (see {@link prepareAgentExec}), and when
|
|
38
|
+
* nothing resolves the server defaults the session to native. Deliberately
|
|
39
|
+
* only the two shipped engines, mirroring the proto validation on
|
|
40
|
+
* `IdentityAccountPreferences.default_harness`.
|
|
41
|
+
*/
|
|
42
|
+
export type HarnessFlag = "" | "native" | "cursor";
|
|
43
|
+
|
|
35
44
|
/** Raw agent-execution flags shared by `run` and `draft` (Go's agentExecFlags). */
|
|
36
45
|
export interface AgentExecFlags {
|
|
37
46
|
readonly message: string;
|
|
@@ -51,6 +60,7 @@ export interface AgentExecFlags {
|
|
|
51
60
|
readonly mode: RunMode;
|
|
52
61
|
readonly serviceTier: ServiceTierFlag;
|
|
53
62
|
readonly thinking: ThinkingFlag;
|
|
63
|
+
readonly harness: HarnessFlag;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/**
|
|
@@ -71,6 +81,13 @@ export interface PreparedRun {
|
|
|
71
81
|
readonly mode: RunMode;
|
|
72
82
|
readonly serviceTier: ServiceTierFlag;
|
|
73
83
|
readonly thinking: ThinkingFlag;
|
|
84
|
+
/**
|
|
85
|
+
* The RESOLVED harness for the new session: the explicit flag, else the
|
|
86
|
+
* account preference (when the caller opted in), else "" — which stays off
|
|
87
|
+
* the wire so the server default (native) applies. Also drives the header's
|
|
88
|
+
* visibility row: a cursor session is never entered silently.
|
|
89
|
+
*/
|
|
90
|
+
readonly harness: HarnessFlag;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
/** Optional behavior switches for {@link prepareAgentExec}. */
|
|
@@ -78,11 +95,24 @@ export interface PrepareAgentExecOptions {
|
|
|
78
95
|
/**
|
|
79
96
|
* When `true` (the caller's backend is Stigmer Cloud) and `--model` is
|
|
80
97
|
* omitted, the model is filled from the caller's account preference
|
|
81
|
-
* (`IdentityAccountPreferences.
|
|
82
|
-
* Local mode has no IdentityAccount, so callers pass
|
|
83
|
-
* the omitted model keeps resolving to the platform
|
|
98
|
+
* (`IdentityAccountPreferences.default_*_model` for the resolved harness)
|
|
99
|
+
* via `whoAmI()`. Local mode has no IdentityAccount, so callers pass
|
|
100
|
+
* `false` there and the omitted model keeps resolving to the platform
|
|
101
|
+
* default.
|
|
84
102
|
*/
|
|
85
103
|
readonly cloudBackend?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* When `true` and `--harness` is omitted, the harness is filled from the
|
|
106
|
+
* caller's account preference (`IdentityAccountPreferences.default_harness`)
|
|
107
|
+
* on cloud. `run` opts in; `draft` deliberately does not: draft executes the
|
|
108
|
+
* seedpack's system creator agents (a utility flow, not the user's own
|
|
109
|
+
* conversation), which are built and exercised on the native harness —
|
|
110
|
+
* silently rerouting them onto the cursor engine (different toolset,
|
|
111
|
+
* premium billing tier) would fail the least-surprise test. An explicit
|
|
112
|
+
* `--harness` on draft still wins; only the silent preference fill is
|
|
113
|
+
* scoped out. Owner-ratified (D5, 2026-08-23).
|
|
114
|
+
*/
|
|
115
|
+
readonly applyAccountHarnessDefault?: boolean;
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
/**
|
|
@@ -100,16 +130,28 @@ export async function prepareAgentExec(
|
|
|
100
130
|
validateMode(flags.mode);
|
|
101
131
|
validateServiceTier(flags.serviceTier);
|
|
102
132
|
validateThinking(flags.thinking);
|
|
133
|
+
validateHarness(flags.harness);
|
|
103
134
|
|
|
104
|
-
// Layered
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
// Layered seeds (oss#293, DD-003): explicit flag > account preference
|
|
136
|
+
// (cloud only) > platform default. Harness resolves first because the model
|
|
137
|
+
// fill is harness-aware: a cursor session fills from default_cursor_model,
|
|
138
|
+
// everything else from default_native_model. `run` and `draft` always
|
|
139
|
+
// create a NEW session (threading lives in `resume`, which does not pass
|
|
140
|
+
// through here), so neither fill can ever contradict an existing session's
|
|
141
|
+
// immutable harness. One whoAmI round trip serves both fills, and it is
|
|
142
|
+
// skipped entirely when explicit flags leave nothing to fill.
|
|
143
|
+
const wantsHarnessFill = flags.harness === "" && options?.applyAccountHarnessDefault === true;
|
|
144
|
+
const wantsModelFill = flags.model === "";
|
|
145
|
+
const accountDefaults =
|
|
146
|
+
options?.cloudBackend === true && (wantsHarnessFill || wantsModelFill)
|
|
147
|
+
? await resolveAccountExecutionDefaults(client)
|
|
148
|
+
: NO_ACCOUNT_DEFAULTS;
|
|
149
|
+
const harness = flags.harness !== "" ? flags.harness : wantsHarnessFill ? accountDefaults.harness : "";
|
|
150
|
+
const model = wantsModelFill
|
|
151
|
+
? harness === "cursor"
|
|
152
|
+
? accountDefaults.cursorModel
|
|
153
|
+
: accountDefaults.nativeModel
|
|
154
|
+
: flags.model;
|
|
113
155
|
|
|
114
156
|
const workspaceEntries = parseWorkspaceEntries(flags.workspace, flags.branch, flags.commit);
|
|
115
157
|
|
|
@@ -144,23 +186,43 @@ export async function prepareAgentExec(
|
|
|
144
186
|
mode: flags.mode,
|
|
145
187
|
serviceTier: flags.serviceTier,
|
|
146
188
|
thinking: flags.thinking,
|
|
189
|
+
harness,
|
|
147
190
|
};
|
|
148
191
|
}
|
|
149
192
|
|
|
193
|
+
/** The account's execution defaults, "" for anything undeclared. */
|
|
194
|
+
interface AccountExecutionDefaults {
|
|
195
|
+
readonly harness: HarnessFlag;
|
|
196
|
+
readonly nativeModel: string;
|
|
197
|
+
readonly cursorModel: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const NO_ACCOUNT_DEFAULTS: AccountExecutionDefaults = { harness: "", nativeModel: "", cursorModel: "" };
|
|
201
|
+
|
|
150
202
|
/**
|
|
151
|
-
* Best-effort read of the caller's
|
|
152
|
-
* (
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
203
|
+
* Best-effort read of the caller's execution defaults
|
|
204
|
+
* (`IdentityAccountPreferences.default_harness` / `default_*_model`) in one
|
|
205
|
+
* whoAmI round trip. Any failure — network, auth, a backend without
|
|
206
|
+
* IdentityAccount — resolves to no defaults and the run proceeds exactly as
|
|
207
|
+
* unfilled flags do today: a missing preference must never fail a run.
|
|
208
|
+
*
|
|
209
|
+
* The persisted harness value is allowlist-guarded to the shipped engines
|
|
210
|
+
* (the same guard the web launcher's useAccountExecutionDefaults applies):
|
|
211
|
+
* a client must not trust stored data it did not write, so anything outside
|
|
212
|
+
* native/cursor is treated as undeclared rather than stamped on the wire.
|
|
157
213
|
*/
|
|
158
|
-
async function
|
|
214
|
+
async function resolveAccountExecutionDefaults(client: Stigmer): Promise<AccountExecutionDefaults> {
|
|
159
215
|
try {
|
|
160
216
|
const account = await client.identityAccount.whoAmI();
|
|
161
|
-
|
|
217
|
+
const prefs = account.spec?.preferences;
|
|
218
|
+
const declared = prefs?.defaultHarness;
|
|
219
|
+
return {
|
|
220
|
+
harness: declared === "native" || declared === "cursor" ? declared : "",
|
|
221
|
+
nativeModel: prefs?.defaultNativeModel ?? "",
|
|
222
|
+
cursorModel: prefs?.defaultCursorModel ?? "",
|
|
223
|
+
};
|
|
162
224
|
} catch {
|
|
163
|
-
return
|
|
225
|
+
return NO_ACCOUNT_DEFAULTS;
|
|
164
226
|
}
|
|
165
227
|
}
|
|
166
228
|
|
|
@@ -227,3 +289,19 @@ export function validateThinking(mode: string): asserts mode is ThinkingFlag {
|
|
|
227
289
|
);
|
|
228
290
|
}
|
|
229
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Validate the `--harness` flag. Empty means "use default": the account
|
|
295
|
+
* preference when the caller opted in, else the platform default (native).
|
|
296
|
+
* Only the shipped engines are accepted — other harness names that appear in
|
|
297
|
+
* UI metadata (copilot, codex, ...) have no proto mapping yet, so rejecting
|
|
298
|
+
* them here is honest, and this check catches spelling errors before a
|
|
299
|
+
* network round trip.
|
|
300
|
+
*/
|
|
301
|
+
export function validateHarness(harness: string): asserts harness is HarnessFlag {
|
|
302
|
+
if (harness !== "" && harness !== "native" && harness !== "cursor") {
|
|
303
|
+
throw new UsageError(
|
|
304
|
+
`invalid --harness value "${harness}": must be "native" or "cursor"`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -12,7 +12,7 @@ import { classify, ExitCode, UsageError } from "../errors/index.js";
|
|
|
12
12
|
import { shareAgent } from "./share.js";
|
|
13
13
|
|
|
14
14
|
const CLOUD = { appOrigin: "https://app.stigmer.ai", isLocal: false };
|
|
15
|
-
const LOCAL = { appOrigin: "http://localhost:
|
|
15
|
+
const LOCAL = { appOrigin: "http://localhost:7234", isLocal: true };
|
|
16
16
|
|
|
17
17
|
function makeAgent(): Agent {
|
|
18
18
|
return create(AgentSchema, {
|
|
@@ -350,7 +350,7 @@ describe("shareAgent output", () => {
|
|
|
350
350
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...LOCAL });
|
|
351
351
|
|
|
352
352
|
const link = result.sections.find((s) => s.title === "Public chat link");
|
|
353
|
-
expect(link?.items).toEqual(["http://localhost:
|
|
353
|
+
expect(link?.items).toEqual(["http://localhost:7234/chat/acme/support-agent"]);
|
|
354
354
|
expect(result.hints.some((h) => h.includes("Stigmer Cloud"))).toBe(true);
|
|
355
355
|
});
|
|
356
356
|
|