@astrosheep/keiyaku 2.9.6 → 2.9.7

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 (54) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/outcome.js +10 -0
  3. package/build/cli/commands/contract/bind/handler.js +10 -4
  4. package/build/cli/commands/contract/bind/meta.js +6 -6
  5. package/build/cli/commands/contract/petition/handler.js +16 -3
  6. package/build/cli/commands/contract/petition/meta.js +4 -4
  7. package/build/cli/commands/metadata.js +3 -2
  8. package/build/cli/commands/projection/tell/handler.js +10 -2
  9. package/build/cli/commands/projection/tell/meta.js +3 -3
  10. package/build/cli/commands/task/catalog.js +2 -0
  11. package/build/cli/commands/task/log/handler.js +12 -0
  12. package/build/cli/commands/task/log/meta.js +8 -0
  13. package/build/cli/flags.js +8 -0
  14. package/build/cli/index.js +10 -6
  15. package/build/cli/parse-flags.js +6 -0
  16. package/build/cli/parse-metadata.js +1 -1
  17. package/build/cli/render/line-width.js +33 -0
  18. package/build/cli/render/path-prefix-compaction.js +88 -0
  19. package/build/cli/render/petition.js +4 -0
  20. package/build/cli/render/projection-activity.js +93 -20
  21. package/build/cli/render/shared.js +34 -12
  22. package/build/cli/render/success-response.js +2 -0
  23. package/build/cli/render/tool-presentation.js +3 -3
  24. package/build/cli/render/wait.js +68 -48
  25. package/build/cli/subagent-guard.js +3 -0
  26. package/build/cli/types.js +1 -1
  27. package/build/core/bind.js +111 -6
  28. package/build/core/draft.js +1 -1
  29. package/build/core/projection/generation/database.js +21 -2
  30. package/build/core/projection/generation/model.js +24 -3
  31. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  32. package/build/core/projection/generation/projection-generation-execution.js +4 -3
  33. package/build/core/projection/generation/projection-generation-runner.js +96 -53
  34. package/build/core/projection/generation/projection-generation-runtime.js +19 -0
  35. package/build/core/projection/generation/store.js +9 -0
  36. package/build/core/projection/generation/transitions.js +105 -3
  37. package/build/core/projection/index.js +2 -2
  38. package/build/core/projection/projection-core.js +1 -1
  39. package/build/core/projection/projection-kill.js +31 -0
  40. package/build/core/projection/projection-life-protocol.js +10 -0
  41. package/build/core/projection/projection-wait.js +53 -15
  42. package/build/core/projection/projection-wake.js +35 -3
  43. package/build/core/projection/tell/database.js +18 -0
  44. package/build/core/projection/tell/model.js +1 -0
  45. package/build/core/projection/tell/store.js +102 -55
  46. package/build/core/registry.js +82 -69
  47. package/build/core/scope.js +9 -9
  48. package/build/core/task/index.js +2 -2
  49. package/build/core/task/task-contract.js +18 -0
  50. package/build/core/task/task-git-store.js +50 -0
  51. package/build/generated/version.js +2 -2
  52. package/package.json +1 -1
  53. package/skills/keiyaku-akuma/SKILL.md +10 -0
  54. package/skills/keiyaku-workflow/SKILL.md +76 -7
@@ -1,5 +1,5 @@
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
5
  const TIME_GUTTER_WIDTH = 6;
@@ -33,6 +33,45 @@ function elapsedLabel(anchorMs, at) {
33
33
  return "now";
34
34
  return formatTimelineDuration(Math.max(0, anchorMs - eventMs));
35
35
  }
