nawabari 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,6 +94,7 @@ The supported standalone sequence is:
94
94
  ```text
95
95
  session create -> session claim(s) -> authorize/checkpoint
96
96
  -> commit/push -> doctor (reconciliation) -> session close/gc
97
+ -> explicit session discard only when the selected work is intentionally disposable
97
98
  ```
98
99
 
99
100
  The JSON envelope is one document on stdout. Success has `ok: true`, a
@@ -106,16 +107,17 @@ failure. Consumers must use these fields and codes, never human presentation.
106
107
 
107
108
  The result schemas expose the following identities:
108
109
 
109
- | Surface | Versioned identities |
110
- | ---------------------- | -------------------------------------------------------------------------------- |
111
- | session lifecycle | `session_id`, `repository`, `worktree`, `branch`, `state` |
112
- | claims | `claim_id`, `session_id`, `resource`, `mode` |
113
- | authorization | `operation`, `allowed`, `code`, `claim_ids` |
114
- | checkpoint evidence | `head`, `changed`, `staged`, `unstaged`, `untracked`, `in_claim`, `out_of_claim` |
115
- | repository evidence | `session_id`, `base_revision`, `head`, `clean`, `paths.stats`, `evidence_hash` |
116
- | bounded diff | `from_revision`, `to_revision`, `paths`, `stats`, `patch`, `evidence_hash` |
117
- | commit/push | `commit_sha`, `remote`, `branch`, `target`, `relation` |
118
- | reconciliation/cleanup | `clean`, `issues`, `candidates`, `cleaned`, `blocked`, `recovery_hints` |
110
+ | Surface | Versioned identities |
111
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
112
+ | session lifecycle | `session_id`, `repository`, `worktree`, `branch`, `state` |
113
+ | explicit discard | `previous_head`, `worktree_removed`, `branch_removed`, `released_claims`, `final_state`, final `session.state`/`terminal_operation` |
114
+ | claims | `claim_id`, `session_id`, `resource`, `mode` |
115
+ | authorization | `operation`, `allowed`, `code`, `claim_ids` |
116
+ | checkpoint evidence | `head`, `changed`, `staged`, `unstaged`, `untracked`, `in_claim`, `out_of_claim` |
117
+ | repository evidence | `session_id`, `base_revision`, `head`, `clean`, `paths.stats`, `evidence_hash` |
118
+ | bounded diff | `from_revision`, `to_revision`, `paths`, `stats`, `patch`, `evidence_hash` |
119
+ | commit/push | `commit_sha`, `remote`, `branch`, `target`, `relation` |
120
+ | reconciliation/cleanup | `clean`, `issues`, `candidates`, `cleaned`, `blocked`, `recovery_hints` |
119
121
 
120
122
  Git subprocesses are bounded at 10 seconds and 64 KiB of output; checkpoint
121
123
  evidence is bounded to 4,096 paths. `GIT_SPAWN_FAILED`, `GIT_TIMEOUT`,
@@ -162,10 +164,12 @@ machine identities; labels and branch names are separate display metadata.
162
164
  git nawabari session create --branch feature/example --worktree ../example-worktree --json
163
165
  git nawabari session id --json
164
166
  git nawabari session show --json
167
+ git nawabari session show <session-id> --json
165
168
  git nawabari session list --json
166
169
  git nawabari status --json
167
170
  git nawabari guard --json
168
171
  git nawabari session close --json
172
+ git nawabari session discard --session <session-id> --json
169
173
  git nawabari gc --dry-run --json
170
174
  git nawabari doctor --json
