@astrosheep/keiyaku 2.9.6 → 2.9.8

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 (87) 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/outcome.js +10 -0
  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/contract/bind/handler.js +10 -4
  12. package/build/cli/commands/contract/bind/meta.js +6 -6
  13. package/build/cli/commands/contract/petition/handler.js +16 -3
  14. package/build/cli/commands/contract/petition/meta.js +4 -4
  15. package/build/cli/commands/metadata.js +3 -2
  16. package/build/cli/commands/projection/status/handler.js +5 -4
  17. package/build/cli/commands/projection/status/meta.js +2 -2
  18. package/build/cli/commands/projection/tell/handler.js +10 -2
  19. package/build/cli/commands/projection/tell/meta.js +3 -3
  20. package/build/cli/commands/task/add/meta.js +9 -1
  21. package/build/cli/commands/task/catalog.js +2 -0
  22. package/build/cli/commands/task/log/handler.js +12 -0
  23. package/build/cli/commands/task/log/meta.js +8 -0
  24. package/build/cli/commands/task/shared.js +2 -1
  25. package/build/cli/completion.js +8 -0
  26. package/build/cli/flags.js +8 -0
  27. package/build/cli/index.js +10 -6
  28. package/build/cli/parse-flags.js +6 -0
  29. package/build/cli/parse-metadata.js +1 -1
  30. package/build/cli/render/kanshi.js +2 -2
  31. package/build/cli/render/line-width.js +33 -0
  32. package/build/cli/render/path-prefix-compaction.js +131 -0
  33. package/build/cli/render/petition.js +4 -0
  34. package/build/cli/render/projection-activity.js +103 -21
  35. package/build/cli/render/shared.js +38 -15
  36. package/build/cli/render/status.js +8 -5
  37. package/build/cli/render/success-response.js +2 -0
  38. package/build/cli/render/tool-presentation.js +3 -3
  39. package/build/cli/render/wait.js +68 -48
  40. package/build/cli/subagent-guard.js +3 -0
  41. package/build/cli/types.js +1 -1
  42. package/build/config/settings/disease.js +4 -4
  43. package/build/config/settings/loader.js +44 -21
  44. package/build/core/addressing.js +40 -9
  45. package/build/core/amend.js +21 -5
  46. package/build/core/bind.js +111 -6
  47. package/build/core/call/context.js +19 -3
  48. package/build/core/call/execution.js +43 -20
  49. package/build/core/draft.js +1 -1
  50. package/build/core/ledger-batch.js +194 -0
  51. package/build/core/projection/generation/database.js +43 -2
  52. package/build/core/projection/generation/model.js +24 -3
  53. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  54. package/build/core/projection/generation/projection-generation-execution.js +46 -37
  55. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  56. package/build/core/projection/generation/projection-generation-process.js +3 -1
  57. package/build/core/projection/generation/projection-generation-runner.js +145 -66
  58. package/build/core/projection/generation/projection-generation-runtime.js +93 -11
  59. package/build/core/projection/generation/store.js +26 -1
  60. package/build/core/projection/generation/transitions.js +193 -14
  61. package/build/core/projection/index.js +5 -5
  62. package/build/core/projection/projection-core.js +1 -1
  63. package/build/core/projection/projection-kill.js +53 -10
  64. package/build/core/projection/projection-life-protocol.js +10 -0
  65. package/build/core/projection/projection-runner-lock.js +177 -37
  66. package/build/core/projection/projection-status.js +143 -55
  67. package/build/core/projection/projection-wait.js +53 -15
  68. package/build/core/projection/projection-wake.js +177 -46
  69. package/build/core/projection/tell/database.js +18 -0
  70. package/build/core/projection/tell/model.js +1 -0
  71. package/build/core/projection/tell/store.js +102 -55
  72. package/build/core/registry.js +82 -69
  73. package/build/core/scope.js +9 -9
  74. package/build/core/status/board.js +42 -4
  75. package/build/core/status/drift.js +21 -5
  76. package/build/core/status/ledger-batch.js +1 -158
  77. package/build/core/task/index.js +2 -2
  78. package/build/core/task/task-contract.js +18 -0
  79. package/build/core/task/task-git-runtime.js +8 -10
  80. package/build/core/task/task-git-store.js +55 -3
  81. package/build/core/worktree-path.js +39 -25
  82. package/build/flow-error.js +1 -1
  83. package/build/generated/version.js +2 -2
  84. package/build/git/refs.js +47 -1
  85. package/package.json +1 -1
  86. package/skills/keiyaku-akuma/SKILL.md +28 -0
  87. package/skills/keiyaku-workflow/SKILL.md +142 -18