36
+ export function formatWallClock(at) {
37
+ const value = new Date(at);
38
+ if (!Number.isFinite(value.getTime()))
39
+ return " : │";
40
+ return `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}│`;
41
+ }
42
+ function commonPrefix(left, right) {
43
+ const a = [...left];
44
+ const b = [...right];
45
+ let index = 0;
46
+ while (index < a.length && index < b.length && a[index] === b[index])
47
+ index += 1;
48
+ return a.slice(0, index).join("");
49
+ }
50
+ function compressedPrefix(prefix) {
51
+ const points = [...prefix];
52
+ if (points.length < 24)
53
+ return undefined;
54
+ let cut = -1;
55
+ for (let index = points.length - 1; index >= 0; index -= 1) {
56
+ if (points[index] === " ") {
57
+ cut = index;
58
+ break;
59
+ }
60
+ }
61
+ if (cut < 0)
62
+ return undefined;
63
+ const stable = points.slice(0, cut).join("");
64
+ const stablePoints = [...stable];
65
+ if (stablePoints.length < 24)
66
+ return undefined;
67
+ return {
68
+ text: `${stablePoints.slice(0, 10).join("")}…${stablePoints.slice(-14).join("")}`,
69
+ resumeAt: cut + 1,
70
+ };
71
+ }
72
+ function timelineGutter(label, spine) {
73
+ return label || `${"".padStart(TIME_GUTTER_WIDTH)}${spine}`;
74
+ }
36
75
  function compactTokens(value) {
37
76
  if (value < 1_000)
38
77
  return String(value);
@@ -92,22 +131,28 @@ export function renderProjectionActivitySummary(activity, input) {
92
131
  function renderTerminalRollup(activity, input) {
93
132
  const summary = summarizeProjectionToolLedger(activity, input.workspaceRoot);
94
133
  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)]
134
+ const rows = input.diagnostic
135
+ ? [{
136
+ kind: "diagnostic",
137
+ line: fitVariableLine(`${elapsedLabel(input.anchorMs, input.diagnostic.at).padStart(TIME_GUTTER_WIDTH)}┆ ${"warning".padEnd(VERB_WIDTH)} `, compactText(input.diagnostic.text), input.maxColumns),
138
+ }]
97
139
  : [];
98
140
  const endPrefix = `${"end".padStart(TIME_GUTTER_WIDTH)}└`;
99
- lines.push(segments.length > 0
100
- ? fitVariableLine(`${endPrefix} `, segments.join(" · "), input.maxColumns)
101
- : endPrefix);
141
+ rows.push({
142
+ kind: "end-anchor",
143
+ line: segments.length > 0
144
+ ? fitVariableLine(`${endPrefix} `, segments.join(" · "), input.maxColumns)
145
+ : endPrefix,
146
+ });
102
147
  for (const file of summary.files) {
103
148
  const repeat = file.count > 1 ? ` ×${file.count}` : "";
104
149
  const diffstat = file.diffstat ? ` +${file.diffstat.additions} −${file.diffstat.deletions}` : "";
105
- lines.push(fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns));
150
+ rows.push({ kind: "file", line: fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns) });
106
151
  }
107
152
  if (summary.hiddenFileCount > 0) {
108
- lines.push(fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns));
153
+ rows.push({ kind: "file", line: fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns) });
109
154
  }
110
- return { lines, hasMutationFiles: summary.files.length > 0 };
155
+ return { rows, hasMutationFiles: summary.files.length > 0 };
111
156
  }
