@mnemom/mnemom 0.17.34-next.1 → 0.17.34-next.3

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 (236) hide show
  1. package/dist/base-url-proxy.mjs +18 -4
  2. package/dist/commands/agent-config.js +5 -1
  3. package/dist/commands/agent-doctor.d.ts +9 -0
  4. package/dist/commands/agent-doctor.js +4 -0
  5. package/dist/commands/agent-run.d.ts +14 -6
  6. package/dist/commands/agent-run.js +44 -27
  7. package/dist/commands/agent-session.d.ts +1 -2
  8. package/dist/commands/agent-session.js +1 -2
  9. package/dist/commands/agent-setup.d.ts +6 -0
  10. package/dist/commands/agent-setup.js +12 -1
  11. package/dist/commands/agent-share-test-issuer.d.ts +67 -0
  12. package/dist/commands/agent-share-test-issuer.js +139 -0
  13. package/dist/commands/agent-share.d.ts +7 -1
  14. package/dist/commands/agent-share.js +14 -4
  15. package/dist/commands/agent.d.ts +18 -5
  16. package/dist/commands/agent.js +186 -59
  17. package/dist/commands/durable-doctor.d.ts +50 -0
  18. package/dist/commands/durable-doctor.js +186 -0
  19. package/dist/commands/durable-lock.d.ts +261 -0
  20. package/dist/commands/durable-lock.js +600 -0
  21. package/dist/commands/durable-sessions.d.ts +87 -0
  22. package/dist/commands/durable-sessions.js +303 -0
  23. package/dist/commands/durable-verbs.d.ts +116 -0
  24. package/dist/commands/durable-verbs.js +393 -0
  25. package/dist/commands/scan.d.ts +12 -1
  26. package/dist/commands/scan.js +22 -3
  27. package/dist/commands/wrap.js +1 -1
  28. package/dist/durable/adopt-live.js +5 -8
  29. package/dist/durable/adopt.d.ts +8 -0
  30. package/dist/durable/adopt.js +27 -6
  31. package/dist/durable/cwd-rule.d.ts +9 -1
  32. package/dist/durable/cwd-rule.js +18 -0
  33. package/dist/durable/flock.d.ts +26 -0
  34. package/dist/durable/flock.js +80 -0
  35. package/dist/durable/launch-spec.d.ts +9 -1
  36. package/dist/durable/launch-spec.js +11 -4
  37. package/dist/durable/seat-proxy-entry.d.ts +11 -0
  38. package/dist/durable/seat-proxy-entry.js +25 -0
  39. package/dist/durable/seat-proxy.d.ts +107 -0
  40. package/dist/durable/seat-proxy.js +599 -0
  41. package/dist/durable/seat-socket.d.ts +46 -0
  42. package/dist/durable/seat-socket.js +179 -0
  43. package/dist/durable/seat-spec-validator.d.ts +28 -0
  44. package/dist/durable/seat-spec-validator.js +59 -0
  45. package/dist/durable/test-mode.mjs +105 -0
  46. package/dist/durable/turn-chain.d.ts +55 -0
  47. package/dist/durable/turn-chain.js +178 -0
  48. package/dist/host/auto-continue.d.ts +67 -0
  49. package/dist/host/auto-continue.js +118 -0
  50. package/dist/host/bootid.d.ts +13 -0
  51. package/dist/host/bootid.js +41 -0
  52. package/dist/host/client.d.ts +125 -0
  53. package/dist/host/client.js +171 -0
  54. package/dist/host/commands.d.ts +2 -0
  55. package/dist/host/commands.js +122 -0
  56. package/dist/host/control.d.ts +33 -0
  57. package/dist/host/control.js +144 -0
  58. package/dist/host/daemon.d.ts +221 -0
  59. package/dist/host/daemon.js +616 -0
  60. package/dist/host/gate.d.ts +46 -0
  61. package/dist/host/gate.js +82 -0
  62. package/dist/host/grant.d.ts +24 -0
  63. package/dist/host/grant.js +24 -0
  64. package/dist/host/instance.d.ts +35 -0
  65. package/dist/host/instance.js +62 -0
  66. package/dist/host/integrity.d.ts +35 -0
  67. package/dist/host/integrity.js +115 -0
  68. package/dist/host/launchd.d.ts +16 -0
  69. package/dist/host/launchd.js +45 -0
  70. package/dist/host/lock.d.ts +20 -0
  71. package/dist/host/lock.js +63 -0
  72. package/dist/host/owner.d.ts +58 -0
  73. package/dist/host/owner.js +123 -0
  74. package/dist/host/plist.d.ts +16 -0
  75. package/dist/host/plist.js +85 -0
  76. package/dist/host/procs.d.ts +27 -0
  77. package/dist/host/procs.js +105 -0
  78. package/dist/host/run.d.ts +65 -0
  79. package/dist/host/run.js +299 -0
  80. package/dist/host/runtime.d.ts +8 -0
  81. package/dist/host/runtime.js +22 -0
  82. package/dist/host/s1-names.d.ts +23 -0
  83. package/dist/host/s1-names.js +22 -0
  84. package/dist/host/seat-credential.d.ts +27 -0
  85. package/dist/host/seat-credential.js +41 -0
  86. package/dist/host/seat-launch-main.d.ts +1 -0
  87. package/dist/host/seat-launch-main.js +59 -0
  88. package/dist/host/seat-launch.d.ts +116 -0
  89. package/dist/host/seat-launch.js +302 -0
  90. package/dist/host/seat.d.ts +48 -0
  91. package/dist/host/seat.js +149 -0
  92. package/dist/host/state.d.ts +33 -0
  93. package/dist/host/state.js +117 -0
  94. package/dist/host/status.d.ts +101 -0
  95. package/dist/host/status.js +307 -0
  96. package/dist/host/tmux.d.ts +56 -0
  97. package/dist/host/tmux.js +182 -0
  98. package/dist/host/transcript.d.ts +30 -0
  99. package/dist/host/transcript.js +130 -0
  100. package/dist/host/verbs.d.ts +52 -0
  101. package/dist/host/verbs.js +282 -0
  102. package/dist/index.d.ts +8 -6
  103. package/dist/index.js +209 -43
  104. package/dist/lib/agent.d.ts +13 -29
  105. package/dist/lib/agent.js +17 -29
  106. package/dist/lib/claude-bin.d.ts +6 -0
  107. package/dist/lib/claude-bin.js +26 -0
  108. package/dist/lib/claude-plugin.d.ts +11 -2
  109. package/dist/lib/claude-plugin.js +10 -1
  110. package/dist/lib/commit-report.d.ts +5 -4
  111. package/dist/lib/commit-report.js +6 -9
  112. package/dist/lib/config.d.ts +8 -1
  113. package/dist/lib/config.js +28 -0
  114. package/dist/lib/durable/api.d.ts +136 -0
  115. package/dist/lib/durable/api.js +220 -0
  116. package/dist/lib/durable/codes.d.ts +8 -0
  117. package/dist/lib/durable/codes.js +2 -0
  118. package/dist/lib/durable/gate.d.ts +48 -0
  119. package/dist/lib/durable/gate.js +122 -0
  120. package/dist/lib/durable/handover.d.ts +101 -0
  121. package/dist/lib/durable/handover.js +175 -0
  122. package/dist/lib/durable/launch.d.ts +199 -0
  123. package/dist/lib/durable/launch.js +461 -0
  124. package/dist/lib/durable/launching.d.ts +15 -0
  125. package/dist/lib/durable/launching.js +70 -0
  126. package/dist/lib/durable/live.d.ts +190 -0
  127. package/dist/lib/durable/live.js +633 -0
  128. package/dist/lib/durable/refusal.d.ts +57 -0
  129. package/dist/lib/durable/refusal.js +78 -0
  130. package/dist/lib/durable/view.d.ts +83 -0
  131. package/dist/lib/durable/view.js +244 -0
  132. package/dist/lib/experimental.d.ts +0 -9
  133. package/dist/lib/experimental.js +18 -17
  134. package/dist/lib/launcher-config.d.ts +13 -0
  135. package/dist/lib/launcher-config.js +32 -0
  136. package/dist/lib/runner/exec-append.d.ts +15 -0
  137. package/dist/lib/runner/exec-append.js +2 -0
  138. package/dist/lib/runner/executor-client.d.ts +2 -1
  139. package/dist/lib/runner/executor-state.d.ts +10 -0
  140. package/dist/lib/runner/executor-state.js +24 -0
  141. package/dist/lib/runner/github-base.d.ts +24 -0
  142. package/dist/lib/runner/github-base.js +27 -0
  143. package/dist/lib/runner/job.d.ts +9 -4
  144. package/dist/lib/runner/job.js +43 -15
  145. package/dist/lib/runner/oidc.d.ts +3 -2
  146. package/dist/lib/runner/oidc.js +7 -4
  147. package/dist/lib/runner/options.d.ts +6 -0
  148. package/dist/lib/runner/options.js +19 -1
  149. package/dist/lib/runner/phases.d.ts +7 -4
  150. package/dist/lib/runner/phases.js +8 -4
  151. package/dist/lib/runner/proxy.d.ts +5 -1
  152. package/dist/lib/runner/proxy.js +7 -1
  153. package/dist/lib/scan/cost-per-pr.d.ts +93 -0
  154. package/dist/lib/scan/cost-per-pr.js +101 -0
  155. package/dist/lib/scan/record.d.ts +27 -1
  156. package/dist/lib/scan/record.js +114 -16
  157. package/dist/lib/scan/report-email.d.ts +26 -6
  158. package/dist/lib/scan/report-email.js +32 -3
  159. package/dist/lib/scan/scan.d.ts +3 -0
  160. package/dist/lib/scan/scan.js +13 -1
  161. package/dist/lib/subscription-login.d.ts +15 -0
  162. package/dist/lib/subscription-login.js +28 -1
  163. package/dist/vendor/durable-contract/index.js +240 -18
  164. package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
  165. package/dist/vendor/shared/durable-contract/admission.js +533 -0
  166. package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
  167. package/dist/vendor/shared/durable-contract/arm.js +133 -0
  168. package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
  169. package/dist/vendor/shared/durable-contract/canonical.js +98 -0
  170. package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
  171. package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
  172. package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
  173. package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
  174. package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
  175. package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
  176. package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
  177. package/dist/vendor/shared/durable-contract/codes.js +475 -0
  178. package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
  179. package/dist/vendor/shared/durable-contract/constants.js +179 -0
  180. package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
  181. package/dist/vendor/shared/durable-contract/doors.js +249 -0
  182. package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
  183. package/dist/vendor/shared/durable-contract/envelope.js +317 -0
  184. package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
  185. package/dist/vendor/shared/durable-contract/events.js +630 -0
  186. package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
  187. package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
  188. package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
  189. package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
  190. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
  191. package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
  192. package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
  193. package/dist/vendor/shared/durable-contract/index.js +37 -0
  194. package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
  195. package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
  196. package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
  197. package/dist/vendor/shared/durable-contract/keys.js +66 -0
  198. package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
  199. package/dist/vendor/shared/durable-contract/lock.js +151 -0
  200. package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
  201. package/dist/vendor/shared/durable-contract/names.js +78 -0
  202. package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
  203. package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
  204. package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
  205. package/dist/vendor/shared/durable-contract/paths.js +35 -0
  206. package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
  207. package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
  208. package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
  209. package/dist/vendor/shared/durable-contract/routes.js +315 -0
  210. package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
  211. package/dist/vendor/shared/durable-contract/schema.js +441 -0
  212. package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
  213. package/dist/vendor/shared/durable-contract/seal.js +99 -0
  214. package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
  215. package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
  216. package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
  217. package/dist/vendor/shared/durable-contract/session-id.js +58 -0
  218. package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
  219. package/dist/vendor/shared/durable-contract/spec.js +286 -0
  220. package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
  221. package/dist/vendor/shared/durable-contract/states.js +218 -0
  222. package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
  223. package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
  224. package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
  225. package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
  226. package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
  227. package/dist/vendor/shared/durable-contract/turn.js +81 -0
  228. package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
  229. package/dist/vendor/shared/durable-contract/vectors.js +208 -0
  230. package/package.json +3 -2
  231. package/plugins/integrity-manifest.json +35 -0
  232. package/plugins/mnemom/hooks/hooks.json +15 -1
  233. package/plugins/mnemom/hooks/register.tsx +60 -2
  234. package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
  235. package/dist/commands/agent-implicit-notice.d.ts +0 -31
  236. package/dist/commands/agent-implicit-notice.js +0 -73
