@astrosheep/keiyaku 4.5.0 → 4.5.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 (59) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +4 -2
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +9 -3
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +16 -5
  5. package/build/integrations/pi/keiyaku.ts +159 -0
  6. package/build/src/akuma/abort.js +9 -9
  7. package/build/src/akuma/akuma-handle.d.ts +4 -2
  8. package/build/src/akuma/akuma-handle.js +14 -2
  9. package/build/src/akuma/akuma-product.d.ts +8 -2
  10. package/build/src/akuma/akuma-product.js +9 -4
  11. package/build/src/akuma/body.d.ts +4 -0
  12. package/build/src/akuma/body.js +21 -14
  13. package/build/src/akuma/heart/index.js +7 -3
  14. package/build/src/akuma/heart/rows.d.ts +1 -1
  15. package/build/src/akuma/heart/rows.js +3 -3
  16. package/build/src/akuma/heart/storage.d.ts +1 -0
  17. package/build/src/akuma/heart/storage.js +28 -6
  18. package/build/src/akuma/identity.d.ts +1 -0
  19. package/build/src/akuma/identity.js +7 -0
  20. package/build/src/akuma/index.d.ts +1 -1
  21. package/build/src/akuma/projection.d.ts +18 -2
  22. package/build/src/akuma/projection.js +11 -1
  23. package/build/src/akuma/providers/claude/index.js +68 -37
  24. package/build/src/akuma/publication.js +3 -0
  25. package/build/src/akuma/request-lifecycle.d.ts +45 -0
  26. package/build/src/akuma/request-lifecycle.js +171 -0
  27. package/build/src/akuma/request-serve.d.ts +9 -32
  28. package/build/src/akuma/request-serve.js +47 -173
  29. package/build/src/akuma/request-wire.js +1 -1
  30. package/build/src/akuma/requests.js +3 -2
  31. package/build/src/cli/commands/akuma-invoke.d.ts +9 -0
  32. package/build/src/cli/commands/akuma-invoke.js +9 -6
  33. package/build/src/cli/commands/akuma.d.ts +1 -0
  34. package/build/src/cli/commands/akuma.js +16 -6
  35. package/build/src/cli/commands/contract.d.ts +3 -2
  36. package/build/src/cli/commands/contract.js +5 -2
  37. package/build/src/cli/commands/status-set.d.ts +4 -0
  38. package/build/src/cli/commands/status-set.js +61 -0
  39. package/build/src/cli/invoke.js +5 -0
  40. package/build/src/cli/parse.js +1 -1
  41. package/build/src/cli/render/akuma-activity.js +11 -0
  42. package/build/src/cli/render/akuma.js +7 -0
  43. package/build/src/cli/render/status-set.d.ts +5 -0
  44. package/build/src/cli/render/status-set.js +14 -0
  45. package/build/src/cli/render/text.js +3 -0
  46. package/build/src/cli/result.d.ts +15 -1
  47. package/build/src/cli/runtime.js +2 -0
  48. package/build/src/cli/square-edge.d.ts +2 -1
  49. package/build/src/cli/square-edge.js +60 -33
  50. package/build/src/contract-worktree.js +18 -5
  51. package/build/src/library/akuma-creation.d.ts +1 -1
  52. package/build/src/library/akuma-creation.js +6 -2
  53. package/build/src/library/fleet.d.ts +12 -1
  54. package/build/src/library/fleet.js +21 -5
  55. package/build/src/library/repo.js +3 -1
  56. package/build/src/runtime/proc/windows-launch.exe +0 -0
  57. package/build/src/world.d.ts +2 -0
  58. package/build/src/world.js +4 -0
  59. package/package.json +8 -3
@@ -1,19 +1,26 @@
1
- import { access, mkdir, readFile, readdir, rm } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { abortableDelay } from "./abort.js";
4
- import { HeldAkumaLeash, admitRequest, readNonterminalRequests, readRequest, readSoul, refuseRequest, reserveRequest, serveRequest, serveUpstreamRequest, voidRequest, } from "./heart/index.js";
5
- import { pathsForAkuId, worldRootForAkumaPaths } from "./identity.js";
6
- import { BIRTH_TIMEOUT_MS, publishAkuma } from "./publication.js";
7
- import { atomicJson, canonicalAkuId, decodeClaim, decodeRecipe, receiptPath, } from "./request-wire.js";
1
+ import { admitRequest, readRequest, refuseRequest, reserveRequest, serveRequest as serveChildRequest, serveUpstreamRequest, voidRequest, } from "./heart/index.js";
2
+ import { worldRootForAkumaPaths } from "./identity.js";
3
+ import { publishAkuma } from "./publication.js";
4
+ import { atomicJson, canonicalAkuId, decodeRecipe, receiptPath, } from "./request-wire.js";
8
5
  import { isTaskMutationAction } from "../task/mutation.js";
