nawabari 0.1.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.
Files changed (41) hide show
  1. package/README.md +347 -5
  2. package/dist/cli.js +770 -51
  3. package/dist/cli.js.map +1 -1
  4. package/dist/contract.d.ts +9 -0
  5. package/dist/contract.js +291 -0
  6. package/dist/contract.js.map +1 -0
  7. package/dist/domain/doctor.d.ts +1 -1
  8. package/dist/domain/doctor.js +99 -90
  9. package/dist/domain/doctor.js.map +1 -1
  10. package/dist/domain/errors.d.ts +1 -1
  11. package/dist/domain/errors.js +5 -1
  12. package/dist/domain/errors.js.map +1 -1
  13. package/dist/domain/session-backend.d.ts +15 -3
  14. package/dist/domain/session-backend.js +348 -7
  15. package/dist/domain/session-backend.js.map +1 -1
  16. package/dist/domain/session.d.ts +247 -2
  17. package/dist/domain/session.js +42 -0
  18. package/dist/domain/session.js.map +1 -1
  19. package/dist/errors.d.ts +2 -2
  20. package/dist/errors.js.map +1 -1
  21. package/dist/git.d.ts +102 -0
  22. package/dist/git.js +714 -30
  23. package/dist/git.js.map +1 -1
  24. package/dist/operation-authorization.d.ts +76 -0
  25. package/dist/operation-authorization.js +41 -0
  26. package/dist/operation-authorization.js.map +1 -0
  27. package/dist/output-budget.d.ts +16 -0
  28. package/dist/output-budget.js +58 -0
  29. package/dist/output-budget.js.map +1 -0
  30. package/dist/presentation.js +76 -22
  31. package/dist/presentation.js.map +1 -1
  32. package/dist/repository-evidence.d.ts +74 -0
  33. package/dist/repository-evidence.js +7 -0
  34. package/dist/repository-evidence.js.map +1 -0
  35. package/dist/resource-claims.d.ts +87 -0
  36. package/dist/resource-claims.js +450 -0
  37. package/dist/resource-claims.js.map +1 -0
  38. package/dist/session-registry.d.ts +251 -1
  39. package/dist/session-registry.js +2094 -257
  40. package/dist/session-registry.js.map +1 -1
  41. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -1,53 +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";
8
+ import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
6
9
  const CLI_NAME = "nawabari";
7
10
  const packageMetadata = createRequire(import.meta.url)("../package.json");
8
11
  const VERSION = packageMetadata.version;
9
- const HELP_TEXT = [
10
- `Usage: ${CLI_NAME} <command> [options]`,
11
- "",
12
- "Commands:",
13
- " session create Request a new Nawabari session",
14
- " session id Resolve the current session identity",
15
- " session show Show the current or selected session",
16
- " session list List repository sessions",
17
- " session close Close the current or selected session",
18
- " status Show Nawabari session status",
19
- " guard [--session id] Authorize the current worktree for mutation",
20
- " gc Detect or clean eligible stale sessions",
21
- " doctor Check local Nawabari prerequisites",
22
- " --help Show this help",
23
- " --version Print the installed version",
24
- "",
25
- "Global options:",
26
- " --json Emit one stable JSON document on stdout",
27
- " -h, --help Show this help",
28
- "",
29
- "Session options:",
30
- " session create --branch <name> --worktree <path> --base <ref> --label <text>",
31
- " session show|close --session <id>",
32
- " gc [--dry-run|--apply]",
33
- ].join("\n");
34
- const HELP_DATA = {
35
- usage: `Usage: ${CLI_NAME} <command> [options]`,
36
- commands: [
37
- "session create",
38
- "session id",
39
- "session show",
40
- "session list",
41
- "session close",
42
- "status",
43
- "guard",
44
- "gc",
45
- "doctor",
46
- ],
47
- options: ["--json", "--help", "--version"],
48
- session_options: ["--branch", "--worktree", "--base", "--label", "--session"],
49
- 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,
50
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);
51
343
  function usageError(code, message, details = null) {
52
344
  return new DomainError(code, message, details);
53
345
  }