@@ -0,0 +1,393 @@
1
+ /**
2
+ * The flat durable owner verbs (BACKBONE C1; S1 owner-verbs.ts, ROUTES.own_*; C11, C16):
3
+ *
4
+ * mnemom agent pause|resume|done|kill <name> [--json]
5
+ * mnemom agent amend <name> [--budget <n>] [--turns <n>] [--json]
6
+ * mnemom agent sessions unlock <name>
7
+ * mnemom agent containment pause|resume <agent>
8
+ *
9
+ * The API decides (S1 owner-verb table, first match wins); the CLI resolves the name through the
10
+ * registry, sends the verb, and renders the answer: a refusal with its S1 fix line and machine
11
+ * code, a 200 no-op with its state, a 202 device step (user_code + verification_uri_complete,
12
+ * polled at the server interval with the 202's device_code and client_id; never a token printed
13
+ * or kept), or the applied change. `done`/`kill` on a tracked session also stop the local tracked
14
+ * claude after the 200 (final F04; SIGTERM, SIGKILL after TRACKED_STOP_GRACE_S).
15
+ *
16
+ * Every dependency is injected (`DurableVerbDeps`), so each branch is unit-tested.
17
+ */
18
+ import { PER_SESSION_BUDGET_LIMIT_USD, TRACKED_STOP_GRACE_S, } from "../vendor/shared/durable-contract/constants.js";
19
+ import { renderFixLine, } from "../vendor/shared/durable-contract/fixlines.js";
20
+ import { errorCodeOf, errorDetailsOf, errorMessageOf, RegistryAnswerError, RegistryUnavailable, } from "../lib/durable/api.js";
21
+ import { DurableRefusal, printDurableRefusal } from "../lib/durable/refusal.js";
22
+ import { isLocked, liveRow, stateLabel } from "../lib/durable/view.js";
23
+ import { CODE } from "../lib/durable/codes.js";
24
+ const VERB_ROUTE = {
25
+ pause: "own_pause",
26
+ resume: "own_resume",
27
+ done: "own_complete",
28
+ kill: "own_kill",
29
+ amend: "own_amend",
30
+ unlock: "own_unlock",
31
+ };
32
+ const APPLIED_TEXT = {
33
+ pause: "paused",
34
+ resume: "resumed; the host daemon picks it up",
35
+ done: "done",
36
+ kill: "killed",
37
+ amend: "amended",
38
+ unlock: "unlocked",
39
+ };
40
+ /** The session a verb acts on: the non-terminal row holding the name, else the newest row. */
41
+ export async function resolveTarget(api, name) {
42
+ let rows;
43
+ try {
44
+ rows = await api.listSessions({ name });
45
+ }
46
+ catch (e) {
47
+ if (e instanceof RegistryAnswerError)
48
+ throw refusalFromAnswer(e.answer, name, "off");
49
+ throw e;
50
+ }
51
+ const row = liveRow(rows) ?? rows[0] ?? null;
52
+ if (row === null) {
53
+ throw new DurableRefusal({
54
+ code: "agent_not_found",
55
+ message: `no session named ${name}`,
56
+ fix: null,
57
+ });
58
+ }
59
+ return row;
60
+ }
61
+ /**
62
+ * A refusal from an API answer: the API's message and code, its rendered fix (details.fix, as the
63
+ * API already applied the fix-line rule), else the CLI's catalogue fallback for that code.
64
+ */
65
+ export function refusalFromAnswer(a, name, launcherKey, row) {
66
+ const code = errorCodeOf(a.body) ?? `http_${a.status}`;
67
+ const details = errorDetailsOf(a.body);
68
+ const apiFix = typeof details.fix === "string" && details.fix !== "" ? details.fix : null;
69
+ const fix = apiFix ?? fallbackFix(code, details, name, launcherKey, row);
70
+ const apiMessage = errorMessageOf(a.body);
71
+ // The refusal line always names the session the verb was for (C02-R05: the user sees what failed).
72
+ const message = apiMessage === null
73
+ ? `${name}: refused (${a.status})`
74
+ : apiMessage.includes(name)
75
+ ? apiMessage
76
+ : `${name}: ${apiMessage}`;
77
+ return new DurableRefusal({ code, message: sanitizeLine(message), fix, apiBody: a.body });
78
+ }
79
+ /** A one-line message: no control characters reach the terminal. */
80
+ function sanitizeLine(s) {
81
+ return (s
82
+ // eslint-disable-next-line no-control-regex
83
+ .replace(/[\u0000-\u001f\u007f]+/g, " ")
84
+ .replace(/ {2,}/g, " ")
85
+ .trim());
86
+ }
87
+ function render(key, params, launcherKey, held) {
88
+ return renderFixLine(key, { launcherKey, nameHeld: held, params });
89
+ }
90
+ /** The catalogue fix the CLI prints when the API body carries none. */
91
+ export function fallbackFix(code, details, name, launcherKey, row) {
92
+ const agent = row?.agent_name ?? row?.agent_id ?? "<agent>";
93
+ switch (code) {
94
+ case CODE.durable_session_ended:
95
+ return details.state_reason === "agent_killed"
96
+ ? render("agent_killed", { name }, launcherKey, false)
97
+ : render("session_ended", { name }, launcherKey, false);
98
+ case CODE.durable_agent_contained:
99
+ return render("agent_contained", { agent }, launcherKey, true);
100
+ case CODE.durable_not_owner:
101
+ return typeof details.owner === "string"
102
+ ? render("not_owner_info", { owner: details.owner }, launcherKey, true)
103
+ : null;
104
+ case CODE.durable_session_login_required:
105
+ return render("device_step_login", {}, launcherKey, true);
106
+ case CODE.durable_unsupported:
107
+ if (details.feature === "launch_spec_change") {
108
+ // The flags come from the answer; without them there is no line to print.
109
+ const flags = Array.isArray(details.flags)
110
+ ? details.flags.filter((f) => typeof f === "string").join(" ")
111
+ : typeof details.flags === "string"
112
+ ? details.flags
113
+ : "";
114
+ return flags.trim() === ""
115
+ ? null
116
+ : render("launch_spec_change", { name, flags: flags.trim() }, launcherKey, true);
117
+ }
118
+ if (details.feature === "above_limit_raise")
119
+ return render("above_limit_raise", { name, limit: String(PER_SESSION_BUDGET_LIMIT_USD) }, launcherKey, true);
120
+ return null;
121
+ default:
122
+ return null;
123
+ }
124
+ }
125
+ function isRecord(v) {
126
+ return v !== null && typeof v === "object" && !Array.isArray(v);
127
+ }
128
+ export function deviceStepOf(body) {
129
+ if (!isRecord(body))
130
+ return null;
131
+ const b = (isRecord(body.device_step) ? body.device_step : body);
132
+ const s = (k) => (typeof b[k] === "string" && b[k] !== "" ? b[k] : null);
133
+ const user = s("user_code");
134
+ const uri = s("verification_uri_complete");
135
+ const dc = s("device_code");
136
+ const cid = s("client_id");
137
+ if (!user || !uri || !dc || !cid)
138
+ return null;
139
+ const interval = typeof b.interval === "number" && b.interval > 0 ? b.interval : 5;
140
+ return {
141
+ user_code: user,
142
+ verification_uri_complete: uri,
143
+ device_code: dc,
144
+ interval,
145
+ client_id: cid,
146
+ ...(s("approval_id") ? { approval_id: s("approval_id") } : {}),
147
+ };
148
+ }
149
+ /** How long the CLI keeps polling a device step before giving up (the approval TTL). */
150
+ export const DEVICE_STEP_POLL_MAX_MS = 15 * 60 * 1000;
151
+ /**
152
+ * Print the device step and poll until it resolves. 0 when applied; throws a DurableRefusal when
153
+ * denied, expired or refused. Never prints the device_code or any token.
154
+ */
155
+ export async function runDeviceStep(step, note, deps) {
156
+ deps.err(`Approve this change: open ${step.verification_uri_complete}`);
157
+ deps.err(`and confirm the code ${step.user_code}.`);
158
+ if (note)
159
+ deps.err(note);
160
+ let interval = step.interval * 1000;
161
+ const deadline = deps.now() + DEVICE_STEP_POLL_MAX_MS;
162
+ for (;;) {
163
+ await deps.sleep(interval);
164
+ const a = await deps.api.devicePoll(step.device_code, step.client_id);
165
+ const code = errorCodeOf(a.body);
166
+ if (a.status < 300 && code === null) {
167
+ deps.err("approved and applied");
168
+ return 0;
169
+ }
170
+ if (code === "authorization_pending" || code === "slow_down") {
171
+ if (code === "slow_down")
172
+ interval += 5000;
173
+ if (deps.now() + interval > deadline) {
174
+ throw new DurableRefusal({
175
+ code: "expired_token",
176
+ message: "the approval expired",
177
+ fix: null,
178
+ });
179
+ }
180
+ continue;
181
+ }
182
+ const why = code === "access_denied"
183
+ ? "the approval was denied"
184
+ : code === "expired_token"
185
+ ? "the approval expired"
186
+ : (errorMessageOf(a.body) ?? "the approval was refused");
187
+ throw new DurableRefusal({ code: code ?? `http_${a.status}`, message: why, fix: null });
188
+ }
189
+ }
190
+ /** Run one verb end to end and return its exit status (refusals rendered here). */
191
+ export async function runVerb(verb, name, body, opts, deps) {
192
+ const launcherKey = deps.launcherKey();
193
+ try {
194
+ const row = await resolveTarget(deps.api, name);
195
+ const a = await deps.api.ownerVerb(VERB_ROUTE[verb], row.conversation_id, row.agent_id, body);
196
+ if (a.status === 202) {
197
+ const step = deviceStepOf(a.body);
198
+ if (step === null) {
199
+ throw new DurableRefusal({
200
+ code: "device_step_malformed",
201
+ message: "the API answered 202 without a usable device step",
202
+ fix: null,
203
+ });
204
+ }
205
+ const contained = row.state === "paused" && row.state_reason === "agent_contained";
206
+ const note = contained
207
+ ? "an org admin other than you must approve this unpause (a solo org approves it itself)"
208
+ : null;
209
+ const rc = await runDeviceStep(step, note, deps);
210
+ if (opts.json)
211
+ deps.out(JSON.stringify({ name, verb, applied: true }));
212
+ return rc;
213
+ }
214
+ if (a.status >= 200 && a.status < 300) {
215
+ if (verb === "done" || verb === "kill") {
216
+ // An ending verb fails closed: only an answer that says the session ended (or a re-read
217
+ // that shows it) counts, and only then is a tracked claude stopped. Never SIGKILL on an
218
+ // ambiguous 2xx (a null body, a proxy-swallowed body, schema drift).
219
+ if (!(isRecord(a.body) && a.body.changed === false)) {
220
+ await confirmEnded(verb, row, a, deps.api, name);
221
+ }
222
+ }
223
+ const changed = !(isRecord(a.body) && a.body.changed === false);
224
+ // --json: stdout carries the one JSON document; the human lines go to stderr.
225
+ const say = opts.json ? deps.err : deps.out;
226
+ if (!changed) {
227
+ say(`${name}: no change (${stateLabel(row)})`);
228
+ if (verb === "resume")
229
+ say(`fix: ${render("resume_live", { name }, launcherKey, true)}`);
230
+ }
231
+ else {
232
+ say(`${name}: ${APPLIED_TEXT[verb]}`);
233
+ if ((verb === "done" || verb === "kill") && !isLocked(row) && deps.stopTrackedClaude) {
234
+ await deps.stopTrackedClaude(row, TRACKED_STOP_GRACE_S);
235
+ }
236
+ }
237
+ if (opts.json)
238
+ deps.out(JSON.stringify(a.body ?? { changed }));
239
+ return 0;
240
+ }
241
+ throw refusalFromAnswer(a, name, launcherKey, row);
242
+ }
243
+ catch (e) {
244
+ return handleVerbError(e, opts.json === true, deps);
245
+ }
246
+ }
247
+ /** The state an applied done / kill writes (S1 owner-verbs: complete -> completed, kill -> killed). */
248
+ const ENDED_STATE = { done: "completed", kill: "killed" };
249
+ /**
250
+ * Confirm an applied done / kill: the answer's own `{changed: true, state}` when it names the ended
251
+ * state, else one re-read of the session by conversation id. Throws a refusal (nothing stopped
252
+ * locally) when neither shows the session ended.
253
+ */
254
+ export async function confirmEnded(verb, row, a, api, name) {
255
+ const want = ENDED_STATE[verb];
256
+ if (isRecord(a.body) && a.body.changed === true && a.body.state === want)
257
+ return;
258
+ const seen = await api.listSessions({ name }).then((rows) => rows.find((r) => r.conversation_id === row.conversation_id) ?? null, () => null);
259
+ if (seen !== null && seen.state === want)
260
+ return;
261
+ throw new DurableRefusal({
262
+ code: "verb_unconfirmed",
263
+ message: `${name}: the API answered ${a.status} but did not confirm the session ${want === "completed" ? "completed" : "was killed"}; nothing was stopped locally`,
264
+ fix: null,
265
+ });
266
+ }
267
+ export function handleVerbError(e, json, deps) {
268
+ if (e instanceof DurableRefusal)
269
+ return printDurableRefusal(e, json, deps);
270
+ if (e instanceof RegistryUnavailable) {
271
+ return printDurableRefusal(new DurableRefusal({
272
+ code: "session_registry_unavailable",
273
+ message: `the session registry is unreachable (${e.message}); nothing was changed`,
274
+ fix: null,
275
+ }), json, deps);
276
+ }
277
+ throw e;
278
+ }
279
+ /** Parse a positive amount (amend --budget); `integer` for a count (amend --turns). */
280
+ export function parsePositive(flag, raw, integer = false) {
281
+ if (raw === undefined)
282
+ return undefined;
283
+ const n = Number(raw);
284
+ if (!Number.isFinite(n) || n <= 0 || (integer && !Number.isInteger(n))) {
285
+ throw new DurableRefusal({
286
+ code: "invalid_argument",
287
+ message: `${flag} must be a positive ${integer ? "whole number" : "number"} (got '${raw}')`,
288
+ fix: null,
289
+ exit: 2,
290
+ });
291
+ }
292
+ return n;
293
+ }
294
+ export async function amendCommand(name, opts, deps) {
295
+ let budget;
296
+ let turns;
297
+ try {
298
+ budget = parsePositive("--budget", opts.budget);
299
+ turns = parsePositive("--turns", opts.turns, true);
300
+ }
301
+ catch (e) {
302
+ return handleVerbError(e, opts.json === true, deps);
303
+ }
304
+ if (budget === undefined && turns === undefined) {
305
+ deps.err("mnemom agent amend: give --budget <n> and/or --turns <n>");
306
+ return 2;
307
+ }
308
+ const body = {};
309
+ if (budget !== undefined)
310
+ body.budget_usd = budget;
311
+ if (turns !== undefined)
312
+ body.max_turns = turns;
313
+ return runVerb("amend", name, body, opts, deps);
314
+ }
315
+ export async function unlockCommand(name, deps) {
316
+ return runVerb("unlock", name, {}, {}, deps);
317
+ }
318
+ /**
319
+ * `mnemom agent containment pause|resume <agent>` (BACKBONE C1, amendment 35; C16-R30): a thin
320
+ * verb over POST /v1/orgs/{org}/agents/{id}/pause|resume. The org comes from the agent record,
321
+ * never the active org. It needs a session login: on an API-key-only credential, or the api's
322
+ * 403 org_scope_requires_session, it exits non-zero with the containment_login line and never
323
+ * sends the API key or prints the raw 403.
324
+ */
325
+ export async function containmentCommand(verb, agentRef, deps) {
326
+ const launcherKey = deps.launcherKey();
327
+ const loginRefusal = () => new DurableRefusal({
328
+ // Never the raw 403 code (C11-R26): the machine code is the S1 fix key.
329
+ code: "containment_login",
330
+ message: `mnemom agent containment ${verb}: org containment acts as a signed-in org admin`,
331
+ fix: render("containment_login", {}, launcherKey, false),
332
+ });
333
+ try {
334
+ if ((await deps.api.credentialType()) !== "jwt")
335
+ throw loginRefusal();
336
+ const { orgId, agentId } = await resolveContainmentAgent(agentRef, deps, launcherKey);
337
+ const a = await deps.api.containment(orgId, agentId, verb);
338
+ if (a.status === 403 && errorCodeOf(a.body) === "org_scope_requires_session")
339
+ throw loginRefusal();
340
+ if (a.status >= 200 && a.status < 300) {
341
+ deps.out(`agent ${agentRef}: ${verb === "pause" ? "paused (contained)" : "active again"}`);
342
+ return 0;
343
+ }
344
+ throw refusalFromAnswer(a, agentRef, launcherKey);
345
+ }
346
+ catch (e) {
347
+ return handleVerbError(e, false, deps);
348
+ }
349
+ }
350
+ /** An agent id as the CLI recognises one (lib/api.ts resolveAgentId). */
351
+ export const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
352
+ /**
353
+ * The containment target and its org, from the agent record (never the active org): an id is read
354
+ * with GET /v1/agents/{id}; a name is looked up in the caller's org fleets and must match exactly one.
355
+ */
356
+ export async function resolveContainmentAgent(agentRef, deps, launcherKey) {
357
+ if (AGENT_ID_RE.test(agentRef)) {
358
+ const ag = await deps.api.getAgent(agentRef);
359
+ if (ag.status !== 200 || !isRecord(ag.body))
360
+ throw refusalFromAnswer(ag, agentRef, launcherKey);
361
+ const rec = (isRecord(ag.body.agent) ? ag.body.agent : ag.body);
362
+ const orgId = typeof rec.org_id === "string" ? rec.org_id : null;
363
+ if (orgId === null) {
364
+ throw new DurableRefusal({
365
+ code: "agent_not_found",
366
+ message: `agent ${agentRef} has no org on record`,
367
+ fix: null,
368
+ });
369
+ }
370
+ return { orgId, agentId: typeof rec.id === "string" ? rec.id : agentRef };
371
+ }
372
+ const found = await deps.api.agentsByName(agentRef);
373
+ if (found.length === 0) {
374
+ throw new DurableRefusal({
375
+ code: "agent_not_found",
376
+ message: `no agent named ${agentRef} in your orgs`,
377
+ fix: null,
378
+ });
379
+ }
380
+ if (found.length > 1) {
381
+ throw new DurableRefusal({
382
+ code: "agent_ambiguous",
383
+ message: `${found.length} agents are named ${agentRef} in your orgs (${found.map((a) => a.id).join(", ")}); name one by its id`,
384
+ fix: null,
385
+ });
386
+ }
387
+ const [only] = found;
388
+ return { orgId: only.org_id, agentId: only.id };
389
+ }
390
+ /** The thin verbs (pause/resume/done/kill) with no body. */
391
+ export async function ownerVerbCommand(verb, name, opts, deps) {
392
+ return runVerb(verb, name, {}, opts, deps);
393
+ }
@@ -5,9 +5,11 @@
5
5
  * arm is unit-testable without Commander (same pattern as commands/usage.ts).
