@astrosheep/keiyaku 4.5.19 → 4.5.21

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 (197) hide show
  1. package/README.md +4 -4
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +14 -7
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +53 -20
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/references/automation.md +237 -0
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +6 -7
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +2 -4
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +3 -3
  8. package/build/src/akuma/akuma-handle.d.ts +5 -1
  9. package/build/src/akuma/akuma-handle.js +4 -1
  10. package/build/src/akuma/akuma-instance.d.ts +7 -2
  11. package/build/src/akuma/akuma-instance.js +14 -7
  12. package/build/src/akuma/akuma-observe.d.ts +11 -0
  13. package/build/src/akuma/akuma-observe.js +23 -0
  14. package/build/src/akuma/akuma-product.d.ts +2 -0
  15. package/build/src/akuma/akuma-product.js +6 -0
  16. package/build/src/akuma/akuma.d.ts +20 -1
  17. package/build/src/akuma/akuma.js +2 -0
  18. package/build/src/akuma/allowed.d.ts +20 -0
  19. package/build/src/akuma/allowed.js +11 -0
  20. package/build/src/akuma/archetype.js +3 -0
  21. package/build/src/akuma/body.d.ts +25 -5
  22. package/build/src/akuma/body.js +70 -31
  23. package/build/src/akuma/call-request.d.ts +5 -0
  24. package/build/src/akuma/call-request.js +14 -4
  25. package/build/src/akuma/fleet-execution.d.ts +28 -0
  26. package/build/src/akuma/fleet-execution.js +36 -4
  27. package/build/src/akuma/fleet-observation.d.ts +54 -0
  28. package/build/src/akuma/fleet-request.d.ts +10 -0
  29. package/build/src/akuma/fleet-request.js +13 -3
  30. package/build/src/akuma/heart/facts.d.ts +2 -0
  31. package/build/src/akuma/heart/index.d.ts +1 -0
  32. package/build/src/akuma/heart/index.js +4 -1
  33. package/build/src/akuma/heart/rows.d.ts +2 -0
  34. package/build/src/akuma/heart/rows.js +7 -5
  35. package/build/src/akuma/heart/schema.d.ts +1 -1
  36. package/build/src/akuma/heart/schema.js +3 -1
  37. package/build/src/akuma/heart/tells.js +6 -5
  38. package/build/src/akuma/heart/timeline.js +1 -1
  39. package/build/src/akuma/provider-recipe.d.ts +1 -0
  40. package/build/src/akuma/provider-recipe.js +5 -0
  41. package/build/src/akuma/provider.d.ts +1 -1
  42. package/build/src/akuma/provider.js +1 -1
  43. package/build/src/akuma/providers/acp/core.js +3 -6
  44. package/build/src/akuma/providers/acp/index.js +3 -0
  45. package/build/src/akuma/providers/claude/index.js +10 -10
  46. package/build/src/akuma/providers/codex-app-server/index.js +12 -11
  47. package/build/src/akuma/providers/execution-environment.d.ts +6 -0
  48. package/build/src/akuma/providers/execution-environment.js +24 -0
  49. package/build/src/akuma/providers/grok-build/index.js +3 -0
  50. package/build/src/akuma/providers/opencode-sdk/index.js +32 -23
  51. package/build/src/akuma/providers/opencode-sdk/session.d.ts +8 -1
  52. package/build/src/akuma/providers/opencode-sdk/session.js +6 -5
  53. package/build/src/akuma/providers/pi/events.js +7 -2
  54. package/build/src/akuma/providers/pi/index.js +5 -2
  55. package/build/src/akuma/request-observation.d.ts +45 -0
  56. package/build/src/akuma/request-observation.js +162 -0
  57. package/build/src/akuma/request-rendezvous.d.ts +6 -3
  58. package/build/src/akuma/request-rendezvous.js +80 -14
  59. package/build/src/akuma/request-serve.js +27 -4
  60. package/build/src/akuma/request-wire.d.ts +5 -0
  61. package/build/src/akuma/request-wire.js +2 -0
  62. package/build/src/akuma/turn-drive.d.ts +1 -0
  63. package/build/src/akuma/turn-drive.js +2 -0
  64. package/build/src/akuma-body.js +5 -0
  65. package/build/src/body/amend.js +6 -4
  66. package/build/src/body/decode.d.ts +3 -1
  67. package/build/src/body/decode.js +53 -17
  68. package/build/src/body/region.d.ts +9 -1
  69. package/build/src/body/region.js +87 -12
  70. package/build/src/body/render.js +1 -1
  71. package/build/src/body/verification.d.ts +3 -1
  72. package/build/src/body/verification.js +4 -1
  73. package/build/src/cli/accepted.js +19 -2
  74. package/build/src/cli/commands/akuma-invoke.d.ts +3 -1
  75. package/build/src/cli/commands/akuma-invoke.js +134 -14
  76. package/build/src/cli/commands/akuma.js +24 -12
  77. package/build/src/cli/commands/contract-help.d.ts +15 -15
  78. package/build/src/cli/commands/contract-help.js +45 -19
  79. package/build/src/cli/commands/contract-invoke.d.ts +3 -0
  80. package/build/src/cli/commands/contract-invoke.js +25 -11
  81. package/build/src/cli/commands/contract.js +6 -4
  82. package/build/src/cli/commands/install.d.ts +1 -1
  83. package/build/src/cli/commands/install.js +1 -1
  84. package/build/src/cli/commands/status-set.js +15 -24
  85. package/build/src/cli/commands/task.js +18 -18
  86. package/build/src/cli/invoke.d.ts +3 -0
  87. package/build/src/cli/invoke.js +58 -23
  88. package/build/src/cli/main.js +5 -0
  89. package/build/src/cli/parse.d.ts +3 -0
  90. package/build/src/cli/parse.js +26 -4
  91. package/build/src/cli/render/akuma-activity.d.ts +100 -2
  92. package/build/src/cli/render/akuma-activity.js +485 -40
  93. package/build/src/cli/render/akuma.d.ts +10 -0
  94. package/build/src/cli/render/akuma.js +31 -21
  95. package/build/src/cli/render/audit.js +1 -1
  96. package/build/src/cli/render/board.js +1 -1
  97. package/build/src/cli/render/catalog.js +16 -13
  98. package/build/src/cli/render/contract-history.js +39 -10
  99. package/build/src/cli/render/contract-observation.js +10 -6
  100. package/build/src/cli/render/contract.js +109 -106
  101. package/build/src/cli/render/execution-progress.d.ts +26 -0
  102. package/build/src/cli/render/execution-progress.js +178 -0
  103. package/build/src/cli/render/kanshi-akuma.js +27 -39
  104. package/build/src/cli/render/kanshi.js +33 -26
  105. package/build/src/cli/render/nuke.js +1 -1
  106. package/build/src/cli/render/receipt.d.ts +1 -1
  107. package/build/src/cli/render/receipt.js +6 -6
  108. package/build/src/cli/render/refusal.js +10 -7
  109. package/build/src/cli/render/region.js +1 -1
  110. package/build/src/cli/render/settings.js +12 -2
  111. package/build/src/cli/render/status-line.d.ts +30 -0
  112. package/build/src/cli/render/status-line.js +58 -0
  113. package/build/src/cli/render/status-set.js +1 -1
  114. package/build/src/cli/render/task.d.ts +2 -0
  115. package/build/src/cli/render/task.js +11 -7
  116. package/build/src/cli/render/terminal.d.ts +0 -1
  117. package/build/src/cli/render/terminal.js +47 -15
  118. package/build/src/cli/result.d.ts +1 -0
  119. package/build/src/cli/runtime.d.ts +2 -0
  120. package/build/src/cli/runtime.js +36 -4
  121. package/build/src/cli/usage.js +2 -2
  122. package/build/src/cli/version.d.ts +1 -0
  123. package/build/src/cli/version.js +25 -0
  124. package/build/src/git/hooks.d.ts +13 -1
  125. package/build/src/git/hooks.js +2 -1
  126. package/build/src/git/workspace.d.ts +1 -1
  127. package/build/src/git/workspace.js +1 -1
  128. package/build/src/identity/mint.d.ts +13 -0
  129. package/build/src/identity/mint.js +26 -0
  130. package/build/src/identity/normalize.d.ts +4 -0
  131. package/build/src/identity/normalize.js +30 -0
  132. package/build/src/index.d.ts +2 -0
  133. package/build/src/index.js +1 -0
  134. package/build/src/kanshi/read.js +17 -4
  135. package/build/src/kanshi/report.d.ts +1 -0
  136. package/build/src/library/akuma-creation.d.ts +2 -0
  137. package/build/src/library/akuma-creation.js +34 -18
  138. package/build/src/library/audit.d.ts +2 -0
  139. package/build/src/library/audit.js +1 -0
  140. package/build/src/library/bind.js +18 -16
  141. package/build/src/library/composition.d.ts +1 -0
  142. package/build/src/library/contract-bind.d.ts +3 -1
  143. package/build/src/library/contract-bind.js +5 -3
  144. package/build/src/library/contract-execution.d.ts +2 -0
  145. package/build/src/library/contract-execution.js +15 -1
  146. package/build/src/library/contract-forwarding.d.ts +4 -0
  147. package/build/src/library/contract-forwarding.js +3 -0
  148. package/build/src/library/contract-handle.d.ts +7 -0
  149. package/build/src/library/contract-handle.js +25 -0
  150. package/build/src/library/contract-operations.d.ts +7 -2
  151. package/build/src/library/contract-operations.js +21 -1
  152. package/build/src/library/contract-types.d.ts +1 -0
  153. package/build/src/library/execution-result.d.ts +2 -2
  154. package/build/src/library/execution.d.ts +8 -0
  155. package/build/src/library/execution.js +102 -0
  156. package/build/src/library/fleet.d.ts +4 -1
  157. package/build/src/library/fleet.js +32 -8
  158. package/build/src/library/keiyaku.d.ts +2 -0
  159. package/build/src/library/mutation.js +15 -7
  160. package/build/src/plugin/akuma-signals.d.ts +6 -0
  161. package/build/src/plugin/akuma-signals.js +9 -1
  162. package/build/src/plugin/public.d.ts +6 -2
  163. package/build/src/plugin/runtime.d.ts +1 -3
  164. package/build/src/plugin/runtime.js +95 -103
  165. package/build/src/protocol/amend.js +4 -3
  166. package/build/src/protocol/attempt.d.ts +2 -0
  167. package/build/src/protocol/bind.js +41 -14
  168. package/build/src/protocol/completion.d.ts +3 -0
  169. package/build/src/protocol/completion.js +35 -11
  170. package/build/src/protocol/deliver.js +165 -92
  171. package/build/src/protocol/execution-observation.d.ts +181 -0
  172. package/build/src/protocol/execution-observation.js +45 -0
  173. package/build/src/protocol/intent.d.ts +18 -22
  174. package/build/src/protocol/intent.js +63 -41
  175. package/build/src/protocol/operations.d.ts +2 -0
  176. package/build/src/protocol/placement.js +5 -1
  177. package/build/src/protocol/progress.d.ts +5 -1
  178. package/build/src/protocol/progress.js +10 -1
  179. package/build/src/protocol/read/status.d.ts +1 -0
  180. package/build/src/protocol/read/status.js +10 -2
  181. package/build/src/protocol/reintegrate.d.ts +1 -1
  182. package/build/src/protocol/reintegrate.js +70 -43
  183. package/build/src/protocol/result-codec.js +27 -11
  184. package/build/src/protocol/review.js +53 -30
  185. package/build/src/protocol/run.d.ts +83 -36
  186. package/build/src/protocol/run.js +90 -75
  187. package/build/src/runtime/proc/run.d.ts +16 -4
  188. package/build/src/runtime/proc/run.js +64 -22
  189. package/build/src/runtime/proc/windows-launch.exe +0 -0
  190. package/build/src/settings.js +5 -14
  191. package/build/src/task/document.d.ts +5 -1
  192. package/build/src/task/document.js +6 -1
  193. package/build/src/verification/execution.d.ts +10 -2
  194. package/build/src/verification/execution.js +143 -42
  195. package/build/src/verification/observation.d.ts +41 -0
  196. package/build/src/verification/observation.js +29 -0
  197. package/package.json +3 -1