112
157
  function timelineEntries(activity) {
113
158
  const entries = [];
@@ -124,11 +169,19 @@ function timelineEntries(activity) {
124
169
  }
125
170
  export function renderProjectionActivity(activity, input) {
126
171
  const lines = [];
172
+ const rowKinds = [];
127
173
  let priorAtMs;
174
+ let priorMinute;
175
+ let priorCompletedRan;
128
176
  let hasEndAnchor = false;
129
177
  let lastRunningTool;
130
178
  const maxColumns = input.maxColumns ?? resolveLineColumns();
131
179
  const entries = timelineEntries(activity);
180
+ const pushRow = (line, kind) => {
181
+ lines.push(line);
182
+ rowKinds.push(kind);
183
+ return true;
184
+ };
132
185
  for (let index = 0; index < entries.length; index += 1) {
133
186
  const entry = entries[index];
134
187
  const atMs = Date.parse(entry.at);
@@ -138,12 +191,15 @@ export function renderProjectionActivity(activity, input) {
138
191
  continue;
139
192
  }
140
193
  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));
194
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(atMs - priorAtMs)} pass in silence`, maxColumns), "silence");
142
195
  }
143
196
  if (Number.isFinite(atMs))
144
197
  priorAtMs = atMs;
145
198
  const row = entry.row;
146
- const label = row.kind === "turn" ? "" : elapsedLabel(input.anchorMs, row.at);
199
+ const minute = row.kind === "turn" ? "" : formatWallClock(row.at);
200
+ const label = row.kind === "turn" || minute === priorMinute ? "" : minute;
201
+ if (minute)
202
+ priorMinute = minute;
147
203
  const spine = row.kind === "turn"
148
204
  ? "├"
149
205
  : input.state === "dead" || input.state === "lost"
@@ -159,27 +215,41 @@ export function renderProjectionActivity(activity, input) {
159
215
  ? "unresolved"
160
216
  : "running";
161
217
  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));
218
+ const fixed = `${timelineGutter(label, spine)} ${(presentation.semanticClass === "run" ? "ran" : presentation.semanticClass).padEnd(VERB_WIDTH)} `;
219
+ let variable = presentation.variableText;
220
+ if (row.state === "completed" && priorCompletedRan !== undefined) {
221
+ const common = commonPrefix(priorCompletedRan, variable);
222
+ const prefix = compressedPrefix(common);
223
+ if (prefix) {
224
+ variable = `${prefix.text} ${[...variable].slice(prefix.resumeAt).join("")}`;
225
+ }
226
+ }
227
+ priorCompletedRan = row.state === "completed" ? presentation.variableText : undefined;
228
+ const budget = Math.max(0, maxColumns - displayColumns(fixed) - displayColumns(presentation.fixedSuffix));
229
+ pushRow(`${fixed}${truncateMiddleColumns(variable, budget)}${presentation.fixedSuffix}`, "activity");
164
230
  continue;
165
231
  }
166
232
  if (row.kind === "said") {
167
- lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns));
233
+ priorCompletedRan = undefined;
234
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns), "activity");
168
235
  }
169
236
  else if (row.kind === "thought") {
237
+ priorCompletedRan = undefined;
170
238
  const text = `${compactText(row.text)}${row.truncated && !row.text.endsWith("…") ? "…" : ""}`;
171
239
  const duration = formatActionDuration(row.durationMs);
172
- lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns));
240
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns), "activity");
173
241
  }
174
242
  else if (row.kind === "warning") {
175
- lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns));
243
+ priorCompletedRan = undefined;
244
+ pushRow(fitVariableLine(`${timelineGutter(label, spine)} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns), "activity");
176
245
  }
177
246
  else {
247
+ priorCompletedRan = undefined;
178
248
  const usage = row.inputTokens !== undefined && row.outputTokens !== undefined
179
249
  ? ` · ${compactTokens(row.inputTokens)}▸${compactTokens(row.outputTokens)} tok`
180
250
  : "";
181
251
  const cost = row.costUsd !== undefined ? ` · $${row.costUsd.toFixed(2)}` : "";
182
- lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn}${usage}${cost}`, maxColumns));
252
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn}${usage}${cost}`, maxColumns), "activity");
183
253
  }
184
254
  }
