nawabari 0.2.0 → 0.4.1

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 (50) hide show
  1. package/README.md +230 -15
  2. package/dist/cli.d.ts +5 -0
  3. package/dist/cli.js +704 -115
  4. package/dist/cli.js.map +1 -1
  5. package/dist/contract.js +135 -2
  6. package/dist/contract.js.map +1 -1
  7. package/dist/domain/errors.d.ts +1 -1
  8. package/dist/domain/errors.js +4 -1
  9. package/dist/domain/errors.js.map +1 -1
  10. package/dist/domain/sandbox-launcher.d.ts +35 -0
  11. package/dist/domain/sandbox-launcher.js +679 -0
  12. package/dist/domain/sandbox-launcher.js.map +1 -0
  13. package/dist/domain/sandbox.d.ts +190 -0
  14. package/dist/domain/sandbox.js +363 -0
  15. package/dist/domain/sandbox.js.map +1 -0
  16. package/dist/domain/session-backend.d.ts +5 -3
  17. package/dist/domain/session-backend.js +102 -4
  18. package/dist/domain/session-backend.js.map +1 -1
  19. package/dist/domain/session.d.ts +116 -2
  20. package/dist/domain/session.js +36 -0
  21. package/dist/domain/session.js.map +1 -1
  22. package/dist/errors.d.ts +1 -1
  23. package/dist/errors.js.map +1 -1
  24. package/dist/git.d.ts +62 -0
  25. package/dist/git.js +299 -0
  26. package/dist/git.js.map +1 -1
  27. package/dist/operation-authorization.d.ts +5 -2
  28. package/dist/operation-authorization.js +6 -7
  29. package/dist/operation-authorization.js.map +1 -1
  30. package/dist/output-budget.d.ts +16 -0
  31. package/dist/output-budget.js +58 -0
  32. package/dist/output-budget.js.map +1 -0
  33. package/dist/presentation.js +28 -4
  34. package/dist/presentation.js.map +1 -1
  35. package/dist/registry/atomic.d.ts +24 -0
  36. package/dist/registry/atomic.js +35 -2
  37. package/dist/registry/atomic.js.map +1 -1
  38. package/dist/registry/errors.d.ts +1 -1
  39. package/dist/registry/errors.js +1 -0
  40. package/dist/registry/errors.js.map +1 -1
  41. package/dist/repository-evidence.d.ts +74 -0
  42. package/dist/repository-evidence.js +7 -0
  43. package/dist/repository-evidence.js.map +1 -0
  44. package/dist/resource-claims.d.ts +25 -3
  45. package/dist/resource-claims.js +57 -5
  46. package/dist/resource-claims.js.map +1 -1
  47. package/dist/session-registry.d.ts +68 -7
  48. package/dist/session-registry.js +422 -149
  49. package/dist/session-registry.js.map +1 -1
  50. package/package.json +5 -3
package/dist/cli.js CHANGED
@@ -1,96 +1,385 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { runDoctor } from "./domain/doctor.js";
3
3
  import { DomainError, EXIT_CODES, failure } from "./domain/errors.js";
4
+ import { MAX_SESSION_LIST_LIMIT, } from "./domain/session.js";
5
+ import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PATHS } from "./repository-evidence.js";
4
6
  import { createLocalSessionBackend } from "./domain/session-backend.js";
5
7
  import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
6
8
  import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
9
+ import { resolveSandboxExecutionRequest, runSandboxedCommand, } from "./domain/sandbox.js";
7
10
  const CLI_NAME = "nawabari";
8
11
  const packageMetadata = createRequire(import.meta.url)("../package.json");
9
12
  const VERSION = packageMetadata.version;