@@ -91,7 +383,29 @@ function parseOptions(arguments_, allowed) {
91
383
  worktree: null,
92
384
  base: null,
93
385
  label: null,
386
+ resource: null,
387
+ resources: [],
388
+ operation: null,
389
+ message: null,
390
+ message_pattern: null,
391
+ remote: null,
392
+ remote_branch: null,
393
+ mode: null,
394
+ claim_id: null,
395
+ repository: null,
94
396
  apply: false,
397
+ force: false,
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,
95
409
  };
96
410
  let dryRun = false;
97
411
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -99,14 +413,30 @@ function parseOptions(arguments_, allowed) {
99
413
  if (!allowed.has(name)) {
100
414
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
101
415
  }
102
- if (name === "--apply" || name === "--dry-run") {
416
+ if (name === "--apply" ||
417
+ name === "--dry-run" ||
418
+ name === "--force" ||
419
+ name === "--create-upstream" ||
420
+ name === "--all" ||
421
+ name === "--history" ||
422
+ name === "--patch") {
103
423
  if (inlineValue !== null) {
104
424
  return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
105
425
  }
106
426
  if (name === "--apply")
107
427
  options.apply = true;
108
- else
428
+ else if (name === "--dry-run")
109
429
  dryRun = true;
430
+ else if (name === "--force")
431
+ options.force = true;
432
+ else if (name === "--create-upstream")
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;
110
440
  continue;
111
441
  }
112
442
  const value = inlineValue ?? arguments_[index + 1];
@@ -125,6 +455,40 @@ function parseOptions(arguments_, allowed) {
125
455
  options.base = value;
126
456
  else if (name === "--label")
127
457
  options.label = value;
458
+ else if (name === "--resource") {
459
+ options.resource = value;
460
+ options.resources.push(value);
461
+ }
462
+ else if (name === "--operation")
463
+ options.operation = value;
464
+ else if (name === "--message")
465
+ options.message = value;
466
+ else if (name === "--message-pattern")
467
+ options.message_pattern = value;
468
+ else if (name === "--remote")
469
+ options.remote = value;
470
+ else if (name === "--remote-branch")
471
+ options.remote_branch = value;
472
+ else if (name === "--mode")
473
+ options.mode = value;
474
+ else if (name === "--claim-id")
475
+ options.claim_id = value;
476
+ else if (name === "--repository")
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;
128
492
  }
129
493
  if (options.apply && dryRun) {
130
494
  return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
@@ -134,6 +498,52 @@ function parseOptions(arguments_, allowed) {
134
498
  function noOptions(arguments_) {
135
499
  return parseOptions(arguments_, new Set());
136
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
+ }
137
547
  function sessionContext(cwd) {
138
548
  return { cwd };
139
549
  }
@@ -147,6 +557,48 @@ async function executeCommand(commandArguments, dependencies) {
147
557
  if (subcommand === undefined) {
148
558
  return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
149
559
  }
560
+ if (subcommand === "claim" || subcommand === "update") {
561
+ const parsed = parseOptions(rest, new Set(["--session", "--resource", "--mode", "--repository"]));
562
+ if (!parsed.ok)
563
+ return parsed;
564
+ if (parsed.value.resource === null) {
565
+ return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
566
+ }
567
+ if (parsed.value.mode === null) {
568
+ return failure(usageError("MISSING_ARGUMENT", "--mode requires a value.", { option: "--mode" }));
569
+ }
570
+ const options = {
571
+ session_id: parsed.value.session_id,
572
+ repository: parsed.value.repository,
573
+ claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
574
+ };
575
+ const operation = subcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
576
+ if (operation === undefined)
577
+ return claimCapabilityUnavailable(subcommand);
578
+ const result = await operation.call(dependencies.backend, context, options);
579
+ return result.ok ? { ok: true, value: result.value } : result;
580
+ }
581
+ if (subcommand === "claims") {
582
+ const parsed = parseOptions(rest, new Set(["--session"]));
583
+ if (!parsed.ok)
584
+ return parsed;
585
+ if (dependencies.backend.listClaims === undefined)
586
+ return claimCapabilityUnavailable("claims");
587
+ const result = await dependencies.backend.listClaims(context, parsed.value.session_id);
588
+ return result.ok ? { ok: true, value: result.value } : result;
589
+ }
590
+ if (subcommand === "release") {
591
+ const parsed = parseOptions(rest, new Set(["--session", "--claim-id"]));
592
+ if (!parsed.ok)
593
+ return parsed;
594
+ if (dependencies.backend.releaseClaims === undefined)
595
+ return claimCapabilityUnavailable("release");
596
+ const result = await dependencies.backend.releaseClaims(context, {
597
+ session_id: parsed.value.session_id,
598
+ claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
599
+ });
600
+ return result.ok ? { ok: true, value: result.value } : result;
601
+ }
150
602
  if (subcommand === "create") {
151
603
  const parsed = parseOptions(rest, new Set(["--branch", "--worktree", "--base", "--label"]));
152
604
  if (!parsed.ok)
@@ -182,25 +634,232 @@ async function executeCommand(commandArguments, dependencies) {
182
634
  return selected.ok ? { ok: true, value: selected.value } : selected;
183
635
  }
184
636
  if (subcommand === "list") {
185
- const parsed = noOptions(rest);
637
+ const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
186
638
  if (!parsed.ok)
187
639
  return parsed;
188
- 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);
189
644
  return result.ok ? { ok: true, value: result.value } : result;
190
645
  }
191
646
  return failure(new DomainError("UNKNOWN_COMMAND", `Unknown session subcommand: ${subcommand}.`, { subcommand }));
192
647
  }
648
+ if (command === "resource") {
649
+ const resourceSubcommand = subcommand ?? "list";
650
+ if (resourceSubcommand === "claim" || resourceSubcommand === "update") {
651
+ const parsed = parseOptions(rest, new Set(["--session", "--resource", "--mode", "--repository"]));
652
+ if (!parsed.ok)
653
+ return parsed;
654
+ if (parsed.value.resource === null || parsed.value.mode === null) {
655
+ return failure(usageError("MISSING_ARGUMENT", "resource claim requires --resource and --mode."));
656
+ }
657
+ const operation = resourceSubcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
658
+ if (operation === undefined)
659
+ return claimCapabilityUnavailable(resourceSubcommand);
660
+ const result = await operation.call(dependencies.backend, context, {
661
+ session_id: parsed.value.session_id,
662
+ repository: parsed.value.repository,
663
+ claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
664
+ });
665
+ return result.ok ? { ok: true, value: result.value } : result;
666
+ }
667
+ if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
668
+ const parsed = parseOptions(rest, new Set(["--session"]));
669
+ if (!parsed.ok)
670
+ return parsed;
671
+ if (dependencies.backend.listClaims === undefined)
672
+ return claimCapabilityUnavailable("list");
673
+ const result = await dependencies.backend.listClaims(context, parsed.value.session_id);
674
+ return result.ok ? { ok: true, value: result.value } : result;
675
+ }
676
+ if (resourceSubcommand === "release") {
677
+ const parsed = parseOptions(rest, new Set(["--session", "--claim-id"]));
678
+ if (!parsed.ok)
679
+ return parsed;
680
+ if (dependencies.backend.releaseClaims === undefined)
681
+ return claimCapabilityUnavailable("release");
682
+ const result = await dependencies.backend.releaseClaims(context, {
683
+ session_id: parsed.value.session_id,
684
+ claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
685
+ });
686
+ return result.ok ? { ok: true, value: result.value } : result;
687
+ }
688
+ return failure(new DomainError("UNKNOWN_COMMAND", `Unknown resource subcommand: ${resourceSubcommand}.`, {
689
+ subcommand: resourceSubcommand,
690
+ }));
691
+ }
692
+ if (command === "authorize") {
693
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--operation", "--resource"]));
694
+ if (!parsed.ok)
695
+ return parsed;
696
+ if (parsed.value.operation === null) {
697
+ return failure(usageError("MISSING_ARGUMENT", "--operation requires a value.", { option: "--operation" }));
698
+ }
699
+ if (parsed.value.resources.length === 0) {
700
+ return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
701
+ }
702
+ if (dependencies.backend.authorizeOperation === undefined)
703
+ return authorizationCapabilityUnavailable();
704
+ const options = {
705
+ session_id: parsed.value.session_id,
706
+ operation: parsed.value.operation,
707
+ resources: parsed.value.resources,
708
+ };
709
+ const result = await dependencies.backend.authorizeOperation(context, options);
710
+ if (!result.ok)
711
+ return result;
712
+ return result.value.allowed
713
+ ? { ok: true, value: result.value }
714
+ : deniedAuthorization(result.value);
715
+ }
716
+ if (command === "checkpoint") {
717
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session"]));
718
+ if (!parsed.ok)
719
+ return parsed;
720
+ if (dependencies.backend.checkpoint === undefined)
721
+ return checkpointCapabilityUnavailable();
722
+ const options = { session_id: parsed.value.session_id };
723
+ const result = await dependencies.backend.checkpoint(context, options);
724
+ return result.ok ? { ok: true, value: result.value } : result;
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
+ }
777
+ if (command === "commit") {
778
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
779
+ if (!parsed.ok)
780
+ return parsed;
781
+ if (parsed.value.message === null) {
782
+ return failure(usageError("MISSING_ARGUMENT", "--message requires a value.", { option: "--message" }));
783
+ }
784
+ if (parsed.value.resources.length === 0) {
785
+ return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
786
+ }
787
+ if (dependencies.backend.commit === undefined)
788
+ return mutationCapabilityUnavailable("commit");
789
+ const result = await dependencies.backend.commit(context, {
790
+ session_id: parsed.value.session_id,
791
+ message: parsed.value.message,
792
+ resources: parsed.value.resources,
793
+ message_pattern: parsed.value.message_pattern,
794
+ });
795
+ return result.ok ? { ok: true, value: result.value } : result;
796
+ }
797
+ if (command === "push") {
798
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--resource", "--remote", "--branch", "--remote-branch", "--force", "--create-upstream"]));
799
+ if (!parsed.ok)
800
+ return parsed;
801
+ if (parsed.value.remote === null) {
802
+ return failure(usageError("MISSING_ARGUMENT", "--remote requires a value.", { option: "--remote" }));
803
+ }
804
+ if (parsed.value.branch === null && parsed.value.remote_branch === null) {
805
+ return failure(usageError("MISSING_ARGUMENT", "--branch requires a value.", { option: "--branch" }));
806
+ }
807
+ if (parsed.value.branch !== null &&
808
+ parsed.value.remote_branch !== null &&
809
+ parsed.value.branch !== parsed.value.remote_branch) {
810
+ return failure(usageError("INVALID_ARGUMENT", "--branch and --remote-branch must identify the same target."));
811
+ }
812
+ if (parsed.value.resources.length === 0) {
813
+ return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
814
+ }
815
+ if (dependencies.backend.push === undefined)
816
+ return mutationCapabilityUnavailable("push");
817
+ const result = await dependencies.backend.push(context, {
818
+ session_id: parsed.value.session_id,
819
+ resources: parsed.value.resources,
820
+ remote: parsed.value.remote,
821
+ branch: parsed.value.branch ?? parsed.value.remote_branch,
822
+ force: parsed.value.force,
823
+ create_upstream: parsed.value.create_upstream,
824
+ });
825
+ return result.ok ? { ok: true, value: result.value } : result;
826
+ }
193
827
  if (command === "status") {
194
- 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"]));
195
829
  if (!parsed.ok)
196
830
  return parsed;
197
- 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);
198
835
  return result.ok ? { ok: true, value: result.value } : result;
199
836
  }
200
837
  if (command === "guard") {
201
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session"]));
838
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--operation", "--resource"]));
202
839
  if (!parsed.ok)
203
840
  return parsed;
841
+ if (parsed.value.resources.length > 0 && parsed.value.operation === null) {
842
+ return failure(usageError("MISSING_ARGUMENT", "--operation is required when --resource is provided.", {
843
+ option: "--operation",
844
+ }));
845
+ }
846
+ if (parsed.value.operation !== null) {
847
+ if (parsed.value.resources.length === 0) {
848
+ return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
849
+ }
850
+ if (dependencies.backend.authorizeOperation === undefined)
851
+ return authorizationCapabilityUnavailable();
852
+ const result = await dependencies.backend.authorizeOperation(context, {
853
+ session_id: parsed.value.session_id,
854
+ operation: parsed.value.operation,
855
+ resources: parsed.value.resources,
856
+ });
857
+ if (!result.ok)
858
+ return result;
859
+ return result.value.allowed
860
+ ? { ok: true, value: result.value }
861
+ : deniedAuthorization(result.value);
862
+ }
204
863
  const result = await dependencies.backend.guard(context, { session_id: parsed.value.session_id });
205
864
  if (!result.ok)
206
865
  return result;
@@ -248,8 +907,56 @@ async function executeCommand(commandArguments, dependencies) {
248
907
  function commandName(commandArguments) {
249
908
  if (commandArguments[0] === "session")
250
909
  return commandArguments.slice(0, 2).join(" ");
910
+ if (commandArguments[0] === "resource")
911
+ return commandArguments.slice(0, 2).join(" ");
912
+ if (commandArguments[0] === "evidence")
913
+ return commandArguments.slice(0, 2).join(" ");
251
914
  return commandArguments[0] ?? "cli";
252
915
  }
916
+ function claimCapabilityUnavailable(operation) {
917
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Resource claim capability is not available.", { operation }));
918
+ }
919
+ function authorizationCapabilityUnavailable() {
920
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Operation authorization capability is not available.", {
921
+ operation: "authorize",
922
+ }));
923
+ }
924
+ function checkpointCapabilityUnavailable() {
925
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Checkpoint evidence capability is not available.", {
926
+ operation: "checkpoint",
927
+ }));
928
+ }
929
+ function repositoryEvidenceCapabilityUnavailable(operation) {
930
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
931
+ }
932
+ function mutationCapabilityUnavailable(operation) {
933
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Governed Git mutation capability is not available.", { operation }));
934
+ }
935
+ function deniedAuthorization(decision) {
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
+ : {};
943
+ return failure(new DomainError(code, `Operation denied: ${code}.`, {
944
+ allowed: false,
945
+ schema_version: decision.schema_version,
946
+ operation: decision.operation,
947
+ required_access: decision.required_access,
948
+ repository: decision.repository,
949
+ worktree: decision.worktree,
950
+ branch: decision.branch,
951
+ session_id: decision.session_id,
952
+ owner_session_id: decision.owner_session_id,
953
+ requested_session_id: decision.requested_session_id,
954
+ state: decision.state,
955
+ resources: decision.resources,
956
+ ...diagnosticDetails,
957
+ details: decision.details,
958
+ }));
959
+ }
253
960
  function emitFailure(mode, command, error, io) {
254
961
  io[mode === "json" ? "stdout" : "stderr"](renderFailure(mode, command, error));
255
962
  return error.exitCode;
@@ -261,14 +968,19 @@ export async function runCli(argv, dependencies = {}) {
261
968
  if (!parsed.ok)
262
969
  return emitFailure(mode, "cli", parsed.error, io);
263
970
  if (parsed.value.help) {
971
+ const spec = helpSpecFor(parsed.value.commandArguments);
264
972
  if (mode === "json")
265
- io.stdout(renderSuccess(mode, "help", HELP_DATA));
973
+ io.stdout(renderSuccess(mode, "help", helpPayload(spec)));
266
974
  else
267
- io.stdout(HELP_TEXT);
975
+ io.stdout(helpText(spec));
268
976
  return EXIT_CODES.success;
269
977
  }
270
978
  if (parsed.value.version) {
271
- const payload = { version: dependencies.version ?? VERSION };
979
+ const payload = {
980
+ version: dependencies.version ?? VERSION,
981
+ contract_id: MACHINE_CONTRACT_ID,
982
+ contract_schema_version: MACHINE_CONTRACT_SCHEMA_VERSION,
983
+ };
272
984
  if (mode === "json")
273
985
  io.stdout(renderSuccess(mode, "version", payload));
274
986
  else
@@ -281,6 +993,13 @@ export async function runCli(argv, dependencies = {}) {
281
993
  io.stdout(HELP_TEXT);
282
994
  return EXIT_CODES.usage;
283
995
  }
996
+ if (parsed.value.commandArguments[0] === "capabilities") {
997
+ const capabilityOptions = noOptions(parsed.value.commandArguments.slice(1));
998
+ if (!capabilityOptions.ok)
999
+ return emitFailure(mode, "capabilities", capabilityOptions.error, io);
1000
+ io.stdout(renderSuccess(mode, "capabilities", machineContract(dependencies.version ?? VERSION)));
1001
+ return EXIT_CODES.success;
1002
+ }
284
1003
  const command = commandName(parsed.value.commandArguments);
285
1004
  const cwd = dependencies.cwd ?? process.cwd();
286
1005
  try {