@astrosheep/keiyaku 4.0.6 → 4.1.0

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 (65) hide show
  1. package/build/src/akuma/archetype.js +8 -1
  2. package/build/src/akuma/provider-recipe.d.ts +2 -0
  3. package/build/src/akuma/provider-recipe.js +10 -0
  4. package/build/src/akuma/providers/acp/config.d.ts +2 -0
  5. package/build/src/akuma/providers/acp/config.js +9 -1
  6. package/build/src/akuma/providers/acp/core.d.ts +5 -2
  7. package/build/src/akuma/providers/acp/core.js +29 -16
  8. package/build/src/akuma/providers/acp/index.js +8 -2
  9. package/build/src/akuma/providers/claude/index.d.ts +1 -1
  10. package/build/src/akuma/providers/claude/index.js +9 -5
  11. package/build/src/akuma/providers/codex-app-server/index.js +4 -1
  12. package/build/src/akuma/providers/grok-build/index.js +15 -1
  13. package/build/src/akuma/providers/index.js +1 -1
  14. package/build/src/akuma/providers/opencode-sdk/index.js +3 -0
  15. package/build/src/akuma/providers/pi/index.js +3 -1
  16. package/build/src/body/amend.js +0 -9
  17. package/build/src/body/region.js +2 -2
  18. package/build/src/cli/accepted.js +2 -0
  19. package/build/src/cli/commands/contract.d.ts +5 -3
  20. package/build/src/cli/commands/contract.js +23 -13
  21. package/build/src/cli/commands/task-invoke.d.ts +4 -1
  22. package/build/src/cli/commands/task-invoke.js +6 -3
  23. package/build/src/cli/commands/task.js +10 -2
  24. package/build/src/cli/parse.js +2 -2
  25. package/build/src/cli/render/akuma.js +42 -11
  26. package/build/src/cli/render/catalog.js +5 -1
  27. package/build/src/cli/render/contract.js +25 -3
  28. package/build/src/cli/render/kanshi.js +142 -94
  29. package/build/src/cli/render/receipt.d.ts +1 -0
  30. package/build/src/cli/render/receipt.js +18 -14
  31. package/build/src/cli/render/refusal.js +6 -0
  32. package/build/src/cli/render/task.js +63 -11
  33. package/build/src/cli/result.d.ts +8 -1
  34. package/build/src/contract-worktree.d.ts +0 -2
  35. package/build/src/contract-worktree.js +13 -24
  36. package/build/src/git/nuke.js +1 -6
  37. package/build/src/git/reconcile.d.ts +0 -2
  38. package/build/src/git/reconcile.js +2 -33
  39. package/build/src/git/tender.d.ts +13 -2
  40. package/build/src/git/tender.js +43 -4
  41. package/build/src/kanshi/read.js +32 -12
  42. package/build/src/kanshi/report.d.ts +2 -1
  43. package/build/src/library/continuation.d.ts +26 -0
  44. package/build/src/library/continuation.js +78 -0
  45. package/build/src/library/contract.d.ts +9 -5
  46. package/build/src/library/contract.js +27 -2
  47. package/build/src/library/delivery.d.ts +18 -13
  48. package/build/src/library/keiyaku.d.ts +1 -1
  49. package/build/src/library/mutation.js +11 -4
  50. package/build/src/protocol/deliver.d.ts +12 -2
  51. package/build/src/protocol/deliver.js +52 -19
  52. package/build/src/protocol/operations.d.ts +7 -2
  53. package/build/src/settlement/fence.d.ts +0 -2
  54. package/build/src/settlement/fence.js +0 -24
  55. package/build/src/task/board.d.ts +21 -2
  56. package/build/src/task/board.js +25 -12
  57. package/build/src/task/identity.js +14 -4
  58. package/build/src/task/index.d.ts +1 -0
  59. package/build/src/task/index.js +7 -1
  60. package/build/src/task/operations.d.ts +2 -1
  61. package/build/src/task/operations.js +15 -4
  62. package/build/src/task/query.js +2 -2
  63. package/build/src/task/store.d.ts +2 -0
  64. package/build/src/task/store.js +12 -83
  65. package/package.json +1 -1
@@ -14,14 +14,20 @@ function associatedIdentity(id, alias, contract) {
14
14
  const contractId = contract === undefined ? undefined : associatedContractId(contract);
15
15
  return `${identity(id, alias)}${contractId === undefined ? "" : ` [${contractId}]`}`;
16
16
  }
