@astrosheep/keiyaku 2.9.7 → 2.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  5. package/build/agents/providers/codex-app-server/session.js +8 -7
  6. package/build/agents/selector.js +12 -1
  7. package/build/cli/commands/akuma/view/handler.js +3 -11
  8. package/build/cli/commands/contract/amend/handler.js +1 -1
  9. package/build/cli/commands/contract/amend/meta.js +4 -4
  10. package/build/cli/commands/projection/status/handler.js +5 -4
  11. package/build/cli/commands/projection/status/meta.js +2 -2
  12. package/build/cli/commands/task/add/meta.js +9 -1
  13. package/build/cli/commands/task/shared.js +2 -1
  14. package/build/cli/completion.js +8 -0
  15. package/build/cli/render/kanshi.js +2 -2
  16. package/build/cli/render/path-prefix-compaction.js +119 -76
  17. package/build/cli/render/projection-activity.js +10 -1
  18. package/build/cli/render/shared.js +8 -7
  19. package/build/cli/render/status.js +8 -5
  20. package/build/cli/render/wait.js +1 -1
  21. package/build/config/settings/disease.js +4 -4
  22. package/build/config/settings/loader.js +44 -21
  23. package/build/core/addressing.js +40 -9
  24. package/build/core/amend.js +21 -5
  25. package/build/core/call/context.js +19 -3
  26. package/build/core/call/execution.js +43 -20
  27. package/build/core/ledger-batch.js +194 -0
  28. package/build/core/projection/generation/database.js +22 -0
  29. package/build/core/projection/generation/projection-generation-execution.js +45 -37
  30. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  31. package/build/core/projection/generation/projection-generation-process.js +3 -1
  32. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  33. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  34. package/build/core/projection/generation/store.js +17 -1
  35. package/build/core/projection/generation/transitions.js +89 -12
  36. package/build/core/projection/index.js +3 -3
  37. package/build/core/projection/projection-kill.js +22 -10
  38. package/build/core/projection/projection-runner-lock.js +177 -37
  39. package/build/core/projection/projection-status.js +143 -55
  40. package/build/core/projection/projection-wake.js +171 -72
  41. package/build/core/status/board.js +42 -4
  42. package/build/core/status/drift.js +21 -5
  43. package/build/core/status/ledger-batch.js +1 -158
  44. package/build/core/task/task-git-runtime.js +8 -10
  45. package/build/core/task/task-git-store.js +5 -3
  46. package/build/core/worktree-path.js +39 -25
  47. package/build/flow-error.js +1 -1
  48. package/build/generated/version.js +2 -2
  49. package/build/git/refs.js +47 -1
  50. package/package.json +1 -1
  51. package/skills/keiyaku-akuma/SKILL.md +18 -0
  52. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -1,4 +1,4 @@
1
- import { AGENT_ACTIVITY_MAX_CHANGES, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, } from "./events.js";
1
+ import { AGENT_ACTIVITY_MAX_CHANGES, AGENT_DONE_FINAL_MESSAGE_PERSISTENCE_PREVIEW_MAX_CHARS, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, } from "./events.js";
2
2
  import { EXEC_EVENT_BUFFER_HARD_CAP } from "./event-channel.js";
3
3
  export function createTruncationMarker(envelope = {
4
4
  seq: EXEC_EVENT_BUFFER_HARD_CAP,
@@ -23,10 +23,10 @@ export function overflowMarkerFromDropped(dropped) {
23
23
  turn: dropped.turn,
24
24
  });
25
25
  }
