@mnemom/mnemom 0.17.34 → 0.17.36

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 (177) hide show
  1. package/dist/commands/agent-config.js +3 -0
  2. package/dist/commands/agent-goal.d.ts +8 -3
  3. package/dist/commands/agent-goal.js +22 -8
  4. package/dist/commands/agent-run.d.ts +14 -2
  5. package/dist/commands/agent-run.js +28 -7
  6. package/dist/commands/agent-share-render.js +1 -1
  7. package/dist/commands/agent-slack.js +32 -0
  8. package/dist/commands/durable-inbox.d.ts +51 -0
  9. package/dist/commands/durable-inbox.js +134 -0
  10. package/dist/commands/durable-questions.d.ts +64 -0
  11. package/dist/commands/durable-questions.js +161 -0
  12. package/dist/commands/scan.d.ts +13 -3
  13. package/dist/commands/scan.js +48 -10
  14. package/dist/durable/seat-proxy.d.ts +12 -1
  15. package/dist/durable/seat-proxy.js +55 -9
  16. package/dist/durable/seat-socket.d.ts +20 -2
  17. package/dist/durable/seat-socket.js +58 -0
  18. package/dist/durable/turn-chain.d.ts +8 -0
  19. package/dist/durable/turn-chain.js +25 -4
  20. package/dist/host/client.d.ts +41 -0
  21. package/dist/host/client.js +7 -0
  22. package/dist/host/control.d.ts +7 -0
  23. package/dist/host/control.js +15 -0
  24. package/dist/host/daemon.d.ts +20 -0
  25. package/dist/host/daemon.js +74 -1
  26. package/dist/host/inbox-delivery.d.ts +174 -0
  27. package/dist/host/inbox-delivery.js +453 -0
  28. package/dist/host/run.d.ts +18 -0
  29. package/dist/host/run.js +61 -0
  30. package/dist/host/s1-names.d.ts +2 -0
  31. package/dist/host/s1-names.js +2 -0
  32. package/dist/host/tmux.d.ts +7 -0
  33. package/dist/host/tmux.js +19 -0
  34. package/dist/index.js +62 -3
  35. package/dist/lib/api.d.ts +5 -0
  36. package/dist/lib/api.js +16 -0
  37. package/dist/lib/config.d.ts +7 -0
  38. package/dist/lib/config.js +21 -0
  39. package/dist/lib/durable/api.d.ts +11 -0
  40. package/dist/lib/durable/api.js +28 -3
  41. package/dist/lib/durable/gate.d.ts +1 -1
  42. package/dist/lib/durable/gate.js +8 -1
  43. package/dist/lib/durable/live.d.ts +5 -0
  44. package/dist/lib/durable/live.js +19 -0
  45. package/dist/lib/experimental.js +3 -0
  46. package/dist/lib/goal-api.d.ts +11 -4
  47. package/dist/lib/goal-api.js +4 -3
  48. package/dist/lib/launcher-config.d.ts +5 -0
  49. package/dist/lib/launcher-config.js +17 -2
  50. package/dist/lib/prompt.d.ts +4 -2
  51. package/dist/lib/prompt.js +15 -5
  52. package/dist/lib/runner/job.d.ts +2 -2
  53. package/dist/lib/runner/job.js +16 -3
  54. package/dist/lib/runner/pr-body.d.ts +17 -0
  55. package/dist/lib/runner/pr-body.js +42 -0
  56. package/dist/lib/runner/proxy.d.ts +18 -3
  57. package/dist/lib/runner/proxy.js +24 -5
  58. package/dist/lib/scan/pr-source.js +2 -0
  59. package/dist/lib/scan/record.d.ts +60 -9
  60. package/dist/lib/scan/record.js +157 -39
  61. package/dist/lib/scan/repo-picker.d.ts +32 -0
  62. package/dist/lib/scan/repo-picker.js +80 -0
  63. package/dist/lib/scan/report.d.ts +6 -8
  64. package/dist/lib/scan/report.js +21 -7
  65. package/dist/lib/scan/share.d.ts +5 -1
  66. package/dist/lib/scan/share.js +15 -5
  67. package/dist/lib/slack-durable.d.ts +123 -0
  68. package/dist/lib/slack-durable.js +237 -0
  69. package/dist/lib/slack-router.d.ts +7 -0
  70. package/dist/lib/slack-router.js +50 -4
  71. package/dist/vendor/durable-contract/index.js +992 -34
  72. package/dist/vendor/shared/durable-contract/admission.d.ts +81 -2
  73. package/dist/vendor/shared/durable-contract/admission.js +319 -2
  74. package/dist/vendor/shared/durable-contract/cli-surface.js +21 -0
  75. package/dist/vendor/shared/durable-contract/codes.d.ts +60 -0
  76. package/dist/vendor/shared/durable-contract/codes.js +22 -0
  77. package/dist/vendor/shared/durable-contract/events.d.ts +4 -2
  78. package/dist/vendor/shared/durable-contract/events.js +55 -1
  79. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +14 -0
  80. package/dist/vendor/shared/durable-contract/inbox.d.ts +299 -0
  81. package/dist/vendor/shared/durable-contract/inbox.js +462 -0
  82. package/dist/vendor/shared/durable-contract/index.d.ts +2 -0
  83. package/dist/vendor/shared/durable-contract/index.js +2 -0
  84. package/dist/vendor/shared/durable-contract/question.d.ts +218 -0
  85. package/dist/vendor/shared/durable-contract/question.js +373 -0
  86. package/dist/vendor/shared/durable-contract/routes.d.ts +44 -2
  87. package/dist/vendor/shared/durable-contract/routes.js +43 -1
  88. package/dist/vendor/shared/durable-contract/turn.d.ts +15 -1
  89. package/dist/vendor/shared/durable-contract/turn.js +33 -1
  90. package/dist/vendor/shared/durable-contract/vectors.d.ts +45 -0
  91. package/dist/vendor/shared/durable-contract/vectors.js +49 -0
  92. package/node_modules/@mnemom/aip-verifier/LICENSE +190 -0
  93. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +478 -0
  94. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +478 -0
  95. package/node_modules/@mnemom/aip-verifier/dist/index.cjs +971 -0
  96. package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +1 -0
  97. package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +330 -0
  98. package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +330 -0
  99. package/node_modules/@mnemom/aip-verifier/dist/index.js +933 -0
  100. package/node_modules/@mnemom/aip-verifier/dist/index.js.map +1 -0
  101. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +879 -0
  102. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +1 -0
  103. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +96 -0
  104. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +96 -0
  105. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +875 -0
  106. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +1 -0
  107. package/node_modules/@mnemom/aip-verifier/package.json +86 -0
  108. package/node_modules/@noble/ed25519/LICENSE +21 -0
  109. package/node_modules/@noble/ed25519/README.md +371 -0
  110. package/node_modules/@noble/ed25519/index.d.ts +391 -0
  111. package/node_modules/@noble/ed25519/index.js +933 -0
  112. package/node_modules/@noble/ed25519/index.ts +1134 -0
  113. package/node_modules/@noble/ed25519/package.json +54 -0
  114. package/node_modules/@noble/hashes/LICENSE +21 -0
  115. package/node_modules/@noble/hashes/README.md +478 -0
  116. package/node_modules/@noble/hashes/_blake.d.ts +20 -0
  117. package/node_modules/@noble/hashes/_blake.js +59 -0
  118. package/node_modules/@noble/hashes/_md.d.ts +95 -0
  119. package/node_modules/@noble/hashes/_md.js +209 -0
  120. package/node_modules/@noble/hashes/_u64.d.ts +36 -0
  121. package/node_modules/@noble/hashes/_u64.js +77 -0
  122. package/node_modules/@noble/hashes/argon2.d.ts +148 -0
  123. package/node_modules/@noble/hashes/argon2.js +577 -0
  124. package/node_modules/@noble/hashes/blake1.d.ts +162 -0
  125. package/node_modules/@noble/hashes/blake1.js +630 -0
  126. package/node_modules/@noble/hashes/blake2.d.ts +203 -0
  127. package/node_modules/@noble/hashes/blake2.js +573 -0
  128. package/node_modules/@noble/hashes/blake3.d.ts +68 -0
  129. package/node_modules/@noble/hashes/blake3.js +310 -0
  130. package/node_modules/@noble/hashes/eskdf.d.ts +91 -0
  131. package/node_modules/@noble/hashes/eskdf.js +225 -0
  132. package/node_modules/@noble/hashes/hkdf.d.ts +70 -0
  133. package/node_modules/@noble/hashes/hkdf.js +152 -0
  134. package/node_modules/@noble/hashes/hmac.d.ts +50 -0
  135. package/node_modules/@noble/hashes/hmac.js +93 -0
  136. package/node_modules/@noble/hashes/index.d.ts +1 -0
  137. package/node_modules/@noble/hashes/index.js +34 -0
  138. package/node_modules/@noble/hashes/legacy.d.ts +107 -0
  139. package/node_modules/@noble/hashes/legacy.js +348 -0
  140. package/node_modules/@noble/hashes/package.json +91 -0
  141. package/node_modules/@noble/hashes/pbkdf2.d.ts +66 -0
  142. package/node_modules/@noble/hashes/pbkdf2.js +176 -0
  143. package/node_modules/@noble/hashes/scrypt.d.ts +98 -0
  144. package/node_modules/@noble/hashes/scrypt.js +304 -0
  145. package/node_modules/@noble/hashes/sha2.d.ts +176 -0
  146. package/node_modules/@noble/hashes/sha2.js +446 -0
  147. package/node_modules/@noble/hashes/sha3-addons.d.ts +533 -0
  148. package/node_modules/@noble/hashes/sha3-addons.js +973 -0
  149. package/node_modules/@noble/hashes/sha3.d.ts +208 -0
  150. package/node_modules/@noble/hashes/sha3.js +475 -0
  151. package/node_modules/@noble/hashes/src/_blake.ts +67 -0
  152. package/node_modules/@noble/hashes/src/_md.ts +227 -0
  153. package/node_modules/@noble/hashes/src/_u64.ts +111 -0
  154. package/node_modules/@noble/hashes/src/argon2.ts +693 -0
  155. package/node_modules/@noble/hashes/src/blake1.ts +677 -0
  156. package/node_modules/@noble/hashes/src/blake2.ts +648 -0
  157. package/node_modules/@noble/hashes/src/blake3.ts +344 -0
  158. package/node_modules/@noble/hashes/src/eskdf.ts +265 -0
  159. package/node_modules/@noble/hashes/src/hkdf.ts +161 -0
  160. package/node_modules/@noble/hashes/src/hmac.ts +130 -0
  161. package/node_modules/@noble/hashes/src/index.ts +33 -0
  162. package/node_modules/@noble/hashes/src/legacy.ts +361 -0
  163. package/node_modules/@noble/hashes/src/pbkdf2.ts +219 -0
  164. package/node_modules/@noble/hashes/src/scrypt.ts +375 -0
  165. package/node_modules/@noble/hashes/src/sha2.ts +486 -0
  166. package/node_modules/@noble/hashes/src/sha3-addons.ts +1157 -0
  167. package/node_modules/@noble/hashes/src/sha3.ts +525 -0
  168. package/node_modules/@noble/hashes/src/utils.ts +978 -0
  169. package/node_modules/@noble/hashes/src/webcrypto.ts +283 -0
  170. package/node_modules/@noble/hashes/utils.d.ts +560 -0
  171. package/node_modules/@noble/hashes/utils.js +687 -0
  172. package/node_modules/@noble/hashes/webcrypto.d.ts +121 -0
  173. package/node_modules/@noble/hashes/webcrypto.js +228 -0
  174. package/package.json +5 -2
  175. package/plugins/integrity-manifest.json +6 -2
  176. package/plugins/mnemom/hooks/hooks.json +12 -0
  177. package/plugins/mnemom/hooks/seat-ask.mjs +221 -0