171
175
  ```
@@ -185,7 +189,14 @@ the resolved parent of that session's worktree.
185
189
  under the repository-scoped mutation lock. The default/integration worktree
186
190
  and its protected branch cannot be session resources. `session id` and the
187
191
  other current-session commands resolve ownership from the current worktree;
188
- callers do not need to repeat the session ID.
192
+ callers do not need to repeat the session ID for current-owner operations.
193
+
194
+ Session-scoped commands use one target grammar: the canonical `--session <id>`
195
+ option is accepted everywhere, and `show`, `inspect`, `claim`, `claims`,
196
+ `release`, `update`, `close`, and `discard` also accept one positional
197
+ `<session-id>` immediately after the subcommand. Supplying both forms is
198
+ rejected as ambiguous. `session discard` always requires one explicit target
199
+ and never infers the current worktree owner.
189
200
 
190
201
  Close is conservative. Dirty worktrees, ambiguous ownership, mismatched Git
191
202
  state, and commits not proven reachable from the integration branch block
@@ -198,6 +209,17 @@ every candidate that is not safe. Cleanup revalidates the physical worktree,
198
209
  branch, and `HEAD` observations immediately before each destructive Git
199
210
  operation.
200
211
 
212
+ `session discard` is the sole explicit destructive abandonment path. It
213
+ revalidates repository identity, the selected session, exact worktree/branch
214
+ ownership, and session/worktree `HEAD` immediately before each Git mutation.
215
+ It may destroy the selected session's unintegrated commits and uncommitted
216
+ worktree contents, removes only that session's worktree/branch, releases only
217
+ its claims, and records `terminal_operation: "discard"` plus the pre-discard
218
+ `HEAD`. It never acts as an implicit fallback for `close`, `gc`, `doctor`, or
219
+ reconciliation; sibling sessions remain untouched. A partial failure leaves a
220
+ retryable closing record and a repeated discard converges or returns an
221
+ explicit terminal idempotent result.
222
+
201
223
  Routine `session list` and `status` output excludes `closed` history and is
202
224
  limited to 64 records. Use `--all` (or `--history`) for an explicit complete
203
225
  history view; closed records remain persisted and are never silently deleted
@@ -363,8 +385,14 @@ differs.
363
385
  could not prove the branch safe and a `--integrated-revision` proof might
364
386
  resolve it (e.g. after a squash/rebase merge), versus `blocked` when
365
387
  supplied evidence failed to prove equivalence, versus `ambiguous` when Git
366
- observation itself was inconclusive. Both surfaces reuse one authority, so
367
- a raw close rejection and `session inspect` never drift apart.
388
+ observation itself was inconclusive. For supplied evidence, bounded
389
+ `proofFailure` details distinguish an unauthoritative revision from a
390
+ tree-equivalence failure. The bounded evidence also includes
391
+ `currentSessionHead`, `suppliedIntegratedRevision`, `resolvedIntegrationSha`,
392
+ `lineageProof`/`authorityProof`, and `contentProof`. `safe_actions` includes
393
+ `discard-session` only as an explicit user choice; it does not authorize an
394
+ implicit cleanup fallback. Both surfaces reuse one authority, so a raw close
395
+ rejection and `session inspect` never drift apart.
368
396
 
369
397
  None of the above weakens fail-closed behavior, changes an error code's
370
398
  meaning, or performs any mutation while producing the rejection.
package/dist/cli.js CHANGED
@@ -48,13 +48,16 @@ const HELP_COMMANDS = [
48
48
  {
49
49
  name: "session show",
50
50
  summary: "Show the current or selected session",
51
- usage: `${CLI_NAME} session show [--session <id>]`,
51
+ usage: `${CLI_NAME} session show [<session-id>|--session <id>]`,
52
52
  options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
53
+ notes: [
54
+ "Target grammar is consistent: an optional first positional <session-id> is an alias for --session <id>; do not supply both.",
55
+ ],
53
56
  },
54
57
  {
55
58
  name: "session inspect",
56
59
  summary: "Report side-effect-free close/cleanup readiness for a session",
57
- usage: `${CLI_NAME} session inspect [--session <id>] [--integrated-revision <rev>]`,
60
+ usage: `${CLI_NAME} session inspect [<session-id>|--session <id>] [--integrated-revision <rev>]`,
58
61
  options: [
59
62
  option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
60
63
  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>" }),
@@ -63,6 +66,7 @@ const HELP_COMMANDS = [
63
66
  "Read-only: never mutates session, claim, Git, worktree, branch, or registry state. Repeated calls are idempotent.",
64
67
  "Derived from the same authoritative close/cleanup Git evidence as session close; does not duplicate or diverge from that logic.",
65
68
  "Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
69
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
66
70
  ],
67
71
  },
68
72
  {
@@ -88,18 +92,21 @@ const HELP_COMMANDS = [
88
92
  {
89
93
  name: "session claim",
90
94
  summary: "Add a canonical resource claim",
91
- usage: `${CLI_NAME} session claim --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
95
+ usage: `${CLI_NAME} session claim [<session-id>] --resource <path-or-glob> --mode <read|write|exclusive-write>`,
92
96
  options: [
93
97
  option("--resource", "Repository-relative resource", { value: "<path-or-glob>", required: true }),
94
98
  option("--mode", "Granted claim mode", { value: "<read|write|exclusive-write>", required: true }),
95
99
  option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
96
100
  option("--repository", "Expected repository identity", { value: "<id>" }),
97
101
  ],
