@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
@@ -1,26 +1,37 @@
1
1
  import { parseAkumaStatus } from "../../akuma/akuma.js";
2
2
  import { toolRepr } from "./akuma-tool.js";
3
- import { displayColumns, renderBoundedTextBlock, safeText, truncateMiddleDisplayText, } from "./terminal.js";
3
+ import { displayColumns, renderBoundedTextBlock, safeText, takeDisplayColumns, truncateMiddleDisplayText, } from "./terminal.js";
4
4
  export const DEFAULT_CONTEXT = { columns: 80, color: false };
5
- const SNAPSHOT_RULER = "────────────────";
6
5
  const TIME_WIDTH = 5;
7
6
  const VERB_WIDTH = 6;
7
+ /**
8
+ * The one blessed ruler: a run of U+2500 exactly as wide as the frame head's
9
+ * widest line, marking the boundary between an observation frame and its content.
10
+ */
11
+ export function frameRule(headLines) {
12
+ const width = headLines.reduce((widest, line) => Math.max(widest, displayColumns(line)), 0);
13
+ return "─".repeat(width);
14
+ }
15
+ /** Tool rows one focused activity view keeps at its opening and final end; the surplus folds in place. */
16
+ const OPENING_TOOL_BUDGET = 3;
17
+ const RECENT_TOOL_BUDGET = 2;
8
18
  function identity(id, alias) {
9
19
  return `${id}${alias === undefined ? "" : ` (${alias})`}`;
10
20
  }
11
21
  function associatedContractId(contract) {
12
22
  return contract.kind === "associated" ? contract.contractId : undefined;
13
23
  }
