@astrosheep/keiyaku 2.9.7 → 2.9.9

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 (57) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/harness/projection.js +8 -6
  5. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  6. package/build/agents/providers/codex-app-server/session.js +8 -7
  7. package/build/agents/selector.js +12 -1
  8. package/build/cli/commands/akuma/view/handler.js +3 -11
  9. package/build/cli/commands/contract/amend/handler.js +1 -1
  10. package/build/cli/commands/contract/amend/meta.js +4 -4
  11. package/build/cli/commands/projection/status/handler.js +5 -4
  12. package/build/cli/commands/projection/status/meta.js +2 -2
  13. package/build/cli/commands/task/add/meta.js +9 -1
  14. package/build/cli/commands/task/shared.js +2 -1
  15. package/build/cli/completion.js +8 -0
  16. package/build/cli/render/kanshi.js +238 -80
  17. package/build/cli/render/path-prefix-compaction.js +119 -76
  18. package/build/cli/render/projection-activity.js +65 -21
  19. package/build/cli/render/shared.js +8 -7
  20. package/build/cli/render/status.js +11 -23
  21. package/build/cli/render/wait.js +42 -64
  22. package/build/config/env-keys.js +1 -1
  23. package/build/config/env.js +1 -1
  24. package/build/config/settings/disease.js +4 -4
  25. package/build/config/settings/loader.js +44 -21
  26. package/build/core/addressing.js +40 -9
  27. package/build/core/amend.js +21 -5
  28. package/build/core/call/context.js +19 -3
  29. package/build/core/call/execution.js +43 -20
  30. package/build/core/ledger-batch.js +194 -0
  31. package/build/core/projection/generation/database.js +22 -0
  32. package/build/core/projection/generation/projection-generation-execution.js +52 -38
  33. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  34. package/build/core/projection/generation/projection-generation-process.js +3 -1
  35. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  36. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  37. package/build/core/projection/generation/store.js +17 -1
  38. package/build/core/projection/generation/transitions.js +89 -12
  39. package/build/core/projection/index.js +3 -3
  40. package/build/core/projection/projection-activity.js +2 -0
  41. package/build/core/projection/projection-kill.js +22 -10
  42. package/build/core/projection/projection-runner-lock.js +177 -37
  43. package/build/core/projection/projection-status.js +183 -60
  44. package/build/core/projection/projection-wake.js +171 -72
  45. package/build/core/status/board.js +55 -14
  46. package/build/core/status/drift.js +21 -5
  47. package/build/core/status/ledger-batch.js +1 -158
  48. package/build/core/task/settlement-git.js +2 -2
  49. package/build/core/task/task-git-runtime.js +8 -10
  50. package/build/core/task/task-git-store.js +5 -3
  51. package/build/core/worktree-path.js +39 -25
  52. package/build/flow-error.js +1 -1
  53. package/build/generated/version.js +2 -2
  54. package/build/git/refs.js +47 -1
  55. package/package.json +1 -1
  56. package/skills/keiyaku-akuma/SKILL.md +18 -0
  57. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -1,88 +1,131 @@