@@ -224,6 +224,9 @@ function deleteKey(cfg, key) {
224
224
  case "slack_allow":
225
225
  delete cfg.slackAllow;
226
226
  break;
227
+ case "slack_owner":
228
+ delete cfg.slackOwner;
229
+ break;
227
230
  case "slack_aliases":
228
231
  delete cfg.slackAliases;
229
232
  break;
@@ -17,6 +17,7 @@
17
17
  * index.ts wires in. Key material is never printed.
18
18
  */
19
19
  import { type SessionRecord } from "../lib/agent-ledger.js";
20
+ import { type LauncherConfig } from "../lib/launcher-config.js";
20
21
  import { type FetchLike, type GoalControlCreds, type GoalShowResponse } from "../lib/goal-api.js";
21
22
  export interface GoalCommandDeps {
22
23
  /** The ledger, newest first. */
@@ -30,10 +31,14 @@ export interface GoalCommandDeps {
30
31
  err: (line: string) => void;
31
32
  }
32
33
  /**
33
- * The Anthropic key the launch would resolve (env, then ~/.mnemom/launcher.json), never prompted
34
- * for. Lazy import: commands/agent.ts is the launcher and carries a heavy import graph.
34
+ * The credential the launch would resolve, never prompted for. The auth mode resolves as the
35
+ * launch does (MNEMOM_AGENT_AUTH, else launcher.toml `auth` folded in under env, else api-key;
36
+ * host/seat-credential.ts does the same). api-key: the Anthropic key (env, then
37
+ * ~/.mnemom/launcher.json) plus the Mnemom key sent beside it. subscription: the launch had no
38
+ * Anthropic key and anchored on its Mnemom key, which the gateway control route accepts as the
39
+ * session credential. Lazy import: commands/agent.ts is the launcher and carries a heavy import graph.
35
40
  */
36
- export declare function resolveLaunchCreds(): Promise<GoalControlCreds>;
41
+ export declare function resolveLaunchCreds(env?: NodeJS.ProcessEnv, loadConfig?: () => LauncherConfig): Promise<GoalControlCreds>;
37
42
  export declare const liveGoalDeps: GoalCommandDeps;
38
43
  export interface GoalShowOptions {
39
44
  thread?: string;
@@ -18,22 +18,36 @@
18
18
  */
19
19
  import { readSessionRecords } from "../lib/agent-ledger.js";
20
20
  import { MNEMOM_DIR } from "../lib/config.js";
21
+ import { applyConfigToEnv, loadLauncherConfig, } from "../lib/launcher-config.js";
21
22
  import { fmt, formatRelativeAge } from "../lib/format.js";
22
23
  import { GoalControlError, goalControlFetch, isValidGoalThread, } from "../lib/goal-api.js";
23
24
  import { askYesNo, isInteractive } from "../lib/prompt.js";
24
25
  import { localGoalFlag, resolveSessionRef } from "./agent-sessions.js";
25
26
  /**
26
- * The Anthropic key the launch would resolve (env, then ~/.mnemom/launcher.json), never prompted
27
- * for. Lazy import: commands/agent.ts is the launcher and carries a heavy import graph.
27
+ * The credential the launch would resolve, never prompted for. The auth mode resolves as the
28
+ * launch does (MNEMOM_AGENT_AUTH, else launcher.toml `auth` folded in under env, else api-key;
29
+ * host/seat-credential.ts does the same). api-key: the Anthropic key (env, then
30
+ * ~/.mnemom/launcher.json) plus the Mnemom key sent beside it. subscription: the launch had no
31
+ * Anthropic key and anchored on its Mnemom key, which the gateway control route accepts as the
32
+ * session credential. Lazy import: commands/agent.ts is the launcher and carries a heavy import graph.
28
33
  */
29
- export async function resolveLaunchCreds() {
34
+ export async function resolveLaunchCreds(env = process.env, loadConfig = loadLauncherConfig) {
30
35
  const { defaultMnemomKeyDeps, resolveAnthropicKey } = await import("./agent.js");
31
- const { readMnemomKey } = await import("../lib/subscription-login.js");
36
+ const { readMnemomKey, resolveAgentAuth } = await import("../lib/subscription-login.js");
37
+ const folded = { ...env };
38
+ applyConfigToEnv(loadConfig(), folded);
39
+ // The Mnemom key the launch sent (env, else the store; never minted here) — a pinned session's
40
+ // control calls need it, and every call is then asked as the launch was.
41
+ const mnemomKey = readMnemomKey(defaultMnemomKeyDeps());
42
+ if (resolveAgentAuth(undefined, folded) === "subscription") {
43
+ if (!mnemomKey) {
44
+ throw new Error("auth = subscription and no Mnemom key in env (MNEMOM_AGENT_MNEMOM_KEY) or ~/.mnemom/launcher.json — " +
45
+ "the goal commands authenticate with the Mnemom key the session launched with");
46
+ }
47
+ return { mnemomKey };
48
+ }
32
49
  try {
33
- const anthropicKey = await resolveAnthropicKey(process.env, { allowPrompt: false });
34
- // The Mnemom key the launch sent beside it (env, else the store; never minted here) — a
35
- // pinned session's control calls need it, and every call is then asked as the launch was.
36
- const mnemomKey = readMnemomKey(defaultMnemomKeyDeps());
50
+ const anthropicKey = await resolveAnthropicKey(folded, { allowPrompt: false });
37
51
  return mnemomKey ? { anthropicKey, mnemomKey } : { anthropicKey };
38
52
  }
39
53
  catch {
@@ -9,6 +9,11 @@
9
9
  *
10
10
  * The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
11
11
  * git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
12
+ *
13
+ * So is the Anthropic key (ruling 34.30.1): the phases' model calls are billed to the runner's own
14
+ * ANTHROPIC_API_KEY, which only the model proxy holds and sets on each upstream request. A runner
15
+ * without one refuses at start, before any credential exchange or claim. The key is read once per
16
+ * process: after rotating it, restart a `--watch` runner.
12
17
  */
13
18
  import { type FetchFn, type RunnerApi } from "../lib/runner/executor-client.js";
14
19
  import { type GithubBases } from "../lib/runner/github-base.js";
@@ -19,6 +24,8 @@ export interface RunDeps {
19
24
  env: NodeJS.ProcessEnv;
20
25
  fetch: FetchFn;
21
26
  apiUrl: string;
27
+ /** The gateway the model proxy forwards to. */
28
+ gatewayUrl: string;
22
29
  /** stdout: one outcome line per job. */
23
30
  emit(line: string): void;
24
31
  /** stderr. */
@@ -27,8 +34,12 @@ export interface RunDeps {
27
34
  stop: AbortSignal;
28
35
  /** The text of this host's executor-state.json, or null when there is none. */
29
36
  readExecutorState?(): Promise<string | null>;
30
- jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases): JobDeps;
37
+ jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases, modelKey: string): JobDeps;
31
38
  }
39
+ /** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
40
+ export declare const RUNNER_MODEL_KEY_MISSING = "runner_model_key_missing";
41
+ /** The refusal of a non-production API paired with the production gateway; a config error, exit 2. */
42
+ export declare const RUNNER_GATEWAY_RING_MISMATCH = "runner_gateway_ring_mismatch";
32
43
  /** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
33
44
  export declare function agentRunCommand(flags: RunFlags, deps: RunDeps): Promise<number>;
34
45
  export declare const GITHUB_API: string;
@@ -45,7 +56,7 @@ export declare function cleanupWorkspace(ws: Workspace): Promise<void>;
45
56
  /** Commits on the job branch that no origin ref has: the clone's base is origin's default branch. */
46
57
  export declare function commitsAhead(git: Git, ws: Workspace): Promise<number>;
47
58
  /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
48
- export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string, apiBase?: string): Promise<string>;
59
+ export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string, body: string, apiBase?: string): Promise<string>;
49
60
  /** merge_on_green: GitHub's auto-merge on the PR (squash). */
50
61
  export declare function enableAutoMerge(fetch: FetchFn, token: string | null, prUrl: string, bases?: GithubBases): Promise<void>;
51
62
  /** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
@@ -55,6 +66,7 @@ export declare function liveJobDeps(api: RunnerApi, opts: {
55
66
  env: NodeJS.ProcessEnv;
56
67
  executorId: string | null;
57
68
  github: GithubBases;
69
+ modelKey: string;
58
70
  }): JobDeps;
59
71
  /** The process wiring: signals, stdout and stderr. Resolves the exit code. */
60
72
  export declare function runAgentRun(flags: RunFlags): Promise<number>;
@@ -9,13 +9,18 @@
9
9
  *
10
10
  * The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
11
11
  * git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
12
+ *
13
+ * So is the Anthropic key (ruling 34.30.1): the phases' model calls are billed to the runner's own
14
+ * ANTHROPIC_API_KEY, which only the model proxy holds and sets on each upstream request. A runner
15
+ * without one refuses at start, before any credential exchange or claim. The key is read once per
16
+ * process: after rotating it, restart a `--watch` runner.
12
17
  */
13
18
  import { spawn, execFile } from "node:child_process";
14
19
  import { randomUUID } from "node:crypto";
15
20
  import { mkdtemp, readFile, rm } from "node:fs/promises";
16
21
  import { homedir, tmpdir } from "node:os";
17
22
  import { join } from "node:path";
18
- import { getApiUrl, getGatewayUrl } from "../lib/config.js";
23
+ import { gatewayRingMismatch, getApiUrl, getGatewayUrl } from "../lib/config.js";
19
24
  import { runnerApi } from "../lib/runner/executor-client.js";
20
25
  import { resolveClaudeBin } from "../lib/claude-bin.js";
21
26
  import { executorIdFromState } from "../lib/runner/executor-state.js";
@@ -28,6 +33,10 @@ import { resolveRunPlan } from "../lib/runner/options.js";
28
33
  import { startModelProxy } from "../lib/runner/proxy.js";
29
34
  import { resolveGithubToken } from "./agent-fuel.js";
30
35
  import { executorStatePath } from "../vendor/durable-contract/index.js";
36
+ /** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
37
+ export const RUNNER_MODEL_KEY_MISSING = "runner_model_key_missing";
38
+ /** The refusal of a non-production API paired with the production gateway; a config error, exit 2. */
39
+ export const RUNNER_GATEWAY_RING_MISMATCH = "runner_gateway_ring_mismatch";
31
40
  /** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
32
41
  export async function agentRunCommand(flags, deps) {
33
42
  const resolved = resolveRunPlan(flags, deps.env);
@@ -38,6 +47,17 @@ export async function agentRunCommand(flags, deps) {
38
47
  const plan = resolved.plan;
39
48
  for (const w of plan.warnings)
40
49
  deps.err(w);
50
+ if (gatewayRingMismatch(deps.apiUrl, deps.gatewayUrl)) {
51
+ deps.err(`mnemom agent run: refused (${RUNNER_GATEWAY_RING_MISMATCH}): the API is ${deps.apiUrl} but the gateway is the production one, ${deps.gatewayUrl}; nothing was claimed`);
52
+ deps.err(" fix: set MNEMOM_GATEWAY_URL to the same cell's gateway as MNEMOM_API_URL");
53
+ return USAGE_EXIT_CODE;
54
+ }
55
+ const modelKey = deps.env.ANTHROPIC_API_KEY?.trim();
56
+ if (!modelKey) {
57
+ deps.err(`mnemom agent run: refused (${RUNNER_MODEL_KEY_MISSING}): this runner has no ANTHROPIC_API_KEY for its phases' model calls; nothing was claimed`);
58
+ deps.err(" fix: set ANTHROPIC_API_KEY on this machine or as a CI secret");
59
+ return USAGE_EXIT_CODE;
60
+ }
41
61
  const gh = resolveGithubBases(deps.env);
42
62
  if (gh.warning)
43
63
  deps.err(gh.warning);
@@ -70,7 +90,7 @@ export async function agentRunCommand(flags, deps) {
70
90
  }
71
91
  const api = runnerApi({ apiUrl: deps.apiUrl, executorKey, fetch: deps.fetch });
72
92
  const loop = { failMode: plan.failMode, stop: deps.stop, emit: deps.emit };
73
- const job = deps.jobDeps(api, executorId, gh.bases);
93
+ const job = deps.jobDeps(api, executorId, gh.bases, modelKey);
74
94
  return plan.form === "once" ? runOnce(job, loop) : runWatch(job, loop);
75
95
  }
76
96
  // ── GitHub and git, for the live driver ──────────────────────────────────────
@@ -146,14 +166,14 @@ export async function commitsAhead(git, ws) {
146
166
  return n;
147
167
  }
148
168
  /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
149
- export async function openPr(git, fetch, token, repo, ws, title, apiBase = GITHUB_BASES.api) {
169
+ export async function openPr(git, fetch, token, repo, ws, title, body, apiBase = GITHUB_BASES.api) {
150
170
  await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
151
171
  const meta = await github(fetch, token, "GET", `/repos/${repo}`, undefined, apiBase);
152
172
  const base = meta.json.default_branch;
153
173
  if (meta.status !== 200 || typeof base !== "string") {
154
174
  throw new Error(`reading ${repo} answered ${meta.status}`);
155
175
  }
156
- const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, { title, head: ws.branch, base }, apiBase);
176
+ const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, { title, body, head: ws.branch, base }, apiBase);
157
177
  const url = pr.json.html_url;
158
178
  if (pr.status !== 201 || typeof url !== "string") {
159
179
  throw new Error(`opening the PR answered ${pr.status}`);
@@ -214,10 +234,10 @@ export function liveJobDeps(api, opts) {
214
234
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
215
235
  log: (line) => process.stderr.write(`mnemom agent run: ${line}\n`),
216
236
  gatewayUrl: `${getGatewayUrl()}/anthropic`,
217
- startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state),
237
+ startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state, opts.modelKey),
218
238
  prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `${gh.web}/${repo}.git`, sessionId),
219
239
  runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
220
- openPr: (repo, ws, title) => openPr(git, opts.fetch, token, repo, ws, title, gh.api),
240
+ openPr: (repo, ws, title, body) => openPr(git, opts.fetch, token, repo, ws, title, body, gh.api),
221
241
  commitsAhead: (ws) => commitsAhead(git, ws),
222
242
  enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
223
243
  cleanup: cleanupWorkspace,
@@ -234,11 +254,12 @@ export function runAgentRun(flags) {
234
254
  env: process.env,
235
255
  fetch,
236
256
  apiUrl: getApiUrl(),
257
+ gatewayUrl: getGatewayUrl(),
237
258
  emit: (line) => process.stdout.write(`${line}\n`),
238
259
  err: (line) => process.stderr.write(`${line}\n`),
239
260
  stop: stop.signal,
240
261
  readExecutorState: () => readFile(executorStatePath(homedir()), "utf8").catch(() => null),
241
- jobDeps: (api, executorId, github) => liveJobDeps(api, { fetch, env: process.env, executorId, github }),
262
+ jobDeps: (api, executorId, github, modelKey) => liveJobDeps(api, { fetch, env: process.env, executorId, github, modelKey }),
242
263
  }).finally(() => {
243
264
  process.off("SIGTERM", onSignal);
244
265
  process.off("SIGINT", onSignal);
@@ -20,7 +20,7 @@ async function loadVerifier(load) {
20
20
  catch (error) {
21
21
  const e = error;
22
22
  if (e.code === "ERR_MODULE_NOT_FOUND" || e.code === "MODULE_NOT_FOUND") {
23
- throw new Error("the aip verifier (@mnemom/aip-verifier) is not installed: run `npm install https://github.com/mnemom/aip/releases/download/aip-verifier-v0.6.0/mnemom-aip-verifier-0.6.0.tgz`", { cause: error });
23
+ throw new Error("the aip verifier (@mnemom/aip-verifier) is not installed: it ships bundled inside @mnemom/mnemom, so reinstall with `npm install -g @mnemom/mnemom`", { cause: error });
24
24
  }
25
25
  throw new Error(`the aip verifier (@mnemom/aip-verifier) failed to load: ${e.message ?? error}`, { cause: error });
26
26
  }
@@ -26,6 +26,7 @@ import { mergeStore, readStore } from "../lib/launcher-store.js";
26
26
  import { askInput, isInteractive } from "../lib/prompt.js";
27
27
  import { explainSlackError, looksLikeAppToken, looksLikeBotToken, deleteMessage, postMessage, resolveSlackToken, sessionPosterName, slackAppManifest, slackCall, SLACK_APP_TOKEN_STORE_KEY, SLACK_TOKEN_STORE_KEY, } from "../lib/slack.js";
28
28
  import { routerStatus, runRouter } from "../lib/slack-router.js";
29
+ import { DurableSlack, slackDurableConfig } from "../lib/slack-durable.js";
29
30
  import { normHandle } from "../lib/slack-route.js";
30
31
  import { findOwnPost, freshInbox, listAliases, liveSessions, readInboxFrom, readOffset, recordPost, removeAlias, setAlias, writeOffset, } from "../lib/slack-state.js";
31
32
  const out = (line = "") => console.log(line);
@@ -96,6 +97,9 @@ async function login() {
96
97
  const me = (await askInput("Your Slack member ID — only allowlisted members can reach a session (U…): ")).trim();
97
98
  if (me) {
98
99
  configKeySpec("slack_allow")?.set(cfg, me);
100
+ // The member id asked for is the person's own: it is also the owner's (durable inbox).
101
+ if (!cfg.slackOwner?.length)
102
+ configKeySpec("slack_owner")?.set(cfg, me);
99
103
  writeLauncherConfig(cfg);
100
104
  }
101
105
  }
@@ -129,6 +133,8 @@ async function status() {
129
133
  out(`sticky threads: ${cfg.slackStickyThreads ? "on" : "off"}`);
130
134
  const allow = cfg.slackAllow ?? [];
131
135
  out(`allowed senders: ${allow.length ? allow.join(", ") : "none — no message will reach a session"}`);
136
+ const owner = cfg.slackOwner ?? [];
137
+ out(`owner: ${owner.length ? owner.join(", ") : "not set — `mnemom agent config set slack_owner <your member id>` (durable inbox)"}`);
132
138
  }
133
139
  export const POST_USAGE = `Usage: mnemom agent slack post [options] [--] <text…|->
134
140
 
@@ -305,6 +311,7 @@ async function router() {
305
311
  if (!app)
306
312
  throw new Error("mnemom agent slack router: no app-level token — run `mnemom agent slack login`.");
307
313
  const cfg = loadLauncherConfig();
314
+ const durable = await routerDurable();
308
315
  await runRouter({
309
316
  botToken: botToken(),
310
317
  appToken: app,
@@ -312,6 +319,31 @@ async function router() {
312
319
  // Both off unless the operator turned them on: off, routing is unchanged.
313
320
  aliases: cfg.slackAliases === true,
314
321
  stickyThreads: cfg.slackStickyThreads === true,
322
+ ...(durable ? { durable } : {}),
323
+ });
324
+ }
325
+ /**
326
+ * S1 amendment 60: with durable on, messages routed to a locked durable session go to its durable
327
+ * inbox (lib/slack-durable.ts). Off: undefined, and the router is unchanged.
328
+ */
329
+ async function routerDurable() {
330
+ const { loadExperimentalState } = await import("../lib/cli-config.js");
331
+ const { durableOn } = await import("../lib/durable/gate.js");
332
+ if (!durableOn(loadExperimentalState().enabled))
333
+ return undefined;
334
+ const { liveDurableApi } = await import("../lib/durable/live.js");
335
+ const api = liveDurableApi();
336
+ const conf = slackDurableConfig(process.env, loadLauncherConfig().slackOwner ?? []);
337
+ console.error(`[slack-router] durable inbox on: ${conf.ownerSlackIds.length} owner id(s), ${Object.keys(conf.agentSenders).length} agent sender(s)`);
338
+ if (conf.ownerSlackIds.length === 0)
339
+ console.error("[slack-router] no owner Slack id: your own messages to a locked durable session will not be sent. Fix: mnemom agent config set slack_owner <your member id>");
340
+ return new DurableSlack({
341
+ listSessions: (f) => api.listSessions(f),
342
+ send: (cid, agentId, body, credential) => api.inbox("own_inbox_send", cid, agentId, body, credential),
343
+ env: process.env,
344
+ ...conf,
345
+ cli: ["mnemom"],
346
+ now: () => Date.now(),
315
347
  });
316
348
  }
317
349
  async function listen() {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The durable inbox verbs (S1 amendment 60, signed off by Wassim 2026-11-02; ROUTES.own_inbox_*):
3
+ *
4
+ * mnemom agent sessions send <name> [text|-] [--idempotency-key k] [--json]
5
+ * mnemom agent sessions send <name> [text|-] --sender-key-env VAR --conversation <cid> --agent-id <id>
6
+ * mnemom agent sessions senders list|add|remove <name> [<agent>] [--label l] [--json]
7
+ *
8
+ * `send` is the owner door into a locked session: the api admits the message, the host daemon
9
+ * delivers it into the seat as the next prompt once the seat is idle, and the turn answering it is
10
+ * admitted unattended (A60 (e)). An allowlisted agent (chief) sends with its own agent-bound key,
11
+ * read from the environment variable --sender-key-env names (never from argv, so it never shows in
12
+ * `ps`); the api then records the message as from that agent on behalf of the owner. That agent cannot
13
+ * list the owner's sessions, so it addresses the session by --conversation and --agent-id.
14
+ *
15
+ * The body is exactly {text, idempotency_key}: the sender is never named by the caller. Every
16
+ * dependency is injected (`InboxVerbDeps`), so each branch is unit-tested.
17
+ */
18
+ import type { ApiAnswer, DurableCredential } from "../lib/durable/api.js";
19
+ import { type DurableVerbDeps, type VerbApi } from "./durable-verbs.js";
20
+ export type InboxRoute = "own_inbox_send" | "own_inbox_list" | "own_inbox_senders" | "own_inbox_sender_add" | "own_inbox_sender_remove";
21
+ /** The slice of DurableApi the inbox verbs use (a fake in tests). */
22
+ export interface InboxApi extends Pick<VerbApi, "listSessions" | "agentsByName"> {
23
+ inbox(route: InboxRoute, cid: string, agentId: string, body?: unknown, credential?: DurableCredential): Promise<ApiAnswer>;
24
+ }
25
+ export interface InboxVerbDeps extends Omit<DurableVerbDeps, "api"> {
26
+ api: InboxApi;
27
+ /** The environment (the sender key is read from it by name). */
28
+ env: Readonly<Record<string, string | undefined>>;
29
+ /** All of stdin, for `send <name> -` (and `send <name>` with no text). */
30
+ readStdin: () => Promise<string>;
31
+ newId?: () => string;
32
+ }
33
+ export interface SendOpts {
34
+ idempotencyKey?: string;
35
+ senderKeyEnv?: string;
36
+ conversation?: string;
37
+ agentId?: string;
38
+ json?: boolean;
39
+ }
40
+ /** The default idempotency key of one CLI send: a retry of the same command is a new message. */
41
+ export declare const cliIdempotencyKey: (id: string) => string;
42
+ /** The sender agent's key from the named environment variable; a usage refusal when unusable. */
43
+ export declare function senderCredential(envName: string | undefined, env: Readonly<Record<string, string | undefined>>): DurableCredential | undefined;
44
+ export declare function sendCommand(name: string, text: string | undefined, opts: SendOpts, deps: InboxVerbDeps): Promise<number>;
45
+ /** An agent id from a name or id: one agent of that name in the caller's orgs, else the word as an id. */
46
+ export declare function resolveAgentId(api: InboxApi, ref: string): Promise<string>;
47
+ export type SendersVerb = "list" | "add" | "remove";
48
+ export declare function sendersCommand(verb: SendersVerb, name: string, agent: string | undefined, opts: {
49
+ label?: string;
50
+ json?: boolean;
51
+ }, deps: InboxVerbDeps): Promise<number>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The durable inbox verbs (S1 amendment 60, signed off by Wassim 2026-11-02; ROUTES.own_inbox_*):
3
+ *
4
+ * mnemom agent sessions send <name> [text|-] [--idempotency-key k] [--json]
5
+ * mnemom agent sessions send <name> [text|-] --sender-key-env VAR --conversation <cid> --agent-id <id>
6
+ * mnemom agent sessions senders list|add|remove <name> [<agent>] [--label l] [--json]
7
+ *
8
+ * `send` is the owner door into a locked session: the api admits the message, the host daemon
9
+ * delivers it into the seat as the next prompt once the seat is idle, and the turn answering it is
10
+ * admitted unattended (A60 (e)). An allowlisted agent (chief) sends with its own agent-bound key,
11
+ * read from the environment variable --sender-key-env names (never from argv, so it never shows in
12
+ * `ps`); the api then records the message as from that agent on behalf of the owner. That agent cannot
13
+ * list the owner's sessions, so it addresses the session by --conversation and --agent-id.
14
+ *
15
+ * The body is exactly {text, idempotency_key}: the sender is never named by the caller. Every
16
+ * dependency is injected (`InboxVerbDeps`), so each branch is unit-tested.
17
+ */
18
+ import { randomUUID } from "node:crypto";
19
+ import { INBOX_AGENT_ID_RE, INBOX_IDEMPOTENCY_KEY_RE, INBOX_SENDER_LABEL_RE, inboxBodyOk, inboxSendResponseOk, inboxSenderChangeResponseOk, inboxSendersResponseOk, } from "../vendor/shared/durable-contract/inbox.js";
20
+ import { DurableRefusal } from "../lib/durable/refusal.js";
21
+ import { handleVerbError, refusalFromAnswer, resolveTarget, } from "./durable-verbs.js";
22
+ const usage = (message) => new DurableRefusal({ code: "invalid_argument", message, fix: null, exit: 2 });
23
+ /** The default idempotency key of one CLI send: a retry of the same command is a new message. */
24
+ export const cliIdempotencyKey = (id) => `cli:${id}`;
25
+ /** The session a send goes to: by name through the registry, or by --conversation/--agent-id. */
26
+ async function sendTarget(name, opts, api) {
27
+ if (opts.conversation !== undefined || opts.agentId !== undefined) {
28
+ if (!opts.conversation || !opts.agentId)
29
+ throw usage("--conversation and --agent-id go together");
30
+ if (!INBOX_AGENT_ID_RE.test(opts.agentId))
31
+ throw usage(`--agent-id is not an agent id (got '${opts.agentId}')`);
32
+ return { cid: opts.conversation, agentId: opts.agentId };
33
+ }
34
+ if (opts.senderKeyEnv !== undefined)
35
+ throw usage("--sender-key-env sends as another agent, which cannot look the session up: give --conversation and --agent-id too");
36
+ const row = await resolveTarget(api, name);
37
+ return { cid: row.conversation_id, agentId: row.agent_id };
38
+ }
39
+ /** The sender agent's key from the named environment variable; a usage refusal when unusable. */
40
+ export function senderCredential(envName, env) {
41
+ if (envName === undefined)
42
+ return undefined;
43
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(envName))
44
+ throw usage(`--sender-key-env takes an environment variable name (got '${envName}')`);
45
+ const key = env[envName]?.trim();
46
+ if (!key)
47
+ throw usage(`--sender-key-env ${envName}: that variable is not set`);
48
+ return { type: "api-key", key };
49
+ }
50
+ export async function sendCommand(name, text, opts, deps) {
51
+ const json = opts.json === true;
52
+ try {
53
+ const body = text === undefined || text === "-" ? await deps.readStdin() : text;
54
+ if (!inboxBodyOk(body))
55
+ throw usage("the message must be 1..16384 UTF-8 bytes with no control character but tab and newline (give it as an argument or on stdin)");
56
+ const key = opts.idempotencyKey ?? cliIdempotencyKey((deps.newId ?? randomUUID)());
57
+ if (!INBOX_IDEMPOTENCY_KEY_RE.test(key))
58
+ throw usage(`--idempotency-key must be 1..200 letters, digits or _ . : - (got '${key.slice(0, 40)}')`);
59
+ const credential = senderCredential(opts.senderKeyEnv, deps.env);
60
+ const t = await sendTarget(name, opts, deps.api);
61
+ const a = await deps.api.inbox("own_inbox_send", t.cid, t.agentId, { text: body, idempotency_key: key }, credential);
62
+ if (a.status !== 200 || !inboxSendResponseOk(a.body))
63
+ throw refusalFromAnswer(a, name, deps.launcherKey());
64
+ const r = a.body;
65
+ const say = json ? deps.err : deps.out;
66
+ const how = r.deduped
67
+ ? "already sent (same idempotency key)"
68
+ : r.coalesced
69
+ ? "already waiting (same message still open)"
70
+ : "queued";
71
+ say(`${name}: ${how}; inbox ${r.inbox_id} (${r.sha.slice(0, 12)}), ${r.status}${r.sender_kind === "agent" ? ", sent as an allowlisted agent on behalf of the owner" : ""}`);
72
+ if (json)
73
+ deps.out(JSON.stringify(a.body));
74
+ return 0;
75
+ }
76
+ catch (e) {
77
+ return handleVerbError(e, json, deps);
78
+ }
79
+ }
80
+ /** An agent id from a name or id: one agent of that name in the caller's orgs, else the word as an id. */
81
+ export async function resolveAgentId(api, ref) {
82
+ const byName = await api.agentsByName(ref).catch(() => []);
83
+ if (byName.length > 1)
84
+ throw usage(`more than one agent is named ${ref}: give its id (mnemom agent list)`);
85
+ const id = byName[0]?.id ?? ref;
86
+ if (!INBOX_AGENT_ID_RE.test(id))
87
+ throw usage(`'${ref}' is not an agent name or id`);
88
+ return id;
89
+ }
90
+ export async function sendersCommand(verb, name, agent, opts, deps) {
91
+ const json = opts.json === true;
92
+ const say = json ? deps.err : deps.out;
93
+ try {
94
+ if (verb !== "list" && agent === undefined)
95
+ throw usage(`mnemom agent sessions senders ${verb} <name> <agent>`);
96
+ if (opts.label !== undefined && (verb !== "add" || !INBOX_SENDER_LABEL_RE.test(opts.label)))
97
+ throw usage("--label goes with `add`: one line of printable ASCII, no brackets, 1..128");
98
+ const row = await resolveTarget(deps.api, name);
99
+ if (verb === "list") {
100
+ const a = await deps.api.inbox("own_inbox_senders", row.conversation_id, row.agent_id);
101
+ if (a.status !== 200 || !inboxSendersResponseOk(a.body))
102
+ throw refusalFromAnswer(a, name, deps.launcherKey(), row);
103
+ const b = a.body;
104
+ if (json)
105
+ deps.out(JSON.stringify(a.body));
106
+ else if (b.senders.length === 0)
107
+ say(`${name}: only the owner may send to agent ${b.recipient_agent_id}'s inbox`);
108
+ else {
109
+ say(`${name}: agents that may send to agent ${b.recipient_agent_id}'s inbox:`);
110
+ for (const s of b.senders)
111
+ say(` ${s.sender_agent_id}${s.label ? ` ${s.label}` : ""} (since ${s.created_at})`);
112
+ }
113
+ return 0;
114
+ }
115
+ const agentId = await resolveAgentId(deps.api, agent);
116
+ const route = verb === "add" ? "own_inbox_sender_add" : "own_inbox_sender_remove";
117
+ const body = verb === "add" && opts.label !== undefined
118
+ ? { agent_id: agentId, label: opts.label }
119
+ : { agent_id: agentId };
120
+ const a = await deps.api.inbox(route, row.conversation_id, row.agent_id, body);
121
+ if (a.status !== 200 || !inboxSenderChangeResponseOk(a.body))
122
+ throw refusalFromAnswer(a, name, deps.launcherKey(), row);
123
+ const changed = a.body.changed;
124
+ say(verb === "add"
125
+ ? `${name}: agent ${agentId} ${changed ? "may now send" : "could already send"} to this agent's inbox`
126
+ : `${name}: agent ${agentId} ${changed ? "may no longer send" : "was not allowed to send"} to this agent's inbox`);
127
+ if (json)
128
+ deps.out(JSON.stringify(a.body));
129
+ return 0;
130
+ }
131
+ catch (e) {
132
+ return handleVerbError(e, json, deps);
133
+ }
134
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The durable question verbs (S1 amendment 61, signed off by Wassim 2026-11-02; ROUTES.own_questions and
3
+ * ROUTES.own_answer):
4
+ *
5
+ * mnemom agent sessions questions <name> [--all] [--json]
6
+ * mnemom agent sessions answer <name> <qid|latest> [choice...] [--dismiss] [--json]
7
+ *
8
+ * A locked seat that calls AskUserQuestion records the question at the api (question.asked) instead
9
+ * of drawing a dialog nobody sees. `questions` lists them (the open ones by default), with the option
10
+ * the seat recommends marked; `answer` answers one (question.answered, via coding_agent) or dismisses
11
+ * it. Each choice is a 1-based option number (comma separated for a multi-select question), an
12
+ * option label, or free text; with several questions, one choice per question in order. The seat
13
+ * gets the answer either live (its hook is still waiting) or as its next inbox message.
14
+ *
15
+ * Every dependency is injected (`QuestionVerbDeps`), so each branch is unit-tested.
16
+ */
17
+ import { type QuestionItem, type QuestionState } from "../vendor/shared/durable-contract/question.js";
18
+ import type { ApiAnswer } from "../lib/durable/api.js";
19
+ import { type DurableVerbDeps, type VerbApi } from "./durable-verbs.js";
20
+ export type QuestionRoute = "own_questions" | "own_answer";
21
+ /** The slice of DurableApi the question verbs use (a fake in tests). */
22
+ export interface QuestionApi extends Pick<VerbApi, "listSessions"> {
23
+ questions(route: QuestionRoute, cid: string, agentId: string, body?: unknown): Promise<ApiAnswer>;
24
+ }
25
+ export interface QuestionVerbDeps extends Omit<DurableVerbDeps, "api"> {
26
+ api: QuestionApi;
27
+ }
28
+ /** One own_questions item as the CLI reads it (shape checked by questionListResponseOk). */
29
+ export interface QuestionRow {
30
+ approval_id: string;
31
+ state: QuestionState;
32
+ questions: QuestionItem[];
33
+ recommended: Array<number | null>;
34
+ answers: Record<string, string> | null;
35
+ answered_via: string | null;
36
+ created_at: string;
37
+ expires_at: string;
38
+ answered_at: string | null;
39
+ }
40
+ /**
41
+ * Whether the owner can still answer `q`. The api reports a question the seat released as
42
+ * `handed_off` whether or not it was answered since (S1 question.ts: answered and handed_off can
43
+ * both hold), so a handed-off question is still waiting for the owner while it has no answered_at
44
+ * and has not expired. Its answer then reaches the seat as an inbox message (S1 §61 (c)/(e)).
45
+ */
46
+ export declare function awaitingAnswer(q: QuestionRow, now: number): boolean;
47
+ /**
48
+ * Text the seat's model wrote, made safe for the owner's terminal: every C0/C1 control character
49
+ * (ESC, CSI, OSC, BEL, CR, ...) and DEL is shown escaped (`\x1b`), so a question cannot move the
50
+ * cursor, rewrite or hide a line, or write the clipboard. S1 question.ts only refuses NUL.
51
+ */
52
+ export declare function terminalSafe(s: string): string;
53
+ /** The human lines for one question row. */
54
+ export declare function renderQuestion(q: QuestionRow, now?: number): string[];
55
+ export declare function questionsCommand(name: string, opts: {
56
+ all?: boolean;
57
+ json?: boolean;
58
+ }, deps: QuestionVerbDeps): Promise<number>;
59
+ /** The answers map for one question row from the owner's choices (one per question, in order). */
60
+ export declare function answersFromChoices(q: QuestionRow, choices: readonly string[]): Record<string, string>;
61
+ export declare function answerCommand(name: string, qid: string, choices: readonly string[], opts: {
62
+ dismiss?: boolean;
63
+ json?: boolean;
64
+ }, deps: QuestionVerbDeps): Promise<number>;