@astrosheep/keiyaku 2.9.5 → 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 (75) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/outcome.js +20 -4
  3. package/build/agents/harness/projection.js +8 -7
  4. package/build/agents/harness/router.js +2 -1
  5. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  6. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  7. package/build/agents/providers/codex-app-server/adapter.js +11 -10
  8. package/build/agents/providers/codex-app-server/registration.js +2 -1
  9. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  10. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  11. package/build/agents/providers/pi/adapter.js +10 -0
  12. package/build/agents/providers/pi/registration.js +3 -1
  13. package/build/cli/commands/contract/bind/handler.js +10 -4
  14. package/build/cli/commands/contract/bind/meta.js +6 -6
  15. package/build/cli/commands/contract/petition/handler.js +16 -3
  16. package/build/cli/commands/contract/petition/meta.js +4 -4
  17. package/build/cli/commands/metadata.js +3 -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/catalog.js +2 -0
  21. package/build/cli/commands/task/log/handler.js +12 -0
  22. package/build/cli/commands/task/log/meta.js +8 -0
  23. package/build/cli/completion.js +9 -2
  24. package/build/cli/flags.js +11 -2
  25. package/build/cli/index.js +10 -6
  26. package/build/cli/parse-flags.js +6 -0
  27. package/build/cli/parse-metadata.js +1 -1
  28. package/build/cli/render/errors.js +18 -1
  29. package/build/cli/render/format.js +5 -2
  30. package/build/cli/render/line-width.js +33 -0
  31. package/build/cli/render/path-prefix-compaction.js +88 -0
  32. package/build/cli/render/petition.js +4 -0
  33. package/build/cli/render/projection-activity.js +93 -20
  34. package/build/cli/render/shared.js +34 -12
  35. package/build/cli/render/success-response.js +2 -0
  36. package/build/cli/render/tool-presentation.js +3 -3
  37. package/build/cli/render/wait.js +68 -48
  38. package/build/cli/subagent-guard.js +3 -0
  39. package/build/cli/types.js +1 -1
  40. package/build/core/audit/candidate.js +48 -0
  41. package/build/core/audit/coordinates.js +180 -0
  42. package/build/core/audit/evidence.js +158 -0
  43. package/build/core/audit/facade.js +42 -0
  44. package/build/core/audit/readiness.js +4 -0
  45. package/build/core/audit/report.js +38 -0
  46. package/build/core/audit.js +1 -650
  47. package/build/core/bind.js +111 -6
  48. package/build/core/draft.js +1 -1
  49. package/build/core/projection/generation/database.js +21 -2
  50. package/build/core/projection/generation/model.js +24 -3
  51. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  52. package/build/core/projection/generation/projection-generation-execution.js +4 -3
  53. package/build/core/projection/generation/projection-generation-runner.js +96 -53
  54. package/build/core/projection/generation/projection-generation-runtime.js +19 -0
  55. package/build/core/projection/generation/store.js +9 -0
  56. package/build/core/projection/generation/transitions.js +105 -3
  57. package/build/core/projection/index.js +2 -2
  58. package/build/core/projection/projection-core.js +1 -1
  59. package/build/core/projection/projection-kill.js +31 -0
  60. package/build/core/projection/projection-life-protocol.js +10 -0
  61. package/build/core/projection/projection-wait.js +53 -15
  62. package/build/core/projection/projection-wake.js +35 -3
  63. package/build/core/projection/tell/database.js +18 -0
  64. package/build/core/projection/tell/model.js +1 -0
  65. package/build/core/projection/tell/store.js +102 -55
  66. package/build/core/registry.js +82 -69
  67. package/build/core/scope.js +9 -9
  68. package/build/core/task/index.js +2 -2
  69. package/build/core/task/query.js +65 -23
  70. package/build/core/task/task-contract.js +18 -0
  71. package/build/core/task/task-git-store.js +50 -0
  72. package/build/generated/version.js +2 -2
  73. package/package.json +1 -1
  74. package/skills/keiyaku-akuma/SKILL.md +10 -0
  75. package/skills/keiyaku-workflow/SKILL.md +82 -7
