nawabari 0.7.1 → 0.9.2

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 (90) hide show
  1. package/README.md +264 -29
  2. package/dist/cli-command-registry.d.ts +50 -0
  3. package/dist/cli-command-registry.js +509 -0
  4. package/dist/cli-command-registry.js.map +1 -0
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +302 -583
  7. package/dist/cli.js.map +1 -1
  8. package/dist/contract.d.ts +6 -0
  9. package/dist/contract.js +296 -192
  10. package/dist/contract.js.map +1 -1
  11. package/dist/domain/cgroups-v2.d.ts +77 -0
  12. package/dist/domain/cgroups-v2.js +434 -0
  13. package/dist/domain/cgroups-v2.js.map +1 -0
  14. package/dist/domain/doctor.d.ts +4 -1
  15. package/dist/domain/doctor.js +64 -13
  16. package/dist/domain/doctor.js.map +1 -1
  17. package/dist/domain/errors.d.ts +1 -1
  18. package/dist/domain/errors.js.map +1 -1
  19. package/dist/domain/landlock.d.ts +55 -0
  20. package/dist/domain/landlock.js +279 -0
  21. package/dist/domain/landlock.js.map +1 -0
  22. package/dist/domain/runtime.d.ts +12 -0
  23. package/dist/domain/runtime.js +50 -0
  24. package/dist/domain/runtime.js.map +1 -0
  25. package/dist/domain/sandbox-launcher.d.ts +26 -0
  26. package/dist/domain/sandbox-launcher.js +312 -23
  27. package/dist/domain/sandbox-launcher.js.map +1 -1
  28. package/dist/domain/sandbox-seccomp.d.ts +46 -0
  29. package/dist/domain/sandbox-seccomp.js +302 -0
  30. package/dist/domain/sandbox-seccomp.js.map +1 -0
  31. package/dist/domain/sandbox.d.ts +45 -1
  32. package/dist/domain/sandbox.js +176 -25
  33. package/dist/domain/sandbox.js.map +1 -1
  34. package/dist/domain/session-backend.d.ts +4 -1
  35. package/dist/domain/session-backend.js +256 -3
  36. package/dist/domain/session-backend.js.map +1 -1
  37. package/dist/domain/session.d.ts +207 -3
  38. package/dist/domain/session.js.map +1 -1
  39. package/dist/errors.d.ts +1 -1
  40. package/dist/failure-code-vocabulary.d.ts +20 -0
  41. package/dist/failure-code-vocabulary.js +216 -0
  42. package/dist/failure-code-vocabulary.js.map +1 -0
  43. package/dist/git.js +92 -6
  44. package/dist/git.js.map +1 -1
  45. package/dist/operation-authorization.d.ts +1 -1
  46. package/dist/product-state-manifest.d.ts +152 -0
  47. package/dist/product-state-manifest.js +240 -0
  48. package/dist/product-state-manifest.js.map +1 -0
  49. package/dist/public-contract.d.ts +25 -0
  50. package/dist/public-contract.js +29 -0
  51. package/dist/public-contract.js.map +1 -0
  52. package/dist/public-state.d.ts +75 -0
  53. package/dist/public-state.js +60 -0
  54. package/dist/public-state.js.map +1 -0
  55. package/dist/registry/lock.d.ts +8 -0
  56. package/dist/registry/lock.js +38 -20
  57. package/dist/registry/lock.js.map +1 -1
  58. package/dist/repository-evidence.d.ts +16 -0
  59. package/dist/repository-evidence.js.map +1 -1
  60. package/dist/resource-claims.d.ts +2 -2
  61. package/dist/resource-claims.js +96 -27
  62. package/dist/resource-claims.js.map +1 -1
  63. package/dist/session-lifecycle-actions.d.ts +64 -0
  64. package/dist/session-lifecycle-actions.js +117 -0
  65. package/dist/session-lifecycle-actions.js.map +1 -0
  66. package/dist/session-lifecycle-classification.d.ts +120 -0
  67. package/dist/session-lifecycle-classification.js +88 -0
  68. package/dist/session-lifecycle-classification.js.map +1 -0
  69. package/dist/session-registry.d.ts +210 -2
  70. package/dist/session-registry.js +1330 -87
  71. package/dist/session-registry.js.map +1 -1
  72. package/dist/state/index.d.ts +5 -0
  73. package/dist/state/index.js +4 -0
  74. package/dist/state/index.js.map +1 -0
  75. package/dist/state/session/actors.d.ts +71 -0
  76. package/dist/state/session/actors.js +259 -0
  77. package/dist/state/session/actors.js.map +1 -0
  78. package/dist/state/session/guards.d.ts +25 -0
  79. package/dist/state/session/guards.js +108 -0
  80. package/dist/state/session/guards.js.map +1 -0
  81. package/dist/state/session/index.d.ts +6 -0
  82. package/dist/state/session/index.js +5 -0
  83. package/dist/state/session/index.js.map +1 -0
  84. package/dist/state/session/machine.d.ts +185 -0
  85. package/dist/state/session/machine.js +408 -0
  86. package/dist/state/session/machine.js.map +1 -0
  87. package/dist/state/session/types.d.ts +94 -0
  88. package/dist/state/session/types.js +14 -0
  89. package/dist/state/session/types.js.map +1 -0
  90. package/package.json +46 -26
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { DomainError, EXIT_CODES, failure } from "./domain/errors.js";
4
4
  import { MAX_SESSION_LIST_LIMIT, } from "./domain/session.js";
5
5
  import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PATHS } from "./repository-evidence.js";
6
6
  import { isResourceClaimMode } from "./resource-claims.js";
7
+ import { OPERATION_VOCABULARY } from "./operation-authorization.js";
7
8
  import { createLocalSessionBackend } from "./domain/session-backend.js";
8
9
  import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
9
10
  import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
@@ -11,521 +12,173 @@ import { resolveSandboxExecutionRequest, runSandboxedCommand, } from "./domain/s
11
12
  const CLI_NAME = "nawabari";
12
13
  const packageMetadata = createRequire(import.meta.url)("../package.json");
13
14
  const VERSION = packageMetadata.version;
