@astrosheep/keiyaku 4.4.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) 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 +32 -3
  10. package/build/src/akuma/akuma-product.js +44 -20
  11. package/build/src/akuma/akuma.d.ts +0 -1
  12. package/build/src/akuma/body.d.ts +1 -7
  13. package/build/src/akuma/body.js +35 -13
  14. package/build/src/akuma/heart/index.js +7 -3
  15. package/build/src/akuma/heart/rows.d.ts +1 -1
  16. package/build/src/akuma/heart/rows.js +3 -3
  17. package/build/src/akuma/heart/storage.d.ts +1 -0
  18. package/build/src/akuma/heart/storage.js +28 -6
  19. package/build/src/akuma/identity.d.ts +1 -0
  20. package/build/src/akuma/identity.js +7 -0
  21. package/build/src/akuma/index.d.ts +1 -2
  22. package/build/src/akuma/projection.d.ts +18 -2
  23. package/build/src/akuma/projection.js +11 -1
  24. package/build/src/akuma/providers/claude/index.js +68 -37
  25. package/build/src/akuma/publication.d.ts +9 -3
  26. package/build/src/akuma/publication.js +20 -3
  27. package/build/src/akuma/request-lifecycle.d.ts +45 -0
  28. package/build/src/akuma/request-lifecycle.js +171 -0
  29. package/build/src/akuma/request-serve.d.ts +9 -32
  30. package/build/src/akuma/request-serve.js +48 -178
  31. package/build/src/akuma/request-wire.js +1 -1
  32. package/build/src/akuma/requests.js +3 -2
  33. package/build/src/cli/commands/akuma-invoke.d.ts +11 -2
  34. package/build/src/cli/commands/akuma-invoke.js +29 -7
  35. package/build/src/cli/commands/akuma.d.ts +1 -0
  36. package/build/src/cli/commands/akuma.js +16 -6
  37. package/build/src/cli/commands/contract.d.ts +3 -2
  38. package/build/src/cli/commands/contract.js +5 -2
  39. package/build/src/cli/commands/status-set.d.ts +4 -0
  40. package/build/src/cli/commands/status-set.js +61 -0
  41. package/build/src/cli/invoke.js +6 -3
  42. package/build/src/cli/parse.js +1 -1
  43. package/build/src/cli/render/akuma-activity.js +11 -0
  44. package/build/src/cli/render/akuma.js +7 -0
  45. package/build/src/cli/render/status-set.d.ts +5 -0
  46. package/build/src/cli/render/status-set.js +14 -0
  47. package/build/src/cli/render/text.js +3 -0
  48. package/build/src/cli/result.d.ts +15 -1
  49. package/build/src/cli/runtime.js +2 -0
  50. package/build/src/cli/square-edge.d.ts +6 -0
  51. package/build/src/cli/square-edge.js +74 -0
  52. package/build/src/contract-worktree.js +18 -5
  53. package/build/src/library/akuma-creation.d.ts +14 -3
  54. package/build/src/library/akuma-creation.js +31 -18
  55. package/build/src/library/fleet.d.ts +12 -1
  56. package/build/src/library/fleet.js +21 -5
  57. package/build/src/library/repo.js +3 -1
  58. package/build/src/runtime/proc/process-exit.js +6 -1
  59. package/build/src/runtime/proc/run.js +6 -10
  60. package/build/src/runtime/proc/termination.d.ts +1 -0
  61. package/build/src/runtime/proc/termination.js +22 -2
  62. package/build/src/runtime/proc/windows-launch.exe +0 -0
  63. package/build/src/runtime/proc/windows-run.js +10 -7
  64. package/build/src/world.d.ts +2 -0
  65. package/build/src/world.js +4 -0
  66. package/package.json +8 -3
  67. package/build/src/akuma/result-route.d.ts +0 -2
  68. package/build/src/akuma/result-route.js +0 -1
@@ -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
+ }
@@ -1,4 +1,3 @@
1
- import { captureRoute } from "@astrosheep/square";
2
1
  import { resolveActor } from "./actor.js";
3
2
  import { isParsedAkumaCommand } from "./commands/akuma.js";
4
3
  import { invokeContractMutation } from "./commands/contract-invoke.js";
