@astrosheep/keiyaku 4.5.26 → 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 (98) 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 -162
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +153 -76
  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-errors.d.ts +6 -0
  10. package/build/src/akuma/akuma-errors.js +11 -0
  11. package/build/src/akuma/akuma-handle.d.ts +54 -1
  12. package/build/src/akuma/akuma-handle.js +79 -8
  13. package/build/src/akuma/akuma-instance.js +6 -22
  14. package/build/src/akuma/akuma-observe.d.ts +6 -0
  15. package/build/src/akuma/akuma-observe.js +3 -1
  16. package/build/src/akuma/akuma-probe.d.ts +20 -1
  17. package/build/src/akuma/akuma-probe.js +20 -3
  18. package/build/src/akuma/akuma.d.ts +13 -0
  19. package/build/src/akuma/akuma.js +1 -0
  20. package/build/src/akuma/allowed.d.ts +2 -1
  21. package/build/src/akuma/allowed.js +9 -4
  22. package/build/src/akuma/archetype.js +3 -3
  23. package/build/src/akuma/body.js +8 -3
  24. package/build/src/akuma/fleet-execution.d.ts +13 -2
  25. package/build/src/akuma/fleet-execution.js +45 -0
  26. package/build/src/akuma/fleet-observation.d.ts +241 -0
  27. package/build/src/akuma/fleet-observation.js +13 -0
  28. package/build/src/akuma/fleet-owner-port.d.ts +12 -0
  29. package/build/src/akuma/fleet-owner-port.js +46 -0
  30. package/build/src/akuma/fleet-request.d.ts +55 -4
  31. package/build/src/akuma/fleet-request.js +101 -6
  32. package/build/src/akuma/heart/index.d.ts +1 -0
  33. package/build/src/akuma/heart/index.js +4 -1
  34. package/build/src/akuma/heart/rows.d.ts +1 -0
  35. package/build/src/akuma/heart/rows.js +8 -0
  36. package/build/src/akuma/heart/soul.js +2 -2
  37. package/build/src/akuma/identity.js +11 -9
  38. package/build/src/akuma/projection.d.ts +36 -0
  39. package/build/src/akuma/projection.js +2 -1
  40. package/build/src/akuma/provider.d.ts +14 -0
  41. package/build/src/akuma/provider.js +66 -4
  42. package/build/src/akuma/providers/acp/events.js +16 -8
  43. package/build/src/akuma/providers/claude/events.js +5 -7
  44. package/build/src/akuma/providers/opencode-sdk/events.js +9 -5
  45. package/build/src/akuma/providers/pi/events.js +4 -3
  46. package/build/src/akuma/request-lifecycle.js +10 -1
  47. package/build/src/akuma-body.js +1 -33
  48. package/build/src/alias/index.js +3 -3
  49. package/build/src/cli/commands/akuma-invoke.d.ts +8 -0
  50. package/build/src/cli/commands/akuma-invoke.js +67 -6
  51. package/build/src/cli/commands/akuma.d.ts +1 -0
  52. package/build/src/cli/commands/akuma.js +19 -30
  53. package/build/src/cli/commands/contract-help.d.ts +1 -0
  54. package/build/src/cli/commands/contract-help.js +9 -5
  55. package/build/src/cli/invoke.js +1 -1
  56. package/build/src/cli/render/akuma-activity.d.ts +7 -1
  57. package/build/src/cli/render/akuma-activity.js +164 -50
  58. package/build/src/cli/render/akuma-tool.d.ts +5 -0
  59. package/build/src/cli/render/akuma-tool.js +118 -14
  60. package/build/src/cli/render/akuma.d.ts +12 -1
  61. package/build/src/cli/render/akuma.js +74 -2
  62. package/build/src/cli/render/catalog.js +2 -3
  63. package/build/src/cli/render/contract-observation.d.ts +1 -0
  64. package/build/src/cli/render/contract-observation.js +20 -1
  65. package/build/src/cli/render/contract.js +4 -4
  66. package/build/src/cli/render/execution-progress.d.ts +1 -0
  67. package/build/src/cli/render/execution-progress.js +77 -3
  68. package/build/src/cli/render/kanshi.js +5 -19
  69. package/build/src/cli/render/terminal.js +3 -1
  70. package/build/src/cli/runtime.d.ts +6 -0
  71. package/build/src/cli/runtime.js +35 -17
  72. package/build/src/core/facts/offer.d.ts +1 -2
  73. package/build/src/core/verbs/abandon.js +1 -1
  74. package/build/src/core/verbs/amend.js +1 -1
  75. package/build/src/core/verbs/arc.js +1 -1
  76. package/build/src/core/verbs/attestation.js +1 -1
  77. package/build/src/core/verbs/bind.js +1 -1
  78. package/build/src/core/verbs/deliver.js +1 -1
  79. package/build/src/core/verbs/placement.js +1 -1
  80. package/build/src/core/verbs/reintegrate.js +1 -1
  81. package/build/src/git/admission.js +18 -7
  82. package/build/src/identity/coordinates.js +1 -1
  83. package/build/src/identity/normalize.d.ts +3 -0
  84. package/build/src/identity/normalize.js +10 -0
  85. package/build/src/identity/selector.d.ts +1 -0
  86. package/build/src/identity/selector.js +15 -4
  87. package/build/src/library/address.d.ts +33 -3
  88. package/build/src/library/address.js +90 -29
  89. package/build/src/library/akuma-creation.d.ts +1 -1
  90. package/build/src/library/akuma-creation.js +10 -52
  91. package/build/src/library/composition.d.ts +14 -1
  92. package/build/src/library/fleet.d.ts +13 -2
  93. package/build/src/library/fleet.js +142 -48
  94. package/build/src/library/keiyaku.d.ts +14 -1
  95. package/build/src/plugin/public.d.ts +2 -0
  96. package/build/src/protocol/completion.js +9 -2
  97. package/build/src/runtime/proc/windows-launch.exe +0 -0
  98. package/package.json +1 -1