10
- const HELP_TEXT = [
11
- `Usage: ${CLI_NAME} <command> [options]`,
12
- "",
13
- "Commands:",
14
- " session create Request a new Nawabari session",
15
- " session id Resolve the current session identity",
16
- " session show Show the current or selected session",
17
- " session list List repository sessions",
18
- " session claim Add a canonical resource claim",
19
- " session update Replace a session's resource claims",
20
- " session claims List canonical resource claims",
21
- " session release Release resource claims",
22
- " session close Close the current or selected session",
23
- " authorize Authorize an operation against concrete claims",
24
- " checkpoint Capture bounded Git execution evidence",
25
- " commit Commit explicit claim-authorized resources",
26
- " push Push the owned branch to an explicit target",
27
- " status Show Nawabari session status",
28
- " guard [--session id] Authorize the current worktree or operation",
29
- " gc Detect or clean eligible stale sessions",
30
- " doctor Check local Nawabari prerequisites",
31
- " capabilities Describe the standalone CLI/JSON contract",
32
- " --help Show this help",
33
- " --version Print the installed version",
34
- "",
35
- "Global options:",
36
- " --json Emit one stable JSON document on stdout",
37
- " -h, --help Show this help",
38
- "",
39
- "Session options:",
40
- " session create --branch <name> --worktree <path> --base <ref> --label <text>",
41
- " session show|close --session <id>",
42
- " session claim|update --session <id> --resource <path-or-glob> --mode <read|write|exclusive-write>",
43
- " session claims|release --session <id> [--claim-id <id>]",
44
- " authorize --session <id> --operation <name> --resource <path> [--resource <path>]",
45
- " checkpoint [--session <id>]",
46
- " commit --message <final-message> --resource <path> [--resource <path>] [--session <id>]",
47
- " push --remote <name> --branch <name> --resource <path> [--create-upstream] [--force] [--session <id>]",
48
- " gc [--dry-run|--apply]",
49
- ].join("\n");
50
- const HELP_DATA = {
51
- usage: `Usage: ${CLI_NAME} <command> [options]`,
52
- commands: [
53
- "session create",
54
- "session id",
55
- "session show",
56
- "session list",
57
- "session claim",
58
- "session update",
59
- "session claims",
60
- "session release",
61
- "resource claim",
62
- "resource update",
63
- "resource list",
64
- "resource release",
65
- "session close",
66
- "authorize",
67
- "checkpoint",
68
- "commit",
69
- "push",
70
- "status",
71
- "guard",
72
- "gc",
73
- "doctor",
74
- "capabilities",
75
- ],
76
- options: ["--json", "--help", "--version"],
77
- session_options: [
78
- "--branch",
79
- "--worktree",
80
- "--base",
81
- "--label",
82
- "--session",
83
- "--resource",
84
- "--mode",
85
- "--claim-id",
86
- "--repository",
87
- ],
88
- authorization_options: ["--session", "--operation", "--resource"],
89
- checkpoint_options: ["--session"],
90
- commit_options: ["--session", "--message", "--resource"],
91
- push_options: ["--session", "--resource", "--remote", "--branch", "--remote-branch", "--force", "--create-upstream"],
92
- gc_options: ["--apply", "--dry-run"],
13
+ const GLOBAL_HELP_OPTIONS = [
14
+ { name: "--json", description: "Emit one stable JSON document on stdout" },
15
+ { name: "--help", description: "Show command-specific help" },
16
+ { name: "--version", description: "Print the installed version" },
17
+ ];
18
+ const option = (name, description, options = {}) => ({ name, description, ...options });
19
+ const HELP_COMMANDS = [
20
+ {
21
+ name: "session create",
22
+ summary: "Request a new Nawabari session",
23
+ usage: `${CLI_NAME} session create [options]`,
24
+ options: [
25
+ option("--branch", "Branch to create; omitted uses the generated session branch", {
26
+ value: "<name>",
27
+ default: "nawabari/session/<session_id>",
28
+ }),
29
+ option("--worktree", "Managed worktree path; omitted uses the resolved repository-local root", {
30
+ value: "<path>",
31
+ default: "<managed_worktree_root>/<repository>-<session_id>",
32
+ }),
33
+ option("--base", "Commit-resolving base ref for the new worktree", { value: "<ref>", default: "HEAD" }),
34
+ option("--label", "Optional display label; never used as an identity", { value: "<text>", default: "omitted" }),
35
+ ],
36
+ notes: ["All create options are optional. Use status --json to discover managed_worktree_root."],
37
+ },
38
+ {
39
+ name: "session id",
40
+ summary: "Resolve the current session identity",
41
+ usage: `${CLI_NAME} session id`,
42
+ options: [],
43
+ },
44
+ {
45
+ name: "session show",
46
+ summary: "Show the current or selected session",
47
+ usage: `${CLI_NAME} session show [--session <id>]`,
48
+ options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
49
+ },
50
+ {
51
+ name: "session run",
52
+ summary: "Run one command inside the protected session sandbox",
53
+ usage: `${CLI_NAME} session run [--session <id>] -- <command> [args...]`,
54
+ options: [option("--session", "Select the active owned session", { value: "<id>" })],
55
+ notes: [
56
+ "The -- terminator is mandatory. The command is passed as argv without a shell, and protected execution is fail-closed.",
57
+ "session exec is an alias.",
58
+ ],
59
+ },
60
+ {
61
+ name: "session list",
62
+ summary: "List bounded repository session records",
63
+ usage: `${CLI_NAME} session list [--all|--history]`,
64
+ options: [
65
+ option("--all", "Include closed history; explicit unbounded history view"),
66
+ option("--history", "Alias for --all"),
67
+ ],
68
+ notes: ["Default output excludes closed records and is limited to 64 records."],
69
+ },
70
+ {
71
+ name: "session claim",
72
+ summary: "Add a canonical resource claim",
73
+ usage: `${CLI_NAME} session claim --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
74
+ options: [
75
+ option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
76
+ option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
77
+ option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
78
+ option("--repository", "Expected repository identity", { value: "<id>" }),
79
+ ],
80
+ },
81
+ {
82
+ name: "session update",
83
+ summary: "Atomically replace a session's complete resource claim set",
84
+ usage: `${CLI_NAME} session update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
85
+ options: [
86
+ option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
87
+ value: "<path-or-glob>",
88
+ required: true,
89
+ }),
90
+ option("--mode", "Mode for the --resource immediately before it; repeatable", {
91
+ value: "<read|write|exclusive-write>",
92
+ required: true,
93
+ }),
94
+ option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
95
+ option("--repository", "Expected repository identity", { value: "<id>" }),
96
+ ],
97
+ notes: [
98
+ "The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
99
+ "on any invalid or conflicting claim the prior set is left unchanged.",
100
+ "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
101
+ ],
102
+ },
103
+ {
104
+ name: "session claims",
105
+ summary: "List canonical resource claims",
106
+ usage: `${CLI_NAME} session claims [--session <id>]`,
107
+ options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
108
+ },
109
+ {
110
+ name: "session release",
111
+ summary: "Release resource claims",
112
+ usage: `${CLI_NAME} session release [--session <id>] [--claim-id <id>]`,
113
+ options: [
114
+ option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
115
+ option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
116
+ ],
117
+ },
118
+ {
119
+ name: "resource claim",
120
+ summary: "Add a canonical resource claim (alias)",
121
+ usage: `${CLI_NAME} resource claim --resource <path-or-glob> --mode <read|write|exclusive-write>`,
122
+ options: [],
123
+ },
124
+ {
125
+ name: "resource update",
126
+ summary: "Atomically replace a session's complete resource claim set (alias)",
127
+ usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
128
+ options: [
129
+ option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
130
+ value: "<path-or-glob>",
131
+ required: true,
132
+ }),
133
+ option("--mode", "Mode for the --resource immediately before it; repeatable", {
134
+ value: "<read|write|exclusive-write>",
135
+ required: true,
136
+ }),
137
+ option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
138
+ option("--repository", "Expected repository identity", { value: "<id>" }),
139
+ ],
140
+ notes: [
141
+ "The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
142
+ "on any invalid or conflicting claim the prior set is left unchanged.",
143
+ "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
144
+ ],
145
+ },
146
+ {
147
+ name: "resource list",
148
+ summary: "List canonical resource claims (alias)",
149
+ usage: `${CLI_NAME} resource list [--session <id>]`,
150
+ options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
151
+ },
152
+ {
153
+ name: "resource release",
154
+ summary: "Release resource claims (alias)",
155
+ usage: `${CLI_NAME} resource release [--session <id>] [--claim-id <id>]`,
156
+ options: [
157
+ option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
158
+ option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
159
+ ],
160
+ },
161
+ {
162
+ name: "session close",
163
+ summary: "Close the current or selected session",
164
+ usage: `${CLI_NAME} session close [--session <id>]`,
165
+ options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
166
+ },
167
+ {
168
+ name: "authorize",
169
+ summary: "Authorize an operation against concrete claims",
170
+ usage: `${CLI_NAME} authorize --operation <name> --resource <path> [--resource <path>] [--session <id>]`,
171
+ options: [
172
+ option("--session", "Assert the current session identity", { value: "<id>" }),
173
+ option("--operation", "Operation vocabulary entry", { value: "<name>", required: true }),
174
+ option("--resource", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
175
+ ],
176
+ },
177
+ {
178
+ name: "checkpoint",
179
+ summary: "Capture bounded Git execution evidence",
180
+ usage: `${CLI_NAME} checkpoint [--session <id>]`,
181
+ options: [option("--session", "Assert the current session identity", { value: "<id>" })],
182
+ },
183
+ {
184
+ name: "evidence snapshot",
185
+ summary: "Capture bounded read-only evidence for one owned session",
186
+ usage: `${CLI_NAME} evidence snapshot --session <id>`,
187
+ options: [option("--session", "Explicit owned session to observe", { value: "<id>", required: true })],
188
+ notes: ["The result is Git-observable physical evidence only; it contains no task or semantic interpretation."],
189
+ },
190
+ {
191
+ name: "diff",
192
+ summary: "Inspect bounded Git evidence for explicit paths",
193
+ usage: `${CLI_NAME} diff --session <id> --path <path> [options]`,
194
+ options: [
195
+ option("--session", "Explicit owned session to observe", { value: "<id>", required: true }),
196
+ option("--path", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
197
+ option("--from", "Commit/ref at the start of the range", { value: "<ref>", default: "HEAD" }),
198
+ option("--to", "Commit/ref at the end of the range; omitted means worktree", { value: "<ref>" }),
199
+ option("--patch", "Include patch text; requires the bounded byte/hunk limits"),
200
+ option("--max-bytes", "Maximum UTF-8 patch bytes", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_BYTES) }),
201
+ option("--max-hunks", "Maximum patch hunks", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_HUNKS) }),
202
+ ],
203
+ },
204
+ {
205
+ name: "commit",
206
+ summary: "Commit explicit claim-authorized resources",
207
+ usage: `${CLI_NAME} commit --message <final-message> --resource <path> [--resource <path>] [--session <id>] [--message-pattern <regex>]`,
208
+ options: [
209
+ option("--session", "Assert the current session identity", { value: "<id>" }),
210
+ option("--message", "Caller-decided final commit message", { value: "<final-message>", required: true }),
211
+ option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
212
+ option("--message-pattern", "Caller-declared commit-message rule; validated only when supplied", {
213
+ value: "<regex>",
214
+ }),
215
+ ],
216
+ },
217
+ {
218
+ name: "push",
219
+ summary: "Push the owned branch to an explicit target",
220
+ usage: `${CLI_NAME} push --remote <name> --branch <name> --resource <path> [options]`,
221
+ options: [
222
+ option("--session", "Assert the current session identity", { value: "<id>" }),
223
+ option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
224
+ option("--remote", "Explicit Git remote", { value: "<name>", required: true }),
225
+ option("--branch", "Explicit target branch", { value: "<name>", required: true }),
226
+ option("--remote-branch", "Explicit remote branch alias for --branch", { value: "<name>" }),
227
+ option("--force", "Allow force-with-lease when relation requires it"),
228
+ option("--create-upstream", "Allow creation of a missing upstream"),
229
+ ],
230
+ },
231
+ {
232
+ name: "status",
233
+ summary: "Show repository context and bounded session status",
234
+ usage: `${CLI_NAME} status [--all|--history]`,
235
+ options: [
236
+ option("--all", "Include closed history; explicit unbounded history view"),
237
+ option("--history", "Alias for --all"),
238
+ ],
239
+ notes: ["The default machine result exposes managed_worktree_root for session-create path discovery."],
240
+ },
241
+ {
242
+ name: "guard",
243
+ summary: "Authorize the current worktree or operation",
244
+ usage: `${CLI_NAME} guard [--session <id>] [--operation <name> --resource <path>]`,
245
+ options: [
246
+ option("--session", "Assert the current session identity", { value: "<id>" }),
247
+ option("--operation", "Authorize an operation when resources are supplied", { value: "<name>" }),
248
+ option("--resource", "Concrete resource; repeatable with --operation", { value: "<path>" }),
249
+ ],
250
+ },
251
+ {
252
+ name: "gc",
253
+ summary: "Detect or clean eligible stale sessions",
254
+ usage: `${CLI_NAME} gc [--dry-run|--apply]`,
255
+ options: [
256
+ option("--apply", "Apply only cleanup that passes safety checks"),
257
+ option("--dry-run", "Preflight eligible stale cleanup without mutation"),
258
+ ],
259
+ notes: [
260
+ "Default stale threshold is 24 hours (86,400,000 ms). Eligibility uses persisted state age or missing/prunable Git worktree physical state; closed history is not a stale candidate.",
261
+ ],
262
+ },
263
+ {
264
+ name: "doctor",
265
+ summary: "Check local Nawabari prerequisites and reconciliation",
266
+ usage: `${CLI_NAME} doctor`,
267
+ options: [],
268
+ },
269
+ {
270
+ name: "capabilities",
271
+ summary: "Describe the standalone CLI/JSON contract",
272
+ usage: `${CLI_NAME} capabilities`,
273
+ options: [],
274
+ },
275
+ ];
276
+ const ROOT_HELP_SPEC = {
277
+ name: "root",
278
+ summary: "Standalone local Git/session ownership CLI",
279
+ usage: `${CLI_NAME} <command> [options]`,
280
+ options: GLOBAL_HELP_OPTIONS,
93
281
  };