185
255
  for (const row of activity.pinned) {
@@ -189,9 +259,9 @@ export function renderProjectionActivity(activity, input) {
189
259
  if (input.showPlan && activity.plan) {
190
260
  const planAtMs = Date.parse(activity.plan.at);
191
261
  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));
262
+ pushRow(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(planAtMs - priorAtMs)} pass in silence`, maxColumns), "silence");
193
263
  }
194
- lines.push(renderPlan(activity.plan, maxColumns));
264
+ pushRow(renderPlan(activity.plan, maxColumns), "plan");
195
265
  }
196
266
  let hasMutationFiles = false;
197
267
  if (["returned", "dead", "lost", "dismissed"].includes(input.state)) {
@@ -201,12 +271,15 @@ export function renderProjectionActivity(activity, input) {
201
271
  ...(input.terminalDiagnostic ? { diagnostic: input.terminalDiagnostic } : {}),
202
272
  maxColumns,
203
273
  });
204
- lines.push(...rollup.lines);
274
+ for (const row of rollup.rows) {
275
+ pushRow(row.line, row.kind);
276
+ }
205
277
  hasEndAnchor = true;
206
278
  hasMutationFiles = rollup.hasMutationFiles;
207
279
  }
208
280
  return {
209
281
  lines,
282
+ rowKinds,
210
283
  ...(lastRunningTool ? { lastRunningTool } : {}),
211
284
  hasEndAnchor,
212
285
  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 { compactPathPreview, compactPatternPair } 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,13 +63,26 @@ 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
79
  const pairs = overlaps.map((overlap) => {
59
80
  const [nextPattern, existingPattern] = overlap.patterns;
81
+ const patternPair = compactPatternPair(nextPattern, existingPattern);
60
82
  const evidence = overlap.kind === "potential"
61
83
  ? "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
+ : `concrete; ${overlap.paths.count} current tracked path${overlap.paths.count === 1 ? "" : "s"}: ${compactPathPreview(overlap.paths.preview)}${overlap.paths.truncated ? ", …" : ""}`;
85
+ return ` ${patternPair} (${evidence})`;
64
86
  });
65
87
  return [
66
88
  `▲ scope overlap with ${contractAddr}:`,
@@ -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";
@@ -3,6 +3,27 @@ import { displayColumns, resolveLineColumns, truncateColumns } from "./line-widt
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
+ }
6
27
  function shortAddress(projectionId) {
7
28
  return /\/([0-9a-f]{8})$/.exec(projectionId)?.[1] ?? projectionId;
8
29
  }
@@ -26,29 +47,10 @@ function isTerminalPhase(phase) {
26
47
  return ["done", "failed", "killed", "dismissed"].includes(phase);
27
48
  }
28
49
  function waitStateText(snapshot) {
29
- switch (snapshot.phase) {
30
- case "active":
31
- case "quiescent": {
32
- const pulse = snapshot.activityAgeMs === undefined ? "unknown" : formatTimelineDuration(snapshot.activityAgeMs);
33
- return `pulse ${pulse} · breathing ${formatTimelineDuration(snapshot.durationMs)}`;
34
- }
35
- case "minting":
36
- return `starting · lived ${formatTimelineDuration(snapshot.durationMs)}`;
37
- case "startup-timeout":
38
- return `startup timed out · lived ${formatTimelineDuration(snapshot.durationMs)}`;
39
- case "unknown":
40
- return `state unknown · observed ${formatTimelineDuration(snapshot.durationMs)}`;
41
- case "done":
42
- return `returned · lived ${formatTimelineDuration(snapshot.durationMs)}`;
43
- case "failed":
44
- return `dead · lived ${formatTimelineDuration(snapshot.durationMs)}`;
45
- case "killed":
46
- return "killed";
47
- case "dismissed":
48
- return "dismissed";
49
- case "lost":
50
- return `no pulse ${snapshot.activityAgeMs === undefined ? "unknown" : formatTimelineDuration(snapshot.activityAgeMs)}`;
51
- }
50
+ const idle = snapshot.activityAgeMs === undefined
51
+ ? formatTimelineDuration(snapshot.durationMs)
52
+ : formatTimelineDuration(snapshot.activityAgeMs);
53
+ return `up ${formatTimelineDuration(snapshot.durationMs)} · idle ${idle}`;
52
54
  }
53
55
  function projectionIdentityParts(projectionId) {
54
56
  const match = /^(.*)\/([0-9a-f]{8})$/.exec(projectionId);
@@ -56,8 +58,7 @@ function projectionIdentityParts(projectionId) {
56
58
  }
57
59
  function waitHeader(projectionId, akuma, snapshot, outputColumns) {
58
60
  const maxColumns = Math.min(WAIT_HEADER_MAX_COLUMNS, outputColumns);
59
- const tellSuffix = snapshot.tellsWaiting > 0 ? ` · ${snapshot.tellsWaiting} tells waiting` : "";
60
- const state = `${waitStateText(snapshot)}${tellSuffix}`;
61
+ const state = waitStateText(snapshot);
61
62
  const identity = projectionIdentityParts(projectionId);
62
63
  // Slug often mirrors the Akuma name; print once when they are the same fact.
63
64
  const description = identity.slug && akuma && identity.slug === akuma
@@ -69,6 +70,14 @@ function waitHeader(projectionId, akuma, snapshot, outputColumns) {
69
70
  const dashCount = Math.max(1, maxColumns - displayColumns(`${prefix} ${state}`));
70
71
  return `${prefix} ${"─".repeat(dashCount)} ${state}`;
71
72
  }
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}`;
79
+ });
80
+ }
72
81
  function ledgerHint(projectionId, snapshot) {
73
82
  const file = snapshot.eventLedgerFile ?? "events.jsonl";
74
83
  return `· ledger: .keiyaku/projection/${projectionId}/${file}`;
@@ -136,7 +145,11 @@ function unknownResponse(projectionId, akuma, snapshot, maxColumns) {
136
145
  ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
137
146
  maxColumns,
138
147
  });