102
+ notes: [
103
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
104
+ ],
98
105
  },
99
106
  {
100
107
  name: "session update",
101
108
  summary: "Atomically replace a session's complete resource claim set",
102
- usage: `${CLI_NAME} session update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
109
+ 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> ...]`,
103
110
  options: [
104
111
  option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
105
112
  value: "<path-or-glob>",
@@ -116,33 +123,43 @@ const HELP_COMMANDS = [
116
123
  "The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
117
124
  "on any invalid or conflicting claim the prior set is left unchanged.",
118
125
  "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
126
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
119
127
  ],
120
128
  },
121
129
  {
122
130
  name: "session claims",
123
131
  summary: "List canonical resource claims",
124
- usage: `${CLI_NAME} session claims [--session <id>]`,
132
+ usage: `${CLI_NAME} session claims [<session-id>|--session <id>]`,
125
133
  options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
134
+ notes: [
135
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
136
+ ],
126
137
  },
127
138
  {
128
139
  name: "session release",
129
140
  summary: "Release resource claims",
130
- usage: `${CLI_NAME} session release [--session <id>] [--claim-id <id>]`,
141
+ usage: `${CLI_NAME} session release [<session-id>|--session <id>] [--claim-id <id>]`,
131
142
  options: [
132
143
  option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
133
144
  option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
134
145
  ],
146
+ notes: [
147
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
148
+ ],
135
149
  },
136
150
  {
137
151
  name: "resource claim",
138
152
  summary: "Add a canonical resource claim (alias)",
139
- usage: `${CLI_NAME} resource claim --resource <path-or-glob> --mode <read|write|exclusive-write>`,
153
+ usage: `${CLI_NAME} resource claim [<session-id>|--session <id>] --resource <path-or-glob> --mode <read|write|exclusive-write>`,
140
154
  options: [],
155
+ notes: [
156
+ "Target grammar matches session claim: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
157
+ ],
141
158
  },
142
159
  {
143
160
  name: "resource update",
144
161
  summary: "Atomically replace a session's complete resource claim set (alias)",
145
- usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
162
+ 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> ...]`,
146
163
  options: [
147
164
  option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
148
165
  value: "<path-or-glob>",
@@ -159,34 +176,65 @@ const HELP_COMMANDS = [
159
176
  "The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
160
177
  "on any invalid or conflicting claim the prior set is left unchanged.",
161
178
  "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
179
+ "Target grammar matches session update: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
162
180
  ],
163
181
  },
164
182
  {
165
183
  name: "resource list",
166
184
  summary: "List canonical resource claims (alias)",
167
- usage: `${CLI_NAME} resource list [--session <id>]`,
185
+ usage: `${CLI_NAME} resource list [<session-id>|--session <id>]`,
168
186
  options: [option("--session", "Select a session; omitted lists all claims", { value: "<id>" })],
187
+ notes: [
188
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
189
+ ],
169
190
  },
170
191
  {
171
192
  name: "resource release",
172
193
  summary: "Release resource claims (alias)",
173
- usage: `${CLI_NAME} resource release [--session <id>] [--claim-id <id>]`,
194
+ usage: `${CLI_NAME} resource release [<session-id>|--session <id>] [--claim-id <id>]`,
174
195
  options: [
175
196
  option("--session", "Target session; omitted resolves the current owner", { value: "<id>" }),
176
197
  option("--claim-id", "Release only one claim; omitted releases all owned claims", { value: "<id>" }),
177
198
  ],
199
+ notes: [
200
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
201
+ ],
178
202
  },
179
203
  {
180
204
  name: "session close",
181
205
  summary: "Close the current or selected session",
182
- usage: `${CLI_NAME} session close [--session <id>] [--integrated-revision <rev>]`,
206
+ usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>] [--fetch-remote <name> --fetch-branch <branch>]`,
183
207
  options: [
184
208
  option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
185
209
  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>" }),
210
+ option("--fetch-remote", "Explicitly fetch one remote integration branch into a disposable proof ref", {
211
+ value: "<name>",
212
+ }),
213
+ option("--fetch-branch", "Integration branch to fetch; requires --fetch-remote", { value: "<branch>" }),
186
214
  ],
187
215
  notes: [
188
216
  "Ordinary ancestry-based close remains the cheap/default path and requires no flags.",
189
- "Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
217
+ "Network access is never implicit; --fetch-remote and --fetch-branch are accepted only with a full lowercase --integrated-revision SHA.",
218
+ "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.",
219
+ "Nawabari never calls provider APIs such as GitHub; only explicit --fetch-remote/--fetch-branch options connect to the configured Git remote.",
220
+ "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
221
+ ],
222
+ },
223
+ {
224
+ name: "session discard",
225
+ summary: "Explicitly discard one selected session and its owned resources",
226
+ usage: `${CLI_NAME} session discard <session-id>|--session <id>`,
227
+ options: [
228
+ option("--session", "Required explicit target; the current session is never inferred", {
229
+ value: "<id>",
230
+ required: true,
231
+ }),
232
+ ],
233
+ notes: [
234
+ "Destructive and explicit: unintegrated commits and uncommitted work in the selected owned worktree may be destroyed.",
235
+ "Discard never changes close, gc, doctor, reconciliation, or integration-lineage proof behavior.",
236
+ "Use exactly one target form: positional <session-id> or --session <id>; do not supply both.",
237
+ "Machine JSON mode is non-interactive and deterministic.",
190
238
  ],
191
239
  },
