nawabari 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { runDoctor } from "./domain/doctor.js";
3
3
  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
+ import { isResourceClaimMode } from "./resource-claims.js";
6
7
  import { createLocalSessionBackend } from "./domain/session-backend.js";
7
8
  import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
8
9
  import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
@@ -116,16 +117,76 @@ const HELP_COMMANDS = [
116
117
  value: "<read|write|exclusive-write>",
117
118
  required: true,
118
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"),
119
124
  option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
120
125
  option("--repository", "Expected repository identity", { value: "<id>" }),
121
126
  ],
122
127
  notes: [
123
- "The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
124
- "on any invalid or conflicting claim the prior set is left unchanged.",
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.",
125
131
  "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
126
132
  "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
127
133
  ],
128
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
+ },
129
190
  {
130
191
  name: "session claims",
131
192
  summary: "List canonical resource claims",
@@ -169,16 +230,76 @@ const HELP_COMMANDS = [
169
230
  value: "<read|write|exclusive-write>",
170
231
  required: true,
171
232
  }),
233
+ option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
234
+ value: "<non-negative-integer>",
235
+ }),
236
+ option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
172
237
  option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
173
238
  option("--repository", "Expected repository identity", { value: "<id>" }),
174
239
  ],
175
240
  notes: [
176
- "The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
177
- "on any invalid or conflicting claim the prior set is left unchanged.",
241
+ "The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
242
+ "use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
243
+ "or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
178
244
  "Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
179
245
  "Target grammar matches session update: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
180
246
  ],
181
247
  },