1
- const MIN_COMPACTION_SAVINGS = 4;
2
- const SAFE_PATTERN_COMPONENT = /^[A-Za-z0-9._*?\[\]-]+$/;
3
- const SAFE_CONCRETE_PATH_COMPONENT = /^[A-Za-z0-9._-]+$/;
4
- /** Compact one displayed pattern-pair without merging distinct overlap facts. */
5
- export function compactPatternPair(left, right) {
6
- const literal = `${left} × ${right}`;
7
- const leftParts = safePatternComponents(left);
8
- const rightParts = safePatternComponents(right);
9
- if (!leftParts || !rightParts)
10
- return literal;
11
- let sharedCount = 0;
12
- const maximumShared = Math.min(leftParts.length, rightParts.length) - 1;
13
- while (sharedCount < maximumShared && leftParts[sharedCount] === rightParts[sharedCount]) {
14
- sharedCount += 1;
15
- }
16
- if (sharedCount === 0)
17
- return literal;
18
- const prefix = leftParts.slice(0, sharedCount).join("/");
19
- const compacted = `${prefix}/{${leftParts.slice(sharedCount).join("/")} × ${rightParts.slice(sharedCount).join("/")}}`;
20
- return savesEnough(literal, compacted) ? compacted : literal;
1
+ const PREFIX_MARKER = "⋯";
2
+ const MIN_FOLDED_PREFIX_LENGTH = 8;
3
+ export const PATH_PREVIEW_BODY_ROWS = 7;
4
+ /** Render one exact pattern/path without giving display punctuation matcher meaning. */
5
+ export function renderExactPath(value) {
6
+ if (!requiresQuoting(value))
7
+ return value;
8
+ return `"${[...value].map(escapeQuotedCharacter).join("")}"`;
21
9
  }
22
- /** Compact a concrete preview through a stable slash-component trie when unambiguous. */
23
- export function compactPathPreview(paths) {
24
- const literal = paths.join(", ");
25
- if (paths.length < 2)
26
- return literal;
27
- const components = paths.map(safeConcretePathComponents);
28
- if (components.some((parts) => !parts))
29
- return literal;
30
- const trie = createTrie();
31
- for (const parts of components)
32
- insertPath(trie, parts);
33
- if (hasTerminalBranch(trie))
34
- return literal;
35
- if (!hasSharedDirectoryPrefix(trie))
36
- return literal;
37
- const compacted = renderTrie(trie);
38
- return savesEnough(literal, compacted) ? compacted : literal;
10
+ /** Fold one qualifying directory prefix while retaining row order and exact suffixes. */
11
+ export function foldPathRows(values) {
12
+ const rendered = values.map(renderExactPath);
13
+ const plainIndexes = values
14
+ .map((value, index) => renderExactPath(value) === value ? index : -1)
15
+ .filter((index) => index >= 0);
16
+ const prefix = bestSharedDirectoryPrefix(values, plainIndexes);
17
+ if (!prefix)
18
+ return { rows: rendered };
19
+ const foldedIndexes = new Set(plainIndexes.filter((index) => values[index].startsWith(prefix)));
20
+ return {
21
+ prefix,
22
+ rows: values.map((value, index) => foldedIndexes.has(index) ? `${PREFIX_MARKER}${value.slice(prefix.length)}` : rendered[index]),
23
+ };
39
24
  }
40
- function safePatternComponents(value) {
41
- return safeSlashComponents(value, SAFE_PATTERN_COMPONENT);
25
+ /** Render one overlap fact as two labeled exact pattern rows. */
26
+ export function renderPatternPair(leftLabel, leftPattern, rightLabel, rightPattern) {
27
+ const folded = foldPathRows([leftPattern, rightPattern]);
28
+ const labelWidth = Math.max(leftLabel.length, rightLabel.length) + 2;
29
+ return [
30
+ ...(folded.prefix ? [`${PREFIX_MARKER} = ${folded.prefix}`] : []),
31
+ `${leftLabel.padEnd(labelWidth)}${folded.rows[0]}`,
32
+ `${rightLabel.padEnd(labelWidth)}${folded.rows[1]}`,
33
+ ];
42
34
  }
43
- function safeConcretePathComponents(value) {
44
- return safeSlashComponents(value, SAFE_CONCRETE_PATH_COMPONENT);
35
+ /** Render a bounded path snapshot; declarations consume the same row budget as paths. */
36
+ export function renderPathPreview(snapshot, bodyRows = PATH_PREVIEW_BODY_ROWS) {
37
+ const plan = planPathPreview(snapshot.preview, Math.max(0, bodyRows));
38
+ const omitted = Math.max(0, snapshot.count - plan.visibleCount);
39
+ return [
40
+ `scope · ${snapshot.count} pattern${snapshot.count === 1 ? "" : "s"}`,
41
+ ...(plan.prefix ? [`${PREFIX_MARKER} = ${plan.prefix}`] : []),
42
+ ...plan.rows,
43
+ ...(omitted > 0 ? [`+${omitted} more patterns`] : []),
44
+ ];
45
45
  }
46
- function safeSlashComponents(value, safeComponent) {
47
- const parts = value.split("/");
48
- if (parts.length === 0 || parts.some((part) => part === "." || part === ".." || !safeComponent.test(part))) {
49
- return null;
46
+ function planPathPreview(preview, bodyRows) {
47
+ const fullValues = preview.slice(0, bodyRows);
48
+ if (fullValues.length < bodyRows) {
49
+ return { ...foldPathRows(fullValues), visibleCount: fullValues.length };
50
50
  }
51
- return parts;
52
- }
53
- function createTrie() {
54
- return { terminal: false, children: new Map() };
55
- }
56
- function insertPath(node, parts) {
57
- let current = node;
58
- for (const part of parts) {
59
- let child = current.children.get(part);
60
- if (!child) {
61
- child = createTrie();
62
- current.children.set(part, child);
63
- }
64
- current = child;
51
+ const chargedValues = preview.slice(0, Math.max(0, bodyRows - 1));
52
+ const chargedFold = foldPathRows(chargedValues);
53
+ if (chargedFold.prefix) {
54
+ return { ...chargedFold, visibleCount: chargedValues.length };
65
55
  }
66
- current.terminal = true;
56
+ return { rows: fullValues.map(renderExactPath), visibleCount: fullValues.length };
67
57
  }
68
- function hasTerminalBranch(node) {
69
- if (node.terminal && node.children.size > 0)
58
+ function requiresQuoting(value) {
59
+ if (value.length === 0 || /^\s|\s$/u.test(value))
70
60
  return true;
71
- return [...node.children.values()].some(hasTerminalBranch);
61
+ return value.includes("\\")
62
+ || value.includes('"')
63
+ || value.includes(PREFIX_MARKER)
64
+ || [...value].some(isControlCharacter);
72
65
  }
73
- function hasSharedDirectoryPrefix(node, isRoot = true) {
74
- if (!isRoot && node.children.size > 1)
75
- return true;
76
- return [...node.children.values()].some((child) => hasSharedDirectoryPrefix(child, false));
66
+ function isControlCharacter(character) {
67
+ const codePoint = character.codePointAt(0);
68
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
69
+ }
70
+ function escapeQuotedCharacter(character) {
71
+ if (character === "\\")
72
+ return "\\\\";
73
+ if (character === '"')
74
+ return '\\"';
75
+ const codePoint = character.codePointAt(0);
76
+ const named = new Map([
77
+ [0x07, "\\a"],
78
+ [0x08, "\\b"],
79
+ [0x09, "\\t"],
80
+ [0x0a, "\\n"],
81
+ [0x0b, "\\v"],
82
+ [0x0c, "\\f"],
83
+ [0x0d, "\\r"],
84
+ ]).get(codePoint);
85
+ if (named)
86
+ return named;
87
+ if (!isControlCharacter(character))
88
+ return character;
89
+ return [...Buffer.from(character, "utf8")]
90
+ .map((byte) => `\\${byte.toString(8).padStart(3, "0")}`)
91
+ .join("");
77
92
  }
78
- function renderTrie(node) {
79
- const entries = [...node.children.entries()].map(([part, child]) => {
80
- if (child.children.size === 0)
81
- return part;
82
- return `${part}/${renderTrie(child)}`;
83
- });
84
- return entries.length === 1 ? entries[0] : `{${entries.join(",")}}`;
93
+ function bestSharedDirectoryPrefix(values, plainIndexes) {
94
+ let best;
95
+ for (let left = 0; left < plainIndexes.length; left += 1) {
96
+ for (let right = left + 1; right < plainIndexes.length; right += 1) {
97
+ const prefix = sharedDirectoryPrefix(values[plainIndexes[left]], values[plainIndexes[right]]);
98
+ if (!prefix || prefix.length < MIN_FOLDED_PREFIX_LENGTH)
99
+ continue;
100
+ const members = plainIndexes.filter((index) => values[index].startsWith(prefix));
101
+ const common = members
102
+ .slice(1)
103
+ .reduce((current, index) => sharedDirectoryPrefix(current, values[index]) ?? "", values[members[0]]);
104
+ if (common.length < MIN_FOLDED_PREFIX_LENGTH)
105
+ continue;
106
+ const candidate = {
107
+ prefix: common,
108
+ count: members.length,
109
+ first: members[0],
110
+ score: common.length * (members.length - 1),
111
+ };
112
+ if (!best
113
+ || candidate.score > best.score
114
+ || (candidate.score === best.score && candidate.count > best.count)
115
+ || (candidate.score === best.score && candidate.count === best.count && candidate.first < best.first)
116
+ || (candidate.score === best.score && candidate.count === best.count && candidate.first === best.first
117
+ && candidate.prefix < best.prefix)) {
118
+ best = candidate;
119
+ }
120
+ }
121
+ }
122
+ return best?.prefix;
85
123
  }
86
- function savesEnough(literal, compacted) {
87
- return literal.length - compacted.length >= MIN_COMPACTION_SAVINGS;
124
+ function sharedDirectoryPrefix(left, right) {
125
+ let sharedLength = 0;
126
+ const maximum = Math.min(left.length, right.length);
127
+ while (sharedLength < maximum && left[sharedLength] === right[sharedLength])
128
+ sharedLength += 1;
129
+ const slash = left.lastIndexOf("/", sharedLength - 1);
130
+ return slash >= 0 ? left.slice(0, slash + 1) : undefined;
88
131
  }
@@ -1,10 +1,31 @@
1
1
  import { compactText } from "./compact-text.js";
2
- import { displayColumns, fitVariableLine, resolveLineColumns, truncateMiddleColumns } from "./line-width.js";
2
+ import { displayColumns, fitVariableLine, resolveLineColumns, truncateMiddleColumns, truncateColumns } from "./line-width.js";
3
3
  import { presentFoldedToolActivity, } from "./tool-presentation.js";
4
4
  import { presentLedgerPath, summarizeToolLedger } from "./tool-ledger-rollup.js";
5
- const TIME_GUTTER_WIDTH = 6;
5
+ const TIME_GUTTER_WIDTH = 5;
6
6
  const VERB_WIDTH = 7;
7
7
  const SILENCE_THRESHOLD_MS = 60_000;
8
+ const TIMELINE_GAP_MARKER = "⋮";
9
+ /** Keep the renderer and its gap grammar while giving Kanshi its smaller live window. */
10
+ export function selectProjectionActivityWindow(activity, ordinaryRows) {
11
+ // A Kanshi flow narrows only the settled history window. Unfinished activity
12
+ // remains pinned in its timeline position; it is not another history row.
13
+ const selected = activity.window.slice(-ordinaryRows);
14
+ const selectedOrders = new Set([
15
+ ...selected.map((row) => row.order),
16
+ ...activity.pinned.map((row) => row.order),
17
+ ]);
18
+ return {
19
+ activity: {
20
+ ...activity,
21
+ window: selected,
22
+ // Turn seals are a wait concern. Kanshi's narrow operational flow contains
23
+ // only ordinary activity plus durable tells.
24
+ timeline: activity.timeline.filter((row) => selectedOrders.has(row.order)),
25
+ },
26
+ leadingOmittedCount: activity.omittedSettledCount + activity.window.length - selected.length,
27
+ };
28
+ }
8
29
  export function formatTimelineDuration(ms) {
9
30
  const totalSeconds = Math.max(0, Math.floor(ms / 1_000));
10
31
  if (totalSeconds < 60)
@@ -39,6 +60,14 @@ export function formatWallClock(at) {
39
60
  return " : │";
40
61
  return `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}│`;
41
62
  }
63
+ /** One durable-tell line dialect shared by wait and the embedded Kanshi flow. */
64
+ export function renderPendingProjectionTell(tell, input) {
65
+ const createdAtMs = Date.parse(tell.createdAt);
66
+ const age = Math.max(0, input.anchorMs - (Number.isFinite(createdAtMs) ? createdAtMs : input.anchorMs));
67
+ const fixed = `${formatWallClock(tell.createdAt)} ${"told".padEnd(VERB_WIDTH)} `;
68
+ const tail = ` — ${formatTimelineDuration(age)}…`;
69
+ return `${fixed}${truncateColumns(`“${tell.text}”`, Math.max(0, input.maxColumns - displayColumns(fixed) - displayColumns(tail)))}${input.renderUnconsumedTail?.(tail) ?? tail}`;
70
+ }
42
71
  function commonPrefix(left, right) {
43
72
  const a = [...left];
44
73
  const b = [...right];
@@ -72,6 +101,9 @@ function compressedPrefix(prefix) {
72
101
  function timelineGutter(label, spine) {
73
102
  return label || `${"".padStart(TIME_GUTTER_WIDTH)}${spine}`;
74
103
  }
104
+ function renderTimelineGap(omittedRowCount, maxColumns) {
105
+ return fitVariableLine(`${timelineGutter("", TIMELINE_GAP_MARKER)} `, `${omittedRowCount} more`, maxColumns);
106
+ }
75
107
  function compactTokens(value) {
76
108
  if (value < 1_000)
77
109
  return String(value);
@@ -144,13 +176,15 @@ function renderTerminalRollup(activity, input) {
144
176
  ? fitVariableLine(`${endPrefix} `, segments.join(" · "), input.maxColumns)
145
177
  : endPrefix,
146
178
  });
147
- for (const file of summary.files) {
148
- const repeat = file.count > 1 ? ` ×${file.count}` : "";
149
- const diffstat = file.diffstat ? ` +${file.diffstat.additions} −${file.diffstat.deletions}` : "";
150
- rows.push({ kind: "file", line: fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns) });
151
- }
152
- if (summary.hiddenFileCount > 0) {
153
- rows.push({ kind: "file", line: fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns) });
179
+ if (input.includeFiles !== false) {
180
+ for (const file of summary.files) {
181
+ const repeat = file.count > 1 ? ` ×${file.count}` : "";
182
+ const diffstat = file.diffstat ? ` +${file.diffstat.additions} −${file.diffstat.deletions}` : "";
183
+ rows.push({ kind: "file", line: fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns) });
184
+ }
185
+ if (summary.hiddenFileCount > 0) {
186
+ rows.push({ kind: "file", line: fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns) });
187
+ }
154
188
  }
155
189
  return { rows, hasMutationFiles: summary.files.length > 0 };
156
190
  }
@@ -170,6 +204,7 @@ function timelineEntries(activity) {
170
204
  export function renderProjectionActivity(activity, input) {
171
205
  const lines = [];
172
206
  const rowKinds = [];
207
+ const rowAts = [];
173
208
  let priorAtMs;
174
209
  let priorMinute;
175
210
  let priorCompletedRan;
@@ -177,9 +212,12 @@ export function renderProjectionActivity(activity, input) {
177
212
  let lastRunningTool;
178
213
  const maxColumns = input.maxColumns ?? resolveLineColumns();
179
214
  const entries = timelineEntries(activity);
180
- const pushRow = (line, kind) => {
215
+ const gapBeforeOrder = input.leadingOmittedCount ? activity.window[0]?.order : undefined;
216
+ let gapEmitted = false;
217
+ const pushRow = (line, kind, at) => {
181
218
  lines.push(line);
182
219
  rowKinds.push(kind);
220
+ rowAts.push(at);
183
221
  return true;
184
222
  };
185
223
  for (let index = 0; index < entries.length; index += 1) {
@@ -190,8 +228,12 @@ export function renderProjectionActivity(activity, input) {
190
228
  priorAtMs = atMs;
191
229
  continue;
192
230
  }
193
- if (priorAtMs !== undefined && Number.isFinite(atMs) && atMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
194
- pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(atMs - priorAtMs)} pass in silence`, maxColumns), "silence");
231
+ if (!gapEmitted && input.leadingOmittedCount && gapBeforeOrder !== undefined && entry.row.order >= gapBeforeOrder) {
232
+ pushRow(renderTimelineGap(input.leadingOmittedCount, maxColumns), "activity", entry.at);
233
+ gapEmitted = true;
234
+ }
235
+ if (input.showSilence !== false && priorAtMs !== undefined && Number.isFinite(atMs) && atMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
236
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(atMs - priorAtMs)} pass in silence`, maxColumns), "silence", entry.at);
195
237
  }
196
238
  if (Number.isFinite(atMs))
197
239
  priorAtMs = atMs;
@@ -226,22 +268,22 @@ export function renderProjectionActivity(activity, input) {
226
268
  }
227
269
  priorCompletedRan = row.state === "completed" ? presentation.variableText : undefined;
228
270
  const budget = Math.max(0, maxColumns - displayColumns(fixed) - displayColumns(presentation.fixedSuffix));
229
- pushRow(`${fixed}${truncateMiddleColumns(variable, budget)}${presentation.fixedSuffix}`, "activity");
271
+ pushRow(`${fixed}${truncateMiddleColumns(variable, budget)}${presentation.fixedSuffix}`, "activity", entry.at);
230
272
  continue;
231
273
  }
232
274
  if (row.kind === "said") {
233
275
  priorCompletedRan = undefined;
234
- pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns), "activity");
276
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns), "activity", entry.at);
235
277
  }
236
278
  else if (row.kind === "thought") {
237
279
  priorCompletedRan = undefined;
238
280
  const text = `${compactText(row.text)}${row.truncated && !row.text.endsWith("…") ? "…" : ""}`;
239
281
  const duration = formatActionDuration(row.durationMs);
240
- pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns), "activity");
282
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns), "activity", entry.at);
241
283
  }
242
284
  else if (row.kind === "warning") {
243
285
  priorCompletedRan = undefined;
244
- pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns), "activity");
286
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns), "activity", entry.at);
245
287
  }