282
+ function helpSpecFor(commandArguments) {
283
+ if (commandArguments.length === 0)
284
+ return ROOT_HELP_SPEC;
285
+ let key = commandArguments[0] === "resource"
286
+ ? `resource ${commandArguments[1] ?? "list"}`
287
+ : commandArguments[0] === "session" && commandArguments[1] === "exec"
288
+ ? "session run"
289
+ : commandArguments.slice(0, 2).join(" ");
290
+ if (key === "resource claims")
291
+ key = "resource list";
292
+ const direct = HELP_COMMANDS.find((spec) => spec.name === key);
293
+ if (direct !== undefined && !direct.name.startsWith("resource "))
294
+ return direct;
295
+ const aliasTarget = direct?.name.replace(/^resource /u, "session ");
296
+ const target = aliasTarget === undefined ? undefined : HELP_COMMANDS.find((spec) => spec.name === aliasTarget);
297
+ if (direct !== undefined && target !== undefined) {
298
+ return {
299
+ name: direct.name,
300
+ summary: direct.summary,
301
+ usage: direct.usage,
302
+ options: direct.options,
303
+ notes: direct.notes,
304
+ };
305
+ }
306
+ return HELP_COMMANDS.find((spec) => spec.name === commandArguments[0]) ?? ROOT_HELP_SPEC;
307
+ }
308
+ function helpPayload(spec) {
309
+ if (spec.name === "root") {
310
+ const optionNames = (options) => options.map((candidate) => candidate.name);
311
+ const sessionOptions = HELP_COMMANDS.filter((command) => command.name.startsWith("session ")).flatMap((command) => command.options);
312
+ const unique = (values) => values.filter((value, index) => values.indexOf(value) === index);
313
+ const sessionListOnlyOptions = new Set(["--all", "--history"]);
314
+ const optionsFor = (name) => optionNames(HELP_COMMANDS.find((command) => command.name === name)?.options ?? []);
315
+ return {
316
+ usage: `Usage: ${spec.usage}`,
317
+ commands: HELP_COMMANDS.map((command) => command.name),
318
+ options: ["--json", "--help", "--version"],
319
+ session_options: unique(sessionOptions.map((option) => option.name).filter((name) => !sessionListOnlyOptions.has(name))),
320
+ authorization_options: optionsFor("authorize"),
321
+ checkpoint_options: optionsFor("checkpoint"),
322
+ commit_options: optionsFor("commit"),
323
+ push_options: optionsFor("push"),
324
+ gc_options: optionsFor("gc"),
325
+ };
326
+ }
327
+ const options = spec.options.map((candidate) => ({
328
+ name: candidate.name,
329
+ ...(candidate.value === undefined ? {} : { value: candidate.value }),
330
+ required: candidate.required === true,
331
+ ...(candidate.default === undefined ? {} : { default: candidate.default }),
332
+ description: candidate.description,
333
+ }));
334
+ return {
335
+ help_for: spec.name,
336
+ usage: spec.usage,
337
+ summary: spec.summary,
338
+ required_options: spec.options
339
+ .filter((candidate) => candidate.required === true)
340
+ .map((candidate) => candidate.name),
341
+ optional_options: spec.options
342
+ .filter((candidate) => candidate.required !== true)
343
+ .map((candidate) => candidate.name),
344
+ defaults: Object.fromEntries(spec.options
345
+ .filter((candidate) => candidate.default !== undefined)
346
+ .map((candidate) => [candidate.name, candidate.default])),
347
+ options,
348
+ ...(spec.notes === undefined ? {} : { notes: [...spec.notes] }),
349
+ };
350
+ }
351
+ function helpText(spec) {
352
+ const lines = [`Usage: ${spec.usage}`, "", spec.summary];
353
+ if (spec.name === "root") {
354
+ lines.push("", "Commands:");
355
+ for (const command of HELP_COMMANDS)
356
+ lines.push(` ${command.name.padEnd(20)} ${command.summary}`);
357
+ lines.push("", "Global options:");
358
+ for (const candidate of GLOBAL_HELP_OPTIONS) {
359
+ const label = candidate.name === "--help" ? "-h, --help" : candidate.name;
360
+ lines.push(` ${label.padEnd(20)} ${candidate.description}`);
361
+ }
362
+ }
363
+ else {
364
+ lines.push("", "Options:");
365
+ if (spec.options.length === 0)
366
+ lines.push(" (none)");
367
+ for (const candidate of spec.options) {
368
+ const label = candidate.value === undefined ? candidate.name : `${candidate.name} ${candidate.value}`;
369
+ const qualifier = candidate.required === true
370
+ ? "required"
371
+ : `optional${candidate.default === undefined ? "" : `; default: ${candidate.default}`}`;
372
+ lines.push(` ${label.padEnd(38)} ${qualifier}; ${candidate.description}`);
373
+ }
374
+ if (spec.notes !== undefined) {
375
+ lines.push("", "Notes:");
376
+ for (const note of spec.notes)
377
+ lines.push(` ${note}`);
378
+ }
379
+ }
380
+ return lines.join("\n");
381
+ }
382
+ const HELP_TEXT = helpText(ROOT_HELP_SPEC);
94
383
  function usageError(code, message, details = null) {
95
384
  return new DomainError(code, message, details);
96
385
  }