@@ -106,14 +105,13 @@ async function invokeAkumaFromEdge(parsed, input) {
106
105
  const contract = parsed.command === "call" && parsed.contract !== undefined
107
106
  ? (await import("./selectors.js")).contractFromInput(repo, parsed.contract).contract
108
107
  : undefined;
109
- const resultRoute = parsed.command === "call" ? captureRoute({ cwd: input.invocationCwd, env: edge.environment }) : null;
110
108
  return await invokeAkuma(parsed, {
111
109
  path,
112
110
  ...(statedCwd === undefined ? {} : { statedCwd }),
113
111
  ...(home === undefined ? {} : { home }),
114
112
  ...(configuration === undefined ? {} : { settings: configuration }),
115
113
  ...(contract === undefined ? (repo === undefined ? {} : { repo }) : { contract }),
116
- ...(resultRoute === null ? {} : { resultRoute }),
114
+ environment: edge.environment,
117
115
  readStdin: edge.readStdin,
118
116
  });
119
117
  }
@@ -160,6 +158,11 @@ async function invokeCatalog(parsed, world, repo, home) {
160
158
  }
161
159
  }
162
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
+ }
163
166
  if (parsed.akuma === true) {
164
167
  if (world === null)
165
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;
@@ -0,0 +1,14 @@
1
+ import { renderAkumaText } from "./akuma.js";
2
+ import { renderKanshiText } from "./kanshi.js";
3
+ export function renderStatusSetText(result, context) {
4
+ return result.entries
5
+ .map((entry) => entry.kind === "contract"
6
+ ? renderKanshiText(entry.report, context, "contract")
7
+ : renderAkumaText({ command: "status", contract: entry.selector, akuma: true, output: "text" }, {
8
+ kind: "akuma",
9
+ action: "status",
10
+ status: entry.status,
11
+ ...(entry.alias === undefined ? {} : { alias: entry.alias }),
12
+ }, context))
13
+ .join("\n\n");
14
+ }
@@ -5,11 +5,14 @@ import { renderConflictMaterialized, renderRefusal } from "./refusal.js";
5
5
  import { renderCatalogText } from "./catalog.js";
6
6
  import { renderNukeText } from "./nuke.js";
7
7
  import { renderRegionText } from "./region.js";
8
+ import { renderStatusSetText } from "./status-set.js";
8
9
  export function renderText(result, context) {
9
10
  if (result.kind === "guidance")
10
11
  return result.guidance;
11
12
  if (result.kind === "status")
12
13
  return renderKanshiText(result.report, context, result.selection);
14
+ if (result.kind === "status-set")
15
+ return renderStatusSetText(result, context);
13
16
  if (result.kind === "catalog")
14
17
  return renderCatalogText(result.catalog);
15
18
  if (result.kind === "contract-history")
@@ -1,5 +1,6 @@
1
1
  import type { AuditReport, ContractHistory, ContractId, ContinuationReport, Delivery, Fact, IntegrationConflictMaterialized, MutationResult, PlacementStop, ReconcileReport, Review, VerificationReuse, VerificationStop } from "../index.js";
2
2
  import type { KanshiReport } from "../kanshi/index.js";
3
+ import type { AkumaObservation } from "../index.js";
3
4
  import type { RegionRead, Section } from "../kanshi/index.js";
4
5
  import type { Catalog } from "../index.js";
5
6
  import type { AmendRegionObservation, RegionObservation } from "../library/region.js";
@@ -211,6 +212,19 @@ export type StatusResult = Readonly<{
211
212
  report: KanshiReport;
212
213
  selection: "world" | "contract";
213
214
  }>;
215
+ export type StatusSetResult = Readonly<{
216
+ kind: "status-set";
217
+ entries: readonly (Readonly<{
218
+ selector: string;
219
+ kind: "contract";
220
+ report: KanshiReport;
221
+ }> | Readonly<{
222
+ selector: string;
223
+ kind: "akuma";
224
+ status: AkumaObservation;
225
+ alias?: string;
226
+ }>)[];
227
+ }>;
214
228
  export type RegionResult = Readonly<{
215
229
  kind: "region";
216
230
  region: Section<RegionRead>;
@@ -227,5 +241,5 @@ export type NukeInvocationResult = Readonly<{
227
241
  kind: "nuke";
228
242
  result: NukeResult;
229
243
  }>;
230
- export type InvocationResult = AcceptedResult | RefusedResult | RetryResult | ObservationResult | GuidanceResult | StatusResult | RegionResult | CatalogResult | ContractHistoryResult | NukeInvocationResult | IntegrationConflictMaterialized;
244
+ export type InvocationResult = AcceptedResult | RefusedResult | RetryResult | ObservationResult | GuidanceResult | StatusResult | StatusSetResult | RegionResult | CatalogResult | ContractHistoryResult | NukeInvocationResult | IntegrationConflictMaterialized;
231
245
  export {};
@@ -79,6 +79,8 @@ function invocationJson(result) {
79
79
  return result.history;
80
80
  case "status":
81
81
  return result.report;
82
+ case "status-set":
83
+ return result.entries;
82
84
  default:
83
85
  return result;
84
86
  }
@@ -0,0 +1,6 @@
1
+ import type { AllocatedAkuma } from "../akuma/identity.js";
2
+ import { type WorldRoot } from "../world.js";
3
+ export declare function recognizeAndListen(worldRoot: WorldRoot, environment: NodeJS.ProcessEnv, allocated: AllocatedAkuma): Promise<{
4
+ committed: boolean;
5
+ rollback(): Promise<void>;
6
+ } | void>;
@@ -0,0 +1,74 @@
1
+ import { bindCurrentParticipant, Square, squareAssignedParticipantName, unbindCurrentParticipant, } from "@astrosheep/square";
2
+ import { keiyakuSquarePath } from "../world.js";
3
+ async function openKeiyakuSquare(path) {
4
+ try {
5
+ return await Square.at({ path });
6
+ }
7
+ catch (error) {
8
+ if (error.code !== "ENOENT" && error.code !== "unavailable")
9
+ throw error;
10
+ try {
11
+ return await Square.build({ path, markdown: "" });
12
+ }
13
+ catch (buildError) {
14
+ try {
15
+ return await Square.at({ path });
16
+ }
17
+ catch {
18
+ throw buildError;
19
+ }
20
+ }
21
+ }
22
+ }
23
+ export async function recognizeAndListen(worldRoot, environment, allocated) {
24
+ const name = squareAssignedParticipantName(environment);
25
+ if (name === undefined)
26
+ return;
27
+ const path = keiyakuSquarePath(worldRoot);
28
+ const square = await openKeiyakuSquare(path);
29
+ let joined = false;
30
+ let bound = false;
31
+ let listening = false;
32
+ const rollback = async () => {
33
+ if (listening || joined) {
34
+ const rollbackSquare = await Square.at({ path });
35
+ try {
36
+ const rollbackParticipant = await rollbackSquare.join(name);
37
+ if (listening)
38
+ await rollbackParticipant.ignore(allocated.id);
39
+ if (joined)
40
+ await rollbackParticipant.done();
41
+ }
42
+ finally {
43
+ await rollbackSquare.close();
44
+ }
45
+ }
46
+ if (bound)
47
+ unbindCurrentParticipant(path, name, environment);
48
+ };
49
+ try {
50
+ const joinedResult = await square.joinWithActivity(name);
51
+ joined = joinedResult.activity !== null;
52
+ const participant = joinedResult.participant;
53
+ bound = bindCurrentParticipant(path, name, environment).created;
54
+ const listener = participant;
55
+ const change = await listener.listen(allocated.id);
56
+ listening = change.activity !== null;
57
+ return {
58
+ committed: true,
59
+ rollback,
60
+ };
61
+ }
62
+ catch (error) {
63
+ try {
64
+ await rollback();
65
+ }
66
+ catch {
67
+ /* preserve the original Square edge failure */
68
+ }
69
+ throw error;
70
+ }
71
+ finally {
72
+ await square.close();
73
+ }
74
+ }
@@ -50,11 +50,16 @@ const FULFILLMENT = [
50
50
  "",
51
51
  "### Deliverer",
52
52
  "",
53
+ "Re-read this file on each wake-up; it may have been updated between sessions.",
53
54
  "Implement and verify the Objective under the Design, Region, and Criteria in this Contract.",
55
+ "Region is a rough intent, not a hard boundary — natural ripple outside it is fine and not a blocker.",
54
56
  "When an Arc is active, stay within that current chapter.",
55
- "For work requiring three or more steps, prefer `keiyaku task -C <worktree>` " + "to organize and manage Tasks.",
57
+ "If you discover a contradiction with this Contract or an unclosed point in its terms,",
58
+ "stop and report it before continuing.",
59
+ "",
60
+ "For work requiring three or more steps, prefer `keiyaku task -C <worktree>` to organize and manage Tasks.",
56
61
  "Promptly update progress for Tasks already present in the current worktree.",
57
- "Leave lifecycle decisions to the caller; report the candidate, verification performed, and any unmet term.",
62
+ "",
58
63
  "Deliver from this worktree. A clean worktree delivers HEAD; uncommitted work",
59
64
  "needs `deliver --include-dirty`, which captures the final non-ignored tree and",
60
65
  "stages or commits nothing. If deliver reports a conflict, run",
@@ -64,9 +69,17 @@ const FULFILLMENT = [
64
69
  "",
65
70
  "### Reviewer",
66
71
  "",
67
- "Review this Contract worktree against the complete Contract.",
68
- "Review the complete current worktree snapshot, not a named candidate commit.",
69
- "Do not modify it; report covered Criteria, findings, and missing evidence.",
72
+ "Re-read this file on each wake-up; it may have been updated between sessions.",
73
+ "Review the complete current worktree snapshot against this Contract. Do not modify it.",
74
+ "Understand the original intent behind the Objective, not just its literal text.",
75
+ "Region is a rough intent, not a hard boundary — natural ripple outside it is not a blocker.",
76
+ "Compare each Criterion individually against the delivered work.",
77
+ "Consolidate findings by root cause — group symptoms that share an underlying issue",
78
+ "rather than listing each occurrence separately.",
79
+ "Dig to the architectural or structural root; flag it directly rather than routing around it.",
80
+ "If you discover a contradiction within the Contract or an unclosed point in its terms,",
81
+ "report it as a finding.",
82
+ "Report covered Criteria, findings, and missing evidence.",
70
83
  ].join("\n");
71
84
  export function renderContractGuidance(state) {
72
85
  return [
@@ -1,5 +1,6 @@
1
1
  import { type AliasBinding } from "../alias/index.js";
2
- import { type AkumaStatus, type ForkReceipt, type ReadonlyRestraint, type ResultRoute } from "../akuma/akuma.js";
2
+ import { type AkumaStatus, type ForkReceipt, type ReadonlyRestraint } from "../akuma/akuma.js";
3
+ import { type AkumaBornCall } from "../akuma/akuma-product.js";
3
4
  import type { AkuId } from "../akuma/identity.js";
4
5
  import { type Dispatch, type DispatchFailure } from "../dispatch/index.js";
5
6
  import { type AkumaAlias } from "../identity/selector.js";
@@ -48,11 +49,9 @@ export type CallInput = Readonly<{
48
49
  contract?: Keiyaku;
49
50
  alias?: AkumaAlias;
50
51
  allowed?: readonly AllowedAction[];
51
- resultRoute?: ResultRoute;
52
52
  }>;
53
53
  export type CallObservation = Readonly<{
54
54
  kind: "detached";
55
- resultRoute: "captured" | "not-captured";
56
55
  }> | Readonly<{
57
56
  kind: "observed";
58
57
  status: AkumaStatus;
@@ -72,6 +71,16 @@ export type CallResult = Readonly<{
72
71
  alias: AliasStage;
73
72
  observation: CallObservation;
74
73
  }>;
74
+ type CallExecution = CallResult["execution"];
75
+ export type BornCall = Readonly<{
76
+ path: WorldRoot;
77
+ born: AkumaBornCall;
78
+ execution: CallExecution;
79
+ mode: "wait" | "detach";
80
+ timeoutMs: number;
81
+ dispatch: DispatchStage;
82
+ alias: AliasStage;
83
+ }>;
75
84
  export type ForkInput = Readonly<{
76
85
  path: WorldRoot;
77
86
  akuma: string;
@@ -89,5 +98,7 @@ export type ForkResult = Readonly<{
89
98
  }>> & Readonly<{
90
99
  parent: AkuId;
91
100
  }>);
101
+ export declare function beginCall(input: CallInput): Promise<BornCall>;
102
+ export declare function finishCall(born: BornCall): Promise<CallResult>;
92
103
  export declare function callKeiyaku(input: CallInput): Promise<CallResult>;
93
104
  export declare function forkKeiyaku(input: ForkInput): Promise<ForkResult>;
@@ -1,6 +1,7 @@
1
1
  import { realpath, stat } from "node:fs/promises";
2
2
  import { moveAlias } from "../alias/index.js";
3
- import { Akuma, akumaCallExecution, callAkumaWithContext, } from "../akuma/akuma.js";
3
+ import { Akuma } from "../akuma/akuma.js";
4
+ import { beginAkumaCall, finishAkumaCall } from "../akuma/akuma-product.js";
4
5
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
5
6
  import { publishDispatch, readDispatch } from "../dispatch/index.js";
6
7
  import { parseAkumaAlias } from "../identity/selector.js";
@@ -77,9 +78,9 @@ function callTimeout(value, mode) {
77
78
  }
78
79
  return value;
79
80
  }
80
- async function observeCall(handle, mode, timeoutMs, resultRoute) {
81
+ async function observeCall(handle, mode, timeoutMs) {
81
82
  if (mode === "detach") {
82
- return { kind: "detached", resultRoute: resultRoute === undefined ? "not-captured" : "captured" };
83
+ return { kind: "detached" };
83
84
  }
84
85
  try {
85
86
  return { kind: "observed", status: await handle.wait(undefined, { timeoutMs }) };
@@ -165,7 +166,7 @@ async function resolveCallExecution(input) {
165
166
  }
166
167
  return undefined;
167
168
  }
168
- export async function callKeiyaku(input) {
169
+ export async function beginCall(input) {
169
170
  const values = requireInput(input, "Keiyaku.call input");
170
171
  onlyKeys(values, [
171
172
  "path",
@@ -180,7 +181,6 @@ export async function callKeiyaku(input) {
180
181
  "contract",
181
182
  "alias",
182
183
  "allowed",
183
- "resultRoute",
184
184
  ], "Keiyaku.call input");
185
185
  const path = nonblank(values.path, "path");
186
186
  const archetype = nonblank(values.archetype, "archetype");
@@ -193,7 +193,6 @@ export async function callKeiyaku(input) {
193
193
  const settings = settingsOption(values.settings);
194
194
  const alias = values.alias === undefined ? undefined : parseAkumaAlias(nonblank(values.alias, "alias"));
195
195
  const seat = values.contract === undefined ? undefined : seatForKeiyaku(values.contract);
196
- const resultRoute = values.resultRoute;
197
196
  const execution = await resolveCallExecution({
198
197
  path,
199
198
  ...(cwd === undefined ? {} : { cwd }),
@@ -206,23 +205,20 @@ export async function callKeiyaku(input) {
206
205
  ...(readonlyRequested === undefined ? {} : { readonly: readonlyRequested }),
207
206
  ...(values.allowed === undefined ? {} : { allowed: values.allowed }),
208
207
  ...(execution === undefined ? {} : { cwd: execution.cwd }),
209
- ...(resultRoute === undefined ? {} : { resultRoute }),
210
208
  };
211
- const handle = execution === undefined ? await world.call(call) : await callAkumaWithContext(world, call, { cwdCanonical: true });
212
- const completedExecution = execution ?? akumaCallExecution(handle);
213
- if (completedExecution === undefined)
214
- throw new Error("Akuma call is missing its birth execution");
215
- const readonly = (await handle.status()).readonly;
209
+ const born = await beginAkumaCall(world, call, execution === undefined ? {} : { cwdCanonical: true });
210
+ const akuma = born.kind === "requested" ? born.id : born.allocated.id;
211
+ const completedExecution = execution ?? born.execution;
216
212
  const dispatch = seat === undefined
217
213
  ? { kind: "none" }
218
- : await dispatchStage({ repository: seat.scope, akuId: handle.id, contractId: seat.id });
214
+ : await dispatchStage({ repository: seat.scope, akuId: akuma, contractId: seat.id });
219
215
  let aliasStage = { kind: "none" };
220
216
  if (alias !== undefined) {
221
217
  if (dispatch.kind === "failed")
222
218
  aliasStage = { kind: "skipped", reason: "dispatch-failed" };
223
219
  else {
224
220
  try {
225
- const moved = await moveAlias({ world: path, alias, akuId: handle.id });
221
+ const moved = await moveAlias({ world: path, alias, akuId: akuma });
226
222
  aliasStage = { kind: "aliased", alias: moved.alias, previous: moved.previous };
227
223
  }
228
224
  catch (error) {
@@ -230,17 +226,34 @@ export async function callKeiyaku(input) {
230
226
  }
231
227
  }
232
228
  }
233
- const observation = await observeCall(handle, mode, timeoutMs, resultRoute);
234
229
  return {
235
- kind: "called",
236
- akuma: handle.id,
237
- ...(readonly === undefined ? {} : { readonly }),
230
+ path,
231
+ born,
238
232
  execution: completedExecution,
233
+ mode,
234
+ timeoutMs,
239
235
  dispatch,
240
236
  alias: aliasStage,
237
+ };
238
+ }
239
+ export async function finishCall(born) {
240
+ const world = akumaWorld(born.path);
241
+ const handle = await finishAkumaCall(world, born.born);
242
+ const readonly = (await handle.status()).readonly;
243
+ const observation = await observeCall(handle, born.mode, born.timeoutMs);
244
+ return {
245
+ kind: "called",
246
+ akuma: handle.id,
247
+ ...(readonly === undefined ? {} : { readonly }),
248
+ execution: born.execution,
249
+ dispatch: born.dispatch,
250
+ alias: born.alias,
241
251
  observation,
242
252
  };
243
253
  }
254
+ export async function callKeiyaku(input) {
255
+ return await finishCall(await beginCall(input));
256
+ }
244
257
  export async function forkKeiyaku(input) {
245
258
  const values = requireInput(input, "Keiyaku.fork input");
246
259
  onlyKeys(values, ["path", "akuma", "at", "repo"], "Keiyaku.fork input");
@@ -1,4 +1,4 @@
1
- import { type ActivityHistory, type AkumaStatus, type InterruptReceipt, type KillEvidence, type TellResult } from "../akuma/index.js";
1
+ import { type ActivityHistory, type OutcomeRow, type AkumaStatus, type InterruptReceipt, type KillEvidence, type TellResult } from "../akuma/index.js";
2
2
  import type { ContractId } from "../core/facts/types.js";
3
3
  import type { TaskRow } from "../task/index.js";
4
4
  import type { WorldRoot } from "../world.js";
@@ -69,6 +69,7 @@ export type AkumaInterruptResult = Readonly<{
69
69
  observation: AkumaObservationStage;
70
70
  }>;
71
71
  export type AkumaHistoryInput = AkumaAddressInput & Readonly<{
72
+ id?: string;
72
73
  before?: number;
73
74
  since?: number;
74
75
  limit?: number;
@@ -79,6 +80,16 @@ export type AkumaHistoryResult = Readonly<{
79
80
  id: AkumaStatus["id"];
80
81
  history: ActivityHistory;
81
82
  contract: DispatchAssociation;
83
+ }> | Readonly<{
84
+ kind: "exact";
85
+ id: AkumaStatus["id"];
86
+ outcome: OutcomeRow;
87
+ contract: DispatchAssociation;
88
+ }> | Readonly<{
89
+ kind: "unknown-history";
90
+ id: AkumaStatus["id"];
91
+ historyId: string;
92
+ contract: DispatchAssociation;
82
93
  }> | Readonly<{
83
94
  kind: "last";
84
95
  id: AkumaStatus["id"];
@@ -6,6 +6,7 @@ import { observeTaskBoard } from "../task/operations.js";
6
6
  import { addressAkuma, addressAkumaSet } from "./address.js";
7
7
  import { requireInput } from "./input.js";
8
8
  import { scopeForRepo } from "./repo.js";
9
+ import { parsePublicHistoryId } from "../akuma/identity.js";
9
10
  function source(path) {
10
11
  return Akuma.of(path);
11
12
  }
@@ -315,15 +316,24 @@ export async function interruptAkuma(input) {
315
316
  const observation = await observeAkumaStage(addressed.path, addressed.id, values.repo);
316
317
  return { id: addressed.id, receipt, observation };
317
318
  }
318
- export async function historyAkuma(input) {
319
- const values = requireInput(input, "Keiyaku.history input");
319
+ function validateHistoryInput(values) {
320
320
  for (const key of Object.keys(values)) {
321
- if (!["path", "akuma", "before", "since", "limit", "last", "repo"].includes(key)) {
321
+ if (!["path", "akuma", "id", "before", "since", "limit", "last", "repo"].includes(key))
322
322
  throw new TypeError(`Keiyaku.history input has unknown field: ${key}`);
323
- }
324
323
  }
325
324
  if (values.last !== undefined && typeof values.last !== "boolean")
326
325
  throw new TypeError("last must be a boolean");
326
+ if (values.id !== undefined && (typeof values.id !== "string" || values.id.trim() === ""))
327
+ throw new TypeError("id must be a nonblank string");
328
+ if (values.id !== undefined && parsePublicHistoryId(values.id) === null)
329
+ throw new TypeError("id must match turn/<positive safe integer>");
330
+ if (values.id !== undefined &&
331
+ (values.last === true || values.before !== undefined || values.since !== undefined || values.limit !== undefined))
332
+ throw new TypeError("id cannot be combined with last, before, since, or limit");
333
+ }
334
+ export async function historyAkuma(input) {
335
+ const values = requireInput(input, "Keiyaku.history input");
336
+ validateHistoryInput(values);
327
337
  const addressed = await addressAkuma(directAddress(values));
328
338
  const handle = source(addressed.path).of({ id: addressed.id });
329
339
  const contract = await dispatchAssociation(values.repo, addressed.id);
@@ -334,9 +344,15 @@ export async function historyAkuma(input) {
334
344
  : { kind: "no-answer", id: addressed.id, contract };
335
345
  }
336
346
  const history = await handle.history({
347
+ ...(values.id === undefined ? {} : { id: values.id }),
337
348
  ...(values.before === undefined ? {} : { before: values.before }),
338
349
  ...(values.since === undefined ? {} : { since: values.since }),
339
350
  ...(values.limit === undefined ? {} : { limit: values.limit }),
340
351
  });
341
- return { kind: "history", id: addressed.id, history, contract };
352
+ if (values.id !== undefined) {
353
+ if ("kind" in history && history.kind === "exact")
354
+ return { kind: "exact", id: addressed.id, outcome: history.outcome, contract };
355
+ return { kind: "unknown-history", id: addressed.id, historyId: values.id, contract };
356
+ }
357
+ return { kind: "history", id: addressed.id, history: history, contract };
342
358
  }
@@ -1,3 +1,5 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { resolve } from "node:path";
1
3
  import { currentBranchOperation, scopeOperation } from "../protocol/operations.js";
2
4
  import { NoGitWorldError } from "../git/repository.js";
3
5
  import { optionalNonblank, requireInput } from "./input.js";
@@ -8,7 +10,7 @@ export { NoGitWorldError };
8
10
  const REPO_SCOPES = new WeakMap();
9
11
  async function resolvePinnedScope(path, gitPath) {
10
12
  return await scopeOperation({
11
- coordinate: path === undefined ? process.cwd() : path,
13
+ coordinate: await realpath(resolve(path === undefined ? process.cwd() : path)),
12
14
  ...(gitPath === undefined ? {} : { gitPath }),
13
15
  });
14
16
  }
@@ -5,12 +5,17 @@ export function detachedExitStatus(code, signal) {
5
5
  export async function retainDetachedExitEvidence(log, path, from, code, signal) {
6
6
  const status = detachedExitStatus(code, signal);
7
7
  const marker = Buffer.from(`[child ${status}]\n`);
8
+ const before = await log.stat();
9
+ if (before.size < from)
10
+ throw new Error("run log shrank before exit evidence was retained");
8
11
  let written = 0;
12
+ let position = before.size;
9
13
  while (written < marker.byteLength) {
10
- const write = await log.write(marker, written, marker.byteLength - written);
14
+ const write = await log.write(marker, written, marker.byteLength - written, position);
11
15
  if (write.bytesWritten === 0)
12
16
  throw new Error("run log exit marker write made no progress");
13
17
  written += write.bytesWritten;
18
+ position += write.bytesWritten;
14
19
  }
15
20
  const evidence = await log.stat();
16
21
  if (evidence.size < from)