@stigmer/cli 3.12.7 → 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.
@@ -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
+ }