@astrosheep/keiyaku 1.0.0 → 1.0.2

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 (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -1,5 +1,11 @@
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. */
4
+ export function shellQuote(value) {
5
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
6
+ return value;
7
+ return `'${value.replace(/'/g, "'\\''")}'`;
8
+ }
3
9
  export const DISPLAY_TEXT_MAX_CHARS = 5000;
4
10
  export const FORMAT_MAYBE_MAX_CHARS = 1200;
5
11
  export const FORMAT_LIST_MAX_ITEMS = 20;
@@ -0,0 +1,70 @@
1
+ export const NON_TTY_LINE_COLUMNS = 120;
2
+ function isCombining(codePoint) {
3
+ return (codePoint >= 0x0300 && codePoint <= 0x036f)
4
+ || (codePoint >= 0x1ab0 && codePoint <= 0x1aff)
5
+ || (codePoint >= 0x1dc0 && codePoint <= 0x1dff)
6
+ || (codePoint >= 0x20d0 && codePoint <= 0x20ff)
7
+ || (codePoint >= 0xfe20 && codePoint <= 0xfe2f)
8
+ || codePoint === 0xfe0f;
9
+ }
10
+ function isWide(codePoint) {
11
+ return codePoint >= 0x1100 && (codePoint <= 0x115f
12
+ || codePoint === 0x2329
13
+ || codePoint === 0x232a
14
+ || (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
15
+ || (codePoint >= 0xac00 && codePoint <= 0xd7a3)
16
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
17
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
18
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
19
+ || (codePoint >= 0xff00 && codePoint <= 0xff60)
20
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
21
+ || (codePoint >= 0x1f300 && codePoint <= 0x1faff)
22
+ || (codePoint >= 0x20000 && codePoint <= 0x3fffd));
23
+ }
24
+ function characterColumns(character) {
25
+ const codePoint = character.codePointAt(0);
26
+ if (codePoint === undefined || codePoint === 0 || isCombining(codePoint))
27
+ return 0;
28
+ if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0))
29
+ return 0;
30
+ return isWide(codePoint) ? 2 : 1;
31
+ }
32
+ export function displayColumns(value) {
33
+ let width = 0;
34
+ for (const character of value)
35
+ width += characterColumns(character);
36
+ return width;
37
+ }
38
+ export function truncateColumns(value, maxColumns) {
39
+ if (maxColumns <= 0)
40
+ return "";
41
+ if (displayColumns(value) <= maxColumns)
42
+ return value;
43
+ if (maxColumns === 1)
44
+ return "…";
45
+ const budget = maxColumns - 1;
46
+ let width = 0;
47
+ let output = "";
48
+ for (const character of value) {
49
+ const next = characterColumns(character);
50
+ if (width + next > budget)
51
+ break;
52
+ output += character;
53
+ width += next;
54
+ }
55
+ return `${output}…`;
56
+ }
57
+ export function resolveLineColumns(stream = process.stdout) {
58
+ return stream.isTTY === true && Number.isInteger(stream.columns) && stream.columns > 0
59
+ ? stream.columns
60
+ : NON_TTY_LINE_COLUMNS;
61
+ }
62
+ /** Preserve the fixed prefix and spend the remaining terminal columns on the variable tail. */
63
+ export function fitVariableLine(fixedPrefix, variableTail, maxColumns) {
64
+ return `${fixedPrefix}${truncateColumns(variableTail, Math.max(0, maxColumns - displayColumns(fixedPrefix)))}`;
65
+ }
66
+ /** Preserve both decision-bearing affixes and truncate only the descriptive middle. */
67
+ export function fitAffixedLine(fixedPrefix, variableMiddle, fixedSuffix, maxColumns) {
68
+ const middleBudget = Math.max(0, maxColumns - displayColumns(fixedPrefix) - displayColumns(fixedSuffix));
69
+ return `${fixedPrefix}${truncateColumns(variableMiddle, middleBudget)}${fixedSuffix}`;
70
+ }
@@ -1,6 +1,7 @@
1
1
  import { assembleResponse, buildPayload, buildSection, colorizeErrorStatus, FORMAT_LIST_MAX_ITEM_CHARS, formatHints, } from "./format.js";
2
2
  import { RESPONSE_MARKERS } from "./response-style.js";
3
3
  import { formatReview, HINTS_LIST_MAX_ITEMS, REVIEW_SECTION_TITLE, toReviewPayload, } from "./shared.js";