246
288
  else {
247
289
  priorCompletedRan = undefined;
@@ -249,7 +291,7 @@ export function renderProjectionActivity(activity, input) {
249
291
  ? ` · ${compactTokens(row.inputTokens)}▸${compactTokens(row.outputTokens)} tok`
250
292
  : "";
251
293
  const cost = row.costUsd !== undefined ? ` · $${row.costUsd.toFixed(2)}` : "";
252
- pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn}${usage}${cost}`, maxColumns), "activity");
294
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn}${usage}${cost}`, maxColumns), "activity", entry.at);
253
295
  }
254
296
  }
255
297
  for (const row of activity.pinned) {
@@ -258,10 +300,10 @@ export function renderProjectionActivity(activity, input) {
258
300
  }
259
301
  if (input.showPlan && activity.plan) {
260
302
  const planAtMs = Date.parse(activity.plan.at);
261
- if (priorAtMs !== undefined && Number.isFinite(planAtMs) && planAtMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
262
- pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(planAtMs - priorAtMs)} pass in silence`, maxColumns), "silence");
303
+ if (input.showSilence !== false && priorAtMs !== undefined && Number.isFinite(planAtMs) && planAtMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
304
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(planAtMs - priorAtMs)} pass in silence`, maxColumns), "silence", activity.plan.at);
263
305
  }