192
240
  {
@@ -347,6 +395,13 @@ function helpPayload(spec) {
347
395
  commit_options: optionsFor("commit"),
348
396
  push_options: optionsFor("push"),
349
397
  gc_options: optionsFor("gc"),
398
+ session_targeting: {
399
+ canonical: "--session <id>",
400
+ positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
401
+ commands: ["show", "inspect", "claim", "claims", "release", "update", "close", "discard"],
402
+ ambiguity: "supplying both positional and --session is rejected",
403
+ discard_requires_explicit_target: true,
404
+ },
350
405
  };
351
406
  }
352
407
  const options = spec.options.map((candidate) => ({
@@ -483,6 +538,8 @@ function parseOptions(arguments_, allowed) {
483
538
  max_bytes: null,
484
539
  max_hunks: null,
485
540
  integrated_revision: null,
541
+ fetch_remote: null,
542
+ fetch_branch: null,
486
543
  };
487
544
  let dryRun = false;
488
545
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -570,6 +627,10 @@ function parseOptions(arguments_, allowed) {
570
627
  options.max_hunks = value;
571
628
  else if (name === "--integrated-revision")
572
629
  options.integrated_revision = value;
630
+ else if (name === "--fetch-remote")
631
+ options.fetch_remote = value;
632
+ else if (name === "--fetch-branch")
633
+ options.fetch_branch = value;
573
634
  }
574
635
  if (options.apply && dryRun) {
575
636
  return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
@@ -579,6 +640,34 @@ function parseOptions(arguments_, allowed) {
579
640
  function noOptions(arguments_) {
580
641
  return parseOptions(arguments_, new Set());
581
642
  }
643
+ /** One unambiguous positional alias shared by every session-targeted command. */
644
+ function splitPositionalSessionTarget(arguments_) {
645
+ const first = arguments_[0];
646
+ if (first === undefined || first.startsWith("-")) {
647
+ return { ok: true, value: { sessionId: null, arguments: arguments_ } };
648
+ }
649
+ return { ok: true, value: { sessionId: first, arguments: arguments_.slice(1) } };
650
+ }
651
+ function parseTargetedOptions(arguments_, allowed, required = false) {
652
+ const positional = splitPositionalSessionTarget(arguments_);
653
+ if (!positional.ok)
654
+ return positional;
655
+ const parsed = parseOptions(positional.value.arguments, allowed);
656
+ if (!parsed.ok)
657
+ return parsed;
658
+ if (positional.value.sessionId !== null && parsed.value.session_id !== null) {
659
+ return failure(usageError("INVALID_ARGUMENT", "Specify a session target either positionally or with --session, not both.", {
660
+ option: "--session",
661
+ }));
662
+ }
663
+ parsed.value.session_id = positional.value.sessionId ?? parsed.value.session_id;
664
+ if (required && parsed.value.session_id === null) {
665
+ return failure(usageError("MISSING_ARGUMENT", "An explicit session target is required; use <session-id> or --session <id>.", {
666
+ option: "--session",
667
+ }));
668
+ }
669
+ return parsed;
670
+ }
582
671
  /**
583
672
  * `update` accepts a complete desired claim set as repeated
584
673
  * `--resource <path> --mode <mode>` pairs. Pairing is by strict local
@@ -588,26 +677,35 @@ function noOptions(arguments_) {
588
677
  * with the wrong mode.
589
678
  */
590
679
  function parseClaimReplacementPairs(arguments_) {
680
+ const positional = splitPositionalSessionTarget(arguments_);
681
+ if (!positional.ok)
682
+ return positional;
591
683
  let sessionId = null;
592
684
  let repository = null;
593
685
  const pairs = [];
594
686
  let pendingResource = null;
595
- for (let index = 0; index < arguments_.length; index += 1) {
596
- const { name, inlineValue } = optionParts(arguments_[index]);
687
+ sessionId = positional.value.sessionId;
688
+ const targetArguments = positional.value.arguments;
689
+ for (let index = 0; index < targetArguments.length; index += 1) {
690
+ const { name, inlineValue } = optionParts(targetArguments[index]);
597
691
  if (name !== "--session" && name !== "--repository" && name !== "--resource" && name !== "--mode") {
598
692
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
599
693
  }
600
694
  if (pendingResource !== null && name !== "--mode") {
601
695
  return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
602
696
  }
603
- const value = inlineValue ?? arguments_[index + 1];
697
+ const value = inlineValue ?? targetArguments[index + 1];
604
698
  if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
605
699
  return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
606
700
  }
607
701
  if (inlineValue === null)
608
702
  index += 1;
609
- if (name === "--session")
703
+ if (name === "--session") {
704
+ if (sessionId !== null) {
705
+ return failure(usageError("INVALID_ARGUMENT", "Specify a session target either positionally or with --session, not both."));
706
+ }
610
707
  sessionId = value;
708
+ }
611
709
  else if (name === "--repository")
612
710
  repository = value;
613
711
  else if (name === "--resource")
@@ -745,7 +843,7 @@ async function executeCommand(commandArguments, dependencies) {
745
843
  return result.ok ? { ok: true, value: result.value } : result;
746
844
  }
747
845
  if (subcommand === "claims") {
748
- const parsed = parseOptions(rest, new Set(["--session"]));
846
+ const parsed = parseTargetedOptions(rest, new Set(["--session"]));
749
847
  if (!parsed.ok)
750
848
  return parsed;
751
849
  if (dependencies.backend.listClaims === undefined)
@@ -754,7 +852,7 @@ async function executeCommand(commandArguments, dependencies) {
754
852
  return result.ok ? { ok: true, value: result.value } : result;
755
853
  }
756
854
  if (subcommand === "release") {
757
- const parsed = parseOptions(rest, new Set(["--session", "--claim-id"]));
855
+ const parsed = parseTargetedOptions(rest, new Set(["--session", "--claim-id"]));
758
856
  if (!parsed.ok)
759
857
  return parsed;
760
858
  if (dependencies.backend.releaseClaims === undefined)
@@ -805,7 +903,7 @@ async function executeCommand(commandArguments, dependencies) {
805
903
  return result.ok ? { ok: true, value: result.value } : result;
806
904
  }
807
905
  if (subcommand === "inspect") {
808
- const parsed = parseOptions(rest, new Set(["--session", "--integrated-revision"]));
906
+ const parsed = parseTargetedOptions(rest, new Set(["--session", "--integrated-revision"]));
809
907
  if (!parsed.ok)
810
908
  return parsed;
811
909
  if (dependencies.backend.sessionDiagnostic === undefined)
@@ -817,6 +915,15 @@ async function executeCommand(commandArguments, dependencies) {
817
915
  const result = await dependencies.backend.sessionDiagnostic(context, options);
818
916
  return result.ok ? { ok: true, value: result.value } : result;
819
917
  }
918
+ if (subcommand === "discard") {
919
+ const parsed = parseTargetedOptions(rest, new Set(["--session"]), true);
920
+ if (!parsed.ok)
921
+ return parsed;
922
+ if (dependencies.backend.discardSession === undefined)
923
+ return sessionDiscardCapabilityUnavailable();
924
+ const result = await dependencies.backend.discardSession(context, parsed.value.session_id);
925
+ return result.ok ? { ok: true, value: result.value } : result;
926
+ }
820
927
  if (subcommand === "id" || subcommand === "show" || subcommand === "close") {
821
928
  if (subcommand === "id") {
822
929
  const parsed = noOptions(rest);
@@ -827,13 +934,27 @@ async function executeCommand(commandArguments, dependencies) {
827
934
  return selected;
828
935
  return { ok: true, value: { session_id: selected.value.session_id } };
829
936
  }
830
- const parsed = parseOptions(rest, new Set(subcommand === "close" ? ["--session", "--integrated-revision"] : ["--session"]));
937
+ const parsed = parseTargetedOptions(rest, new Set(subcommand === "close"
938
+ ? ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"]
939
+ : ["--session"]));
831
940
  if (!parsed.ok)
832
941
  return parsed;
833
942
  if (subcommand === "close") {
943
+ const hasFetchRemote = parsed.value.fetch_remote !== null;
944
+ const hasFetchBranch = parsed.value.fetch_branch !== null;
945
+ if (hasFetchRemote !== hasFetchBranch) {
946
+ return failure(usageError("INVALID_ARGUMENT", "--fetch-remote and --fetch-branch must be supplied together for an explicit integration fetch."));
947
+ }
948
+ if ((hasFetchRemote || hasFetchBranch) && parsed.value.integrated_revision === null) {
949
+ return failure(usageError("MISSING_ARGUMENT", "Explicit integration fetch requires --integrated-revision <full-sha>.", {
950
+ option: "--integrated-revision",
951
+ }));
952
+ }
834
953
  const closeOptions = {
835
954
  session_id: parsed.value.session_id,
836
955
  integrated_revision: parsed.value.integrated_revision,
956
+ fetch_remote: parsed.value.fetch_remote,
957
+ fetch_branch: parsed.value.fetch_branch,
837
958
  };
838
959
  const selected = await dependencies.backend.closeSession(context, closeOptions);
839
960
  return selected.ok ? { ok: true, value: selected.value } : selected;
@@ -885,7 +1006,7 @@ async function executeCommand(commandArguments, dependencies) {
885
1006
  return result.ok ? { ok: true, value: result.value } : result;
886
1007
  }
887
1008
  if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
888
- const parsed = parseOptions(rest, new Set(["--session"]));
1009
+ const parsed = parseTargetedOptions(rest, new Set(["--session"]));
889
1010
  if (!parsed.ok)
890
1011
  return parsed;
891
1012
  if (dependencies.backend.listClaims === undefined)
@@ -894,7 +1015,7 @@ async function executeCommand(commandArguments, dependencies) {
894
1015
  return result.ok ? { ok: true, value: result.value } : result;
895
1016
  }
896
1017
  if (resourceSubcommand === "release") {
897
- const parsed = parseOptions(rest, new Set(["--session", "--claim-id"]));
1018
+ const parsed = parseTargetedOptions(rest, new Set(["--session", "--claim-id"]));
898
1019
  if (!parsed.ok)
899
1020
  return parsed;
900
1021
  if (dependencies.backend.releaseClaims === undefined)
@@ -1151,6 +1272,11 @@ function sessionDiagnosticCapabilityUnavailable() {
1151
1272
  operation: "session.inspect",
1152
1273
  }));
1153
1274
  }
1275
+ function sessionDiscardCapabilityUnavailable() {
1276
+ return failure(new DomainError("BACKEND_UNAVAILABLE", "Session discard capability is not available.", {
1277
+ operation: "session.discard",
1278
+ }));
1279
+ }
1154
1280
  function repositoryEvidenceCapabilityUnavailable(operation) {
1155
1281
  return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
1156
1282
  }