4
+ import { prependAddressReceipt, renderAddressCarrier } from "./address.js";
4
5
  export function buildErrorResponse(input) {
5
6
  const shouldRaiseProtocolError = input.errorType === "runtime_error";
6
7
  const text = buildErrorText(input);
@@ -24,7 +25,8 @@ function buildErrorText(input) {
24
25
  const errorSection = buildSection("Error", input.message);
25
26
  const reviewSection = input.review ? buildSection(REVIEW_SECTION_TITLE, formatReview(input.review)) : null;
26
27
  const hintsSection = buildSection("Hints", formatHints(input.hints, { maxItems: HINTS_LIST_MAX_ITEMS, maxItemChars: FORMAT_LIST_MAX_ITEM_CHARS }));
27
- return assembleResponse(colorizeErrorStatus(`${RESPONSE_MARKERS.failure} Failed [${input.errorCode}]`), "Request could not be completed.", [errorSection, reviewSection, hintsSection].filter((section) => section !== null));
28
+ const body = assembleResponse(colorizeErrorStatus(`${RESPONSE_MARKERS.failure} Failed [${input.errorCode}]`), "Request could not be completed.", [errorSection, reviewSection, hintsSection].filter((section) => section !== null));
29
+ return input.addressCarrier ? `${renderAddressCarrier(input.addressCarrier)}\n${body}` : body;
28
30
  }
29
31
  export function buildHelpResponse(input) {
30
32
  return {
@@ -33,8 +35,9 @@ export function buildHelpResponse(input) {
33
35
  };
34
36
  }
35
37
  export function buildLogResponse(result) {
36
- return {
38
+ const { address: _address, ...resultData } = result;
39
+ return prependAddressReceipt({
37
40
  text: result.markdown,
38
- payload: buildPayload(result),
39
- };
41
+ payload: buildPayload(resultData),
42
+ }, result.address);
40
43
  }
@@ -2,8 +2,9 @@ import { shortCommitHash } from "../../core/render.js";
2
2
  import { assembleResponse, buildPayload, buildSection, formatMaybe, formatWarnings, } from "./format.js";
3
3
  import { DIFF_OVERVIEW_SECTION_TITLE, RESPONSE_MARKERS, WARNING_SECTION_TITLE } from "./response-style.js";
4
4
  import { BRANCH_LABEL, INFO_BRANCH_MAX_CHARS, INFO_COMMIT_MAX_CHARS, INFO_PLEA_MAX_CHARS, } from "./shared.js";
5
+ import { prependAddressReceipt } from "./address.js";
5
6
  export function buildPetitionClaimResponse(result, input) {
6
- const { status: _status, ...resultData } = result;
7
+ const { status: _status, address: _address, ...resultData } = result;
7
8
  const renderedCommit = result.commit ? shortCommitHash(result.commit) : undefined;
8
9
  const commitSuffix = renderedCommit ? ` [${renderedCommit}]` : "";
9
10
  const summary = result.deletedBranch
@@ -25,8 +26,8 @@ export function buildPetitionClaimResponse(result, input) {
25
26
  ...formatMaybe("Oath", input.oath, INFO_PLEA_MAX_CHARS),
26
27
  ];
27
28
  const text = assembleResponse(`${RESPONSE_MARKERS.claim} Keiyaku Fulfilled (Claim)`, summary, [ledgerSection, diffSection, warningSection].filter((section) => section !== null), infoLines);
28
- return {
29
+ return prependAddressReceipt({
29
30
  text,
30
31
  payload: buildPayload(resultData),
31
- };
32
+ }, result.address);
32
33
  }
@@ -0,0 +1,201 @@
1
+ import { compactText } from "./compact-text.js";
2
+ import { fitAffixedLine, fitVariableLine, resolveLineColumns } from "./line-width.js";
3
+ import { presentFoldedToolActivity, } from "./tool-presentation.js";
4
+ import { presentLedgerPath, summarizeToolLedger } from "./tool-ledger-rollup.js";
5
+ const TIME_GUTTER_WIDTH = 6;
6
+ const VERB_WIDTH = 7;
7
+ const SILENCE_THRESHOLD_MS = 60_000;
8
+ export function formatTimelineDuration(ms) {
9
+ const totalSeconds = Math.max(0, Math.floor(ms / 1_000));
10
+ if (totalSeconds < 60)
11
+ return `${totalSeconds}s`;
12
+ const totalMinutes = Math.floor(totalSeconds / 60);
13
+ const seconds = totalSeconds % 60;
14
+ if (totalMinutes < 60)
15
+ return `${totalMinutes}m${String(seconds).padStart(2, "0")}s`;
16
+ const hours = Math.floor(totalMinutes / 60);
17
+ const minutes = totalMinutes % 60;
18
+ if (hours < 24)
19
+ return `${hours}h${String(minutes).padStart(2, "0")}m`;
20
+ const days = Math.floor(hours / 24);
21
+ return `${days}d${String(hours % 24).padStart(2, "0")}h`;
22
+ }
23
+ function formatActionDuration(ms) {
24
+ if (ms === undefined)
25
+ return undefined;
26
+ if (ms < 10_000 && ms % 1_000 !== 0)
27
+ return `${(ms / 1_000).toFixed(1)}s`;
28
+ return formatTimelineDuration(ms);
29
+ }
30
+ function elapsedLabel(anchorMs, at) {
31
+ const eventMs = Date.parse(at);
32
+ if (!Number.isFinite(eventMs))
33
+ return "now";
34
+ return formatTimelineDuration(Math.max(0, anchorMs - eventMs));
35
+ }
36
+ function compactTokens(value) {
37
+ if (value < 1_000)
38
+ return String(value);
39
+ const compact = (value / 1_000).toFixed(value < 10_000 ? 1 : 0).replace(/\.0$/, "");
40
+ return `${compact}k`;
41
+ }
42
+ function warningText(row) {
43
+ const warning = row.warning;
44
+ const modelRoute = warning.originalModel && warning.fallbackModel
45
+ ? ` ${warning.originalModel}→${warning.fallbackModel}`
46
+ : warning.direction
47
+ ? ` ${warning.direction}`
48
+ : "";
49
+ return `provider ${warning.reason}${modelRoute}${row.count > 1 ? ` (${row.count})` : ""}`;
50
+ }
51
+ function renderPlan(plan, maxColumns) {
52
+ const width = 5;
53
+ const filled = plan.total === 0 ? 0 : Math.min(width, Math.round((plan.done / plan.total) * width));
54
+ const bar = `${"▰".repeat(filled)}${"▱".repeat(width - filled)}`;
55
+ const fixed = `${"now".padStart(TIME_GUTTER_WIDTH)}│ ${"plan".padEnd(VERB_WIDTH)} ${bar} ${plan.done}/${plan.total}`;
56
+ return fitVariableLine(fixed, plan.activeItem ? ` → ${compactText(plan.activeItem)}` : "", maxColumns);
57
+ }
58
+ function rollupCountLabel(semanticClass) {
59
+ return semanticClass === "run" ? "ran" : semanticClass;
60
+ }
61
+ function summarizeProjectionToolLedger(activity, workspaceRoot) {
62
+ return summarizeToolLedger(activity.ledgerActivities, activity.ledgerTurns, (value) => presentLedgerPath(value, workspaceRoot));
63
+ }
64
+ function toolCountSegments(summary) {
65
+ return summary.counts.map(({ semanticClass, count }) => `${count} ${rollupCountLabel(semanticClass)}`);
66
+ }
67
+ export function renderProjectionActivitySummary(activity, input) {
68
+ const summary = summarizeProjectionToolLedger(activity, input.workspaceRoot);
69
+ const segments = toolCountSegments(summary);
70
+ if (segments.length === 0)
71
+ return undefined;
72
+ const spine = input.state === "dead" || input.state === "lost" ? "┆" : "│";
73
+ return fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}${spine} `, segments.join(" · "), input.maxColumns ?? resolveLineColumns());
74
+ }
75
+ function renderTerminalRollup(activity, input) {
76
+ const summary = summarizeProjectionToolLedger(activity, input.workspaceRoot);
77
+ const segments = [
78
+ ...toolCountSegments(summary),
79
+ ...(summary.tokens === undefined ? [] : [`${compactTokens(summary.tokens)} tok`]),
80
+ ...(summary.costUsd === undefined ? [] : [`$${summary.costUsd.toFixed(2)}`]),
81
+ ];
82
+ const lines = input.diagnostic
83
+ ? [fitVariableLine(`${elapsedLabel(input.anchorMs, input.diagnostic.at).padStart(TIME_GUTTER_WIDTH)}┆ ${"warning".padEnd(VERB_WIDTH)} `, compactText(input.diagnostic.text), input.maxColumns)]
84
+ : [];
85
+ const endPrefix = `${"end".padStart(TIME_GUTTER_WIDTH)}└`;
86
+ lines.push(segments.length > 0
87
+ ? fitVariableLine(`${endPrefix} `, segments.join(" · "), input.maxColumns)
88
+ : endPrefix);
89
+ for (const file of summary.files) {
90
+ const repeat = file.count > 1 ? ` ×${file.count}` : "";
91
+ const diffstat = file.diffstat ? ` +${file.diffstat.additions} −${file.diffstat.deletions}` : "";
92
+ lines.push(fitVariableLine("", `${file.path}${repeat}${diffstat}`, input.maxColumns));
93
+ }
94
+ if (summary.hiddenFileCount > 0) {
95
+ lines.push(fitVariableLine("", `… ${summary.hiddenFileCount} more files`, input.maxColumns));
96
+ }
97
+ return { lines, hasMutationFiles: summary.files.length > 0 };
98
+ }
99
+ function timelineEntries(activity) {
100
+ const entries = [];
101
+ for (const row of activity.timeline) {
102
+ if (row.kind === "ran") {
103
+ for (const observation of row.observations) {
104
+ if (observation.order !== row.order)
105
+ entries.push({ kind: "clock", order: observation.order, at: observation.at });
106
+ }
107
+ }
108
+ entries.push({ kind: "row", order: row.order, at: row.at, row });
109
+ }
110
+ return entries.sort((left, right) => left.order - right.order);
111
+ }
112
+ export function renderProjectionActivity(activity, input) {
113
+ const lines = [];
114
+ let priorAtMs;
115
+ let hasEndAnchor = false;
116
+ let lastRunningTool;
117
+ const maxColumns = input.maxColumns ?? resolveLineColumns();
118
+ const entries = timelineEntries(activity);
119
+ for (let index = 0; index < entries.length; index += 1) {
120
+ const entry = entries[index];
121
+ const atMs = Date.parse(entry.at);
122
+ if (entry.kind === "clock") {
123
+ if (Number.isFinite(atMs))
124
+ priorAtMs = atMs;
125
+ continue;
126
+ }
127
+ if (priorAtMs !== undefined && Number.isFinite(atMs) && atMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
128
+ lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(atMs - priorAtMs)} pass in silence`, maxColumns));
129
+ }
130
+ if (Number.isFinite(atMs))
131
+ priorAtMs = atMs;
132
+ const row = entry.row;
133
+ const label = row.kind === "turn" ? "" : elapsedLabel(input.anchorMs, row.at);
134
+ const spine = row.kind === "turn"
135
+ ? "├"
136
+ : input.state === "dead" || input.state === "lost"
137
+ ? "┆"
138
+ : "│";
139
+ if (row.kind === "ran") {
140
+ const duration = formatActionDuration(row.durationMs);
141
+ const activityState = row.state === "completed"
142
+ ? "completed"
143
+ : input.state === "dead" || input.state === "lost"
144
+ ? "interrupted"
145
+ : input.state === "stalled" || input.state === "unknown"
146
+ ? "unresolved"
147
+ : "running";
148
+ const presentation = presentFoldedToolActivity(row, { state: activityState, ...(duration ? { duration } : {}) }, (value) => presentLedgerPath(value, input.workspaceRoot) ?? value);
149
+ const fixed = `${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${(presentation.semanticClass === "run" ? "ran" : presentation.semanticClass).padEnd(VERB_WIDTH)} `;
150
+ lines.push(fitAffixedLine(fixed, presentation.variableText, presentation.fixedSuffix, maxColumns));
151
+ continue;
152
+ }
153
+ if (row.kind === "said") {
154
+ lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"said".padEnd(VERB_WIDTH)} `, `“${compactText(row.text)}”`, maxColumns));
155
+ }
156
+ else if (row.kind === "thought") {
157
+ const text = `${compactText(row.text)}${row.truncated && !row.text.endsWith("…") ? "…" : ""}`;
158
+ const duration = formatActionDuration(row.durationMs);
159
+ lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"thought".padEnd(VERB_WIDTH)} `, `${duration ? `${duration} — ` : ""}“${text}”`, maxColumns));
160
+ }
161
+ else if (row.kind === "warning") {
162
+ lines.push(fitVariableLine(`${label.padStart(TIME_GUTTER_WIDTH)}${spine} ${"warning".padEnd(VERB_WIDTH)} `, warningText(row), maxColumns));
163
+ }
164
+ else {
165
+ const usage = row.inputTokens !== undefined && row.outputTokens !== undefined
166
+ ? ` · ${compactTokens(row.inputTokens)}▸${compactTokens(row.outputTokens)} tok`
167
+ : "";
168
+ const cost = row.costUsd !== undefined ? ` · $${row.costUsd.toFixed(2)}` : "";
169
+ lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}├ `, `turn ${row.turn} sealed${usage}${cost}`, maxColumns));
170
+ }
171
+ }
172
+ for (const row of activity.pinned) {
173
+ if (row.kind === "ran" && row.state === "running")
174
+ lastRunningTool = compactText(row.tool);
175
+ }
176
+ if (input.showPlan && activity.plan) {
177
+ const planAtMs = Date.parse(activity.plan.at);
178
+ if (priorAtMs !== undefined && Number.isFinite(planAtMs) && planAtMs - priorAtMs >= SILENCE_THRESHOLD_MS) {
179
+ lines.push(fitVariableLine(`${"".padStart(TIME_GUTTER_WIDTH)}┆ `, `${formatTimelineDuration(planAtMs - priorAtMs)} pass in silence`, maxColumns));
180
+ }
181
+ lines.push(renderPlan(activity.plan, maxColumns));
182
+ }
183
+ let hasMutationFiles = false;
184
+ if (["returned", "dead", "lost", "dismissed"].includes(input.state)) {
185
+ const rollup = renderTerminalRollup(activity, {
186
+ anchorMs: input.anchorMs,
187
+ ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
188
+ ...(input.terminalDiagnostic ? { diagnostic: input.terminalDiagnostic } : {}),
189
+ maxColumns,
190
+ });
191
+ lines.push(...rollup.lines);
192
+ hasEndAnchor = true;
193
+ hasMutationFiles = rollup.hasMutationFiles;
194
+ }
195
+ return {
196
+ lines,
197
+ ...(lastRunningTool ? { lastRunningTool } : {}),
198
+ hasEndAnchor,
199
+ hasMutationFiles,
200
+ };
201
+ }
@@ -1,8 +1,9 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs";
3
- import { ACTOR_IDENTITY, KEIYAKU_FILE } from "../../keiyaku.js";
4
- import { assembleResponse, buildSection, DISPLAY_TEXT_MAX_CHARS, FORMAT_LIST_MAX_ITEM_CHARS, FORMAT_LIST_MAX_ITEMS, formatMaybe, formatWarnings, truncateForDisplay, } from "./format.js";
5
- import { RESPONSE_MARKERS, WARNING_SECTION_TITLE } from "./response-style.js";
3
+ import { KEIYAKU_FILE } from "../../keiyaku.js";
4
+ import { assembleResponse, buildSection, DISPLAY_TEXT_MAX_CHARS, FORMAT_LIST_MAX_ITEM_CHARS, FORMAT_LIST_MAX_ITEMS, formatMaybe, formatWarnings, shellQuote, truncateForDisplay, } from "./format.js";
5
+ import { WARNING_SECTION_TITLE } from "./response-style.js";
6
+ import { prependAddressReceipt } from "./address.js";
6
7
  export function textResponse(text) {
7
8
  return {
8
9
  text,
@@ -41,6 +42,7 @@ export function buildBindResponse(result, cwd) {
41
42
  });
42
43
  }
43
44
  export function buildForfeitResponse(result) {
45
+ const { address: _address, ...resultData } = result;
44
46
  const base = `Forfeited ${result.contractId}.`;
45
47
  const warnings = [
46
48
  ...(result.cleanup.worktreeRemoveFailure
@@ -55,10 +57,10 @@ export function buildForfeitResponse(result) {
55
57
  : []),
56
58
  ];
57
59
  if (warnings.length === 0) {
58
- return coreTextResponse(base, result);
60
+ return prependAddressReceipt(coreTextResponse(base, resultData), result.address);
59
61
  }
60
62
  const warningSection = buildSection(WARNING_SECTION_TITLE, formatWarnings(warnings));
61
- return coreTextResponse(assembleResponse(base, "", [warningSection].filter((section) => section !== null)), result);
63
+ return prependAddressReceipt(coreTextResponse(assembleResponse(base, "", [warningSection].filter((section) => section !== null)), resultData), result.address);
62
64
  }
63
65
  export const RULES_SECTION_MAX_CHARS = 5000;
64
66
  export const BRANCH_LABEL = "Workspace";
@@ -71,10 +73,8 @@ export const STATUS_COMMANDS_SECTION_TITLE = "Commands";
71
73
  export const CONTENT_SECTION_TITLE = "Content";
72
74
  export const STATUS_ACTIVE_SUMMARY_PREFIX = "● Active";
73
75
  export const STATUS_INACTIVE_SUMMARY = "○ Inactive";
74
- export const SUMMON_RESPONSE_HEADING = `${RESPONSE_MARKERS.arc} Summoned`;
75
76
  export const SAVED_TO_PREFIX = "Saved to";
76
- export const SUMMON_RESPONSE_PATH_HINT_PREFIX = `Run \`keiyaku resume <response-path>\` with this path to summon the same ${ACTOR_IDENTITY} again.`;
77
- export const INFO_AGENT_MAX_CHARS = 120;
77
+ export const INFO_AKUMA_MAX_CHARS = 120;
78
78
  export const INFO_FROM_DRAFT_MAX_CHARS = 300;