248
+ {
249
+ name: "resource mutate",
250
+ summary: "Atomically apply exact-resource claim additions, changes, and releases (alias)",
251
+ usage: `${CLI_NAME} resource mutate [<session>|--session <id>] [--repository <id>] ` +
252
+ `(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
253
+ `(--if-generation <non-negative-safe-int> | --force)`,
254
+ options: [
255
+ option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
256
+ option("--mode", "Mode for the immediately preceding --upsert-resource", {
257
+ value: "<read|write|exclusive-write>",
258
+ }),
259
+ option("--release-resource", "Exact repository-relative resource to release; repeatable", {
260
+ value: "<path-or-glob>",
261
+ }),
262
+ option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
263
+ value: "<non-negative-safe-int>",
264
+ }),
265
+ option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
266
+ option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
267
+ option("--repository", "Expected repository identity", { value: "<id>" }),
268
+ ],
269
+ notes: [
270
+ "Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
271
+ "Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
272
+ "Target grammar matches session mutate: optional first positional <session> is an alias for --session <id>; do not supply both.",
273
+ ],
274
+ },
275
+ {
276
+ name: "resource transition",
277
+ summary: "Atomically transition one exact resource claim mode (alias)",
278
+ usage: `${CLI_NAME} resource transition [<session>|--session <id>] [--repository <id>] ` +
279
+ `--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
280
+ `(--if-generation <non-negative-safe-int> | --force)`,
281
+ options: [
282
+ option("--resource", "Exact repository-relative resource to acquire or change", {
283
+ value: "<path-or-glob>",
284
+ required: true,
285
+ }),
286
+ option("--mode", "Target mode for the immediately preceding --resource", {
287
+ value: "<read|write|exclusive-write>",
288
+ required: true,
289
+ }),
290
+ option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
291
+ value: "<non-negative-safe-int>",
292
+ }),
293
+ option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
294
+ option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
295
+ option("--repository", "Expected repository identity", { value: "<id>" }),
296
+ ],
297
+ notes: [
298
+ "Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
299
+ "Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
300
+ "The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
301
+ ],
302
+ },
182
303
  {
183
304
  name: "resource list",
184
305
  summary: "List canonical resource claims (alias)",
@@ -203,14 +324,20 @@ const HELP_COMMANDS = [
203
324
  {
204
325
  name: "session close",
205
326
  summary: "Close the current or selected session",
206
- usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>]`,
327
+ usage: `${CLI_NAME} session close [<session-id>|--session <id>] [--integrated-revision <rev>] [--fetch-remote <name> --fetch-branch <branch>]`,
207
328
  options: [
208
329
  option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
209
330
  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>" }),
331
+ option("--fetch-remote", "Explicitly fetch one remote integration branch into a disposable proof ref", {
332
+ value: "<name>",
333
+ }),
334
+ option("--fetch-branch", "Integration branch to fetch; requires --fetch-remote", { value: "<branch>" }),
210
335
  ],
211
336
  notes: [
212
337
  "Ordinary ancestry-based close remains the cheap/default path and requires no flags.",
213
- "Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
338
+ "Network access is never implicit; --fetch-remote and --fetch-branch are accepted only with a full lowercase --integrated-revision SHA.",
339
+ "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.",
340
+ "Nawabari never calls provider APIs such as GitHub; only explicit --fetch-remote/--fetch-branch options connect to the configured Git remote.",
214
341
  "Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
215
342
  ],
216
343
  },
@@ -392,7 +519,18 @@ function helpPayload(spec) {
392
519
  session_targeting: {
393
520
  canonical: "--session <id>",
394
521
  positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
395
- commands: ["show", "inspect", "claim", "claims", "release", "update", "close", "discard"],
522
+ commands: [
523
+ "show",
524
+ "inspect",
525
+ "claim",
526
+ "claims",
527
+ "release",
528
+ "update",
529
+ "mutate",
530
+ "transition",
531
+ "close",
532
+ "discard",
533
+ ],
396
534
  ambiguity: "supplying both positional and --session is rejected",
397
535
  discard_requires_explicit_target: true,
398
536
  },
@@ -532,6 +670,8 @@ function parseOptions(arguments_, allowed) {
532
670
  max_bytes: null,
533
671
  max_hunks: null,
534
672
  integrated_revision: null,
673
+ fetch_remote: null,
674
+ fetch_branch: null,
535
675
  };
536
676
  let dryRun = false;
537
677
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -619,6 +759,10 @@ function parseOptions(arguments_, allowed) {
619
759
  options.max_hunks = value;
620
760
  else if (name === "--integrated-revision")
621
761
  options.integrated_revision = value;
762
+ else if (name === "--fetch-remote")
763
+ options.fetch_remote = value;
764
+ else if (name === "--fetch-branch")
765
+ options.fetch_branch = value;
622
766
  }
623
767
  if (options.apply && dryRun) {
624
768
  return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
@@ -656,6 +800,213 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
656
800
  }
657
801
  return parsed;
658
802
  }
803
+ const CLAIM_CONCURRENCY_OPTION_NAMES = new Set(["--if-generation", "--force"]);
804
+ function isClaimConcurrencyOption(name) {
805
+ return CLAIM_CONCURRENCY_OPTION_NAMES.has(name);
806
+ }
807
+ /** Shared #176 CAS/force option vocabulary and value consumption. */
808
+ function consumeClaimConcurrencyOption(name, inlineValue, nextValue, state) {
809
+ if (name === "--force") {
810
+ if (inlineValue !== null) {
811
+ return failure(usageError("INVALID_ARGUMENT", "--force does not accept a value.", { option: name }));
812
+ }
813
+ if (state.force) {
814
+ return failure(usageError("INVALID_ARGUMENT", "--force may be supplied only once.", { option: name }));
815
+ }
816
+ return { ok: true, value: { state: { ...state, force: true }, consumed_next_value: false } };
817
+ }
818
+ const value = inlineValue ?? nextValue;
819
+ const negativeGeneration = /^-\d+$/u.test(value ?? "");
820
+ if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-") && !negativeGeneration)) {
821
+ return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
822
+ }
823
+ if (state.expected_claim_set_generation !== null) {
824
+ return failure(usageError("INVALID_ARGUMENT", "--if-generation may be supplied only once.", { option: name }));
825
+ }
826
+ if (!/^\d+$/u.test(value)) {
827
+ return failure(usageError("INVALID_ARGUMENT", "--if-generation requires a non-negative safe integer.", {
828
+ option: name,
829
+ value,
830
+ }));
831
+ }
832
+ const parsedGeneration = Number(value);
833
+ if (!Number.isSafeInteger(parsedGeneration)) {
834
+ return failure(usageError("INVALID_ARGUMENT", "--if-generation requires a non-negative safe integer.", {
835
+ option: name,
836
+ value,
837
+ }));
838
+ }
839
+ return {
840
+ ok: true,
841
+ value: {
842
+ state: { ...state, expected_claim_set_generation: parsedGeneration },
843
+ consumed_next_value: inlineValue === null,
844
+ },
845
+ };
846
+ }
847
+ function finalizeClaimConcurrency(state, requireConcurrencyIntent) {
848
+ if (!requireConcurrencyIntent && (state.force || state.expected_claim_set_generation !== null)) {
849
+ return failure(usageError("INVALID_ARGUMENT", "session claim does not accept --if-generation or --force.", {
850
+ options: ["--if-generation", "--force"],
851
+ }));
852
+ }
853
+ if (requireConcurrencyIntent && state.force && state.expected_claim_set_generation !== null) {
854
+ return failure(usageError("INVALID_ARGUMENT", "Specify exactly one of --if-generation or --force, not both.", {
855
+ options: ["--if-generation", "--force"],
856
+ }));
857
+ }
858
+ if (requireConcurrencyIntent && !state.force && state.expected_claim_set_generation === null) {
859
+ return failure(usageError("MISSING_ARGUMENT", "Exactly one of --if-generation or --force is required.", {
860
+ options: ["--if-generation", "--force"],
861
+ }));
862
+ }
863
+ return { ok: true, value: state };
864
+ }
865
+ /**
866
+ * Parse the public atomic delta grammar without interpreting resource identity.
867
+ * Resource canonicalization, duplicate/conflict authority, and all mutation
868
+ * semantics remain in the domain/backend primitive.
869
+ */
870
+ function parseClaimDeltaMutation(arguments_) {
871
+ const positional = splitPositionalSessionTarget(arguments_);
872
+ if (!positional.ok)
873
+ return positional;
874
+ let sessionId = positional.value.sessionId;
875
+ let repository = null;
876
+ const deltas = [];
877
+ let pendingUpsertResource = null;
878
+ let concurrencyState = {
879
+ expected_claim_set_generation: null,
880
+ force: false,
881
+ };
882
+ const recognized = new Set([
883
+ "--session",
884
+ "--repository",
885
+ "--upsert-resource",
886
+ "--mode",
887
+ "--release-resource",
888
+ ...CLAIM_CONCURRENCY_OPTION_NAMES,
889
+ ]);
890
+ 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 }));
891
+ const targetArguments = positional.value.arguments;
892
+ for (let index = 0; index < targetArguments.length; index += 1) {
893
+ const { name, inlineValue } = optionParts(targetArguments[index]);
894
+ if (!recognized.has(name)) {
895
+ if (pendingUpsertResource !== null)
896
+ return rejectPending(name);
897
+ return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
898
+ }
899
+ if (pendingUpsertResource !== null && name !== "--mode")
900
+ return rejectPending(name);
901
+ if (isClaimConcurrencyOption(name)) {
902
+ const consumed = consumeClaimConcurrencyOption(name, inlineValue, targetArguments[index + 1], concurrencyState);
903
+ if (!consumed.ok)
904
+ return consumed;
905
+ concurrencyState = consumed.value.state;
906
+ if (consumed.value.consumed_next_value)
907
+ index += 1;
908
+ continue;
909
+ }
910
+ const value = inlineValue ?? targetArguments[index + 1];
911
+ if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
912
+ return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
913
+ }
914
+ if (inlineValue === null)
915
+ index += 1;
916
+ if (name === "--session") {
917
+ if (sessionId !== null) {
918
+ return failure(usageError("INVALID_ARGUMENT", "Specify a session target either positionally or with --session, not both.", {
919
+ option: "--session",
920
+ }));
921
+ }
922
+ sessionId = value;
923
+ }
924
+ else if (name === "--repository") {
925
+ repository = value;
926
+ }
927
+ else if (name === "--upsert-resource") {
928
+ pendingUpsertResource = value;
929
+ }
930
+ else if (name === "--release-resource") {
931
+ deltas.push({ kind: "release", resource: value });
932
+ }
933
+ else {
934
+ if (pendingUpsertResource === null) {
935
+ return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --upsert-resource.", {
936
+ option: "--mode",
937
+ }));
938
+ }
939
+ if (!isResourceClaimMode(value)) {
940
+ return failure(usageError("INVALID_ARGUMENT", "--mode requires read, write, or exclusive-write.", {
941
+ option: "--mode",
942
+ value,
943
+ }));
944
+ }
945
+ deltas.push({
946
+ kind: "upsert",
947
+ resource: pendingUpsertResource,
948
+ mode: value,
949
+ });
950
+ pendingUpsertResource = null;
951
+ }
952
+ }
953
+ if (pendingUpsertResource !== null) {
954
+ return failure(usageError("MISSING_ARGUMENT", "--upsert-resource must be immediately followed by --mode.", {
955
+ option: "--mode",
956
+ }));
957
+ }
958
+ if (deltas.length === 0) {
959
+ return failure(usageError("MISSING_ARGUMENT", "At least one --upsert-resource or --release-resource is required.", {
960
+ options: ["--upsert-resource", "--release-resource"],
961
+ }));
962
+ }
963
+ const concurrency = finalizeClaimConcurrency(concurrencyState, true);
964
+ if (!concurrency.ok)
965
+ return concurrency;
966
+ return {
967
+ ok: true,
968
+ value: {
969
+ session_id: sessionId,
970
+ repository,
971
+ deltas,
972
+ expected_claim_set_generation: concurrency.value.expected_claim_set_generation,
973
+ force: concurrency.value.force,
974
+ },
975
+ };
976
+ }
977
+ /**
978
+ * Parse the one-resource transition convenience grammar. The underlying
979
+ * mutation remains the canonical multi-delta primitive; this parser only
980
+ * constrains its public projection to exactly one upsert pair.
981
+ */
982
+ function parseClaimTransition(arguments_) {
983
+ const parsed = parseClaimReplacementPairs(arguments_);
984
+ if (!parsed.ok)
985
+ return parsed;
986
+ if (parsed.value.pairs.length !== 1) {
987
+ return failure(usageError("INVALID_ARGUMENT", "transition accepts exactly one --resource/--mode pair.", {
988
+ pair_count: parsed.value.pairs.length,
989
+ }));
990
+ }
991
+ const pair = parsed.value.pairs[0];
992
+ if (pair === undefined || !isResourceClaimMode(pair.mode)) {
993
+ return failure(usageError("INVALID_ARGUMENT", "--mode requires read, write, or exclusive-write.", {
994
+ option: "--mode",
995
+ value: pair?.mode ?? null,
996
+ }));
997
+ }
998
+ return {
999
+ ok: true,
1000
+ value: {
1001
+ session_id: parsed.value.session_id,
1002
+ repository: parsed.value.repository,
1003
+ resource: pair.resource,
1004
+ mode: pair.mode,
1005
+ expected_claim_set_generation: parsed.value.expected_claim_set_generation,
1006
+ force: parsed.value.force,
1007
+ },
1008
+ };
1009
+ }
659
1010
  /**
660
1011
  * `update` accepts a complete desired claim set as repeated
661
1012
  * `--resource <path> --mode <mode>` pairs. Pairing is by strict local
@@ -664,7 +1015,7 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
664
1015
  * parallel-array position, so argv order can never associate a resource
665
1016
  * with the wrong mode.
666
1017
  */
667
- function parseClaimReplacementPairs(arguments_) {
1018
+ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true) {
668
1019
  const positional = splitPositionalSessionTarget(arguments_);
669
1020
  if (!positional.ok)
670
1021
  return positional;
@@ -672,16 +1023,34 @@ function parseClaimReplacementPairs(arguments_) {
672
1023
  let repository = null;
673
1024
  const pairs = [];
674
1025
  let pendingResource = null;
1026
+ let concurrencyState = {
1027
+ expected_claim_set_generation: null,
1028
+ force: false,
1029
+ };
675
1030
  sessionId = positional.value.sessionId;
676
1031
  const targetArguments = positional.value.arguments;
677
1032
  for (let index = 0; index < targetArguments.length; index += 1) {
678
1033
  const { name, inlineValue } = optionParts(targetArguments[index]);
679
- if (name !== "--session" && name !== "--repository" && name !== "--resource" && name !== "--mode") {
1034
+ const isConcurrencyOption = isClaimConcurrencyOption(name);
1035
+ if (name !== "--session" &&
1036
+ name !== "--repository" &&
1037
+ name !== "--resource" &&
1038
+ name !== "--mode" &&
1039
+ !(requireConcurrencyIntent && isConcurrencyOption)) {
680
1040
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
681
1041
  }
682
1042
  if (pendingResource !== null && name !== "--mode") {
683
1043
  return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
684
1044
  }
1045
+ if (requireConcurrencyIntent && isConcurrencyOption) {
1046
+ const consumed = consumeClaimConcurrencyOption(name, inlineValue, targetArguments[index + 1], concurrencyState);
1047
+ if (!consumed.ok)
1048
+ return consumed;
1049
+ concurrencyState = consumed.value.state;
1050
+ if (consumed.value.consumed_next_value)
1051
+ index += 1;
1052
+ continue;
1053
+ }
685
1054
  const value = inlineValue ?? targetArguments[index + 1];
686
1055
  if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
687
1056
  return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
@@ -714,7 +1083,19 @@ function parseClaimReplacementPairs(arguments_) {
714
1083
  if (pairs.length === 0) {
715
1084
  return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
716
1085
  }
717
- return { ok: true, value: { session_id: sessionId, repository, pairs } };
1086
+ const concurrency = finalizeClaimConcurrency(concurrencyState, requireConcurrencyIntent);
1087
+ if (!concurrency.ok)
1088
+ return concurrency;
1089
+ return {
1090
+ ok: true,
1091
+ value: {
1092
+ session_id: sessionId,
1093
+ repository,
1094
+ pairs,
1095
+ expected_claim_set_generation: concurrency.value.expected_claim_set_generation,
1096
+ force: concurrency.value.force,
1097
+ },
1098
+ };
718
1099
  }
719
1100
  /**
720
1101
  * `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
@@ -723,7 +1104,7 @@ function parseClaimReplacementPairs(arguments_) {
723
1104
  * first pair (last-wins).
724
1105
  */
725
1106
  function parseSingleClaimPair(arguments_) {
726
- const parsed = parseClaimReplacementPairs(arguments_);
1107
+ const parsed = parseClaimReplacementPairs(arguments_, false);
727
1108
  if (!parsed.ok)
728
1109
  return parsed;
729
1110
  if (parsed.value.pairs.length > 1) {
@@ -794,6 +1175,40 @@ function sessionContext(cwd) {
794
1175
  async function resolveSelectedSession(backend, context, sessionId) {
795
1176
  return sessionId === null ? backend.resolveCurrentSession(context) : backend.getSession(context, sessionId);
796
1177
  }
1178
+ async function executeClaimDeltaMutation(arguments_, backend, context, operation) {
1179
+ const parsed = parseClaimDeltaMutation(arguments_);
1180
+ if (!parsed.ok)
1181
+ return parsed;
1182
+ if (backend.applyClaimDeltas === undefined)
1183
+ return claimCapabilityUnavailable(operation);
1184
+ const concurrency = parsed.value.force
1185
+ ? { force: true }
1186
+ : { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
1187
+ const result = await backend.applyClaimDeltas(context, {
1188
+ session_id: parsed.value.session_id,
1189
+ repository: parsed.value.repository,
1190
+ deltas: parsed.value.deltas,
1191
+ ...concurrency,
1192
+ });
1193
+ return result.ok ? { ok: true, value: result.value } : result;
1194
+ }
1195
+ async function executeClaimTransition(arguments_, backend, context, operation) {
1196
+ const parsed = parseClaimTransition(arguments_);
1197
+ if (!parsed.ok)
1198
+ return parsed;
1199
+ if (backend.applyClaimDeltas === undefined)
1200
+ return claimCapabilityUnavailable(operation);
1201
+ const concurrency = parsed.value.force
1202
+ ? { force: true }
1203
+ : { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
1204
+ const result = await backend.applyClaimDeltas(context, {
1205
+ session_id: parsed.value.session_id,
1206
+ repository: parsed.value.repository,
1207
+ deltas: [{ kind: "upsert", resource: parsed.value.resource, mode: parsed.value.mode }],
1208
+ ...concurrency,
1209
+ });
1210
+ return result.ok ? { ok: true, value: result.value } : result;
1211
+ }
797
1212
  async function executeCommand(commandArguments, dependencies) {
798
1213
  const [command, subcommand, ...rest] = commandArguments;
799
1214
  const context = sessionContext(dependencies.cwd);
@@ -820,6 +1235,9 @@ async function executeCommand(commandArguments, dependencies) {
820
1235
  return parsed;
821
1236
  if (dependencies.backend.updateClaims === undefined)
822
1237
  return claimCapabilityUnavailable(subcommand);
1238
+ const concurrency = parsed.value.force
1239
+ ? { force: true }
1240
+ : { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
823
1241
  const result = await dependencies.backend.updateClaims(context, {
824
1242
  session_id: parsed.value.session_id,
825
1243
  repository: parsed.value.repository,
@@ -827,9 +1245,16 @@ async function executeCommand(commandArguments, dependencies) {
827
1245
  resource: pair.resource,
828
1246
  mode: pair.mode,
829
1247
  })),
1248
+ ...concurrency,
830
1249
  });
831
1250
  return result.ok ? { ok: true, value: result.value } : result;
832
1251
  }
1252
+ if (subcommand === "mutate") {
1253
+ return executeClaimDeltaMutation(rest, dependencies.backend, context, "session mutate");
1254
+ }
1255
+ if (subcommand === "transition") {
1256
+ return executeClaimTransition(rest, dependencies.backend, context, "session transition");
1257
+ }
833
1258
  if (subcommand === "claims") {
834
1259
  const parsed = parseTargetedOptions(rest, new Set(["--session"]));
835
1260
  if (!parsed.ok)
@@ -848,6 +1273,7 @@ async function executeCommand(commandArguments, dependencies) {
848
1273
  const result = await dependencies.backend.releaseClaims(context, {
849
1274
  session_id: parsed.value.session_id,
850
1275
  claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
1276
+ force: true,
851
1277
  });
852
1278
  return result.ok ? { ok: true, value: result.value } : result;
853
1279
  }
@@ -922,13 +1348,27 @@ async function executeCommand(commandArguments, dependencies) {
922
1348
  return selected;
923
1349
  return { ok: true, value: { session_id: selected.value.session_id } };
924
1350
  }
925
- const parsed = parseTargetedOptions(rest, new Set(subcommand === "close" ? ["--session", "--integrated-revision"] : ["--session"]));
1351
+ const parsed = parseTargetedOptions(rest, new Set(subcommand === "close"
1352
+ ? ["--session", "--integrated-revision", "--fetch-remote", "--fetch-branch"]
1353
+ : ["--session"]));
926
1354
  if (!parsed.ok)
927
1355
  return parsed;
928
1356
  if (subcommand === "close") {
1357
+ const hasFetchRemote = parsed.value.fetch_remote !== null;
1358
+ const hasFetchBranch = parsed.value.fetch_branch !== null;
1359
+ if (hasFetchRemote !== hasFetchBranch) {
1360
+ return failure(usageError("INVALID_ARGUMENT", "--fetch-remote and --fetch-branch must be supplied together for an explicit integration fetch."));
1361
+ }
1362
+ if ((hasFetchRemote || hasFetchBranch) && parsed.value.integrated_revision === null) {
1363
+ return failure(usageError("MISSING_ARGUMENT", "Explicit integration fetch requires --integrated-revision <full-sha>.", {
1364
+ option: "--integrated-revision",
1365
+ }));
1366
+ }
929
1367
  const closeOptions = {
930
1368
  session_id: parsed.value.session_id,
931
1369
  integrated_revision: parsed.value.integrated_revision,
1370
+ fetch_remote: parsed.value.fetch_remote,
1371
+ fetch_branch: parsed.value.fetch_branch,
932
1372
  };
933
1373
  const selected = await dependencies.backend.closeSession(context, closeOptions);
934
1374
  return selected.ok ? { ok: true, value: selected.value } : selected;
@@ -969,6 +1409,9 @@ async function executeCommand(commandArguments, dependencies) {
969
1409
  return parsed;
970
1410
  if (dependencies.backend.updateClaims === undefined)
971
1411
  return claimCapabilityUnavailable(resourceSubcommand);
1412
+ const concurrency = parsed.value.force
1413
+ ? { force: true }
1414
+ : { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
972
1415
  const result = await dependencies.backend.updateClaims(context, {
973
1416
  session_id: parsed.value.session_id,
974
1417
  repository: parsed.value.repository,
@@ -976,9 +1419,16 @@ async function executeCommand(commandArguments, dependencies) {
976
1419
  resource: pair.resource,
977
1420
  mode: pair.mode,
978
1421
  })),
1422
+ ...concurrency,
979
1423
  });
980
1424
  return result.ok ? { ok: true, value: result.value } : result;
981
1425
  }
1426
+ if (resourceSubcommand === "mutate") {
1427
+ return executeClaimDeltaMutation(rest, dependencies.backend, context, "resource mutate");
1428
+ }
1429
+ if (resourceSubcommand === "transition") {
1430
+ return executeClaimTransition(rest, dependencies.backend, context, "resource transition");
1431
+ }
982
1432
  if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
983
1433
  const parsed = parseTargetedOptions(rest, new Set(["--session"]));
984
1434
  if (!parsed.ok)
@@ -997,6 +1447,7 @@ async function executeCommand(commandArguments, dependencies) {
997
1447
  const result = await dependencies.backend.releaseClaims(context, {
998
1448
  session_id: parsed.value.session_id,
999
1449
  claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
1450
+ force: true,
1000
1451
  });
1001
1452
  return result.ok ? { ok: true, value: result.value } : result;
1002
1453
  }