14
- const GLOBAL_HELP_OPTIONS = [
15
- { name: "--json", description: "Emit one stable JSON document on stdout" },
16
- { name: "--help", description: "Show command-specific help" },
17
- { name: "--version", description: "Print the installed version" },
18
- ];
19
- const option = (name, description, options = {}) => ({ name, description, ...options });
20
- const HELP_COMMANDS = [
21
- {
22
- name: "session create",
23
- summary: "Request a new Nawabari session",
24
- usage: `${CLI_NAME} session create [options]`,
25
- options: [
26
- option("--branch", "Branch to create; omitted uses the generated session branch", {
27
- value: "<name>",
28
- default: "nawabari/session/<session_id>",
29
- }),
30
- option("--worktree", "Exact managed worktree path override; mutually exclusive with --worktree-root", {
31
- value: "<path>",
32
- default: "<managed_worktree_root>/<repository>-<session_id>",
33
- }),
34
- option("--worktree-root", "Managed root to place the worktree under; Nawabari derives the final path. Mutually exclusive with --worktree", { value: "<path>", default: "resolved repository-local root" }),
35
- option("--base", "Commit-resolving base ref for the new worktree", { value: "<ref>", default: "HEAD" }),
36
- option("--label", "Optional display label; never used as an identity", { value: "<text>", default: "omitted" }),
37
- ],
38
- notes: [
39
- "All create options are optional. Use status --json to discover managed_worktree_root.",
40
- "--worktree and --worktree-root cannot be combined.",
41
- ],
42
- },
43
- {
44
- name: "session id",
45
- summary: "Resolve the current session identity",
46
- usage: `${CLI_NAME} session id`,
47
- options: [],
48
- },
49
- {
50
- name: "session show",
51
- summary: "Show the current or selected session",
52
- usage: `${CLI_NAME} session show [<session-id>|--session <id>]`,
53
- options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
54
- notes: [
55
- "Target grammar is consistent: an optional first positional <session-id> is an alias for --session <id>; do not supply both.",
56
- ],
57
- },
58
- {
59
- name: "session inspect",
60
- summary: "Report side-effect-free close/cleanup readiness for a session",
61
- usage: `${CLI_NAME} session inspect [<session-id>|--session <id>] [--integrated-revision <rev>]`,
62
- options: [
63
- option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
64
- option("--integrated-revision", "Externally evidenced revision to test for non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
65
- ],
66
- notes: [
67
- "Read-only: never mutates session, claim, Git, worktree, branch, or registry state. Repeated calls are idempotent.",
68
- "Derived from the same authoritative close/cleanup Git evidence as session close; does not duplicate or diverge from that logic.",
69
- "Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
70
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
71
- ],
72
- },
73
- {
74
- name: "session run",
75
- summary: "Run one command inside the protected session sandbox",
76
- usage: `${CLI_NAME} session run [--session <id>] -- <command> [args...]`,
77
- options: [option("--session", "Select the active owned session", { value: "<id>" })],
78
- notes: [
79
- "The -- terminator is mandatory. The command is passed as argv without a shell, and protected execution is fail-closed.",
80
- "session exec is an alias.",
81
- ],
82
- },
83
- {
84
- name: "session list",
85
- summary: "List bounded repository session records",
86
- usage: `${CLI_NAME} session list [--all|--history]`,
87
- options: [
88
- option("--all", "Include closed history; explicit unbounded history view"),
89
- option("--history", "Alias for --all"),
90
- ],
91
- notes: ["Default output excludes closed records and is limited to 64 records."],
92
- },
93
- {
94
- name: "session claim",
95
- summary: "Add a canonical resource claim",
96
- usage: `${CLI_NAME} session claim [<session-id>] --resource <path-or-glob> --mode <read|write|exclusive-write>`,
97
- options: [
98
- option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
99
- option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
100
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
101
- option("--repository", "Expected repository identity", { value: "<id>" }),
102
- ],
103
- notes: [
104
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
105
- ],
106
- },
107
- {
108
- name: "session update",
109
- summary: "Atomically replace a session's complete resource claim set",
110
- usage: `${CLI_NAME} session update [<session-id>] --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...]`,
111
- options: [
112
- option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
113
- value: "<path-or-glob>",
114
- required: true,
115
- }),
116
- option("--mode", "Mode for the --resource immediately before it; repeatable", {
117
- value: "<read|write|exclusive-write>",
118
- required: true,
119
- }),
120
- option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
121
- value: "<non-negative-integer>",
122
- }),
123
- option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
124
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
125
- option("--repository", "Expected repository identity", { value: "<id>" }),
126
- ],
127
- notes: [
128
- "The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
129
- "use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
130
- "or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
131
- "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
132
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
133
- ],
134
- },
135
- {
136
- name: "session mutate",
137
- summary: "Atomically apply exact-resource claim additions, changes, and releases",
138
- usage: `${CLI_NAME} session mutate [<session>|--session <id>] [--repository <id>] ` +
139
- `(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
140
- `(--if-generation <non-negative-safe-int> | --force)`,
141
- options: [
142
- option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
143
- option("--mode", "Mode for the immediately preceding --upsert-resource", {
144
- value: "<read|write|exclusive-write>",
145
- }),
146
- option("--release-resource", "Exact repository-relative resource to release; repeatable", {
147
- value: "<path-or-glob>",
148
- }),
149
- option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
150
- value: "<non-negative-safe-int>",
151
- }),
152
- option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
153
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
154
- option("--repository", "Expected repository identity", { value: "<id>" }),
155
- ],
156
- notes: [
157
- "Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
158
- "Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
159
- "Target grammar: optional first positional <session> is an alias for --session <id>; do not supply both.",
160
- ],
161
- },
162
- {
163
- name: "session transition",
164
- summary: "Atomically transition one exact resource claim mode",
165
- usage: `${CLI_NAME} session transition [<session>|--session <id>] [--repository <id>] ` +
166
- `--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
167
- `(--if-generation <non-negative-safe-int> | --force)`,
168
- options: [
169
- option("--resource", "Exact repository-relative resource to acquire or change", {
170
- value: "<path-or-glob>",
171
- required: true,
172
- }),
173
- option("--mode", "Target mode for the immediately preceding --resource", {
174
- value: "<read|write|exclusive-write>",
175
- required: true,
176
- }),
177
- option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
178
- value: "<non-negative-safe-int>",
179
- }),
180
- option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
181
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
182
- option("--repository", "Expected repository identity", { value: "<id>" }),
183
- ],
184
- notes: [
185
- "Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
186
- "Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
187
- "The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
188
- ],
189
- },
190
- {
191
- name: "session claims",
192
- summary: "List canonical resource claims",
193
- usage: `${CLI_NAME} session claims [<session-id>|--session <id>]`,
194
- options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
195
- notes: [
196
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
197
- ],
198
- },
199
- {
200
- name: "session release",
201
- summary: "Release resource claims",
202
- usage: `${CLI_NAME} session release [<session-id>|--session <id>] (--resource <path-or-glob> ... | --claim-id <id> ... | --all) (--if-generation <n> | --force)`,
203
- options: [
204
- option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
205
- option("--resource", "Release one exact canonical resource; repeatable", { value: "<path-or-glob>" }),
206
- option("--claim-id", "Release one owned claim ID; repeatable", { value: "<id>" }),
207
- option("--all", "Explicitly release all claims owned by the target session"),
208
- option("--if-generation", "Require the expected claim-set generation; mutually exclusive with --force", {
209
- value: "<non-negative-safe-int>",
210
- }),
211
- option("--force", "Explicitly allow unconditional release; mutually exclusive with --if-generation"),
212
- ],
213
- notes: [
214
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
215
- "Exactly one selector family is required: repeated --resource, repeated --claim-id, or explicit --all.",
216
- "Exactly one destructive concurrency intent is required: --if-generation <non-negative-safe-int> or --force.",
217
- ],
218
- },
219
- {
220
- name: "resource claim",
221
- summary: "Add a canonical resource claim (alias)",
222
- usage: `${CLI_NAME} resource claim [<session-id>|--session <id>] --resource <path-or-glob> --mode <read|write|exclusive-write>`,
223
- options: [],
224
- notes: [
225
- "Target grammar matches session claim: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
226
- ],
227
- },
228
- {
229
- name: "resource update",
230
- summary: "Atomically replace a session's complete resource claim set (alias)",
231
- usage: `${CLI_NAME} resource update [<session-id>|--session <id>] --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...]`,
232
- options: [
233
- option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
234
- value: "<path-or-glob>",
235
- required: true,
236
- }),
237
- option("--mode", "Mode for the --resource immediately before it; repeatable", {
238
- value: "<read|write|exclusive-write>",
239
- required: true,
240
- }),
241
- option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
242
- value: "<non-negative-integer>",
243
- }),
244
- option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
245
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
246
- option("--repository", "Expected repository identity", { value: "<id>" }),
247
- ],
248
- notes: [
249
- "The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
250
- "use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
251
- "or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
252
- "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
253
- "Target grammar matches session update: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
254
- ],
255
- },
256
- {
257
- name: "resource mutate",
258
- summary: "Atomically apply exact-resource claim additions, changes, and releases (alias)",
259
- usage: `${CLI_NAME} resource mutate [<session>|--session <id>] [--repository <id>] ` +
260
- `(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
261
- `(--if-generation <non-negative-safe-int> | --force)`,
262
- options: [
263
- option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
264
- option("--mode", "Mode for the immediately preceding --upsert-resource", {
265
- value: "<read|write|exclusive-write>",
266
- }),
267
- option("--release-resource", "Exact repository-relative resource to release; repeatable", {
268
- value: "<path-or-glob>",
269
- }),
270
- option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
271
- value: "<non-negative-safe-int>",
272
- }),
273
- option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
274
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
275
- option("--repository", "Expected repository identity", { value: "<id>" }),
276
- ],
277
- notes: [
278
- "Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
279
- "Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
280
- "Target grammar matches session mutate: optional first positional <session> is an alias for --session <id>; do not supply both.",
281
- ],
282
- },
283
- {
284
- name: "resource transition",
285
- summary: "Atomically transition one exact resource claim mode (alias)",
286
- usage: `${CLI_NAME} resource transition [<session>|--session <id>] [--repository <id>] ` +
287
- `--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
288
- `(--if-generation <non-negative-safe-int> | --force)`,
289
- options: [
290
- option("--resource", "Exact repository-relative resource to acquire or change", {
291
- value: "<path-or-glob>",
292
- required: true,
293
- }),
294
- option("--mode", "Target mode for the immediately preceding --resource", {
295
- value: "<read|write|exclusive-write>",
296
- required: true,
297
- }),
298
- option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
299
- value: "<non-negative-safe-int>",
300
- }),
301
- option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
302
- option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
303
- option("--repository", "Expected repository identity", { value: "<id>" }),
304
- ],
305
- notes: [
306
- "Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
307
- "Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
308
- "The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
309
- ],
310
- },
311
- {
312
- name: "resource list",
313
- summary: "List canonical resource claims (alias)",
314
- usage: `${CLI_NAME} resource list [<session-id>|--session <id>]`,
315
- options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
316
- notes: [
317
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
318
- ],
319
- },
320
- {
321
- name: "resource release",
322
- summary: "Release resource claims (alias)",
323
- usage: `${CLI_NAME} resource release [<session-id>|--session <id>] (--resource <path-or-glob> ... | --claim-id <id> ... | --all) (--if-generation <n> | --force)`,
324
- options: [
325
- option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
326
- option("--resource", "Release one exact canonical resource; repeatable", { value: "<path-or-glob>" }),
327
- option("--claim-id", "Release one owned claim ID; repeatable", { value: "<id>" }),
328
- option("--all", "Explicitly release all claims owned by the target session"),
329
- option("--if-generation", "Require the expected claim-set generation; mutually exclusive with --force", {
330
- value: "<non-negative-safe-int>",
331
- }),
332
- option("--force", "Explicitly allow unconditional release; mutually exclusive with --if-generation"),
333
- ],
334
- notes: [
335
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
336
- "Exactly one selector family is required: repeated --resource, repeated --claim-id, or explicit --all.",
337
- "Exactly one destructive concurrency intent is required: --if-generation <non-negative-safe-int> or --force.",
338
- ],
339
- },
340
- {
341
- name: "session close",
342
- summary: "Close the current or selected session",
343
- usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>] [--fetch-remote <name> --fetch-branch <branch>]`,
344
- options: [
345
- option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
346
- option("--integrated-revision", "Externally evidenced revision proving non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
347
- option("--fetch-remote", "Explicitly fetch one remote integration branch into a disposable proof ref", {
348
- value: "<name>",
349
- }),
350
- option("--fetch-branch", "Integration branch to fetch; requires --fetch-remote", { value: "<branch>" }),
351
- ],
352
- notes: [
353
- "Ordinary ancestry-based close remains the cheap/default path and requires no flags.",
354
- "Network access is never implicit; --fetch-remote and --fetch-branch are accepted only with a full lowercase --integrated-revision SHA.",
355
- "Fetch updates only a disposable internal proof ref with no tags, FETCH_HEAD, tracking-ref, or local integration-branch changes; remote tip races fail closed.",
356
- "Nawabari never calls provider APIs such as GitHub; only explicit --fetch-remote/--fetch-branch options connect to the configured Git remote.",
357
- "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
358
- ],
359
- },
360
- {
361
- name: "session discard",
362
- summary: "Explicitly discard one selected session and its owned resources",
363
- usage: `${CLI_NAME} session discard <session-id>|--session <id>`,
364
- options: [
365
- option("--session", "Required explicit target; the current session is never inferred", {
366
- value: "<id>",
367
- required: true,
368
- }),
369
- ],
370
- notes: [
371
- "Destructive and explicit: unintegrated commits and uncommitted work in the selected owned worktree may be destroyed.",
372
- "Discard never changes close, gc, doctor, reconciliation, or integration-lineage proof behavior.",
373
- "Use exactly one target form: positional <session-id> or --session <id>; do not supply both.",
374
- "Machine JSON mode is non-interactive and deterministic.",
375
- ],
376
- },
377
- {
378
- name: "authorize",
379
- summary: "Authorize an operation against concrete claims",
380
- usage: `${CLI_NAME} authorize --operation <name> --resource <path> [--resource <path>] [--session <id>]`,
381
- options: [
382
- option("--session", "Assert the current session identity", { value: "<id>" }),
383
- option("--operation", "Operation vocabulary entry", { value: "<name>", required: true }),
384
- option("--resource", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
385
- ],
386
- },
387
- {
388
- name: "checkpoint",
389
- summary: "Capture bounded Git execution evidence",
390
- usage: `${CLI_NAME} checkpoint [--session <id>]`,
391
- options: [option("--session", "Assert the current session identity", { value: "<id>" })],
392
- },
393
- {
394
- name: "evidence snapshot",
395
- summary: "Capture bounded read-only evidence for one owned session",
396
- usage: `${CLI_NAME} evidence snapshot --session <id>`,
397
- options: [option("--session", "Explicit owned session to observe", { value: "<id>", required: true })],
398
- notes: ["The result is Git-observable physical evidence only; it contains no task or semantic interpretation."],
399
- },
400
- {
401
- name: "diff",
402
- summary: "Inspect bounded Git evidence for explicit paths",
403
- usage: `${CLI_NAME} diff --session <id> --path <path> [options]`,
404
- options: [
405
- option("--session", "Explicit owned session to observe", { value: "<id>", required: true }),
406
- option("--path", "Concrete repository-relative path; repeatable", { value: "<path>", required: true }),
407
- option("--from", "Commit/ref at the start of the range", { value: "<ref>", default: "HEAD" }),
408
- option("--to", "Commit/ref at the end of the range; omitted means worktree", { value: "<ref>" }),
409
- option("--patch", "Include patch text; requires the bounded byte/hunk limits"),
410
- option("--max-bytes", "Maximum UTF-8 patch bytes", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_BYTES) }),
411
- option("--max-hunks", "Maximum patch hunks", { value: "<n>", default: String(EVIDENCE_MAX_DIFF_HUNKS) }),
412
- ],
413
- },
414
- {
415
- name: "commit",
416
- summary: "Commit explicit claim-authorized resources",
417
- usage: `${CLI_NAME} commit --message <final-message> --resource <path> [--resource <path>] [--session <id>] [--message-pattern <regex>]`,
418
- options: [
419
- option("--session", "Assert the current session identity", { value: "<id>" }),
420
- option("--message", "Caller-decided final commit message", { value: "<final-message>", required: true }),
421
- option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
422
- option("--message-pattern", "Caller-declared commit-message rule; validated only when supplied", {
423
- value: "<regex>",
424
- }),
425
- ],
426
- },
427
- {
428
- name: "push",
429
- summary: "Push the owned branch to an explicit target",
430
- usage: `${CLI_NAME} push --remote <name> --branch <name> --resource <path> [options]`,
431
- options: [
432
- option("--session", "Assert the current session identity", { value: "<id>" }),
433
- option("--resource", "Claim-covered concrete path; repeatable", { value: "<path>", required: true }),
434
- option("--remote", "Explicit Git remote", { value: "<name>", required: true }),
435
- option("--branch", "Explicit target branch", { value: "<name>", required: true }),
436
- option("--remote-branch", "Explicit remote branch alias for --branch", { value: "<name>" }),
437
- option("--force", "Allow force-with-lease when relation requires it"),
438
- option("--create-upstream", "Allow creation of a missing upstream"),
439
- ],
440
- },
441
- {
442
- name: "status",
443
- summary: "Show repository context and bounded session status",
444
- usage: `${CLI_NAME} status [--all|--history]`,
445
- options: [
446
- option("--all", "Include closed history; explicit unbounded history view"),
447
- option("--history", "Alias for --all"),
448
- ],
449
- notes: ["The default machine result exposes managed_worktree_root for session-create path discovery."],
450
- },
451
- {
452
- name: "guard",
453
- summary: "Authorize the current worktree or operation",
454
- usage: `${CLI_NAME} guard [--session <id>] [--operation <name> --resource <path>]`,
455
- options: [
456
- option("--session", "Assert the current session identity", { value: "<id>" }),
457
- option("--operation", "Authorize an operation when resources are supplied", { value: "<name>" }),
458
- option("--resource", "Concrete resource; repeatable with --operation", { value: "<path>" }),
459
- ],
460
- },
461
- {
462
- name: "gc",
463
- summary: "Detect or clean eligible stale sessions",
464
- usage: `${CLI_NAME} gc [--dry-run|--apply]`,
465
- options: [
466
- option("--apply", "Apply only cleanup that passes safety checks"),
467
- option("--dry-run", "Preflight eligible stale cleanup without mutation"),
468
- ],
469
- notes: [
470
- "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.",
471
- ],
472
- },
473
- {
474
- name: "doctor",
475
- summary: "Check local Nawabari prerequisites and reconciliation",
476
- usage: `${CLI_NAME} doctor`,
477
- options: [],
478
- },
479
- {
480
- name: "capabilities",
481
- summary: "Describe the standalone CLI/JSON contract",
482
- usage: `${CLI_NAME} capabilities`,
483
- options: [],
484
- },
15
+ import { CLI_COMMAND_REGISTRY, GLOBAL_HELP_OPTIONS, ROOT_HELP_SPEC, publicCliCommandDefinitions, publicCliCommandNames, resolveCliCommandDefinition, canonicalCommandForName, } from "./cli-command-registry.js";
16
+ export { CLI_COMMAND_REGISTRY, COMMAND_REGISTRY, publicCliCommandDefinitions, publicCliCommandNames, resolveCliCommandDefinition, canonicalCommandForName, } from "./cli-command-registry.js";
17
+ /**
18
+ * The dispatcher deliberately keeps its command branches specialized, but
19
+ * this inventory makes the executable surface explicit for structural parity
20
+ * checks. Aliases are listed because they are public entry points too.
21
+ */
22
+ export const DISPATCHER_COMMAND_INVENTORY = [
23
+ "session create",
24
+ "session id",
25
+ "session show",
26
+ "session inspect",
27
+ "session run",
28
+ "session exec",
29
+ "session list",
30
+ "session claim",
31
+ "resource claim",
32
+ "session update",
33
+ "resource update",
34
+ "session mutate",
35
+ "resource mutate",
36
+ "session transition",
37
+ "resource transition",
38
+ "session claims",
39
+ "resource list",
40
+ "resource claims",
41
+ "session release",
42
+ "resource release",
43
+ "session close",
44
+ "session discard",
45
+ "authorize",
46
+ "checkpoint",
47
+ "evidence snapshot",
48
+ "diff",
49
+ "commit",
50
+ "push",
51
+ "status",
52
+ "guard",
53
+ "gc",
54
+ "doctor",
55
+ "migrate",
56
+ "capabilities",
485
57
  ];
486
- const ROOT_HELP_SPEC = {
487
- name: "root",
488
- summary: "Standalone local Git/session ownership CLI",
489
- usage: `${CLI_NAME} <command> [options]`,
490
- options: GLOBAL_HELP_OPTIONS,
58
+ /** Dispatcher option inventory, keyed by canonical registry command identity. */
59
+ export const DISPATCHER_OPTION_INVENTORY = {
60
+ "session create": ["--branch", "--worktree", "--worktree-root", "--base", "--label"],
61
+ "session id": [],
62
+ "session show": ["--session"],
63
+ "session inspect": ["--session", "--integrated-revision"],
64
+ "session run": ["--session"],
65
+ "session list": ["--all", "--history", "--limit", "--offset"],
66
+ "session claim": ["--resource", "--mode", "--session", "--repository"],
67
+ "session update": ["--resource", "--mode", "--if-generation", "--force", "--session", "--repository"],
68
+ "session mutate": [
69
+ "--upsert-resource",
70
+ "--mode",
71
+ "--release-resource",
72
+ "--if-generation",
73
+ "--force",
74
+ "--session",
75
+ "--repository",
76
+ ],
77
+ "session transition": ["--resource", "--mode", "--if-generation", "--force", "--session", "--repository"],
78
+ "session claims": ["--session"],
79
+ "session release": ["--session", "--resource", "--claim-id", "--all", "--if-generation", "--force"],
80
+ "session close": ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"],
81
+ "session discard": ["--session", "--preview"],
82
+ authorize: ["--session", "--operation", "--resource"],
83
+ checkpoint: ["--session"],
84
+ "evidence snapshot": ["--session"],
85
+ diff: ["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"],
86
+ commit: ["--session", "--message", "--resource", "--all-claimed", "--message-pattern"],
87
+ push: [
88
+ "--session",
89
+ "--resource",
90
+ "--all-claimed",
91
+ "--remote",
92
+ "--branch",
93
+ "--remote-branch",
94
+ "--force",
95
+ "--create-upstream",
96
+ ],
97
+ status: ["--all", "--history", "--limit", "--offset"],
98
+ guard: ["--session", "--operation", "--resource"],
99
+ gc: ["--apply", "--dry-run"],
100
+ doctor: [],
101
+ migrate: [],
102
+ capabilities: [],
491
103
  };
104
+ function optionNames(definition) {
105
+ return definition.options.flatMap((candidate) => [candidate.name, ...(candidate.aliases ?? [])]);
106
+ }
107
+ function canonicalName(name) {
108
+ return CLI_COMMAND_REGISTRY.find((definition) => definition.name === name || definition.aliases?.includes(name))
109
+ ?.name;
110
+ }
111
+ /** Return the registry-backed option set used by a specialized dispatcher parser. */
112
+ export function dispatcherAllowedOptions(command) {
113
+ const canonical = canonicalName(command);
114
+ if (canonical === undefined)
115
+ throw new Error(`Dispatcher command is not registered: ${command}`);
116
+ return new Set(DISPATCHER_OPTION_INVENTORY[canonical] ?? []);
117
+ }
118
+ /**
119
+ * Verify both executable command/option inventory and registry metadata. This
120
+ * is intentionally structural: parser semantics remain in their command
121
+ * handlers, while drift becomes a deterministic failure in tests/startup.
122
+ */
123
+ export function validateCliRegistryParity() {
124
+ const publicNames = publicCliCommandNames();
125
+ const publicSet = new Set(publicNames);
126
+ const dispatcherSet = new Set(DISPATCHER_COMMAND_INVENTORY);
127
+ const missingFromDispatcher = publicNames.filter((name) => !dispatcherSet.has(name));
128
+ const missingFromRegistry = DISPATCHER_COMMAND_INVENTORY.filter((name) => !publicSet.has(name));
129
+ if (missingFromDispatcher.length > 0 || missingFromRegistry.length > 0) {
130
+ throw new Error(`CLI command registry parity failure: missing_from_dispatcher=${missingFromDispatcher.join(",")}; ` +
131
+ `missing_from_registry=${missingFromRegistry.join(",")}`);
132
+ }
133
+ for (const name of publicNames) {
134
+ const canonical = canonicalName(name);
135
+ if (canonical === undefined)
136
+ throw new Error(`CLI command registry cannot resolve ${name}`);
137
+ const expected = new Set(DISPATCHER_OPTION_INVENTORY[canonical] ?? []);
138
+ const actual = new Set(optionNames(resolveCliCommandDefinition(name)));
139
+ const missing = [...actual].filter((option) => !expected.has(option));
140
+ const extra = [...expected].filter((option) => !actual.has(option));
141
+ if (missing.length > 0 || extra.length > 0) {
142
+ throw new Error(`CLI option registry parity failure for ${name}: missing_from_dispatcher=${missing.join(",")}; ` +
143
+ `missing_from_registry=${extra.join(",")}`);
144
+ }
145
+ }
146
+ for (const command of ["authorize", "guard"]) {
147
+ const definition = resolveCliCommandDefinition(command);
148
+ const operation = definition?.options.find((candidate) => candidate.name === "--operation");
149
+ if (operation === undefined || operation.values === undefined) {
150
+ throw new Error(`CLI operation vocabulary parity failure for ${command}: missing --operation values`);
151
+ }
152
+ if (operation.values.join("\u0000") !== OPERATION_VOCABULARY.join("\u0000")) {
153
+ throw new Error(`CLI operation vocabulary parity failure for ${command}: expected=${OPERATION_VOCABULARY.join(",")}; ` +
154
+ `actual=${operation.values.join(",")}`);
155
+ }
156
+ }
157
+ }
158
+ validateCliRegistryParity();
492
159
  function helpSpecFor(commandArguments) {
493
160
  if (commandArguments.length === 0)
494
161
  return ROOT_HELP_SPEC;
495
- let key = commandArguments[0] === "resource"
162
+ const key = commandArguments[0] === "resource"
496
163
  ? `resource ${commandArguments[1] ?? "list"}`
497
- : commandArguments[0] === "session" && commandArguments[1] === "exec"
498
- ? "session run"
499
- : commandArguments.slice(0, 2).join(" ");
500
- if (key === "resource claims")
501
- key = "resource list";
502
- const direct = HELP_COMMANDS.find((spec) => spec.name === key);
503
- if (direct !== undefined && !direct.name.startsWith("resource "))
504
- return direct;
505
- const aliasTarget = direct?.name.replace(/^resource /u, "session ");
506
- const target = aliasTarget === undefined ? undefined : HELP_COMMANDS.find((spec) => spec.name === aliasTarget);
507
- if (direct !== undefined && target !== undefined) {
508
- return {
509
- name: direct.name,
510
- summary: direct.summary,
511
- usage: direct.usage,
512
- options: direct.options,
513
- notes: direct.notes,
514
- };
515
- }
516
- return HELP_COMMANDS.find((spec) => spec.name === commandArguments[0]) ?? ROOT_HELP_SPEC;
164
+ : commandArguments.slice(0, 2).join(" ");
165
+ return resolveCliCommandDefinition(key) ?? resolveCliCommandDefinition(commandArguments[0]) ?? ROOT_HELP_SPEC;
517
166
  }
518
167
  function helpPayload(spec) {
519
168
  if (spec.name === "root") {
169
+ const publicCommands = publicCliCommandDefinitions();
520
170
  const optionNames = (options) => options.map((candidate) => candidate.name);
521
- const sessionOptions = HELP_COMMANDS.filter((command) => command.name.startsWith("session ")).flatMap((command) => command.options);
171
+ const sessionOptions = publicCommands
172
+ .filter((command) => command.name.startsWith("session "))
173
+ .flatMap((command) => command.options);
522
174
  const unique = (values) => values.filter((value, index) => values.indexOf(value) === index);
523
175
  const sessionListOnlyOptions = new Set(["--all", "--history"]);
524
- const optionsFor = (name) => optionNames(HELP_COMMANDS.find((command) => command.name === name)?.options ?? []);
176
+ const sessionTargetingCommands = CLI_COMMAND_REGISTRY.filter((command) => command.name.startsWith("session ") && command.usage.includes("<session")).map((command) => command.name.slice("session ".length));
177
+ const optionsFor = (name) => optionNames(publicCommands.find((command) => command.name === name)?.options ?? []);
525
178
  return {
526
179
  usage: `Usage: ${spec.usage}`,
527
- commands: HELP_COMMANDS.map((command) => command.name),
528
- options: ["--json", "--help", "--version"],
180
+ commands: publicCommands.map((command) => command.name),
181
+ options: GLOBAL_HELP_OPTIONS.map((candidate) => candidate.name),
529
182
  session_options: unique(sessionOptions.map((option) => option.name).filter((name) => !sessionListOnlyOptions.has(name))),
530
183
  authorization_options: optionsFor("authorize"),
531
184
  checkpoint_options: optionsFor("checkpoint"),
@@ -535,18 +188,7 @@ function helpPayload(spec) {
535
188
  session_targeting: {
536
189
  canonical: "--session <id>",
537
190
  positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
538
- commands: [
539
- "show",
540
- "inspect",
541
- "claim",
542
- "claims",
543
- "release",
544
- "update",
545
- "mutate",
546
- "transition",
547
- "close",
548
- "discard",
549
- ],
191
+ commands: sessionTargetingCommands,
550
192
  ambiguity: "supplying both positional and --session is rejected",
551
193
  discard_requires_explicit_target: true,
552
194
  },
@@ -554,13 +196,26 @@ function helpPayload(spec) {
554
196
  }
555
197
  const options = spec.options.map((candidate) => ({
556
198
  name: candidate.name,
199
+ ...(candidate.aliases === undefined ? {} : { aliases: [...candidate.aliases] }),
200
+ ...(candidate.alias_of === undefined ? {} : { alias_of: candidate.alias_of }),
557
201
  ...(candidate.value === undefined ? {} : { value: candidate.value }),
558
202
  required: candidate.required === true,
203
+ ...(candidate.repeatable === undefined ? {} : { repeatable: candidate.repeatable }),
559
204
  ...(candidate.default === undefined ? {} : { default: candidate.default }),
205
+ ...(candidate.minimum === undefined ? {} : { minimum: candidate.minimum }),
206
+ ...(candidate.maximum === undefined ? {} : { maximum: candidate.maximum }),
207
+ ...(candidate.values === undefined ? {} : { values: [...candidate.values] }),
208
+ ...(candidate.required_unless === undefined ? {} : { required_unless: [...candidate.required_unless] }),
209
+ ...(candidate.mutually_exclusive_with === undefined
210
+ ? {}
211
+ : { mutually_exclusive_with: [...candidate.mutually_exclusive_with] }),
560
212
  description: candidate.description,
561
213
  }));
214
+ const canonical = canonicalCommandForName(spec.name);
562
215
  return {
563
216
  help_for: spec.name,
217
+ ...(canonical === undefined || canonical.name === spec.name ? {} : { canonical_command: canonical.name }),
218
+ ...(canonical?.aliases === undefined || canonical.name !== spec.name ? {} : { aliases: [...canonical.aliases] }),
564
219
  usage: spec.usage,
565
220
  summary: spec.summary,
566
221
  required_options: spec.options
@@ -580,8 +235,9 @@ function helpText(spec) {
580
235
  const lines = [`Usage: ${spec.usage}`, "", spec.summary];
581
236
  if (spec.name === "root") {
582
237
  lines.push("", "Commands:");
583
- for (const command of HELP_COMMANDS)
238
+ for (const command of publicCliCommandDefinitions()) {
584
239
  lines.push(` ${command.name.padEnd(20)} ${command.summary}`);
240
+ }
585
241
  lines.push("", "Global options:");
586
242
  for (const candidate of GLOBAL_HELP_OPTIONS) {
587
243
  const label = candidate.name === "--help" ? "-h, --help" : candidate.name;
@@ -597,7 +253,15 @@ function helpText(spec) {
597
253
  const qualifier = candidate.required === true
598
254
  ? "required"
599
255
  : `optional${candidate.default === undefined ? "" : `; default: ${candidate.default}`}`;
600
- lines.push(` ${label.padEnd(38)} ${qualifier}; ${candidate.description}`);
256
+ const bounds = candidate.minimum === undefined && candidate.maximum === undefined
257
+ ? ""
258
+ : `; bounds: ${candidate.minimum ?? "-∞"}..${candidate.maximum ?? "∞"}`;
259
+ const values = candidate.values === undefined ? "" : `; values: ${candidate.values.join(", ")}`;
260
+ const requiredUnless = candidate.required_unless === undefined ? "" : `; required unless: ${candidate.required_unless.join(", ")}`;
261
+ const mutuallyExclusive = candidate.mutually_exclusive_with === undefined
262
+ ? ""
263
+ : `; mutually exclusive with: ${candidate.mutually_exclusive_with.join(", ")}`;
264
+ lines.push(` ${label.padEnd(38)} ${qualifier}${bounds}${values}${requiredUnless}${mutuallyExclusive}; ${candidate.description}`);
601
265
  }
602
266
  if (spec.notes !== undefined) {
603
267
  lines.push("", "Notes:");
@@ -675,6 +339,7 @@ function parseOptions(arguments_, allowed) {
675
339
  apply: false,
676
340
  force: false,
677
341
  create_upstream: false,
342
+ all_claimed: false,
678
343
  all: false,
679
344
  history: false,
680
345
  limit: null,
@@ -688,6 +353,7 @@ function parseOptions(arguments_, allowed) {
688
353
  integrated_revision: null,
689
354
  fetch_remote: null,
690
355
  fetch_branch: null,
356
+ preview: false,
691
357
  };
692
358
  let dryRun = false;
693
359
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -699,9 +365,11 @@ function parseOptions(arguments_, allowed) {
699
365
  name === "--dry-run" ||
700
366
  name === "--force" ||
701
367
  name === "--create-upstream" ||
368
+ name === "--all-claimed" ||
702
369
  name === "--all" ||
703
370
  name === "--history" ||
704
- name === "--patch") {
371
+ name === "--patch" ||
372
+ name === "--preview") {
705
373
  if (inlineValue !== null) {
706
374
  return failure(usageError("INVALID_ARGUMENT", `${name} does not accept a value.`, { option: name }));
707
375
  }
@@ -713,12 +381,16 @@ function parseOptions(arguments_, allowed) {
713
381
  options.force = true;
714
382
  else if (name === "--create-upstream")
715
383
  options.create_upstream = true;
384
+ else if (name === "--all-claimed")
385
+ options.all_claimed = true;
716
386
  else if (name === "--all")
717
387
  options.all = true;
718
388
  else if (name === "--history")
719
389
  options.history = true;
720
- else
390
+ else if (name === "--patch")
721
391
  options.patch = true;
392
+ else
393
+ options.preview = true;
722
394
  continue;
723
395
  }
724
396
  const value = inlineValue ?? arguments_[index + 1];
@@ -895,7 +567,7 @@ function parseReleaseSelection(arguments_) {
895
567
  expected_claim_set_generation: null,
896
568
  force: false,
897
569
  };
898
- const recognized = new Set(["--session", "--resource", "--claim-id", "--all", ...CLAIM_CONCURRENCY_OPTION_NAMES]);
570
+ const recognized = dispatcherAllowedOptions("session release");
899
571
  const targetArguments = positional.value.arguments;
900
572
  for (let index = 0; index < targetArguments.length; index += 1) {
901
573
  const { name, inlineValue } = optionParts(targetArguments[index]);
@@ -985,14 +657,7 @@ function parseClaimDeltaMutation(arguments_) {
985
657
  expected_claim_set_generation: null,
986
658
  force: false,
987
659
  };
988
- const recognized = new Set([
989
- "--session",
990
- "--repository",
991
- "--upsert-resource",
992
- "--mode",
993
- "--release-resource",
994
- ...CLAIM_CONCURRENCY_OPTION_NAMES,
995
- ]);
660
+ const recognized = dispatcherAllowedOptions("session mutate");
996
661
  const rejectPending = (name) => failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --upsert-resource and its --mode; only --mode is permitted immediately after --upsert-resource.`, { option: name }));