9
6
  import { executeRequest } from "./request-execution.js";
10
- const POLL_MS = 100;
7
+ import { BodyRequestPump as LifecycleBodyRequestPump, } from "./request-lifecycle.js";
8
+ export { clearBodyRequestTransport, settleBodyRequests } from "./request-lifecycle.js";
11
9
  function diagnostic(error) {
12
10
  return error instanceof Error ? error.message : String(error);
13
11
  }
14
12
  function isTask(value) {
15
13
  return isTaskMutationAction(value.action);
16
14
  }
15
+ function refusalRecipe(value) {
16
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
17
+ const recipe = value;
18
+ if (Array.isArray(recipe.allowed))
19
+ return recipe;
20
+ return { ...recipe, allowed: [] };
21
+ }
22
+ return { allowed: [] };
23
+ }
17
24
  function acceptedReceipt(fact, service) {
18
25
  return {
19
26
  id: fact.id,
@@ -62,7 +69,7 @@ function receiptFor(fact) {
62
69
  }
63
70
  return null;
64
71
  }
65
- async function projectReceipt(directory, fact, outcome) {
72
+ async function projectReceipt(directory, transportId, fact, outcome) {
66
73
  const receipt = outcome === undefined
67
74
  ? receiptFor(fact)
68
75
  : fact.action === "akuma.call"
@@ -71,29 +78,45 @@ async function projectReceipt(directory, fact, outcome) {
71
78
  if (receipt === null)
72
79
  return;
73
80
  try {
74
- await atomicJson(receiptPath(directory, fact.id), receipt);
81
+ await atomicJson(receiptPath(directory, transportId), receipt);
75
82
  }
76
83
  catch (error) {
77
84
  if (error.code !== "ENOENT")
78
85
  throw error;
79
86
  }
80
87
  }
88
+ async function refuseInvalidCall(input) {
89
+ let fact = await admitRequest(input.paths, {
90
+ ...input.claim,
91
+ recipe: refusalRecipe(input.claim.recipe),
92
+ admittedAt: input.now(),
93
+ });
94
+ const existing = receiptFor(fact);
95
+ if (existing !== null) {
96
+ await projectReceipt(input.directory, input.transportId, fact);
97
+ return;
98
+ }
99
+ fact = await refuseRequest(input.paths, input.claim.id, "invalid akuma.call recipe");
100
+ await projectReceipt(input.directory, input.transportId, fact);
101
+ }
81
102
  async function serveCall(input) {
82
103
  input.signal.throwIfAborted();
83
104
  if (!input.admissionOpen())
84
105
  return;
85
106
  const recipe = decodeRecipe(input.claim.recipe);
86
- if (recipe === null)
107
+ if (recipe === null) {
108
+ await refuseInvalidCall(input);
87
109
  return;
110
+ }
88
111
  let fact = await admitRequest(input.paths, { ...input.claim, recipe, admittedAt: input.now() });
89
112
  const existing = receiptFor(fact);
90
113
  if (existing !== null) {
91
- await projectReceipt(input.directory, fact);
114
+ await projectReceipt(input.directory, input.transportId, fact);
92
115
  return;
93
116
  }
94
117
  if (!input.admissionOpen()) {
95
118
  fact = await voidRequest(input.paths, input.claim.id, "body closed request admission");
96
- await projectReceipt(input.directory, fact);
119
+ await projectReceipt(input.directory, input.transportId, fact);
97
120
  return;
98
121
  }
99
122
  if (fact.action !== "akuma.call" || fact.state !== "admitted") {
@@ -103,7 +126,7 @@ async function serveCall(input) {
103
126
  const world = worldRootForAkumaPaths(input.paths);
104
127
  if (request.world !== world) {
105
128
  fact = await refuseRequest(input.paths, request.id, `request world ${request.world} does not match ${world}`);
106
- await projectReceipt(input.directory, fact);
129
+ await projectReceipt(input.directory, input.transportId, fact);
107
130
  return;
108
131
  }
109
132
  try {
@@ -136,7 +159,7 @@ async function serveCall(input) {
136
159
  });
137
160
  },
138
161
  });
139
- fact = await serveRequest(input.paths, request.id, published.id);
162
+ fact = await serveChildRequest(input.paths, request.id, published.id);
140
163
  }
141
164
  catch (error) {
142
165
  const current = await readRequest(input.paths, request.id);
@@ -147,7 +170,7 @@ async function serveCall(input) {
147
170
  ? await voidRequest(input.paths, request.id, diagnostic(error))
148
171
  : current;
149
172
  }
150
- await projectReceipt(input.directory, fact);
173
+ await projectReceipt(input.directory, input.transportId, fact);
151
174
  }
152
175
  function failure(error) {
153
176
  const value = error !== null && typeof error === "object" ? error : null;
@@ -174,11 +197,11 @@ async function serveUpstream(input) {
174
197
  });
175
198
  const existing = receiptFor(fact);
176
199
  if (existing !== null) {
177
- await projectReceipt(input.directory, fact);
200
+ await projectReceipt(input.directory, input.transportId, fact);
178
201
  return;
179
202
  }
180
203
  if (fact.state === "served") {
181
- await projectReceipt(input.directory, fact, {
204
+ await projectReceipt(input.directory, input.transportId, fact, {
182
205
  kind: "failed",
183
206
  failure: {
184
207
  kind: "failed",
@@ -189,7 +212,7 @@ async function serveUpstream(input) {
189
212
  }
190
213
  if (!input.admissionOpen()) {
191
214
  fact = await voidRequest(input.paths, input.claim.id, "body closed request admission");
192
- await projectReceipt(input.directory, fact);
215
+ await projectReceipt(input.directory, input.transportId, fact);
193
216
  return;
194
217
  }
195
218
  if (fact.state !== "admitted" || fact.action === "akuma.call") {
@@ -207,171 +230,22 @@ async function serveUpstream(input) {
207
230
  catch (error) {
208
231
  outcome = { kind: "failed", failure: failure(error) };
209
232
  fact = await voidRequest(input.paths, request.id, "body failed before serving the request");
210
- await projectReceipt(input.directory, fact);
233
+ await projectReceipt(input.directory, input.transportId, fact);
211
234
  return;
212
235
  }
213
236
  fact =
214
237
  service === undefined
215
238
  ? await voidRequest(input.paths, request.id, "body completed without a durable Contract fact reference")
216
239
  : await serveUpstreamRequest(input.paths, request.id, service);
217
- await projectReceipt(input.directory, fact, outcome);
240
+ await projectReceipt(input.directory, input.transportId, fact, outcome);
218
241
  }
219
- async function serve(input) {
242
+ export async function serveRequest(input) {
220
243
  return input.claim.action === "akuma.call"
221
244
  ? await serveCall({ ...input, claim: input.claim })
222
245
  : await serveUpstream({ ...input, claim: input.claim });
223
246
  }
224
- async function requestFiles(directory) {
225
- try {
226
- return (await readdir(directory)).filter((name) => name.endsWith(".request.json")).sort();
227
- }
228
- catch (error) {
229
- if (error.code === "ENOENT")
230
- return [];
231
- throw error;
232
- }
233
- }
234
- export class BodyRequestPump {
235
- input;
236
- directory;
237
- failure;
238
- closeSignal = new AbortController();
239
- executionSignal = new AbortController();
240
- handled = new Set();
241
- running;
242
- cancelBody;
243
- admissionClosed = false;
244
- constructor(input) {
245
- this.input = input;
246
- this.directory = join(input.paths.directory, "requests", String(input.bodySequence));
247
- this.cancelBody = () => {
248
- this.stopAdmission();
249
- if (!this.executionSignal.signal.aborted) {
250
- this.executionSignal.abort(input.signal.reason);
251
- }
252
- };
253
- input.signal.addEventListener("abort", this.cancelBody, { once: true });
254
- if (input.signal.aborted)
255
- this.cancelBody();
256
- this.running = this.run();
257
- this.failure = this.running.then(() => new Promise(() => { }), (error) => Promise.reject(error));
258
- }
247
+ export class BodyRequestPump extends LifecycleBodyRequestPump {
259
248
  static async open(input) {
260
- await mkdir(join(input.paths.directory, "requests", String(input.bodySequence)), { recursive: true });
261
- return new BodyRequestPump(input);
262
- }
263
- async run() {
264
- while (!this.closeSignal.signal.aborted) {
265
- for (const name of await requestFiles(this.directory)) {
266
- if (this.closeSignal.signal.aborted)
267
- return;
268
- const id = name.slice(0, -".request.json".length);
269
- if (this.handled.has(id))
270
- continue;
271
- const path = join(this.directory, name);
272
- const claim = decodeClaim(await readFile(path, "utf8"), id);
273
- if (claim === null) {
274
- await rm(path, { force: true });
275
- this.handled.add(id);
276
- continue;
277
- }
278
- if (!this.admissionClosed) {
279
- await serve({
280
- directory: this.directory,
281
- claim,
282
- ...this.input,
283
- signal: this.executionSignal.signal,
284
- admissionOpen: () => !this.admissionClosed,
285
- });
286
- }
287
- this.handled.add(id);
288
- }
289
- try {
290
- await abortableDelay(POLL_MS, this.closeSignal.signal);
291
- }
292
- catch (error) {
293
- if (this.closeSignal.signal.aborted)
294
- return;
295
- throw error;
296
- }
297
- }
298
- }
299
- async close() {
300
- this.stopAdmission();
301
- try {
302
- await this.running;
303
- }
304
- finally {
305
- this.input.signal.removeEventListener("abort", this.cancelBody);
306
- await rm(this.directory, { recursive: true, force: true });
307
- }
308
- }
309
- stopAdmission() {
310
- this.admissionClosed = true;
311
- if (!this.closeSignal.signal.aborted) {
312
- this.closeSignal.abort(new Error("Body request pump closed"));
313
- }
314
- }
315
- }
316
- export async function clearBodyRequestTransport(paths) {
317
- await rm(join(paths.directory, "requests"), { recursive: true, force: true });
318
- }
319
- function matchingRequestOrigin(soul, parent, id) {
320
- return soul.origin.kind === "request" && soul.origin.parent === parent && soul.origin.requestId === id;
321
- }
322
- async function settleReservedSoul(paths, parent, request, soul) {
323
- await (matchingRequestOrigin(soul, parent.id, request.id)
324
- ? serveRequest(paths, request.id, request.child)
325
- : voidRequest(paths, request.id, "reserved child origin does not match the request"));
326
- }
327
- async function settleReserved(paths, parent, request, now, signal) {
328
- const childPaths = pathsForAkuId(request.world, request.child);
329
- const deadline = performance.now() + BIRTH_TIMEOUT_MS;
330
- for (;;) {
331
- signal?.throwIfAborted();
332
- if (!(await access(childPaths.directory).then(() => true, () => false))) {
333
- await voidRequest(paths, request.id, "reserved child directory is absent");
334
- return true;
335
- }
336
- const soul = await readSoul(childPaths);
337
- if (soul !== null) {
338
- await settleReservedSoul(paths, parent, request, soul);
339
- return true;
340
- }
341
- const leash = await HeldAkumaLeash.try(childPaths);
342
- if (leash !== null) {
343
- try {
344
- const settled = await readSoul(childPaths);
345
- if (settled !== null) {
346
- await settleReservedSoul(paths, parent, request, settled);
347
- }
348
- else {
349
- await leash.sealIfUnborn(childPaths, {
350
- evidence: "request settlement",
351
- at: now(),
352
- });
353
- await voidRequest(paths, request.id, "reserved child was sealed unborn");
354
- }
355
- }
356
- finally {
357
- leash.release();
358
- }
359
- return true;
360
- }
361
- if (performance.now() >= deadline)
362
- return false;
363
- await abortableDelay(Math.min(POLL_MS, Math.max(0, deadline - performance.now())), signal);
364
- }
365
- }
366
- export async function settleBodyRequests(paths, parent, now, signal) {
367
- let pending = false;
368
- for (const request of await readNonterminalRequests(paths)) {
369
- signal?.throwIfAborted();
370
- if (request.state === "admitted") {
371
- await voidRequest(paths, request.id, "body died before serving the request");
372
- }
373
- else if (request.state === "reserved" && !(await settleReserved(paths, parent, request, now, signal)))
374
- pending = true;
249
+ return (await LifecycleBodyRequestPump.openWithService(input, async (claim) => await serveRequest(claim)));
375
250
  }
376
- return pending ? "pending" : "settled";
377
251
  }
@@ -210,7 +210,7 @@ export function decodeClaim(bytes, fileId) {
210
210
  const value = object(decoded);
211
211
  if (value === null ||
212
212
  !exactKeys(value, ["action", "id", "payload"]) ||
213
- value.id !== fileId ||
213
+ !UUID.test(fileId) ||
214
214
  typeof value.id !== "string" ||
215
215
  !UUID.test(value.id))
216
216
  return null;
@@ -26,12 +26,13 @@ export function injectedBodyRequests() {
26
26
  }
27
27
  async function requestBody(input) {
28
28
  input.signal?.throwIfAborted();
29
- await atomicJson(requestPath(input.directory, input.claim.id), {
29
+ const transportId = randomUUID();
30
+ await atomicJson(requestPath(input.directory, transportId), {
30
31
  id: input.claim.id,
31
32
  action: input.claim.action,
32
33
  payload: requestPayload(input.claim),
33
34
  });
34
- const path = receiptPath(input.directory, input.claim.id);
35
+ const path = receiptPath(input.directory, transportId);
35
36
  for (;;) {
36
37
  try {
37
38
  const receipt = decodeReceipt(await readFile(path, "utf8"), input.claim.id, input.claim.action);
@@ -3,6 +3,7 @@ import { Keiyaku, type AkumaKillResult, type AkumaHistoryResult, type AkumaObser
3
3
  import type { Settings } from "../../settings.js";
4
4
  import type { WorldRoot } from "../../world.js";
5
5
  import type { InvokedAkumaCommand } from "./akuma.js";
6
+ import { finishCall } from "../../library/akuma-creation.js";
6
7
  export type AkumaInvocationResult = Readonly<{
7
8
  kind: "akuma";
8
9
  action: "call";
@@ -55,6 +56,13 @@ export type AkumaInvocationResult = Readonly<{
55
56
  mode: "no-answer";
56
57
  historyResult: AkumaHistoryResult;
57
58
  alias?: string;
59
+ }> | Readonly<{
60
+ kind: "akuma";
61
+ action: "history";
62
+ akuma: AkuId;
63
+ mode: "exact";
64
+ historyResult: AkumaHistoryResult;
65
+ alias?: string;
58
66
  }> | Readonly<{
59
67
  kind: "akuma";
60
68
  action: "fork";
@@ -74,6 +82,7 @@ type InvokeInput = Readonly<{
74
82
  repo?: Repo;
75
83
  environment: NodeJS.ProcessEnv;
76
84
  readStdin(): Promise<string>;
85
+ finishCall?: typeof finishCall;
77
86
  }>;
78
87
  export declare function invokeAkuma(command: InvokedAkumaCommand, input: InvokeInput): Promise<AkumaInvocationResult>;
79
88
  export declare function invokeAkumaStatus(path: WorldRoot, akuma: string, alias?: string, repo?: Repo): Promise<AkumaInvocationResult>;
@@ -58,6 +58,7 @@ async function invokeHistory(command, input) {
58
58
  ...(command.before === undefined ? {} : { before: command.before }),
59
59
  ...(command.since === undefined ? {} : { since: command.since }),
60
60
  ...(command.limit === undefined ? {} : { limit: command.limit }),
61
+ ...(command.id === undefined ? {} : { id: command.id }),
61
62
  last: command.last,
62
63
  });
63
64
  return {
@@ -66,11 +67,13 @@ async function invokeHistory(command, input) {
66
67
  akuma: result.id,
67
68
  historyResult: result,
68
69
  ...(inputAlias(command.akuma) === undefined ? {} : { alias: command.akuma }),
69
- ...(result.kind === "history"
70
- ? { mode: "page", history: result.history }
71
- : result.kind === "last"
72
- ? { mode: "last", answer: result.answer }
73
- : { mode: "no-answer" }),
70
+ ...(command.id !== undefined
71
+ ? { mode: "exact" }
72
+ : result.kind === "history"
73
+ ? { mode: "page", history: result.history }
74
+ : result.kind === "last"
75
+ ? { mode: "last", answer: result.answer }
76
+ : { mode: "no-answer" }),
74
77
  };
75
78
  }
76
79
  async function invokeFork(command, input) {
@@ -121,7 +124,7 @@ export async function invokeAkuma(command, input) {
121
124
  : undefined;
122
125
  let result;
123
126
  try {
124
- result = await finishCall(born);
127
+ result = await (input.finishCall ?? finishCall)(born, listener?.participantName);
125
128
  }
126
129
  catch (error) {
127
130
  if (listener?.committed === true) {
@@ -38,6 +38,7 @@ export type ParsedAkumaCommand = Output & ((Readonly<{
38
38
  }> & Addressed & Prompted) | (Readonly<{
39
39
  command: "history";
40
40
  last: boolean;
41
+ id?: string;
41
42
  before?: number;
42
43
  since?: number;
43
44
  limit?: number;
@@ -3,6 +3,7 @@ import { parseAkuId } from "../../akuma/identity.js";
3
3
  import { parseDuration as decodeDuration } from "../../duration.js";
4
4
  import { parseAkumaAlias, parseAkumaGlob } from "../../identity/selector.js";
5
5
  import { decodeAllowedActions } from "../../akuma/allowed.js";
6
+ import { parsePublicHistoryId } from "../../akuma/identity.js";
6
7
  const AKUMA_COMMAND_SPECS = {
7
8
  call: {
8
9
  arity: 1,
@@ -49,8 +50,8 @@ const AKUMA_COMMAND_SPECS = {
49
50
  history: {
50
51
  arity: 1,
51
52
  stdin: false,
52
- flags: { before: "value", since: "value", limit: "value", last: "boolean", json: "boolean" },
53
- usage: "history <aku/...|@alias|kei/...> [--before <index> | --since <index>] [--limit <count>] [--last]",
53
+ flags: { id: "value", before: "value", since: "value", limit: "value", last: "boolean", json: "boolean" },
54
+ usage: "history <aku/...|@alias|kei/...> [--id <historyId> | --before <index> | --since <index>] [--limit <count>] [--last]",
54
55
  purpose: "Read Akuma execution history or one complete Contract journal and Dispatch timeline.",
55
56
  },
56
57
  fork: {
@@ -203,22 +204,31 @@ function parseWait(rawSelectors, flags, output, fail) {
203
204
  }
204
205
  function parseHistory(selector, flags, output, fail) {
205
206
  if (selector.startsWith("kei/")) {
206
- if (flags.before !== undefined || flags.since !== undefined || flags.limit !== undefined || flags.last === true) {
207
- fail("history kei/... does not accept --before, --since, --limit, or --last");
207
+ if ([flags.id, flags.before, flags.since, flags.limit, flags.last].some((value) => value !== undefined)) {
208
+ fail("history kei/... does not accept --id, --before, --since, --limit, or --last");
208
209
  }
209
210
  return { command: "history", contract: selector, output };
210
211
  }
212
+ return parseAkumaHistory(selector, flags, output, fail);
213
+ }
214
+ function parseAkumaHistory(selector, flags, output, fail) {
215
+ const bounded = flags.before !== undefined || flags.since !== undefined || flags.limit !== undefined;
211
216
  if (flags.before !== undefined && flags.since !== undefined)
212
217
  fail("history --before and --since are mutually exclusive");
213
- if (flags.last === true && (flags.before !== undefined || flags.since !== undefined || flags.limit !== undefined)) {
218
+ if (flags.id !== undefined && (flags.last === true || bounded))
219
+ fail("history --id cannot be combined with --last, --before, --since, or --limit");
220
+ if (flags.last === true && bounded)
214
221
  fail("history --last cannot be combined with --before, --since, or --limit");
215
- }
216
222
  const limit = flags.limit === undefined ? undefined : positiveIndex(flags.limit, "--limit", fail);
217
223
  if (limit !== undefined && limit > 5_000)
218
224
  fail("--limit must be no greater than 5000");
225
+ const id = flags.id === undefined ? undefined : stringFlag(flags.id, "history --id requires a nonblank historyId", fail);
226
+ if (id !== undefined && parsePublicHistoryId(id) === null)
227
+ fail("history --id requires turn/<positive safe integer>");
219
228
  return {
220
229
  command: "history",
221
230
  akuma: validateDirect(selector, fail),
231
+ ...(id === undefined ? {} : { id }),
222
232
  last: flags.last === true,
223
233
  ...(flags.before === undefined ? {} : { before: positiveIndex(flags.before, "--before", fail) }),
224
234
  ...(flags.since === undefined ? {} : { since: positiveIndex(flags.since, "--since", fail) }),
@@ -56,6 +56,7 @@ export type ParsedAbandon = Output & Actor & Readonly<{
56
56
  export type ParsedStatus = Output & (Readonly<{
57
57
  command: "status";
58
58
  contract?: string;
59
+ selectors?: readonly string[];
59
60
  akuma?: never;
60
61
  }> | Readonly<{
61
62
  command: "status";
@@ -178,8 +179,8 @@ export declare const CONTRACT_COMMAND_SPECS: {
178
179
  readonly flags: {
179
180
  readonly json: "boolean";
180
181
  };
181
- readonly usage: "status [<contract>|@name|<aku/...>]";
182
- readonly purpose: "Read the world status board or one Contract projection.";
182
+ readonly usage: "status [<contract>|@name|<aku/...>]...";
183
+ readonly purpose: "Read the world status board or one or more Contract and Akuma projections.";
183
184
  };
184
185
  readonly show: {
185
186
  readonly positional: "optional";
@@ -87,8 +87,8 @@ export const CONTRACT_COMMAND_SPECS = {
87
87
  positional: "optional",
88
88
  stdin: "none",
89
89
  flags: { json: "boolean" },
90
- usage: "status [<contract>|@name|<aku/...>]",
91
- purpose: "Read the world status board or one Contract projection.",
90
+ usage: "status [<contract>|@name|<aku/...>]...",
91
+ purpose: "Read the world status board or one or more Contract and Akuma projections.",
92
92
  },
93
93
  show: {
94
94
  positional: "optional",
@@ -244,6 +244,9 @@ function parseReview(parts) {
244
244
  };
245
245
  }
246
246
  function parseStatus(parts) {
247
+ if (parts.positionals.length > 1) {
248
+ return { command: "status", selectors: parts.positionals, output: parts.output };
249
+ }
247
250
  const contract = parts.positionals[0];
248
251
  if (contract?.startsWith("aku/") === true) {
249
252
  return { command: "status", contract, akuma: true, output: parts.output };
@@ -0,0 +1,4 @@
1
+ import type { StatusSetResult } from "../result.js";
2
+ import type { Repo } from "../../library/repo.js";
3
+ import type { WorldRoot } from "../../world.js";
4
+ export declare function invokeStatusSet(selectors: readonly string[], world: WorldRoot | null, repo: Repo | undefined): Promise<StatusSetResult>;
@@ -0,0 +1,61 @@
1
+ import { CliUsageError } from "../parse.js";
2
+ import { canonicalContractSelector } from "../selectors.js";
3
+ async function statusSetAkuma(selector, id, alias, world, repo) {
4
+ if (world === null)
5
+ throw new CliUsageError("no Keiyaku world contains the Akuma selector");
6
+ const result = await (await import("./akuma-invoke.js")).invokeAkumaStatus(world, id, alias, repo);
7
+ if (result.action !== "status")
8
+ throw new Error("status invocation returned a non-status Akuma result");
9
+ return {
10
+ selector,
11
+ kind: "akuma",
12
+ status: result.status,
13
+ ...(result.alias === undefined ? {} : { alias: result.alias }),
14
+ };
15
+ }
16
+ async function statusSetEntry(selector, world, repo, observed) {
17
+ let resolved = selector;
18
+ let alias;
19
+ if (selector.startsWith("@")) {
20
+ if (observed === undefined)
21
+ throw new CliUsageError("cannot resolve a named status selector");
22
+ const { resolveNamedAddress } = await import("../../library/address.js");
23
+ const address = resolveNamedAddress({ selector, report: observed.report, aliases: observed.aliases });
24
+ if (address.kind === "akuma")
25
+ return await statusSetAkuma(selector, address.id, selector, world, repo);
26
+ resolved = address.id;
27
+ alias = selector;
28
+ }
29
+ if (resolved.startsWith("aku/"))
30
+ return await statusSetAkuma(selector, resolved, alias, world, repo);
31
+ if (repo === undefined)
32
+ throw new CliUsageError("cannot select a contract while the Contract world is absent");
33
+ if (observed === undefined)
34
+ throw new CliUsageError("cannot observe Contract status without a Kanshi report");
35
+ return {
36
+ selector,
37
+ kind: "contract",
38
+ report: (await import("../../kanshi/index.js")).selectKanshi({
39
+ report: observed.report,
40
+ contract: canonicalContractSelector(resolved),
41
+ }),
42
+ };
43
+ }
44
+ export async function invokeStatusSet(selectors, world, repo) {
45
+ if (selectors.length < 2)
46
+ throw new CliUsageError("status requires at least two selectors");
47
+ if (selectors.some((selector) => !selector.startsWith("aku/") && !selector.startsWith("@")) && repo === undefined) {
48
+ throw new CliUsageError("cannot select a contract while the Contract world is absent");
49
+ }
50
+ const needsKanshi = selectors.some((selector) => !selector.startsWith("aku/"));
51
+ const observed = needsKanshi
52
+ ? await (await import("../../kanshi/index.js")).observeKanshi({
53
+ world,
54
+ ...(repo === undefined ? {} : { repo }),
55
+ })
56
+ : undefined;
57
+ const entries = [];
58
+ for (const selector of selectors)
59
+ entries.push(await statusSetEntry(selector, world, repo, observed));
60
+ return { kind: "status-set", entries };
61
+ }
@@ -158,6 +158,11 @@ async function invokeCatalog(parsed, world, repo, home) {
158
158
  }
159
159
  }
160
160
  async function invokeStatus(parsed, world, repo) {
161
+ const selectors = "selectors" in parsed ? parsed.selectors : undefined;
162
+ if (selectors !== undefined) {
163
+ const { invokeStatusSet } = await import("./commands/status-set.js");
164
+ return await invokeStatusSet(selectors, world, repo);
165
+ }
161
166
  if (parsed.akuma === true) {
162
167
  if (world === null)
163
168
  throw new CliUsageError("no Keiyaku world contains the invocation cwd");
@@ -164,7 +164,7 @@ function scanArgv(argv) {
164
164
  if (spec.positional === "none" && state.positionals.length > 0) {
165
165
  refuse(command, `${command} accepts no contract`);
166
166
  }
167
- if (spec.positional === "optional" && state.positionals.length > 1) {
167
+ if (spec.positional === "optional" && state.positionals.length > 1 && command !== "status") {
168
168
  refuse(command, `${command} accepts at most one contract`);
169
169
  }
170
170
  if (spec.stdin === "required" && !state.stdin) {
@@ -316,6 +316,11 @@ export function akumaRawAnswer(result) {
316
316
  }
317
317
  if (result.action === "wait" && result.result.observations.length === 1)
318
318
  return statusAnswer(result.result.observations[0]);
319
+ if (result.action === "history" && result.mode === "exact" && result.historyResult.kind === "exact") {
320
+ return result.historyResult.outcome.outcome.kind === "answered"
321
+ ? result.historyResult.outcome.outcome.answer
322
+ : result.historyResult.outcome.outcome.diagnostic;
323
+ }
319
324
  return undefined;
320
325
  }
321
326
  export function waitText(result, context) {
@@ -336,6 +341,12 @@ export function waitText(result, context) {
336
341
  return [...blocks, `${done} of ${total} done`].join("\n\n");
337
342
  }
338
343
  export function historyText(command, result, context) {
344
+ if (result.mode === "exact") {
345
+ const exact = result.historyResult;
346
+ if (exact.kind !== "exact")
347
+ return `${exact.kind === "unknown-history" ? exact.historyId : "unknown"} has no matching retained outcome`;
348
+ return exact.outcome.outcome.kind === "answered" ? exact.outcome.outcome.answer : exact.outcome.outcome.diagnostic;
349
+ }
339
350
  if (command.last)
340
351
  return result.mode === "last" ? result.answer : "no answer retained";
341
352
  if (result.mode !== "page")
@@ -123,6 +123,11 @@ function tellExitCode(result) {
123
123
  function forkExitCode(result) {
124
124
  return result.receipt.kind === "forked" ? 0 : result.receipt.kind === "upstream-forked" ? 2 : 1;
125
125
  }
126
+ function historyExitCode(result) {
127
+ if (result.mode !== "exact")
128
+ return 0;
129
+ return result.historyResult.kind === "exact" ? 0 : 1;
130
+ }
126
131
  export function akumaExitCode(result) {
127
132
  switch (result.action) {
128
133
  case "call":
@@ -133,6 +138,8 @@ export function akumaExitCode(result) {
133
138
  return tellExitCode(result);
134
139
  case "fork":
135
140
  return forkExitCode(result);
141
+ case "history":
142
+ return historyExitCode(result);
136
143
  default:
137
144
  return 0;
138
145
  }
@@ -0,0 +1,5 @@
1
+ import type { InvocationResult } from "../result.js";
2
+ import type { TextRenderContext } from "./terminal.js";
3
+ export declare function renderStatusSetText(result: Extract<InvocationResult, {
4
+ kind: "status-set";
5
+ }>, context?: TextRenderContext): string;