139
- const historicalLines = rendered.lines.map((line) => line.replace(/[┆├]/, "│"));
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(/[┆├]/, "│"));
140
153
  return {
141
154
  ...textResponse([
142
155
  `? ${akuma} · projection: ${projectionId} · unknown`,
@@ -186,7 +199,7 @@ export function buildWaitResponse(projectionId, result, options = {}) {
186
199
  && (result.terminalSnapshot || isTerminalPhase(result.snapshot.phase))
187
200
  ? { ...result.snapshot, phase: "active" }
188
201
  : result.snapshot;
189
- const maxColumns = options.maxColumns ?? resolveLineColumns();
202
+ const maxColumns = Math.min(120, options.maxColumns ?? resolveLineColumns());
190
203
  if (result.state === "terminal" && result.verdict.state === "killed") {
191
204
  return {
192
205
  ...textResponse(`× killed ${projectionId}`),
@@ -203,6 +216,14 @@ export function buildWaitResponse(projectionId, result, options = {}) {
203
216
  if (state === "returned") {
204
217
  return returnedResponse(projectionId, result);
205
218
  }
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;
206
227
  const rendered = renderProjectionActivity(snapshot.activity, {
207
228
  anchorMs: snapshot.observedAtMs,
208
229
  state,
@@ -210,28 +231,27 @@ export function buildWaitResponse(projectionId, result, options = {}) {
210
231
  ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
211
232
  maxColumns,
212
233
  });
213
- const storageDiagnostics = (snapshot.eventDiagnostics ?? []).map((diagnostic) => `‼ ${diagnostic.kind} · ${diagnostic.detail}`);
214
- const lines = [
215
- waitHeader(projectionId, result.pact.akuma, snapshot, maxColumns),
216
- ...storageDiagnostics,
217
- ...rendered.lines,
218
- ]
219
- .map((line) => truncateColumns(line, maxColumns));
220
- if (rendered.lines.length === 0 && state === "running") {
221
- lines.push(" not a word out of it yet — pulse is steady");
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++ });
222
238
  }
223
- // Live/timeout observation faces keep the bounded activity window and add the
224
- // existing compact rollup after those rows. Terminal faces already own end└
225
- // and must not duplicate it; returned response-only faces never reach here.
226
- if (!rendered.hasEndAnchor) {
227
- const summary = renderProjectionActivitySummary(snapshot.activity, {
228
- state,
229
- ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
230
- maxColumns,
231
- });
232
- if (summary) {
233
- lines.push(summary);
234
- }
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`);
235
255
  }
236
256
  const nextActions = state === "running" && options.runningActions === false
237
257
  ? []
@@ -11,6 +11,9 @@ function isSubagentSafeCommand(args) {
11
11
  const subcommand = args[1];
12
12
  return subcommand === "list" || subcommand === "show" || SUBAGENT_SAFE_FLAGS.has(subcommand ?? "");
13
13
  }
14
+ if (head === "task") {
15
+ return args[1] === "log" || SUBAGENT_SAFE_FLAGS.has(args[1] ?? "");
16
+ }
14
17
  return ((head !== undefined && SUBAGENT_SAFE_COMMANDS.has(head)) ||
15
18
  (args.length === 1 && head !== undefined && SUBAGENT_SAFE_FLAGS.has(head)));
16
19
  }
@@ -1 +1 @@
1
- export const TASK_SUBCOMMANDS = ["add", "show", "ls", "start", "stop", "done", "drop", "update", "doctor"];
1
+ export const TASK_SUBCOMMANDS = ["add", "show", "ls", "log", "start", "stop", "done", "drop", "update", "doctor"];
@@ -3,12 +3,12 @@ import { assertSettingsKnobUsable } from "../config/settings/disease.js";
3
3
  import { loadKeiyakuSettings } from "../config/settings/loader.js";
4
4
  import { FlowError } from "../flow-error.js";
5
5
  import { activeLedgerRef, appendEntry, appendWithRetry, readLedger } from "./ledger.js";
6
- import { describeMissingBindSections, materializeBindDraft, mergeBindDraft, missingBindDraftSections, parseBindDraft, parsePartialBindDraft, renderMissingBindSectionsTemplate, } from "./draft.js";
7
- import { assertValidContractId, buildContractId } from "./ids.js";
8
- import { registerScope, releaseScope } from "./registry.js";
6
+ import { describeMissingBindSections, materializeBindDraft, mergeBindDraft, missingBindDraftSections, parseBindDraft, parsePartialBindDraft, renderBindDraft, renderMissingBindSectionsTemplate, } from "./draft.js";
7
+ import { assertValidContractId, buildContractId, isCommissionId } from "./ids.js";
8
+ import { previewScopeRegistration, registerScope, releaseScope } from "./registry.js";
9
9
  import { resolveDefaultBranch } from "./status/drift.js";
10
10
  import { normalizeTargetRef } from "./target-ref.js";
11
- import { reconcileTaskForfeit, settleTaskBoardForBind } from "./task/index.js";
11
+ import { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind } from "./task/index.js";
12
12
  import { bootstrapEntryDataFromObservation, isBootstrapSuccess, runWorktreeBootstrap, } from "./worktree-bootstrap.js";
13
13
  import { contractWorktreePathFromBind, stableRepoRoot } from "./worktree-path.js";
14
14
  import { allocateWorktreePlace, cleanupCreatedBindResources, createContractBranch, defaultCreateWorktree, materializeContractView, resolveTreeRoot, } from "./bind-workspace.js";
@@ -138,6 +138,99 @@ export function composeTaskBindDraft(input) {
138
138
  }
139
139
  return materializeBindDraft(merged);
140
140
  }
141
+ async function validateBindAfter(cwd, after) {
142
+ const unique = new Set();
143
+ for (const contractId of after) {
144
+ if (unique.has(contractId)) {
145
+ throw new FlowError("INVALID_TARGET", `contract ${contractId} selected more than once by --after`);
146
+ }
147
+ unique.add(contractId);
148
+ if (!isCommissionId(contractId)) {
149
+ throw new FlowError("INVALID_TARGET", `after contract ${contractId} is not a valid contract id`);
150
+ }
151
+ if (!await readLedger(cwd, contractId)) {
152
+ throw new FlowError("INVALID_TARGET", `after contract ${contractId} does not exist`);
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * The common read-only bind admission candidate. Mutation uses this candidate
158
+ * before task settlement; preview stops after it has observed all admission
159
+ * facts and rendered the canonical draft.
160
+ */
161
+ async function admitBindCandidate(input, draft) {
162
+ const id = buildContractId({ name: draft.name, nowMs: input.nowMs, randomBytes: input.randomBytes });
163
+ await assertValidContractId(input.cwd, id);
164
+ await validateBindAfter(input.cwd, input.after);
165
+ const provisionalEntry = makeBindEntry({
166
+ id,
167
+ at: new Date(input.nowMs).toISOString(),
168
+ actor: input.actor,
169
+ draft,
170
+ // Preview never writes the task-board commit. The current target head is
171
+ // sufficient for its read-only provisional entry and worktree coordinate.
172
+ base: input.base,
173
+ target: input.target,
174
+ after: input.after,
175
+ workspace: input.workspace,
176
+ place: input.place,
177
+ bootstrap: input.bootstrapPlan,
178
+ });
179
+ const tree = await resolveTreeRoot(input.cwd, input.workspace, id, provisionalEntry);
180
+ const scopeOverlaps = await previewScopeRegistration(input.cwd, {
181
+ contractId: id,
182
+ scope: draft.scope,
183
+ tree,
184
+ exclusive: input.exclusive,
185
+ });
186
+ return {
187
+ id,
188
+ draft,
189
+ canonicalDraft: renderBindDraft(draft),
190
+ target: input.target,
191
+ place: input.workspace === "worktree" ? input.place ?? null : null,
192
+ workspace: input.workspace,
193
+ after: input.after,
194
+ scopeOverlaps,
195
+ };
196
+ }
197
+ export async function previewBindContract(input) {
198
+ const isTaskBound = input.taskIds !== undefined && input.taskIds.length > 0;
199
+ const ordinaryDraft = !isTaskBound ? parseBindDraft(input.draft) : undefined;
200
+ const workspace = input.workspace ?? "here";
201
+ const defaultBranch = await resolveDefaultBranch(input.cwd);
202
+ if (!defaultBranch) {
203
+ throw new FlowError("INTERNAL_STATE", "cannot bind: no default branch");
204
+ }
205
+ const target = normalizeTargetRef(defaultBranch.branch);
206
+ const loadedSettings = await loadKeiyakuSettings(input.cwd);
207
+ let bootstrapPlan;
208
+ if (workspace === "worktree") {
209
+ assertSettingsKnobUsable(loadedSettings, "worktreeBootstrap");
210
+ bootstrapPlan = resolvedBootstrapPlan(loadedSettings.knobs?.worktreeBootstrap);
211
+ }
212
+ const randomBytes = input.randomBytes?.(0) ?? randomBytesDefault();
213
+ const place = workspace === "worktree" ? await allocateWorktreePlace(input.cwd, randomBytes) : undefined;
214
+ const taskAdmission = isTaskBound
215
+ ? await previewTaskBoardForBind({ cwd: input.cwd, taskIds: input.taskIds })
216
+ : { tasks: [] };
217
+ const draft = isTaskBound
218
+ ? composeTaskBindDraft({ task: taskAdmission.tasks[0], draft: input.draft })
219
+ : ordinaryDraft;
220
+ return await admitBindCandidate({
221
+ cwd: input.cwd,
222
+ actor: input.actor,
223
+ nowMs: input.nowMs,
224
+ after: input.after ?? [],
225
+ exclusive: input.exclusive,
226
+ workspace,
227
+ randomBytes,
228
+ base: defaultBranch.head,
229
+ target,
230
+ bootstrapPlan,
231
+ place,
232
+ }, draft);
233
+ }
141
234
  export async function bindContract(input) {
142
235
  const isTaskBound = input.taskIds !== undefined && input.taskIds.length > 0;
143
236
  const ordinaryDraft = !isTaskBound ? parseBindDraft(input.draft) : undefined;
@@ -161,8 +254,20 @@ export async function bindContract(input) {
161
254
  let settlement;
162
255
  try {
163
256
  const prepare = async (draft) => {
164
- const id = buildContractId({ name: draft.name, nowMs: input.nowMs, randomBytes });
165
- await assertValidContractId(input.cwd, id);
257
+ const candidate = await admitBindCandidate({
258
+ cwd: input.cwd,
259
+ actor: input.actor,
260
+ nowMs: input.nowMs,
261
+ after: input.after ?? [],
262
+ exclusive: input.exclusive,
263
+ workspace,
264
+ randomBytes,
265
+ base: defaultBranch.head,
266
+ target,
267
+ bootstrapPlan,
268
+ place,
269
+ }, draft);
270
+ const id = candidate.id;
166
271
  const ref = activeLedgerRef(id);
167
272
  return {
168
273
  contractId: id,
@@ -204,7 +204,7 @@ export function renderMissingBindSectionsTemplate(missing) {
204
204
  continue;
205
205
  }
206
206
  if (section === "scope") {
207
- lines.push("## Scope", "```", "<pattern>", "<another-pattern>", "```");
207
+ lines.push("## Scope", "~~~", "<pattern>", "<another-pattern>", "~~~");
208
208
  continue;
209
209
  }
210
210
  lines.push("## Checks", "- <verification>");