@@ -45,8 +45,8 @@ export function renderKanshiBoard(status) {
45
45
  const name = contract.place ? `${contract.slug} @${contract.place}` : contract.slug;
46
46
  const spark = arcSparkline(contract.arcs);
47
47
  const facts = [];
48
- const lifecycle = contract.state.state;
49
- if (lifecycle === "bound" || lifecycle === "active" || lifecycle === "petitioned") {
48
+ const lifecycle = contract.state.state === "active" ? "open" : contract.state.state;
49
+ if (lifecycle === "bound" || lifecycle === "open" || lifecycle === "petitioned") {
50
50
  facts.push(lifecycle);
51
51
  }
52
52
  const arcMark = contract.currentArc.state === "open"
@@ -54,6 +54,39 @@ export function truncateColumns(value, maxColumns) {
54
54
  }
55
55
  return `${output}…`;
56
56
  }
57
+ /** Truncate descriptive command text in the middle while retaining both ends. */
58
+ export function truncateMiddleColumns(value, maxColumns) {
59
+ if (maxColumns <= 0)
60
+ return "";
61
+ if (displayColumns(value) <= maxColumns)
62
+ return value;
63
+ if (maxColumns === 1)
64
+ return "…";
65
+ const budget = maxColumns - 1;
66
+ const leftBudget = Math.ceil(budget / 2);
67
+ const rightBudget = Math.floor(budget / 2);
68
+ let left = "";
69
+ let width = 0;
70
+ for (const character of value) {
71
+ const next = characterColumns(character);
72
+ if (width + next > leftBudget)
73
+ break;
74
+ left += character;
75
+ width += next;
76
+ }
77
+ const chars = [...value];
78
+ let right = "";
79
+ width = 0;
80
+ for (let index = chars.length - 1; index >= 0; index -= 1) {
81
+ const character = chars[index];
82
+ const next = characterColumns(character);
83
+ if (width + next > rightBudget)
84
+ break;
85
+ right = character + right;
86
+ width += next;
87
+ }
88
+ return `${left}…${right}`;
89
+ }
57
90
  export function resolveLineColumns(stream = process.stdout) {
58
91
  return stream.isTTY === true && Number.isInteger(stream.columns) && stream.columns > 0
59
92
  ? stream.columns
@@ -0,0 +1,131 @@
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("")}"`;
9
+ }
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
+ };
24
+ }
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
+ ];
34
+ }
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
+ }
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
+ }
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 };
55
+ }
56
+ return { rows: fullValues.map(renderExactPath), visibleCount: fullValues.length };
57
+ }
58
+ function requiresQuoting(value) {
59
+ if (value.length === 0 || /^\s|\s$/u.test(value))
60
+ return true;
61
+ return value.includes("\\")
62
+ || value.includes('"')
63
+ || value.includes(PREFIX_MARKER)
64
+ || [...value].some(isControlCharacter);
65
+ }
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("");
92
+ }
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;
123
+ }
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;
131
+ }
@@ -4,6 +4,10 @@ import { assembleResponse, buildPayload, buildSection, formatMaybe, formatWarnin
4
4
  import { DIFF_OVERVIEW_SECTION_TITLE, RESPONSE_MARKERS, WARNING_SECTION_TITLE } from "./response-style.js";
5
5
  import { INFO_COMMIT_MAX_CHARS, INFO_PLEA_MAX_CHARS, } from "./shared.js";
6
6
  import { prependAddressReceipt } from "./address.js";
7
+ /** Public consequence for a completed renew phase of petition --renew. */
8
+ export function renderPetitionRenewedConsequence(input) {
9
+ return `renewed onto ${input.newBase.slice(0, 8)} · ${input.sealed?.receipts.length ?? 0} receipts replayed`;
10
+ }
7
11
  function formatPetitionSealSource(source) {
8
12
  return source.arc === null
9
13
  ? `no open arc · fence ${source.fence}`
@@ -1,10 +1,11 @@
1
1
  import { compactText } from "./compact-text.js";
2
- import { fitAffixedLine, fitVariableLine, resolveLineColumns } from "./line-width.js";
2
+ import { displayColumns, fitVariableLine, resolveLineColumns, truncateMiddleColumns } 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 = "⋮";
8
9
  export function formatTimelineDuration(ms) {
9
10
  const totalSeconds = Math.max(0, Math.floor(ms / 1_000));
10
11
  if (totalSeconds < 60)
@@ -33,6 +34,48 @@ function elapsedLabel(anchorMs, at) {
33
34
  return "now";
34
35
  return formatTimelineDuration(Math.max(0, anchorMs - eventMs));
35
36
  }
37
+ export function formatWallClock(at) {
38
+ const value = new Date(at);
39
+ if (!Number.isFinite(value.getTime()))
40
+ return " : │";
41
+ return `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}│`;
42
+ }
43
+ function commonPrefix(left, right) {
44
+ const a = [...left];
45
+ const b = [...right];
46
+ let index = 0;
47
+ while (index < a.length && index < b.length && a[index] === b[index])
48
+ index += 1;
49
+ return a.slice(0, index).join("");
50
+ }
51
+ function compressedPrefix(prefix) {
52
+ const points = [...prefix];
53
+ if (points.length < 24)
54
+ return undefined;
55
+ let cut = -1;
56
+ for (let index = points.length - 1; index >= 0; index -= 1) {
57
+ if (points[index] === " ") {
58
+ cut = index;
59
+ break;
60
+ }
61
+ }
62
+ if (cut < 0)
63
+ return undefined;
64
+ const stable = points.slice(0, cut).join("");
65
+ const stablePoints = [...stable];
66
+ if (stablePoints.length < 24)
67
+ return undefined;
68
+ return {
69
+ text: `${stablePoints.slice(0, 10).join("")}…${stablePoints.slice(-14).join("")}`,
70
+ resumeAt: cut + 1,
71
+ };
72
+ }
73
+ function timelineGutter(label, spine) {
74
+ return label || `${"".padStart(TIME_GUTTER_WIDTH)}${spine}`;
75
+ }
76
+ function renderTimelineGap(omittedEventCount, maxColumns) {
77
+ return fitVariableLine(`${timelineGutter("", TIMELINE_GAP_MARKER)} `, `${omittedEventCount} more`, maxColumns);
78
+ }
36
79
  function compactTokens(value) {
37
80
  if (value < 1_000)
38
81
  return String(value);
@@ -92,22 +135,28 @@ export function renderProjectionActivitySummary(activity, input) {
92
135
  function renderTerminalRollup(activity, input) {
93
136
  const summary = summarizeProjectionToolLedger(activity, input.workspaceRoot);
94
137
  const segments = projectionSummarySegments(summary);
95
- const lines = input.diagnostic
96
- ? [fitVariableLine(`${elapsedLabel(input.anchorMs, input.diagnostic.at).padStart(TIME_GUTTER_WIDTH)}┆ ${"warning".padEnd(VERB_WIDTH)} `, compactText(input.diagnostic.text), input.maxColumns)]
138
+ const rows = input.diagnostic
139
+ ? [{
140
+ kind: "diagnostic",
141
+ line: fitVariableLine(`${elapsedLabel(input.anchorMs, input.diagnostic.at).padStart(TIME_GUTTER_WIDTH)}┆ ${"warning".padEnd(VERB_WIDTH)} `, compactText(input.diagnostic.text), input.maxColumns),
142
+ }]
97
143
  : [];
98
144
  const endPrefix = `${"end".padStart(TIME_GUTTER_WIDTH)}└`;
99
- lines.push(segments.length > 0
100
- ? fitVariableLine(`${endPrefix} `, segments.join(" · "), input.maxColumns)
101
- : endPrefix);
145
+ rows.push({
146
+ kind: "end-anchor",
147
+ line: segments.length > 0
148
+ ? fitVariableLine(`${endPrefix} `, segments.join(" · "), input.maxColumns)
149
+ : endPrefix,
150
+ });
102
151
  for (const file of summary.files) {
103
152
  const repeat = file.count > 1 ? ` ×${file.count}` : "";
104
153
  const diffstat = file.diffstat ? ` +${file.diffstat.additions} −${file.diffstat.deletions}` : "";
105
- lines.push(fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns));
154
+ rows.push({ kind: "file", line: fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns) });
106
155
  }
107
156
  if (summary.hiddenFileCount > 0) {
108
- lines.push(fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns));
157
+ rows.push({ kind: "file", line: fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns) });
109
158
  }
110
- return { lines, hasMutationFiles: summary.files.length > 0 };
159
+ return { rows, hasMutationFiles: summary.files.length > 0 };
111
160
  }
112
161
  function timelineEntries(activity) {
113
162
  const entries = [];
@@ -124,26 +173,42 @@ function timelineEntries(activity) {
124
173
  }
125
174
  export function renderProjectionActivity(activity, input) {
126
175
  const lines = [];
176
+ const rowKinds = [];
127
177
  let priorAtMs;
178
+ let priorOrder;
179
+ let priorMinute;
180
+ let priorCompletedRan;
128
181
  let hasEndAnchor = false;
129
182
  let lastRunningTool;
130
183
  const maxColumns = input.maxColumns ?? resolveLineColumns();
131
184
  const entries = timelineEntries(activity);
185
+ const pushRow = (line, kind) => {
186
+ lines.push(line);
187
+ rowKinds.push(kind);
188
+ return true;
189
+ };
132
190
  for (let index = 0; index < entries.length; index += 1) {
133
191
  const entry = entries[index];
134
192
  const atMs = Date.parse(entry.at);
193
+ if (priorOrder !== undefined && entry.order > priorOrder + 1) {
194
+ pushRow(renderTimelineGap(entry.order - priorOrder - 1, maxColumns), "activity");
195
+ }
196
+ priorOrder = entry.order;
135
197
  if (entry.kind === "clock") {
136
198
  if (Number.isFinite(atMs))
137
199
  priorAtMs = atMs;
138
200
  continue;
139
201
  }
140
202
  if (priorAtMs !== undefined && Number.isFinite(atMs) && atMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
141
- lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(atMs - priorAtMs)} pass in silence`, maxColumns));
203
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(atMs - priorAtMs)} pass in silence`, maxColumns), "silence");
142
204
  }
143
205
  if (Number.isFinite(atMs))
144
206
  priorAtMs = atMs;
145
207
  const row = entry.row;
146
- const label = row.kind === "turn" ? "" : elapsedLabel(input.anchorMs, row.at);
208
+ const minute = row.kind === "turn" ? "" : formatWallClock(row.at);
209
+ const label = row.kind === "turn" || minute === priorMinute ? "" : minute;
210
+ if (minute)
211
+ priorMinute = minute;
147
212
  const spine = row.kind === "turn"
148
213
  ? "├"
149
214
  : input.state === "dead" || input.state === "lost"
@@ -159,27 +224,41 @@ export function renderProjectionActivity(activity, input) {
159
224
  ? "unresolved"
160
225
  : "running";
161
226
  const presentation = presentFoldedToolActivity(row, { state: activityState, ...(duration ? { duration } : {}) }, (value) => presentLedgerPath(value, input.workspaceRoot) ?? value);
162
- const fixed = `${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${(presentation.semanticClass === "run" ? "ran" : presentation.semanticClass).padEnd(VERB_WIDTH)} `;
163
- lines.push(fitAffixedLine(fixed, presentation.variableText, presentation.fixedSuffix, maxColumns));
227
+ const fixed = `${timelineGutter(label, spine)} ${(presentation.semanticClass === "run" ? "ran" : presentation.semanticClass).padEnd(VERB_WIDTH)} `;
228
+ let variable = presentation.variableText;
229
+ if (row.state === "completed" && priorCompletedRan !== undefined) {
230
+ const common = commonPrefix(priorCompletedRan, variable);
231
+ const prefix = compressedPrefix(common);
232
+ if (prefix) {
233
+ variable = `${prefix.text} ${[...variable].slice(prefix.resumeAt).join("")}`;
234
+ }
235
+ }
236
+ priorCompletedRan = row.state === "completed" ? presentation.variableText : undefined;
237
+ const budget = Math.max(0, maxColumns - displayColumns(fixed) - displayColumns(presentation.fixedSuffix));
238
+ pushRow(`${fixed}${truncateMiddleColumns(variable, budget)}${presentation.fixedSuffix}`, "activity");
164
239
  continue;
165
240
  }
166
241
  if (row.kind === "said") {
167
- lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns));
242
+ priorCompletedRan = undefined;
243
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns), "activity");
168
244
  }