79
79
  export const EXISTING_BRANCHES_MAX_ITEMS = FORMAT_LIST_MAX_ITEMS;
80
80
  export const INFO_BRANCH_MAX_CHARS = 200;
@@ -108,10 +108,44 @@ export function formatPersistenceSummary(responsePath) {
108
108
  return "";
109
109
  return `${SAVED_TO_PREFIX} ${responsePath}.`;
110
110
  }
111
- export function formatSummonPersistenceSummary(responsePath) {
112
- if (!responsePath)
113
- return "";
114
- return `${formatPersistenceSummary(responsePath)} ${SUMMON_RESPONSE_PATH_HINT_PREFIX}`;
111
+ export function formatCallCompletionHeading(projection) {
112
+ switch (projection?.phase) {
113
+ case "active":
114
+ case "quiescent": return "still out there";
115
+ case "done": return "returned";
116
+ case "failed": return "dead";
117
+ case "dismissed": return "dismissed";
118
+ case "lost": return undefined;
119
+ default: return "no record of this akuma";
120
+ }
121
+ }
122
+ /** Machine-generated durable revive command: artifactId + explicit repository coordinate. */
123
+ export function formatDurableReviveCommand(input) {
124
+ return input.repositoryCoordinate
125
+ ? `keiyaku revive ${input.artifactId} --repo ${shellQuote(input.repositoryCoordinate)}`
126
+ : `keiyaku revive ${input.artifactId}`;
127
+ }
128
+ export function projectionRejoinHintLines(projectionId) {
129
+ return [
130
+ "Come back for the result:",
131
+ ` » keiyaku wait ${projectionId}`,
132
+ "Steer it while it works:",
133
+ ` » keiyaku tell ${projectionId} "your words"`,
134
+ ];
135
+ }
136
+ export function formatCallNextAction(input) {
137
+ if (input.projection?.phase === "active") {
138
+ return projectionRejoinHintLines(input.projection.id).join("\n");
139
+ }
140
+ if (input.projection?.phase === "done") {
141
+ return [
142
+ "The akuma is back.",
143
+ ...(input.artifactId ? [`record: ${input.artifactId}`] : []),
144
+ "talk to it:",
145
+ ` » keiyaku tell ${input.projection.id} "your words"`,
146
+ ].join("\n");
147
+ }
148
+ return "⟶ keiyaku status";
115
149
  }
116
150
  export function appendPersistenceSummary(base, responsePath) {
117
151
  const persistence = formatPersistenceSummary(responsePath);
@@ -1,7 +1,8 @@
1
1
  import { formatSettingsDiseases } from "../../config/settings.js";
2
2
  import { formatAgeMs, projectionGlyph, renderKanshiBoard, shortCommitHash } from "../../core/render.js";
3
+ import { renderProjectionActivitySummary, } from "./projection-activity.js";
3
4
  export const STATUS_DISEASES_SECTION_TITLE = "Diseases";
4
- export const STATUS_PROJECTION_SECTION_TITLE = "projection";
5
+ export const STATUS_PROJECTION_SECTION_TITLE = "akuma";
5
6
  export const STATUS_CLAIM_RECEIPTS_SECTION_TITLE = "claim receipts";
6
7
  export function renderSettingsDiseasesSection(diseases) {
7
8
  if (diseases.length === 0)
@@ -21,22 +22,67 @@ function renderProjectionAge(row) {
21
22
  return "dismissed";
22
23
  if (row.state === "failed")
23
24
  return "failed";
24
- return formatAgeMs(row.heartbeatAgeMs) ?? "lost";
25
+ if (row.state === "startup-timeout")
26
+ return "startup timed out";
27
+ if (row.state === "unknown")
28
+ return "unknown";
29
+ return formatAgeMs(row.activityAgeMs) ?? "";
30
+ }
31
+ function renderProjectionTellCounts(row) {
32
+ const { inbox, inflight, submitted, consumed } = row.tellCounts;
33
+ if (inbox === 0 && inflight === 0 && submitted === 0 && consumed === 0)
34
+ return "";
35
+ return ` tell ${inbox}▸${inflight}▸${submitted}▸${consumed}`;
36
+ }
37
+ function projectionActivityState(row) {
38
+ switch (row.state) {
39
+ case "done": return "returned";
40
+ case "failed":
41
+ case "dead": return "dead";
42
+ case "lost": return "lost";
43
+ case "dismissed": return "dismissed";
44
+ case "minting":
45
+ case "out": return "running";
46
+ case "startup-timeout": return "stalled";
47
+ case "unknown": return "unknown";
48
+ }
49
+ }
50
+ function renderProjectionActivityLine(row) {
51
+ if (row.state === "startup-timeout" || row.state === "unknown")
52
+ return undefined;
53
+ if (!row.activity)
54
+ return undefined;
55
+ return renderProjectionActivitySummary(row.activity, {
56
+ state: projectionActivityState(row),
57
+ ...(row.workspaceRoot ? { workspaceRoot: row.workspaceRoot } : {}),
58
+ });
25
59
  }
26
60
  function renderProjectionRow(row) {
27
- const glyph = projectionGlyph(row.state === "failed" ? "lost" : row.state);
28
- if (row.state === "minting") {
29
- return ` ${glyph} ${row.id.padEnd(18)} (minting)${renderProjectionDiagnostics(row)}`.trimEnd();
61
+ const glyph = projectionGlyph(row.state === "failed"
62
+ ? "lost"
63
+ : row.state === "startup-timeout"
64
+ ? "minting"
65
+ : row.state);
66
+ let heading;
67
+ if (row.state === "startup-timeout" || row.state === "unknown") {
68
+ const akuma = (row.akuma ?? "(unknown)").padEnd(18);
69
+ heading = ` ${glyph} ${row.id} ${akuma} ${renderProjectionAge(row)}`.trimEnd();
70
+ }
71
+ else if (row.state === "minting") {
72
+ heading = ` ${glyph} ${row.id.padEnd(18)} (minting)${renderProjectionDiagnostics(row)}`.trimEnd();
30
73
  }
31
- const age = renderProjectionAge(row).padEnd(9);
32
- const akuma = (row.akuma ?? "(unknown)").padEnd(18);
33
- const tell = `tell ${row.tellCounts.inbox}▸${row.tellCounts.inflight}▸${row.tellCounts.submitted}▸${row.tellCounts.consumed}`;
34
- return ` ${glyph} ${row.id} ${akuma} ${age} ${tell}${renderProjectionDiagnostics(row)}`.trimEnd();
74
+ else {
75
+ const age = renderProjectionAge(row).padEnd(9);
76
+ const akuma = (row.akuma ?? "(unknown)").padEnd(18);
77
+ heading = ` ${glyph} ${row.id} ${akuma} ${age}${renderProjectionTellCounts(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
78
+ }
79
+ const activity = renderProjectionActivityLine(row);
80
+ return activity ? [heading, activity] : [heading];
35
81
  }
36
82
  export function renderProjectionStatusSection(board) {
37
83
  if (!board || board.rows.length === 0)
38
84
  return "";
39
- return `${STATUS_PROJECTION_SECTION_TITLE}\n${board.rows.map(renderProjectionRow).join("\n")}\n`;
85
+ return `${STATUS_PROJECTION_SECTION_TITLE}\n${board.rows.flatMap(renderProjectionRow).join("\n")}\n`;
40
86
  }
41
87
  function renderClaimReconciliationSection(recon) {
42
88
  if (!recon)
@@ -59,6 +105,9 @@ export function renderStatusBoard(board) {
59
105
  if (!projectionText && !receiptsText && board.diseases.length === 0)
60
106
  return boardText;
61
107
  const sections = [boardText.trimEnd()];
108
+ if (board.targetFilter !== null) {
109
+ sections.push("repository-wide — not filtered by target");
110
+ }
62
111
  if (projectionText)
63
112
  sections.push(projectionText.trimEnd());
64
113
  if (receiptsText)
@@ -0,0 +1,74 @@
1
+ import * as path from "node:path";
2
+ import { activitySemanticClass } from "./tool-presentation.js";
3
+ const CLASS_ORDER = ["run", "read", "write", "edit", "search", "tool"];
4
+ export function presentLedgerPath(value, workspaceRoot) {
5
+ const candidate = value.trim();
6
+ if (!candidate)
7
+ return undefined;
8
+ if (!path.isAbsolute(candidate))
9
+ return candidate;
10
+ if (!workspaceRoot || !path.isAbsolute(workspaceRoot))
11
+ return candidate;
12
+ const relative = path.relative(workspaceRoot, candidate);
13
+ if (relative === "")
14
+ return ".";
15
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`))
16
+ return candidate;
17
+ return relative.split(path.sep).join("/");
18
+ }
19
+ export function summarizeToolLedger(activities, turns, presentPath) {
20
+ const counts = new Map();
21
+ const files = new Map();
22
+ for (const row of activities) {
23
+ const semanticClass = activitySemanticClass(row.call);
24
+ counts.set(semanticClass, (counts.get(semanticClass) ?? 0) + 1);
25
+ if (row.call.kind !== "fileChange")
26
+ continue;
27
+ const pathDiffstat = row.call.changes.length === 1 && (row.call.omittedChanges ?? 0) === 0
28
+ ? row.result?.diffstat
29
+ : undefined;
30
+ for (const change of row.call.changes) {
31
+ if (change.op === "delete")
32
+ continue;
33
+ const visible = presentPath(change.path);
34
+ if (!visible)
35
+ continue;
36
+ const fileClass = change.op === "add" ? "write" : "edit";
37
+ const prior = files.get(visible);
38
+ if (prior) {
39
+ prior.semanticClass = fileClass;
40
+ prior.count += 1;
41
+ prior.allHaveDiffstat &&= pathDiffstat !== undefined;
42
+ prior.additions += pathDiffstat?.additions ?? 0;
43
+ prior.deletions += pathDiffstat?.deletions ?? 0;
44
+ }
45
+ else {
46
+ files.set(visible, {
47
+ semanticClass: fileClass,
48
+ count: 1,
49
+ additions: pathDiffstat?.additions ?? 0,
50
+ deletions: pathDiffstat?.deletions ?? 0,
51
+ allHaveDiffstat: pathDiffstat !== undefined,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ const visibleFiles = [...files].map(([visiblePath, file]) => ({
57
+ semanticClass: file.semanticClass,
58
+ path: visiblePath,
59
+ count: file.count,
60
+ ...(file.allHaveDiffstat ? { diffstat: { additions: file.additions, deletions: file.deletions } } : {}),
61
+ }));
62
+ const tokenFacts = turns.filter((turn) => turn.inputTokens !== undefined && turn.outputTokens !== undefined);
63
+ const costFacts = turns.filter((turn) => turn.costUsd !== undefined);
64
+ return {
65
+ counts: CLASS_ORDER.flatMap((semanticClass) => {
66
+ const count = counts.get(semanticClass);
67
+ return count === undefined ? [] : [{ semanticClass, count }];
68
+ }),
69
+ files: visibleFiles.slice(0, 10),
70
+ hiddenFileCount: Math.max(0, visibleFiles.length - 10),
71
+ ...(tokenFacts.length ? { tokens: tokenFacts.reduce((sum, turn) => sum + turn.inputTokens + turn.outputTokens, 0) } : {}),
72
+ ...(costFacts.length ? { costUsd: costFacts.reduce((sum, turn) => sum + turn.costUsd, 0) } : {}),
73
+ };
74
+ }