nawabari 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,96 +1,345 @@
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";
7
9
  const CLI_NAME = "nawabari";
8
10
  const packageMetadata = createRequire(import.meta.url)("../package.json");
9
11
  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"],
12
+ const GLOBAL_HELP_OPTIONS = [
13
+ { name: "--json", description: "Emit one stable JSON document on stdout" },
14
+ { name: "--help", description: "Show command-specific help" },
15
+ { name: "--version", description: "Print the installed version" },
16
+ ];
17
+ const option = (name, description, options = {}) => ({ name, description, ...options });
18
+ const HELP_COMMANDS = [
19
+ {
20
+ name: "session create",
21
+ summary: "Request a new Nawabari session",
22
+ usage: `${CLI_NAME} session create [options]`,
23
+ options: [
24
+ option("--branch", "Branch to create; omitted uses the generated session branch", {
25
+ value: "<name>",
26
+ default: "nawabari/session/<session_id>",
27
+ }),
28
+ option("--worktree", "Managed worktree path; omitted uses the resolved repository-local root", {
29
+ value: "<path>",
30
+ default: "<managed_worktree_root>/<repository>-<session_id>",
31
+ }),
32
+ option("--base", "Commit-resolving base ref for the new worktree", { value: "<ref>", default: "HEAD" }),
33
+ option("--label", "Optional display label; never used as an identity", { value: "<text>", default: "omitted" }),
34
+ ],
35
+ notes: ["All create options are optional. Use status --json to discover managed_worktree_root."],
36
+ },
37
+ {
38
+ name: "session id",
39
+ summary: "Resolve the current session identity",
40
+ usage: `${CLI_NAME} session id`,
41
+ options: [],
42
+ },
43
+ {
44
+ name: "session show",
45
+ summary: "Show the current or selected session",
46
+ usage: `${CLI_NAME} session show [--session <id>]`,
47
+ options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
48
+ },
49
+ {
50
+ name: "session list",
51
+ summary: "List bounded repository session records",
52
+ usage: `${CLI_NAME} session list [--all|--history]`,
53
+ options: [
54
+ option("--all", "Include closed history; explicit unbounded history view"),
55
+ option("--history", "Alias for --all"),
56
+ ],
57
+ notes: ["Default output excludes closed records and is limited to 64 records."],
58
+ },
59
+ {
60
+ name: "session claim",
61
+ summary: "Add a canonical resource claim",
62
+ usage: `${CLI_NAME} session claim --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
63
+ options: [
64
+ option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
65
+ option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
66
+ option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
67
+ option("--repository", "Expected repository identity", { value: "<id>" }),
68
+ ],
69
+ },
70
+ {
71
+ name: "session update",
72
+ summary: "Replace a session's resource claims",
73
+ usage: `${CLI_NAME} session update --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 claims",
83
+ summary: "List canonical resource claims",
84
+ usage: `${CLI_NAME} session claims [--session <id>]`,
85
+ options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
86
+ },
87
+ {
88
+ name: "session release",
89
+ summary: "Release resource claims",
90
+ usage: `${CLI_NAME} session release [--session <id>] [--claim-id <id>]`,
91
+ options: [
92
+ option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
93
+ option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
94
+ ],
95
+ },
96
+ {
97
+ name: "resource claim",
98
+ summary: "Add a canonical resource claim (alias)",
99
+ usage: `${CLI_NAME} resource claim --resource <path-or-glob> --mode <read|write|exclusive-write>`,
100
+ options: [],
101
+ },
102
+ {
103
+ name: "resource update",
104
+ summary: "Replace a session's resource claims (alias)",
105
+ usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write>`,
106
+ options: [],
107
+ },
108
+ {
109
+ name: "resource list",
110
+ summary: "List canonical resource claims (alias)",
111
+ usage: `${CLI_NAME} resource list [--session <id>]`,
112
+ options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
113
+ },
114
+ {
115
+ name: "resource release",
116
+ summary: "Release resource claims (alias)",
117
+ usage: `${CLI_NAME} resource release [--session <id>] [--claim-id <id>]`,
118
+ options: [
119
+ option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
120
+ option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
121
+ ],
122
+ },
123
+ {
124
+ name: "session close",
125
+ summary: "Close the current or selected session",
126
+ usage: `${CLI_NAME} session close [--session <id>]`,
127
+ options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
128
+ },
129
+ {
130
+ name: "authorize",
131
+ summary: "Authorize an operation against concrete claims",
132
+ usage: `${CLI_NAME} authorize --operation <name> --resource <path> [--resource <path>] [--session <id>]`,
133
+ options: [
134
+ option("--session", "Assert the current session identity", { value: "<id>" }),
135
+ option("--operation", "Operation vocabulary entry", { value: "<name>", required: true }),
136
+ option("--resource", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
137
+ ],
138
+ },
139
+ {
140
+ name: "checkpoint",
141
+ summary: "Capture bounded Git execution evidence",
142
+ usage: `${CLI_NAME} checkpoint [--session <id>]`,
143
+ options: [option("--session", "Assert the current session identity", { value: "<id>" })],
144
+ },
145
+ {
146
+ name: "evidence snapshot",
147
+ summary: "Capture bounded read-only evidence for one owned session",
148
+ usage: `${CLI_NAME} evidence snapshot --session <id>`,
149
+ options: [option("--session", "Explicit owned session to observe", { value: "<id>", required: true })],
150
+ notes: ["The result is Git-observable physical evidence only; it contains no task or semantic interpretation."],
151
+ },
152
+ {
153
+ name: "diff",
154
+ summary: "Inspect bounded Git evidence for explicit paths",
155
+ usage: `${CLI_NAME} diff --session <id> --path <path> [options]`,
156
+ options: [
157
+ option("--session", "Explicit owned session to observe", { value: "<id>", required: true }),
158
+ option("--path", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
159
+ option("--from", "Commit/ref at the start of the range", { value: "<ref>", default: "HEAD" }),
160
+ option("--to", "Commit/ref at the end of the range; omitted means worktree", { value: "<ref>" }),
161
+ option("--patch", "Include patch text; requires the bounded byte/hunk limits"),
162
+ option("--max-bytes", "Maximum UTF-8 patch bytes", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_BYTES) }),
163
+ option("--max-hunks", "Maximum patch hunks", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_HUNKS) }),
164
+ ],
165
+ },
166
+ {
167
+ name: "commit",
168
+ summary: "Commit explicit claim-authorized resources",
169
+ usage: `${CLI_NAME} commit --message <final-message> --resource <path> [--resource <path>] [--session <id>] [--message-pattern <regex>]`,
170
+ options: [
171
+ option("--session", "Assert the current session identity", { value: "<id>" }),
172
+ option("--message", "Caller-decided final commit message", { value: "<final-message>", required: true }),
173
+ option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
174
+ option("--message-pattern", "Caller-declared commit-message rule; validated only when supplied", {
175
+ value: "<regex>",
176
+ }),
177
+ ],
178
+ },
179
+ {
180
+ name: "push",
181
+ summary: "Push the owned branch to an explicit target",
182
+ usage: `${CLI_NAME} push --remote <name> --branch <name> --resource <path> [options]`,
183
+ options: [
184
+ option("--session", "Assert the current session identity", { value: "<id>" }),
185
+ option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
186
+ option("--remote", "Explicit Git remote", { value: "<name>", required: true }),
187
+ option("--branch", "Explicit target branch", { value: "<name>", required: true }),
188
+ option("--remote-branch", "Explicit remote branch alias for --branch", { value: "<name>" }),
189
+ option("--force", "Allow force-with-lease when relation requires it"),
190
+ option("--create-upstream", "Allow creation of a missing upstream"),
191
+ ],
192
+ },
193
+ {
194
+ name: "status",
195
+ summary: "Show repository context and bounded session status",
196
+ usage: `${CLI_NAME} status [--all|--history]`,
197
+ options: [
198
+ option("--all", "Include closed history; explicit unbounded history view"),
199
+ option("--history", "Alias for --all"),
200
+ ],
201
+ notes: ["The default machine result exposes managed_worktree_root for session-create path discovery."],
202
+ },
203
+ {
204
+ name: "guard",
205
+ summary: "Authorize the current worktree or operation",
206
+ usage: `${CLI_NAME} guard [--session <id>] [--operation <name> --resource <path>]`,
207
+ options: [
208
+ option("--session", "Assert the current session identity", { value: "<id>" }),
209
+ option("--operation", "Authorize an operation when resources are supplied", { value: "<name>" }),
210
+ option("--resource", "Concrete resource; repeatable with --operation", { value: "<path>" }),
211
+ ],
212
+ },
213
+ {
214
+ name: "gc",
215
+ summary: "Detect or clean eligible stale sessions",
216
+ usage: `${CLI_NAME} gc [--dry-run|--apply]`,
217
+ options: [
218
+ option("--apply", "Apply only cleanup that passes safety checks"),
219
+ option("--dry-run", "Preflight eligible stale cleanup without mutation"),
220
+ ],
221
+ notes: [
222
+ "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.",
223
+ ],
224
+ },
225
+ {
226
+ name: "doctor",
227
+ summary: "Check local Nawabari prerequisites and reconciliation",
228
+ usage: `${CLI_NAME} doctor`,
229
+ options: [],
230
+ },
231
+ {
232
+ name: "capabilities",
233
+ summary: "Describe the standalone CLI/JSON contract",
234
+ usage: `${CLI_NAME} capabilities`,
235
+ options: [],
236
+ },
237
+ ];
238
+ const ROOT_HELP_SPEC = {
239
+ name: "root",
240
+ summary: "Standalone local Git/session ownership CLI",
241
+ usage: `${CLI_NAME} <command> [options]`,
242
+ options: GLOBAL_HELP_OPTIONS,
93
243
  };
244
+ function helpSpecFor(commandArguments) {
245
+ if (commandArguments.length === 0)
246
+ return ROOT_HELP_SPEC;
247
+ let key = commandArguments[0] === "resource"
248
+ ? `resource ${commandArguments[1] ?? "list"}`
249
+ : commandArguments.slice(0, 2).join(" ");
250
+ if (key === "resource claims")
251
+ key = "resource list";
252
+ const direct = HELP_COMMANDS.find((spec) => spec.name === key);
253
+ if (direct !== undefined && !direct.name.startsWith("resource "))
254
+ return direct;
255
+ const aliasTarget = direct?.name.replace(/^resource /u, "session ");
256
+ const target = aliasTarget === undefined ? undefined : HELP_COMMANDS.find((spec) => spec.name === aliasTarget);
257
+ if (direct !== undefined && target !== undefined) {
258
+ return {
259
+ name: direct.name,
260
+ summary: direct.summary,
261
+ usage: direct.usage,
262
+ options: direct.options,
263
+ notes: direct.notes,
264
+ };
265
+ }
266
+ return HELP_COMMANDS.find((spec) => spec.name === commandArguments[0]) ?? ROOT_HELP_SPEC;
267
+ }
268
+ function helpPayload(spec) {
269
+ if (spec.name === "root") {
270
+ const optionNames = (options) => options.map((candidate) => candidate.name);
271
+ const sessionOptions = HELP_COMMANDS.filter((command) => command.name.startsWith("session ")).flatMap((command) => command.options);
272
+ const unique = (values) => values.filter((value, index) => values.indexOf(value) === index);
273
+ const sessionListOnlyOptions = new Set(["--all", "--history"]);
274
+ const optionsFor = (name) => optionNames(HELP_COMMANDS.find((command) => command.name === name)?.options ?? []);
275
+ return {
276
+ usage: `Usage: ${spec.usage}`,
277
+ commands: HELP_COMMANDS.map((command) => command.name),
278
+ options: ["--json", "--help", "--version"],
279
+ session_options: unique(sessionOptions.map((option) => option.name).filter((name) => !sessionListOnlyOptions.has(name))),
280
+ authorization_options: optionsFor("authorize"),
281
+ checkpoint_options: optionsFor("checkpoint"),
282
+ commit_options: optionsFor("commit"),
283
+ push_options: optionsFor("push"),
284
+ gc_options: optionsFor("gc"),
285
+ };
286
+ }
287
+ const options = spec.options.map((candidate) => ({
288
+ name: candidate.name,
289
+ ...(candidate.value === undefined ? {} : { value: candidate.value }),
290
+ required: candidate.required === true,
291
+ ...(candidate.default === undefined ? {} : { default: candidate.default }),
292
+ description: candidate.description,
293
+ }));
294
+ return {
295
+ help_for: spec.name,
296
+ usage: spec.usage,
297
+ summary: spec.summary,
298
+ required_options: spec.options
299
+ .filter((candidate) => candidate.required === true)
300
+ .map((candidate) => candidate.name),
301
+ optional_options: spec.options
302
+ .filter((candidate) => candidate.required !== true)
303
+ .map((candidate) => candidate.name),
304
+ defaults: Object.fromEntries(spec.options
305
+ .filter((candidate) => candidate.default !== undefined)
306
+ .map((candidate) => [candidate.name, candidate.default])),
307
+ options,
308
+ ...(spec.notes === undefined ? {} : { notes: [...spec.notes] }),
309
+ };
310
+ }
311
+ function helpText(spec) {
312
+ const lines = [`Usage: ${spec.usage}`, "", spec.summary];
313
+ if (spec.name === "root") {
314
+ lines.push("", "Commands:");
315
+ for (const command of HELP_COMMANDS)
316
+ lines.push(` ${command.name.padEnd(20)} ${command.summary}`);
317
+ lines.push("", "Global options:");
318
+ for (const candidate of GLOBAL_HELP_OPTIONS) {
319
+ const label = candidate.name === "--help" ? "-h, --help" : candidate.name;
320
+ lines.push(` ${label.padEnd(20)} ${candidate.description}`);
321
+ }
322
+ }
323
+ else {
324
+ lines.push("", "Options:");
325
+ if (spec.options.length === 0)
326
+ lines.push(" (none)");
327
+ for (const candidate of spec.options) {
328
+ const label = candidate.value === undefined ? candidate.name : `${candidate.name} ${candidate.value}`;
329
+ const qualifier = candidate.required === true
330
+ ? "required"
331
+ : `optional${candidate.default === undefined ? "" : `; default: ${candidate.default}`}`;
332
+ lines.push(` ${label.padEnd(38)} ${qualifier}; ${candidate.description}`);
333
+ }
334
+ if (spec.notes !== undefined) {
335
+ lines.push("", "Notes:");
336
+ for (const note of spec.notes)
337
+ lines.push(` ${note}`);
338
+ }
339
+ }
340
+ return lines.join("\n");
341
+ }
342
+ const HELP_TEXT = helpText(ROOT_HELP_SPEC);
94
343
  function usageError(code, message, details = null) {
95
344
  return new DomainError(code, message, details);
96
345
  }
@@ -138,6 +387,7 @@ function parseOptions(arguments_, allowed) {
138
387
  resources: [],
139
388
  operation: null,
140
389
  message: null,
390
+ message_pattern: null,
141
391
  remote: null,
142
392
  remote_branch: null,
143
393
  mode: null,
@@ -146,6 +396,16 @@ function parseOptions(arguments_, allowed) {
146
396
  apply: false,
147
397
  force: false,
148
398
  create_upstream: false,
399
+ all: false,
400
+ history: false,
401
+ limit: null,
402
+ offset: null,
403
+ paths: [],
404
+ from_revision: null,
405
+ to_revision: null,
406
+ patch: false,
407
+ max_bytes: null,
408
+ max_hunks: null,
149
409
  };
150
410
  let dryRun = false;
151
411
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -153,7 +413,13 @@ function parseOptions(arguments_, allowed) {
153
413
  if (!allowed.has(name)) {
154
414
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
155
415
  }
156
- if (name === "--apply" || name === "--dry-run" || name === "--force" || name === "--create-upstream") {
416
+ if (name === "--apply" ||
417
+ name === "--dry-run" ||
418
+ name === "--force" ||
419
+ name === "--create-upstream" ||
420
+ name === "--all" ||
421
+ name === "--history" ||
422
+ name === "--patch") {
157
423
  if (inlineValue !== null) {
158
424
  return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
159
425
  }
@@ -163,8 +429,14 @@ function parseOptions(arguments_, allowed) {
163
429
  dryRun = true;
164
430
  else if (name === "--force")
165
431
  options.force = true;
166
- else
432
+ else if (name === "--create-upstream")
167
433
  options.create_upstream = true;
434
+ else if (name === "--all")
435
+ options.all = true;
436
+ else if (name === "--history")
437
+ options.history = true;
438
+ else
439
+ options.patch = true;
168
440
  continue;
169
441
  }
170
442
  const value = inlineValue ?? arguments_[index + 1];
@@ -191,6 +463,8 @@ function parseOptions(arguments_, allowed) {
191
463
  options.operation = value;
192
464
  else if (name === "--message")
193
465
  options.message = value;
466
+ else if (name === "--message-pattern")
467
+ options.message_pattern = value;
194
468
  else if (name === "--remote")
195
469
  options.remote = value;
196
470
  else if (name === "--remote-branch")
@@ -201,6 +475,20 @@ function parseOptions(arguments_, allowed) {
201
475
  options.claim_id = value;
202
476
  else if (name === "--repository")
203
477
  options.repository = value;
478
+ else if (name === "--limit")
479
+ options.limit = value;
480
+ else if (name === "--offset")
481
+ options.offset = value;
482
+ else if (name === "--path")
483
+ options.paths.push(value);
484
+ else if (name === "--from")
485
+ options.from_revision = value;
486
+ else if (name === "--to")
487
+ options.to_revision = value;
488
+ else if (name === "--max-bytes")
489
+ options.max_bytes = value;
490
+ else if (name === "--max-hunks")
491
+ options.max_hunks = value;
204
492
  }
205
493
  if (options.apply && dryRun) {
206
494
  return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
@@ -210,6 +498,52 @@ function parseOptions(arguments_, allowed) {
210
498
  function noOptions(arguments_) {
211
499
  return parseOptions(arguments_, new Set());
212
500
  }
501
+ function sessionListingOptions(parsed) {
502
+ const parseInteger = (option, value) => {
503
+ if (value === null)
504
+ return { ok: true, value: undefined };
505
+ if (!/^\d+$/u.test(value)) {
506
+ return failure(usageError("INVALID_ARGUMENT", `${option} requires a non-negative integer.`, { option, value }));
507
+ }
508
+ const parsedValue = Number(value);
509
+ if (!Number.isSafeInteger(parsedValue)) {
510
+ return failure(usageError("INVALID_ARGUMENT", `${option} is outside the safe integer range.`, { option }));
511
+ }
512
+ return { ok: true, value: parsedValue };
513
+ };
514
+ const limit = parseInteger("--limit", parsed.limit);
515
+ if (!limit.ok)
516
+ return limit;
517
+ if (limit.value !== undefined && (limit.value < 1 || limit.value > MAX_SESSION_LIST_LIMIT)) {
518
+ return failure(usageError("INVALID_ARGUMENT", `--limit must be between 1 and ${MAX_SESSION_LIST_LIMIT}.`, {
519
+ option: "--limit",
520
+ max: MAX_SESSION_LIST_LIMIT,
521
+ }));
522
+ }
523
+ const offset = parseInteger("--offset", parsed.offset);
524
+ if (!offset.ok)
525
+ return offset;
526
+ return {
527
+ ok: true,
528
+ value: {
529
+ include_closed: parsed.all || parsed.history,
530
+ ...(limit.value === undefined ? {} : { limit: limit.value }),
531
+ ...(offset.value === undefined ? {} : { offset: offset.value }),
532
+ },
533
+ };
534
+ }
535
+ function boundedEvidenceInteger(option, value, max) {
536
+ if (value === null)
537
+ return { ok: true, value: undefined };
538
+ if (!/^\d+$/u.test(value)) {
539
+ return failure(usageError("INVALID_ARGUMENT", `${option} requires a positive integer.`, { option, value }));
540
+ }
541
+ const parsed = Number(value);
542
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > max) {
543
+ return failure(usageError("INVALID_ARGUMENT", `${option} must be between 1 and ${max}.`, { option, max, value }));
544
+ }
545
+ return { ok: true, value: parsed };
546
+ }
213
547
  function sessionContext(cwd) {
214
548
  return { cwd };
215
549
  }
@@ -300,10 +634,13 @@ async function executeCommand(commandArguments, dependencies) {
300
634
  return selected.ok ? { ok: true, value: selected.value } : selected;
301
635
  }
302
636
  if (subcommand === "list") {
303
- const parsed = noOptions(rest);
637
+ const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
304
638
  if (!parsed.ok)
305
639
  return parsed;
306
- const result = await dependencies.backend.listSessions(context);
640
+ const options = sessionListingOptions(parsed.value);
641
+ if (!options.ok)
642
+ return options;
643
+ const result = await dependencies.backend.listSessions(context, options.value);
307
644
  return result.ok ? { ok: true, value: result.value } : result;
308
645
  }
309
646
  return failure(new DomainError("UNKNOWN_COMMAND", `Unknown session subcommand: ${subcommand}.`, { subcommand }));
@@ -386,8 +723,59 @@ async function executeCommand(commandArguments, dependencies) {
386
723
  const result = await dependencies.backend.checkpoint(context, options);
387
724
  return result.ok ? { ok: true, value: result.value } : result;
388
725
  }
726
+ if (command === "evidence" && subcommand === "snapshot") {
727
+ const parsed = parseOptions(rest, new Set(["--session"]));
728
+ if (!parsed.ok)
729
+ return parsed;
730
+ if (parsed.value.session_id === null) {
731
+ return failure(usageError("MISSING_ARGUMENT", "evidence snapshot requires --session.", { option: "--session" }));
732
+ }
733
+ if (dependencies.backend.repositoryEvidence === undefined) {
734
+ return repositoryEvidenceCapabilityUnavailable("evidence snapshot");
735
+ }
736
+ const options = { session_id: parsed.value.session_id };
737
+ const result = await dependencies.backend.repositoryEvidence(context, options);
738
+ return result.ok ? { ok: true, value: result.value } : result;
739
+ }
740
+ if (command === "diff") {
741
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"]));
742
+ if (!parsed.ok)
743
+ return parsed;
744
+ if (parsed.value.session_id === null) {
745
+ return failure(usageError("MISSING_ARGUMENT", "diff requires --session.", { option: "--session" }));
746
+ }
747
+ if (parsed.value.paths.length === 0) {
748
+ return failure(usageError("MISSING_ARGUMENT", "diff requires at least one --path.", { option: "--path" }));
749
+ }
750
+ if (parsed.value.paths.length > EVIDENCE_MAX_DIFF_PATHS) {
751
+ return failure(usageError("INVALID_ARGUMENT", `diff accepts at most ${EVIDENCE_MAX_DIFF_PATHS} paths.`, {
752
+ option: "--path",
753
+ max: EVIDENCE_MAX_DIFF_PATHS,
754
+ }));
755
+ }
756
+ const maxBytes = boundedEvidenceInteger("--max-bytes", parsed.value.max_bytes, EVIDENCE_MAX_DIFF_BYTES);
757
+ if (!maxBytes.ok)
758
+ return maxBytes;
759
+ const maxHunks = boundedEvidenceInteger("--max-hunks", parsed.value.max_hunks, EVIDENCE_MAX_DIFF_HUNKS);
760
+ if (!maxHunks.ok)
761
+ return maxHunks;
762
+ if (dependencies.backend.repositoryDiff === undefined) {
763
+ return repositoryEvidenceCapabilityUnavailable("diff");
764
+ }
765
+ const options = {
766
+ session_id: parsed.value.session_id,
767
+ paths: parsed.value.paths,
768
+ from: parsed.value.from_revision,
769
+ to: parsed.value.to_revision,
770
+ include_patch: parsed.value.patch,
771
+ max_bytes: maxBytes.value,
772
+ max_hunks: maxHunks.value,
773
+ };
774
+ const result = await dependencies.backend.repositoryDiff(context, options);
775
+ return result.ok ? { ok: true, value: result.value } : result;
776
+ }
389
777
  if (command === "commit") {
390
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource"]));
778
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
391
779
  if (!parsed.ok)
392
780
  return parsed;
393
781
  if (parsed.value.message === null) {
@@ -402,6 +790,7 @@ async function executeCommand(commandArguments, dependencies) {
402
790
  session_id: parsed.value.session_id,
403
791
  message: parsed.value.message,
404
792
  resources: parsed.value.resources,
793
+ message_pattern: parsed.value.message_pattern,
405
794
  });
406
795
  return result.ok ? { ok: true, value: result.value } : result;
407
796
  }
@@ -436,10 +825,13 @@ async function executeCommand(commandArguments, dependencies) {
436
825
  return result.ok ? { ok: true, value: result.value } : result;
437
826
  }
438
827
  if (command === "status") {
439
- const parsed = noOptions([subcommand, ...rest].filter((argument) => argument !== undefined));
828
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--all", "--history", "--limit", "--offset"]));
440
829
  if (!parsed.ok)
441
830
  return parsed;
442
- const result = await dependencies.backend.status(context);
831
+ const options = sessionListingOptions(parsed.value);
832
+ if (!options.ok)
833
+ return options;
834
+ const result = await dependencies.backend.status(context, options.value);
443
835
  return result.ok ? { ok: true, value: result.value } : result;
444
836
  }
445
837
  if (command === "guard") {
@@ -517,6 +909,8 @@ function commandName(commandArguments) {
517
909
  return commandArguments.slice(0, 2).join(" ");
518
910
  if (commandArguments[0] === "resource")
519
911
  return commandArguments.slice(0, 2).join(" ");
912
+ if (commandArguments[0] === "evidence")
913
+ return commandArguments.slice(0, 2).join(" ");
520
914
  return commandArguments[0] ?? "cli";
521
915
  }
522
916
  function claimCapabilityUnavailable(operation) {
@@ -532,11 +926,20 @@ function checkpointCapabilityUnavailable() {
532
926
  operation: "checkpoint",
533
927
  }));
534
928
  }
929
+ function repositoryEvidenceCapabilityUnavailable(operation) {
930
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
931
+ }
535
932
  function mutationCapabilityUnavailable(operation) {
536
933
  return failure(new DomainError("BACKEND_UNAVAILABLE", "Governed Git mutation capability is not available.", { operation }));
537
934
  }
538
935
  function deniedAuthorization(decision) {
539
936
  const code = decision.code === "ALLOWED" ? "OPERATION_REJECTED" : decision.code;
937
+ const diagnosticDetails = code === "INSUFFICIENT_CLAIM_MODE"
938
+ ? {
939
+ resource: decision.details.resource,
940
+ granted_modes: decision.details.grantedModes,
941
+ }
942
+ : {};
540
943
  return failure(new DomainError(code, `Operation denied: ${code}.`, {
541
944
  allowed: false,
542
945
  schema_version: decision.schema_version,
@@ -550,6 +953,7 @@ function deniedAuthorization(decision) {
550
953
  requested_session_id: decision.requested_session_id,
551
954
  state: decision.state,
552
955
  resources: decision.resources,
956
+ ...diagnosticDetails,
553
957
  details: decision.details,
554
958
  }));
555
959
  }
@@ -564,10 +968,11 @@ export async function runCli(argv, dependencies = {}) {
564
968
  if (!parsed.ok)
565
969
  return emitFailure(mode, "cli", parsed.error, io);
566
970
  if (parsed.value.help) {
971
+ const spec = helpSpecFor(parsed.value.commandArguments);
567
972
  if (mode === "json")
568
- io.stdout(renderSuccess(mode, "help", HELP_DATA));
973
+ io.stdout(renderSuccess(mode, "help", helpPayload(spec)));
569
974
  else
570
- io.stdout(HELP_TEXT);
975
+ io.stdout(helpText(spec));
571
976
  return EXIT_CODES.success;
572
977
  }
573
978
  if (parsed.value.version) {