169
245
  else if (row.kind === "thought") {
246
+ priorCompletedRan = undefined;
170
247
  const text = `${compactText(row.text)}${row.truncated && !row.text.endsWith("…") ? "…" : ""}`;
171
248
  const duration = formatActionDuration(row.durationMs);
172
- lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns));
249
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns), "activity");
173
250
  }
174
251
  else if (row.kind === "warning") {
175
- lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns));
252
+ priorCompletedRan = undefined;
253
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns), "activity");
176
254
  }
177
255
  else {
256
+ priorCompletedRan = undefined;
178
257
  const usage = row.inputTokens !== undefined && row.outputTokens !== undefined
179
258
  ? ` · ${compactTokens(row.inputTokens)}▸${compactTokens(row.outputTokens)} tok`
180
259
  : "";
181
260
  const cost = row.costUsd !== undefined ? ` · $${row.costUsd.toFixed(2)}` : "";
182
- lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn}${usage}${cost}`, maxColumns));
261
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn}${usage}${cost}`, maxColumns), "activity");
183
262
  }
184
263
  }
185
264
  for (const row of activity.pinned) {
@@ -189,9 +268,9 @@ export function renderProjectionActivity(activity, input) {
189
268
  if (input.showPlan && activity.plan) {
190
269
  const planAtMs = Date.parse(activity.plan.at);
191
270
  if (priorAtMs !== undefined && Number.isFinite(planAtMs) && planAtMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
192
- lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(planAtMs - priorAtMs)} pass in silence`, maxColumns));
271
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(planAtMs - priorAtMs)} pass in silence`, maxColumns), "silence");
193
272
  }