@@ -8,5 +8,11 @@ type CalledPluginSignalInput = Readonly<{
8
8
  callerAkumaId?: string;
9
9
  contractId?: string;
10
10
  }>;
11
+ export declare function emitInitiatingPluginSignal(input: Readonly<{
12
+ world: WorldRoot;
13
+ settings?: Settings;
14
+ initiator: string;
15
+ reportDiagnostic: (message: string) => void;
16
+ }>): Promise<void>;
11
17
  export declare function emitCalledPluginSignal(input: CalledPluginSignalInput): Promise<void>;
12
18
  export {};
@@ -1,11 +1,19 @@
1
1
  import { pluginRuntime } from "./runtime.js";
2
+ export async function emitInitiatingPluginSignal(input) {
3
+ const runtime = await pluginRuntime({
4
+ world: input.world,
5
+ ...(input.settings === undefined ? {} : { settings: input.settings }),
6
+ reportDiagnostic: input.reportDiagnostic,
7
+ });
8
+ await runtime.emit({ kind: "akuma.initiating", initiator: input.initiator }, input.reportDiagnostic);
9
+ }
2
10
  export async function emitCalledPluginSignal(input) {
3
11
  const runtime = await pluginRuntime({
4
12
  world: input.world,
5
13
  ...(input.settings === undefined ? {} : { settings: input.settings }),
6
14
  reportDiagnostic: input.reportDiagnostic,
7
15
  });
8
- runtime.emit({
16
+ await runtime.emit({
9
17
  kind: "akuma.called",
10
18
  akumaId: input.akumaId,
11
19
  ...(input.callerAkumaId === undefined ? {} : { callerAkumaId: input.callerAkumaId }),
@@ -16,6 +16,9 @@ export type PluginOutcome = Readonly<{
16
16
  }>;
17
17
  export type PluginBodyEnd = "exited" | "broke-off" | "put-down" | "hung";
18
18
  export type PluginSignalMap = Readonly<{
19
+ "akuma.initiating": Readonly<{
20
+ initiator: string;
21
+ }>;
19
22
  "akuma.called": Readonly<{
20
23
  akumaId: string;
21
24
  callerAkumaId?: string;
@@ -24,6 +27,7 @@ export type PluginSignalMap = Readonly<{
24
27
  "akuma.turn-outcome": Readonly<{
25
28
  akumaId: string;
26
29
  turnSequence: number;
30
+ initiator?: string;
27
31
  outcome: PluginOutcome;
28
32
  contractId?: string;
29
33
  }>;
@@ -43,7 +47,7 @@ export type PluginSignal = {
43
47
  export type PluginHooks = Readonly<{
44
48
  [K in keyof PluginSignalMap]?: (signal: Readonly<{
45
49
  kind: K;
46
- }> & PluginSignalMap[K]) => Promise<void> | void;
50
+ }> & PluginSignalMap[K], cancellation?: AbortSignal) => Promise<void> | void;
47
51
  }>;
48
52
  export type PluginContext = Readonly<{
49
53
  world: WorldRoot;
@@ -52,7 +56,7 @@ export type PluginContext = Readonly<{
52
56
  }>;
53
57
  export type KeiyakuPlugin = Readonly<{
54
58
  manifest: PluginManifest;
55
- activate(context: PluginContext): Promise<PluginInstance> | PluginInstance;
59
+ activate(context: PluginContext, cancellation?: AbortSignal): Promise<PluginInstance> | PluginInstance;
56
60
  }>;
57
61
  export type PluginInstance = Readonly<{
58
62
  signals?: PluginHooks;
@@ -1,6 +1,6 @@
1
1
  import { type Settings } from "../settings.js";
2
2
  import type { WorldRoot } from "../world.js";
3
- import type { PluginSignal, PluginSignalMap } from "./public.js";
3
+ import type { PluginSignal } from "./public.js";
4
4
  type PluginDiagnostic = (diagnostic: string) => void;
5
5
  type PluginRuntimeInput = Readonly<{
6
6
  world: WorldRoot;
@@ -9,8 +9,6 @@ type PluginRuntimeInput = Readonly<{
9
9
  }>;
10
10
  export type PluginRuntime = Readonly<{
11
11
  emit(signal: PluginSignal, reportDiagnostic?: PluginDiagnostic): Promise<void>;
12
- drain(kind?: keyof PluginSignalMap): Promise<void>;
13
12
  }>;
14
13
  export declare function pluginRuntime(input: PluginRuntimeInput): Promise<PluginRuntime>;
15
- export declare function drainPluginRuntime(world: WorldRoot): Promise<void>;
16
14
  export {};
@@ -4,13 +4,14 @@ import { isAbsolute, join, posix, relative, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { settings } from "../settings.js";
6
6
  const DIAGNOSTIC_LIMIT = 500;
7
- const PLUGIN_DRAIN_TIMEOUT_MS = 5_000;
7
+ const PLUGIN_ATTEMPT_TIMEOUT_MS = 5_000;
8
8
  const PLUGIN_NAMESPACE = "plugins";
9
9
  const SQUARE_PLUGIN_PACKAGE = "@astrosheep/keiyaku-plugin-square";
10
10
  const BUILTIN_PLUGINS = Object.freeze([
11
11
  Object.freeze({ id: "square", package: SQUARE_PLUGIN_PACKAGE, config: undefined }),
12
12
  ]);
13
13
  const PROCESS_RUNTIMES = new Map();
14
+ const ABORTED = Symbol("plugin attempt aborted");
14
15
  function object(value) {
15
16
  return value !== null && typeof value === "object" && !Array.isArray(value);
16
17
  }
@@ -27,6 +28,43 @@ function diagnostic(report, subject, stage, error) {
27
28
  // Diagnostics are optional side effects too.
28
29
  }
29
30
  }
31
+ function timeoutError() {
32
+ return new Error(`timed out after ${PLUGIN_ATTEMPT_TIMEOUT_MS}ms`);
33
+ }
34
+ async function boundedAttempt(attempt, timeoutMs = PLUGIN_ATTEMPT_TIMEOUT_MS) {
35
+ if (timeoutMs <= 0)
36
+ return { kind: "timed-out" };
37
+ const controller = new AbortController();
38
+ let timeout;
39
+ const completed = Promise.resolve()
40
+ .then(() => attempt(controller.signal))
41
+ .then((value) => ({ kind: "settled", value }), (error) => ({ kind: "failed", error }));
42
+ const expired = new Promise((resolve) => {
43
+ timeout = setTimeout(() => {
44
+ controller.abort(timeoutError());
45
+ resolve({ kind: "timed-out" });
46
+ }, timeoutMs);
47
+ });
48
+ const result = await Promise.race([completed, expired]);
49
+ if (timeout !== undefined)
50
+ clearTimeout(timeout);
51
+ return result;
52
+ }
53
+ async function settleBefore(promise, cancellation) {
54
+ if (cancellation.aborted)
55
+ return ABORTED;
56
+ let releaseAbort;
57
+ const aborted = new Promise((resolve) => {
58
+ releaseAbort = () => resolve(ABORTED);
59
+ cancellation.addEventListener("abort", releaseAbort, { once: true });
60
+ });
61
+ try {
62
+ return await Promise.race([promise, aborted]);
63
+ }
64
+ finally {
65
+ cancellation.removeEventListener("abort", releaseAbort);
66
+ }
67
+ }
30
68
  function selectedEntry(entry, report) {
31
69
  if (entry.name.trim().length === 0) {
32
70
  diagnostic(report, "<unnamed>", "selection", new TypeError("plugin setting name must be nonblank"));
@@ -170,7 +208,10 @@ function handlers(pluginId, instance) {
170
208
  throw new TypeError("plugin signals must be an object");
171
209
  const registered = [];
172
210
  for (const [kind, handler] of Object.entries(instance.signals)) {
173
- if (kind !== "akuma.called" && kind !== "akuma.turn-outcome" && kind !== "akuma.body-ended") {
211
+ if (kind !== "akuma.initiating" &&
212
+ kind !== "akuma.called" &&
213
+ kind !== "akuma.turn-outcome" &&
214
+ kind !== "akuma.body-ended") {
174
215
  throw new TypeError(`plugin signal is unknown: ${kind}`);
175
216
  }
176
217
  if (typeof handler !== "function")
@@ -214,7 +255,8 @@ async function importFromWorld(world, packageName) {
214
255
  hooks.deregister();
215
256
  }
216
257
  }
217
- async function activate(selected, world, activated, report, onStarted) {
258
+ async function activate(input) {
259
+ const { selected, world, activated, report, cancellation, onStarted } = input;
218
260
  let started = false;
219
261
  const markStarted = () => {
220
262
  if (started)
@@ -231,6 +273,10 @@ async function activate(selected, world, activated, report, onStarted) {
231
273
  diagnostic(report, selected.id, "import", error);
232
274
  return [];
233
275
  }
276
+ if (cancellation.aborted) {
277
+ markStarted();
278
+ return [];
279
+ }
234
280
  let candidate;
235
281
  try {
236
282
  candidate = manifest(module.default);
@@ -264,9 +310,15 @@ async function activate(selected, world, activated, report, onStarted) {
264
310
  diagnostic(report, selected.id, "validation", error);
265
311
  return [];
266
312
  }
313
+ if (cancellation.aborted) {
314
+ markStarted();
315
+ return [];
316
+ }
267
317
  markStarted();
268
318
  try {
269
- const instance = await candidate.activate(context);
319
+ const instance = await candidate.activate(context, cancellation);
320
+ if (cancellation.aborted)
321
+ return [];
270
322
  const registered = handlers(candidate.manifest.id, instance);
271
323
  activated.add(candidate.manifest.id);
272
324
  return registered;
@@ -276,23 +328,12 @@ async function activate(selected, world, activated, report, onStarted) {
276
328
  return [];
277
329
  }
278
330
  }
279
- function deliver(entry, signal, reportDiagnostic, inFlight) {
280
- try {
281
- const delivery = Promise.resolve(entry.handler(signal)).catch((error) => diagnostic(reportDiagnostic, entry.pluginId, "signal", error));
282
- const deliveries = inFlight.get(signal.kind) ?? new Set();
283
- deliveries.add(delivery);
284
- inFlight.set(signal.kind, deliveries);
285
- void delivery.finally(() => deliveries.delete(delivery));
286
- }
287
- catch (error) {
288
- diagnostic(reportDiagnostic, entry.pluginId, "signal", error);
289
- }
290
- }
291
- function deliverTo(handlers, signal, reportDiagnostic, inFlight) {
292
- for (const entry of handlers) {
293
- if (entry.kind === signal.kind)
294
- deliver(entry, signal, reportDiagnostic, inFlight);
295
- }
331
+ async function deliver(entry, signal, reportDiagnostic, timeoutMs) {
332
+ const result = await boundedAttempt((cancellation) => entry.handler(signal, cancellation), timeoutMs);
333
+ if (result.kind === "timed-out")
334
+ diagnostic(reportDiagnostic, entry.pluginId, "signal", timeoutError());
335
+ if (result.kind === "failed")
336
+ diagnostic(reportDiagnostic, entry.pluginId, "signal", result.error);
296
337
  }
297
338
  export async function pluginRuntime(input) {
298
339
  let runtime = PROCESS_RUNTIMES.get(input.world);
@@ -302,101 +343,52 @@ export async function pluginRuntime(input) {
302
343
  }
303
344
  return await runtime;
304
345
  }
305
- export async function drainPluginRuntime(world) {
306
- const runtime = PROCESS_RUNTIMES.get(world);
307
- if (runtime !== undefined)
308
- await (await runtime).drain();
309
- }
310
- async function drainWithDeadline(pending) {
311
- let timeout;
312
- try {
313
- await Promise.race([
314
- pending,
315
- new Promise((resolve) => {
316
- timeout = setTimeout(resolve, PLUGIN_DRAIN_TIMEOUT_MS);
317
- }),
318
- ]);
319
- }
320
- finally {
321
- clearTimeout(timeout);
322
- }
323
- }
324
346
  async function createPluginRuntime(input) {
325
347
  const report = input.reportDiagnostic;
326
348
  const selected = selectedPlugins(input.settings ?? (await settings({ root: input.world })), report);
327
349
  const activated = new Set();
328
- const registered = new Map();
329
- const settled = new Set();
330
- const pending = new Map();
331
- const settledPromises = [];
332
- const inFlight = new Map();
333
350
  let previousStarted = Promise.resolve();
334
- for (const [index, entry] of selected.entries()) {
351
+ const activations = selected.map((entry) => {
335
352
  let releaseStarted;
336
- let releaseSettled;
337
353
  const started = new Promise((resolve) => {
338
354
  releaseStarted = resolve;
339
355
  });
340
- settledPromises.push(new Promise((resolve) => {
341
- releaseSettled = resolve;
342
- }));
343
- void previousStarted.then(async () => {
344
- try {
345
- const handlers = await activate(entry, input.world, activated, report, releaseStarted);
346
- if (handlers.length > 0)
347
- registered.set(index, handlers);
348
- settled.add(index);
349
- const queued = pending.get(index);
350
- if (queued !== undefined) {
351
- pending.delete(index);
352
- for (const signal of queued)
353
- deliverTo(handlers, signal, report, inFlight);
354
- }
355
- }
356
- catch (error) {
357
- diagnostic(report, entry.id, "activation", error);
358
- settled.add(index);
359
- pending.delete(index);
360
- }
361
- finally {
362
- releaseStarted();
363
- releaseSettled();
364
- }
356
+ const activation = previousStarted.then(async () => {
357
+ const result = await boundedAttempt((cancellation) => activate({ selected: entry, world: input.world, activated, report, cancellation, onStarted: releaseStarted }));
358
+ releaseStarted();
359
+ if (result.kind === "settled")
360
+ return { handlers: result.value, timedOut: false };
361
+ diagnostic(report, entry.id, "activation", result.kind === "failed" ? result.error : timeoutError());
362
+ return { handlers: [], timedOut: result.kind === "timed-out" };
365
363
  });
366
364
  previousStarted = started;
367
- }
365
+ return activation;
366
+ });
368
367
  return Object.freeze({
369
- emit(signal, reportDiagnostic = report) {
370
- const deliveries = [...registered.entries()]
371
- .sort(([left], [right]) => left - right)
372
- .flatMap(([, handlers]) => handlers)
373
- .filter((handler) => handler.kind === signal.kind);
374
- for (const [index] of selected.entries()) {
375
- if (!settled.has(index)) {
376
- const queue = pending.get(index);
377
- if (queue === undefined)
378
- pending.set(index, [signal]);
379
- else
380
- queue.push(signal);
381
- }
368
+ async emit(signal, reportDiagnostic = report) {
369
+ const deadline = performance.now() + PLUGIN_ATTEMPT_TIMEOUT_MS;
370
+ const activated = await boundedAttempt(async (cancellation) => {
371
+ const handlers = await Promise.all(activations.map(async (activation, index) => {
372
+ const activatedHandlers = await settleBefore(activation, cancellation);
373
+ if (activatedHandlers !== ABORTED) {
374
+ if (activatedHandlers.timedOut)
375
+ diagnostic(reportDiagnostic, selected[index].id, "signal", timeoutError());
376
+ return activatedHandlers.handlers;
377
+ }
378
+ diagnostic(reportDiagnostic, selected[index].id, "signal", timeoutError());
379
+ return [];
380
+ }));
381
+ return handlers.flat();
382
+ });
383
+ if (activated.kind === "failed") {
384
+ diagnostic(reportDiagnostic, "runtime", "signal", activated.error);
385
+ return;
382
386
  }
383
- for (const entry of deliveries)
384
- deliver(entry, signal, reportDiagnostic, inFlight);
385
- return Promise.resolve();
386
- },
387
- async drain(kind) {
388
- const settle = async () => {
389
- await Promise.all(settledPromises);
390
- for (;;) {
391
- const pending = kind === undefined
392
- ? [...inFlight.values()].flatMap((deliveries) => [...deliveries])
393
- : [...(inFlight.get(kind) ?? [])];
394
- if (pending.length === 0)
395
- return;
396
- await Promise.all(pending);
397
- }
398
- };
399
- await drainWithDeadline(settle());
387
+ if (activated.kind === "timed-out")
388
+ return;
389
+ await Promise.all(activated.value
390
+ .filter((entry) => entry.kind === signal.kind)
391
+ .map((entry) => deliver(entry, signal, reportDiagnostic, Math.max(0, deadline - performance.now()))));
400
392
  },
401
393
  });
402
394
  }
@@ -30,14 +30,15 @@ export async function amendOperation(input) {
30
30
  const initial = await observeContractsForAdmissionAt(input.scope, input.channel, [input.contractId]);
31
31
  source = contractState(initial.decision, input.contractId)?.terms;
32
32
  }
33
+ // Deriving the caller-stamped document is pure: it never depends on the latest Contract.
34
+ const amendment = source === undefined || input.deriveAmendment === undefined
35
+ ? undefined
36
+ : { source, ...input.deriveAmendment(source) };
33
37
  return intentOutcomeWithSeatClose(await withPrivateStatePublicationSeat(input.scope, async (seat) => {
34
38
  const attempts = mintAttempts({ entryCount: 1 });
35
39
  for (let index = 0; index < attempts.length; index += 1) {
36
40
  let observation = await observeContractsForAdmissionAt(input.scope, input.channel, [input.contractId]);
37
41
  const state = contractState(observation.decision, input.contractId);
38
- const amendment = source === undefined || input.deriveAmendment === undefined
39
- ? undefined
40
- : { source, ...input.deriveAmendment(source) };
41
42
  if (amendment !== undefined) {
42
43
  observation = await extendPrerequisiteClosureAt(input.channel, observation, [
43
44
  ...new Set([...(state?.terms.after ?? []), ...amendment.terms.after]),
@@ -35,6 +35,8 @@ export type AttemptTerminal = Readonly<{
35
35
  kind: "collision";
36
36
  }> | PublicationFailed;
37
37
  export type DecidedOfferResult<Refusal = never> = AcceptedAdmission | AttemptTerminal | Readonly<{
38
+ kind: "stale";
39
+ }> | Readonly<{
38
40
  kind: "redecide";
39
41
  }> | Readonly<{
40
42
  kind: "refused";
@@ -1,8 +1,8 @@
1
- import { normalizeTargetBranch, observeBindCoordinates } from "../git/observe.js";
1
+ import { normalizeTargetBranch, observeBindCoordinates, } from "../git/observe.js";
2
2
  import { gitObjectIdForSnapshot } from "../git/identity.js";
3
3
  import { contractId } from "../core/facts/types.js";
4
4
  import { decideBind } from "../core/verbs/bind.js";
5
- import { admitIntent } from "./intent.js";
5
+ import { admitPreparedIntent } from "./intent.js";
6
6
  import { complete } from "./outcome.js";
7
7
  export function decodeTargetInputRefusal(value) {
8
8
  if (value === null || typeof value !== "object" || Array.isArray(value))
@@ -29,19 +29,44 @@ export function decodeForkSourceMovedRefusal(value) {
29
29
  throw new Error("malformed fork-source refusal");
30
30
  }
31
31
  }
32
- async function bindPreparation(input, selection, seed) {
32
+ /**
33
+ * Acquire the repeatable Git coordinates outside the publication seat. The artifact carries the
34
+ * observed start/target plus the expected ref assertions that publication must still confirm.
35
+ */
36
+ async function prepareBindCoordinates(input, selection) {
33
37
  if (input.verification.kind === "refused") {
34
- return { kind: "prepared", input: { ...seed, preparation: input.verification } };
38
+ return { kind: "prepared", prepared: { kind: "declaration-refused", preparation: input.verification } };
35
39
  }
36
40
  const observed = await observeBindCoordinates(input.scope, selection);
37
41
  if (observed === null)
38
- return { kind: "refused", refusal: { kind: "target-missing" } };
39
- if (!("start" in observed)) {
40
- return { kind: "refused", refusal: { kind: "unborn-head" } };
42
+ return { kind: "prepared", prepared: { kind: "unresolved", reason: "target-missing" } };
43
+ if ("kind" in observed)
44
+ return { kind: "prepared", prepared: { kind: "unresolved", reason: "unborn-head" } };
45
+ return {
46
+ kind: "prepared",
47
+ prepared: { kind: "resolved", observed },
48
+ ...(input.coordinates === undefined
49
+ ? { assertions: [{ ref: observed.target ?? "HEAD", oid: gitObjectIdForSnapshot(observed.start) }] }
50
+ : {}),
51
+ };
52
+ }
53
+ /**
54
+ * Assemble the decision input from the fresh in-custody observation. Coordinates fixed by the
55
+ * invocation are never replaced; mutable coordinate disappearance is read again as current evidence.
56
+ */
57
+ async function assembleBindInput(input, selection, seed, prepared) {
58
+ if (prepared.kind === "declaration-refused") {
59
+ return { kind: "prepared", input: { ...seed, preparation: prepared.preparation } };
60
+ }
61
+ if (prepared.kind === "unresolved") {
62
+ const current = await observeBindCoordinates(input.scope, selection);
63
+ if (current === null)
64
+ return { kind: "refused", refusal: { kind: "target-missing" } };
65
+ if ("kind" in current)
66
+ return { kind: "refused", refusal: { kind: "unborn-head" } };
67
+ return { kind: "stale" };
41
68
  }
42
- const start = input.coordinates?.start ?? observed.start;
43
- const oid = gitObjectIdForSnapshot(input.coordinates === undefined ? start : observed.start);
44
- const assertions = input.coordinates === undefined ? [{ ref: observed.target ?? "HEAD", oid }] : [];
69
+ const observed = prepared.observed;
45
70
  return {
46
71
  kind: "prepared",
47
72
  input: {
@@ -50,7 +75,7 @@ async function bindPreparation(input, selection, seed) {
50
75
  kind: "prepared",
51
76
  data: {
52
77
  coordinates: {
53
- start,
78
+ start: input.coordinates?.start ?? observed.start,
54
79
  ...(observed.target === undefined ? {} : { target: observed.target }),
55
80
  workspace: input.workspace,
56
81
  },
@@ -58,7 +83,6 @@ async function bindPreparation(input, selection, seed) {
58
83
  },
59
84
  },
60
85
  },
61
- assertions,
62
86
  };
63
87
  }
64
88
  export async function bindOperation(input) {
@@ -71,13 +95,16 @@ export async function bindOperation(input) {
71
95
  }
72
96
  const at = new Date().toISOString();
73
97
  const id = input.contractId;
74
- return complete(await admitIntent(input.channel, input.scope, {
98
+ return complete(await admitPreparedIntent(input.channel, input.scope, {
75
99
  contractId: id,
76
100
  ...(input.actor === undefined ? {} : { actor: input.actor }),
77
101
  at,
78
102
  }, decideBind, {
79
103
  observedContracts: [id, ...input.terms.after, ...(input.source === undefined ? [] : [input.source.contractId])],
80
- prepareInput: async (_observation, original) => bindPreparation(input, selection, original),
104
+ preparation: {
105
+ external: async () => await prepareBindCoordinates(input, selection),
106
+ assemble: async (_observation, original, prepared) => await assembleBindInput(input, selection, original, prepared),
107
+ },
81
108
  ...(input.source === undefined
82
109
  ? {}
83
110
  : {
@@ -6,6 +6,9 @@ import { type ContractCheckpoint, type ExecutionProgress, type ExecutionStop } f
6
6
  import type { DocumentDerivation, PlacementStop, VerificationStop } from "./operations.js";
7
7
  export type CandidateCompletion = Readonly<{
8
8
  integration: SnapshotId;
9
+ /** The reference this placement advanced and the head it advanced from, both observed at completion time. */
10
+ predecessor?: SnapshotId;
11
+ target?: string;
9
12
  verification?: Readonly<{
10
13
  mode: "ran" | "reused";
11
14
  verdict: "satisfied" | "unsatisfied";
@@ -82,6 +82,8 @@ function completedResult(cursor) {
82
82
  const integration = state.currentIntegration?.snapshot;
83
83
  if (integration === undefined)
84
84
  throw new Error("accepted placement requires its integration snapshot");
85
+ const target = state.coordinates.target;
86
+ const predecessor = state.currentIntegration?.predecessor;
85
87
  // Never attach a superseded run's verdict to the final integration.
86
88
  const current = currentVerifiedAttestation(state);
87
89
  const verification = current === undefined
@@ -96,7 +98,11 @@ function completedResult(cursor) {
96
98
  checkpoint: cursor.checkpoint,
97
99
  evidence: {
98
100
  ...evidence,
99
- completion: { integration, ...(verification === undefined ? {} : { verification }) },
101
+ completion: {
102
+ integration,
103
+ ...(predecessor === undefined || target === undefined ? {} : { predecessor, target }),
104
+ ...(verification === undefined ? {} : { verification }),
105
+ },
100
106
  ...(current === undefined || verification?.mode !== "reused" ? {} : { verificationReuse: current }),
101
107
  ...(current?.verdict !== "unsatisfied" || current.summary === undefined
102
108
  ? {}
@@ -113,17 +119,35 @@ function stoppedResult(cursor, stop) {
113
119
  async function placeCurrentCandidate(input, cursor) {
114
120
  cursor.stage = "placement";
115
121
  input.signal?.throwIfAborted();
116
- const result = await admitPlacement({
117
- channel: input.channel,
118
- repository: input.repository,
119
- progress: input.progress,
120
- target: cursor.checkpoint.state.coordinates.target,
121
- placement: {
122
- contractId: cursor.checkpoint.state.id,
123
- ...(input.actor === undefined ? {} : { actor: input.actor }),
124
- at: timestamp(),
125
- },
122
+ input.progress.observe({
123
+ kind: "stage",
124
+ contractId: cursor.checkpoint.state.id,
125
+ stage: "placement",
126
+ state: "started",
126
127
  });
128
+ const result = await (async () => {
129
+ try {
130
+ return await admitPlacement({
131
+ channel: input.channel,
132
+ repository: input.repository,
133
+ progress: input.progress,
134
+ target: cursor.checkpoint.state.coordinates.target,
135
+ placement: {
136
+ contractId: cursor.checkpoint.state.id,
137
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
138
+ at: timestamp(),
139
+ },
140
+ });
141
+ }
142
+ finally {
143
+ input.progress.observe({
144
+ kind: "stage",
145
+ contractId: cursor.checkpoint.state.id,
146
+ stage: "placement",
147
+ state: "finished",
148
+ });
149
+ }
150
+ })();
127
151
  if (result.kind === "accepted") {
128
152
  cursor.checkpoint = contractCheckpoint(result);
129
153
  input.progress.recordResidue(result.state.id, result);