14
- export function associatedIdentity(id, alias, contract) {
15
- const contractId = contract === undefined ? undefined : associatedContractId(contract);
16
- return `${identity(id, alias)}${contractId === undefined ? "" : ` [${contractId}]`}`;
24
+ export function associatedIdentity(id, alias, _contract) {
25
+ return identity(id, alias);
17
26
  }
18
27
  export function snapshotHeading(id, alias, contract) {
19
28
  const contractId = contract === undefined ? undefined : associatedContractId(contract);
20
- return [identity(id, alias), contractId === undefined ? SNAPSHOT_RULER : `└─ ${contractId}`];
29
+ const head = [identity(id, alias), ...(contractId === undefined ? [] : [`└─ ${contractId}`])];
30
+ return [...head, frameRule(head)];
21
31
  }
22
32
  function answeredHeading(id, alias) {
23
- return [`✓ came back ${identity(id, alias)}`, SNAPSHOT_RULER].join("\n");
33
+ const heading = `✓ came back ${identity(id, alias)}`;
34
+ return [heading, frameRule([heading])];
24
35
  }
25
36
  function contractFacts(contract) {
26
37
  return contract.kind === "failed" ? [`! contract failed ${safeText(contract.diagnostic)}`] : [];
@@ -30,12 +41,14 @@ function unobservedText(id, diagnostic) {
30
41
  }
31
42
  function lifeLabel(life) {
32
43
  if (life === "running")
33
- return "● STILL RUNNING";
44
+ return "● running";
34
45
  if (life === "asleep")
35
46
  return "✓ came back";
36
47
  if (life === "killed")
37
- return " killed";
38
- return `? ${life}`;
48
+ return "× killed";
49
+ if (life === "hung")
50
+ return "? hung";
51
+ return "! stranded";
39
52
  }
40
53
  function clock(at) {
41
54
  const date = new Date(at);
@@ -78,6 +91,18 @@ function mark(row) {
78
91
  }
79
92
  return "│";
80
93
  }
94
+ /** Strip Markdown decoration from preview prose without rewriting the words. */
95
+ function undecorated(text) {
96
+ return (text
97
+ .replace(/^ {0,3}#{1,6}[ \t]+/gmu, "")
98
+ .replace(/^ {0,3}(?:[-*+]|\d+\.)[ \t]+/gmu, "")
99
+ .replace(/\*\*([^*\n]+?)\*\*/gu, "$1")
100
+ // A single pair of asterisks is emphasis only when it does not sit inside a word, path, or glob.
101
+ .replace(/(?<![\w*/\\])\*([^\s*/\\](?:[^*/\\\n]*[^\s*/\\])?)\*(?![\w*/\\])/gu, "$1")
102
+ .replace(/`([^`\n]+?)`/gu, "$1")
103
+ .replace(/\s+/gu, " ")
104
+ .trim());
105
+ }
81
106
  function rowText(row) {
82
107
  if (row.kind === "said" ||
83
108
  row.kind === "thought" ||
@@ -85,13 +110,13 @@ function rowText(row) {
85
110
  row.kind === "call" ||
86
111
  row.kind === "tell") {
87
112
  return {
88
- text: row.text,
113
+ text: undecorated(row.text),
89
114
  lines: row.kind === "said" || row.kind === "thought" ? 2 : row.kind === "tell" || row.kind === "call" ? 1 : 2,
90
115
  };
91
116
  }
92
117
  if (row.kind === "outcome")
93
118
  return row.outcome.kind === "answered"
94
- ? { text: row.outcome.answer, lines: 3 }
119
+ ? { text: undecorated(row.outcome.answer), lines: 3 }
95
120
  : { text: row.outcome.diagnostic, lines: 2 };
96
121
  if (row.kind === "turn")
97
122
  return { text: "", lines: 1 };
@@ -112,19 +137,41 @@ function eventPrefix(glyph, verb, time) {
112
137
  function continuationPrefix() {
113
138
  return eventPrefix("│", "");
114
139
  }
140
+ /** Pad to a terminal-column width; raw string length is never the measuring stick. */
141
+ function padToDisplay(text, width) {
142
+ const remaining = width - displayColumns(text);
143
+ return remaining > 0 ? `${text}${" ".repeat(remaining)}` : text;
144
+ }
145
+ function plainLayout() {
146
+ return {
147
+ head: (time, glyph, verb) => eventPrefix(glyph, verb, time),
148
+ continuation: continuationPrefix,
149
+ marker: (count) => `${" ".repeat(TIME_WIDTH)} ⋮ ${count} omitted`,
150
+ };
151
+ }
152
+ function sourceLayout(source, width) {
153
+ const gutter = () => `${" ".repeat(TIME_WIDTH)} ${padToDisplay(source, width())} `;
154
+ return {
155
+ head: (time, glyph, verb) => `${time === undefined ? " ".repeat(TIME_WIDTH) : time.padEnd(TIME_WIDTH)} ${padToDisplay(source, width())} ${glyph} ${verb.padEnd(VERB_WIDTH)} `,
156
+ // Continuations blank the time and source columns and align under the mark.
157
+ continuation: () => `${" ".repeat(TIME_WIDTH)} ${" ".repeat(width())} │ ${" ".repeat(VERB_WIDTH)} `,
158
+ marker: (count) => `${gutter()}⋮ ${count} omitted`,
159
+ };
160
+ }
115
161
  function quotedBody(row) {
116
162
  return (row.kind === "said" ||
117
163
  row.kind === "thought" ||
118
164
  row.kind === "tell" ||
119
165
  (row.kind === "outcome" && row.outcome.kind === "answered"));
120
166
  }
167
+ /** Quote every rendered body line, slicing each line's prefix by its display width. */
121
168
  function quoteLines(lines, prefix) {
122
- const prefixWidth = prefix.length;
169
+ const prefixWidth = displayColumns(prefix);
123
170
  return lines.map((line) => {
124
- const body = line.slice(prefixWidth);
171
+ const { text: head, rest: body } = takeDisplayColumns(line, prefixWidth);
125
172
  if (body.length === 0)
126
173
  return line;
127
- return `${line.slice(0, prefixWidth)}“${body}”`;
174
+ return `${head}“${body}”`;
128
175
  });
129
176
  }
130
177
  function renderMiddleEllipsis(first, text, suffix, columns) {
@@ -136,7 +183,7 @@ function renderMiddleEllipsis(first, text, suffix, columns) {
136
183
  const showSuffix = suffix.length > 0 && withSuffix >= 6;
137
184
  return `${first}${truncateMiddleDisplayText(text, Math.max(0, showSuffix ? withSuffix : remaining))}${showSuffix ? suffix : ""}`;
138
185
  }
139
- function renderRow(row, context, history, first) {
186
+ function renderRow(row, context, history, first, continuation) {
140
187
  const value = rowText(row);
141
188
  const quoted = quotedBody(row);
142
189
  const quoteWidth = quoted ? 2 : 0;
@@ -145,37 +192,419 @@ function renderRow(row, context, history, first) {
145
192
  }
146
193
  const lines = renderBoundedTextBlock(value.text, {
147
194
  first,
148
- continuation: continuationPrefix(),
195
+ continuation,
149
196
  columns: context.columns - quoteWidth,
150
197
  lines: history ? Number.MAX_SAFE_INTEGER : value.lines,
151
198
  ...("truncated" in row && row.truncated === true ? { truncated: true } : {}),
152
199
  });
153
200
  return quoted ? quoteLines(lines, first) : lines;
154
201
  }
155
- function groupedEntries(entries, context, history = false) {
202
+ function groupedEntries(entries, context, history = false, layout = plainLayout()) {
156
203
  const lines = [];
157
204
  let previousClock;
158
205
  for (const entry of entries) {
159
206
  if (entry.kind === "gap") {
160
- lines.push(`${" ".repeat(TIME_WIDTH)} ⋮ ${entry.count} omitted`);
207
+ lines.push(layout.marker(entry.count));
161
208
  continue;
162
209
  }
163
210
  const row = entry.row;
164
211
  const at = clock(row.at);
165
212
  const changed = previousClock === undefined || at !== previousClock;
166
- lines.push(...renderRow(row, context, history, eventPrefix(mark(row), label(row), changed ? at : undefined)));
213
+ lines.push(...renderRow(row, context, history, layout.head(changed ? at : undefined, mark(row), label(row)), layout.continuation()));
167
214
  previousClock = at;
168
215
  }
169
216
  return lines;
170
217
  }
171
- function groupedRows(rows, context, history = false) {
172
- return groupedEntries(rows.filter((row) => row.kind !== "turn").map((row) => ({ kind: "row", row })), context, history);
218
+ function groupedRows(rows, context, history = false, layout = plainLayout()) {
219
+ return groupedEntries(rows.filter((row) => row.kind !== "turn").map((row) => ({ kind: "row", row })), context, history, layout);
220
+ }
221
+ /** Ordered entry stream of one retained snapshot; an idle outcome keeps its sequence position. */
222
+ function orderedSnapshotEntries(snapshot) {
223
+ if (snapshot.kind !== "idle" || snapshot.outcome === undefined)
224
+ return snapshot.entries;
225
+ const entries = [];
226
+ let outcomeInserted = false;
227
+ for (const entry of snapshot.entries) {
228
+ if (!outcomeInserted && entry.kind === "row" && entry.row.sequence > snapshot.outcome.sequence) {
229
+ entries.push({ kind: "row", row: snapshot.outcome });
230
+ outcomeInserted = true;
231
+ }
232
+ entries.push(entry);
233
+ }
234
+ if (!outcomeInserted)
235
+ entries.push({ kind: "row", row: snapshot.outcome });
236
+ return entries;
237
+ }
238
+ /**
239
+ * Focus one complete snapshot without making its provider-side gaps mean more
240
+ * than they do. Thought rows disappear; known tool rows use the same opening
241
+ * and recent budgets as live activity, while every other row keeps its place.
242
+ */
243
+ function focusedSnapshotEntries(entries) {
244
+ const tools = entries.filter((entry) => entry.kind === "row" && entry.row.kind === "tool");
245
+ const focused = [];
246
+ let toolIndex = 0;
247
+ let omittedTools = 0;
248
+ const flushOmittedTools = () => {
249
+ if (omittedTools > 0)
250
+ focused.push({ kind: "gap", count: omittedTools });
251
+ omittedTools = 0;
252
+ };
253
+ for (const entry of entries) {
254
+ if (entry.kind === "row" && entry.row.kind === "thought")
255
+ continue;
256
+ if (entry.kind !== "row" || entry.row.kind !== "tool") {
257
+ flushOmittedTools();
258
+ focused.push(entry);
259
+ continue;
260
+ }
261
+ const keep = toolIndex < OPENING_TOOL_BUDGET || toolIndex >= tools.length - RECENT_TOOL_BUDGET;
262
+ toolIndex += 1;
263
+ if (keep)
264
+ flushOmittedTools();
265
+ else
266
+ omittedTools += 1;
267
+ if (keep)
268
+ focused.push(entry);
269
+ }
270
+ flushOmittedTools();
271
+ return focused;
272
+ }
273
+ /**
274
+ * Shared snapshot activity rendering. Full snapshots focus known tool evidence;
275
+ * `latest` preserves compact callers' established newest-entry selection.
276
+ */
277
+ export function snapshotActivityLines(snapshot, context, selection = {}) {
278
+ const entries = orderedSnapshotEntries(snapshot);
279
+ if (selection.latest !== true)
280
+ return groupedEntries(focusedSnapshotEntries(entries), context);
281
+ const latest = entries.filter((entry) => entry.kind === "row").at(-1);
282
+ return latest === undefined ? [] : groupedEntries([latest], context);
283
+ }
284
+ /**
285
+ * One status' timeline without its conclusion: the newest open entry is still
286
+ * moving, and an idle outcome is the command's final result rather than a
287
+ * settled row.
288
+ */
289
+ function settledTimeline(snapshot) {
290
+ switch (snapshot.kind) {
291
+ case "unborn":
292
+ return snapshot;
293
+ case "open":
294
+ return { ...snapshot, entries: snapshot.entries.slice(0, -1) };
295
+ case "idle": {
296
+ const { outcome: _outcome, ...rest } = snapshot;
297
+ return { ...rest, entries: snapshot.entries };
298
+ }
299
+ }
300
+ }
301
+ function settledRows(snapshot) {
302
+ return orderedSnapshotEntries(settledTimeline(snapshot)).flatMap((entry) => entry.kind === "row" ? [entry.row] : []);
303
+ }
304
+ function renderStreamRow(state, row, lines, context, layout) {
305
+ const at = clock(row.at);
306
+ const changed = state.previousClock === undefined || at !== state.previousClock;
307
+ lines.push(...renderRow(row, context, false, layout.head(changed ? at : undefined, mark(row), label(row)), layout.continuation()));
308
+ state.previousClock = at;
309
+ }
310
+ function coalesceDeferredGaps(state) {
311
+ state.deferred = state.deferred.reduce((entries, entry) => {
312
+ const previous = entries.at(-1);
313
+ if (entry.kind === "gap" && previous?.kind === "gap") {
314
+ entries[entries.length - 1] = { kind: "gap", count: previous.count + entry.count };
315
+ }
316
+ else {
317
+ entries.push(entry);
318
+ }
319
+ return entries;
320
+ }, []);
321
+ }
322
+ function omitOldestDeferredTool(state) {
323
+ const index = state.deferred.findIndex((entry) => entry.kind === "row" && entry.row.kind === "tool");
324
+ if (index === -1)
325
+ throw new Error("activity tail lost its pending tool");
326
+ state.deferred[index] = { kind: "gap", count: 1 };
327
+ coalesceDeferredGaps(state);
328
+ }
329
+ /** Emit only a prefix whose omission runs can no longer join an unresolved tail tool. */
330
+ function flushSafeActivityPrefix(state, lines, context, layout) {
331
+ for (;;) {
332
+ const first = state.deferred[0];
333
+ if (first === undefined || (first.kind === "row" && first.row.kind === "tool"))
334
+ return;
335
+ if (first.kind === "row") {
336
+ state.deferred.shift();
337
+ renderStreamRow(state, first.row, lines, context, layout);
338
+ continue;
339
+ }
340
+ const next = state.deferred[1];
341
+ if (next === undefined || (next.kind === "row" && next.row.kind === "tool"))
342
+ return;
343
+ state.deferred.shift();
344
+ lines.push(layout.marker(first.count));
345
+ }
346
+ }
347
+ function observeActivitySnapshot(state, snapshot, context, layout) {
348
+ const rows = settledRows(snapshot)
349
+ .filter((row) => state.newestSequence === undefined || row.sequence > state.newestSequence)
350
+ // Thoughts remain retained activity, but are ineligible for default live progress.
351
+ .filter((row) => row.kind !== "thought");
352
+ if (rows.length === 0)
353
+ return [];
354
+ state.newestSequence = rows.reduce((newest, row) => Math.max(newest, row.sequence), state.newestSequence ?? rows[0].sequence);
355
+ const lines = [];
356
+ for (const row of rows) {
357
+ if (row.kind === "tool" && state.openingTools < OPENING_TOOL_BUDGET) {
358
+ state.openingTools += 1;
359
+ renderStreamRow(state, row, lines, context, layout);
360
+ continue;
361
+ }
362
+ if (row.kind !== "tool" && state.deferred.length === 0) {
363
+ renderStreamRow(state, row, lines, context, layout);
364
+ continue;
365
+ }
366
+ state.deferred.push({ kind: "row", row });
367
+ if (row.kind === "tool") {
368
+ const pendingTools = state.deferred.filter((entry) => entry.kind === "row" && entry.row.kind === "tool").length;
369
+ if (pendingTools > RECENT_TOOL_BUDGET)
370
+ omitOldestDeferredTool(state);
371
+ }
372
+ flushSafeActivityPrefix(state, lines, context, layout);
373
+ }
374
+ return lines;
375
+ }
376
+ function flushActivityTail(state, context, layout) {
377
+ const lines = [];
378
+ for (const entry of state.deferred) {
379
+ if (entry.kind === "gap")
380
+ lines.push(layout.marker(entry.count));
381
+ else
382
+ renderStreamRow(state, entry.row, lines, context, layout);
383
+ }
384
+ state.deferred = [];
385
+ return lines;
386
+ }
387
+ /**
388
+ * Append-only live view over one command's successive settled snapshots. The
389
+ * first three tools stream immediately; later tools wait in a two-row tail
390
+ * until the command ends. As a newer tool displaces an older tail candidate,
391
+ * its body becomes an in-place omission count. Narrative waits only while a
392
+ * preceding tail tool still decides its position.
393
+ */
394
+ export function activityStream(context, layout = plainLayout()) {
395
+ const state = {
396
+ newestSequence: undefined,
397
+ previousClock: undefined,
398
+ openingTools: 0,
399
+ deferred: [],
400
+ };
401
+ const seed = (snapshot) => {
402
+ const rows = settledRows(snapshot);
403
+ if (rows.length > 0)
404
+ state.newestSequence = rows.reduce((newest, row) => Math.max(newest, row.sequence), rows[0].sequence);
405
+ };
406
+ const observe = (snapshot) => observeActivitySnapshot(state, snapshot, context, layout);
407
+ const flush = () => flushActivityTail(state, context, layout);
408
+ return Object.assign(observe, { seed, flush });
409
+ }
410
+ function conclusionMarkVerb(status, answered) {
411
+ if (status.life === "running")
412
+ return { mark: "●", verb: "still running", waited: true };
413
+ if (answered)
414
+ return { mark: "✓", verb: "answered", waited: false };
415
+ if (status.life === "asleep")
416
+ return { mark: "!", verb: "failed", waited: false };
417
+ if (status.life === "killed")
418
+ return { mark: "×", verb: "killed", waited: false };
419
+ if (status.life === "hung")
420
+ return { mark: "?", verb: "hung", waited: false };
421
+ if (status.life === "untidy")
422
+ return { mark: "!", verb: "untidy", waited: false };
423
+ return { mark: "!", verb: "stranded", waited: false };
424
+ }
425
+ /**
426
+ * The durable moment an Akuma settled, read from the outcome row its own timeline
427
+ * retained. Absent for an ending that leaves no outcome row, so the caller falls
428
+ * back to the observation that noticed it.
429
+ */
430
+ function settleMoment(status) {
431
+ const outcome = status.timeline.kind === "idle" ? status.timeline.outcome : undefined;
432
+ if (outcome === undefined)
433
+ return undefined;
434
+ const at = new Date(outcome.at).getTime();
435
+ return Number.isFinite(at) ? at : undefined;
436
+ }
437
+ function clockFromMs(at) {
438
+ return clock(new Date(at).toISOString());
439
+ }
440
+ function durationText(durationMs) {
441
+ const seconds = Math.max(0, Math.round(durationMs / 1000));
442
+ if (seconds < 60)
443
+ return `${seconds}s`;
444
+ return `${Math.floor(seconds / 60)}m${String(seconds % 60)}s`;
445
+ }
446
+ function createWaitObservationState() {
447
+ return {
448
+ streams: new Map(),
449
+ attributed: new Map(),
450
+ sources: new Map(),
451
+ settledAt: new Map(),
452
+ sourceWidth: 0,
453
+ opened: false,
454
+ observed: false,
455
+ };
456
+ }
457
+ function registerWaitSource(state, id, alias) {
458
+ if (state.sources.has(id))
459
+ return;
460
+ const label = alias ?? id;
461
+ state.sources.set(id, label);
462
+ state.sourceWidth = Math.max(state.sourceWidth, displayColumns(label));
463
+ }
464
+ function observeWaitRound(state, round, context, now) {
465
+ state.observed = true;
466
+ // Establish the whole round's sources before any row so widths stay aligned within it.
467
+ for (const member of round)
468
+ registerWaitSource(state, member.status.id, member.alias);
469
+ const lines = [];
470
+ for (const { status, alias, contract } of round) {
471
+ const known = state.streams.get(status.id);
472
+ if (known === undefined) {
473
+ if (state.opened)
474
+ lines.push("");
475
+ lines.push(...snapshotHeading(status.id, alias, contract));
476
+ state.opened = true;
477
+ state.attributed.set(status.id, alias);
478
+ // Only a plural wait attributes its rows; a single-target stream keeps the plain row grammar.
479
+ const stream = activityStream(context, state.sources.size > 1
480
+ ? sourceLayout(state.sources.get(status.id) ?? status.id, () => state.sourceWidth)
481
+ : plainLayout());
482
+ state.streams.set(status.id, stream);
483
+ // A wait starts at the current settled frontier: its backlog is neither evidence nor budget.
484
+ stream.seed(status.timeline);
485
+ }
486
+ else {
487
+ lines.push(...known(status.timeline));
488
+ }
489
+ if (!state.settledAt.has(status.id) && waitComplete(status))
490
+ state.settledAt.set(status.id, settleMoment(status) ?? now());
491
+ }
492
+ return lines;
493
+ }
494
+ function concludeWaitStream(state, result, startedAt, now) {
495
+ const tail = [];
496
+ // The returned observation can be newer than the final callback. Advance every known stream
497
+ // before flushing it, while still flushing members that became unobserved at the end.
498
+ for (const observation of result.observations) {
499
+ const stream = state.streams.get(observation.status.id);
500
+ if (stream !== undefined)
501
+ tail.push(...stream(observation.status.timeline));
502
+ }
503
+ for (const stream of state.streams.values())
504
+ tail.push(...stream.flush());
505
+ const end = now();
506
+ const total = result.observations.length + result.unobserved.length;
507
+ const multi = total > 1;
508
+ const conclusions = result.observations.map((observation) => {
509
+ const status = observation.status;
510
+ const answered = statusAnswer(observation) !== undefined;
511
+ const complete = waitComplete(status);
512
+ const at = complete ? (state.settledAt.get(status.id) ?? end) : end;
513
+ const durationMs = Math.max(0, at - startedAt);
514
+ const { mark, verb, waited } = conclusionMarkVerb(status, answered);
515
+ const target = multi ? ` ${padToDisplay(state.sources.get(status.id) ?? status.id, state.sourceWidth)}` : "";
516
+ return `${clockFromMs(at)}${target} ${mark} ${verb} — ${waited ? "waited " : ""}${durationText(durationMs)}`;
517
+ });
518
+ const unobservedLines = result.unobserved.map((member) => unobservedText(member.id, member.diagnostic));
519
+ const answeredSingle = !multi && result.observations.length === 1 && statusAnswer(result.observations[0]) !== undefined;
520
+ const blocks = [
521
+ ...(unobservedLines.length > 0 ? [unobservedLines.join("\n")] : []),
522
+ ...(conclusions.length > 0 ? [(multi ? [""] : []).concat(conclusions).join("\n")] : []),
523
+ ];
524
+ const body = [...tail, ...blocks].join("\n");
525
+ // One blank line keeps the bare stdout answer visually separate from the stream.
526
+ return answeredSingle ? `${body}\n\n` : body;
527
+ }
528
+ /**
529
+ * Live view over a wait's successive observation rounds, one append-only
530
+ * stream per selected Akuma. Each Akuma's stream opens with its identity frame
531
+ * — the identity and Contract association the observed facts carry — before any
532
+ * row; an already settled Akuma prints that frame and never replays backlog
533
+ * rows. `conclude` renders the closing rows once the wait ends: one row per
534
+ * observed Akuma in `<clock> <mark> <verb> — <duration>` grammar, with a target
535
+ * named for a multi-target scoreboard and no activity replay.
536
+ */
537
+ export function waitObservationStream(context, options = {}) {
538
+ const now = options.now ?? (() => Date.now());
539
+ const startedAt = now();
540
+ const state = createWaitObservationState();
541
+ const select = (selected) => {
542
+ for (const member of selected)
543
+ registerWaitSource(state, member.id, member.alias);
544
+ };
545
+ const observe = (round) => observeWaitRound(state, round, context, now);
546
+ const conclude = (result) => concludeWaitStream(state, result, startedAt, now);
547
+ return { select, observe, conclude, streamed: () => state.observed };
548
+ }
549
+ function failedOutcomeDiagnostic(status) {
550
+ const outcome = status.timeline.kind === "idle" ? status.timeline.outcome : undefined;
551
+ return outcome !== undefined && outcome.outcome.kind === "failed" ? outcome.outcome.diagnostic : undefined;
552
+ }
553
+ /**
554
+ * Live view over one observing call: its identity frame and birth diagnostics
555
+ * open the stream once, settled rows follow as they arrive, and the return
556
+ * appends one conclusion in single-target wait grammar. The stream never
557
+ * replays its own activity as a final snapshot, and it never carries the
558
+ * birth receipt's cwd row.
559
+ */
560
+ export function callObservationStream(context, head, options = {}) {
561
+ const now = options.now ?? (() => Date.now());
562
+ const startedAt = now();
563
+ const stream = activityStream(context);
564
+ let opened = false;
565
+ const open = (lines) => {
566
+ if (opened)
567
+ return;
568
+ opened = true;
569
+ lines.push(...snapshotHeading(head.id, head.alias, head.contract), ...head.facts);
570
+ };
571
+ const observe = (status) => {
572
+ const lines = [];
573
+ open(lines);
574
+ lines.push(...stream(status.timeline));
575
+ return lines;
576
+ };
577
+ const conclude = (observation) => {
578
+ const lines = [];
579
+ open(lines);
580
+ if (observation.kind === "observed")
581
+ lines.push(...stream(observation.status.timeline));
582
+ lines.push(...stream.flush());
583
+ if (observation.kind === "failed") {
584
+ lines.push(`! error ${safeText(observation.failure.diagnostic)}`);
585
+ return lines.join("\n");
586
+ }
587
+ if (observation.kind !== "observed")
588
+ return lines.join("\n");
589
+ const end = now();
590
+ const status = observation.status;
591
+ const answered = statusAnswer({ status }) !== undefined;
592
+ const at = waitComplete(status) ? (settleMoment(status) ?? end) : end;
593
+ const durationMs = Math.max(0, at - startedAt);
594
+ const { mark, verb, waited } = conclusionMarkVerb(status, answered);
595
+ lines.push(`${clockFromMs(at)} ${mark} ${verb} — ${waited ? "waited " : ""}${durationText(durationMs)}`);
596
+ const failure = failedOutcomeDiagnostic(status);
597
+ if (failure !== undefined)
598
+ lines.push(`! error ${safeText(failure)}`);
599
+ return lines.join("\n");
600
+ };
601
+ return { observe, conclude, opened: () => opened };
173
602
  }
174
603
  function taskDispositionMark(disposition) {
175
604
  if (disposition === "done")
176
605
  return "✓";
177
606
  if (disposition === "drop")
178
- return "";
607
+ return "×";
179
608
  if (disposition === "on_hold")
180
609
  return "⧗";
181
610
  if (disposition === "in_progress")
@@ -186,6 +615,8 @@ function changeStat(change) {
186
615
  return change.diffstat === undefined ? "+? -?" : `+${change.diffstat.added} -${change.diffstat.removed}`;
187
616
  }
188
617
  function renderReportedChangeLines(snapshot) {
618
+ if (snapshot.reportedChanges.length === 0 && snapshot.reportedChangesOmitted === 0)
619
+ return [];
189
620
  const width = snapshot.reportedChanges.reduce((max, change) => Math.max(max, changeStat(change).length), 0);
190
621
  return [
191
622
  `changes ${snapshot.reportedChanges.length + snapshot.reportedChangesOmitted}`,
@@ -211,6 +642,8 @@ function renderTaskContextLines(created, columns) {
211
642
  return [];
212
643
  if (created.kind === "failed")
213
644
  return [`! tasks failed ${safeText(created.diagnostic)}`];
645
+ if (created.rows.length === 0)
646
+ return [];
214
647
  return [`tasks ${created.rows.length}`, ...created.rows.flatMap((row) => renderTaskRow(row, columns))];
215
648
  }
216
649
  function answerContextLines(observation, columns) {
@@ -220,21 +653,17 @@ function answerContextLines(observation, columns) {
220
653
  ];
221
654
  }
222
655
  function answeredBlock(observation, answer, alias, columns) {
656
+ const frame = answeredHeading(observation.status.id, alias).join("\n");
223
657
  const context = answerContextLines(observation, columns).join("\n");
224
658
  if (context.length === 0)
225
- return `${answeredHeading(observation.status.id, alias)}\n${answer}`;
659
+ return `${frame}\n${answer}`;
226
660
  const separator = answer.endsWith("\n") ? "\n" : "\n\n";
227
- return `${answeredHeading(observation.status.id, alias)}\n${answer}${separator}${context}`;
661
+ return `${frame}\n${answer}${separator}${context}`;
228
662
  }
229
663
  function snapshotCore(view, context, options) {
230
- const snapshot = view.status.timeline;
231
- const activity = snapshot.kind === "idle" && snapshot.outcome !== undefined
232
- ? groupedRows([
233
- ...snapshot.entries.filter((entry) => entry.kind === "row").map((entry) => entry.row),
234
- snapshot.outcome,
235
- ].sort((left, right) => left.sequence - right.sequence), context)
236
- : groupedEntries(snapshot.entries, context);
664
+ const activity = snapshotActivityLines(view.status.timeline, context);
237
665
  const facts = [
666
+ ...(options.showAllowed === true ? [`allowed ${view.status.allowed.join(", ") || "none"}`] : []),
238
667
  ...(view.status.readonly?.enforcement === "none" ? [`! ${safeText(view.status.readonly.diagnostic)}`] : []),
239
668
  ...contractFacts(view.contract),
240
669
  ...(options.facts ?? []),
@@ -298,12 +727,26 @@ function answerCallFailed(result) {
298
727
  }
299
728
  export function akumaRawAnswer(result) {
300
729
  if (result.action === "call") {
730
+ if (result.streamed === true) {
731
+ // A streamed call's stdout is its answer or nothing; diagnostics belong to stderr.
732
+ if (answerCallFailed(result.result) || result.result.observation.kind !== "observed")
733
+ return "";
734
+ return statusAnswer({ status: parseAkumaStatus(result.result.observation.status) }) ?? "";
735
+ }
301
736
  if (answerCallFailed(result.result) || result.result.observation.kind !== "observed")
302
737
  return undefined;
303
738
  return statusAnswer({ status: parseAkumaStatus(result.result.observation.status) });
304
739
  }
305
- if (result.action === "wait" && result.result.observations.length === 1)
306
- return statusAnswer(result.result.observations[0]);
740
+ if (result.action === "wait") {
741
+ if (result.streamed === true) {
742
+ // Plural waits leave stdout empty so one answer can never be mistaken for the whole result.
743
+ const total = result.result.observations.length + result.result.unobserved.length;
744
+ const single = total === 1 ? result.result.observations[0] : undefined;
745
+ return single === undefined ? "" : (statusAnswer(single) ?? "");
746
+ }
747
+ if (result.result.observations.length === 1)
748
+ return statusAnswer(result.result.observations[0]);
749
+ }
307
750
  if (result.action === "history" && result.mode === "exact" && result.historyResult.kind === "exact") {
308
751
  return result.historyResult.outcome.outcome.kind === "answered"
309
752
  ? result.historyResult.outcome.outcome.answer
@@ -339,10 +782,11 @@ export function historyText(command, result, context) {
339
782
  return result.mode === "last" ? result.answer : "no answer retained";
340
783
  if (result.mode !== "page")
341
784
  throw new Error("history result lacks page");
342
- return [
343
- ...snapshotHeading(result.akuma, result.alias, result.historyResult.contract),
344
- ...groupedRows(result.history.rows, context, true),
345
- ].join("\n");
785
+ const rows = groupedRows(result.history.rows, context, true);
786
+ const paging = result.history.omitted > 0
787
+ ? [` ⋮ ${result.history.omitted} earlier turns · showing last ${result.history.rows.length}`]
788
+ : [];
789
+ return [...snapshotHeading(result.akuma, result.alias, result.historyResult.contract), ...paging, ...rows].join("\n");
346
790
  }
347
791
  export function tellText(result, context) {
348
792
  const wake = result.result.tell.wake;
@@ -350,7 +794,8 @@ export function tellText(result, context) {
350
794
  const row = groupedRows([result.result.tell.row], context).join("\n");
351
795
  if (wake.kind === "failed") {
352
796
  const child = "child" in wake ? wake.child : undefined;
353
- return `${target}\n${SNAPSHOT_RULER}\n${row}\n! tell delivery failed · ${safeText(wake.diagnostic)}${child === undefined ? "" : ` · log ${child.log.path} ${child.log.from}..${child.log.to}`}`;
797
+ const failure = `! tell delivery failed · ${safeText(wake.diagnostic)}${child === undefined ? "" : ` · log ${child.log.path} ${child.log.from}..${child.log.to}`}`;
798
+ return `${target}\n${row}\n${renderBoundedTextBlock(failure, { first: "", continuation: " ", columns: context.columns, lines: Number.MAX_SAFE_INTEGER }).join("\n")}`;
354
799
  }
355
- return `${target}\n${SNAPSHOT_RULER}\n${row}`;
800
+ return `${target}\n${row}`;
356
801
  }