@@ -99,7 +388,17 @@ function parseGlobalArguments(argv) {
99
388
  let help = false;
100
389
  let version = false;
101
390
  const commandArguments = [];
391
+ let passthrough = false;
102
392
  for (const argument of argv) {
393
+ if (passthrough) {
394
+ commandArguments.push(argument);
395
+ continue;
396
+ }
397
+ if (argument === "--") {
398
+ passthrough = true;
399
+ commandArguments.push(argument);
400
+ continue;
401
+ }
103
402
  if (argument === "--json") {
104
403
  json = true;
105
404
  }
@@ -138,6 +437,7 @@ function parseOptions(arguments_, allowed) {
138
437
  resources: [],
139
438
  operation: null,
140
439
  message: null,
440
+ message_pattern: null,
141
441
  remote: null,
142
442
  remote_branch: null,
143
443
  mode: null,
@@ -146,6 +446,16 @@ function parseOptions(arguments_, allowed) {
146
446
  apply: false,
147
447
  force: false,
148
448
  create_upstream: false,
449
+ all: false,
450
+ history: false,
451
+ limit: null,
452
+ offset: null,
453
+ paths: [],
454
+ from_revision: null,
455
+ to_revision: null,
456
+ patch: false,
457
+ max_bytes: null,
458
+ max_hunks: null,
149
459
  };
150
460
  let dryRun = false;
151
461
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -153,7 +463,13 @@ function parseOptions(arguments_, allowed) {
153
463
  if (!allowed.has(name)) {
154
464
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
155
465
  }
156
- if (name === "--apply" || name === "--dry-run" || name === "--force" || name === "--create-upstream") {
466
+ if (name === "--apply" ||
467
+ name === "--dry-run" ||
468
+ name === "--force" ||
469
+ name === "--create-upstream" ||
470
+ name === "--all" ||
471
+ name === "--history" ||
472
+ name === "--patch") {
157
473
  if (inlineValue !== null) {
158
474
  return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
159
475
  }
@@ -163,8 +479,14 @@ function parseOptions(arguments_, allowed) {
163
479
  dryRun = true;
164
480
  else if (name === "--force")
165
481
  options.force = true;
166
- else
482
+ else if (name === "--create-upstream")
167
483
  options.create_upstream = true;
484
+ else if (name === "--all")
485
+ options.all = true;
486
+ else if (name === "--history")
487
+ options.history = true;
488
+ else
489
+ options.patch = true;
168
490
  continue;
169
491
  }
170
492
  const value = inlineValue ?? arguments_[index + 1];
@@ -191,6 +513,8 @@ function parseOptions(arguments_, allowed) {
191
513
  options.operation = value;
192
514
  else if (name === "--message")
193
515
  options.message = value;
516
+ else if (name === "--message-pattern")
517
+ options.message_pattern = value;
194
518
  else if (name === "--remote")
195
519
  options.remote = value;
196
520
  else if (name === "--remote-branch")
@@ -201,6 +525,20 @@ function parseOptions(arguments_, allowed) {
201
525
  options.claim_id = value;
202
526
  else if (name === "--repository")
203
527
  options.repository = value;
528
+ else if (name === "--limit")
529
+ options.limit = value;
530
+ else if (name === "--offset")
531
+ options.offset = value;
532
+ else if (name === "--path")
533
+ options.paths.push(value);
534
+ else if (name === "--from")
535
+ options.from_revision = value;
536
+ else if (name === "--to")
537
+ options.to_revision = value;
538
+ else if (name === "--max-bytes")
539
+ options.max_bytes = value;
540
+ else if (name === "--max-hunks")
541
+ options.max_hunks = value;
204
542
  }
205
543
  if (options.apply && dryRun) {
206
544
  return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
@@ -210,6 +548,129 @@ function parseOptions(arguments_, allowed) {
210
548
  function noOptions(arguments_) {
211
549
  return parseOptions(arguments_, new Set());
212
550
  }
551
+ /**
552
+ * `update` accepts a complete desired claim set as repeated
553
+ * `--resource <path> --mode <mode>` pairs. Pairing is by strict local
554
+ * adjacency (each `--resource` must be immediately followed by its own
555
+ * `--mode`, with no other option able to intervene) rather than by
556
+ * parallel-array position, so argv order can never associate a resource
557
+ * with the wrong mode.
558
+ */
559
+ function parseClaimReplacementPairs(arguments_) {
560
+ let sessionId = null;
561
+ let repository = null;
562
+ const pairs = [];
563
+ let pendingResource = null;
564
+ for (let index = 0; index < arguments_.length; index += 1) {
565
+ const { name, inlineValue } = optionParts(arguments_[index]);
566
+ if (name !== "--session" && name !== "--repository" && name !== "--resource" && name !== "--mode") {
567
+ return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
568
+ }
569
+ if (pendingResource !== null && name !== "--mode") {
570
+ return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
571
+ }
572
+ const value = inlineValue ?? arguments_[index + 1];
573
+ if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
574
+ return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
575
+ }
576
+ if (inlineValue === null)
577
+ index += 1;
578
+ if (name === "--session")
579
+ sessionId = value;
580
+ else if (name === "--repository")
581
+ repository = value;
582
+ else if (name === "--resource")
583
+ pendingResource = value;
584
+ else {
585
+ if (pendingResource === null) {
586
+ return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --resource.", {
587
+ option: "--mode",
588
+ }));
589
+ }
590
+ pairs.push({ resource: pendingResource, mode: value });
591
+ pendingResource = null;
592
+ }
593
+ }
594
+ if (pendingResource !== null) {
595
+ return failure(usageError("MISSING_ARGUMENT", "--resource must be immediately followed by --mode.", { option: "--mode" }));
596
+ }
597
+ if (pairs.length === 0) {
598
+ return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
599
+ }
600
+ return { ok: true, value: { session_id: sessionId, repository, pairs } };
601
+ }
602
+ /**
603
+ * `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
604
+ * strict-adjacency scan as `update` so a second `--resource`/`--mode` (in
605
+ * any order) is rejected outright instead of silently overwriting the
606
+ * first pair (last-wins).
607
+ */
608
+ function parseSingleClaimPair(arguments_) {
609
+ const parsed = parseClaimReplacementPairs(arguments_);
610
+ if (!parsed.ok)
611
+ return parsed;
612
+ if (parsed.value.pairs.length > 1) {
613
+ return failure(usageError("INVALID_ARGUMENT", "claim accepts exactly one --resource/--mode pair.", {
614
+ pair_count: parsed.value.pairs.length,
615
+ }));
616
+ }
617
+ const [pair] = parsed.value.pairs;
618
+ return {
619
+ ok: true,
620
+ value: {
621
+ session_id: parsed.value.session_id,
622
+ repository: parsed.value.repository,
623
+ resource: pair.resource,
624
+ mode: pair.mode,
625
+ },
626
+ };
627
+ }
628
+ function sessionListingOptions(parsed) {
629
+ const parseInteger = (option, value) => {
630
+ if (value === null)
631
+ return { ok: true, value: undefined };
632
+ if (!/^\d+$/u.test(value)) {
633
+ return failure(usageError("INVALID_ARGUMENT", `${option} requires a non-negative integer.`, { option, value }));
634
+ }
635
+ const parsedValue = Number(value);
636
+ if (!Number.isSafeInteger(parsedValue)) {
637
+ return failure(usageError("INVALID_ARGUMENT", `${option} is outside the safe integer range.`, { option }));
638
+ }
639
+ return { ok: true, value: parsedValue };
640
+ };
641
+ const limit = parseInteger("--limit", parsed.limit);
642
+ if (!limit.ok)
643
+ return limit;
644
+ if (limit.value !== undefined && (limit.value < 1 || limit.value > MAX_SESSION_LIST_LIMIT)) {
645
+ return failure(usageError("INVALID_ARGUMENT", `--limit must be between 1 and ${MAX_SESSION_LIST_LIMIT}.`, {
646
+ option: "--limit",
647
+ max: MAX_SESSION_LIST_LIMIT,
648
+ }));
649
+ }
650
+ const offset = parseInteger("--offset", parsed.offset);
651
+ if (!offset.ok)
652
+ return offset;
653
+ return {
654
+ ok: true,
655
+ value: {
656
+ include_closed: parsed.all || parsed.history,
657
+ ...(limit.value === undefined ? {} : { limit: limit.value }),
658
+ ...(offset.value === undefined ? {} : { offset: offset.value }),
659
+ },
660
+ };
661
+ }
662
+ function boundedEvidenceInteger(option, value, max) {
663
+ if (value === null)
664
+ return { ok: true, value: undefined };
665
+ if (!/^\d+$/u.test(value)) {
666
+ return failure(usageError("INVALID_ARGUMENT", `${option} requires a positive integer.`, { option, value }));
667
+ }
668
+ const parsed = Number(value);
669
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
670
+ return failure(usageError("INVALID_ARGUMENT", `${option} must be between 1 and ${max}.`, { option, max, value }));
671
+ }
672
+ return { ok: true, value: parsed };
673
+ }
213
674
  function sessionContext(cwd) {
214
675
  return { cwd };
215
676
  }
@@ -223,25 +684,33 @@ async function executeCommand(commandArguments, dependencies) {
223
684
  if (subcommand === undefined) {
224
685
  return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
225
686
  }
226
- if (subcommand === "claim" || subcommand === "update") {
227
- const parsed = parseOptions(rest, new Set(["--session", "--resource", "--mode", "--repository"]));
687
+ if (subcommand === "claim") {
688
+ const parsed = parseSingleClaimPair(rest);
228
689
  if (!parsed.ok)
229
690
  return parsed;
230
- if (parsed.value.resource === null) {
231
- return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
232
- }
233
- if (parsed.value.mode === null) {
234
- return failure(usageError("MISSING_ARGUMENT", "--mode requires a value.", { option: "--mode" }));
235
- }
236
- const options = {
691
+ if (dependencies.backend.claimResources === undefined)
692
+ return claimCapabilityUnavailable(subcommand);
693
+ const result = await dependencies.backend.claimResources(context, {
237
694
  session_id: parsed.value.session_id,
238
695
  repository: parsed.value.repository,
239
696
  claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
240
- };
241
- const operation = subcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
242
- if (operation === undefined)
697
+ });
698
+ return result.ok ? { ok: true, value: result.value } : result;
699
+ }
700
+ if (subcommand === "update") {
701
+ const parsed = parseClaimReplacementPairs(rest);
702
+ if (!parsed.ok)
703
+ return parsed;
704
+ if (dependencies.backend.updateClaims === undefined)
243
705
  return claimCapabilityUnavailable(subcommand);
244
- const result = await operation.call(dependencies.backend, context, options);
706
+ const result = await dependencies.backend.updateClaims(context, {
707
+ session_id: parsed.value.session_id,
708
+ repository: parsed.value.repository,
709
+ claims: parsed.value.pairs.map((pair) => ({
710
+ resource: pair.resource,
711
+ mode: pair.mode,
712
+ })),
713
+ });
245
714
  return result.ok ? { ok: true, value: result.value } : result;
246
715
  }
247
716
  if (subcommand === "claims") {
@@ -278,6 +747,28 @@ async function executeCommand(commandArguments, dependencies) {
278
747
  const result = await dependencies.backend.createSession(context, options);
279
748
  return result.ok ? { ok: true, value: result.value } : result;
280
749
  }
750
+ if (subcommand === "run" || subcommand === "exec") {
751
+ const delimiter = rest.indexOf("--");
752
+ if (delimiter === -1) {
753
+ return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
754
+ }
755
+ const parsed = parseOptions(rest.slice(0, delimiter), new Set(["--session"]));
756
+ if (!parsed.ok)
757
+ return parsed;
758
+ const command = rest[delimiter + 1];
759
+ if (command === undefined || command.length === 0) {
760
+ return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
761
+ }
762
+ const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
763
+ if (!request.ok)
764
+ return request;
765
+ const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
766
+ const result = await runner(request.value, {
767
+ command,
768
+ args: rest.slice(delimiter + 2),
769
+ });
770
+ return result.ok ? { ok: true, value: result.value } : result;
771
+ }
281
772
  if (subcommand === "id" || subcommand === "show" || subcommand === "close") {
282
773
  if (subcommand === "id") {
283
774
  const parsed = noOptions(rest);
@@ -300,33 +791,48 @@ async function executeCommand(commandArguments, dependencies) {
300
791
  return selected.ok ? { ok: true, value: selected.value } : selected;
301
792
  }
302
793
  if (subcommand === "list") {
303
- const parsed = noOptions(rest);
794
+ const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
304
795
  if (!parsed.ok)
305
796
  return parsed;
306
- const result = await dependencies.backend.listSessions(context);
797
+ const options = sessionListingOptions(parsed.value);
798
+ if (!options.ok)
799
+ return options;
800
+ const result = await dependencies.backend.listSessions(context, options.value);
307
801
  return result.ok ? { ok: true, value: result.value } : result;
308
802
  }
309
803
  return failure(new DomainError("UNKNOWN_COMMAND", `Unknown session subcommand: ${subcommand}.`, { subcommand }));
310
804
  }
311
805
  if (command === "resource") {
312
806
  const resourceSubcommand = subcommand ?? "list";
313
- if (resourceSubcommand === "claim" || resourceSubcommand === "update") {
314
- const parsed = parseOptions(rest, new Set(["--session", "--resource", "--mode", "--repository"]));
807
+ if (resourceSubcommand === "claim") {
808
+ const parsed = parseSingleClaimPair(rest);
315
809
  if (!parsed.ok)
316
810
  return parsed;
317
- if (parsed.value.resource === null || parsed.value.mode === null) {
318
- return failure(usageError("MISSING_ARGUMENT", "resource claim requires --resource and --mode."));
319
- }
320
- const operation = resourceSubcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
321
- if (operation === undefined)
811
+ if (dependencies.backend.claimResources === undefined)
322
812
  return claimCapabilityUnavailable(resourceSubcommand);
323
- const result = await operation.call(dependencies.backend, context, {
813
+ const result = await dependencies.backend.claimResources(context, {
324
814
  session_id: parsed.value.session_id,
325
815
  repository: parsed.value.repository,
326
816
  claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
327
817
  });
328
818
  return result.ok ? { ok: true, value: result.value } : result;
329
819
  }
820
+ if (resourceSubcommand === "update") {
821
+ const parsed = parseClaimReplacementPairs(rest);
822
+ if (!parsed.ok)
823
+ return parsed;
824
+ if (dependencies.backend.updateClaims === undefined)
825
+ return claimCapabilityUnavailable(resourceSubcommand);
826
+ const result = await dependencies.backend.updateClaims(context, {
827
+ session_id: parsed.value.session_id,
828
+ repository: parsed.value.repository,
829
+ claims: parsed.value.pairs.map((pair) => ({
830
+ resource: pair.resource,
831
+ mode: pair.mode,
832
+ })),
833
+ });
834
+ return result.ok ? { ok: true, value: result.value } : result;
835
+ }
330
836
  if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
331
837
  const parsed = parseOptions(rest, new Set(["--session"]));
332
838
  if (!parsed.ok)
@@ -386,8 +892,59 @@ async function executeCommand(commandArguments, dependencies) {
386
892
  const result = await dependencies.backend.checkpoint(context, options);
387
893
  return result.ok ? { ok: true, value: result.value } : result;
388
894
  }
895
+ if (command === "evidence" && subcommand === "snapshot") {
896
+ const parsed = parseOptions(rest, new Set(["--session"]));
897
+ if (!parsed.ok)
898
+ return parsed;
899
+ if (parsed.value.session_id === null) {
900
+ return failure(usageError("MISSING_ARGUMENT", "evidence snapshot requires --session.", { option: "--session" }));
901
+ }
902
+ if (dependencies.backend.repositoryEvidence === undefined) {
903
+ return repositoryEvidenceCapabilityUnavailable("evidence snapshot");
904
+ }
905
+ const options = { session_id: parsed.value.session_id };
906
+ const result = await dependencies.backend.repositoryEvidence(context, options);
907
+ return result.ok ? { ok: true, value: result.value } : result;
908
+ }
909
+ if (command === "diff") {
910
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"]));
911
+ if (!parsed.ok)
912
+ return parsed;
913
+ if (parsed.value.session_id === null) {
914
+ return failure(usageError("MISSING_ARGUMENT", "diff requires --session.", { option: "--session" }));
915
+ }
916
+ if (parsed.value.paths.length === 0) {
917
+ return failure(usageError("MISSING_ARGUMENT", "diff requires at least one --path.", { option: "--path" }));
918
+ }
919
+ if (parsed.value.paths.length > EVIDENCE_MAX_DIFF_PATHS) {
920
+ return failure(usageError("INVALID_ARGUMENT", `diff accepts at most ${EVIDENCE_MAX_DIFF_PATHS} paths.`, {
921
+ option: "--path",
922
+ max: EVIDENCE_MAX_DIFF_PATHS,
923
+ }));
924
+ }
925
+ const maxBytes = boundedEvidenceInteger("--max-bytes", parsed.value.max_bytes, EVIDENCE_MAX_DIFF_BYTES);
926
+ if (!maxBytes.ok)
927
+ return maxBytes;
928
+ const maxHunks = boundedEvidenceInteger("--max-hunks", parsed.value.max_hunks, EVIDENCE_MAX_DIFF_HUNKS);
929
+ if (!maxHunks.ok)
930
+ return maxHunks;
931
+ if (dependencies.backend.repositoryDiff === undefined) {
932
+ return repositoryEvidenceCapabilityUnavailable("diff");
933
+ }
934
+ const options = {
935
+ session_id: parsed.value.session_id,
936
+ paths: parsed.value.paths,
937
+ from: parsed.value.from_revision,
938
+ to: parsed.value.to_revision,
939
+ include_patch: parsed.value.patch,
940
+ max_bytes: maxBytes.value,
941
+ max_hunks: maxHunks.value,
942
+ };
943
+ const result = await dependencies.backend.repositoryDiff(context, options);
944
+ return result.ok ? { ok: true, value: result.value } : result;
945
+ }
389
946
  if (command === "commit") {
390
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource"]));
947
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
391
948
  if (!parsed.ok)
392
949
  return parsed;
393
950
  if (parsed.value.message === null) {
@@ -402,6 +959,7 @@ async function executeCommand(commandArguments, dependencies) {
402
959
  session_id: parsed.value.session_id,
403
960
  message: parsed.value.message,
404
961
  resources: parsed.value.resources,
962
+ message_pattern: parsed.value.message_pattern,
405
963
  });
406
964
  return result.ok ? { ok: true, value: result.value } : result;
407
965
  }
@@ -436,10 +994,13 @@ async function executeCommand(commandArguments, dependencies) {
436
994
  return result.ok ? { ok: true, value: result.value } : result;
437
995
  }
438
996
  if (command === "status") {
439
- const parsed = noOptions([subcommand, ...rest].filter((argument) => argument !== undefined));
997
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--all", "--history", "--limit", "--offset"]));
440
998
  if (!parsed.ok)
441
999
  return parsed;
442
- const result = await dependencies.backend.status(context);
1000
+ const options = sessionListingOptions(parsed.value);
1001
+ if (!options.ok)
1002
+ return options;
1003
+ const result = await dependencies.backend.status(context, options.value);
443
1004
  return result.ok ? { ok: true, value: result.value } : result;
444
1005
  }
445
1006
  if (command === "guard") {
@@ -517,6 +1078,8 @@ function commandName(commandArguments) {
517
1078
  return commandArguments.slice(0, 2).join(" ");
518
1079
  if (commandArguments[0] === "resource")
519
1080
  return commandArguments.slice(0, 2).join(" ");
1081
+ if (commandArguments[0] === "evidence")
1082
+ return commandArguments.slice(0, 2).join(" ");
520
1083
  return commandArguments[0] ?? "cli";
521
1084
  }
522
1085
  function claimCapabilityUnavailable(operation) {
@@ -532,11 +1095,20 @@ function checkpointCapabilityUnavailable() {
532
1095
  operation: "checkpoint",
533
1096
  }));
534
1097
  }
1098
+ function repositoryEvidenceCapabilityUnavailable(operation) {
1099
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
1100
+ }
535
1101
  function mutationCapabilityUnavailable(operation) {
536
1102
  return failure(new DomainError("BACKEND_UNAVAILABLE", "Governed Git mutation capability is not available.", { operation }));
537
1103
  }
538
1104
  function deniedAuthorization(decision) {
539
1105
  const code = decision.code === "ALLOWED" ? "OPERATION_REJECTED" : decision.code;
1106
+ const diagnosticDetails = code === "INSUFFICIENT_CLAIM_MODE"
1107
+ ? {
1108
+ resource: decision.details.resource,
1109
+ granted_modes: decision.details.grantedModes,
1110
+ }
1111
+ : {};
540
1112
  return failure(new DomainError(code, `Operation denied: ${code}.`, {
541
1113
  allowed: false,
542
1114
  schema_version: decision.schema_version,
@@ -550,6 +1122,7 @@ function deniedAuthorization(decision) {
550
1122
  requested_session_id: decision.requested_session_id,
551
1123
  state: decision.state,
552
1124
  resources: decision.resources,
1125
+ ...diagnosticDetails,
553
1126
  details: decision.details,
554
1127
  }));
555
1128
  }
@@ -559,15 +1132,18 @@ function emitFailure(mode, command, error, io) {
559
1132
  }
560
1133
  export async function runCli(argv, dependencies = {}) {
561
1134
  const io = dependencies.io ?? defaultCliIO();
562
- const mode = argv.includes("--json") ? "json" : "human";
1135
+ const delimiter = argv.indexOf("--");
1136
+ const globalArguments = delimiter === -1 ? argv : argv.slice(0, delimiter);
1137
+ const mode = globalArguments.includes("--json") ? "json" : "human";
563
1138
  const parsed = parseGlobalArguments(argv);
564
1139
  if (!parsed.ok)
565
1140
  return emitFailure(mode, "cli", parsed.error, io);
566
1141
  if (parsed.value.help) {
1142
+ const spec = helpSpecFor(parsed.value.commandArguments);
567
1143
  if (mode === "json")
568
- io.stdout(renderSuccess(mode, "help", HELP_DATA));
1144
+ io.stdout(renderSuccess(mode, "help", helpPayload(spec)));
569
1145
  else
570
- io.stdout(HELP_TEXT);
1146
+ io.stdout(helpText(spec));
571
1147
  return EXIT_CODES.success;
572
1148
  }
573
1149
  if (parsed.value.version) {
@@ -599,10 +1175,23 @@ export async function runCli(argv, dependencies = {}) {
599
1175
  const cwd = dependencies.cwd ?? process.cwd();
600
1176
  try {
601
1177
  const backend = dependencies.backend ?? createLocalSessionBackend();
602
- const result = await executeCommand(parsed.value.commandArguments, { backend, cwd });
1178
+ const result = await executeCommand(parsed.value.commandArguments, {
1179
+ backend,
1180
+ cwd,
1181
+ sandboxRunner: dependencies.sandboxRunner,
1182
+ sandboxProbe: dependencies.sandboxProbe,
1183
+ sandboxRuntimeLayout: dependencies.sandboxRuntimeLayout,
1184
+ });
603
1185
  if (!result.ok)
604
1186
  return emitFailure(mode, command, result.error, io);
605
1187
  io.stdout(renderSuccess(mode, command, result.value));
1188
+ if ((command === "session run" || command === "session exec") &&
1189
+ typeof result.value.exit_code === "number" &&
1190
+ result.value.exit_code !== 0) {
1191
+ return result.value.exit_code >= 1 && result.value.exit_code <= 255
1192
+ ? result.value.exit_code
1193
+ : EXIT_CODES.rejected;
1194
+ }
606
1195
  return EXIT_CODES.success;
607
1196
  }
608
1197
  catch {