264
- pushRow(renderPlan(activity.plan, maxColumns), "plan");
306
+ pushRow(renderPlan(activity.plan, maxColumns), "plan", activity.plan.at);
265
307
  }
266
308
  let hasMutationFiles = false;
267
309
  if (["returned", "dead", "lost", "dismissed"].includes(input.state)) {
@@ -269,10 +311,11 @@ export function renderProjectionActivity(activity, input) {
269
311
  anchorMs: input.anchorMs,
270
312
  ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
271
313
  ...(input.terminalDiagnostic ? { diagnostic: input.terminalDiagnostic } : {}),
314
+ includeFiles: input.showTerminalFiles !== false,
272
315
  maxColumns,
273
316
  });
274
317
  for (const row of rollup.rows) {
275
- pushRow(row.line, row.kind);
318
+ pushRow(row.line, row.kind, new Date(input.anchorMs).toISOString());
276
319
  }
277
320
  hasEndAnchor = true;
278
321
  hasMutationFiles = rollup.hasMutationFiles;
@@ -280,6 +323,7 @@ export function renderProjectionActivity(activity, input) {
280
323
  return {
281
324
  lines,
282
325
  rowKinds,
326
+ rowAts,
283
327
  ...(lastRunningTool ? { lastRunningTool } : {}),
284
328
  hasEndAnchor,
285
329
  hasMutationFiles,
@@ -3,7 +3,7 @@ import { commissionSlug, formatCommissionCoordinate } from "../../core/ids.js";
3
3
  import { assembleResponse, buildSection, DISPLAY_TEXT_MAX_CHARS, FORMAT_LIST_MAX_ITEM_CHARS, FORMAT_LIST_MAX_ITEMS, formatMaybe, formatWarnings, shellQuote, truncateForDisplay, } from "./format.js";
4
4
  import { WARNING_SECTION_TITLE } from "./response-style.js";
5
5
  import { prependAddressReceipt } from "./address.js";
6
- import { compactPathPreview, compactPatternPair } from "./path-prefix-compaction.js";
6
+ import { renderPathPreview, renderPatternPair } from "./path-prefix-compaction.js";
7
7
  export function textResponse(text) {
8
8
  const payload = {};
9
9
  return {
@@ -76,16 +76,17 @@ function renderScopeOverlapWarnings(overlaps) {
76
76
  return [...overlapsByCounterparty.entries()].map(([contractId, overlaps]) => renderScopeOverlapGroup(commissionSlug(contractId), overlaps));
77
77
  }
78
78
  function renderScopeOverlapGroup(contractAddr, overlaps) {
79
- const pairs = overlaps.map((overlap) => {
79
+ const nextAddr = commissionSlug(overlaps[0].contracts[0]);
80
+ const pairs = overlaps.flatMap((overlap) => {
80
81
  const [nextPattern, existingPattern] = overlap.patterns;
81
- const patternPair = compactPatternPair(nextPattern, existingPattern);
82
+ const patternPair = renderPatternPair(nextAddr, nextPattern, contractAddr, existingPattern);
82
83
  const evidence = overlap.kind === "potential"
83
- ? "potential; no current tracked path proves it"
84
- : `concrete; ${overlap.paths.count} current tracked path${overlap.paths.count === 1 ? "" : "s"}: ${compactPathPreview(overlap.paths.preview)}${overlap.paths.truncated ? ", …" : ""}`;
85
- return ` ${patternPair} (${evidence})`;
84
+ ? ["potential · no current tracked path proves it"]
85
+ : renderPathPreview(overlap.paths);
86
+ return [...patternPair, ...evidence].map((line) => ` ${line}`);
86
87
  });
87
88
  return [
88
- `▲ scope overlap with ${contractAddr}:`,
89
+ `▲ overlap ${nextAddr} ↔ ${contractAddr}`,
89
90
  ...pairs,
90
91
  ` consider --after ${contractAddr}`,
91
92
  ].join("\n");
@@ -69,12 +69,12 @@ function terminalFailureSuffix(row) {
69
69
  function renderProjectionOmission(board) {
70
70
  if (!board.omitted)
71
71
  return undefined;
72
- const counts = ["failed", "dead", "done", "killed", "dismissed", "incompatible"]
72
+ const counts = ["failed", "dead", "done", "killed", "dismissed"]
73
73
  .flatMap((state) => {
74
74
  const count = board.omitted?.byState[state] ?? 0;
75
75
  return count > 0 ? [`${count} ${state}`] : [];
76
76
  });
77
- return ` · +${board.omitted.total} historical projections not shown (${counts.join(" · ")}) — keiyaku status --all`;
77
+ return counts.length > 0 ? `+ ${counts.join(" · ")}` : undefined;
78
78
  }
79
79
  function renderProjectionAge(row) {
80
80
  const duration = formatAgeMs(row.durationMs) ?? "?";
@@ -170,15 +170,18 @@ export function renderProjectionFilteredStatus(board, projectionId) {
170
170
  if (!row)
171
171
  return `${heading}\nno projections on projection ${projectionId}\n`;
172
172
  if (row.state === "failed" || row.terminalFailure) {
173
- // Same face bytes as wait; CLI success write supplies the final newline.
174
- return renderTerminalFailureFace(projectionId, row.terminalFailure).join("\n");
173
+ const lines = [...renderTerminalFailureFace(projectionId, row.terminalFailure), "", `» keiyaku wait ${projectionId}`];
174
+ return lines.join("\n");
175
175
  }
176
176
  const lines = [heading, "", ...renderProjectionRow(row), ""];
177
177
  if (row.observation.compatibility === "incompatible") {
178
178
  return `${lines.join("\n")}\n`;
179
179
  }
180
- if (!isTerminalProjectionStatus(row))
181
- lines.push(`» ${formatTellCommand(projectionId, '"..."')}`);
180
+ if (isTerminalProjectionStatus(row)) {
181
+ lines.push(`» keiyaku wait ${projectionId}`);
182
+ return lines.join("\n");
183
+ }
184
+ lines.push(`» ${formatTellCommand(projectionId, '"..."')}`);
182
185
  lines.push(`» keiyaku wait ${projectionId}`);
183
186
  return `${lines.join("\n")}\n`;
184
187
  }
@@ -206,27 +209,12 @@ function renderClaimReconciliationSection(recon) {
206
209
  */
207
210
  export function renderStatusBoard(board) {
208
211
  const boardText = renderKanshiBoard(board);
209
- const projectionText = renderProjectionStatusSection(board.projections);
210
- const receiptsText = renderClaimReconciliationSection(board.claimReconciliation);
211
- const taskText = renderTaskStatusSection(board.tasks);
212
212
  const taskDiseaseText = [
213
213
  ...(board.tasks?.diseases ?? []).map((disease) => disease.message),
214
214
  ...(board.tasks?.diagnostic ? [board.tasks.diagnostic] : []),
215
215
  ];
216
216
  const diseasesText = renderSettingsDiseasesSection(board.diseases, taskDiseaseText);
217
- if (!projectionText && !receiptsText && !taskText && diseasesText === "")
217
+ if (diseasesText === "")
218
218
  return boardText;
219
- const sections = [boardText.trimEnd()];
220
- if (board.targetFilter !== null && (projectionText || receiptsText || diseasesText)) {
221
- sections.push("repository-wide — not filtered by target");
222
- }
223
- if (projectionText)
224
- sections.push(projectionText.trimEnd());
225
- if (receiptsText)
226
- sections.push(receiptsText.trimEnd());
227
- if (taskText)
228
- sections.push(taskText.trimEnd());
229
- if (diseasesText)
230
- sections.push(diseasesText.trimEnd());
231
- return `${sections.join("\n\n")}\n`;
219
+ return `${[boardText.trimEnd(), diseasesText.trimEnd()].join("\n\n")}\n`;
232
220
  }
@@ -1,29 +1,8 @@
1
- import { formatTimelineDuration, renderProjectionActivity, renderProjectionActivitySummary, } from "./projection-activity.js";
1
+ import { formatTimelineDuration, renderPendingProjectionTell, renderProjectionActivity, } from "./projection-activity.js";
2
2
  import { displayColumns, resolveLineColumns, truncateColumns } from "./line-width.js";
3
3
  import { formatTellCommand, projectionRejoinHintLines, textResponse } from "./shared.js";
4
4
  import { renderTerminalFailureFace } from "./terminal-failure.js";
5
5
  const WAIT_HEADER_MAX_COLUMNS = 60;
6
- const WAIT_BODY_MAX_ROWS = 10;
7
- const WAIT_ROW_PRIORITY = {
8
- "end-anchor": 100,
9
- "storage-diagnostic": 90,
10
- diagnostic: 90,
11
- tell: 80,
12
- summary: 70,
13
- activity: 60,
14
- plan: 55,
15
- liveness: 50,
16
- silence: 65,
17
- file: 20,
18
- };
19
- /** One owner admits every wait-body row, then restores presentation order. */
20
- export function selectWaitBodyRows(rows, limit = WAIT_BODY_MAX_ROWS) {
21
- const selected = rows
22
- .slice()
23
- .sort((left, right) => WAIT_ROW_PRIORITY[right.kind] - WAIT_ROW_PRIORITY[left.kind] || left.order - right.order)
24
- .slice(0, limit);
25
- return selected.sort((left, right) => left.order - right.order);
26
- }
27
6
  function shortAddress(projectionId) {
28
7
  return /\/([0-9a-f]{8})$/.exec(projectionId)?.[1] ?? projectionId;
29
8
  }
@@ -70,12 +49,23 @@ function waitHeader(projectionId, akuma, snapshot, outputColumns) {
70
49
  const dashCount = Math.max(1, maxColumns - displayColumns(`${prefix} ${state}`));
71
50
  return `${prefix} ${"─".repeat(dashCount)} ${state}`;
72
51
  }
73
- function pendingTellLines(snapshot, maxColumns) {
74
- return snapshot.tells.window.map((tell) => {
75
- const age = Math.max(0, snapshot.observedAtMs - Date.parse(tell.createdAt));
76
- const fixed = `${"".padStart(6)}│ ${"tell".padEnd(7)} `;
77
- const suffix = ` · undelivered ${formatTimelineDuration(Number.isFinite(age) ? age : 0)}`;
78
- return `${fixed}${truncateColumns(`“${tell.text}”`, Math.max(0, maxColumns - displayColumns(fixed) - displayColumns(suffix)))}${suffix}`;
52
+ function pendingTellRows(snapshot, maxColumns, orderOffset) {
53
+ return snapshot.tells.window.map((tell, index) => ({
54
+ line: renderPendingProjectionTell(tell, {
55
+ anchorMs: snapshot.observedAtMs,
56
+ maxColumns,
57
+ }),
58
+ at: tell.createdAt,
59
+ order: orderOffset + index,
60
+ }));
61
+ }
62
+ function orderFlowRows(rows) {
63
+ return rows.slice().sort((left, right) => {
64
+ const leftAt = Date.parse(left.at ?? "");
65
+ const rightAt = Date.parse(right.at ?? "");
66
+ const safeLeft = Number.isFinite(leftAt) ? leftAt : Number.POSITIVE_INFINITY;
67
+ const safeRight = Number.isFinite(rightAt) ? rightAt : Number.POSITIVE_INFINITY;
68
+ return safeLeft - safeRight || left.order - right.order;
79
69
  });
80
70
  }
81
71
  function ledgerHint(projectionId, snapshot) {
@@ -142,14 +132,13 @@ function unknownResponse(projectionId, akuma, snapshot, maxColumns) {
142
132
  anchorMs: snapshot.observedAtMs,
143
133
  state: "unknown",
144
134
  showPlan: false,
135
+ showSilence: false,
136
+ showTerminalFiles: false,
145
137
  ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
146
138
  maxColumns,
139
+ leadingOmittedCount: snapshot.activity.omittedSettledCount,
147
140
  });
148
- const historicalLines = selectWaitBodyRows(rendered.lines.map((line, index) => ({
149
- kind: rendered.rowKinds[index] ?? "activity",
150
- line,
151
- order: index,
152
- }))).map((row) => row.line.replace(/[┆├]/, "│"));
141
+ const historicalLines = rendered.lines.map((line) => line.replace(/[┆├]/, "│"));
153
142
  return {
154
143
  ...textResponse([
155
144
  `? ${akuma} · projection: ${projectionId} · unknown`,
@@ -216,43 +205,32 @@ export function buildWaitResponse(projectionId, result, options = {}) {
216
205
  if (state === "returned") {
217
206
  return returnedResponse(projectionId, result);
218
207
  }
219
- const tells = pendingTellLines(snapshot, maxColumns);
220
- const summary = ["running", "stalled", "unknown"].includes(state)
221
- ? renderProjectionActivitySummary(snapshot.activity, {
222
- state,
223
- ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
224
- maxColumns,
225
- })
226
- : undefined;
227
208
  const rendered = renderProjectionActivity(snapshot.activity, {
228
209
  anchorMs: snapshot.observedAtMs,
229
210
  state,
230
- showPlan: state === "running",
211
+ showPlan: false,
212
+ showSilence: false,
213
+ showTerminalFiles: false,
231
214
  ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
232
215
  maxColumns,
216
+ leadingOmittedCount: snapshot.activity.omittedSettledCount,
233
217
  });
234
- let order = 0;
235
- const bodyPlan = [];
236
- for (const diagnostic of snapshot.eventDiagnostics ?? []) {
237
- bodyPlan.push({ kind: "storage-diagnostic", line: `‼ ${diagnostic.kind} · ${diagnostic.detail}`, order: order++ });
238
- }
239
- rendered.lines.forEach((line, index) => {
240
- bodyPlan.push({ kind: rendered.rowKinds[index] ?? "activity", line, order: order++ });
241
- });
242
- for (const line of tells)
243
- bodyPlan.push({ kind: "tell", line, order: order++ });
244
- if (rendered.lines.length === 0 && tells.length === 0 && state === "running") {
245
- bodyPlan.push({ kind: "liveness", line: " not a word out of it yet — idle", order: order++ });
246
- }
247
- if (!rendered.hasEndAnchor && summary) {
248
- bodyPlan.push({ kind: "summary", line: summary, order: order++ });
249
- }
250
- const bodyLines = selectWaitBodyRows(bodyPlan).map((row) => truncateColumns(row.line, maxColumns));
251
- const lines = [waitHeader(projectionId, result.pact.akuma, snapshot, maxColumns), ...bodyLines];
252
- const pendingTellCount = snapshot.tells.totals.undelivered;
253
- if (pendingTellCount > 0) {
254
- lines.push(`${pendingTellCount} tell${pendingTellCount === 1 ? "" : "s"} undelivered`);
255
- }
218
+ const diagnosticLines = (snapshot.eventDiagnostics ?? [])
219
+ .map((diagnostic) => `‼ ${diagnostic.kind} · ${diagnostic.detail}`);
220
+ const activityRows = rendered.lines.map((line, index) => ({
221
+ line,
222
+ at: rendered.rowAts[index],
223
+ order: index,
224
+ }));
225
+ const flowLines = orderFlowRows([
226
+ ...activityRows,
227
+ ...pendingTellRows(snapshot, maxColumns, activityRows.length),
228
+ ]).map((row) => truncateColumns(row.line, maxColumns));
229
+ const lines = [
230
+ waitHeader(projectionId, result.pact.akuma, snapshot, maxColumns),
231
+ ...diagnosticLines,
232
+ ...flowLines,
233
+ ];
256
234
  const nextActions = state === "running" && options.runningActions === false
257
235
  ? []
258
236
  : nextActionLines(options.alias ?? projectionId, result, state);