@stigmer/cli 3.12.5 → 3.12.7

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 (57) 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/setup.d.ts.map +1 -1
  6. package/commands/setup.js +38 -18
  7. package/commands/setup.js.map +1 -1
  8. package/local/daemon/components.d.ts.map +1 -1
  9. package/local/daemon/components.js +11 -1
  10. package/local/daemon/components.js.map +1 -1
  11. package/local/daemon/env.d.ts +6 -0
  12. package/local/daemon/env.d.ts.map +1 -1
  13. package/local/daemon/env.js +8 -0
  14. package/local/daemon/env.js.map +1 -1
  15. package/local/daemon/launch.d.ts.map +1 -1
  16. package/local/daemon/launch.js +15 -0
  17. package/local/daemon/launch.js.map +1 -1
  18. package/local/operator-config.d.ts +28 -0
  19. package/local/operator-config.d.ts.map +1 -0
  20. package/local/operator-config.js +77 -0
  21. package/local/operator-config.js.map +1 -0
  22. package/local/setup/wizard.d.ts +16 -3
  23. package/local/setup/wizard.d.ts.map +1 -1
  24. package/local/setup/wizard.js +72 -13
  25. package/local/setup/wizard.js.map +1 -1
  26. package/package.json +5 -5
  27. package/resources/run/agent-exec.d.ts.map +1 -1
  28. package/resources/run/agent-exec.js +1 -0
  29. package/resources/run/agent-exec.js.map +1 -1
  30. package/resources/run/create.d.ts +2 -1
  31. package/resources/run/create.d.ts.map +1 -1
  32. package/resources/run/create.js +11 -6
  33. package/resources/run/create.js.map +1 -1
  34. package/resources/run/prepare.d.ts +15 -0
  35. package/resources/run/prepare.d.ts.map +1 -1
  36. package/resources/run/prepare.js +14 -0
  37. package/resources/run/prepare.js.map +1 -1
  38. package/resources/task-configs.js +10 -1
  39. package/resources/task-configs.js.map +1 -1
  40. package/src/commands/agent-exec-flags.ts +5 -2
  41. package/src/commands/setup.ts +48 -18
  42. package/src/local/daemon/components.test.ts +20 -0
  43. package/src/local/daemon/components.ts +9 -1
  44. package/src/local/daemon/env.test.ts +21 -0
  45. package/src/local/daemon/env.ts +14 -0
  46. package/src/local/daemon/launch.ts +17 -0
  47. package/src/local/operator-config.test.ts +72 -0
  48. package/src/local/operator-config.ts +95 -0
  49. package/src/local/setup/wizard.test.ts +46 -1
  50. package/src/local/setup/wizard.ts +82 -13
  51. package/src/resources/run/agent-exec.test.ts +1 -0
  52. package/src/resources/run/agent-exec.ts +1 -0
  53. package/src/resources/run/create.test.ts +48 -1
  54. package/src/resources/run/create.ts +11 -6
  55. package/src/resources/run/prepare.test.ts +14 -1
  56. package/src/resources/run/prepare.ts +25 -0
  57. package/src/resources/task-configs.ts +10 -1
@@ -23,7 +23,7 @@ const SERVER_GATE_TIMEOUT_MS = 30_000;
23
23
  */