@@ -57,9 +57,6 @@ function prepareBindInput(stdin, flags) {
57
57
  checks: flags.bindChecks,
58
58
  });
59
59
  }
60
- if (flags.dryRun) {
61
- throw new FlowError("EMPTY_PARAM", "--dry-run requires --place, --objective, --scope, and --checks");
62
- }
63
60
  if (!nonblankStdin) {
64
61
  // --task may supply name/objective defaults; core merges and reports missing sections.
65
62
  if (flags.bindTaskIds !== undefined && flags.bindTaskIds.length > 0) {
@@ -188,11 +185,17 @@ function selectedArgument(parsed) {
188
185
  if (stdin.selector === undefined)
189
186
  return undefined;
190
187
  if (stdin.selector.kind === "positional") {
191
- return { token: parsed.positional[stdin.selector.index], replace: () => parsed.flags };
188
+ const token = parsed.positional[stdin.selector.index];
189
+ if (token === undefined && !stdin.selector.required)
190
+ return undefined;
191
+ return { token, replace: () => parsed.flags };
192
192
  }
193
193
  const flagName = stdin.selector.name;
194
+ const token = parsed.flags[flagName];
195
+ if (token === undefined && !stdin.selector.required)
196
+ return undefined;
194
197
  return {
195
- token: parsed.flags[flagName],
198
+ token,
196
199
  replace: (value) => ({ ...parsed.flags, [flagName]: value }),
197
200
  };
198
201
  }
@@ -248,7 +251,8 @@ export async function runCliCommand(tokens) {
248
251
  await printFailure(error, cwd);
249
252
  return 1;
250
253
  }
251
- // Bind dry-run must compile and return before repository/cwd resolution or filesystem effects.
254
+ // Bind input compilation is side-effect free. Dry-run then performs the same
255
+ // repository admission as bind but does not persist an artifact on failure.
252
256
  if (parsed.command === "bind") {
253
257
  try {
254
258
  stdin = prepareBindInput(stdin, parsed.flags);
@@ -54,6 +54,9 @@ export function assignFlag(flags, name, value) {
54
54
  case "detach":
55
55
  flags.detach = Boolean(value);
56
56
  return;
57
+ case "interrupt":
58
+ flags.interrupt = Boolean(value);
59
+ return;
57
60
  case "draft-only":
58
61
  flags.draftOnly = Boolean(value);
59
62
  return;
@@ -102,6 +105,9 @@ export function assignFlag(flags, name, value) {
102
105
  case "append-scope":
103
106
  flags.appendScope = [...(flags.appendScope ?? []), String(value)];
104
107
  return;
108
+ case "renew":
109
+ flags.renewBeforePetition = Boolean(value);
110
+ return;
105
111
  case "resolve":
106
112
  flags.renewResolve = Boolean(value);
107
113
  return;
@@ -83,7 +83,7 @@ export function commandPositionalRange(command) {
83
83
  if (command === "task add") {
84
84
  return { min: 1, max: 1, label: "<BODY|->" };
85
85
  }
86
- if (command === "task show" || command === "task start" || command === "task drop") {
86
+ if (command === "task show" || command === "task log" || command === "task start" || command === "task drop") {
87
87
  return { min: 1, max: 1, label: "<ID>" };
88
88
  }
89
89
  if (command === "task update") {
@@ -1,4 +1,4 @@
1
- import { asMessage, isFlowError, pickHintsFromError } from "../../flow-error.js";
1
+ import { asMessage, isFlowError, pickHintsFromError, } from "../../flow-error.js";
2
2
  import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
3
3
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
4
4
  import { loadAkumaCatalog } from "../../config/akuma-loader.js";
@@ -6,6 +6,20 @@ import { appendDebugLog } from "../../telemetry/debug-log.js";
6
6
  import { ACTOR_IDENTITY, TOOL_NAMES, BUILTIN_AGENT_NAMES } from "../../keiyaku.js";
7
7
  import { buildErrorResponse } from "./misc.js";
8
8
  import { addressCarrierFromError } from "../../core/addressing.js";
9
+ import { shellQuote } from "./format.js";
10
+ const TASK_FILTER_GRAMMAR_EXAMPLE = "title~audit,scope";
11
+ function formatTaskFilterExpression(terms) {
12
+ return terms.map((term) => `${term.field}${term.operator}${term.values.join(",")}`).join("|");
13
+ }
14
+ function renderInvalidTaskFilterMessage(facts) {
15
+ const guidance = facts.recoveredTerms.length === 0
16
+ ? `Grammar example: keiyaku task ls --filter ${shellQuote(TASK_FILTER_GRAMMAR_EXAMPLE)}`
17
+ : `Paste-ready equivalent for parsed terms: keiyaku task ls --filter ${shellQuote(formatTaskFilterExpression(facts.recoveredTerms))}`;
18
+ return `invalid task filter: ${facts.detail}. Valid fields: ${facts.fields.join(", ")}. `
19
+ + `Valid operators: ${facts.operators.join(", ")}. `
20
+ + `Repeat --filter for AND; use commas for same-field OR or | between complete filters. `
21
+ + guidance;
22
+ }
9
23
  function renderScopeViolation(evidence) {
10
24
  if (evidence.outcome === "no-positive-match") {
11
25
  return ` ${evidence.path}\n no positive pattern matched`;
@@ -35,6 +49,9 @@ export function renderScopeAuditMessage(scope, evidence) {
35
49
  ].join("\n");
36
50
  }
37
51
  function renderErrorMessage(error) {
52
+ if (isFlowError(error) && error.facts?.kind === "invalid_task_filter") {
53
+ return renderInvalidTaskFilterMessage(error.facts);
54
+ }
38
55
  if (isFlowError(error) && error.facts?.kind === "scope_violation") {
39
56
  return renderScopeAuditMessage(error.facts.scope, error.facts.violations);
40
57
  }
@@ -1,10 +1,13 @@
1
1
  import { getConfig } from "../../config/env.js";
2
2
  import { SECTION_ICONS } from "./response-style.js";
3
- /** Quote a shell argument only when the token is not already a single safe word. */
3
+ // Command-copy output is POSIX shell syntax: close the quote, emit a literal
4
+ // apostrophe, then reopen the quote. Other shell dialects are not supported.
5
+ const POSIX_SINGLE_QUOTE_ESCAPE = "'\\''";
6
+ /** Quote a POSIX shell argument only when the token is not already a single safe word. */
4
7
  export function shellQuote(value) {
5
8
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
6
9
  return value;
7
- return `'${value.replace(/'/g, "'\\''")}'`;
10
+ return `'${value.replace(/'/g, POSIX_SINGLE_QUOTE_ESCAPE)}'`;
8
11
  }
9
12
  export function formatAgeMs(ms) {
10
13
  if (ms === undefined)
@@ -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,88 @@
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;
21
+ }
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;
39
+ }
40
+ function safePatternComponents(value) {
41
+ return safeSlashComponents(value, SAFE_PATTERN_COMPONENT);
42
+ }
43
+ function safeConcretePathComponents(value) {
44
+ return safeSlashComponents(value, SAFE_CONCRETE_PATH_COMPONENT);
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;
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;
65
+ }
66
+ current.terminal = true;
67
+ }
68
+ function hasTerminalBranch(node) {
69
+ if (node.terminal && node.children.size > 0)
70
+ return true;
71
+ return [...node.children.values()].some(hasTerminalBranch);
72
+ }
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));
77
+ }
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(",")}}`;
85
+ }
86
+ function savesEnough(literal, compacted) {
87
+ return literal.length - compacted.length >= MIN_COMPACTION_SAVINGS;
88
+ }
@@ -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,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";