17
- function snapshotHeading(id, alias, contract) {
17
+ function ruler(columns) {
18
+ return "─".repeat(Math.max(OPENING_STROKE.length, columns));
19
+ }
20
+ function snapshotHeading(id, alias, contract, columns) {
18
21
  const contractId = contract === undefined ? undefined : associatedContractId(contract);
19
22
  return [
20
- OPENING_STROKE,
21
23
  identity(id, alias),
24
+ ruler(columns),
22
25
  ...(contractId === undefined ? [] : [`└─ ${contractId}`]),
23
26
  ];
24
27
  }
28
+ function answeredHeading(id, alias, columns) {
29
+ return [`✓ came back ${identity(id, alias)}`, ruler(columns)].join("\n");
30
+ }
25
31
  function contractFacts(contract) {
26
32
  return contract.kind === "failed" ? [`! contract failed ${safeText(contract.diagnostic)}`] : [];
27
33
  }
@@ -32,7 +38,7 @@ function lifeLabel(life) {
32
38
  if (life === "running")
33
39
  return "● STILL RUNNING";
34
40
  if (life === "asleep")
35
- return "○ asleep";
41
+ return "✓ came back";
36
42
  if (life === "killed")
37
43
  return "× killed";
38
44
  return `? ${life}`;
@@ -214,6 +220,19 @@ function renderTaskContextLines(created, columns) {
214
220
  return [`! tasks failed ${safeText(created.diagnostic)}`];
215
221
  return [`tasks ${created.rows.length}`, ...created.rows.flatMap((row) => renderTaskRow(row, columns))];
216
222
  }
223
+ function answerContextLines(observation, columns) {
224
+ return [
225
+ ...renderTaskContextLines(observation.createdTasks, columns),
226
+ ...renderReportedChangeLines(observation.status.timeline),
227
+ ];
228
+ }
229
+ function answeredBlock(observation, answer, alias, columns) {
230
+ const context = answerContextLines(observation, columns).join("\n");
231
+ if (context.length === 0)
232
+ return `${answeredHeading(observation.status.id, alias, columns)}\n${answer}`;
233
+ const separator = answer.endsWith("\n") ? "\n" : "\n\n";
234
+ return `${answeredHeading(observation.status.id, alias, columns)}\n${answer}${separator}${context}`;
235
+ }
217
236
  function snapshotText(view, context, options = {}) {
218
237
  const snapshot = view.status.timeline;
219
238
  const activity = snapshot.kind === "idle" && snapshot.outcome !== undefined
@@ -227,13 +246,16 @@ function snapshotText(view, context, options = {}) {
227
246
  ...contractFacts(view.contract),
228
247
  ...(options.facts ?? []),
229
248
  ];
249
+ const taskContext = renderTaskContextLines(view.createdTasks, context.columns);
250
+ const changes = renderReportedChangeLines(snapshot);
230
251
  const footer = options.showLife === false ? [] : ["", lifeLabel(view.status.life)];
231
252
  return [
232
- ...snapshotHeading(view.status.id, options.alias, view.contract),
253
+ ...snapshotHeading(view.status.id, options.alias, view.contract, context.columns),
233
254
  ...facts,
234
255
  ...activity,
235
- ...renderTaskContextLines(view.createdTasks, context.columns),
236
- ...renderReportedChangeLines(snapshot),
256
+ ...(activity.length > 0 && taskContext.length > 0 ? [""] : []),
257
+ ...taskContext,
258
+ ...changes,
237
259
  ...footer,
238
260
  ].join("\n");
239
261
  }
@@ -244,18 +266,27 @@ function observationStageText(id, observation, context, options = {}) {
244
266
  }
245
267
  function waitText(result, context) {
246
268
  const alias = result.alias;
269
+ const total = result.result.observations.length + result.result.unobserved.length;
270
+ const done = result.result.observations.filter((observation) => observation.status.life !== "running").length;
247
271
  const blocks = [
248
- ...result.result.observations.map((observation) => snapshotText(observation, context, { ...(alias === undefined ? {} : { alias }) })),
272
+ ...result.result.observations.map((observation) => {
273
+ const answer = statusAnswer(observation);
274
+ if (answer !== undefined)
275
+ return answeredBlock(observation, answer, alias, context.columns);
276
+ return snapshotText(observation, context, { ...(alias === undefined ? {} : { alias }) });
277
+ }),
249
278
  ...result.result.unobserved.map((member) => unobservedText(member.id, member.diagnostic)),
250
279
  ];
251
- return blocks.join("\n\n");
280
+ if (total <= 1)
281
+ return blocks.join("\n\n");
282
+ return [...blocks, `${done} of ${total} done`].join("\n\n");
252
283
  }
253
284
  function historyText(command, result, context) {
254
285
  if (command.last)
255
286
  return result.mode === "last" ? result.answer : "no answer retained";
256
287
  if (result.mode !== "page")
257
288
  throw new Error("history result lacks page");
258
- return [...snapshotHeading(result.akuma, result.alias, result.historyResult.contract), ...groupedRows(result.history.rows, context, true)].join("\n");
289
+ return [...snapshotHeading(result.akuma, result.alias, result.historyResult.contract, context.columns), ...groupedRows(result.history.rows, context, true)].join("\n");
259
290
  }
260
291
  function tellText(result, context) {
261
292
  const facts = typeof result.result.tell.wake === "string" ? [] : [`! error ${safeText(result.result.tell.wake.diagnostic)}`];
@@ -317,7 +348,7 @@ function callText(result, context) {
317
348
  if (result.result.observation.kind === "detached") {
318
349
  const lines = [...snapshotHeading(result.result.akuma, alias, contractId === undefined
319
350
  ? { kind: "none" }
320
- : { kind: "associated", contractId }), ...cwd, ...restraint, ...facts];
351
+ : { kind: "associated", contractId }, context.columns), ...cwd, ...restraint, ...facts];
321
352
  if (!callFailed(result.result)) {
322
353
  const selector = result.result.alias.kind === "aliased" ? result.result.alias.alias.alias : result.result.akuma;
323
354
  lines.push(`$ keiyaku -C ${posixShellArgument(result.world)} wait ${selector} --timeout 5m`);
@@ -327,7 +358,7 @@ function callText(result, context) {
327
358
  if (result.result.observation.kind === "failed") {
328
359
  return [...snapshotHeading(result.result.akuma, alias, contractId === undefined
329
360
  ? { kind: "none" }
330
- : { kind: "associated", contractId }), ...cwd, ...restraint, ...facts, `! error ${safeText(result.result.observation.failure.diagnostic)}`].join("\n");
361
+ : { kind: "associated", contractId }, context.columns), ...cwd, ...restraint, ...facts, `! error ${safeText(result.result.observation.failure.diagnostic)}`].join("\n");
331
362
  }
332
363
  return snapshotText({
333
364
  status: result.result.observation.status,
@@ -12,5 +12,9 @@ export function renderCatalogText(catalog) {
12
12
  ...(row.description === undefined ? [] : [` ${safeText(row.description)}`]),
13
13
  ]).join("\n");
14
14
  }
15
- return catalog.rows.map((row) => `${safeText(row.id)} - ${row.life}`).join("\n");
15
+ return catalog.rows.map((row) => {
16
+ if (!("lifeAt" in row))
17
+ return `${safeText(row.id)} - ${row.life}`;
18
+ return `${safeText(row.id)} - ${row.life} - runtime ${row.lifeAt ?? "-"} - last activity ${row.lastActivityAt ?? "-"}`;
19
+ }).join("\n");
16
20
  }
@@ -1,5 +1,5 @@
1
1
  import { renderAcceptedAudit } from "./audit.js";
2
- import { appendHookPayload, cleanupLines, hookFailureSummary, leakLines, outcomeLines, receiptPayload, receiptRow, reuseLines, stopLines, titleLines, } from "./receipt.js";
2
+ import { appendHookPayload, cleanupLines, hookFailureSummary, leakLines, outcomeLines, receiptPayload, receiptRow, reuseLines, stopLines, stopSummary, titleLines, } from "./receipt.js";
3
3
  import { gitShortStat, renderOpaqueBlock } from "./terminal.js";
4
4
  const HANG = " ";
5
5
  function isRecord(value) {
@@ -217,6 +217,28 @@ function movementLines(result, columns) {
217
217
  receiptRow(lines, "~", "target", [{ text: `moved · re-integrated x${count}` }], columns);
218
218
  return lines;
219
219
  }
220
+ function continuationLines(result, columns) {
221
+ const report = result.continuation;
222
+ if (report === undefined)
223
+ return [];
224
+ const lines = [];
225
+ for (const contractId of report.claimed) {
226
+ receiptRow(lines, "✓", "continuation", [
227
+ { text: "complete" },
228
+ { text: contractId, opaque: true },
229
+ ], columns);
230
+ }
231
+ for (const { contractId, stop } of report.stopped) {
232
+ const detail = "kind" in stop ? [{ text: stop.kind }] : stopSummary(stop, contractId);
233
+ receiptRow(lines, "!", "continuation", [
234
+ { text: "blocked" },
235
+ { text: contractId, opaque: true },
236
+ { text: "·" },
237
+ ...detail,
238
+ ], columns);
239
+ }
240
+ return lines;
241
+ }
220
242
  function renderAcceptedBind(result, columns) {
221
243
  const lines = titleLines("✓", "bound", result.contract, columns);
222
244
  receiptRow(lines, " ", "workspace", [
@@ -239,7 +261,7 @@ function renderAcceptedDeliver(result, columns) {
239
261
  const complete = result.completion !== undefined;
240
262
  const title = complete ? "delivered" : "deliver — not complete";
241
263
  const lines = titleLines("✓", title, result.contract, columns);
242
- lines.push(...movementLines(result, columns), ...completionLines(result, columns));
264
+ lines.push(...movementLines(result, columns), ...completionLines(result, columns), ...continuationLines(result, columns));
243
265
  if (!complete)
244
266
  receiptRow(lines, " ", "candidate", [{ text: "kept" }], columns);
245
267
  if (result.verification !== undefined) {
@@ -258,7 +280,7 @@ function renderAcceptedDeliver(result, columns) {
258
280
  function renderAcceptedReview(result, columns) {
259
281
  const complete = result.completion !== undefined;
260
282
  const lines = titleLines("✓", `review ${result.verdict} — ${complete ? "complete" : "not complete"}`, result.contract, columns);
261
- lines.push(...movementLines(result, columns), ...completionLines(result, columns));
283
+ lines.push(...movementLines(result, columns), ...completionLines(result, columns), ...continuationLines(result, columns));
262
284
  if (!complete)
263
285
  receiptRow(lines, " ", "candidate", [{ text: "kept" }], columns);
264
286
  if (result.placement !== undefined) {
@@ -1,8 +1,8 @@
1
1
  import { displayColumns, renderTextBlock, safeText, tone } from "./terminal.js";
2
- const RULE = "─";
3
- const HORIZON = "───";
4
2
  const PLUMB = " │ ";
5
3
  const PATH_PREFIX = `${PLUMB}↳ `;
4
+ const MAX_VISIBLE_ROWS = 10;
5
+ const FIELD_WIDTH = 8;
6
6
  function contractMark(row) {
7
7
  if (row.phase === "claimed")
8
8
  return "✓";
@@ -46,6 +46,13 @@ function gateMark(report) {
46
46
  return "?";
47
47
  return report.current.verdict === "satisfied" ? "✓" : "!";
48
48
  }
49
+ function worldGateMark(report) {
50
+ if (report.current.kind === "stale")
51
+ return "~";
52
+ if (report.current.kind !== "attested")
53
+ return " ";
54
+ return report.current.verdict === "satisfied" ? "✓" : "✗";
55
+ }
49
56
  function formatAge(source, observedAt) {
50
57
  if (source === null || source === undefined)
51
58
  return "—";
@@ -76,11 +83,11 @@ function targetFacts(row) {
76
83
  return ["no target"];
77
84
  const facts = [`target ${name}`];
78
85
  if (row.targetLag.kind === "unknown")
79
- facts.push("behind unknown");
86
+ facts.push(`commits behind ${name} unknown`);
80
87
  if (row.targetLag.kind === "counted")
81
- facts.push(`behind ${row.targetLag.behind}`);
88
+ facts.push(`${row.targetLag.behind} commits behind ${name}`);
82
89
  if (row.targetObservation?.drift === true)
83
- facts.push("drift");
90
+ facts.push("target moved");
84
91
  return facts;
85
92
  }
86
93
  function workspaceState(row) {
@@ -140,10 +147,24 @@ function akumaHot(row) {
140
147
  return true;
141
148
  return row.contract?.observed === "missing" || row.contract?.observed === "unavailable";
142
149
  }
143
- function aperture(label, columns) {
144
- const prefix = `──[ ${label} ]`;
145
- const fill = Math.max(1, columns - displayColumns(prefix));
146
- return `${prefix}${RULE.repeat(fill)}`;
150
+ function activitySnapshotLine(row) {
151
+ const snapshot = row.snapshot;
152
+ if (snapshot === undefined)
153
+ return undefined;
154
+ const entries = snapshot.entries.flatMap((entry) => entry.kind === "row" ? [entry.row] : []);
155
+ const latest = entries.at(-1);
156
+ if (latest === undefined) {
157
+ if (snapshot.kind === "idle" && snapshot.outcome !== undefined) {
158
+ return snapshot.outcome.outcome.kind === "answered" ? snapshot.outcome.outcome.answer : snapshot.outcome.outcome.diagnostic;
159
+ }
160
+ return "empty";
161
+ }
162
+ if (latest.kind === "said" || latest.kind === "thought" || latest.kind === "note" || latest.kind === "call" || latest.kind === "tell") {
163
+ return latest.text;
164
+ }
165
+ if (latest.kind === "tool")
166
+ return latest.name;
167
+ return "activity";
147
168
  }
148
169
  function plumbFacts(facts, columns) {
149
170
  const clean = facts.map(safeText).filter((fact) => fact.length > 0);
@@ -185,11 +206,28 @@ function renderGates(reports, observedAt, columns, includeSummaries) {
185
206
  }
186
207
  return lines;
187
208
  }
209
+ function fieldPrefix(label) {
210
+ return ` ${label.padEnd(FIELD_WIDTH, " ")}`;
211
+ }
212
+ function fieldLine(label, value) {
213
+ return `${fieldPrefix(label)}${safeText(value)}`;
214
+ }
215
+ function fieldBlock(label, value, columns) {
216
+ const continuation = " ".repeat(fieldPrefix(label).length);
217
+ return renderTextBlock(value, continuation, columns).map((line, index) => index === 0
218
+ ? `${fieldPrefix(label)}${line.slice(continuation.length)}`
219
+ : line);
220
+ }
221
+ function renderWorldGates(reports) {
222
+ if (reports.length === 0)
223
+ return [];
224
+ return [fieldLine("GATES", reports.map((report) => `[${worldGateMark(report)}] ${report.gate}`).join(" "))];
225
+ }
188
226
  function failure(name, section, context) {
189
- return [aperture(name, context.columns), `${tone(`! ${safeText(section.failure.message)}`, "alert", context.color)}`];
227
+ return [`[ ${name} ]`, "", `${tone(`! ${safeText(section.failure.message)}`, "alert", context.color)}`];
190
228
  }
191
- function sectionAbsent(name, columns) {
192
- return [aperture(name, columns), `${PLUMB}${name.toLowerCase()} absent`];
229
+ function sectionAbsent(name, _columns) {
230
+ return [`[ ${name} ]`, "", `${PLUMB}${name.toLowerCase()} absent`];
193
231
  }
194
232
  function endpointFact(id, observed) {
195
233
  if (observed === "missing")
@@ -249,22 +287,69 @@ function renderContractRow(row, report, context, selection) {
249
287
  lines.push(...renderNamespaceTasks(row, context));
250
288
  return lines;
251
289
  }
290
+ function isTerminalContract(row) {
291
+ return row.phase === "claimed" || row.phase === "abandoned";
292
+ }
293
+ function isTerminalTask(row) {
294
+ return row.disposition === "done" || row.disposition === "drop";
295
+ }
296
+ function renderWorldContractRow(row, report, context) {
297
+ const hot = contractHot(row);
298
+ const title = row.title ?? "title unavailable";
299
+ const phase = `${row.phase} · ${formatAge(row.phaseAt, report.observedAt)}`;
300
+ if (!hot)
301
+ return [identityLine(contractMark(row), row.id, `· "${title}" · ${phase} · ${targetFacts(row).join(" · ")}`)];
302
+ const lines = [identityLine(contractMark(row), row.id)];
303
+ lines.push(...fieldBlock("TITLE", title, context.columns));
304
+ lines.push(fieldLine("STATE", phase));
305
+ lines.push(fieldLine("GIT", [...targetFacts(row), workspaceState(row)].join(" · ")));
306
+ if (showWorkspacePath(row, hot)
307
+ && row.workspaceObservation.kind !== "unappointed"
308
+ && row.workspaceObservation.kind !== "failed"
309
+ && row.workspaceObservation.location.kind === "worktree") {
310
+ lines.push(fieldLine("DIR", row.workspaceObservation.location.path));
311
+ }
312
+ lines.push(...renderWorldGates(row.gates.reports));
313
+ const linked = [];
314
+ if (row.holder.kind === "held")
315
+ linked.push(row.holder.taskId);
316
+ if (row.holder.kind === "unavailable")
317
+ linked.push("holder unavailable");
318
+ for (const attached of row.fleet) {
319
+ const aliases = attached.aliases.length === 0 ? "" : ` (${attached.aliases.join(" ")})`;
320
+ linked.push(`${attached.id}${aliases}`);
321
+ }
322
+ if (linked.length > 0) {
323
+ lines.push(fieldLine("LINKED", linked[0]));
324
+ for (const item of linked.slice(1))
325
+ lines.push(` ${" ".repeat(FIELD_WIDTH)}${safeText(item)}`);
326
+ }
327
+ return lines;
328
+ }
329
+ function selectRows(rows, isHot) {
330
+ return [...rows.filter(isHot), ...rows.filter((row) => !isHot(row))].slice(0, MAX_VISIBLE_ROWS);
331
+ }
332
+ function sectionFooter(visible, total, unit, selector) {
333
+ if (visible === total)
334
+ return [` (all ${total} live ${unit} shown)`];
335
+ return [` + ${total - visible} more live ${unit} not shown`, ` keiyaku ls ${selector}/`];
336
+ }
337
+ function sectionBlock(name, unit, selector, rows, total) {
338
+ const lines = [`[ ${name} ] ${total} live`, ""];
339
+ for (const row of rows)
340
+ lines.push(...row);
341
+ lines.push("", ...sectionFooter(rows.length, total, unit, selector));
342
+ return lines;
343
+ }
252
344
  function renderContracts(report, context) {
253
345
  const section = report.contracts;
254
346
  if (section.kind === "absent")
255
347
  return sectionAbsent("KEIYAKU", context.columns);
256
348
  if (section.kind === "failed")
257
349
  return failure("KEIYAKU", section, context);
258
- const rows = [
259
- ...section.value.rows.filter(contractHot),
260
- ...section.value.rows.filter((row) => !contractHot(row)),
261
- ];
262
- const lines = [aperture("KEIYAKU", context.columns)];
263
- for (const row of rows)
264
- lines.push(...renderContractRow(row, report, context, "world"));
265
- const attention = rows.filter(contractHot).length;
266
- lines.push(aperture(`${rows.length} keiyaku · ${attention} attention`, context.columns));
267
- return lines;
350
+ const live = section.value.rows.filter((row) => !isTerminalContract(row));
351
+ const rows = selectRows(live, contractHot);
352
+ return sectionBlock("KEIYAKU", "keiyaku", "kei", rows.map((row) => renderWorldContractRow(row, report, context)), live.length);
268
353
  }
269
354
  function renderSelectedContract(report, context) {
270
355
  const section = report.contracts;
@@ -281,32 +366,24 @@ function renderTasks(report, context) {
281
366
  return sectionAbsent("TASK", context.columns);
282
367
  if (section.kind === "failed")
283
368
  return failure("TASK", section, context);
284
- const rows = [
285
- ...section.value.rows.filter(taskHot),
286
- ...section.value.rows.filter((row) => !taskHot(row)),
287
- ];
288
- const lines = [aperture("TASK", context.columns)];
289
- for (const row of rows) {
369
+ const live = section.value.rows.filter((row) => !isTerminalTask(row));
370
+ const rows = selectRows(live, taskHot);
371
+ const rowLines = rows.map((row) => {
290
372
  const compact = !taskHot(row);
291
- lines.push(identityLine(taskMark(row), row.id));
292
373
  const relation = row.contract === undefined
293
374
  ? ["unbound"]
294
375
  : [endpointFact(row.contract.id, row.contract.observed)];
295
- const facts = [row.title, row.disposition, `P${row.priority}`, ...relation];
296
376
  if (compact)
297
- lines.push(...plumbFacts(facts, context.columns));
298
- else {
299
- lines.push(...plumbBlock(row.title, context.columns));
300
- lines.push(...plumbFacts([row.disposition, `P${row.priority}`], context.columns));
301
- for (const blocker of row.blockers ?? []) {
302
- lines.push(...plumbFacts([`blocked by ${blocker.id}`], context.columns));
303
- }
304
- lines.push(...plumbFacts(relation, context.columns));
305
- }
306
- }
307
- const attention = rows.filter(taskHot).length;
308
- lines.push(aperture(`${rows.length} task · ${attention} attention`, context.columns));
309
- return lines;
377
+ return [identityLine(taskMark(row), row.id, `· "${row.title}" · ${row.disposition} · P${row.priority} · ${relation.join(" · ")}`)];
378
+ const lines = [identityLine(taskMark(row), row.id)];
379
+ lines.push(...fieldBlock("TITLE", row.title, context.columns));
380
+ lines.push(fieldLine("STATE", `${row.disposition} · P${row.priority}`));
381
+ for (const blocker of row.blockers ?? [])
382
+ lines.push(fieldLine("BLOCKED", blocker.id));
383
+ lines.push(fieldLine("LINKED", relation.join(" · ")));
384
+ return lines;
385
+ });
386
+ return sectionBlock("TASK", "task", "task", rowLines, live.length);
310
387
  }
311
388
  function akumaLabel(row) {
312
389
  const aliases = row.aliases ?? [];
@@ -318,13 +395,15 @@ function renderAkuma(report, context) {
318
395
  return sectionAbsent("FLEET", context.columns);
319
396
  if (section.kind === "failed")
320
397
  return failure("FLEET", section, context);
321
- const rows = [
322
- ...section.value.rows.filter(akumaHot),
323
- ...section.value.rows.filter((row) => !akumaHot(row)),
324
- ];
325
- const lines = [aperture("FLEET", context.columns)];
326
- for (const row of rows) {
327
- lines.push(identityLine(akumaMark(row.life), row.id, akumaLabel(row)));
398
+ const live = section.value.rows
399
+ .filter((row) => row.life === "running")
400
+ .sort((left, right) => {
401
+ const l = "lastActivityAt" in left && left.lastActivityAt !== null ? Date.parse(left.lastActivityAt) : -Infinity;
402
+ const r = "lastActivityAt" in right && right.lastActivityAt !== null ? Date.parse(right.lastActivityAt) : -Infinity;
403
+ return r - l;
404
+ });
405
+ const rows = live.slice(0, MAX_VISIBLE_ROWS);
406
+ const rowLines = rows.map((row) => {
328
407
  const lifeAt = "lifeAt" in row ? row.lifeAt : null;
329
408
  const life = `${row.life} · ${formatAge(lifeAt, report.observedAt)}`;
330
409
  const activity = "lastActivityAt" in row && row.lastActivityAt !== null
@@ -336,57 +415,26 @@ function renderAkuma(report, context) {
336
415
  const relation = row.contract === undefined
337
416
  ? ["unbound"]
338
417
  : [endpointFact(row.contract.id, row.contract.observed)];
418
+ const snapshot = activitySnapshotLine(row);
339
419
  if (!akumaHot(row))
340
- lines.push(...plumbFacts([...key, ...relation], context.columns));
341
- else {
342
- lines.push(...plumbFacts(key, context.columns));
343
- lines.push(...plumbFacts(relation, context.columns));
344
- }
345
- }
346
- const attention = rows.filter(akumaHot).length;
347
- lines.push(aperture(`${rows.length} akuma · ${attention} attention`, context.columns));
348
- return lines;
349
- }
350
- function sectionCount(name, section) {
351
- if (section.kind === "present")
352
- return `${section.value.rows.length} ${name}`;
353
- return `${section.kind} ${name}`;
354
- }
355
- function worldCoordinate(report) {
356
- const project = report.root;
357
- const branch = report.branch?.startsWith("refs/heads/") === true
358
- ? report.branch.slice("refs/heads/".length)
359
- : report.branch;
360
- const state = report.contracts.kind === "present" ? report.contracts.value.state ?? null : null;
361
- return [project, branch, state].filter((part) => part !== null && part.length > 0).map(safeText).join(" ");
362
- }
363
- function splitHorizon(report, columns) {
364
- if (report.root === null)
365
- return ["✗ keiyaku world absent"];
366
- const aggregate = [
367
- sectionCount("keiyaku", report.contracts),
368
- sectionCount("akuma", report.akuma),
369
- sectionCount("task", report.tasks),
370
- ].join(" · ");
371
- const world = worldCoordinate(report);
372
- const minimum = `kanshi ${HORIZON} ${aggregate} ${HORIZON} ${world}`;
373
- if (displayColumns(minimum) > columns)
374
- return [minimum];
375
- const fill = columns - displayColumns(minimum);
376
- const left = Math.floor(fill / 2);
377
- const right = Math.ceil(fill / 2);
378
- return [`kanshi ${HORIZON}${RULE.repeat(left)} ${aggregate} ${HORIZON}${RULE.repeat(right)} ${world}`];
420
+ return [identityLine(akumaMark(row.life), row.id, `${akumaLabel(row)} · ${[...key, ...relation, ...(snapshot === undefined ? [] : [snapshot])].join(" · ")}`.trim())];
421
+ const lines = [identityLine(akumaMark(row.life), row.id, akumaLabel(row))];
422
+ lines.push(fieldLine("LIFE", key.join(" · ")));
423
+ if (snapshot !== undefined)
424
+ lines.push(fieldLine("SNAPSHOT", snapshot));
425
+ lines.push(fieldLine("LINKED", relation.join(" · ")));
426
+ return lines;
427
+ });
428
+ return sectionBlock("FLEET", "akuma", "aku", rowLines, live.length);
379
429
  }
380
430
  export function renderKanshiText(report, context = { columns: 80, color: false }, selection = "world") {
381
431
  if (selection === "contract")
382
432
  return renderSelectedContract(report, context).join("\n");
383
433
  return [
384
- ...splitHorizon(report, context.columns),
385
- "",
386
434
  ...renderContracts(report, context),
387
- "",
388
- ...renderTasks(report, context),
389
- "",
435
+ "", "",
390
436
  ...renderAkuma(report, context),
437
+ "", "",
438
+ ...renderTasks(report, context),
391
439
  ].join("\n");
392
440
  }
@@ -15,6 +15,7 @@ export declare function hookFailureSummary(failure: HookFailure): string;
15
15
  export declare function appendHookPayload(lines: string[], failure: HookFailure): void;
16
16
  export declare function reuseLines(reuse: VerificationReuse | undefined, columns: number): readonly string[];
17
17
  export declare function stopLines(label: "verification" | "completion", stop: VerificationStop | PlacementStop, columns: number, addressed: string): readonly string[];
18
+ export declare function stopSummary(stop: VerificationStop | PlacementStop, addressed: string): readonly ReceiptSegment[];
18
19
  export declare function cleanupLines(cleanup: NonNullable<AcceptedDeliverResult["cleanup"]>, columns: number): readonly string[];
19
20
  export declare function leakLines(leak: NonNullable<AcceptedDeliverResult["leak"]>, columns: number): readonly string[];
20
21
  export {};
@@ -86,6 +86,23 @@ function targetMovedDetail(stop) {
86
86
  ];
87
87
  }
88
88
  export function stopLines(label, stop, columns, addressed) {
89
+ const detail = stopSummary(stop, addressed);
90
+ const lines = [];
91
+ const state = label === "verification" ? "failed" : "blocked";
92
+ receiptRow(lines, "!", label, [{ text: state }, { text: "·" }, ...detail], columns);
93
+ lines.push(...prerequisiteRows(stop, columns));
94
+ if ("failure" in stop && stop.failure === "environment-failure" && "command" in stop) {
95
+ appendHookPayload(lines, stop.detail);
96
+ }
97
+ if ("retry" in stop && stop.retry?.kind === "publication-failed") {
98
+ receiptPayload(lines, "diagnostic", stop.retry.diagnostic);
99
+ }
100
+ else if ("failure" in stop && "diagnostic" in stop) {
101
+ receiptPayload(lines, "diagnostic", stop.diagnostic);
102
+ }
103
+ return lines;
104
+ }
105
+ export function stopSummary(stop, addressed) {
89
106
  const detail = [];
90
107
  if ("refusal" in stop && stop.refusal !== undefined) {
91
108
  detail.push({ text: stop.refusal.kind });
@@ -105,20 +122,7 @@ export function stopLines(label, stop, columns, addressed) {
105
122
  detail.push(...targetMovedDetail(stop));
106
123
  }
107
124
  }
108
- const lines = [];
109
- const state = label === "verification" ? "failed" : "blocked";
110
- receiptRow(lines, "!", label, [{ text: state }, { text: "·" }, ...detail], columns);
111
- lines.push(...prerequisiteRows(stop, columns));
112
- if ("failure" in stop && stop.failure === "environment-failure" && "command" in stop) {
113
- appendHookPayload(lines, stop.detail);
114
- }
115
- if ("retry" in stop && stop.retry?.kind === "publication-failed") {
116
- receiptPayload(lines, "diagnostic", stop.retry.diagnostic);
117
- }
118
- else if ("failure" in stop && "diagnostic" in stop) {
119
- receiptPayload(lines, "diagnostic", stop.diagnostic);
120
- }
121
- return lines;
125
+ return detail;
122
126
  }
123
127
  export function cleanupLines(cleanup, columns) {
124
128
  const lines = [];
@@ -52,6 +52,12 @@ export function renderRefusalFacts(refusal, indent, columns, addressed) {
52
52
  }
53
53
  if (refusal.kind === "dirty-workspace")
54
54
  return renderDirtyRefusal(refusal, indent, columns, identity);
55
+ if (refusal.kind === "unmerged-paths") {
56
+ return [
57
+ ...renderOpaqueBlock(refusalHead(refusal.kind, identity, []), indent, columns),
58
+ ...collectionLines("paths", refusal.paths, indent, columns),
59
+ ];
60
+ }
55
61
  if (refusal.kind === "integration-failed") {
56
62
  const lines = [
57
63
  ...renderOpaqueBlock(refusalHead(refusal.kind, identity, [`reason=${refusal.reason}`, `targetHead=${refusal.targetHead}`]), indent, columns),