@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
@@ -12,7 +12,7 @@ import { join } from "node:path";
12
12
  import { load as loadConfig } from "../config/config.js";
13
13
  import { configPath } from "../config/paths.js";
14
14
  import { CommandResult } from "../output/command-result.js";
15
- import { SERVER_PORT, TEMPORAL_UI_PORT, WEB_CONSOLE_PORT } from "./constants.js";
15
+ import { SERVER_PORT, TEMPORAL_UI_PORT } from "./constants.js";
16
16
  import { resolveApiKey, resolveProvider } from "./llm-config.js";
17
17
  import { tcpConnects } from "./net/tcp.js";
18
18
  import { dataDir } from "./paths.js";
@@ -144,7 +144,9 @@ function addWebUiSection(result: CommandResult, health: HealthState): void {
144
144
  if (!temporalRunning && !webRunning) return;
145
145
 
146
146
  const section = result.addSection("Web UI");
147
- if (webRunning) section.field("Console", `http://localhost:${WEB_CONSOLE_PORT}`);
147
+ // The console shares the server's unified port (DD-012) — same origin as
148
+ // the API, served by the server itself.
149
+ if (webRunning) section.field("Console", `http://localhost:${SERVER_PORT}`);
148
150
  if (temporalRunning) section.field("Temporal", `http://localhost:${TEMPORAL_UI_PORT}`);
149
151
  }
150
152
 
@@ -1,13 +1,30 @@
1
- // Embedded web-console availability.
1
+ // Web-console availability against the server's unified port.
2
2
  //
3
- // The Go CLI embeds the console's static assets via go:embed and serves them on
4
- // :8234. There is no TS equivalent bundled in T05, so the daemon detects the
5
- // console as unavailable and skips it cleanly (recording a "stopped" state),
6
- // exactly as the Go daemon does when built without assets. Real serving — and
7
- // the asset-bundling decision behind it — is deferred to T06.
3
+ // Since DD-012 (oss sub-project console-serving) the SERVER serves the
4
+ // console: the static export ships inside @stigmer/server-slim and the
5
+ // unified port's lane 4 serves it, synthesizing /config.json in-process.
6
+ // The CLI therefore PROBES rather than serves — /config.json answers 200
7
+ // exactly when a console export is bundled with the running server (a
8
+ // dev-tree server without the export falls through to the RPC adapter's
9
+ // 404), so the daemon's component state reports what a browser would
10
+ // actually find. The Go-era embedded console (go:embed on :8234) and the
11
+ // T05/T06 stub this file used to be are both retired by that design.
8
12
 
9
- /** Whether an embedded web console is available to serve. Always false until
10
- * T06 wires asset bundling + an HTTP handler. */
11
- export function isWebConsoleAvailable(): boolean {
12
- return false;
13
+ import { SERVER_PORT } from "../constants.js";
14
+
15
+ /** Keep the daemon's settle path snappy: a hung probe must not stall `stigmer up`. */
16
+ const PROBE_TIMEOUT_MS = 3000;
17
+
18
+ /** Whether the running server serves a bundled web console. */
19
+ export async function isWebConsoleAvailable(
20
+ serverPort: number = SERVER_PORT,
21
+ ): Promise<boolean> {
22
+ try {
23
+ const response = await fetch(`http://127.0.0.1:${serverPort}/config.json`, {
24
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
25
+ });
26
+ return response.ok;
27
+ } catch {
28
+ return false;
29
+ }
13
30
  }
@@ -127,6 +127,6 @@ describe("runOAuthFlow", () => {
127
127
  sleep: noopSleep,
128
128
  log: () => {},
129
129
  });
130
- expect(openBrowser).toHaveBeenCalledWith("http://localhost:8234/acme/mcp-servers/github");
130
+ expect(openBrowser).toHaveBeenCalledWith("http://localhost:7234/acme/mcp-servers/github");
131
131
  });
132
132
  });
@@ -11,6 +11,7 @@ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
11
11
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
12
12
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
13
13
  import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
+ import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
14
15
  import {
15
16
  LocalPathSourceSchema,
16
17
  WorkspaceEntrySchema,
@@ -48,6 +49,7 @@ function makePrepared(overrides: Partial<PreparedRun> = {}): PreparedRun {
48
49
  mode: "",
49
50
  serviceTier: "",
50
51
  thinking: "",
52
+ harness: "",
51
53
  ...overrides,
52
54
  };
53
55
  }
@@ -127,4 +129,27 @@ describe("executeResolvedAgent", () => {
127
129
  expect(sent[0]?.spec?.sessionSpec).toBeUndefined();
128
130
  expect(sent[0]?.spec?.agentId).toBe("agt_1");
129
131
  });
132
+
133
+ it("threads the resolved harness onto the wire and surfaces it in the header (oss#293)", async () => {
134
+ // The revert-detection seam: if the prepared→create threading is ever
135
+ // dropped, this wire assertion fails — not just a flag-parsing test.
136
+ const { client, creates } = fakeBackend();
137
+
138
+ await executeResolvedAgent({
139
+ agent: makeAgent(),
140
+ prepared: makePrepared({ harness: "cursor" }),
141
+ org: "acme",
142
+ downloadDir: "",
143
+ outputMode: "inline",
144
+ client,
145
+ });
146
+
147
+ const sent = creates();
148
+ expect(sent).toHaveLength(1);
149
+ expect(sent[0]?.spec?.sessionSpec?.harness).toBe(Harness.CURSOR);
150
+ // D2 visibility: a cursor session must announce itself before streaming —
151
+ // whether the flag or the account preference selected it.
152
+ expect(stderrLines.join("")).toContain("Harness:");
153
+ expect(stderrLines.join("")).toContain("Cursor");
154
+ });
130
155
  });
@@ -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 { SessionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
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) {