@astrosheep/keiyaku 4.5.27 → 4.5.28

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 (40) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +3 -3
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +82 -163
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +153 -78
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +68 -64
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +115 -146
  9. package/build/src/akuma/akuma-handle.d.ts +1 -0
  10. package/build/src/akuma/akuma-handle.js +4 -1
  11. package/build/src/akuma/akuma-observe.d.ts +6 -0
  12. package/build/src/akuma/akuma-observe.js +3 -1
  13. package/build/src/akuma/akuma.d.ts +1 -0
  14. package/build/src/akuma/akuma.js +1 -0
  15. package/build/src/akuma/fleet-execution.d.ts +7 -1
  16. package/build/src/akuma/fleet-execution.js +12 -11
  17. package/build/src/akuma/fleet-observation.d.ts +3 -0
  18. package/build/src/akuma/identity.js +11 -9
  19. package/build/src/alias/index.js +3 -3
  20. package/build/src/cli/commands/akuma-invoke.js +13 -4
  21. package/build/src/cli/commands/akuma.js +9 -15
  22. package/build/src/cli/commands/contract-help.d.ts +1 -0
  23. package/build/src/cli/commands/contract-help.js +1 -0
  24. package/build/src/cli/invoke.js +1 -1
  25. package/build/src/cli/render/akuma-activity.js +6 -1
  26. package/build/src/cli/render/akuma.d.ts +9 -0
  27. package/build/src/cli/render/akuma.js +48 -8
  28. package/build/src/identity/normalize.d.ts +3 -0
  29. package/build/src/identity/normalize.js +10 -0
  30. package/build/src/identity/selector.d.ts +1 -0
  31. package/build/src/identity/selector.js +15 -4
  32. package/build/src/library/address.js +2 -2
  33. package/build/src/library/akuma-creation.d.ts +1 -1
  34. package/build/src/library/akuma-creation.js +10 -52
  35. package/build/src/library/composition.d.ts +2 -1
  36. package/build/src/library/fleet.d.ts +2 -1
  37. package/build/src/library/fleet.js +7 -3
  38. package/build/src/library/keiyaku.d.ts +2 -1
  39. package/build/src/protocol/completion.js +9 -2
  40. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { identityCoordinate, identitySegments } from "../identity/coordinates.js";
5
- import { normalizeIdentityStem } from "../identity/normalize.js";
5
+ import { isCanonicalAkumaName, validateAkumaName } from "../identity/normalize.js";
6
6
  const HEX8 = /^[0-9a-f]{8}$/u;