194
- lines.push(renderPlan(activity.plan, maxColumns));
273
+ pushRow(renderPlan(activity.plan, maxColumns), "plan");
195
274
  }
196
275
  let hasMutationFiles = false;
197
276
  if (["returned", "dead", "lost", "dismissed"].includes(input.state)) {
@@ -201,12 +280,15 @@ export function renderProjectionActivity(activity, input) {
201
280
  ...(input.terminalDiagnostic ? { diagnostic: input.terminalDiagnostic } : {}),
202
281
  maxColumns,
203
282
  });
204
- lines.push(...rollup.lines);
283
+ for (const row of rollup.rows) {
284
+ pushRow(row.line, row.kind);
285
+ }
205
286
  hasEndAnchor = true;
206
287
  hasMutationFiles = rollup.hasMutationFiles;
207
288
  }
208
289
  return {
209
290
  lines,
291
+ rowKinds,
210
292
  ...(lastRunningTool ? { lastRunningTool } : {}),
211
293
  hasEndAnchor,
212
294
  hasMutationFiles,
@@ -3,6 +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 { renderPathPreview, renderPatternPair } from "./path-prefix-compaction.js";
6
7
  export function textResponse(text) {
7
8
  const payload = {};
8
9
  return {
@@ -24,18 +25,26 @@ export function buildBindDraftResponse(draftPath) {
24
25
  const payload = { draftPath };
25
26
  return coreTextResponse(`Draft saved to ${draftPath}.`, payload);
26
27
  }
28
+ export function buildBindPreviewResponse(result) {
29
+ const overlapWarnings = renderScopeOverlapWarnings(result.scopeOverlaps);
30
+ const payload = {
31
+ id: result.id,
32
+ canonicalDraft: result.canonicalDraft,
33
+ target: result.target,
34
+ place: result.place,
35
+ workspace: result.workspace,
36
+ after: result.after,
37
+ warnings: overlapWarnings,
38
+ scopeOverlaps: result.scopeOverlaps,
39
+ };
40
+ return {
41
+ ...coreTextResponse(result.canonicalDraft, payload),
42
+ ...(overlapWarnings.length > 0 ? { stderr: ["Warnings:", ...overlapWarnings].join("\n") } : {}),
43
+ };
44
+ }
27
45
  export function buildBindResponse(result, cwd) {
28
46
  void cwd;
29
- const overlapsByCounterparty = new Map();
30
- for (const overlap of result.scopeOverlaps) {
31
- const counterparty = overlap.contracts[1];
32
- const group = overlapsByCounterparty.get(counterparty);
33
- if (group)
34
- group.push(overlap);
35
- else
36
- overlapsByCounterparty.set(counterparty, [overlap]);
37
- }
38
- const overlapWarnings = [...overlapsByCounterparty.entries()].map(([contractId, overlaps]) => renderScopeOverlapGroup(commissionSlug(contractId), overlaps));
47
+ const overlapWarnings = renderScopeOverlapWarnings(result.scopeOverlaps);
39
48
  const place = result.place;
40
49
  const payload = {
41
50
  id: result.id,
@@ -54,16 +63,30 @@ export function buildBindResponse(result, cwd) {
54
63
  ...(overlapWarnings.length > 0 ? ["Warnings:", ...overlapWarnings] : []),
55
64
  ].join("\n"), payload);
56
65
  }
66
+ function renderScopeOverlapWarnings(overlaps) {
67
+ const overlapsByCounterparty = new Map();
68
+ for (const overlap of overlaps) {
69
+ const counterparty = overlap.contracts[1];
70
+ const group = overlapsByCounterparty.get(counterparty);
71
+ if (group)
72
+ group.push(overlap);
73
+ else
74
+ overlapsByCounterparty.set(counterparty, [overlap]);
75
+ }
76
+ return [...overlapsByCounterparty.entries()].map(([contractId, overlaps]) => renderScopeOverlapGroup(commissionSlug(contractId), overlaps));
77
+ }
57
78
  function renderScopeOverlapGroup(contractAddr, overlaps) {
58
- const pairs = overlaps.map((overlap) => {
79
+ const nextAddr = commissionSlug(overlaps[0].contracts[0]);
80
+ const pairs = overlaps.flatMap((overlap) => {
59
81
  const [nextPattern, existingPattern] = overlap.patterns;
82
+ const patternPair = renderPatternPair(nextAddr, nextPattern, contractAddr, existingPattern);
60
83
  const evidence = overlap.kind === "potential"
61
- ? "potential; no current tracked path proves it"
62
- : `concrete; ${overlap.paths.count} current tracked path${overlap.paths.count === 1 ? "" : "s"}: ${overlap.paths.preview.join(", ")}${overlap.paths.truncated ? ", …" : ""}`;
63
- return ` ${nextPattern} × ${existingPattern} (${evidence})`;
84
+ ? ["potential · no current tracked path proves it"]
85
+ : renderPathPreview(overlap.paths);
86
+ return [...patternPair, ...evidence].map((line) => ` ${line}`);
64
87
  });
65
88
  return [
66
- `▲ scope overlap with ${contractAddr}:`,
89
+ `▲ overlap ${nextAddr} ↔ ${contractAddr}`,
67
90
  ...pairs,
68
91
  ` consider --after ${contractAddr}`,
69
92
  ].join("\n");
@@ -74,7 +74,7 @@ function renderProjectionOmission(board) {
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 ` · +${board.omitted.total} historical projections not shown (${counts.join(" · ")})`;
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
  }
@@ -62,6 +62,8 @@ export async function writeSuccessResponse(command, flags, response) {
62
62
  if (command === "bind" && flags.dryRun) {
63
63
  // Canonical Markdown already ends with exactly one newline.
64
64
  await writeCliOutput(process.stdout, responseText(response));
65
+ if (response.stderr)
66
+ await writeCliOutput(process.stderr, `${response.stderr}\n`);
65
67
  return;
66
68
  }
67
69
  if (command !== "revive") {
@@ -53,9 +53,9 @@ export function presentToolActivity(input) {
53
53
  }
54
54
  else if (input.state === "running") {
55
55
  if (message)
56
- variableText = `${primary} — …${message}`;
56
+ variableText = `${primary} — ${message}`;
57
57
  else
58
- fixedSuffix = " — still running…";
58
+ fixedSuffix = ` — ${input.duration ?? "0s"}…`;
59
59
  }
60
60
  else if (input.result?.status === "error") {
61
61
  if (input.result.exitCode !== undefined) {
@@ -70,7 +70,7 @@ export function presentToolActivity(input) {
70
70
  fixedSuffix = ` — +${input.result.diffstat.additions} −${input.result.diffstat.deletions}`;
71
71
  }
72
72
  else if (input.call.kind === "run") {
73
- fixedSuffix = ` — done${input.duration ? ` ${input.duration}` : ""}`;
73
+ fixedSuffix = input.duration ? ` — ${input.duration}` : "";
74
74
  }
75
75
  else if (input.call.kind === "other") {
76
76
  fixedSuffix = " — done";