24
24
  export function buildServerEnv(config: DaemonConfig, base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
25
25
  const configDir = dirname(config.dataDir); // ~/.stigmer
26
- return {
26
+ const env: NodeJS.ProcessEnv = {
27
27
  ...base,
28
28
  GRPC_PORT: String(SERVER_PORT),
29
29
  TEMPORAL_HOST_PORT: config.temporalAddress,
@@ -37,6 +37,14 @@ export function buildServerEnv(config: DaemonConfig, base: NodeJS.ProcessEnv = p
37
37
  // is visible and enforceable here, next to the runner's LOCAL_ARTIFACT_PATH.
38
38
  ARTIFACT_LOCAL_BASE_PATH: join(config.dataDir, ARTIFACTS_SUBDIR),
39
39
  };
40
+ // Explicit set (after the base spread) so the launcher-resolved identity wins
41
+ // over a stale inherited value — the delivery path for a `stigmer setup`-
42
+ // persisted operator identity (oss#796; the ANTHROPIC_API_KEY precedent in
43
+ // buildRunnerEnv). Server child only: the runner resolves caller identity
44
+ // from the session's stamped created_by, never from its own env.
45
+ if (config.operatorEmail !== undefined) env.STIGMER_OPERATOR_EMAIL = config.operatorEmail;
46
+ if (config.operatorName !== undefined) env.STIGMER_OPERATOR_NAME = config.operatorName;
47
+ return env;
40
48
  }
41
49
 
42
50
  /**
@@ -28,6 +28,8 @@ describe("buildDaemonEnv + readDaemonConfig", () => {
28
28
  cursorApiKey: undefined,
29
29
  anthropicApiKey: undefined,
30
30
  activityRouting: undefined,
31
+ operatorEmail: undefined,
32
+ operatorName: undefined,
31
33
  });
32
34
  });
33
35
 
@@ -61,6 +63,25 @@ describe("buildDaemonEnv + readDaemonConfig", () => {
61
63
  expect(config.anthropicApiKey).toBe("sk-ant-env");
62
64
  });
63
65
 
66
+ // The operator identity rides the same config-file-only delivery path as the
67
+ // Anthropic key (oss#796): persisted by `stigmer setup`, absent from the
68
+ // shell, written into the contract explicitly by the launcher.
69
+ it("delivers a launcher-resolved operator identity with no help from the base env", () => {
70
+ const env = buildDaemonEnv(
71
+ { ...baseInputs, operatorEmail: "ada@example.com", operatorName: "Ada Lovelace" },
72
+ {},
73
+ );
74
+ const config = readDaemonConfig(env);
75
+ expect(config.operatorEmail).toBe("ada@example.com");
76
+ expect(config.operatorName).toBe("Ada Lovelace");
77
+ });
78
+
79
+ it("passes a shell-exported operator identity through from the base env", () => {
80
+ const env = buildDaemonEnv(baseInputs, { STIGMER_OPERATOR_EMAIL: "env@example.com" });
81
+ expect(readDaemonConfig(env).operatorEmail).toBe("env@example.com");
82
+ expect(readDaemonConfig(env).operatorName).toBeUndefined();
83
+ });
84
+
64
85
  // Other provider keys have no contract slot; they reach the runner solely via
65
86
  // shell-env inheritance (the child env spreads the base env).
66
87
  it("leaves shell-exported non-Anthropic keys in the env without parsing them", () => {
@@ -19,6 +19,8 @@ export const DaemonEnvVar = {
19
19
  CursorApiKey: "CURSOR_API_KEY",
20
20
  AnthropicApiKey: "ANTHROPIC_API_KEY",
21
21
  ActivityRouting: "STIGMER_ACTIVITY_ROUTING",
22
+ OperatorEmail: "STIGMER_OPERATOR_EMAIL",
23
+ OperatorName: "STIGMER_OPERATOR_NAME",
22
24
  } as const;
23
25
 
24
26
  /** Resolved runner launch coordinates. */
@@ -41,6 +43,8 @@ export interface DaemonConfig {
41
43
  cursorApiKey?: string;
42
44
  anthropicApiKey?: string;
43
45
  activityRouting?: string;
46
+ operatorEmail?: string;
47
+ operatorName?: string;
44
48
  }
45
49
 
46
50
  /** Inputs the launcher already resolved, to encode into the daemon env. */
@@ -60,6 +64,12 @@ export interface DaemonEnvInputs {
60
64
  // persisted delivery path — other keys (OPENAI_API_KEY, CURSOR_API_KEY)
61
65
  // reach the runner solely via shell-env inheritance.
62
66
  anthropicApiKey?: string;
67
+ // Operator identity resolved by the launcher (env > config file), the same
68
+ // persisted-delivery reasoning as the Anthropic key (oss#796). Consumed by
69
+ // the SERVER child only — the runner resolves caller identity from the
70
+ // session's stamped created_by, never from its own env.
71
+ operatorEmail?: string;
72
+ operatorName?: string;
63
73
  }
64
74
 
65
75
  /**
@@ -82,6 +92,8 @@ export function buildDaemonEnv(inputs: DaemonEnvInputs, base: NodeJS.ProcessEnv
82
92
  env[DaemonEnvVar.RunnerAppDir] = inputs.runner.appDir;
83
93
  }
84
94
  if (inputs.anthropicApiKey !== undefined) env[DaemonEnvVar.AnthropicApiKey] = inputs.anthropicApiKey;
95
+ if (inputs.operatorEmail !== undefined) env[DaemonEnvVar.OperatorEmail] = inputs.operatorEmail;
96
+ if (inputs.operatorName !== undefined) env[DaemonEnvVar.OperatorName] = inputs.operatorName;
85
97
  return env;
86
98
  }
87
99
 
@@ -105,6 +117,8 @@ export function readDaemonConfig(env: NodeJS.ProcessEnv = process.env): DaemonCo
105
117
  cursorApiKey: nonEmpty(env[DaemonEnvVar.CursorApiKey]),
106
118
  anthropicApiKey: nonEmpty(env[DaemonEnvVar.AnthropicApiKey]),
107
119
  activityRouting: nonEmpty(env[DaemonEnvVar.ActivityRouting]),
120
+ operatorEmail: nonEmpty(env[DaemonEnvVar.OperatorEmail]),
121
+ operatorName: nonEmpty(env[DaemonEnvVar.OperatorName]),
108
122
  };
109
123
  }
110
124
 
@@ -22,6 +22,7 @@ import { findProcessByPort, isProcessAlive, killProcess } from "../state/proc.js
22
22
  import { type StartupConfig, removeStartupConfig, saveStartupConfig } from "../state/startup-config.js";
23
23
  import { rotateLogs } from "../state/log-rotation.js";
24
24
  import { resolveApiKey, resolveProvider } from "../llm-config.js";
25
+ import { resolveOperatorIdentity } from "../operator-config.js";
25
26
  import { ensureRunner } from "../runtime/runner.js";
26
27
  import { ensureServerBinary } from "../runtime/server.js";
27
28
  import { TemporalManager } from "../temporal/manager.js";
@@ -77,6 +78,7 @@ export async function up(options: UpOptions = {}, home: string = homedir()): Pro
77
78
  serverBin,
78
79
  runner,
79
80
  ...resolveLlmKeyInputs(config),
81
+ ...resolveOperatorIdentityInputs(config),
80
82
  },
81
83
  process.env,
82
84
  );
@@ -235,6 +237,21 @@ function resolveLlmKeyInputs(config: Config): Pick<DaemonEnvInputs, "anthropicAp
235
237
  return key === "" ? {} : { anthropicApiKey: key };
236
238
  }
237
239
 
240
+ // Operator-identity delivery for the server child (oss#796): same persisted-
241
+ // delivery reasoning as the LLM key above — a setup-persisted identity lives
242
+ // only in the config file, so the launcher must write it into the daemon
243
+ // contract explicitly. resolveOperatorIdentity's precedence (env > config,
244
+ // sources never mixed) makes both cases one code path.
245
+ function resolveOperatorIdentityInputs(
246
+ config: Config,
247
+ ): Pick<DaemonEnvInputs, "operatorEmail" | "operatorName"> {
248
+ const identity = resolveOperatorIdentity(config);
249
+ if (identity === undefined) return {};
250
+ return identity.name === undefined
251
+ ? { operatorEmail: identity.email }
252
+ : { operatorEmail: identity.email, operatorName: identity.name };
253
+ }
254
+
238
255
  // Default to managed Temporal unless the opaque local config explicitly opts
239
256
  // out — zero-config `stigmer up` must work on a fresh machine.
240
257
  function isTemporalManaged(config: Config): boolean {
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Config } from "../config/config.js";
3
+ import {
4
+ readOperator,
5
+ resolveOperatorIdentity,
6
+ setOperator,
7
+ validateOperatorIdentity,
8
+ } from "./operator-config.js";
9
+
10
+ function config(local?: unknown): Config {
11
+ return { backend: { type: "local", local } };
12
+ }
13
+
14
+ describe("resolveOperatorIdentity", () => {
15
+ it("honors env > config, never mixing sources", () => {
16
+ const cfg = config({ operator: { email: "cfg@example.com", name: "Config Name" } });
17
+
18
+ // Env email brings the env name (or none) — the config name must not
19
+ // ride along under an env identity, that would be attribution lying.
20
+ expect(resolveOperatorIdentity(cfg, { STIGMER_OPERATOR_EMAIL: "env@example.com" })).toEqual({
21
+ email: "env@example.com",
22
+ });
23
+ expect(
24
+ resolveOperatorIdentity(cfg, {
25
+ STIGMER_OPERATOR_EMAIL: "env@example.com",
26
+ STIGMER_OPERATOR_NAME: "Env Name",
27
+ }),
28
+ ).toEqual({ email: "env@example.com", name: "Env Name" });
29
+
30
+ expect(resolveOperatorIdentity(cfg, {})).toEqual({
31
+ email: "cfg@example.com",
32
+ name: "Config Name",
33
+ });
34
+ });
35
+
36
+ it("returns undefined when neither source has an email — the anonymous default", () => {
37
+ expect(resolveOperatorIdentity(config(), {})).toBeUndefined();
38
+ expect(resolveOperatorIdentity(config({ operator: {} }), {})).toBeUndefined();
39
+ // Whitespace-only values are unset, matching the server's TrimSpace.
40
+ expect(resolveOperatorIdentity(config(), { STIGMER_OPERATOR_EMAIL: " " })).toBeUndefined();
41
+ });
42
+
43
+ it("trims values, matching the server's boot check", () => {
44
+ const cfg = config({ operator: { email: " ada@example.com ", name: " Ada " } });
45
+ expect(resolveOperatorIdentity(cfg, {})).toEqual({ email: "ada@example.com", name: "Ada" });
46
+ });
47
+ });
48
+
49
+ describe("validateOperatorIdentity", () => {
50
+ it("mirrors the server's boot rules: minimal @ check, name requires email", () => {
51
+ expect(validateOperatorIdentity("ada@example.com", "Ada")).toBeUndefined();
52
+ expect(validateOperatorIdentity("ada@example.com", "")).toBeUndefined();
53
+ expect(validateOperatorIdentity("", "")).toBeUndefined(); // "no identity" is valid
54
+
55
+ expect(validateOperatorIdentity("not-an-email", "")).toContain("missing '@'");
56
+ expect(validateOperatorIdentity("", "Ada")).toContain("set both or neither");
57
+ });
58
+ });
59
+
60
+ describe("setOperator", () => {
61
+ it("preserves sibling local keys and can clear the section", () => {
62
+ const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic" } });
63
+ const updated = setOperator(cfg, { email: "ada@example.com", name: "Ada" });
64
+ expect(readOperator(updated)).toEqual({ email: "ada@example.com", name: "Ada" });
65
+ expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
66
+ expect((updated.backend.local as { llm: unknown }).llm).toEqual({ provider: "anthropic" });
67
+
68
+ const cleared = setOperator(updated, undefined);
69
+ expect(readOperator(cleared)).toBeUndefined();
70
+ expect((cleared.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
71
+ });
72
+ });
@@ -0,0 +1,95 @@
1
+ // Typed view over `backend.local.operator` — the self-hosted operator
2
+ // identity `setup` persists and the launcher delivers to the server child
3
+ // (oss#796, the UX-completion arm of oss#400's configured operator identity).
4
+ //
5
+ // The lens pattern mirrors llm-config.ts: config/config.ts carries
6
+ // `backend.local` verbatim so unknown siblings are never dropped; this module
7
+ // owns exactly one sub-tree (`local.operator`) and resolves the effective
8
+ // identity with env-override precedence (STIGMER_OPERATOR_EMAIL >
9
+ // config file). Sources never mix: an env-provided email brings the
10
+ // env-provided name (or none), a config-provided email brings the config
11
+ // name — half-and-half identities would be attribution lies.
12
+ //
13
+ // Validation deliberately duplicates the server's boot check
14
+ // (stigmer-server pkg/config/config.go loadOperatorIdentity): trim both
15
+ // values; a non-empty email must contain '@' (nothing more — the server is
16
+ // equally minimal on purpose); a name without an email is refused because
17
+ // the email IS the identity. Checking here surfaces the failure at setup
18
+ // time instead of at the next `stigmer up`.
19
+
20
+ import type { Config } from "../config/config.js";
21
+
22
+ /** Operator identity as persisted under `backend.local.operator` (snake_case = YAML keys). */
23
+ export interface OperatorSettings {
24
+ email?: string;
25
+ name?: string;
26
+ }
27
+
28
+ interface LocalSection {
29
+ operator?: OperatorSettings;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ function localSection(config: Config): LocalSection {
34
+ const local = config.backend.local;
35
+ return local !== null && typeof local === "object" ? (local as LocalSection) : {};
36
+ }
37
+
38
+ /** The persisted operator settings, if any. */
39
+ export function readOperator(config: Config): OperatorSettings | undefined {
40
+ return localSection(config).operator;
41
+ }
42
+
43
+ /**
44
+ * Effective operator identity: env > config file, sources never mixed.
45
+ * Returns `undefined` when neither source provides an email (the anonymous
46
+ * default oss#400 preserves).
47
+ */
48
+ export function resolveOperatorIdentity(
49
+ config: Config,
50
+ env: NodeJS.ProcessEnv = process.env,
51
+ ): OperatorSettings | undefined {
52
+ const envEmail = (env.STIGMER_OPERATOR_EMAIL ?? "").trim();
53
+ if (envEmail !== "") {
54
+ const envName = (env.STIGMER_OPERATOR_NAME ?? "").trim();
55
+ return envName === "" ? { email: envEmail } : { email: envEmail, name: envName };
56
+ }
57
+ const persisted = readOperator(config);
58
+ const email = (persisted?.email ?? "").trim();
59
+ if (email === "") return undefined;
60
+ const name = (persisted?.name ?? "").trim();
61
+ return name === "" ? { email } : { email, name };
62
+ }
63
+
64
+ /**
65
+ * Validate an operator identity the way the server's boot check will
66
+ * (byte-similar messages so setup-time and boot-time failures read as one
67
+ * rule). Returns an error message, or undefined when valid. An empty email
68
+ * with an empty name is valid — it means "no identity", the wizard's skip.
69
+ */
70
+ export function validateOperatorIdentity(email: string, name: string): string | undefined {
71
+ const trimmedEmail = email.trim();
72
+ const trimmedName = name.trim();
73
+ if (trimmedEmail !== "" && !trimmedEmail.includes("@")) {
74
+ return `"${trimmedEmail}" is not an email address (missing '@')`;
75
+ }
76
+ if (trimmedName !== "" && trimmedEmail === "") {
77
+ return "an operator name without an email is not an identity — set both or neither";
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ /**
83
+ * Return a copy of `config` with the operator section replaced, preserving
84
+ * every other key under `backend.local`. Passing `undefined` clears the
85
+ * section.
86
+ */
87
+ export function setOperator(config: Config, settings: OperatorSettings | undefined): Config {
88
+ const local: LocalSection = { ...localSection(config) };
89
+ if (settings === undefined) {
90
+ delete local.operator;
91
+ } else {
92
+ local.operator = settings;
93
+ }
94
+ return { ...config, backend: { ...config.backend, local } };
95
+ }
@@ -1,7 +1,14 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { Config } from "../../config/config.js";
3
3
  import { readLlm } from "../llm-config.js";
4
- import { PROVIDER_CHOICES, applyChoice, buildLlmForChoice } from "./wizard.js";
4
+ import { readOperator } from "../operator-config.js";
5
+ import {
6
+ PROVIDER_CHOICES,
7
+ applyChoice,
8
+ applyOperatorIdentity,
9
+ buildLlmForChoice,
10
+ buildOperatorForInputs,
11
+ } from "./wizard.js";
5
12
 
6
13
  function config(local?: unknown): Config {
7
14
  return { backend: { type: "local", local } };
@@ -47,3 +54,41 @@ describe("applyChoice", () => {
47
54
  expect(readLlm(updated)).toBeUndefined();
48
55
  });
49
56
  });
57
+
58
+ describe("buildOperatorForInputs", () => {
59
+ it("persists email (and optional name), trimmed", () => {
60
+ expect(buildOperatorForInputs(" ada@example.com ", "")).toEqual({ email: "ada@example.com" });
61
+ expect(buildOperatorForInputs("ada@example.com", " Ada ")).toEqual({
62
+ email: "ada@example.com",
63
+ name: "Ada",
64
+ });
65
+ });
66
+
67
+ it("returns undefined for empty inputs — nothing to persist, identity stays anonymous", () => {
68
+ expect(buildOperatorForInputs("", "")).toBeUndefined();
69
+ expect(buildOperatorForInputs(" ", "")).toBeUndefined();
70
+ });
71
+
72
+ // The server's boot rules, surfaced at setup time (oss#796): a malformed
73
+ // identity must never be persisted only to fail the next `stigmer up`.
74
+ it("refuses what the server's boot check would refuse", () => {
75
+ expect(() => buildOperatorForInputs("not-an-email", "")).toThrow(/missing '@'/);
76
+ expect(() => buildOperatorForInputs("", "Ada")).toThrow(/set both or neither/);
77
+ });
78
+ });
79
+
80
+ describe("applyOperatorIdentity", () => {
81
+ it("writes the identity while preserving siblings", () => {
82
+ const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic" } });
83
+ const updated = applyOperatorIdentity(cfg, "ada@example.com", "Ada");
84
+ expect(readOperator(updated)).toEqual({ email: "ada@example.com", name: "Ada" });
85
+ expect(readLlm(updated)).toEqual({ provider: "anthropic" });
86
+ expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
87
+ });
88
+
89
+ it("empty inputs leave the config unchanged (skip, not clear)", () => {
90
+ const cfg = config({ operator: { email: "keep@example.com" } });
91
+ const updated = applyOperatorIdentity(cfg, "", "");
92
+ expect(readOperator(updated)).toEqual({ email: "keep@example.com" });
93
+ });
94
+ });
@@ -7,13 +7,19 @@
7
7
  // Local agent execution is Anthropic-only (the native runner only constructs
8
8
  // Anthropic clients and the registry's native entries are Anthropic models),
9
9
  // so the interactive flow is a single API-key prompt rather than a provider
10
- // menu. There is no model concept here at all: the platform model registry
11
- // owns the execution default, and per-run overrides belong to `stigmer run
12
- // --model` (oss#314 removed the dead setup-level pin).
10
+ // menu, followed by the skippable operator-identity prompt (oss#796). There
11
+ // is no model concept here at all: the platform model registry owns the
12
+ // execution default, and per-run overrides belong to `stigmer run --model`
13
+ // (oss#314 removed the dead setup-level pin).
13
14
 
14
15
  import type { Config } from "../../config/config.js";
15
16
  import { type LlmSettings, setLlm } from "../llm-config.js";
16
- import { promptSecret } from "./prompt.js";
17
+ import {
18
+ type OperatorSettings,
19
+ setOperator,
20
+ validateOperatorIdentity,
21
+ } from "../operator-config.js";
22
+ import { promptLine, promptSecret } from "./prompt.js";
17
23
 
18
24
  /**
19
25
  * The providers `stigmer setup` accepts. This is the single source of truth
@@ -47,25 +53,88 @@ export function applyChoice(config: Config, choice: ProviderChoice, inputs: Sele
47
53
  return setLlm(config, buildLlmForChoice(choice, inputs));
48
54
  }
49
55
 
56
+ /**
57
+ * Resolve operator-identity inputs to the settings to persist, validating the
58
+ * way the server's boot check will (oss#796) — the pure decision core shared
59
+ * by the flag path and the interactive prompt, like buildLlmForChoice. An
60
+ * empty email with an empty name yields `undefined` (nothing to persist —
61
+ * identity stays whatever it was; the anonymous default is the absence of
62
+ * the section, not an empty one). Throws on inputs the server would refuse
63
+ * at boot, so the failure surfaces at setup time.
64
+ */
65
+ export function buildOperatorForInputs(email: string, name: string): OperatorSettings | undefined {
66
+ const error = validateOperatorIdentity(email, name);
67
+ if (error !== undefined) {
68
+ throw new Error(error);
69
+ }
70
+ const trimmedEmail = email.trim();
71
+ if (trimmedEmail === "") return undefined;
72
+ const trimmedName = name.trim();
73
+ return trimmedName === "" ? { email: trimmedEmail } : { email: trimmedEmail, name: trimmedName };
74
+ }
75
+
76
+ /** Apply operator-identity inputs to a config, returning the updated copy. */
77
+ export function applyOperatorIdentity(config: Config, email: string, name: string): Config {
78
+ const settings = buildOperatorForInputs(email, name);
79
+ return settings === undefined ? config : setOperator(config, settings);
80
+ }
81
+
50
82
  /**
51
83
  * Run the interactive setup against the current terminal: one Anthropic
52
- * API-key prompt. A key already in the environment is used without prompting
53
- * (and never persisted — env precedence makes a copy redundant); an empty
54
- * entry is an explicit skip.
84
+ * API-key prompt, then the (skippable) operator-identity prompt. A key
85
+ * already in the environment is used without prompting (and never persisted —
86
+ * env precedence makes a copy redundant); an empty entry is an explicit skip.
55
87
  */
56
88
  export async function runInteractiveWizard(config: Config, env: NodeJS.ProcessEnv = process.env): Promise<Config> {
57
89
  process.stderr.write("\nStigmer runs local agents on Anthropic Claude models.\n");
58
90
  process.stderr.write("The model is chosen automatically from the platform model registry.\n\n");
59
91
 
92
+ let updated: Config;
60
93
  if (env.ANTHROPIC_API_KEY) {
61
94
  process.stderr.write("Using ANTHROPIC_API_KEY from environment\n");
62
- return applyChoice(config, "anthropic");
95
+ updated = applyChoice(config, "anthropic");
96
+ } else {
97
+ const key = (await promptSecret("Enter your Anthropic API key (press Enter to skip)")).trim();
98
+ if (key === "") {
99
+ process.stderr.write("Skipped. Agents won't execute until a provider is configured (stigmer setup).\n");
100
+ updated = applyChoice(config, "skip");
101
+ } else {
102
+ updated = applyChoice(config, "anthropic", { apiKey: key });
103
+ }
104
+ }
105
+
106
+ return runOperatorIdentityStep(updated, env);
107
+ }
108
+
109
+ /**
110
+ * The operator-identity step (oss#796): opt-in, skippable, and env-aware in
111
+ * the same way the API-key step is — an env-provided identity is used without
112
+ * prompting and never persisted (env precedence makes a copy redundant). An
113
+ * invalid entry is refused and NOT persisted (re-run `stigmer setup`), the
114
+ * same rule the server enforces at boot, just earlier.
115
+ */
116
+ async function runOperatorIdentityStep(config: Config, env: NodeJS.ProcessEnv): Promise<Config> {
117
+ process.stderr.write(
118
+ "\nAn operator identity attributes local creates to you and unlocks identity-gated MCP tools.\n",
119
+ );
120
+
121
+ if ((env.STIGMER_OPERATOR_EMAIL ?? "").trim() !== "") {
122
+ process.stderr.write("Using STIGMER_OPERATOR_EMAIL from environment\n");
123
+ return config;
124
+ }
125
+
126
+ const email = (await promptLine("Operator email (press Enter to skip): ")).trim();
127
+ if (email === "") {
128
+ return config;
63
129
  }
130
+ const name = (await promptLine("Operator display name (optional): ")).trim();
64
131
 
65
- const key = (await promptSecret("Enter your Anthropic API key (press Enter to skip)")).trim();
66
- if (key === "") {
67
- process.stderr.write("Skipped. Agents won't execute until a provider is configured (stigmer setup).\n");
68
- return applyChoice(config, "skip");
132
+ try {
133
+ return applyOperatorIdentity(config, email, name);
134
+ } catch (error) {
135
+ process.stderr.write(
136
+ `Not saved: ${error instanceof Error ? error.message : String(error)} — re-run stigmer setup.\n`,
137
+ );
138
+ return config;
69
139
  }
70
- return applyChoice(config, "anthropic", { apiKey: key });
71
140
  }
@@ -47,6 +47,7 @@ function makePrepared(overrides: Partial<PreparedRun> = {}): PreparedRun {
47
47
  autoApproveAll: false,
48
48
  mode: "",
49
49
  serviceTier: "",
50
+ thinking: "",
50
51
  ...overrides,
51
52
  };
52
53
  }
@@ -43,6 +43,7 @@ export async function executeResolvedAgent(input: ResolvedAgentExecInput): Promi
43
43
  model: prepared.model,
44
44
  mode: prepared.mode,
45
45
  serviceTier: prepared.serviceTier,
46
+ thinking: prepared.thinking,
46
47
  autoApproveAll: prepared.autoApproveAll,
47
48
  });
48
49
 
@@ -4,7 +4,7 @@
4
4
  // is faked to capture the exact proto sent to the RPC.
5
5
 
6
6
  import { describe, expect, it } from "vitest";
7
- import { InteractionMode, ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import { InteractionMode, ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
8
8
  import { create } from "@bufbuild/protobuf";
9
9
  import {
10
10
  LocalPathSourceSchema,
@@ -41,6 +41,7 @@ describe("createAgentExecution", () => {
41
41
  model: "claude",
42
42
  mode: "plan",
43
43
  serviceTier: "",
44
+ thinking: "",
44
45
  autoApproveAll: true,
45
46
  });
46
47
 
@@ -69,6 +70,7 @@ describe("createAgentExecution", () => {
69
70
  model: "",
70
71
  mode: "",
71
72
  serviceTier: "",
73
+ thinking: "",
72
74
  autoApproveAll: false,
73
75
  });
74
76
  expect(exec.spec?.message).toBe("hi");
@@ -88,6 +90,7 @@ describe("createAgentExecution", () => {
88
90
  model: "composer-2.5",
89
91
  mode: "",
90
92
  serviceTier: "fast",
93
+ thinking: "",
91
94
  autoApproveAll: false,
92
95
  });
93
96
  expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.FAST);
@@ -108,11 +111,52 @@ describe("createAgentExecution", () => {
108
111
  model: "",
109
112
  mode: "",
110
113
  serviceTier: "standard",
114
+ thinking: "",
111
115
  autoApproveAll: false,
112
116
  });
113
117
  expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.STANDARD);
114
118
  });
115
119
 
120
+ it("maps --thinking enabled to the enum (#772)", async () => {
121
+ const { fn } = fakeController();
122
+ const exec = await createAgentExecution(fn, {
123
+ agentId: "agt_1",
124
+ orgId: "acme",
125
+ message: "x",
126
+ runtimeEnv: {},
127
+ attachments: [],
128
+ workspaceFileRefs: [],
129
+ workspaceEntries: [],
130
+ model: "claude-haiku-4-5",
131
+ mode: "",
132
+ serviceTier: "",
133
+ thinking: "enabled",
134
+ autoApproveAll: false,
135
+ });
136
+ expect(exec.spec?.executionConfig?.thinkingMode).toBe(ThinkingMode.ENABLED);
137
+ });
138
+
139
+ it("maps an explicit --thinking disabled to DISABLED, not UNSPECIFIED", async () => {
140
+ // The tier's #772 twin: unspecified-vs-explicit-disabled is the same
141
+ // load-bearing ledger distinction.
142
+ const { fn } = fakeController();
143
+ const exec = await createAgentExecution(fn, {
144
+ agentId: "agt_1",
145
+ orgId: "acme",
146
+ message: "x",
147
+ runtimeEnv: {},
148
+ attachments: [],
149
+ workspaceFileRefs: [],
150
+ workspaceEntries: [],
151
+ model: "",
152
+ mode: "",
153
+ serviceTier: "",
154
+ thinking: "disabled",
155
+ autoApproveAll: false,
156
+ });
157
+ expect(exec.spec?.executionConfig?.thinkingMode).toBe(ThinkingMode.DISABLED);
158
+ });
159
+
116
160
  it("leaves InteractionMode unspecified for agent mode", async () => {
117
161
  const { fn } = fakeController();
118
162
  const exec = await createAgentExecution(fn, {
@@ -126,6 +170,7 @@ describe("createAgentExecution", () => {
126
170
  model: "m",
127
171
  mode: "agent",
128
172
  serviceTier: "",
173
+ thinking: "",
129
174
  autoApproveAll: false,
130
175
  });
131
176
  expect(exec.spec?.sessionId).toBe("ses_1");
@@ -152,6 +197,7 @@ describe("createAgentExecution", () => {
152
197
  model: "",
153
198
  mode: "",
154
199
  serviceTier: "",
200
+ thinking: "",
155
201
  autoApproveAll: false,
156
202
  });
157
203
 
@@ -175,6 +221,7 @@ describe("createAgentExecution", () => {
175
221
  model: "",
176
222
  mode: "",
177
223
  serviceTier: "",
224
+ thinking: "",
178
225
  autoApproveAll: false,
179
226
  });
180
227
  expect(exec.spec?.sessionSpec).toBeUndefined();