nawabari 0.6.1 → 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)",
@@ -398,7 +519,18 @@ function helpPayload(spec) {
398
519
  session_targeting: {
399
520
  canonical: "--session <id>",
400
521
  positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
401
- 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
+ ],
402
534
  ambiguity: "supplying both positional and --session is rejected",
403
535
  discard_requires_explicit_target: true,
404
536
  },
@@ -668,6 +800,213 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
668
800
  }
669
801
  return parsed;
670
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
+ }
671
1010
  /**
672
1011
  * `update` accepts a complete desired claim set as repeated
673
1012
  * `--resource <path> --mode <mode>` pairs. Pairing is by strict local
@@ -676,7 +1015,7 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
676
1015
  * parallel-array position, so argv order can never associate a resource
677
1016
  * with the wrong mode.
678
1017
  */
679
- function parseClaimReplacementPairs(arguments_) {
1018
+ function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true) {
680
1019
  const positional = splitPositionalSessionTarget(arguments_);
681
1020
  if (!positional.ok)
682
1021
  return positional;
@@ -684,16 +1023,34 @@ function parseClaimReplacementPairs(arguments_) {
684
1023
  let repository = null;
685
1024
  const pairs = [];
686
1025
  let pendingResource = null;
1026
+ let concurrencyState = {
1027
+ expected_claim_set_generation: null,
1028
+ force: false,
1029
+ };
687
1030
  sessionId = positional.value.sessionId;
688
1031
  const targetArguments = positional.value.arguments;
689
1032
  for (let index = 0; index < targetArguments.length; index += 1) {
690
1033
  const { name, inlineValue } = optionParts(targetArguments[index]);
691
- 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)) {
692
1040
  return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
693
1041
  }
694
1042
  if (pendingResource !== null && name !== "--mode") {
695
1043
  return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
696
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
+ }
697
1054
  const value = inlineValue ?? targetArguments[index + 1];
698
1055
  if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
699
1056
  return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
@@ -726,7 +1083,19 @@ function parseClaimReplacementPairs(arguments_) {
726
1083
  if (pairs.length === 0) {
727
1084
  return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
728
1085
  }
729
- 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
+ };
730
1099
  }
731
1100
  /**
732
1101
  * `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
@@ -735,7 +1104,7 @@ function parseClaimReplacementPairs(arguments_) {
735
1104
  * first pair (last-wins).
736
1105
  */
737
1106
  function parseSingleClaimPair(arguments_) {
738
- const parsed = parseClaimReplacementPairs(arguments_);
1107
+ const parsed = parseClaimReplacementPairs(arguments_, false);
739
1108
  if (!parsed.ok)
740
1109
  return parsed;
741
1110
  if (parsed.value.pairs.length > 1) {
@@ -806,6 +1175,40 @@ function sessionContext(cwd) {
806
1175
  async function resolveSelectedSession(backend, context, sessionId) {
807
1176
  return sessionId === null ? backend.resolveCurrentSession(context) : backend.getSession(context, sessionId);
808
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
+ }
809
1212
  async function executeCommand(commandArguments, dependencies) {
810
1213
  const [command, subcommand, ...rest] = commandArguments;
811
1214
  const context = sessionContext(dependencies.cwd);
@@ -832,6 +1235,9 @@ async function executeCommand(commandArguments, dependencies) {
832
1235
  return parsed;
833
1236
  if (dependencies.backend.updateClaims === undefined)
834
1237
  return claimCapabilityUnavailable(subcommand);
1238
+ const concurrency = parsed.value.force
1239
+ ? { force: true }
1240
+ : { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
835
1241
  const result = await dependencies.backend.updateClaims(context, {
836
1242
  session_id: parsed.value.session_id,
837
1243
  repository: parsed.value.repository,
@@ -839,9 +1245,16 @@ async function executeCommand(commandArguments, dependencies) {
839
1245
  resource: pair.resource,
840
1246
  mode: pair.mode,
841
1247
  })),
1248
+ ...concurrency,
842
1249
  });
843
1250
  return result.ok ? { ok: true, value: result.value } : result;
844
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
+ }
845
1258
  if (subcommand === "claims") {
846
1259
  const parsed = parseTargetedOptions(rest, new Set(["--session"]));
847
1260
  if (!parsed.ok)
@@ -860,6 +1273,7 @@ async function executeCommand(commandArguments, dependencies) {
860
1273
  const result = await dependencies.backend.releaseClaims(context, {
861
1274
  session_id: parsed.value.session_id,
862
1275
  claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
1276
+ force: true,
863
1277
  });
864
1278
  return result.ok ? { ok: true, value: result.value } : result;
865
1279
  }
@@ -995,6 +1409,9 @@ async function executeCommand(commandArguments, dependencies) {
995
1409
  return parsed;
996
1410
  if (dependencies.backend.updateClaims === undefined)
997
1411
  return claimCapabilityUnavailable(resourceSubcommand);
1412
+ const concurrency = parsed.value.force
1413
+ ? { force: true }
1414
+ : { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
998
1415
  const result = await dependencies.backend.updateClaims(context, {
999
1416
  session_id: parsed.value.session_id,
1000
1417
  repository: parsed.value.repository,
@@ -1002,9 +1419,16 @@ async function executeCommand(commandArguments, dependencies) {
1002
1419
  resource: pair.resource,
1003
1420
  mode: pair.mode,
1004
1421
  })),
1422
+ ...concurrency,
1005
1423
  });
1006
1424
  return result.ok ? { ok: true, value: result.value } : result;
1007
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
+ }
1008
1432
  if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
1009
1433
  const parsed = parseTargetedOptions(rest, new Set(["--session"]));
1010
1434
  if (!parsed.ok)
@@ -1023,6 +1447,7 @@ async function executeCommand(commandArguments, dependencies) {
1023
1447
  const result = await dependencies.backend.releaseClaims(context, {
1024
1448
  session_id: parsed.value.session_id,
1025
1449
  claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
1450
+ force: true,
1026
1451
  });
1027
1452
  return result.ok ? { ok: true, value: result.value } : result;
1028
1453
  }