@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.
Files changed (106) hide show
  1. package/commands/agent-exec-flags.d.ts +1 -0
  2. package/commands/agent-exec-flags.d.ts.map +1 -1
  3. package/commands/agent-exec-flags.js +3 -1
  4. package/commands/agent-exec-flags.js.map +1 -1
  5. package/commands/run.js +3 -0
  6. package/commands/run.js.map +1 -1
  7. package/commands/up.d.ts.map +1 -1
  8. package/commands/up.js +22 -6
  9. package/commands/up.js.map +1 -1
  10. package/config/resolve.d.ts +5 -2
  11. package/config/resolve.d.ts.map +1 -1
  12. package/config/resolve.js +7 -4
  13. package/config/resolve.js.map +1 -1
  14. package/local/constants.d.ts +8 -3
  15. package/local/constants.d.ts.map +1 -1
  16. package/local/constants.js +8 -3
  17. package/local/constants.js.map +1 -1
  18. package/local/daemon/components.d.ts.map +1 -1
  19. package/local/daemon/components.js +4 -2
  20. package/local/daemon/components.js.map +1 -1
  21. package/local/daemon/env.d.ts +16 -3
  22. package/local/daemon/env.d.ts.map +1 -1
  23. package/local/daemon/env.js +16 -3
  24. package/local/daemon/env.js.map +1 -1
  25. package/local/daemon/launch.d.ts.map +1 -1
  26. package/local/daemon/launch.js +5 -4
  27. package/local/daemon/launch.js.map +1 -1
  28. package/local/daemon/process.d.ts.map +1 -1
  29. package/local/daemon/process.js +8 -5
  30. package/local/daemon/process.js.map +1 -1
  31. package/local/runtime/index.d.ts +2 -2
  32. package/local/runtime/index.d.ts.map +1 -1
  33. package/local/runtime/index.js +4 -2
  34. package/local/runtime/index.js.map +1 -1
  35. package/local/runtime/node.d.ts +8 -0
  36. package/local/runtime/node.d.ts.map +1 -1
  37. package/local/runtime/node.js +74 -23
  38. package/local/runtime/node.js.map +1 -1
  39. package/local/runtime/runner.d.ts +2 -1
  40. package/local/runtime/runner.d.ts.map +1 -1
  41. package/local/runtime/runner.js +4 -31
  42. package/local/runtime/runner.js.map +1 -1
  43. package/local/runtime/runtimes-install.d.ts +13 -0
  44. package/local/runtime/runtimes-install.d.ts.map +1 -0
  45. package/local/runtime/runtimes-install.js +60 -0
  46. package/local/runtime/runtimes-install.js.map +1 -0
  47. package/local/runtime/server.d.ts +26 -30
  48. package/local/runtime/server.d.ts.map +1 -1
  49. package/local/runtime/server.js +106 -108
  50. package/local/runtime/server.js.map +1 -1
  51. package/local/status.d.ts.map +1 -1
  52. package/local/status.js +4 -2
  53. package/local/status.js.map +1 -1
  54. package/local/webconsole/index.d.ts +2 -3
  55. package/local/webconsole/index.d.ts.map +1 -1
  56. package/local/webconsole/index.js +24 -10
  57. package/local/webconsole/index.js.map +1 -1
  58. package/package.json +5 -5
  59. package/resources/run/agent-exec.d.ts.map +1 -1
  60. package/resources/run/agent-exec.js +2 -0
  61. package/resources/run/agent-exec.js.map +1 -1
  62. package/resources/run/create.d.ts +2 -1
  63. package/resources/run/create.d.ts.map +1 -1
  64. package/resources/run/create.js +21 -3
  65. package/resources/run/create.js.map +1 -1
  66. package/resources/run/header.d.ts +9 -1
  67. package/resources/run/header.d.ts.map +1 -1
  68. package/resources/run/header.js +3 -0
  69. package/resources/run/header.js.map +1 -1
  70. package/resources/run/prepare.d.ts +41 -3
  71. package/resources/run/prepare.d.ts.map +1 -1
  72. package/resources/run/prepare.js +53 -16
  73. package/resources/run/prepare.js.map +1 -1
  74. package/src/commands/agent-exec-flags.ts +5 -2
  75. package/src/commands/run.ts +3 -0
  76. package/src/commands/up.ts +24 -6
  77. package/src/config/resolve.test.ts +1 -1
  78. package/src/config/resolve.ts +7 -4
  79. package/src/local/constants.ts +8 -4
  80. package/src/local/daemon/components.test.ts +9 -1
  81. package/src/local/daemon/components.ts +4 -2
  82. package/src/local/daemon/daemon.integration.test.ts +1 -1
  83. package/src/local/daemon/env.test.ts +15 -4
  84. package/src/local/daemon/env.ts +33 -5
  85. package/src/local/daemon/launch.ts +5 -4
  86. package/src/local/daemon/process.ts +7 -4
  87. package/src/local/runtime/index.ts +6 -5
  88. package/src/local/runtime/node.ts +99 -23
  89. package/src/local/runtime/runner.ts +5 -36
  90. package/src/local/runtime/runtime.test.ts +58 -14
  91. package/src/local/runtime/runtimes-install.test.ts +56 -0
  92. package/src/local/runtime/runtimes-install.ts +72 -0
  93. package/src/local/runtime/server.test.ts +178 -114
  94. package/src/local/runtime/server.ts +149 -129
  95. package/src/local/status.ts +4 -2
  96. package/src/local/webconsole/index.ts +27 -10
  97. package/src/resources/connect/oauth.test.ts +1 -1
  98. package/src/resources/run/agent-exec.test.ts +25 -0
  99. package/src/resources/run/agent-exec.ts +2 -0
  100. package/src/resources/run/create.test.ts +92 -1
  101. package/src/resources/run/create.ts +25 -6
  102. package/src/resources/run/header.test.ts +18 -0
  103. package/src/resources/run/header.ts +11 -1
  104. package/src/resources/run/prepare.test.ts +247 -5
  105. package/src/resources/run/prepare.ts +99 -21
  106. 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
- /** Stub whose whoAmI resolves an account carrying the given preference. */
87
- function clientWithPreference(defaultNativeModel: string): Stigmer {
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.default_native_model`) via `whoAmI()`.
82
- * Local mode has no IdentityAccount, so callers pass `false` there and
83
- * the omitted model keeps resolving to the platform default.
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 model seed (oss#293 Phase 1.5): an explicit --model always wins;
105
- // an omitted one fills from the account preference on cloud. `run` and
106
- // `draft` always create a NEW session (threading lives in `resume`, which
107
- // does not pass through here), so the fill never injects a native model
108
- // into an existing cursor-harness session.
109
- const model =
110
- flags.model === "" && options?.cloudBackend === true
111
- ? await resolveModelFromAccountPreference(client)
112
- : flags.model;
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 default model for native-harness runs
152
- * (CLI sessions are native — there is no --harness flag yet, so the cursor
153
- * default is deliberately not consulted). Any failure — network, auth, a
154
- * backend without IdentityAccount — resolves to "" and the run proceeds
155
- * exactly as an unfilled --model does today: a missing preference must
156
- * never fail a run.
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 resolveModelFromAccountPreference(client: Stigmer): Promise<string> {
214
+ async function resolveAccountExecutionDefaults(client: Stigmer): Promise<AccountExecutionDefaults> {
159
215
  try {
160
216
  const account = await client.identityAccount.whoAmI();
161
- return account.spec?.preferences?.defaultNativeModel ?? "";
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:8234", isLocal: true };
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:8234/chat/acme/support-agent"]);
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