6
6
  */
7
7
  import { type GitHubCredential } from "../lib/scan/credential.js";
8
- import { type ScanDeps } from "../lib/scan/scan.js";
8
+ import { type ScanDeps, type ScanResult } from "../lib/scan/scan.js";
9
+ import { selectSpend } from "../lib/scan/spend/spend-tiers.js";
9
10
  import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
10
11
  import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
12
+ import { type CostSpend } from "../lib/scan/cost-per-pr.js";
11
13
  import { type ReportEmailDeps } from "../lib/scan/report-email.js";
12
14
  import { type ReportRecipients } from "../lib/scan/share.js";
13
15
  export interface ScanCommandOptions {
@@ -64,6 +66,15 @@ export declare function resolveRecipients(o: {
64
66
  }): Promise<ReportRecipients>;
65
67
  /** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
66
68
  export declare function resolveSelfReportedSpend(o: ScanCommandOptions): Promise<number | null>;
69
+ /**
70
+ * The one spend the cost-per-PR figures split, matching the tier the scan chose. Null leaves them out.
71
+ * Tier 1 sends the measured Anthropic total; tier 2 NEVER does (a figure split from the stated monthly
72
+ * spend would let that number be recovered, and the tier 2 line promises it is "not sent"); tier 3 (or
73
+ * tiers off) the modelled range. Sources are never mixed.
74
+ */
75
+ export declare function resolveCostSpend(o: ScanCommandOptions, result: ScanResult, spend: ReturnType<typeof selectSpend> | null, measured: {
76
+ usd: number;
77
+ } | null): CostSpend | null;
67
78
  export declare const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
68
79
  export declare const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
69
80
  /**
@@ -13,6 +13,7 @@ import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape
13
13
  import { getAuthInfo } from "../lib/auth.js";
14
14
  import { listMyOrgs } from "../lib/api.js";
15
15
  import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
16
+ import { modelledSpend } from "../lib/scan/cost-per-pr.js";
16
17
  import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
17
18
  import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
18
19
  import { buildSharePayload, parseShareLevel, promptShareLevel, } from "../lib/scan/share.js";
@@ -67,6 +68,23 @@ export async function resolveSelfReportedSpend(o) {
67
68
  throw new Error(SPEND_INVALID_MSG);
68
69
  return n;
69
70
  }
71
+ /**
72
+ * The one spend the cost-per-PR figures split, matching the tier the scan chose. Null leaves them out.
73
+ * Tier 1 sends the measured Anthropic total; tier 2 NEVER does (a figure split from the stated monthly
74
+ * spend would let that number be recovered, and the tier 2 line promises it is "not sent"); tier 3 (or
75
+ * tiers off) the modelled range. Sources are never mixed.
76
+ */
77
+ export function resolveCostSpend(o, result, spend, measured) {
78
+ if (spend === null)
79
+ return modelledSpend(result.spend);
80
+ if (spend.tier === 1) {
81
+ const usd = measured?.usd ?? o.anthropicUsageUsd;
82
+ return typeof usd === "number" ? { source: "anthropic_usage", usd } : null;
83
+ }
84
+ if (spend.tier === 2)
85
+ return null;
86
+ return spend.tier === 3 ? modelledSpend(result.spend) : null;
87
+ }
70
88
  export const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
71
89
  export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
72
90
  /**
@@ -194,7 +212,7 @@ export async function scanCommand(o) {
194
212
  const level = parseShareLevel(o.share) ?? (await promptShareLevel());
195
213
  // Every successful scan emails the report to the person who ran it, at every share level, no opt-out.
196
214
  // Mnemom keeps nothing from a share=none send (the server sends the email and does nothing else).
197
- const emailFailure = await emailReport(o, emailOrg, result, level, period, spend);
215
+ const emailFailure = await emailReport(o, emailOrg, result, level, spend, period, resolveCostSpend(o, result, spend, measured));
198
216
  if (level === "none") {
199
217
  console.log("Share level: nothing. No share payload is built; Mnemom keeps nothing (the report email is only sent to you).");
200
218
  if (emailFailure)
@@ -211,16 +229,17 @@ export async function scanCommand(o) {
211
229
  * Emails the report. Loud on every failure: prints the reason name, never the server's words, and
212
230
  * returns it so the caller exits non-zero after the local report is shown.
213
231
  */
214
- async function emailReport(o, org, result, level, period, spend) {
232
+ async function emailReport(o, org, result, level, spend, period, costSpend) {
215
233
  const fail = (f) => {
216
234
  console.log(failureMessage(f));
217
235
  return f;
218
236
  };
219
237
  const report = buildEmailReport(result, {
220
238
  orgId: org,
221
- period,
222
239
  now: o.now ?? Date.now,
223
240
  sendSpendRange: spend ? spend.shareable && spend.tier === 3 : true,
241
+ period,
242
+ costSpend,
224
243
  });
225
244
  const body = buildEmailBody(report, level, result.coverage.complete, o.ctoEmail?.trim());
226
245
  const outcome = await sendReportEmail(org, body, o.emailDeps);
@@ -41,7 +41,7 @@ const PROVIDERS = {
41
41
  label: "Google Gemini",
42
42
  keyEnv: "GOOGLE_API_KEY",
43
43
  keyHeader: "x-goog-api-key",
44
- defaultModel: "gemini-2.0-flash",
44
+ defaultModel: "gemini-2.5-flash",
45
45
  basePath: "/gemini",
46
46
  },
47
47
  };
@@ -11,7 +11,7 @@ import { executorStateOk, executorStatePath, LAUNCHER_KEYS, } from "../vendor/du
11
11
  import { getApiUrl, MNEMOM_DIR } from "../lib/config.js";
12
12
  import { resolveAuth } from "../lib/auth.js";
13
13
  import { readSessionRecords } from "../lib/agent-ledger.js";
14
- import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
14
+ import { durableLauncherKey, LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
15
15
  import { mnemomFetch } from "../lib/version-gate.js";
16
16
  import { httpAdoptApi } from "./adopt-api.js";
17
17
  import { liveCwdDeps } from "./cwd-rule.js";
@@ -37,21 +37,18 @@ export function ledgerFactsFrom(records, cid) {
37
37
  }
38
38
  /** The launcher key `durable` (MNEMOM_AGENT_DURABLE, then launcher.toml), default off. */
39
39
  export function launcherKeyFrom(env, tomlText) {
40
- const ok = (v) => typeof v === "string" && LAUNCHER_KEYS.includes(v);
41
- const e = env.MNEMOM_AGENT_DURABLE?.trim().toLowerCase();
42
- if (ok(e))
43
- return e;
40
+ let configured;
44
41
  if (tomlText !== null) {
45
42
  try {
46
43
  const v = parseToml(tomlText).durable;
47
- if (ok(v))
48
- return v;
44
+ if (typeof v === "string" && LAUNCHER_KEYS.includes(v))
45
+ configured = v;
49
46
  }
50
47
  catch {
51
48
  // an unparseable launcher.toml is the launcher's error to report, not adopt's
52
49
  }
53
50
  }
54
- return "off";
51
+ return durableLauncherKey(env, configured);
55
52
  }
56
53
  /** The executor state file, or null when absent or not the frozen S1 shape. */
57
54
  export function executorStateFrom(text) {
@@ -172,12 +172,20 @@ export interface AdoptPlan {
172
172
  export declare function ruleCwd(live: LiveClaude, realpath: (p: string) => string | null): string | null;
173
173
  /** The session row of a GET answer: the body itself, or its `session`; agent_id also from `agent.id`. */
174
174
  export declare function rowOf(a: ApiAnswer): SessionRow | null;
175
+ /**
176
+ * A session's fix lines name it by its registry row's own name (the row discovery matched by
177
+ * conversation id); only then does the name hold a row, so only then may a line carry the done step.
178
+ * The environment's name is the launcher's display name (agent-scenario), which no verb resolves.
179
+ */
180
+ export declare function nameHeldOf(s: Pick<PlannedSession, "name" | "_row">): boolean;
175
181
  /** What a refusal's fix line may be filled from. */
176
182
  export interface RefusalContext {
177
183
  name: string;
178
184
  launcherKey: LauncherKey;
179
185
  /** The group's agent (for agent_contained when the refusal names none). */
180
186
  agent?: string;
187
+ /** Whether the name holds a registry row (nameHeldOf): only then does the line carry the done step. */
188
+ nameHeld: boolean;
181
189
  }
182
190
  /**
183
191
  * The S1 fix line for a lock refusal answered by the check or the lock route, or null when the
@@ -76,6 +76,14 @@ export function rowOf(a) {
76
76
  return { ...inner, agent_id: inner.agent.id };
77
77
  return inner;
78
78
  }
79
+ /**
80
+ * A session's fix lines name it by its registry row's own name (the row discovery matched by
81
+ * conversation id); only then does the name hold a row, so only then may a line carry the done step.
82
+ * The environment's name is the launcher's display name (agent-scenario), which no verb resolves.
83
+ */
84
+ export function nameHeldOf(s) {
85
+ return typeof s._row?.name === "string" && s._row.name !== "" && s._row.name === s.name;
86
+ }
79
87
  async function planSession(live, deps, launcherKey) {
80
88
  const facts = deps.ledger(live.conversationId);
81
89
  const base = {
@@ -117,6 +125,8 @@ async function planSession(live, deps, launcherKey) {
117
125
  if (!row)
118
126
  return unadoptable("row_unreadable");
119
127
  Object.assign(base, { _row: row });
128
+ if (typeof row.name === "string" && row.name !== "")
129
+ base.name = row.name;
120
130
  const agentId = row.agent_id ?? facts.agentId;
121
131
  if (row.durable_mode === "locked") {
122
132
  await compareLocked(base, live, row, deps, launcherKey);
@@ -137,7 +147,7 @@ async function planSession(live, deps, launcherKey) {
137
147
  base.cwd_kind = verdict.cwd_kind;
138
148
  base.reason = `cwd_${verdict.cwd_kind}`;
139
149
  // A tracked row holds the name, so the fix carries the done clause (S1 fix-line rule).
140
- base.fix = cwdFixLine(verdict, { name: live.sessionName, launcherKey, nameHeld: true });
150
+ base.fix = cwdFixLine(verdict, { name: base.name, launcherKey, nameHeld: nameHeldOf(base) });
141
151
  base.note = cwdNote(verdict.cwd_kind, base.fix);
142
152
  return { s: base, agentId, agentLabel: facts.agentLabel };
143
153
  }
@@ -199,7 +209,8 @@ async function compareLocked(base, live, row, deps, launcherKey) {
199
209
  // read (gaps/c4-relock-sole-unlock.md): it is never offered from here.
200
210
  // launchSpecChangeFixLines' tuple type guarantees the launch_spec_change line is first.
201
211
  base.fix = launchSpecChangeFixLines({
202
- name: live.sessionName,
212
+ name: base.name,
213
+ nameHeld: nameHeldOf(base),
203
214
  flags: specUserArgv(built.spec).join(" "),
204
215
  launcherKey,
205
216
  soleLockedSession: false,
@@ -234,7 +245,7 @@ export function refusalFix(code, details, ctx) {
234
245
  params.repo = repos.join(", ");
235
246
  if (placeholdersOf(key).some((p) => params[p] === undefined))
236
247
  return null;
237
- return renderFixLine(key, { launcherKey: ctx.launcherKey, nameHeld: true, params });
248
+ return renderFixLine(key, { launcherKey: ctx.launcherKey, nameHeld: ctx.nameHeld, params });
238
249
  }
239
250
  const LOCK_REFUSAL_FIX_CODES = Object.freeze({
240
251
  durable_lock_not_entitled: true,
@@ -299,7 +310,7 @@ export async function planAdopt(deps, opts = {}) {
299
310
  const checkout = root ?? ownCheckout ?? null;
300
311
  if (checkout === null)
301
312
  continue;
302
- s.fix = cwdFixLine({ ok: false, cwd_kind: "not_git", params: { checkout } }, { name: s.name, launcherKey, nameHeld: true });
313
+ s.fix = cwdFixLine({ ok: false, cwd_kind: "not_git", params: { checkout } }, { name: s.name, launcherKey, nameHeld: nameHeldOf(s) });
303
314
  s.note = cwdNote("not_git", s.fix);
304
315
  }
305
316
  const adoptable = g.sessions.filter((s) => s.verdict === "adopt");
@@ -343,7 +354,12 @@ export async function planAdopt(deps, opts = {}) {
343
354
  else if (a.kind === "error") {
344
355
  g.code = a.code ?? `http_${a.status}`;
345
356
  g.reason = a.message;
346
- g.fix = refusalFix(a.code, a.details, { name: s.name, launcherKey, agent: g.agent });
357
+ g.fix = refusalFix(a.code, a.details, {
358
+ name: s.name,
359
+ launcherKey,
360
+ agent: g.agent,
361
+ nameHeld: nameHeldOf(s),
362
+ });
347
363
  }
348
364
  else {
349
365
  g.code = "durable_precondition_unavailable";
@@ -513,7 +529,12 @@ export async function handover(s, g, exec, deps, facts, launcherKey) {
513
529
  name: s.name,
514
530
  committed: false,
515
531
  code: a.code ?? `http_${a.status}`,
516
- fix: refusalFix(a.code, a.details, { name: s.name, launcherKey, agent: g.agent }),
532
+ fix: refusalFix(a.code, a.details, {
533
+ name: s.name,
534
+ launcherKey,
535
+ agent: g.agent,
536
+ nameHeld: nameHeldOf(s),
537
+ }),
517
538
  };
518
539
  }
519
540
  // No attempt was decisive and a request may still be in flight: only a re-read that shows the
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * The decision is pure over injected facts; liveCwdDeps is the thin driver.
19
19
  */
20
- import { type CwdKind, type LauncherKey } from "../vendor/durable-contract/index.js";
20
+ import { type CwdCheckCause, type CwdKind, type LauncherKey } from "../vendor/durable-contract/index.js";
21
21
  export interface CwdDeps {
22
22
  /** realpath(p), or null when it does not resolve. */
23
23
  realpath(p: string): string | null;
@@ -48,6 +48,14 @@ export declare function projectSlug(path: string): string;
48
48
  * not --resume it).
49
49
  */
50
50
  export declare function classifyCwd(cwd: string, deps: CwdDeps, ccSessionId?: string | null): CwdVerdict;
51
+ /**
52
+ * The daemon's spawn-time cwd check (attestation check `cwd`, C14-R04; mnemom-test #1002): the
53
+ * committed cwd's realpath must equal `git rev-parse --show-toplevel` there. Null when it holds;
54
+ * otherwise the S1 CWD_CHECK_CAUSES value: `missing` when there is no committed cwd or it does not
55
+ * resolve, `not_toplevel` when it resolves but is not a work tree's top level (a subdirectory, or
56
+ * not in a git work tree at all).
57
+ */
58
+ export declare function spawnCwdCause(cwd: string | null, deps: Pick<CwdDeps, "realpath" | "gitToplevel">): CwdCheckCause | null;
51
59
  /**
52
60
  * The S1 fix line for a cwd refusal (fix-line rule: the done clause when a non-terminal row holds
53
61
  * the name, the config suffix when the launcher key is not `always`), or null when a path the