26
- function truncateText(value) {
26
+ function truncateText(value, limit = AGENT_EVENT_TEXT_MAX_CHARS) {
27
27
  return {
28
- value: value.slice(0, AGENT_EVENT_TEXT_MAX_CHARS),
29
- truncated: value.length > AGENT_EVENT_TEXT_MAX_CHARS,
28
+ value: value.slice(0, limit),
29
+ truncated: value.length > limit,
30
30
  };
31
31
  }
32
32
  /** Bound unknown native payloads before they enter the projection event stream. */
@@ -186,7 +186,9 @@ function normalizeBodyForPersistence(body) {
186
186
  return { body: { ...body, text: text.value }, truncated: text.truncated };
187
187
  }
188
188
  case "done": {
189
- const finalMessage = body.finalMessage === undefined ? undefined : truncateText(body.finalMessage);
189
+ const finalMessage = body.finalMessage === undefined
190
+ ? undefined
191
+ : truncateText(body.finalMessage, AGENT_DONE_FINAL_MESSAGE_PERSISTENCE_PREVIEW_MAX_CHARS);
190
192
  return {
191
193
  body: { ...body, ...(finalMessage ? { finalMessage: finalMessage.value } : {}) },
192
194
  truncated: finalMessage?.truncated ?? false,
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- export const AGENT_EVENT_TEXT_MAX_CHARS = 4_096;
2
+ export const AGENT_EVENT_TEXT_MAX_CHARS = 16_384;
3
+ export const AGENT_DONE_FINAL_MESSAGE_PERSISTENCE_PREVIEW_MAX_CHARS = 65_536;
3
4
  export const AGENT_THOUGHT_TEXT_MAX_CHARS = 4_000;
4
5
  export const AGENT_PLAN_MAX_ITEMS = 64;
5
6
  export const AGENT_PLAN_ITEM_TEXT_MAX_CHARS = 200;
6
7
  export const AGENT_ACTIVITY_MAX_CHANGES = 32;
7
- export const AGENT_UNKNOWN_PAYLOAD_MAX_CHARS = 4_096;
8
+ export const AGENT_UNKNOWN_PAYLOAD_MAX_CHARS = AGENT_EVENT_TEXT_MAX_CHARS;
8
9
  export const activityCallSchema = z.discriminatedUnion("kind", [
9
10
  z.object({ kind: z.literal("run"), command: z.string(), description: z.string().optional() }).strict(),
10
11
  z.object({
@@ -243,7 +243,12 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
243
243
  builderDiagnostic = (text) => builder.emit({ type: "diagnostic", text });
244
244
  emitTurnStarted = () => builder.emit({ type: "turn", phase: "started" });
245
245
  const env = buildSubagentEnv(options.env);
246
- server = new CodexAppServerProcess(executable, env, options.codexHome, options.onEvidence, (text) => builder.emit({ type: "diagnostic", text }));
246
+ server = new CodexAppServerProcess(executable, env, {
247
+ cwd,
248
+ codexHome: options.codexHome,
249
+ onEvidence: options.onEvidence,
250
+ onEvidenceDiagnostic: (text) => builder.emit({ type: "diagnostic", text }),
251
+ });
247
252
  const { turnCompleted } = createCodexAppServerTurnMonitor({
248
253
  server,
249
254
  builder,
@@ -3,8 +3,6 @@ import { emitProviderEvidence, sha256Utf8 } from "../../harness/runtime.js";
3
3
  import { coerceString } from "../../harness/runtime.js";
4
4
  import { CODEX_APP_SERVER_PROVIDER } from "./identity.js";
5
5
  export class CodexAppServerProcess {
6
- onEvidence;
7
- onEvidenceDiagnostic;
8
6
  child;
9
7
  pending = new Map();
10
8
  notificationListeners = new Set();
@@ -17,15 +15,18 @@ export class CodexAppServerProcess {
17
15
  gracefulClose;
18
16
  forceClose;
19
17
  codexHome;
20
- constructor(executable, env, codexHome, onEvidence, onEvidenceDiagnostic) {
21
- this.onEvidence = onEvidence;
22
- this.onEvidenceDiagnostic = onEvidenceDiagnostic;
23
- this.codexHome = codexHome;
18
+ onEvidence;
19
+ onEvidenceDiagnostic;
20
+ constructor(executable, env, options) {
21
+ this.codexHome = options.codexHome;
22
+ this.onEvidence = options.onEvidence;
23
+ this.onEvidenceDiagnostic = options.onEvidenceDiagnostic;
24
24
  const spawnEnv = {
25
25
  ...env,
26
- ...(codexHome ? { CODEX_HOME: codexHome } : {}),
26
+ ...(options.codexHome ? { CODEX_HOME: options.codexHome } : {}),
27
27
  };
28
28
  this.child = spawn(executable, ["app-server", "--listen", "stdio://"], {
29
+ cwd: options.cwd,
29
30
  env: spawnEnv,
30
31
  stdio: ["pipe", "pipe", "pipe"],
31
32
  });
@@ -77,7 +77,18 @@ export async function selectSubagent(agentName, cwd) {
77
77
  throw new FlowError("INVALID_SETTINGS", `Akuma '${selected}' references missing provider instance '${instanceName}'.`);
78
78
  }
79
79
  if (!("instance" in providerCandidate) || providerCandidate.instance === undefined) {
80
- throw new FlowError("INVALID_SETTINGS", `Akuma '${selected}' references invalid provider instance '${instanceName}': ${providerCandidate.reason}`);
80
+ const source = providerCandidate.source === "global" ? "global" : "local";
81
+ throw new FlowError("INVALID_SETTINGS", `Akuma '${selected}' references invalid provider instance '${instanceName}': ${providerCandidate.reason}`, {
82
+ facts: {
83
+ kind: "invalid_settings",
84
+ diseases: [{
85
+ source,
86
+ coordinate: providerCandidate.coordinate,
87
+ knob: "providers",
88
+ reason: providerCandidate.reason,
89
+ }],
90
+ },
91
+ });
81
92
  }
82
93
  const providerInstance = providerCandidate.instance;
83
94
  const adapter = providerInstance.kind;
@@ -2,19 +2,12 @@ import { FlowError } 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";
5
- import { readProjectionStatusBoard } from "../../../../core/projection/index.js";
5
+ import { readAkumaNonterminalProjectionStatus } from "../../../../core/projection/index.js";
6
6
  import { isResponseArtifactId } from "../../../../core/response-artifact-id.js";
7
7
  import { textResponse } from "../../../render/shared.js";
8
8
  import { renderProjectionStatusSection } from "../../../render/status.js";
9
9
  import { projectDirectory } from "../../shared.js";
10
10
  import { renderAkumaShow, renderAkumaView } from "../../akuma.js";
11
- const NONTERMINAL_PROJECTION_STATES = new Set([
12
- "minting",
13
- "startup-timeout",
14
- "unknown",
15
- "out",
16
- "lost",
17
- ]);
18
11
  export const handler = async (_stdin, flags, _signal, positional) => {
19
12
  const [name] = positional;
20
13
  if (!name)
@@ -28,8 +21,7 @@ export const handler = async (_stdin, flags, _signal, positional) => {
28
21
  assertSettingsKnobUsable(loaded, "default");
29
22
  assertSettingsKnobUsable(loaded, "reviewer");
30
23
  const profile = renderAkumaShow(await loadAkumaCatalog(cwd), loaded.providerInstances, loaded.knobs, name).trimEnd();
31
- const projectionStatus = await readProjectionStatusBoard(cwd, Date.now(), { showAll: true });
32
- const rows = (projectionStatus?.rows ?? []).filter((row) => row.akuma === name && NONTERMINAL_PROJECTION_STATES.has(row.state));
33
- const projections = renderProjectionStatusSection({ rows }).trimEnd();
24
+ const projectionStatus = await readAkumaNonterminalProjectionStatus(cwd, Date.now(), name);
25
+ const projections = renderProjectionStatusSection(projectionStatus).trimEnd();
34
26
  return textResponse(renderAkumaView(profile, projections));
35
27
  };
@@ -5,7 +5,7 @@ import { buildAmendResponse } from "../../../render/arc.js";
5
5
  import { renderCoreResult } from "../../../render/shared.js";
6
6
  import { effectiveDirectory, repositoryDirectory } from "../../shared.js";
7
7
  export const handler = async (stdin, flags) => {
8
- const parsed = buildAmendInput(stdin, effectiveDirectory(flags), flags.contractId, flags.contractAddressSource);
8
+ const parsed = buildAmendInput(stdin, effectiveDirectory(flags), flags.contractId, flags.contractAddressSource, flags.appendScope);
9
9
  const cwd = await repositoryDirectory(flags, "amend");
10
10
  const address = await resolveExistingContractAddress(cwd, flags.contractId, flags.contractAddressSource, effectiveDirectory(flags));
11
11
  return renderCoreResult(amendKeiyaku({
@@ -2,11 +2,11 @@ export const meta = {
2
2
  command: "amend",
3
3
  summary: "Record an amendment",
4
4
  usage: [
5
- "keiyaku amend [-C|--cwd DIR] [--repo DIR] [--contract ADDR] - < amendment.md",
6
- "keiyaku @addr amend [-C|--cwd DIR] [--repo DIR] - < amendment.md",
5
+ "keiyaku amend [-C|--cwd DIR] [--repo DIR] [--contract ADDR] [--append-scope PATTERN] - < amendment.md",
6
+ "keiyaku @addr amend [-C|--cwd DIR] [--repo DIR] [--append-scope PATTERN] - < amendment.md",
7
7
  ],
8
8
  stdin: { mode: "document", selector: { kind: "positional", index: 0, required: true, rejectBlank: true } },
9
- flags: ["cwd", "repo", "contract"],
10
- purpose: "Append a prose amendment and optional Scope Append gitignore-pattern delta.",
9
+ flags: ["cwd", "repo", "contract", "append-scope"],
10
+ purpose: "Append a prose amendment and one optional ordered scope-pattern delta from --append-scope or Scope Append.",
11
11
  completionRank: 8,
12
12
  };
@@ -6,6 +6,10 @@ import { renderProjectionFilteredStatus, renderStatusBoard } from "../../../rend
6
6
  import { textResponse } from "../../../render/shared.js";
7
7
  import { projectDirectory, repositoryDirectory } from "../../shared.js";
8
8
  export const handler = async (_stdin, flags, _signal, positional) => {
9
+ const [projectionId] = positional;
10
+ if (projectionId !== undefined && !isProjectionId(projectionId)) {
11
+ throw new FlowError("EMPTY_PARAM", `invalid projection id: ${projectionId}`);
12
+ }
9
13
  // Status is read-only and must open outside Git. projectDirectory still
10
14
  // honors --repo as a repository coordinate when the operator names one.
11
15
  const cwd = flags.target === undefined
@@ -14,12 +18,9 @@ export const handler = async (_stdin, flags, _signal, positional) => {
14
18
  const target = flags.target === undefined ? undefined : normalizeTargetRef(flags.target);
15
19
  const board = await readKanshiBoard(cwd, Date.now(), {
16
20
  ...(target === undefined ? {} : { target }),
17
- showAllProjections: flags.all === true || positional.length === 1,
21
+ ...(projectionId === undefined ? {} : { projectionId }),
18
22
  });
19
- const [projectionId] = positional;
20
23
  if (projectionId === undefined)
21
24
  return textResponse(renderStatusBoard(board));
22
- if (!isProjectionId(projectionId))
23
- throw new FlowError("EMPTY_PARAM", `invalid projection id: ${projectionId}`);
24
25
  return textResponse(renderProjectionFilteredStatus(board, projectionId));
25
26
  };
@@ -1,9 +1,9 @@
1
1
  export const meta = {
2
2
  command: "status",
3
3
  summary: "Show the Kanshi board",
4
- usage: ["keiyaku status [PROJECTION] [-C|--cwd DIR] [--repo DIR] [--target REF] [--all]"],
4
+ usage: ["keiyaku status [PROJECTION] [-C|--cwd DIR] [--repo DIR] [--target REF]"],
5
5
  stdin: { mode: "argument" },
6
- flags: ["cwd", "repo", "target", "all"],
6
+ flags: ["cwd", "repo", "target"],
7
7
  purpose: "Read the Kanshi board or filter it to one projection without mutation.",
8
8
  completionRank: 14,
9
9
  };
@@ -1,11 +1,19 @@
1
+ const TASK_ADD_USAGE_PREFIX = "keiyaku task add [-C|--cwd DIR] [--needs ID]... [--parent ID] [--from ID]... [--pri 0-3]";
1
2
  export const meta = {
2
3
  command: "task add",
3
4
  summary: "Add a task",
4
- usage: ["keiyaku task add [-C|--cwd DIR] [--needs ID] [--parent ID] [--from ID] [--pri 0-3] <BODY|->"],
5
+ usage: [
6
+ `${TASK_ADD_USAGE_PREFIX} <BODY>`,
7
+ `${TASK_ADD_USAGE_PREFIX} - < task.md`,
8
+ ],
5
9
  stdin: {
6
10
  mode: "argument",
7
11
  selector: { kind: "positional", index: 0, required: true, rejectBlank: true },
8
12
  },
9
13
  flags: ["cwd", "needs", "parent", "from", "pri"],
10
14
  purpose: "Create a lightweight task from a title and optional body.",
15
+ notes: [
16
+ "<BODY> and literal - are mutually exclusive body selectors. Use literal - with stdin for multiline prose.",
17
+ "Only literal - reads stdin; unselected piped bytes are ignored and never change a literal <BODY>.",
18
+ ],
11
19
  };
@@ -17,7 +17,8 @@ export function parseTaskAddInput(input) {
17
17
  if (!input.trim())
18
18
  throw new FlowError("EMPTY_PARAM", "task add input cannot be blank");
19
19
  const lines = input.split(/\r?\n/);
20
- const title = lines[0]?.trim() ?? "";
20
+ const firstLine = lines[0]?.startsWith("# ") ? lines[0].slice(2) : lines[0];
21
+ const title = firstLine?.trim() ?? "";
21
22
  if (!title)
22
23
  throw new FlowError("EMPTY_PARAM", "task title cannot be blank");
23
24
  if (lines.length > 1 && lines[1].trim() !== "") {
@@ -19,6 +19,10 @@ function topLevelCommands() {
19
19
  function subcommandsFor(parent) {
20
20
  return [...(getCommandMetadata(parent).subcommands ?? [])];
21
21
  }
22
+ function topLevelOptionsFor(command) {
23
+ const metadata = allCommandMetadata().find((meta) => meta.command === command && !command.includes(" "));
24
+ return metadata ? metadata.flags.map((flag) => `--${flag}`) : [];
25
+ }
22
26
  function filterPrefix(items, word) {
23
27
  return items.filter((item) => item.startsWith(word));
24
28
  }
@@ -50,6 +54,10 @@ export async function renderCompletionCandidates(input) {
50
54
  if (input.previous === "--filter") {
51
55
  return filterPrefix([TASK_FILTER_COMPLETION], word).join("\n");
52
56
  }
57
+ const options = input.previous ? topLevelOptionsFor(input.previous) : [];
58
+ if (options.length > 0) {
59
+ return filterPrefix(options, word).join("\n");
60
+ }
53
61
  return filterPrefix(topLevelCommands(), word).join("\n");
54
62
  }
55
63
  export function renderCompletionScript(shell = "bash") {
@@ -45,8 +45,8 @@ export function renderKanshiBoard(status) {
45
45
  const name = contract.place ? `${contract.slug} @${contract.place}` : contract.slug;
46
46
  const spark = arcSparkline(contract.arcs);
47
47
  const facts = [];
48
- const lifecycle = contract.state.state;
49
- if (lifecycle === "bound" || lifecycle === "active" || lifecycle === "petitioned") {
48
+ const lifecycle = contract.state.state === "active" ? "open" : contract.state.state;
49
+ if (lifecycle === "bound" || lifecycle === "open" || lifecycle === "petitioned") {
50
50
  facts.push(lifecycle);
51
51
  }
52
52
  const arcMark = contract.currentArc.state === "open"
@@ -1,88 +1,131 @@
1
- const MIN_COMPACTION_SAVINGS = 4;
2
- const SAFE_PATTERN_COMPONENT = /^[A-Za-z0-9._*?\[\]-]+$/;
3
- const SAFE_CONCRETE_PATH_COMPONENT = /^[A-Za-z0-9._-]+$/;
4
- /** Compact one displayed pattern-pair without merging distinct overlap facts. */
5
- export function compactPatternPair(left, right) {
6
- const literal = `${left} × ${right}`;
7
- const leftParts = safePatternComponents(left);
8
- const rightParts = safePatternComponents(right);
9
- if (!leftParts || !rightParts)
10
- return literal;
11
- let sharedCount = 0;
12
- const maximumShared = Math.min(leftParts.length, rightParts.length) - 1;
13
- while (sharedCount < maximumShared && leftParts[sharedCount] === rightParts[sharedCount]) {
14
- sharedCount += 1;
15
- }
16
- if (sharedCount === 0)
17
- return literal;
18
- const prefix = leftParts.slice(0, sharedCount).join("/");
19
- const compacted = `${prefix}/{${leftParts.slice(sharedCount).join("/")} × ${rightParts.slice(sharedCount).join("/")}}`;
20
- return savesEnough(literal, compacted) ? compacted : literal;
1
+ const PREFIX_MARKER = "⋯";
2
+ const MIN_FOLDED_PREFIX_LENGTH = 8;
3
+ export const PATH_PREVIEW_BODY_ROWS = 7;
4
+ /** Render one exact pattern/path without giving display punctuation matcher meaning. */
5
+ export function renderExactPath(value) {
6
+ if (!requiresQuoting(value))
7
+ return value;
8
+ return `"${[...value].map(escapeQuotedCharacter).join("")}"`;
21
9
  }
22
- /** Compact a concrete preview through a stable slash-component trie when unambiguous. */
23
- export function compactPathPreview(paths) {
24
- const literal = paths.join(", ");
25
- if (paths.length < 2)
26
- return literal;
27
- const components = paths.map(safeConcretePathComponents);
28
- if (components.some((parts) => !parts))
29
- return literal;
30
- const trie = createTrie();
31
- for (const parts of components)
32
- insertPath(trie, parts);
33
- if (hasTerminalBranch(trie))
34
- return literal;
35
- if (!hasSharedDirectoryPrefix(trie))
36
- return literal;
37
- const compacted = renderTrie(trie);
38
- return savesEnough(literal, compacted) ? compacted : literal;
10
+ /** Fold one qualifying directory prefix while retaining row order and exact suffixes. */
11
+ export function foldPathRows(values) {
12
+ const rendered = values.map(renderExactPath);
13
+ const plainIndexes = values
14
+ .map((value, index) => renderExactPath(value) === value ? index : -1)
15
+ .filter((index) => index >= 0);
16
+ const prefix = bestSharedDirectoryPrefix(values, plainIndexes);
17
+ if (!prefix)
18
+ return { rows: rendered };
19
+ const foldedIndexes = new Set(plainIndexes.filter((index) => values[index].startsWith(prefix)));
20
+ return {
21
+ prefix,
22
+ rows: values.map((value, index) => foldedIndexes.has(index) ? `${PREFIX_MARKER}${value.slice(prefix.length)}` : rendered[index]),
23
+ };
39
24
  }
40
- function safePatternComponents(value) {
41
- return safeSlashComponents(value, SAFE_PATTERN_COMPONENT);
25
+ /** Render one overlap fact as two labeled exact pattern rows. */
26
+ export function renderPatternPair(leftLabel, leftPattern, rightLabel, rightPattern) {
27
+ const folded = foldPathRows([leftPattern, rightPattern]);
28
+ const labelWidth = Math.max(leftLabel.length, rightLabel.length) + 2;
29
+ return [
30
+ ...(folded.prefix ? [`${PREFIX_MARKER} = ${folded.prefix}`] : []),
31
+ `${leftLabel.padEnd(labelWidth)}${folded.rows[0]}`,
32
+ `${rightLabel.padEnd(labelWidth)}${folded.rows[1]}`,
33
+ ];
42
34
  }
43
- function safeConcretePathComponents(value) {
44
- return safeSlashComponents(value, SAFE_CONCRETE_PATH_COMPONENT);
35
+ /** Render a bounded path snapshot; declarations consume the same row budget as paths. */
36
+ export function renderPathPreview(snapshot, bodyRows = PATH_PREVIEW_BODY_ROWS) {
37
+ const plan = planPathPreview(snapshot.preview, Math.max(0, bodyRows));
38
+ const omitted = Math.max(0, snapshot.count - plan.visibleCount);
39
+ return [
40
+ `scope · ${snapshot.count} pattern${snapshot.count === 1 ? "" : "s"}`,
41
+ ...(plan.prefix ? [`${PREFIX_MARKER} = ${plan.prefix}`] : []),
42
+ ...plan.rows,
43
+ ...(omitted > 0 ? [`+${omitted} more patterns`] : []),
44
+ ];
45
45
  }
46
- function safeSlashComponents(value, safeComponent) {
47
- const parts = value.split("/");
48
- if (parts.length === 0 || parts.some((part) => part === "." || part === ".." || !safeComponent.test(part))) {
49
- return null;
46
+ function planPathPreview(preview, bodyRows) {
47
+ const fullValues = preview.slice(0, bodyRows);
48
+ if (fullValues.length < bodyRows) {
49
+ return { ...foldPathRows(fullValues), visibleCount: fullValues.length };
50
50
  }
51
- return parts;
52
- }
53
- function createTrie() {
54
- return { terminal: false, children: new Map() };
55
- }
56
- function insertPath(node, parts) {
57
- let current = node;
58
- for (const part of parts) {
59
- let child = current.children.get(part);
60
- if (!child) {
61
- child = createTrie();
62
- current.children.set(part, child);
63
- }
64
- current = child;
51
+ const chargedValues = preview.slice(0, Math.max(0, bodyRows - 1));
52
+ const chargedFold = foldPathRows(chargedValues);
53
+ if (chargedFold.prefix) {
54
+ return { ...chargedFold, visibleCount: chargedValues.length };
65
55
  }
66
- current.terminal = true;
56
+ return { rows: fullValues.map(renderExactPath), visibleCount: fullValues.length };
67
57
  }
68
- function hasTerminalBranch(node) {
69
- if (node.terminal && node.children.size > 0)
58
+ function requiresQuoting(value) {
59
+ if (value.length === 0 || /^\s|\s$/u.test(value))
70
60
  return true;
71
- return [...node.children.values()].some(hasTerminalBranch);
61
+ return value.includes("\\")
62
+ || value.includes('"')
63
+ || value.includes(PREFIX_MARKER)
64
+ || [...value].some(isControlCharacter);
72
65
  }
73
- function hasSharedDirectoryPrefix(node, isRoot = true) {
74
- if (!isRoot && node.children.size > 1)
75
- return true;
76
- return [...node.children.values()].some((child) => hasSharedDirectoryPrefix(child, false));
66
+ function isControlCharacter(character) {
67
+ const codePoint = character.codePointAt(0);
68
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
69
+ }
70
+ function escapeQuotedCharacter(character) {
71
+ if (character === "\\")
72
+ return "\\\\";
73
+ if (character === '"')
74
+ return '\\"';
75
+ const codePoint = character.codePointAt(0);
76
+ const named = new Map([
77
+ [0x07, "\\a"],
78
+ [0x08, "\\b"],
79
+ [0x09, "\\t"],
80
+ [0x0a, "\\n"],
81
+ [0x0b, "\\v"],
82
+ [0x0c, "\\f"],
83
+ [0x0d, "\\r"],
84
+ ]).get(codePoint);
85
+ if (named)
86
+ return named;
87
+ if (!isControlCharacter(character))
88
+ return character;
89
+ return [...Buffer.from(character, "utf8")]
90
+ .map((byte) => `\\${byte.toString(8).padStart(3, "0")}`)
91
+ .join("");
77
92
  }
78
- function renderTrie(node) {
79
- const entries = [...node.children.entries()].map(([part, child]) => {
80
- if (child.children.size === 0)
81
- return part;
82
- return `${part}/${renderTrie(child)}`;
83
- });
84
- return entries.length === 1 ? entries[0] : `{${entries.join(",")}}`;
93
+ function bestSharedDirectoryPrefix(values, plainIndexes) {
94
+ let best;
95
+ for (let left = 0; left < plainIndexes.length; left += 1) {
96
+ for (let right = left + 1; right < plainIndexes.length; right += 1) {
97
+ const prefix = sharedDirectoryPrefix(values[plainIndexes[left]], values[plainIndexes[right]]);
98
+ if (!prefix || prefix.length < MIN_FOLDED_PREFIX_LENGTH)
99
+ continue;
100
+ const members = plainIndexes.filter((index) => values[index].startsWith(prefix));
101
+ const common = members
102
+ .slice(1)
103
+ .reduce((current, index) => sharedDirectoryPrefix(current, values[index]) ?? "", values[members[0]]);
104
+ if (common.length < MIN_FOLDED_PREFIX_LENGTH)
105
+ continue;
106
+ const candidate = {
107
+ prefix: common,
108
+ count: members.length,
109
+ first: members[0],
110
+ score: common.length * (members.length - 1),
111
+ };
112
+ if (!best
113
+ || candidate.score > best.score
114
+ || (candidate.score === best.score && candidate.count > best.count)
115
+ || (candidate.score === best.score && candidate.count === best.count && candidate.first < best.first)
116
+ || (candidate.score === best.score && candidate.count === best.count && candidate.first === best.first
117
+ && candidate.prefix < best.prefix)) {
118
+ best = candidate;
119
+ }
120
+ }
121
+ }
122
+ return best?.prefix;
85
123
  }
86
- function savesEnough(literal, compacted) {
87
- return literal.length - compacted.length >= MIN_COMPACTION_SAVINGS;
124
+ function sharedDirectoryPrefix(left, right) {
125
+ let sharedLength = 0;
126
+ const maximum = Math.min(left.length, right.length);
127
+ while (sharedLength < maximum && left[sharedLength] === right[sharedLength])
128
+ sharedLength += 1;
129
+ const slash = left.lastIndexOf("/", sharedLength - 1);
130
+ return slash >= 0 ? left.slice(0, slash + 1) : undefined;
88
131
  }
@@ -2,9 +2,10 @@ import { compactText } from "./compact-text.js";
2
2
  import { displayColumns, fitVariableLine, resolveLineColumns, truncateMiddleColumns } from "./line-width.js";
3
3
  import { presentFoldedToolActivity, } from "./tool-presentation.js";
4
4
  import { presentLedgerPath, summarizeToolLedger } from "./tool-ledger-rollup.js";
5
- const TIME_GUTTER_WIDTH = 6;
5
+ const TIME_GUTTER_WIDTH = 5;
6
6
  const VERB_WIDTH = 7;
7
7
  const SILENCE_THRESHOLD_MS = 60_000;
8
+ const TIMELINE_GAP_MARKER = "⋮";
8
9
  export function formatTimelineDuration(ms) {
9
10
  const totalSeconds = Math.max(0, Math.floor(ms / 1_000));
10
11
  if (totalSeconds < 60)
@@ -72,6 +73,9 @@ function compressedPrefix(prefix) {
72
73
  function timelineGutter(label, spine) {
73
74
  return label || `${"".padStart(TIME_GUTTER_WIDTH)}${spine}`;
74
75
  }
76
+ function renderTimelineGap(omittedEventCount, maxColumns) {
77
+ return fitVariableLine(`${timelineGutter("", TIMELINE_GAP_MARKER)} `, `${omittedEventCount} more`, maxColumns);
78
+ }
75
79
  function compactTokens(value) {
76
80
  if (value < 1_000)
77
81
  return String(value);
@@ -171,6 +175,7 @@ export function renderProjectionActivity(activity, input) {
171
175
  const lines = [];
172
176
  const rowKinds = [];
173
177
  let priorAtMs;
178
+ let priorOrder;
174
179
  let priorMinute;
175
180
  let priorCompletedRan;
176
181
  let hasEndAnchor = false;
@@ -185,6 +190,10 @@ export function renderProjectionActivity(activity, input) {
185
190
  for (let index = 0; index < entries.length; index += 1) {
186
191
  const entry = entries[index];
187
192
  const atMs = Date.parse(entry.at);
193
+ if (priorOrder !== undefined && entry.order > priorOrder + 1) {
194
+ pushRow(renderTimelineGap(entry.order - priorOrder - 1, maxColumns), "activity");
195
+ }
196
+ priorOrder = entry.order;
188
197
  if (entry.kind === "clock") {
189
198
  if (Number.isFinite(atMs))
190
199
  priorAtMs = atMs;
@@ -3,7 +3,7 @@ import { commissionSlug, formatCommissionCoordinate } from "../../core/ids.js";
3
3
  import { assembleResponse, buildSection, DISPLAY_TEXT_MAX_CHARS, FORMAT_LIST_MAX_ITEM_CHARS, FORMAT_LIST_MAX_ITEMS, formatMaybe, formatWarnings, shellQuote, truncateForDisplay, } from "./format.js";
4
4
  import { WARNING_SECTION_TITLE } from "./response-style.js";
5
5
  import { prependAddressReceipt } from "./address.js";
6
- import { compactPathPreview, compactPatternPair } from "./path-prefix-compaction.js";
6
+ import { renderPathPreview, renderPatternPair } from "./path-prefix-compaction.js";
7
7
  export function textResponse(text) {
8
8
  const payload = {};
9
9
  return {
@@ -76,16 +76,17 @@ function renderScopeOverlapWarnings(overlaps) {
76
76
  return [...overlapsByCounterparty.entries()].map(([contractId, overlaps]) => renderScopeOverlapGroup(commissionSlug(contractId), overlaps));
77
77
  }
78
78
  function renderScopeOverlapGroup(contractAddr, overlaps) {
79
- const pairs = overlaps.map((overlap) => {
79
+ const nextAddr = commissionSlug(overlaps[0].contracts[0]);
80
+ const pairs = overlaps.flatMap((overlap) => {
80
81
  const [nextPattern, existingPattern] = overlap.patterns;
81
- const patternPair = compactPatternPair(nextPattern, existingPattern);
82
+ const patternPair = renderPatternPair(nextAddr, nextPattern, contractAddr, existingPattern);
82
83
  const evidence = overlap.kind === "potential"
83
- ? "potential; no current tracked path proves it"
84
- : `concrete; ${overlap.paths.count} current tracked path${overlap.paths.count === 1 ? "" : "s"}: ${compactPathPreview(overlap.paths.preview)}${overlap.paths.truncated ? ", …" : ""}`;
85
- return ` ${patternPair} (${evidence})`;
84
+ ? ["potential · no current tracked path proves it"]
85
+ : renderPathPreview(overlap.paths);
86
+ return [...patternPair, ...evidence].map((line) => ` ${line}`);
86
87
  });
87
88
  return [
88
- `▲ scope overlap with ${contractAddr}:`,
89
+ `▲ overlap ${nextAddr} ↔ ${contractAddr}`,
89
90
  ...pairs,
90
91
  ` consider --after ${contractAddr}`,
91
92
  ].join("\n");
@@ -74,7 +74,7 @@ function renderProjectionOmission(board) {
74
74
  const count = board.omitted?.byState[state] ?? 0;
75
75
  return count > 0 ? [`${count} ${state}`] : [];
76
76
  });
77
- return ` · +${board.omitted.total} historical projections not shown (${counts.join(" · ")}) — keiyaku status --all`;
77
+ return ` · +${board.omitted.total} historical projections not shown (${counts.join(" · ")})`;
78
78
  }
79
79
  function renderProjectionAge(row) {
80
80
  const duration = formatAgeMs(row.durationMs) ?? "?";
@@ -170,15 +170,18 @@ export function renderProjectionFilteredStatus(board, projectionId) {
170
170
  if (!row)
171
171
  return `${heading}\nno projections on projection ${projectionId}\n`;
172
172
  if (row.state === "failed" || row.terminalFailure) {
173
- // Same face bytes as wait; CLI success write supplies the final newline.
174
- return renderTerminalFailureFace(projectionId, row.terminalFailure).join("\n");
173
+ const lines = [...renderTerminalFailureFace(projectionId, row.terminalFailure), "", `» keiyaku wait ${projectionId}`];
174
+ return lines.join("\n");
175
175
  }
176
176
  const lines = [heading, "", ...renderProjectionRow(row), ""];
177
177
  if (row.observation.compatibility === "incompatible") {
178
178
  return `${lines.join("\n")}\n`;
179
179
  }
180
- if (!isTerminalProjectionStatus(row))
181
- lines.push(`» ${formatTellCommand(projectionId, '"..."')}`);
180
+ if (isTerminalProjectionStatus(row)) {
181
+ lines.push(`» keiyaku wait ${projectionId}`);
182
+ return lines.join("\n");
183
+ }
184
+ lines.push(`» ${formatTellCommand(projectionId, '"..."')}`);
182
185
  lines.push(`» keiyaku wait ${projectionId}`);
183
186
  return `${lines.join("\n")}\n`;
184
187
  }
@@ -73,7 +73,7 @@ function waitHeader(projectionId, akuma, snapshot, outputColumns) {
73
73
  function pendingTellLines(snapshot, maxColumns) {
74
74
  return snapshot.tells.window.map((tell) => {
75
75
  const age = Math.max(0, snapshot.observedAtMs - Date.parse(tell.createdAt));
76
- const fixed = `${"".padStart(6)}│ ${"tell".padEnd(7)} `;
76
+ const fixed = `${"".padStart(5)}│ ${"tell".padEnd(7)} `;
77
77
  const suffix = ` · undelivered ${formatTimelineDuration(Number.isFinite(age) ? age : 0)}`;
78
78
  return `${fixed}${truncateColumns(`“${tell.text}”`, Math.max(0, maxColumns - displayColumns(fixed) - displayColumns(suffix)))}${suffix}`;
79
79
  });