7
7
  export function parsePublicHistoryId(value) {
8
8
  const match = /^turn\/([1-9][0-9]*)$/u.exec(value);
@@ -12,10 +12,7 @@ export function parsePublicHistoryId(value) {
12
12
  return Number.isSafeInteger(sequence) ? sequence : null;
13
13
  }
14
14
  export function archetypeName(value) {
15
- if (value.length === 0 || normalizeIdentityStem({ source: value }) !== value) {
16
- throw new TypeError("Akuma name must be one normalized human identity segment");
17
- }
18
- return value;
15
+ return validateAkumaName(value);
19
16
  }
20
17
  function suffixSegment(value) {
21
18
  if (!HEX8.test(value))
@@ -32,15 +29,20 @@ export function parseAkuId(value) {
32
29
  const segments = identitySegments({ family: "aku", value });
33
30
  if (segments.length !== 2)
34
31
  throw new TypeError("Akuma identity must be aku/<akuma>/<hex8>");
35
- const archetype = archetypeName(segments[0]);
32
+ const archetype = segments[0];
33
+ // Existing identities remain addressable after new births gain a name limit.
34
+ if (!isCanonicalAkumaName(archetype))
35
+ throw new TypeError("Akuma name must be one normalized human identity segment");
36
36
  const suffix = suffixSegment(segments[1]);
37
- return { id: akuId({ archetype, suffix }), archetype, suffix };
37
+ return { id: value, archetype, suffix };
38
38
  }
39
39
  export function akumaRunRoot(worldRoot) {
40
40
  return join(worldRoot, ".keiyaku", "akuma", "run");
41
41
  }
42
42
  export function akumaPaths(input) {
43
- const directory = join(input.runRoot, `${archetypeName(input.archetype)}-${suffixSegment(input.suffix)}`);
43
+ if (!isCanonicalAkumaName(input.archetype))
44
+ throw new TypeError("Akuma name must be one normalized human identity segment");
45
+ const directory = join(input.runRoot, `${input.archetype}-${suffixSegment(input.suffix)}`);
44
46
  return {
45
47
  directory,
46
48
  heart: join(directory, "heart.db"),
@@ -54,7 +56,7 @@ export function akuIdFromDirectoryName(name) {
54
56
  throw new Error(`invalid Akuma run directory ${name}`);
55
57
  const archetype = name.slice(0, -9);
56
58
  const suffix = name.slice(-8);
57
- return { id: akuId({ archetype, suffix }), archetype, suffix };
59
+ return parseAkuId(`aku/${archetype}/${suffix}`);
58
60
  }
59
61
  export async function ensureAkumaRunRoot(worldRoot) {
60
62
  const runRoot = akumaRunRoot(worldRoot);
@@ -4,7 +4,7 @@ import { parseAkuId } from "../akuma/identity.js";
4
4
  import { replaceFileDurably } from "../coordination/durable-file.js";
5
5
  import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
6
6
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
7
- import { parseAkumaAlias } from "../identity/selector.js";
7
+ import { parseAkumaAlias, parseReadableAkumaAlias } from "../identity/selector.js";
8
8
  const VERSION = 1;
9
9
  function paths(world) {
10
10
  if (typeof world !== "string" || world.trim().length === 0) {
@@ -56,7 +56,7 @@ function decode(path, bytes) {
56
56
  if (typeof rawAkuId !== "string")
57
57
  corruption(`Alias target must be an AkuId: ${path}`);
58
58
  try {
59
- bindings.push({ alias: parseAkumaAlias(rawAlias), akuId: parseAkuId(rawAkuId).id });
59
+ bindings.push({ alias: parseReadableAkumaAlias(rawAlias), akuId: parseAkuId(rawAkuId).id });
60
60
  }
61
61
  catch (error) {
62
62
  return corruption(`Alias binding is invalid: ${path}`, error);
@@ -114,7 +114,7 @@ export async function readAliases(world) {
114
114
  return await read(paths(world).authority);
115
115
  }
116
116
  export async function resolveAlias(world, value) {
117
- const alias = parseAkumaAlias(value);
117
+ const alias = parseReadableAkumaAlias(value);
118
118
  return (await readAliases(world)).find((binding) => binding.alias === alias)?.akuId ?? null;
119
119
  }
120
120
  export async function moveAlias(input) {
@@ -4,7 +4,7 @@ import { emitInitiatingPluginSignal } from "../../plugin/akuma-signals.js";
4
4
  import { observeAkumaStatus } from "../../akuma/akuma-observe.js";
5
5
  import { AuthorityCorruptionError, Keiyaku, } from "../../index.js";
6
6
  import { callObservationStream, waitObservationStream } from "../render/akuma-activity.js";
7
- import { callObservationHead, waitedTellProgress } from "../render/akuma.js";
7
+ import { callObservationHead, tellWaitProgressStream } from "../render/akuma.js";
8
8
  import { killAkuma, tellAkuma, tellWaitAkuma, waitAkuma } from "../../library/fleet.js";
9
9
  import { localExecutionContext } from "../../akuma/requests.js";
10
10
  import { Akuma, Schema } from "../../akuma/index.js";
@@ -188,6 +188,8 @@ function decodeWaitedTellSchema(result, schema) {
188
188
  }
189
189
  async function invokeWaitedTell(command, input, body) {
190
190
  const schema = command.schema === undefined ? undefined : await schemaFromFile(command.schema);
191
+ const alias = inputAlias(command.akuma);
192
+ const progress = command.output === "text" ? tellWaitProgressStream(undefined, alias, resultContext()) : undefined;
191
193
  const result = await tellWaitAkuma({
192
194
  ...(await inputInitiator(input)),
193
195
  path: input.path,
@@ -198,11 +200,18 @@ async function invokeWaitedTell(command, input, body) {
198
200
  ...(command.interrupt ? { interrupt: true } : {}),
199
201
  ...(input.repo === undefined ? {} : { repo: input.repo }),
200
202
  ...(input.signal === undefined ? {} : { signal: input.signal }),
203
+ ...(progress === undefined
204
+ ? {}
205
+ : {
206
+ observe: {
207
+ admitted: (tell, id) => writeProgress(progress.admitted(tell, id).join("\n")),
208
+ observe: (observation) => writeProgress(progress.observe(observation).join("\n")),
209
+ },
210
+ }),
201
211
  }, input.execution ?? localExecutionContext());
202
212
  const rendered = schema === undefined ? result : decodeWaitedTellSchema(result, schema);
203
- const alias = inputAlias(command.akuma);
204
- if (command.output === "text")
205
- writeProgress(waitedTellProgress(rendered, alias, resultContext()));
213
+ if (progress !== undefined)
214
+ writeProgress(progress.conclude(rendered).join("\n"));
206
215
  return {
207
216
  kind: "akuma",
208
217
  action: "tell",
@@ -11,20 +11,19 @@ const AKUMA_COMMAND_SPECS = {
11
11
  contract: "value",
12
12
  alias: "value",
13
13
  wait: "value",
14
- detach: "boolean",
15
14
  allowed: "repeatable",
16
15
  json: "boolean",
17
16
  schema: "value",
18
17
  },
19
- usage: "call <akuma-name> [--contract <kei/...>] [--workdir <path>] [--alias @name] [--allowed <product.action>]... [--schema <file>] [--wait <duration> | -d | --detach] (<prompt> | -)",
18
+ usage: "call <akuma-name> [--contract <kei/...>] [--workdir <path>] [--alias <name>] [--allowed <product.action>]... [--schema <file>] [--wait <duration>] (<prompt> | -)",
20
19
  purpose: "Birth an Akuma from <akuma-name> with one prompt.",
21
20
  details: [
22
21
  "Give <prompt> as one argument, or use - to read stdin.",
23
- "Default: --wait 5m. An explicit --wait replaces that duration; -d and --detach return after birth.",
24
- "--contract dispatches the born Akuma to that Contract.",
22
+ "Default: return after birth without waiting for the first answer. Explicit --wait observes the first work for that duration.",
23
+ "--contract associates the born Akuma with that Contract for Dispatch; it never selects a workdir.",
25
24
  "--workdir selects the execution directory; a relative path is relative to the invocation cwd.",
26
- "Without --workdir, a Contract call uses its appointed worktree; an unassociated call uses the invocation cwd.",
27
- "--alias assigns the world-local @name selector to the born Akuma.",
25
+ "Without --workdir, the call uses the invocation cwd whether or not --contract is present.",
26
+ "--alias <name> assigns the world-local @name selector to the born Akuma; @name is also accepted.",
28
27
  `Legal actions: ${ALLOWED_ACTIONS.join(", ")}.`,
29
28
  `Default actions: ${DEFAULT_ALLOWED_ACTIONS.join(", ")}.`,
30
29
  "Repeated --allowed adds actions to the selected Akuma's defaults; it never narrows them.",
@@ -156,11 +155,7 @@ function scanAkuma(action, argv, fail) {
156
155
  stdin = true;
157
156
  }
158
157
  else if (token === "-d") {
159
- if (action !== "call")
160
- fail(`option ${token} is not valid for ${action}`);
161
- if (flags.detach !== undefined)
162
- fail("duplicate option: --detach");
163
- flags.detach = true;
158
+ fail(`option ${token} is not valid for ${action}`);
164
159
  }
165
160
  else if (!token.startsWith("--")) {
166
161
  if (isBlankInput(token))
@@ -303,15 +298,14 @@ function parseCall(flags, archetype, prompt, output, fail) {
303
298
  let alias;
304
299
  if (flags.alias !== undefined) {
305
300
  try {
306
- alias = parseAkumaAlias(stringFlag(flags.alias, "--alias requires @name", fail));
301
+ const name = stringFlag(flags.alias, "--alias requires a name", fail);
302
+ alias = parseAkumaAlias(name.startsWith("@") ? name : `@${name}`);
307
303
  }
308
304
  catch (error) {
309
305
  fail(error instanceof Error ? error.message : "invalid Akuma alias");
310
306
  }
311
307
  }
312
- if (flags.detach === true && flags.wait !== undefined)
313
- fail("call --wait and --detach are mutually exclusive");
314
- const mode = flags.detach === true ? "detach" : "wait";
308
+ const mode = flags.wait === undefined ? "detach" : "wait";
315
309
  const timeoutMs = flags.wait === undefined ? undefined : parseDuration(flags.wait, "--wait", fail);
316
310
  const allowed = parseAllowedFlag(flags.allowed, fail);
317
311
  const schema = flags.schema === undefined ? undefined : stringFlag(flags.schema, "call --schema requires a file", fail);
@@ -94,6 +94,7 @@ export declare const CONTRACT_COMMAND_SPECS: {
94
94
  };
95
95
  readonly usage: "status [<contract>|@name|<aku/...>]...";
96
96
  readonly purpose: "Read the world status board or one or more Contract and Akuma projections.";
97
+ readonly details: "An Akuma projection reports that Akuma's frozen execution workdir; --contract associates a Contract without selecting one.";
97
98
  };
98
99
  readonly show: {
99
100
  readonly positional: "optional";
@@ -166,6 +166,7 @@ export const CONTRACT_COMMAND_SPECS = {
166
166
  flags: { json: "boolean" },
167
167
  usage: "status [<contract>|@name|<aku/...>]...",
168
168
  purpose: "Read the world status board or one or more Contract and Akuma projections.",
169
+ details: "An Akuma projection reports that Akuma's frozen execution workdir; --contract associates a Contract without selecting one.",
169
170
  },
170
171
  show: {
171
172
  positional: "optional",
@@ -348,7 +348,7 @@ async function callExecutionCwd(input) {
348
348
  if (input.workdir !== undefined) {
349
349
  return await canonicalBirthCwd(resolve(input.cwd, input.workdir), `workdir is not an existing directory: ${input.workdir}`);
350
350
  }
351
- return input.command.contract === undefined ? input.cwd : undefined;
351
+ return input.cwd;
352
352
  }
353
353
  async function invokeParsedAkuma(input) {
354
354
  const executionCwd = await callExecutionCwd({
@@ -872,6 +872,7 @@ function answeredBlock(observation, answer, alias, columns) {
872
872
  function snapshotCore(view, context, options) {
873
873
  const activity = snapshotActivityLines(view.status.timeline, context);
874
874
  const facts = [
875
+ ...(view.status.cwd === undefined ? [] : [`cwd ${safeText(view.status.cwd)}`]),
875
876
  ...(options.showAllowed === true ? [`allowed ${view.status.allowed.join(", ") || "none"}`] : []),
876
877
  ...(view.status.readonly?.enforcement === "none" ? [`! ${safeText(view.status.readonly.diagnostic)}`] : []),
877
878
  ...contractFacts(view.contract),
@@ -1050,7 +1051,11 @@ export function historyText(command, result, context) {
1050
1051
  export function tellText(result, context) {
1051
1052
  const wake = result.result.tell.wake;
1052
1053
  const target = identity(result.result.akuma, result.alias);
1053
- const row = groupedRows([result.result.tell.row], context).join("\n");
1054
+ const row = groupedRows([result.result.tell.row], context, false, {
1055
+ ...plainLayout(),
1056
+ head: (time, _glyph, _verb, columns) => eventPrefix(wake.kind === "held" ? "⧗" : "⧖", "tell", time, columns),
1057
+ singleLine: true,
1058
+ }).join("\n");
1054
1059
  if (wake.kind === "failed") {
1055
1060
  const child = "child" in wake ? wake.child : undefined;
1056
1061
  const failure = `! tell delivery failed · ${safeText(wake.diagnostic)}${child === undefined ? "" : ` · log ${child.log.path} ${child.log.from}..${child.log.to}`}`;
@@ -2,7 +2,16 @@ import type { AkumaInvocationResult } from "../commands/akuma-invoke.js";
2
2
  import type { ParsedCommand } from "../parse.js";
3
3
  import { type ObservedCallHead } from "./akuma-activity.js";
4
4
  import type { AkumaTellWaitResult } from "../../akuma/fleet-observation.js";
5
+ import type { AkuId } from "../../akuma/identity.js";
6
+ import type { LiveStatusObservation } from "../../akuma/akuma-observe.js";
7
+ import type { TellResult } from "../../akuma/akuma.js";
5
8
  import { type TextRenderContext } from "./terminal.js";
9
+ export type TellWaitProgress = Readonly<{
10
+ admitted: (tell: TellResult, id: AkuId) => readonly string[];
11
+ observe: (observation: LiveStatusObservation) => readonly string[];
12
+ conclude: (result: AkumaTellWaitResult) => readonly string[];
13
+ }>;
14
+ export declare function tellWaitProgressStream(akuma: AkuId | undefined, alias: string | undefined, context: TextRenderContext): TellWaitProgress;
6
15
  export declare function waitedTellProgress(result: AkumaTellWaitResult, alias: string | undefined, context: TextRenderContext): string;
7
16
  export declare function akumaRawAnswer(result: AkumaInvocationResult): string | undefined;
8
17
  /**
@@ -1,6 +1,52 @@
1
1
  import { parseAkumaStatus } from "../../akuma/akuma.js";
2
- import { DEFAULT_CONTEXT, akumaRawAnswer as akumaActivityRawAnswer, associatedIdentity, historyText, killResultText, mutationObservationStageText, snapshotHeading, snapshotText, tellText, waitText, } from "./akuma-activity.js";
2
+ import { activityStream, DEFAULT_CONTEXT, akumaRawAnswer as akumaActivityRawAnswer, associatedIdentity, historyText, killResultText, mutationObservationStageText, snapshotHeading, snapshotText, tellText, waitText, } from "./akuma-activity.js";
3
3
  import { safeText } from "./terminal.js";
4
+ function tellWaitConclusion(result) {
5
+ return result.observation.reason === "answered"
6
+ ? "✓ answered"
7
+ : result.observation.reason === "failed"
8
+ ? `! failed · ${safeText(result.observation.diagnostic)}`
9
+ : result.observation.reason === "unanswered"
10
+ ? "○ unanswered"
11
+ : "⧖ deadline";
12
+ }
13
+ export function tellWaitProgressStream(akuma, alias, context) {
14
+ let target = akuma;
15
+ const stream = activityStream(context);
16
+ let opened = false;
17
+ return {
18
+ admitted: (tell, id) => {
19
+ target = id;
20
+ if (target === undefined)
21
+ throw new Error("Tell progress admitted without identity");
22
+ return [
23
+ tellText({
24
+ kind: "akuma",
25
+ action: "tell",
26
+ mode: "ordinary",
27
+ result: { akuma: target, tell },
28
+ body: "",
29
+ ...(alias === undefined ? {} : { alias }),
30
+ }, context),
31
+ ];
32
+ },
33
+ observe: (observation) => {
34
+ const lines = [];
35
+ if (target === undefined)
36
+ throw new Error("Tell progress observed before admission");
37
+ if (!opened) {
38
+ opened = true;
39
+ lines.push(...snapshotHeading(target, alias, undefined));
40
+ lines.push(...stream.seed({ snapshot: observation.status.timeline, rows: observation.rows }));
41
+ }
42
+ else {
43
+ lines.push(...stream({ snapshot: observation.status.timeline, rows: observation.rows }));
44
+ }
45
+ return lines;
46
+ },
47
+ conclude: (result) => [...stream.flush(), tellWaitConclusion(result)],
48
+ };
49
+ }
4
50
  export function waitedTellProgress(result, alias, context) {
5
51
  const receipt = tellText({
6
52
  kind: "akuma",
@@ -10,13 +56,7 @@ export function waitedTellProgress(result, alias, context) {
10
56
  body: "",
11
57
  ...(alias === undefined ? {} : { alias }),
12
58
  }, context);
13
- const conclusion = result.observation.reason === "answered"
14
- ? "✓ answered"
15
- : result.observation.reason === "failed"
16
- ? `! failed · ${safeText(result.observation.diagnostic)}`
17
- : result.observation.reason === "unanswered"
18
- ? "○ unanswered"
19
- : "⧖ deadline";
59
+ const conclusion = tellWaitConclusion(result);
20
60
  return `${receipt}\n${conclusion}`;
21
61
  }
22
62
  function waitedTellRawAnswer(result) {
@@ -1,3 +1,6 @@
1
+ export declare const MAX_AKUMA_NAME_BYTES = 64;
2
+ export declare function isCanonicalAkumaName(value: string): boolean;
3
+ export declare function validateAkumaName(value: string): string;
1
4
  export declare function normalizeIdentityStem(input: Readonly<{
2
5
  source: string;
3
6
  }>): string;
@@ -5,6 +5,16 @@ const SUFFIX = /^[\p{Letter}\p{Number}]+$/u;
5
5
  function graphemes(value) {
6
6
  return [...GRAPHEMES.segment(value)].map(({ segment }) => segment);
7
7
  }
8
+ export const MAX_AKUMA_NAME_BYTES = 64;
9
+ export function isCanonicalAkumaName(value) {
10
+ return value.length > 0 && normalizeIdentityStem({ source: value }) === value;
11
+ }
12
+ export function validateAkumaName(value) {
13
+ if (!isCanonicalAkumaName(value) || Buffer.byteLength(value, "utf8") > MAX_AKUMA_NAME_BYTES) {
14
+ throw new TypeError(`Akuma name must be one normalized human identity segment of at most ${MAX_AKUMA_NAME_BYTES} UTF-8 bytes`);
15
+ }
16
+ return value;
17
+ }
8
18
  export function normalizeIdentityStem(input) {
9
19
  const source = input.source.normalize("NFKC").toLowerCase().normalize("NFKC");
10
20
  let result = "";
@@ -6,6 +6,7 @@ export type AkumaAlias = string & {
6
6
  export type AkumaGlob = string & {
7
7
  readonly [AKUMA_GLOB]: true;
8
8
  };
9
+ export declare function parseReadableAkumaAlias(value: string): AkumaAlias;
9
10
  export declare function parseAkumaAlias(value: string): AkumaAlias;
10
11
  export declare function parseAkumaGlob(value: string): AkumaGlob;
11
12
  export declare function matchesAkumaGlob(glob: AkumaGlob, value: string): boolean;
@@ -1,8 +1,20 @@
1
- const AKUMA_ALIAS_PATTERN = /^@[a-z][a-z0-9-]{0,63}$/u;
1
+ import { MAX_AKUMA_NAME_BYTES, normalizeIdentityStem, validateAkumaName } from "./normalize.js";
2
2
  const AKUMA_GLOB_SUFFIX = /^[0-9a-f*]+$/u;
3
+ const LEGACY_AKUMA_ALIAS_PATTERN = /^@[a-z][a-z0-9-]{0,63}$/u;
4
+ // Old alias spellings remain readable, but new bindings use canonical Akuma names.
5
+ export function parseReadableAkumaAlias(value) {
6
+ if (LEGACY_AKUMA_ALIAS_PATTERN.test(value))
7
+ return value;
8
+ return parseAkumaAlias(value);
9
+ }
3
10
  export function parseAkumaAlias(value) {
4
- if (!AKUMA_ALIAS_PATTERN.test(value)) {
5
- throw new TypeError("Akuma alias must match ^@[a-z][a-z0-9-]{0,63}$");
11
+ if (!value.startsWith("@"))
12
+ throw new TypeError("Akuma alias selector must start with @");
13
+ try {
14
+ validateAkumaName(value.slice(1));
15
+ }
16
+ catch {
17
+ throw new TypeError(`Akuma alias name must be a normalized Akuma name of at most ${MAX_AKUMA_NAME_BYTES} UTF-8 bytes`);
6
18
  }
7
19
  return value;
8
20
  }
@@ -26,4 +38,3 @@ export function matchesAkumaGlob(glob, value) {
26
38
  .replaceAll("*", "[^/]*");
27
39
  return new RegExp(`^${pattern}$`, "u").test(value);
28
40
  }
29
- import { normalizeIdentityStem } from "./normalize.js";
@@ -5,7 +5,7 @@ import { akumaAddressability, requireBornAkuma } from "../akuma/akuma-probe.js";
5
5
  import { parseAkuId } from "../akuma/identity.js";
6
6
  import { contractId } from "../core/facts/types.js";
7
7
  import { readDispatches } from "../dispatch/index.js";
8
- import { matchesAkumaGlob, parseAkumaAlias, parseAkumaGlob, } from "../identity/selector.js";
8
+ import { matchesAkumaGlob, parseReadableAkumaAlias, parseAkumaGlob, } from "../identity/selector.js";
9
9
  import { World } from "../world.js";
10
10
  import { requireInput } from "./input.js";
11
11
  import { scopeForRepo } from "./repo.js";
@@ -34,7 +34,7 @@ function nonblank(value, label) {
34
34
  }
35
35
  function parseAddressAlias(selector) {
36
36
  try {
37
- return parseAkumaAlias(selector);
37
+ return parseReadableAkumaAlias(selector);
38
38
  }
39
39
  catch (error) {
40
40
  if (!(error instanceof TypeError))
@@ -73,7 +73,7 @@ export type CallResult = Readonly<{
73
73
  readonly?: ReadonlyRestraint;
74
74
  execution: Readonly<{
75
75
  cwd: string;
76
- source: "input" | "contract-worktree" | "caller" | "process" | "world";
76
+ source: "input" | "caller" | "process" | "world";
77
77
  }>;
78
78
  dispatch: DispatchStage;
79
79
  alias: AliasStage;
@@ -9,7 +9,6 @@ import { AuthorityCorruptionError } from "../core/facts/errors.js";
9
9
  import { publishDispatch, readDispatch } from "../dispatch/index.js";
10
10
  import { parseAkumaAlias } from "../identity/selector.js";
11
11
  import { emitCalledPluginSignal } from "../plugin/akuma-signals.js";
12
- import { readManagedWorktreeAppointment } from "../workspace-place.js";
13
12
  import { World } from "../world.js";
14
13
  import { Akuma as PublicAkuma } from "../akuma/akuma-instance.js";
15
14
  import { requestForwardedFleetTellAnswer } from "../akuma/fleet-request.js";
@@ -17,7 +16,6 @@ import { localExecutionContext } from "../akuma/requests.js";
17
16
  import { callReadonly, canonicalBirthCwd } from "../akuma/call-input.js";
18
17
  import { requireInput } from "./input.js";
19
18
  import { addressAkuma } from "./address.js";
20
- import { KeiyakuRefused } from "./contract.js";
21
19
  import { seatForKeiyaku } from "./contract-handle.js";
22
20
  import { scopeForRepo } from "./repo.js";
23
21
  function nonblank(value, label) {
@@ -67,10 +65,10 @@ function integrationFailure(error) {
67
65
  };
68
66
  }
69
67
  function callMode(value) {
70
- if (value === undefined || value === "wait")
71
- return "wait";
72
- if (value === "detach")
68
+ if (value === undefined || value === "detach")
73
69
  return "detach";
70
+ if (value === "wait")
71
+ return "wait";
74
72
  throw new TypeError("mode must be wait or detach");
75
73
  }
76
74
  function callTimeout(value, mode) {
@@ -196,49 +194,13 @@ async function forkDispatchStage(input) {
196
194
  return { kind: "failed", failure: integrationFailure(error) };
197
195
  }
198
196
  }
199
- function unavailableWorkspace(contractId, detail) {
200
- return new Error(`Contract workspace is unavailable: ${contractId} ${detail}`);
201
- }
202
- async function currentManagedContract(contract, contractId) {
203
- let state;
204
- try {
205
- state = await contract.state();
206
- }
207
- catch (error) {
208
- if (error instanceof Error && error.message === `contract does not exist: ${contractId}`) {
209
- throw new KeiyakuRefused({ kind: "contract-missing", contractId });
210
- }
211
- throw error;
212
- }
213
- if (state.terminal !== null)
214
- throw new KeiyakuRefused({ kind: "terminal", contractId: state.id });
215
- return state;
216
- }
217
197
  async function resolveCallExecution(input) {
218
- if (input.cwd !== undefined) {
219
- return {
220
- cwd: await canonicalBirthCwd(input.cwd),
221
- source: "input",
222
- };
223
- }
224
- if (input.contract !== undefined) {
225
- const seat = seatForKeiyaku(input.contract);
226
- if (seat === null)
227
- throw new TypeError("contract must be a Keiyaku");
228
- const state = await currentManagedContract(input.contract, seat.id);
229
- const appointment = await readManagedWorktreeAppointment(seat.scope, state.id);
230
- if (appointment.kind === "unappointed") {
231
- throw unavailableWorkspace(state.id, "is unappointed; use reconcile");
232
- }
233
- if (appointment.kind === "failed") {
234
- throw new Error(`${appointment.diagnostic}; use reconcile`);
235
- }
236
- return {
237
- cwd: await canonicalBirthCwd(appointment.path, `Contract workspace is unavailable: ${appointment.path}`),
238
- source: "contract-worktree",
239
- };
240
- }
241
- return undefined;
198
+ if (input.cwd === undefined)
199
+ return undefined;
200
+ return {
201
+ cwd: await canonicalBirthCwd(input.cwd),
202
+ source: "input",
203
+ };
242
204
  }
243
205
  async function resolveAliasStage(path, alias, dispatch, akuma) {
244
206
  if (alias === undefined)
@@ -326,11 +288,7 @@ function schemaTell(input) {
326
288
  }
327
289
  async function prepareCall(input, context) {
328
290
  const parsed = await parseCallInput(input);
329
- const execution = await resolveCallExecution({
330
- path: parsed.path,
331
- ...(parsed.cwd === undefined ? {} : { cwd: parsed.cwd }),
332
- ...(parsed.seat === undefined ? {} : { contract: parsed.values.contract }),
333
- });
291
+ const execution = await resolveCallExecution(parsed.cwd === undefined ? {} : { cwd: parsed.cwd });
334
292
  const world = akumaWorld(parsed.path, parsed.home, parsed.settings, context);
335
293
  const { born, akuma } = await admitCall({
336
294
  path: parsed.path,
@@ -16,7 +16,7 @@ export declare function composeLibrary(context?: ExecutionContext, composition?:
16
16
  readonly?: import("../akuma/provider-recipe.js").ReadonlyRestraint;
17
17
  execution: Readonly<{
18
18
  cwd: string;
19
- source: "input" | "contract-worktree" | "caller" | "process" | "world";
19
+ source: "input" | "caller" | "process" | "world";
20
20
  }>;
21
21
  dispatch: import("./akuma-creation.js").DispatchStage;
22
22
  alias: import("./akuma-creation.js").AliasStage;
@@ -357,6 +357,7 @@ export declare function composeLibrary(context?: ExecutionContext, composition?:
357
357
  };
358
358
  } | undefined;
359
359
  };
360
+ cwd?: string | undefined;
360
361
  readonly?: {
361
362
  enforcement: "native";
362
363
  } | {
@@ -1,7 +1,7 @@
1
1
  /** @architectureCompositionRoot */
2
2
  import { type ActivityHistory, type OutcomeRow, type AkumaStatus, type InterruptReceipt } from "../akuma/akuma.js";
3
3
  import { type ExecutionContext } from "../akuma/requests.js";
4
- import type { WaitObserver } from "../akuma/fleet-execution.js";
4
+ import type { TellWaitObserver, WaitObserver } from "../akuma/fleet-execution.js";
5
5
  import { type DispatchAssociation } from "../dispatch/index.js";
6
6
  import { type Schema } from "../akuma/schema.js";
7
7
  import { type AkumaAddressInput, type AkumaSetAddressInput } from "./address.js";
@@ -23,6 +23,7 @@ export type AkumaTellWaitInput = AkumaAddressInput & Readonly<{
23
23
  interrupt?: boolean;
24
24
  initiator?: string;
25
25
  signal?: AbortSignal;
26
+ observe?: TellWaitObserver;
26
27
  }>;
27
28
  export type { TellResult, TellWake } from "../akuma/akuma.js";
28
29
  export type { CreatedTaskObservation } from "../task/created-observation.js";
@@ -285,10 +285,9 @@ export async function tellAkuma(input, execution = localExecutionContext()) {
285
285
  ...(callerSignal === undefined ? {} : { signal: callerSignal }),
286
286
  });
287
287
  }
288
- export async function tellWaitAkuma(input, execution = localExecutionContext()) {
289
- const values = requireInput(input, "Keiyaku tell wait input");
288
+ function validateTellWaitInput(values) {
290
289
  for (const key of Object.keys(values)) {
291
- if (!["path", "akuma", "body", "repo", "timeoutMs", "schema", "interrupt", "initiator", "signal"].includes(key)) {
290
+ if (!["path", "akuma", "body", "repo", "timeoutMs", "schema", "interrupt", "initiator", "signal", "observe"].includes(key)) {
292
291
  throw new TypeError(`Keiyaku tell wait input has unknown field: ${key}`);
293
292
  }
294
293
  }
@@ -302,6 +301,10 @@ export async function tellWaitAkuma(input, execution = localExecutionContext())
302
301
  }
303
302
  if (values.interrupt !== undefined && typeof values.interrupt !== "boolean")
304
303
  throw new TypeError("interrupt must be a boolean");
304
+ }
305
+ export async function tellWaitAkuma(input, execution = localExecutionContext()) {
306
+ const values = requireInput(input, "Keiyaku tell wait input");
307
+ validateTellWaitInput(values);
305
308
  const callerSignal = signal(values.signal);
306
309
  const channel = executionChannel(execution);
307
310
  if (channel.kind === "body-request") {
@@ -328,6 +331,7 @@ export async function tellWaitAkuma(input, execution = localExecutionContext())
328
331
  ...(input.schema === undefined ? {} : { schemaJson: schemaJsonText(input.schema) }),
329
332
  ...(input.interrupt === true ? { interrupt: true } : {}),
330
333
  ...(input.initiator === undefined ? {} : { initiator: input.initiator }),
334
+ ...(input.observe === undefined ? {} : { onObserve: input.observe }),
331
335
  ...(callerSignal === undefined ? {} : { signal: callerSignal }),
332
336
  });
333
337
  }
@@ -48,7 +48,7 @@ export declare const Keiyaku: Readonly<{
48
48
  readonly?: import("../akuma/provider-recipe.js").ReadonlyRestraint;
49
49
  execution: Readonly<{
50
50
  cwd: string;
51
- source: "input" | "contract-worktree" | "caller" | "process" | "world";
51
+ source: "input" | "caller" | "process" | "world";
52
52
  }>;
53
53
  dispatch: import("./akuma-creation.js").DispatchStage;
54
54
  alias: import("./akuma-creation.js").AliasStage;
@@ -389,6 +389,7 @@ export declare const Keiyaku: Readonly<{
389
389
  };
390
390
  } | undefined;
391
391
  };
392
+ cwd?: string | undefined;
392
393
  readonly?: {
393
394
  enforcement: "native";
394
395
  } | {
@@ -3,6 +3,7 @@ import { admitPlacement, observeTargetPlacement } from "./placement.js";
3
3
  import { reintegrateOperation } from "./reintegrate.js";
4
4
  import { contractCheckpoint, executionStop, } from "./progress.js";
5
5
  import { placementStop, timestamp, unpackVerificationOutcome } from "./operations.js";
6
+ import { VERIFIED } from "../verification/declaration.js";
6
7
  const MAX_REINTEGRATION_CYCLES = 3;
7
8
  function reintegrationStop(result) {
8
9
  if (result.kind === "placement-failed")
@@ -154,15 +155,21 @@ async function placeCurrentCandidate(input, cursor) {
154
155
  }
155
156
  return result;
156
157
  }
158
+ /** Verification blocks completion only when the Contract selected the verified gate; placement owns gate eligibility. */
159
+ function verificationBlocks(state) {
160
+ return state.terms.gates.includes(VERIFIED);
161
+ }
157
162
  async function verifyCandidateReadiness(input, cursor) {
158
163
  const observedPlacement = await observeCandidateTarget(input, cursor);
159
164
  await verifyCurrentCandidate(input, cursor);
160
- if (cursor.evidence.verification !== undefined || observedPlacement !== undefined) {
165
+ const verification = cursor.evidence.verification;
166
+ const blocking = verification !== undefined && verificationBlocks(cursor.checkpoint.state);
167
+ if (blocking || observedPlacement !== undefined) {
161
168
  return {
162
169
  kind: "stopped",
163
170
  checkpoint: cursor.checkpoint,
164
171
  evidence: { ...cursor.evidence, ...(observedPlacement === undefined ? {} : { placement: observedPlacement }) },
165
- stop: cursor.evidence.verification ?? observedPlacement,
172
+ stop: blocking ? verification : observedPlacement,
166
173
  };
167
174
  }
168
175
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "4.5.27",
3
+ "version": "4.5.28",
4
4
  "workspaces": [
5
5
  "plugins/*"
6
6
  ],