@@ -1,7 +1,75 @@
1
1
  import { parseAkumaStatus } from "../../akuma/akuma.js";
2
- import { DEFAULT_CONTEXT, akumaRawAnswer, 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
- export { akumaRawAnswer } from "./akuma-activity.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
+ }
50
+ export function waitedTellProgress(result, alias, context) {
51
+ const receipt = tellText({
52
+ kind: "akuma",
53
+ action: "tell",
54
+ mode: "ordinary",
55
+ result: { akuma: result.akuma, tell: result.tell },
56
+ body: "",
57
+ ...(alias === undefined ? {} : { alias }),
58
+ }, context);
59
+ const conclusion = tellWaitConclusion(result);
60
+ return `${receipt}\n${conclusion}`;
61
+ }
62
+ function waitedTellRawAnswer(result) {
63
+ if (result.result.observation.reason !== "answered")
64
+ return "";
65
+ const answer = result.result.observation.answer;
66
+ return typeof answer === "string" ? answer : JSON.stringify(answer);
67
+ }
68
+ export function akumaRawAnswer(result) {
69
+ if (result.action === "tell" && result.mode === "wait")
70
+ return waitedTellRawAnswer(result);
71
+ return akumaActivityRawAnswer(result);
72
+ }
5
73
  function dispatchLines(stage) {
6
74
  if (stage.kind === "none")
7
75
  return [];
@@ -83,6 +151,8 @@ export function renderAkumaText(command, result, context = DEFAULT_CONTEXT) {
83
151
  case "wait":
84
152
  return waitText(result, context);
85
153
  case "tell":
154
+ if (result.mode === "wait")
155
+ return "";
86
156
  return result.mode === "ordinary"
87
157
  ? tellText(result, context)
88
158
  : mutationObservationStageText(result.result.id, result.result.observation, context, {
@@ -125,6 +195,8 @@ function killExitCode(result) {
125
195
  function tellExitCode(result) {
126
196
  if (result.mode === "schema")
127
197
  return 0;
198
+ if (result.mode === "wait")
199
+ return result.result.observation.reason === "failed" ? 2 : 0;
128
200
  return result.mode === "ordinary"
129
201
  ? result.result.tell.wake.kind === "failed"
130
202
  ? 2
@@ -1,4 +1,4 @@
1
- import { abbreviateGitIds, afterWording, candidateFact, dependentWording, gateFact, gitIdsInRow, verificationFact, targetFacts, } from "./contract-observation.js";
1
+ import { abbreviateGitIds, afterWording, candidateIntegrationFacts, dependentWording, gateFact, gitIdsInRow, targetFacts, } from "./contract-observation.js";
2
2
  import { safeText } from "./terminal.js";
3
3
  import { dispositionText, taskMark } from "./task.js";
4
4
  function akumaMark(life) {
@@ -87,10 +87,9 @@ function renderContractCatalog(catalog) {
87
87
  const blocks = rows.map((row) => {
88
88
  const lines = [
89
89
  `${catalogMark(row)} ${safeText(row.id)} · ${row.phase} · ${formatAge(row.phaseAt, catalog.observedAt)} · ${safeText(row.title ?? "title unavailable")}`,
90
- ` ${candidateFact(row.delivery)}`,
90
+ ...candidateIntegrationFacts(row.delivery, row.verification, abbreviations).map((fact) => ` ${safeText(fact)}`),
91
91
  ...targetFacts(row, abbreviations).map((fact) => ` ${safeText(fact)}`),
92
92
  ...[],
93
- ...(verificationFact(row.verification) === undefined ? [] : [` ${verificationFact(row.verification)}`]),
94
93
  ...row.after.map((edge) => ` ${afterWording(edge)}`),
95
94
  ...(row.dependents.length === 0 ? [] : [` dependents ${row.dependents.map(dependentWording).join(" · ")}`]),
96
95
  ...(row.gates.reports.length === 0 ? [] : [` ${row.gates.reports.map(gateFact).join(" ")}`]),
@@ -3,6 +3,7 @@ export declare function gateGlyph(report: ContractGateReport): string;
3
3
  export declare function gateFact(report: ContractGateReport): string;
4
4
  export declare function candidateFact(delivery: ContractRow["delivery"]): string;
5
5
  export declare function verificationFact(status: ContractRow["verification"]): string | undefined;
6
+ export declare function candidateIntegrationFacts(delivery: ContractRow["delivery"], verification: ContractRow["verification"], abbreviations: ReadonlyMap<string, string>): readonly string[];
6
7
  export declare function afterWording(edge: ContractAfterEdge): string;
7
8
  export declare function dependentWording(dependent: ContractDependent): string;
8
9
  export declare function mergeSummary(observation: ContractWorkspaceObservation): string | undefined;
@@ -19,9 +19,28 @@ export function verificationFact(status) {
19
19
  if (status === undefined)
20
20
  return undefined;
21
21
  if (status.kind === "recorded")
22
- return `verification ${status.verdict}${status.snapshot === undefined ? "" : ` · on ${shortGitId(status.snapshot)}`}`;
22
+ return `verification ${status.verdict}${status.snapshot === undefined ? "" : ` · snapshot ${shortGitId(status.snapshot)}`}`;
23
23
  return `verification ${status.kind}`;
24
24
  }
25
+ export function candidateIntegrationFacts(delivery, verification, abbreviations) {
26
+ const verificationLine = verificationFact(verification);
27
+ if (delivery === null) {
28
+ return [candidateFact(delivery), ...(verificationLine === undefined ? [] : [verificationLine])];
29
+ }
30
+ const integration = displayGitId(delivery.integration.snapshot, abbreviations);
31
+ const foldedVerification = verification?.kind === "recorded" && verification.snapshot === delivery.integration.snapshot
32
+ ? ` · verification ${verification.verdict}`
33
+ : "";
34
+ return [
35
+ candidateFact(delivery),
36
+ `tender commit ${displayGitId(delivery.tenderSnapshot, abbreviations)}`,
37
+ `integration result ${integration}${foldedVerification}`,
38
+ `predecessor ${displayGitId(delivery.integration.predecessor, abbreviations)}`,
39
+ `method ${delivery.method}`,
40
+ `content identity (not commit) ${delivery.integration.changeId}`,
41
+ ...(foldedVerification.length === 0 && verificationLine !== undefined ? [verificationLine] : []),
42
+ ];
43
+ }
25
44
  export function afterWording(edge) {
26
45
  if (edge.endpoint.kind === "claimed")
27
46
  return `after ${edge.contractId} · claimed`;
@@ -152,9 +152,9 @@ function shortGitId(value) {
152
152
  /**
153
153
  * Shared presentation of a completed placement, so a deliver and a review that placed the same candidate read
154
154
  * identically: one git-shaped target movement row, naming the reference it advanced, then the final lifecycle
155
- * state. Satisfied Verification adds one fact row naming the commit the verdict ran against; placement made that
156
- * commit the reference's new head, so the two rows share one sha. Verification mode words stay in JSON and
157
- * `history`, never in ordinary receipt text. A completion without an advanced reference states no movement.
155
+ * state. Satisfied Verification adds one fact row naming the integrated result; placement made that commit the
156
+ * reference's new head, so the movement and integrated-result rows share one sha. Verification mode words stay in
157
+ * JSON and `history`, never in ordinary receipt text. A completion without an advanced reference states no movement.
158
158
  * Journal ULIDs stay in JSON and `history`, never in ordinary receipt text.
159
159
  */
160
160
  function completedPlacementLines(result, columns) {
@@ -169,7 +169,7 @@ function completedPlacementLines(result, columns) {
169
169
  ], columns);
170
170
  }
171
171
  if (completion.verification?.verdict === "satisfied") {
172
- receiptRow(lines, " ", "verification", [{ text: "satisfied" }, { text: `· on ${shortGitId(completion.integration)}` }], columns);
172
+ receiptRow(lines, " ", "integration result", [{ text: `${shortGitId(completion.integration)} · verification satisfied` }], columns);
173
173
  }
174
174
  lines.push(...nonGatingVerificationLines(result, columns));
175
175
  // Placement admission always admits the claim entry beside the movement, so a completed placement is claimed.
@@ -16,6 +16,7 @@ export declare class ExecutionProgressRenderer {
16
16
  private declarationFailed;
17
17
  private phaseFailed;
18
18
  private phaseUnknown;
19
+ private readonly liveOutput;
19
20
  constructor(input: ExecutionProgressOptions);
20
21
  consume(event: ExecutionEvent): Promise<void>;
21
22
  finish(): void;
@@ -1,12 +1,12 @@
1
1
  import { renderOpaqueBlock, safeText } from "./terminal.js";
2
2
  import { StatusLine } from "./status-line.js";
3
3
  const LIVE_OUTPUT_BYTES = 4 * 1024;
4
- function boundedUtf8Prefix(value) {
4
+ function boundedUtf8Prefix(value, maximumBytes = LIVE_OUTPUT_BYTES) {
5
5
  let bytes = 0;
6
6
  let text = "";
7
7
  for (const character of value) {
8
8
  const size = Buffer.byteLength(character);
9
- if (bytes + size > LIVE_OUTPUT_BYTES)
9
+ if (bytes + size > maximumBytes)
10
10
  return { text, truncated: true };
11
11
  bytes += size;
12
12
  text += character;
@@ -47,6 +47,74 @@ function outputLines(observation, context) {
47
47
  ...(output.truncated ? [" [live output truncated]"] : []),
48
48
  ];
49
49
  }
50
+ class VerificationLiveOutput {
51
+ streams = new Map();
52
+ activeStream;
53
+ reset() {
54
+ this.streams.clear();
55
+ this.activeStream = undefined;
56
+ }
57
+ lines(observation, context) {
58
+ const stream = observation.stream;
59
+ const lines = this.flushOtherStreams(stream, context);
60
+ const previous = this.streams.get(stream);
61
+ const state = previous ?? { bytes: 0, truncated: false, pending: "", prefix: " " };
62
+ if (this.activeStream !== stream) {
63
+ state.prefix = " ";
64
+ lines.push(stream);
65
+ this.activeStream = stream;
66
+ }
67
+ this.streams.set(stream, state);
68
+ if (state.truncated)
69
+ return lines;
70
+ const output = boundedUtf8Prefix(observation.text, LIVE_OUTPUT_BYTES - state.bytes);
71
+ state.bytes += Buffer.byteLength(output.text);
72
+ state.pending += output.text;
73
+ lines.push(...this.drainCompleteLines(state, context));
74
+ this.streams.set(stream, state);
75
+ if (output.truncated) {
76
+ lines.push(...this.drainPartialLine(state, context));
77
+ state.truncated = true;
78
+ lines.push(" [live output truncated]");
79
+ }
80
+ return lines;
81
+ }
82
+ finish(context) {
83
+ const lines = [];
84
+ for (const state of this.streams.values())
85
+ lines.push(...this.drainPartialLine(state, context));
86
+ this.streams.clear();
87
+ this.activeStream = undefined;
88
+ return lines;
89
+ }
90
+ flushOtherStreams(stream, context) {
91
+ const lines = [];
92
+ for (const [otherStream, state] of this.streams)
93
+ if (otherStream !== stream)
94
+ lines.push(...this.drainPartialLine(state, context));
95
+ return lines;
96
+ }
97
+ drainCompleteLines(state, context) {
98
+ const lines = [];
99
+ for (;;) {
100
+ const newline = state.pending.indexOf("\n");
101
+ if (newline < 0)
102
+ break;
103
+ const line = state.pending.slice(0, newline);
104
+ state.pending = state.pending.slice(newline + 1);
105
+ lines.push(...renderOpaqueBlock(line, state.prefix, context.columns));
106
+ state.prefix = " ";
107
+ }
108
+ return lines;
109
+ }
110
+ drainPartialLine(state, context) {
111
+ if (state.pending.length === 0)
112
+ return [];
113
+ const lines = renderOpaqueBlock(state.pending, state.prefix, context.columns);
114
+ state.pending = "";
115
+ return lines;
116
+ }
117
+ }
50
118
  /** Sparse rendering for consumers that do not own a live stream. */
51
119
  export function executionProgressLines(event, context) {
52
120
  switch (event.kind) {
@@ -75,6 +143,7 @@ export class ExecutionProgressRenderer {
75
143
  declarationFailed = false;
76
144
  phaseFailed = false;
77
145
  phaseUnknown = false;
146
+ liveOutput = new VerificationLiveOutput();
78
147
  constructor(input) {
79
148
  this.input = input;
80
149
  this.status = new StatusLine(input.stream, input);
@@ -101,10 +170,12 @@ export class ExecutionProgressRenderer {
101
170
  async consumeVerification(event) {
102
171
  const observation = event.observation;
103
172
  if (observation.kind === "output") {
104
- await this.write(outputLines(observation, this.input.context));
173
+ await this.write(this.liveOutput.lines(observation, this.input.context));
105
174
  return;
106
175
  }
107
176
  if (observation.state === "started") {
177
+ await this.write(this.liveOutput.finish(this.input.context));
178
+ this.liveOutput.reset();
108
179
  this.verificationStartedAt ??= (this.input.now ?? (() => performance.now()))();
109
180
  if (this.status.isTTY)
110
181
  this.status.show((duration) => `verify ● ${phaseDetail(observation)} · ${elapsed(duration)}`);
@@ -112,6 +183,7 @@ export class ExecutionProgressRenderer {
112
183
  await this.write([phaseStartLine(observation)]);
113
184
  return;
114
185
  }
186
+ await this.write(this.liveOutput.finish(this.input.context));
115
187
  const mark = phaseMark(observation.outcome);
116
188
  this.phaseFailed ||= mark === "×";
117
189
  this.phaseUnknown ||= mark === "?";
@@ -127,6 +199,8 @@ export class ExecutionProgressRenderer {
127
199
  await this.write([phaseFinishLine(observation)]);
128
200
  }
129
201
  write(lines) {
202
+ if (lines.length === 0)
203
+ return Promise.resolve();
130
204
  if (this.status.isTTY) {
131
205
  this.status.writeBlock(lines);
132
206
  return Promise.resolve();
@@ -1,4 +1,4 @@
1
- import { abbreviateGitIds, afterWording, candidateFact, dependentWording, displayGitId, gateFact, gitIdsInRow, mergeSummary, verificationFact, targetFacts, } from "./contract-observation.js";
1
+ import { abbreviateGitIds, afterWording, candidateIntegrationFacts, dependentWording, displayGitId, gateFact, gitIdsInRow, mergeSummary, targetFacts, } from "./contract-observation.js";
2
2
  import { entityLines, elapsedMilliseconds, identityLine, plumbFacts, RECENT_TONE_MS, renderSectionBlock, linkedEntityLines, safeText, tone, } from "./terminal.js";
3
3
  import { akumaMark, endpointFact, formatAge, NARROW_COLUMNS, renderAkuma } from "./kanshi-akuma.js";
4
4
  import { dispositionText } from "./task.js";
@@ -179,7 +179,7 @@ function renderSelectedContractRow(row, report, context, abbreviations) {
179
179
  : []),
180
180
  ]);
181
181
  lines.push(...semanticBlock("gates", gateFacts, context));
182
- lines.push(...semanticBlock("candidate/integration", candidateFacts(row, abbreviations), context));
182
+ lines.push(...semanticBlock("candidate/integration", candidateIntegrationFacts(row.delivery, row.verification, abbreviations), context));
183
183
  lines.push(...semanticBlock("target", targetFacts(row, abbreviations), context));
184
184
  const workspaceFacts = [workspaceState(row)];
185
185
  if (row.workspaceObservation.kind !== "unappointed" &&
@@ -199,31 +199,17 @@ function renderSelectedContractRow(row, report, context, abbreviations) {
199
199
  }
200
200
  return lines;
201
201
  }
202
- function candidateFacts(row, abbreviations) {
203
- const verification = verificationFact(row.verification);
204
- if (row.delivery === null)
205
- return [candidateFact(row.delivery), ...(verification === undefined ? [] : [verification])];
206
- const delivery = row.delivery;
207
- return [
208
- candidateFact(delivery),
209
- `tender commit ${displayGitId(delivery.tenderSnapshot, abbreviations)}`,
210
- `integration commit ${displayGitId(delivery.integration.snapshot, abbreviations)} · predecessor ${displayGitId(delivery.integration.predecessor, abbreviations)}`,
211
- `method ${delivery.method}`,
212
- `content identity (not commit) ${delivery.integration.changeId}`,
213
- ...(verification === undefined ? [] : [verification]),
214
- ];
215
- }
216
202
  function renderWorldContractRow(row, report, context) {
217
203
  const title = row.title ?? "title unavailable";
204
+ const abbreviations = gitAbbreviations(report);
218
205
  const contractFacts = [
219
- candidateFact(row.delivery),
220
- ...targetFacts(row, gitAbbreviations(report)),
206
+ ...candidateIntegrationFacts(row.delivery, row.verification, abbreviations),
207
+ ...targetFacts(row, abbreviations),
221
208
  ...[],
222
209
  ...(linkedAkumaSummary(row, report) === undefined ? [] : [linkedAkumaSummary(row, report)]),
223
210
  ...row.after.map(afterWording),
224
211
  ...(row.dependents.length === 0 ? [] : [`dependents ${row.dependents.map(dependentWording).join(" · ")}`]),
225
212
  ...row.gates.reports.map(gateFact),
226
- ...(verificationFact(row.verification) === undefined ? [] : [verificationFact(row.verification)]),
227
213
  ];
228
214
  const linkedFacts = [
229
215
  ...(row.holder.kind === "held" ? [linkedTask(report, row.holder.taskId)] : []),
@@ -158,7 +158,9 @@ export function quotedText(value) {
158
158
  .join(""));
159
159
  }
160
160
  export function safeText(value) {
161
- return value.replaceAll(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, (character) => (/\s/u.test(character) ? " " : "�"));
161
+ return value.replaceAll(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, (character) =>
162
+ // ZWJ and ZWNJ are legitimate joiners that carry a grapheme cluster's shape; they move no cursor.
163
+ character === "\u200c" || character === "\u200d" ? character : /\s/u.test(character) ? " " : "�");
162
164
  }
163
165
  export function checkoutNotFollowableLines(refusal) {
164
166
  const lines = [
@@ -5,4 +5,10 @@ export declare function writeExecutionProgress(events: AsyncIterable<ExecutionEv
5
5
  export declare function writeTask(command: Extract<ParsedCommand, {
6
6
  command: "task";
7
7
  }>, result: TaskInvocationResult): Promise<number>;
8
+ type AkumaFailureProjection = Readonly<{
9
+ body: string;
10
+ exitCode: 1 | 3;
11
+ }>;
12
+ export declare function akumaFailureProjection(error: unknown, command: ParsedCommand): Promise<AkumaFailureProjection | undefined>;
8
13
  export declare function runCliCommand(invocation: ParsedExecution): Promise<number>;
14
+ export {};
@@ -133,19 +133,37 @@ async function writeResult(command, result) {
133
133
  writeCliStream(process.stdout, body);
134
134
  return invocationExitCode(contractResult);
135
135
  }
136
- function writeWorldScopeRefusal(error, command) {
137
- const guide = usageGuideForCommand(command);
138
- const body = command.output === "json"
139
- ? JSON.stringify(error.refusal)
140
- : [
141
- `× selector ${error.refusal.kind}`,
142
- ` world ${safeText(error.refusal.world)}`,
143
- ...error.refusal.ids.map((id) => ` given ${safeText(id)}`),
144
- ` accepts ${guide.accepts}`,
145
- ` help ${guide.help}`,
146
- ].join("\n");
147
- writeCliStream(process.stderr, body);
148
- return 1;
136
+ export async function akumaFailureProjection(error, command) {
137
+ const [{ AkumaNotBornError, AkumaObservationError }, { AkumaAddressError, AkumaWorldScopeError }] = await Promise.all([import("../akuma/akuma-errors.js"), import("../library/address.js")]);
138
+ if (error instanceof AkumaNotBornError) {
139
+ return {
140
+ body: command.output === "json" ? error.message : `× Akuma not found ${safeText(error.id)}`,
141
+ exitCode: 1,
142
+ };
143
+ }
144
+ if (error instanceof AkumaAddressError) {
145
+ const body = error.refusal.kind === "akuma-alias-not-found"
146
+ ? `× Akuma alias not found ${safeText(error.refusal.alias)}`
147
+ : `× invalid Akuma ${safeText(error.refusal.selector)}`;
148
+ return { body: command.output === "json" ? error.message : body, exitCode: 1 };
149
+ }
150
+ if (error instanceof AkumaWorldScopeError) {
151
+ return {
152
+ body: command.output === "json"
153
+ ? JSON.stringify(error.refusal)
154
+ : error.refusal.ids.map((id) => `× Akuma not in this World ${safeText(id)}`).join("\n"),
155
+ exitCode: 1,
156
+ };
157
+ }
158
+ if (error instanceof AkumaObservationError) {
159
+ return {
160
+ body: command.output === "json"
161
+ ? error.message
162
+ : `× Akuma observation failed ${safeText(error.id)} — ${safeText(error.diagnostic)}`,
163
+ exitCode: 3,
164
+ };
165
+ }
166
+ return undefined;
149
167
  }
150
168
  async function commandFailureText(error, command) {
151
169
  const diagnostic = error instanceof Error ? error.message : String(error);
@@ -189,10 +207,10 @@ export async function runCliCommand(invocation) {
189
207
  : executionFailureLines(receipt, category, diagnostic, displayContext().columns).join("\n"));
190
208
  return 3;
191
209
  }
192
- if (command.command !== "install") {
193
- const { AkumaWorldScopeError } = await import("../library/address.js");
194
- if (error instanceof AkumaWorldScopeError)
195
- return writeWorldScopeRefusal(error, command);
210
+ const akumaFailure = await akumaFailureProjection(error, command);
211
+ if (akumaFailure !== undefined) {
212
+ writeCliStream(process.stderr, akumaFailure.body);
213
+ return akumaFailure.exitCode;
196
214
  }
197
215
  if (command.command === "bind") {
198
216
  const { BindDraftError } = await import("./draft.js");
@@ -1,7 +1,6 @@
1
- import type { ContractHead, ContractId, JournalEntry, SnapshotId } from "./types.js";
1
+ import type { ContractId, JournalEntry, SnapshotId } from "./types.js";
2
2
  export type ContractJournalAppend = Readonly<{
3
3
  readonly contractId: ContractId;
4
- readonly expectedHead: ContractHead | null;
5
4
  readonly entries: readonly JournalEntry[];
6
5
  }>;
7
6
  /** A target operation is optional because claim can be journal-only. */
@@ -29,5 +29,5 @@ export function decideAbandon({ input, attempt, observation, }) {
29
29
  ...(input.actor === undefined ? {} : { actor: input.actor }),
30
30
  data: input.note === undefined ? {} : { note: input.note },
31
31
  };
32
- return { kind: "offer", offer: { facts: [{ contractId: id, expectedHead: current.head, entries: [abandoned] }] } };
32
+ return { kind: "offer", offer: { facts: [{ contractId: id, entries: [abandoned] }] } };
33
33
  }
@@ -57,5 +57,5 @@ export function decideAmend({ input, attempt, observation, }) {
57
57
  ...(input.actor === undefined ? {} : { actor: input.actor }),
58
58
  data,
59
59
  };
60
- return { kind: "offer", offer: { facts: [{ contractId: id, expectedHead: current.head, entries: [entry] }] } };
60
+ return { kind: "offer", offer: { facts: [{ contractId: id, entries: [entry] }] } };
61
61
  }
@@ -36,6 +36,6 @@ export function decideArc({ input, attempt, observation }) {
36
36
  };
37
37
  return {
38
38
  kind: "offer",
39
- offer: { facts: [{ contractId: id, expectedHead: current.head, entries: [arc] }] },
39
+ offer: { facts: [{ contractId: id, entries: [arc] }] },
40
40
  };
41
41
  }
@@ -35,6 +35,6 @@ export function decideAttestation({ input, attempt, observation, }) {
35
35
  };
36
36
  return {
37
37
  kind: "offer",
38
- offer: { facts: [{ contractId: input.contractId, expectedHead: state.head, entries: [attestation] }] },
38
+ offer: { facts: [{ contractId: input.contractId, entries: [attestation] }] },
39
39
  };
40
40
  }
@@ -38,5 +38,5 @@ export function decideBind({ input, attempt, observation, }) {
38
38
  ...(input.actor === undefined ? {} : { actor: input.actor }),
39
39
  data,
40
40
  };
41
- return { kind: "offer", offer: { facts: [{ contractId: id, expectedHead: null, entries: [bind] }] } };
41
+ return { kind: "offer", offer: { facts: [{ contractId: id, entries: [bind] }] } };
42
42
  }
@@ -48,5 +48,5 @@ export function decideDeliver({ input, attempt, observation, }) {
48
48
  data: input.preparation.data,
49
49
  };
50
50
  entries.push(deliver);
51
- return { kind: "offer", offer: { facts: [{ contractId: input.contractId, expectedHead: state.head, entries }] } };
51
+ return { kind: "offer", offer: { facts: [{ contractId: input.contractId, entries }] } };
52
52
  }
@@ -104,7 +104,7 @@ export function decidePlacement({ input, attempt, observation, }) {
104
104
  return {
105
105
  kind: "offer",
106
106
  offer: {
107
- facts: [{ contractId: id, expectedHead: current.head, entries: [claimed] }],
107
+ facts: [{ contractId: id, entries: [claimed] }],
108
108
  ...(current.coordinates.target === undefined
109
109
  ? {}
110
110
  : {
@@ -19,6 +19,6 @@ export function decideReintegrate({ input, attempt, observation, }) {
19
19
  };
20
20
  return {
21
21
  kind: "offer",
22
- offer: { facts: [{ contractId: input.contractId, expectedHead: state.head, entries: [reintegrated] }] },
22
+ offer: { facts: [{ contractId: input.contractId, entries: [reintegrated] }] },
23
23
  };
24
24
  }
@@ -1,4 +1,6 @@
1
1
  import { decodeJournal, encodeEntry } from "../core/facts/codec.js";
2
+ import { AuthorityCorruptionError } from "../core/facts/errors.js";
3
+ import { foldJournal } from "../core/facts/fold.js";
2
4
  import { GIT_FORMAT_BYTES, GIT_FORMAT_PATH, GIT_REF, updateGitTreeFromFrozenDirectories, updateRefsAtomically, writeBlob, writeStateCommit, } from "./repository.js";
3
5
  import { contractJournalPath, contractJournalPaths, gitObjectIdForSnapshot, mintContractHead, mintSnapshotId, } from "./identity.js";
4
6
  import { validPath } from "./tree.js";
@@ -57,6 +59,19 @@ async function buildOffer(repository, admission, appends, companions) {
57
59
  const snapshot = admission.snapshot;
58
60
  const changes = new Map();
59
61
  const heads = {};
62
+ const candidates = appends.map((append) => {
63
+ const current = readCanonicalJournal(admission, append.contractId);
64
+ let journal = current.bytes;
65
+ for (const entry of append.entries)
66
+ journal = Buffer.concat([journal, Buffer.from(encodeEntry(entry))]);
67
+ const entries = decodeJournal(journal.toString("utf8"));
68
+ const foreign = entries.find((entry) => entry.contract !== append.contractId);
69
+ if (foreign !== undefined) {
70
+ throw new AuthorityCorruptionError(`candidate journal entry belongs to ${foreign.contract}, not ${append.contractId}`);
71
+ }
72
+ const state = foldJournal(append.contractId, entries);
73
+ return { append, current, journal, state };
74
+ });
60
75
  if (snapshot.commit === null) {
61
76
  changes.set(GIT_FORMAT_PATH, { oid: await writeBlob(repository, GIT_FORMAT_BYTES), mode: "100644", type: "blob" });
62
77
  }
@@ -67,14 +82,10 @@ async function buildOffer(repository, admission, appends, companions) {
67
82
  type: "blob",
68
83
  });
69
84
  }
70
- for (const append of appends) {
71
- const current = readCanonicalJournal(admission, append.contractId);
72
- let journal = current.bytes;
73
- for (const entry of append.entries)
74
- journal = Buffer.concat([journal, Buffer.from(encodeEntry(entry))]);
85
+ for (const candidate of candidates) {
86
+ const { append, current, journal, state } = candidate;
75
87
  const blob = await writeBlob(repository, journal);
76
- const entries = decodeJournal(journal.toString("utf8"));
77
- const terminal = entries.some((entry) => entry.kind === "claimed" || entry.kind === "abandoned");
88
+ const terminal = state.terminal !== null;
78
89
  const destination = contractJournalPath(append.contractId, terminal ? "terminal" : "active");
79
90
  if (current.path !== null && current.path !== destination)
80
91
  changes.set(current.path, null);
@@ -2,7 +2,7 @@ function prefix(family) {
2
2
  return `${family}/`;
3
3
  }
4
4
  function validSegment(segment) {
5
- return segment.length > 0 && !/[\/\s\p{Cc}]/u.test(segment);
5
+ return segment.length > 0 && segment !== "." && segment !== ".." && !/[\/\s\p{Cc}]/u.test(segment);
6
6
  }
7
7
  export function identityCoordinate(input) {
8
8
  if (input.segments.length === 0 || !input.segments.every(validSegment)) {
@@ -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;