997
662
  const targetArguments = positional.value.arguments;
998
663
  for (let index = 0; index < targetArguments.length; index += 1) {
@@ -1086,7 +751,7 @@ function parseClaimDeltaMutation(arguments_) {
1086
751
  * constrains its public projection to exactly one upsert pair.
1087
752
  */
1088
753
  function parseClaimTransition(arguments_) {
1089
- const parsed = parseClaimReplacementPairs(arguments_);
754
+ const parsed = parseClaimReplacementPairs(arguments_, true, "session transition");
1090
755
  if (!parsed.ok)
1091
756
  return parsed;
1092
757
  if (parsed.value.pairs.length !== 1) {
@@ -1121,7 +786,7 @@ function parseClaimTransition(arguments_) {
1121
786
  * parallel-array position, so argv order can never associate a resource
1122
787
  * with the wrong mode.
1123
788
  */
1124
- function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true) {
789
+ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true, command = "session update") {
1125
790
  const positional = splitPositionalSessionTarget(arguments_);
1126
791
  if (!positional.ok)
1127
792
  return positional;
@@ -1135,14 +800,11 @@ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true)
1135
800
  };
1136
801
  sessionId = positional.value.sessionId;
1137
802
  const targetArguments = positional.value.arguments;
803
+ const recognized = dispatcherAllowedOptions(command);
1138
804
  for (let index = 0; index < targetArguments.length; index += 1) {
1139
805
  const { name, inlineValue } = optionParts(targetArguments[index]);
1140
806
  const isConcurrencyOption = isClaimConcurrencyOption(name);
1141
- if (name !== "--session" &&
1142
- name !== "--repository" &&
1143
- name !== "--resource" &&
1144
- name !== "--mode" &&
1145
- !(requireConcurrencyIntent && isConcurrencyOption)) {
807
+ if (!recognized.has(name) || (!requireConcurrencyIntent && isConcurrencyOption)) {
1146
808
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
1147
809
  }
1148
810
  if (pendingResource !== null && name !== "--mode") {
@@ -1210,7 +872,7 @@ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true)
1210
872
  * first pair (last-wins).
1211
873
  */
1212
874
  function parseSingleClaimPair(arguments_) {
1213
- const parsed = parseClaimReplacementPairs(arguments_, false);
875
+ const parsed = parseClaimReplacementPairs(arguments_, false, "session claim");
1214
876
  if (!parsed.ok)
1215
877
  return parsed;
1216
878
  if (parsed.value.pairs.length > 1) {
@@ -1333,6 +995,39 @@ async function executeRelease(arguments_, backend, context, operation) {
1333
995
  const result = await backend.releaseClaims(context, options);
1334
996
  return result.ok ? { ok: true, value: result.value } : result;
1335
997
  }
998
+ /**
999
+ * The one dispatcher boundary for protected managed execution.
1000
+ *
1001
+ * The public registry owns the command identity (including aliases); this
1002
+ * function owns only the routing contract. Resolution is always enforced
1003
+ * and the resolved request is handed to the #145 launcher. In particular,
1004
+ * there is no ambient child-process path to select when resolution or launch
1005
+ * fails.
1006
+ */
1007
+ async function executeProtectedSessionCommand(arguments_, dependencies, context) {
1008
+ const delimiter = arguments_.indexOf("--");
1009
+ if (delimiter === -1) {
1010
+ return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
1011
+ }
1012
+ const parsed = parseOptions(arguments_.slice(0, delimiter), new Set(["--session"]));
1013
+ if (!parsed.ok)
1014
+ return parsed;
1015
+ const command = arguments_[delimiter + 1];
1016
+ if (command === undefined || command.length === 0) {
1017
+ return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
1018
+ }
1019
+ const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
1020
+ if (!request.ok)
1021
+ return request;
1022
+ // `sandboxRunner` is an injection seam for unit tests. Production always
1023
+ // reaches the canonical protected launcher exported by #145.
1024
+ const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
1025
+ const result = await runner(request.value, {
1026
+ command,
1027
+ args: arguments_.slice(delimiter + 2),
1028
+ });
1029
+ return result.ok ? { ok: true, value: result.value } : result;
1030
+ }
1336
1031
  async function executeCommand(commandArguments, dependencies) {
1337
1032
  const [command, subcommand, ...rest] = commandArguments;
1338
1033
  const context = sessionContext(dependencies.cwd);
@@ -1340,6 +1035,12 @@ async function executeCommand(commandArguments, dependencies) {
1340
1035
  if (subcommand === undefined) {
1341
1036
  return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
1342
1037
  }
1038
+ // Resolve the subcommand through the canonical registry before dispatch.
1039
+ // This keeps `session exec` an alias of the same protected route instead
1040
+ // of creating a second launch path.
1041
+ if (canonicalCommandForName(`session ${subcommand}`)?.name === "session run") {
1042
+ return executeProtectedSessionCommand(rest, dependencies, context);
1043
+ }
1343
1044
  if (subcommand === "claim") {
1344
1045
  const parsed = parseSingleClaimPair(rest);
1345
1046
  if (!parsed.ok)
@@ -1380,7 +1081,7 @@ async function executeCommand(commandArguments, dependencies) {
1380
1081
  return executeClaimTransition(rest, dependencies.backend, context, "session transition");
1381
1082
  }
1382
1083
  if (subcommand === "claims") {
1383
- const parsed = parseTargetedOptions(rest, new Set(["--session"]));
1084
+ const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session claims"));
1384
1085
  if (!parsed.ok)
1385
1086
  return parsed;
1386
1087
  if (dependencies.backend.listClaims === undefined)
@@ -1392,7 +1093,7 @@ async function executeCommand(commandArguments, dependencies) {
1392
1093
  return executeRelease(rest, dependencies.backend, context, "session release");
1393
1094
  }
1394
1095
  if (subcommand === "create") {
1395
- const parsed = parseOptions(rest, new Set(["--branch", "--worktree", "--worktree-root", "--base", "--label"]));
1096
+ const parsed = parseOptions(rest, dispatcherAllowedOptions("session create"));
1396
1097
  if (!parsed.ok)
1397
1098
  return parsed;
1398
1099
  if (parsed.value.worktree !== null && parsed.value.worktree_root !== null) {
@@ -1408,30 +1109,8 @@ async function executeCommand(commandArguments, dependencies) {
1408
1109
  const result = await dependencies.backend.createSession(context, options);
1409
1110
  return result.ok ? { ok: true, value: result.value } : result;
1410
1111
  }
1411
- if (subcommand === "run" || subcommand === "exec") {
1412
- const delimiter = rest.indexOf("--");
1413
- if (delimiter === -1) {
1414
- return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
1415
- }
1416
- const parsed = parseOptions(rest.slice(0, delimiter), new Set(["--session"]));
1417
- if (!parsed.ok)
1418
- return parsed;
1419
- const command = rest[delimiter + 1];
1420
- if (command === undefined || command.length === 0) {
1421
- return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
1422
- }
1423
- const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
1424
- if (!request.ok)
1425
- return request;
1426
- const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
1427
- const result = await runner(request.value, {
1428
- command,
1429
- args: rest.slice(delimiter + 2),
1430
- });
1431
- return result.ok ? { ok: true, value: result.value } : result;
1432
- }
1433
1112
  if (subcommand === "inspect") {
1434
- const parsed = parseTargetedOptions(rest, new Set(["--session", "--integrated-revision"]));
1113
+ const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session inspect"));
1435
1114
  if (!parsed.ok)
1436
1115
  return parsed;
1437
1116
  if (dependencies.backend.sessionDiagnostic === undefined)
@@ -1444,9 +1123,15 @@ async function executeCommand(commandArguments, dependencies) {
1444
1123
  return result.ok ? { ok: true, value: result.value } : result;
1445
1124
  }
1446
1125
  if (subcommand === "discard") {
1447
- const parsed = parseTargetedOptions(rest, new Set(["--session"]), true);
1126
+ const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session discard"), true);
1448
1127
  if (!parsed.ok)
1449
1128
  return parsed;
1129
+ if (parsed.value.preview) {
1130
+ if (dependencies.backend.discardPreview === undefined)
1131
+ return sessionDiscardPreviewCapabilityUnavailable();
1132
+ const preview = await dependencies.backend.discardPreview(context, parsed.value.session_id);
1133
+ return preview.ok ? { ok: true, value: preview.value } : preview;
1134
+ }
1450
1135
  if (dependencies.backend.discardSession === undefined)
1451
1136
  return sessionDiscardCapabilityUnavailable();
1452
1137
  const result = await dependencies.backend.discardSession(context, parsed.value.session_id);
@@ -1462,9 +1147,7 @@ async function executeCommand(commandArguments, dependencies) {
1462
1147
  return selected;
1463
1148
  return { ok: true, value: { session_id: selected.value.session_id } };
1464
1149
  }
1465
- const parsed = parseTargetedOptions(rest, new Set(subcommand === "close"
1466
- ? ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"]
1467
- : ["--session"]));
1150
+ const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions(subcommand === "close" ? "session close" : "session show"));
1468
1151
  if (!parsed.ok)
1469
1152
  return parsed;
1470
1153
  if (subcommand === "close") {
@@ -1491,7 +1174,7 @@ async function executeCommand(commandArguments, dependencies) {
1491
1174
  return selected.ok ? { ok: true, value: selected.value } : selected;
1492
1175
  }
1493
1176
  if (subcommand === "list") {
1494
- const parsed = parseOptions(rest, new Set(["--all", "--history", "--limit", "--offset"]));
1177
+ const parsed = parseOptions(rest, dispatcherAllowedOptions("session list"));
1495
1178
  if (!parsed.ok)
1496
1179
  return parsed;
1497
1180
  const options = sessionListingOptions(parsed.value);
@@ -1544,7 +1227,7 @@ async function executeCommand(commandArguments, dependencies) {
1544
1227
  return executeClaimTransition(rest, dependencies.backend, context, "resource transition");
1545
1228
  }
1546
1229
  if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
1547
- const parsed = parseTargetedOptions(rest, new Set(["--session"]));
1230
+ const parsed = parseTargetedOptions(rest, dispatcherAllowedOptions("session claims"));
1548
1231
  if (!parsed.ok)
1549
1232
  return parsed;
1550
1233
  if (dependencies.backend.listClaims === undefined)
@@ -1560,7 +1243,7 @@ async function executeCommand(commandArguments, dependencies) {
1560
1243
  }));
1561
1244
  }
1562
1245
  if (command === "authorize") {
1563
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--operation", "--resource"]));
1246
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("authorize"));
1564
1247
  if (!parsed.ok)
1565
1248
  return parsed;
1566
1249
  if (parsed.value.operation === null) {
@@ -1584,7 +1267,7 @@ async function executeCommand(commandArguments, dependencies) {
1584
1267
  : deniedAuthorization(result.value);
1585
1268
  }
1586
1269
  if (command === "checkpoint") {
1587
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session"]));
1270
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("checkpoint"));
1588
1271
  if (!parsed.ok)
1589
1272
  return parsed;
1590
1273
  if (dependencies.backend.checkpoint === undefined)
@@ -1594,7 +1277,7 @@ async function executeCommand(commandArguments, dependencies) {
1594
1277
  return result.ok ? { ok: true, value: result.value } : result;
1595
1278
  }
1596
1279
  if (command === "evidence" && subcommand === "snapshot") {
1597
- const parsed = parseOptions(rest, new Set(["--session"]));
1280
+ const parsed = parseOptions(rest, dispatcherAllowedOptions("evidence snapshot"));
1598
1281
  if (!parsed.ok)
1599
1282
  return parsed;
1600
1283
  if (parsed.value.session_id === null) {
@@ -1608,7 +1291,7 @@ async function executeCommand(commandArguments, dependencies) {
1608
1291
  return result.ok ? { ok: true, value: result.value } : result;
1609
1292
  }
1610
1293
  if (command === "diff") {
1611
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--path", "--from", "--to", "--patch", "--max-bytes", "--max-hunks"]));
1294
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("diff"));
1612
1295
  if (!parsed.ok)
1613
1296
  return parsed;
1614
1297
  if (parsed.value.session_id === null) {
@@ -1645,13 +1328,18 @@ async function executeCommand(commandArguments, dependencies) {
1645
1328
  return result.ok ? { ok: true, value: result.value } : result;
1646
1329
  }
1647
1330
  if (command === "commit") {
1648
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--message", "--resource", "--message-pattern"]));
1331
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("commit"));
1649
1332
  if (!parsed.ok)
1650
1333
  return parsed;
1651
1334
  if (parsed.value.message === null) {
1652
1335
  return failure(usageError("MISSING_ARGUMENT", "--message requires a value.", { option: "--message" }));
1653
1336
  }
1654
- if (parsed.value.resources.length === 0) {
1337
+ if (parsed.value.all_claimed && parsed.value.resources.length > 0) {
1338
+ return failure(usageError("INVALID_ARGUMENT", "--resource and --all-claimed cannot be used together.", {
1339
+ options: ["--resource", "--all-claimed"],
1340
+ }));
1341
+ }
1342
+ if (!parsed.value.all_claimed && parsed.value.resources.length === 0) {
1655
1343
  return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
1656
1344
  }
1657
1345
  if (dependencies.backend.commit === undefined)
@@ -1660,12 +1348,13 @@ async function executeCommand(commandArguments, dependencies) {
1660
1348
  session_id: parsed.value.session_id,
1661
1349
  message: parsed.value.message,
1662
1350
  resources: parsed.value.resources,
1351
+ ...(parsed.value.all_claimed ? { all_claimed: true } : {}),
1663
1352
  message_pattern: parsed.value.message_pattern,
1664
1353
  });
1665
1354
  return result.ok ? { ok: true, value: result.value } : result;
1666
1355
  }
1667
1356
  if (command === "push") {
1668
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--resource", "--remote", "--branch", "--remote-branch", "--force", "--create-upstream"]));
1357
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("push"));
1669
1358
  if (!parsed.ok)
1670
1359
  return parsed;
1671
1360
  if (parsed.value.remote === null) {
@@ -1679,7 +1368,12 @@ async function executeCommand(commandArguments, dependencies) {
1679
1368
  parsed.value.branch !== parsed.value.remote_branch) {
1680
1369
  return failure(usageError("INVALID_ARGUMENT", "--branch and --remote-branch must identify the same target."));
1681
1370
  }
1682
- if (parsed.value.resources.length === 0) {
1371
+ if (parsed.value.all_claimed && parsed.value.resources.length > 0) {
1372
+ return failure(usageError("INVALID_ARGUMENT", "--resource and --all-claimed cannot be used together.", {
1373
+ options: ["--resource", "--all-claimed"],
1374
+ }));
1375
+ }
1376
+ if (!parsed.value.all_claimed && parsed.value.resources.length === 0) {
1683
1377
  return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
1684
1378
  }
1685
1379
  if (dependencies.backend.push === undefined)
@@ -1687,6 +1381,7 @@ async function executeCommand(commandArguments, dependencies) {
1687
1381
  const result = await dependencies.backend.push(context, {
1688
1382
  session_id: parsed.value.session_id,
1689
1383
  resources: parsed.value.resources,
1384
+ ...(parsed.value.all_claimed ? { all_claimed: true } : {}),
1690
1385
  remote: parsed.value.remote,
1691
1386
  branch: parsed.value.branch ?? parsed.value.remote_branch,
1692
1387
  force: parsed.value.force,
@@ -1695,7 +1390,7 @@ async function executeCommand(commandArguments, dependencies) {
1695
1390
  return result.ok ? { ok: true, value: result.value } : result;
1696
1391
  }
1697
1392
  if (command === "status") {
1698
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--all", "--history", "--limit", "--offset"]));
1393
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("status"));
1699
1394
  if (!parsed.ok)
1700
1395
  return parsed;
1701
1396
  const options = sessionListingOptions(parsed.value);
@@ -1705,7 +1400,7 @@ async function executeCommand(commandArguments, dependencies) {
1705
1400
  return result.ok ? { ok: true, value: result.value } : result;
1706
1401
  }
1707
1402
  if (command === "guard") {
1708
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--session", "--operation", "--resource"]));
1403
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("guard"));
1709
1404
  if (!parsed.ok)
1710
1405
  return parsed;
1711
1406
  if (parsed.value.resources.length > 0 && parsed.value.operation === null) {
@@ -1749,7 +1444,7 @@ async function executeCommand(commandArguments, dependencies) {
1749
1444
  }));
1750
1445
  }
1751
1446
  if (command === "gc") {
1752
- const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), new Set(["--apply", "--dry-run"]));
1447
+ const parsed = parseOptions([subcommand, ...rest].filter((argument) => argument !== undefined), dispatcherAllowedOptions("gc"));
1753
1448
  if (!parsed.ok)
1754
1449
  return parsed;
1755
1450
  const options = { apply: parsed.value.apply };
@@ -1760,7 +1455,7 @@ async function executeCommand(commandArguments, dependencies) {
1760
1455
  const parsed = noOptions([subcommand, ...rest].filter((argument) => argument !== undefined));
1761
1456
  if (!parsed.ok)
1762
1457
  return parsed;
1763
- const report = await runDoctor(dependencies.cwd);
1458
+ const report = await runDoctor(dependencies.cwd, dependencies.sandboxProbe);
1764
1459
  if (!report.ok)
1765
1460
  return report;
1766
1461
  if (report.value.ok)
@@ -1768,8 +1463,18 @@ async function executeCommand(commandArguments, dependencies) {
1768
1463
  return failure(new DomainError("DOCTOR_FAILED", "One or more local Nawabari checks failed.", {
1769
1464
  checks: report.value.checks,
1770
1465
  repository: report.value.repository,
1466
+ sandbox: report.value.sandbox,
1771
1467
  }));
1772
1468
  }
1469
+ if (command === "migrate") {
1470
+ const parsed = noOptions([subcommand, ...rest].filter((argument) => argument !== undefined));
1471
+ if (!parsed.ok)
1472
+ return parsed;
1473
+ if (dependencies.backend.migrate === undefined)
1474
+ return migrationCapabilityUnavailable();
1475
+ const result = await dependencies.backend.migrate(context);
1476
+ return result.ok ? { ok: true, value: result.value } : result;
1477
+ }
1773
1478
  if (command === undefined)
1774
1479
  return failure(new DomainError("NO_COMMAND", "A command is required."));
1775
1480
  return failure(new DomainError("UNKNOWN_COMMAND", `Unknown command: ${command}.`, { command }));
@@ -1777,8 +1482,9 @@ async function executeCommand(commandArguments, dependencies) {
1777
1482
  function commandName(commandArguments) {
1778
1483
  if (commandArguments[0] === "session")
1779
1484
  return commandArguments.slice(0, 2).join(" ");
1780
- if (commandArguments[0] === "resource")
1781
- return commandArguments.slice(0, 2).join(" ");
1485
+ if (commandArguments[0] === "resource") {
1486
+ return commandArguments[1] === undefined ? "resource list" : commandArguments.slice(0, 2).join(" ");
1487
+ }
1782
1488
  if (commandArguments[0] === "evidence")
1783
1489
  return commandArguments.slice(0, 2).join(" ");
1784
1490
  return commandArguments[0] ?? "cli";
@@ -1786,6 +1492,11 @@ function commandName(commandArguments) {
1786
1492
  function claimCapabilityUnavailable(operation) {
1787
1493
  return failure(new DomainError("BACKEND_UNAVAILABLE", "Resource claim capability is not available.", { operation }));
1788
1494
  }
1495
+ function migrationCapabilityUnavailable() {
1496
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Registry migration capability is not available.", {
1497
+ operation: "migrate",
1498
+ }));
1499
+ }
1789
1500
  function authorizationCapabilityUnavailable() {
1790
1501
  return failure(new DomainError("BACKEND_UNAVAILABLE", "Operation authorization capability is not available.", {
1791
1502
  operation: "authorize",
@@ -1806,6 +1517,11 @@ function sessionDiscardCapabilityUnavailable() {
1806
1517
  operation: "session.discard",
1807
1518
  }));
1808
1519
  }
1520
+ function sessionDiscardPreviewCapabilityUnavailable() {
1521
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Session discard preview capability is not available.", {
1522
+ operation: "session.discard.preview",
1523
+ }));
1524
+ }
1809
1525
  function repositoryEvidenceCapabilityUnavailable(operation) {
1810
1526
  return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
1811
1527
  }
@@ -1948,12 +1664,15 @@ export async function runCli(argv, dependencies = {}) {
1948
1664
  const enriched = await enrichInvalidSessionIdError(result.error, backend, sessionContext(cwd));
1949
1665
  return emitFailure(mode, command, enriched, io);
1950
1666
  }
1667
+ const childExitCode = result.value.exit_code;
1668
+ const childSignal = result.value.signal;
1951
1669
  io.stdout(renderSuccess(mode, command, result.value));
1952
1670
  if ((command === "session run" || command === "session exec") &&
1953
- typeof result.value.exit_code === "number" &&
1954
- result.value.exit_code !== 0) {
1955
- return result.value.exit_code >= 1 && result.value.exit_code <= 255
1956
- ? result.value.exit_code
1671
+ (childSignal !== null || (typeof childExitCode === "number" && childExitCode !== 0))) {
1672
+ if (childSignal !== null)
1673
+ return EXIT_CODES.rejected;
1674
+ return typeof childExitCode === "number" && childExitCode >= 1 && childExitCode <= 255
1675
+ ? childExitCode
1957
1676
  : EXIT_CODES.rejected;
1958
1677
  }
1959
1678
  return EXIT_CODES.success;