@astrosheep/keiyaku 2.9.4 → 2.9.6

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 (143) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +1 -0
  3. package/build/agents/harness/execution-handle.js +1 -0
  4. package/build/agents/harness/outcome.js +31 -5
  5. package/build/agents/harness/projection.js +151 -78
  6. package/build/agents/harness/router.js +2 -1
  7. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  8. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  9. package/build/agents/providers/codex-app-server/adapter.js +43 -10
  10. package/build/agents/providers/codex-app-server/events.js +23 -0
  11. package/build/agents/providers/codex-app-server/registration.js +2 -1
  12. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  13. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  14. package/build/agents/providers/pi/adapter.js +10 -0
  15. package/build/agents/providers/pi/events.js +4 -3
  16. package/build/agents/providers/pi/registration.js +3 -1
  17. package/build/agents/types.js +2 -0
  18. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  19. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  20. package/build/cli/commands/akuma/view/meta.js +1 -1
  21. package/build/cli/commands/contract/amend/meta.js +4 -4
  22. package/build/cli/commands/contract/arc/handler.js +1 -1
  23. package/build/cli/commands/contract/arc/meta.js +4 -4
  24. package/build/cli/commands/contract/audit/handler.js +16 -0
  25. package/build/cli/commands/contract/audit/meta.js +16 -0
  26. package/build/cli/commands/contract/bind/meta.js +20 -11
  27. package/build/cli/commands/contract/catalog.js +2 -0
  28. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  29. package/build/cli/commands/contract/log/meta.js +1 -1
  30. package/build/cli/commands/contract/petition/handler.js +1 -2
  31. package/build/cli/commands/contract/petition/meta.js +6 -6
  32. package/build/cli/commands/contract/renew/handler.js +2 -2
  33. package/build/cli/commands/contract/renew/meta.js +4 -4
  34. package/build/cli/commands/metadata.js +3 -2
  35. package/build/cli/commands/projection/call/meta.js +4 -1
  36. package/build/cli/commands/projection/kill/meta.js +1 -1
  37. package/build/cli/commands/projection/revive/meta.js +4 -1
  38. package/build/cli/commands/projection/status/meta.js +1 -1
  39. package/build/cli/commands/projection/tell/handler.js +1 -1
  40. package/build/cli/commands/projection/tell/meta.js +4 -1
  41. package/build/cli/commands/projection/wait/meta.js +1 -1
  42. package/build/cli/commands/skills/install/meta.js +1 -1
  43. package/build/cli/commands/skills/skills/meta.js +1 -1
  44. package/build/cli/commands/system/completion/meta.js +1 -1
  45. package/build/cli/commands/system/dump-env/meta.js +1 -1
  46. package/build/cli/commands/system/guide/meta.js +1 -1
  47. package/build/cli/commands/task/add/handler.js +2 -2
  48. package/build/cli/commands/task/add/meta.js +4 -1
  49. package/build/cli/commands/task/doctor/meta.js +1 -1
  50. package/build/cli/commands/task/done/meta.js +1 -1
  51. package/build/cli/commands/task/drop/meta.js +1 -1
  52. package/build/cli/commands/task/ls/handler.js +10 -4
  53. package/build/cli/commands/task/ls/meta.js +3 -3
  54. package/build/cli/commands/task/show/meta.js +1 -1
  55. package/build/cli/commands/task/start/meta.js +1 -1
  56. package/build/cli/commands/task/stop/meta.js +1 -1
  57. package/build/cli/commands/task/task/meta.js +1 -1
  58. package/build/cli/commands/task/update/handler.js +2 -3
  59. package/build/cli/commands/task/update/meta.js +4 -1
  60. package/build/cli/completion.js +9 -2
  61. package/build/cli/flags.js +19 -3
  62. package/build/cli/help.js +3 -3
  63. package/build/cli/index.js +59 -75
  64. package/build/cli/output-stream-error-policy.js +19 -5
  65. package/build/cli/parse-flags.js +19 -2
  66. package/build/cli/parse-metadata.js +1 -7
  67. package/build/cli/parse-selectors.js +1 -0
  68. package/build/cli/parse.js +16 -7
  69. package/build/cli/projection-address.js +6 -1
  70. package/build/cli/render/audit.js +121 -0
  71. package/build/cli/render/errors.js +18 -1
  72. package/build/cli/render/format.js +5 -2
  73. package/build/cli/render/misc.js +3 -1
  74. package/build/cli/render/projection-activity.js +12 -6
  75. package/build/cli/render/status.js +28 -18
  76. package/build/cli/render/success-response.js +6 -5
  77. package/build/cli/render/task.js +12 -43
  78. package/build/cli/render/terminal-failure.js +7 -2
  79. package/build/cli/render/wait.js +36 -8
  80. package/build/config/env-keys.js +2 -0
  81. package/build/config/env.js +3 -0
  82. package/build/core/amend.js +20 -94
  83. package/build/core/arc.js +14 -6
  84. package/build/core/audit/candidate.js +48 -0
  85. package/build/core/audit/coordinates.js +180 -0
  86. package/build/core/audit/evidence.js +158 -0
  87. package/build/core/audit/facade.js +42 -0
  88. package/build/core/audit/readiness.js +4 -0
  89. package/build/core/audit/report.js +38 -0
  90. package/build/core/audit.js +1 -0
  91. package/build/core/bind.js +1 -1
  92. package/build/core/call/execution.js +1 -0
  93. package/build/core/contract-view.js +2 -3
  94. package/build/core/draft.js +33 -20
  95. package/build/core/entry.js +3 -4
  96. package/build/core/markdown/lex.js +2 -1
  97. package/build/core/projection/generation/database.js +41 -16
  98. package/build/core/projection/generation/model.js +22 -2
  99. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  100. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  101. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  102. package/build/core/projection/generation/store.js +1 -1
  103. package/build/core/projection/generation/transitions.js +19 -3
  104. package/build/core/projection/index.js +2 -2
  105. package/build/core/projection/projection-execution-observer.js +35 -21
  106. package/build/core/projection/projection-life-observer.js +16 -2
  107. package/build/core/projection/projection-status.js +67 -22
  108. package/build/core/projection/projection-terminal-failure.js +2 -0
  109. package/build/core/projection/projection-wait.js +6 -9
  110. package/build/core/projection/projection-wake.js +30 -6
  111. package/build/core/renew.js +41 -22
  112. package/build/core/scope.js +129 -2
  113. package/build/core/seal.js +36 -15
  114. package/build/core/settlement/petition-claim-gates.js +4 -33
  115. package/build/core/settlement/petition-preview.js +1 -1
  116. package/build/core/settlement/petition.js +10 -3
  117. package/build/core/settlement/settlement.js +3 -10
  118. package/build/core/settlement/verification.js +59 -0
  119. package/build/core/status/board.js +48 -28
  120. package/build/core/stored-agent-event.js +3 -2
  121. package/build/core/task/board.js +1 -0
  122. package/build/core/task/commands.js +20 -12
  123. package/build/core/task/document.js +5 -0
  124. package/build/core/task/index.js +1 -1
  125. package/build/core/task/model.js +8 -1
  126. package/build/core/task/query.js +214 -0
  127. package/build/core/task/settlement-git.js +115 -16
  128. package/build/core/task/settlement-policy.js +17 -9
  129. package/build/core/task/source-board.js +1 -0
  130. package/build/core/task/task-contract.js +125 -51
  131. package/build/core/task/task-store-repository.js +1 -0
  132. package/build/core/task/task.js +2 -0
  133. package/build/flow-error.js +2 -4
  134. package/build/generated/version.js +2 -2
  135. package/build/git/diff/parsers.js +23 -13
  136. package/build/git/diff/structured.js +127 -0
  137. package/build/index.js +7 -7
  138. package/build/telemetry/logger.js +4 -2
  139. package/package.json +1 -1
  140. package/skills/keiyaku/SKILL.md +2 -2
  141. package/skills/keiyaku-akuma/SKILL.md +17 -6
  142. package/skills/keiyaku-task/SKILL.md +16 -7
  143. package/skills/keiyaku-workflow/SKILL.md +48 -13
@@ -0,0 +1,121 @@
1
+ import { asMessage, FlowError } from "../../flow-error.js";
2
+ function streamLines(name, result) {
3
+ const stream = result[name];
4
+ return [
5
+ `${name} tail (${stream.truncated ? `truncated, ${stream.omittedBytes} bytes omitted` : "complete, 0 bytes omitted"}):`,
6
+ stream.tail || "(empty)",
7
+ ];
8
+ }
9
+ function renderVerification(report) {
10
+ if (report.verification.status === "not_configured")
11
+ return ["not configured"];
12
+ if (report.verification.status === "not_evaluated")
13
+ return [`not evaluated: ${report.verification.reason}`];
14
+ if (report.verification.status === "unknown")
15
+ return [`unknown: ${report.verification.cause.code}: ${report.verification.cause.message}`];
16
+ const lines = [];
17
+ for (const result of report.verification.results) {
18
+ lines.push(`command ${result.label}`, `exec ${result.command}`, `argv ${JSON.stringify(result.argv)}`, `head ${result.head}`);
19
+ if (result.status === "unverifiable") {
20
+ lines.push(`not executable: ${result.cause}`, "");
21
+ continue;
22
+ }
23
+ lines.push(`\`\`\`${result.executor}`, result.body, "\`\`\`", `exit ${result.exitCode} · duration ${result.durationMs}ms`, ...streamLines("stdout", result), ...streamLines("stderr", result), "");
24
+ }
25
+ return lines;
26
+ }
27
+ function renderScope(report) {
28
+ if (!report.scope)
29
+ return ["not evaluated"];
30
+ return report.scope.flatMap((row) => {
31
+ const decisive = row.matcher.decisivePattern
32
+ ? `rule ${row.matcher.decisivePattern.index}:${row.matcher.decisivePattern.pattern} (${row.matcher.decisivePattern.effect})`
33
+ : `rule none (${row.matcher.outcome})`;
34
+ const explanations = row.explanations.map((item) => {
35
+ if (item.kind === "file-rename")
36
+ return ` explain file rename ${item.from} -> ${item.to} (${item.similarity}%)`;
37
+ if (item.kind === "symbol-rename")
38
+ return ` explain symbol rename ${item.from} -> ${item.to} at h${item.hunk} old:${item.oldLine ?? "-"} new:${item.newLine ?? "-"}`;
39
+ return ` explain import at h${item.hunk} old:${item.oldLine ?? "-"} new:${item.newLine ?? "-"}`;
40
+ });
41
+ const residue = row.residue.map((item) => ` residue h${item.hunk} old:${item.oldLine ?? "-"} new:${item.newLine ?? "-"} ${item.kind} ${item.content}`);
42
+ return [`${row.class} ${row.path} · ${decisive}`, ...explanations, ...residue];
43
+ });
44
+ }
45
+ function renderReadiness(report) {
46
+ const readiness = report.readiness;
47
+ if (!readiness)
48
+ return ["not evaluated: coordinate race"];
49
+ return [
50
+ `delivery residue tracked=${readiness.deliveryResidue.tracked.length} staged=${readiness.deliveryResidue.staged.length} unstaged=${readiness.deliveryResidue.unstaged.length} untracked=${readiness.deliveryResidue.untracked.length}`,
51
+ `arc open=${readiness.arc.open ?? "none"} max=${readiness.arc.max} fence=${readiness.arc.fence} loose=${readiness.arc.looseCommits.length}`,
52
+ `renew session=${readiness.renewSession.open ? "open" : "none"}`,
53
+ `rewrite guard=${readiness.rewriteGuard.liveProjection ? "live projection" : "clear"}`,
54
+ ...readiness.dependencies.map((item) => `dependency ${item.id} ${item.satisfied ? "satisfied" : `open (${item.state})`}`),
55
+ `petition state=${readiness.petition.state}${readiness.petition.queue ? ` queue=${readiness.petition.queue.position ?? "?"}/${readiness.petition.queue.length}` : " queue=none"}`,
56
+ ];
57
+ }
58
+ function omissionLine(omission) {
59
+ const subject = omission.hunk === null ? "file" : `hunk ${omission.hunk}`;
60
+ return `omit ${subject} ${omission.path} · ${omission.bytes} bytes · ${omission.reason} · retrieve ${omission.retrieval.argv.map((part) => JSON.stringify(part)).join(" ")}`;
61
+ }
62
+ function renderDiff(report) {
63
+ if (!report.diff || !report.presentation)
64
+ return ["not evaluated"];
65
+ const selected = report.presentation.selected.map((item) => item.content);
66
+ const omissions = report.presentation.omissions.map(omissionLine);
67
+ const rows = report.files?.map((file) => `${file.status} ${file.oldPath ? `${file.oldPath} -> ` : ""}${file.path} · +${file.stats.insertions} -${file.stats.deletions} · ${file.class}`) ?? [];
68
+ return [
69
+ `${report.diff.stat.filesChanged} files · +${report.diff.stat.insertions} -${report.diff.stat.deletions}`,
70
+ `budget ${report.presentation.budget.limit} bytes · used ${report.presentation.budget.used} bytes · omissions ${report.presentation.omissions.length}`,
71
+ ...rows,
72
+ ...selected,
73
+ ...omissions,
74
+ ];
75
+ }
76
+ export function buildAuditResponse(report, json) {
77
+ const payload = report;
78
+ if (json)
79
+ return { text: JSON.stringify(report), payload };
80
+ const text = [
81
+ `Audit ${report.contract}`,
82
+ "",
83
+ "Coordinates",
84
+ `ledger ${report.coordinates.ledger.head}`,
85
+ `delivery ${report.coordinates.delivery.ref} ${report.coordinates.delivery.head} tree ${report.coordinates.delivery.tree}`,
86
+ `target ${report.coordinates.target.ref} ${report.coordinates.target.head} tree ${report.coordinates.target.tree}`,
87
+ "",
88
+ "Criteria",
89
+ ...report.terms.criteria.map((term) => `criterion ${term}`),
90
+ ...report.terms.scope.map((term) => `scope ${term}`),
91
+ ...report.terms.appendScope.map((term) => `append-scope ${term}`),
92
+ "",
93
+ "Renew Rehearsal",
94
+ JSON.stringify(report.rehearsal),
95
+ "",
96
+ "Diff",
97
+ ...renderDiff(report),
98
+ `counts files=${report.counts.files} selected=${report.counts.selected} omitted=${report.counts.omitted} in-scope=${report.counts.inScope} downgraded=${report.counts.downgraded} violated=${report.counts.violated} findings=${report.counts.findings}`,
99
+ "",
100
+ "Scope",
101
+ ...renderScope(report),
102
+ "",
103
+ "Verification",
104
+ ...renderVerification(report),
105
+ "Readiness",
106
+ ...renderReadiness(report),
107
+ "",
108
+ "Findings",
109
+ ...(report.findings.length === 0 ? ["none"] : report.findings.flatMap((finding) => [`${finding.code}: ${finding.detail}`, `Next: ${finding.nextCommand}`])),
110
+ ].join("\n");
111
+ return { text, payload };
112
+ }
113
+ export function buildAuditConstructionFailure(error, json) {
114
+ const cause = { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) };
115
+ const payload = { audit: { status: "construction_failed", cause } };
116
+ return {
117
+ text: json ? JSON.stringify(payload) : `Audit construction failure\n${cause.code}: ${cause.message}`,
118
+ payload,
119
+ exitCode: 1,
120
+ };
121
+ }
@@ -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)
@@ -5,7 +5,9 @@ import { prependAddressReceipt, renderAddressCarrier } from "./address.js";
5
5
  import { commissionSlug } from "../../core/ids.js";
6
6
  export function buildErrorResponse(input) {
7
7
  const shouldRaiseProtocolError = input.errorType === "runtime_error";
8
- const text = buildErrorText(input);
8
+ const text = input.errorCode === "PROJECTION_INCOMPATIBLE"
9
+ ? `${colorizeErrorStatus(`${RESPONSE_MARKERS.failure} Failed [${input.errorCode}]`)}\n ${input.message}`
10
+ : buildErrorText(input);
9
11
  const payload = {
10
12
  error: {
11
13
  code: input.errorCode,
@@ -71,9 +71,19 @@ function summarizeProjectionToolLedger(activity, workspaceRoot) {
71
71
  function toolCountSegments(summary) {
72
72
  return summary.counts.map(({ semanticClass, count }) => `${count} ${rollupCountLabel(semanticClass)}`);
73
73
  }
74
+ function projectionSummarySegments(summary) {
75
+ const segments = toolCountSegments(summary);
76
+ if (summary.tokens !== undefined) {
77
+ segments.push(`${compactTokens(summary.tokens)} tok`);
78
+ }
79
+ if (summary.costUsd !== undefined) {
80
+ segments.push(`$${summary.costUsd.toFixed(2)}`);
81
+ }
82
+ return segments;
83
+ }
74
84
  export function renderProjectionActivitySummary(activity, input) {
75
85
  const summary = summarizeProjectionToolLedger(activity, input.workspaceRoot);
76
- const segments = toolCountSegments(summary);
86
+ const segments = projectionSummarySegments(summary);
77
87
  if (segments.length === 0)
78
88
  return undefined;
79
89
  const spine = input.state === "dead" || input.state === "lost" ? "┆" : "│";
@@ -81,11 +91,7 @@ export function renderProjectionActivitySummary(activity, input) {
81
91
  }
82
92
  function renderTerminalRollup(activity, input) {
83
93
  const summary = summarizeProjectionToolLedger(activity, input.workspaceRoot);
84
- const segments = [
85
- ...toolCountSegments(summary),
86
- ...(summary.tokens === undefined ? [] : [`${compactTokens(summary.tokens)} tok`]),
87
- ...(summary.costUsd === undefined ? [] : [`$${summary.costUsd.toFixed(2)}`]),
88
- ];
94
+ const segments = projectionSummarySegments(summary);
89
95
  const lines = input.diagnostic
90
96
  ? [fitVariableLine(`${elapsedLabel(input.anchorMs, input.diagnostic.at).padStart(TIME_GUTTER_WIDTH)}┆ ${"warning".padEnd(VERB_WIDTH)} `, compactText(input.diagnostic.text), input.maxColumns)]
91
97
  : [];
@@ -25,6 +25,7 @@ function projectionGlyph(state) {
25
25
  case "startup-timeout":
26
26
  return "!";
27
27
  case "unknown":
28
+ case "incompatible":
28
29
  return "?";
29
30
  }
30
31
  }
@@ -44,9 +45,9 @@ function renderTaskStatusSection(tasks) {
44
45
  tasks.blocked > 0 ? `${tasks.blocked} blocked` : undefined,
45
46
  ].filter((segment) => segment !== undefined);
46
47
  const lines = [`tasks · ${segments.join(" / ")}`];
47
- lines.push(...tasks.topTasks.map((task) => ` ${task.id} · P${task.pri} · ${task.state === "in_progress" ? "in_progress" : "ready"} · ${task.title}`));
48
+ lines.push(...tasks.topTasks.map((task) => ` ${task.id} · P${task.pri} · ${task.state === "in_progress" ? "in_progress" : "ready"} · age ${formatAgeMs(task.ageMs)} · active ${formatAgeMs(task.activityAgeMs)} · ${task.title}`));
48
49
  if (tasks.omitted > 0) {
49
- lines.push(` · +${tasks.omitted} more · shown in_progress then ready; each by priority, created, id`);
50
+ lines.push(` · +${tasks.omitted} more — keiyaku task ls`);
50
51
  }
51
52
  return `${lines.join("\n")}\n`;
52
53
  }
@@ -68,30 +69,34 @@ function terminalFailureSuffix(row) {
68
69
  function renderProjectionOmission(board) {
69
70
  if (!board.omitted)
70
71
  return undefined;
71
- const counts = ["failed", "dead", "done", "killed", "dismissed"]
72
+ const counts = ["failed", "dead", "done", "killed", "dismissed", "incompatible"]
72
73
  .flatMap((state) => {
73
74
  const count = board.omitted?.byState[state] ?? 0;
74
75
  return count > 0 ? [`${count} ${state}`] : [];
75
76
  });
76
- return ` · +${board.omitted.total} not shown (${counts.join(" · ")}) — keiyaku status --all`;
77
+ return ` · +${board.omitted.total} historical projections not shown (${counts.join(" · ")}) — keiyaku status --all`;
77
78
  }
78
79
  function renderProjectionAge(row) {
79
- const age = formatAgeMs(row.activityAgeMs);
80
+ const duration = formatAgeMs(row.durationMs) ?? "?";
81
+ const activity = formatAgeMs(row.activityAgeMs) ?? "?";
82
+ const facts = `dur ${duration} · active ${activity}`;
80
83
  if (row.state === "done")
81
- return age ? `done ${age}` : "done";
84
+ return `done · ${facts}`;
82
85
  if (row.state === "killed")
83
- return age ? `killed ${age}` : "killed";
86
+ return `killed · ${facts}`;
84
87
  if (row.state === "dismissed")
85
- return age ? `dismissed ${age}` : "dismissed";
88
+ return `dismissed · ${facts}`;
86
89
  if (row.state === "failed")
87
- return age ? `failed ${age}` : "failed";
90
+ return `failed · ${facts}`;
88
91
  if (row.state === "dead")
89
- return age ? `dead ${age}` : "dead";
92
+ return `dead · ${facts}`;
90
93
  if (row.state === "startup-timeout")
91
- return "startup timed out";
94
+ return `startup timed out · ${facts}`;
92
95
  if (row.state === "unknown")
93
- return "unknown";
94
- return formatAgeMs(row.activityAgeMs) ?? "";
96
+ return `unknown · ${facts}`;
97
+ if (row.state === "incompatible")
98
+ return "incompatible history";
99
+ return facts;
95
100
  }
96
101
  function renderProjectionTellCounts(row) {
97
102
  const { inbox, inflight, submitted, consumed } = row.tellCounts;
@@ -111,10 +116,11 @@ function projectionActivityState(row) {
111
116
  case "out": return "running";
112
117
  case "startup-timeout": return "stalled";
113
118
  case "unknown": return "unknown";
119
+ case "incompatible": return "unknown";
114
120
  }
115
121
  }
116
122
  function renderProjectionActivityLine(row) {
117
- if (row.state === "startup-timeout" || row.state === "unknown")
123
+ if (row.state === "startup-timeout" || row.state === "unknown" || row.state === "incompatible")
118
124
  return undefined;
119
125
  if (!row.activity)
120
126
  return undefined;
@@ -133,15 +139,15 @@ function renderProjectionRow(row) {
133
139
  : row.state);
134
140
  const identity = row.alias ? `${row.id} (${row.alias})` : row.id;
135
141
  let heading;
136
- if (row.state === "startup-timeout" || row.state === "unknown") {
142
+ if (row.state === "startup-timeout" || row.state === "unknown" || row.state === "incompatible") {
137
143
  const akuma = (row.akuma ?? "(unknown)").padEnd(18);
138
144
  heading = ` ${glyph} ${identity} ${akuma} ${renderProjectionAge(row)}${terminalFailureSuffix(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
139
145
  }
140
146
  else if (row.state === "minting") {
141
- heading = ` ${glyph} ${identity.padEnd(18)} (minting)${terminalFailureSuffix(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
147
+ heading = ` ${glyph} ${identity.padEnd(18)} (minting) · ${renderProjectionAge(row)}${terminalFailureSuffix(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
142
148
  }
143
149
  else {
144
- const age = renderProjectionAge(row).padEnd(9);
150
+ const age = renderProjectionAge(row);
145
151
  const akuma = (row.akuma ?? "(unknown)").padEnd(18);
146
152
  heading = ` ${glyph} ${identity} ${akuma} ${age}${renderProjectionTellCounts(row)}${terminalFailureSuffix(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
147
153
  }
@@ -154,7 +160,8 @@ function isTerminalProjectionStatus(row) {
154
160
  || row.state === "killed"
155
161
  || row.state === "dismissed"
156
162
  || row.state === "failed"
157
- || row.state === "dead";
163
+ || row.state === "dead"
164
+ || row.state === "incompatible";
158
165
  }
159
166
  export function renderProjectionFilteredStatus(board, projectionId) {
160
167
  const head = board.defaultHead ? shortCommitHash(board.defaultHead) : "(no HEAD)";
@@ -167,6 +174,9 @@ export function renderProjectionFilteredStatus(board, projectionId) {
167
174
  return renderTerminalFailureFace(projectionId, row.terminalFailure).join("\n");
168
175
  }
169
176
  const lines = [heading, "", ...renderProjectionRow(row), ""];
177
+ if (row.observation.compatibility === "incompatible") {
178
+ return `${lines.join("\n")}\n`;
179
+ }
170
180
  if (!isTerminalProjectionStatus(row))
171
181
  lines.push(`» ${formatTellCommand(projectionId, '"..."')}`);
172
182
  lines.push(`» keiyaku wait ${projectionId}`);
@@ -1,4 +1,5 @@
1
1
  import { repositoryCoordinateFromAddress } from "../commands/shared.js";
2
+ import { writeCliOutput } from "../output-stream-error-policy.js";
2
3
  import { renderResolvedAddressReceipt } from "./address.js";
3
4
  import { formatCallNextAction } from "./shared.js";
4
5
  function responseText(response) {
@@ -54,24 +55,24 @@ function conversationalFooter(response) {
54
55
  }
55
56
  return lines.join("\n");
56
57
  }
57
- export function writeSuccessResponse(command, flags, response) {
58
+ export async function writeSuccessResponse(command, flags, response) {
58
59
  // Plain tell is always silent; tell --wait owns the ordinary wait response.
59
60
  if (command === "tell" && flags.timeoutMs === undefined)
60
61
  return;
61
62
  if (command === "bind" && flags.dryRun) {
62
63
  // Canonical Markdown already ends with exactly one newline.
63
- process.stdout.write(responseText(response));
64
+ await writeCliOutput(process.stdout, responseText(response));
64
65
  return;
65
66
  }
66
67
  if (command !== "revive") {
67
- process.stdout.write(`${responseText(response)}\n`);
68
+ await writeCliOutput(process.stdout, `${responseText(response)}\n`);
68
69
  return;
69
70
  }
70
71
  const data = responseData(response);
71
72
  const answer = typeof data.summary === "string" ? data.summary.trim() : responseText(response).trim();
72
73
  const footer = conversationalFooter(response);
73
- process.stdout.write(`${answer}\n`);
74
+ await writeCliOutput(process.stdout, `${answer}\n`);
74
75
  if (footer) {
75
- process.stderr.write(`${footer}\n`);
76
+ await writeCliOutput(process.stderr, `${footer}\n`);
76
77
  }
77
78
  }
@@ -1,17 +1,11 @@
1
1
  import { commissionSlug } from "../../core/ids.js";
2
+ import { deriveTaskDisplay as deriveStructuredTaskDisplay, selectTaskList, } from "../../core/task/index.js";
2
3
  export function shortContractId(contractId) {
3
4
  return commissionSlug(contractId);
4
5
  }
5
6
  export function deriveTaskDisplay(task) {
6
- if (task.state === "done")
7
- return "done";
8
- if (task.state === "drop")
9
- return "dropped";
10
- if (task.contractId !== undefined)
11
- return `bound(${shortContractId(task.contractId)})`;
12
- if (task.state === "in_progress")
13
- return "in_progress";
14
- return task.blocked ? "blocked" : "ready";
7
+ const display = deriveStructuredTaskDisplay(task);
8
+ return display === "bound" ? `bound(${shortContractId(task.contractId)})` : display;
15
9
  }
16
10
  function renderTaskRowWithId(task, id) {
17
11
  return `${id} · P${task.pri} · ${deriveTaskDisplay(task)} · ${task.title}`;
@@ -25,43 +19,14 @@ export function renderTaskMutationRow(task) {
25
19
  export function renderTaskRow(task, completeId = false) {
26
20
  return completeId ? renderTaskMutationRow(task) : renderTaskListRow(task);
27
21
  }
28
- function displayOrder(display) {
29
- if (display === "in_progress")
30
- return 0;
31
- if (display === "ready")
32
- return 1;
33
- if (display === "blocked")
34
- return 2;
35
- if (display.startsWith("bound("))
36
- return 3;
37
- return display === "done" ? 4 : 5;
38
- }
39
- function compareText(left, right) {
40
- return left < right ? -1 : left > right ? 1 : 0;
41
- }
42
- function compareTasks(left, right) {
43
- const displayDifference = displayOrder(deriveTaskDisplay(left)) - displayOrder(deriveTaskDisplay(right));
44
- if (displayDifference !== 0)
45
- return displayDifference;
46
- if (left.pri !== right.pri)
47
- return left.pri - right.pri;
48
- const createdDifference = compareText(left.createdAt, right.createdAt);
49
- return createdDifference || compareText(left.id, right.id);
50
- }
51
- function selected(task, selection) {
52
- const display = deriveTaskDisplay(task);
53
- const terminal = display === "done" || display === "dropped";
54
- if (selection === "done")
55
- return terminal;
56
- if (selection === "default")
57
- return !terminal;
58
- return true;
59
- }
60
22
  export function groupAndSortTasks(tasks, selection = "default") {
61
- return tasks.filter((task) => selected(task, selection)).sort(compareTasks);
23
+ return selectTaskList(tasks, { selection });
62
24
  }
63
25
  export function renderTaskList(tasks, options = {}) {
64
- return groupAndSortTasks(tasks, options.selection).map(renderTaskListRow).join("\n");
26
+ return selectTaskList(tasks, options).map(renderTaskListRow).join("\n");
27
+ }
28
+ export function renderSelectedTaskList(tasks) {
29
+ return tasks.map(renderTaskListRow).join("\n");
65
30
  }
66
31
  function renderIds(ids) {
67
32
  return ids.length === 0 ? "(none)" : ids.join(", ");
@@ -128,6 +93,7 @@ export function taskJson(task) {
128
93
  from: [...task.from],
129
94
  contractId: task.contractId ?? null,
130
95
  createdAt: task.createdAt,
96
+ updatedAt: task.updatedAt ?? task.createdAt,
131
97
  creator: task.creator,
132
98
  };
133
99
  }
@@ -151,3 +117,6 @@ export function renderTaskShowJson(task, tree) {
151
117
  export function renderTaskListJson(tasks, selection = "default") {
152
118
  return `${JSON.stringify(groupAndSortTasks(tasks, selection).map(taskJson))}\n`;
153
119
  }
120
+ export function renderSelectedTaskListJson(tasks) {
121
+ return `${JSON.stringify(tasks.map(taskJson))}\n`;
122
+ }
@@ -14,6 +14,10 @@ export function terminalFailureColdStatement(failure) {
14
14
  switch (failure.code) {
15
15
  case "SUBAGENT_TIMEOUT":
16
16
  return "the akuma went silent";
17
+ case "SUBAGENT_TRANSPORT_ERROR":
18
+ return "the provider transport failed";
19
+ case "SUBAGENT_POLICY_BUDGET":
20
+ return "the execution policy budget expired";
17
21
  case "SUBAGENT_PROVIDER_UNAVAILABLE":
18
22
  return "the provider did not answer";
19
23
  case "SUBAGENT_NON_ZERO_EXIT":
@@ -29,8 +33,9 @@ function terminalFailureEvidence(failure, statement) {
29
33
  && failure.code === "SUBAGENT_NON_ZERO_EXIT"
30
34
  && failure.exitCode !== undefined;
31
35
  const parts = [];
32
- if (failure.timeoutMs !== undefined)
33
- parts.push(`idle ${failure.timeoutMs}ms`);
36
+ if (failure.timeoutMs !== undefined) {
37
+ parts.push(failure.code === "SUBAGENT_POLICY_BUDGET" ? `budget ${failure.timeoutMs}ms` : `idle ${failure.timeoutMs}ms`);
38
+ }
34
39
  if (failure.signal)
35
40
  parts.push(`signal ${failure.signal}`);
36
41
  if (!exitAlreadyInStatement && failure.exitCode !== undefined)
@@ -1,4 +1,4 @@
1
- import { formatTimelineDuration, renderProjectionActivity, } from "./projection-activity.js";
1
+ import { formatTimelineDuration, renderProjectionActivity, renderProjectionActivitySummary, } from "./projection-activity.js";
2
2
  import { displayColumns, resolveLineColumns, truncateColumns } from "./line-width.js";
3
3
  import { formatTellCommand, projectionRejoinHintLines, textResponse } from "./shared.js";
4
4
  import { renderTerminalFailureFace } from "./terminal-failure.js";
@@ -220,7 +220,22 @@ export function buildWaitResponse(projectionId, result, options = {}) {
220
220
  if (rendered.lines.length === 0 && state === "running") {
221
221
  lines.push(" not a word out of it yet — pulse is steady");
222
222
  }
223
- const nextActions = nextActionLines(options.alias ?? projectionId, result, state);
223
+ // Live/timeout observation faces keep the bounded activity window and add the
224
+ // existing compact rollup after those rows. Terminal faces already own end└
225
+ // and must not duplicate it; returned response-only faces never reach here.
226
+ if (!rendered.hasEndAnchor) {
227
+ const summary = renderProjectionActivitySummary(snapshot.activity, {
228
+ state,
229
+ ...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
230
+ maxColumns,
231
+ });
232
+ if (summary) {
233
+ lines.push(summary);
234
+ }
235
+ }
236
+ const nextActions = state === "running" && options.runningActions === false
237
+ ? []
238
+ : nextActionLines(options.alias ?? projectionId, result, state);
224
239
  if (nextActions.length > 0)
225
240
  lines.push("");
226
241
  lines.push(...nextActions);
@@ -237,15 +252,28 @@ export function buildProjectionWaitSetResponse(result) {
237
252
  const rendered = result.results
238
253
  .slice()
239
254
  .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
240
- .map((member) => ({ id: member.id, response: buildWaitResponse(member.id, member.result) }));
241
- const text = rendered.map(({ id, response }) => `${id}\n${response.text}`).join("\n\n");
242
- const selectionSucceeded = result.selection === "all"
243
- ? result.results.every((member) => member.result.state === "terminal" && member.result.verdict.state === "completed")
244
- : result.selectedId !== undefined;
255
+ .map((member) => ({
256
+ id: member.id,
257
+ result: member.result,
258
+ response: buildWaitResponse(member.id, member.result, { runningActions: false }),
259
+ }));
260
+ const liveIds = rendered
261
+ .filter((member) => member.result.state === "timeout")
262
+ .map((member) => member.id);
263
+ const blocks = rendered.map(({ id, response }) => `${id}\n${response.text}`);
264
+ if (liveIds.length > 0) {
265
+ blocks.push([
266
+ "Come back for the result:",
267
+ ` » keiyaku wait ${liveIds.join(" ")} --${result.selection} [--timeout DURATION]`,
268
+ "Steer them while they work:",
269
+ ` » ${formatTellCommand("<projection-id>", '"your words"')}`,
270
+ ].join("\n"));
271
+ }
272
+ const text = blocks.join("\n\n");
245
273
  const exitCode = rendered.reduce((current, { response }) => {
246
274
  if (response.exitCode === 130)
247
275
  return 130;
248
276
  return response.exitCode === 1 ? 1 : current;
249
- }, selectionSucceeded ? undefined : 1);
277
+ }, undefined);
250
278
  return exitCode === undefined ? textResponse(text) : { ...textResponse(text), exitCode };
251
279
  }
@@ -10,6 +10,7 @@ export const ENV_KEYS = {
10
10
  AGENT_OVERRIDE: "KEIYAKU_AGENT_OVERRIDE",
11
11
  DISABLE_SUBAGENT_SPAWN: DISABLE_SUBAGENT_SPAWN_ENV_KEY,
12
12
  SUBAGENT_EXEC_IDLE_TIMEOUT_MS: "KEIYAKU_SUBAGENT_EXEC_IDLE_TIMEOUT_MS",
13
+ SUBAGENT_EXEC_MAX_DURATION_MS: "KEIYAKU_SUBAGENT_EXEC_MAX_DURATION_MS",
13
14
  CODEX_EXEC_MAX_CAPTURE_CHARS: "KEIYAKU_CODEX_EXEC_MAX_CAPTURE_CHARS",
14
15
  CLAUDE_EXEC_MAX_CAPTURE_CHARS: "KEIYAKU_CLAUDE_EXEC_MAX_CAPTURE_CHARS",
15
16
  OPENCODE_EXEC_MAX_CAPTURE_CHARS: "KEIYAKU_OPENCODE_EXEC_MAX_CAPTURE_CHARS",
@@ -26,6 +27,7 @@ const ENV_KEY_ENTRIES = [
26
27
  { key: ENV_KEYS.AGENT_OVERRIDE, defaultValue: "", description: "Override default agent name", category: "subagent" },
27
28
  { key: ENV_KEYS.DISABLE_SUBAGENT_SPAWN, defaultValue: "0", description: "Disable subprocess subagent spawning when set to 1", category: "subagent" },
28
29
  { key: ENV_KEYS.SUBAGENT_EXEC_IDLE_TIMEOUT_MS, defaultValue: "300000", description: "Subagent provider-ingress idle timeout in milliseconds", category: "subagent" },
30
+ { key: ENV_KEYS.SUBAGENT_EXEC_MAX_DURATION_MS, defaultValue: "3600000", description: "Subagent absolute execution policy budget in milliseconds", category: "subagent" },
29
31
  { key: ENV_KEYS.CODEX_EXEC_MAX_CAPTURE_CHARS, defaultValue: "200000", description: "Codex runtime output/diagnostics capture cap", category: "subagent" },
30
32
  { key: ENV_KEYS.CLAUDE_EXEC_MAX_CAPTURE_CHARS, defaultValue: "200000", description: "Claude runtime output/diagnostics capture cap", category: "subagent" },
31
33
  { key: ENV_KEYS.OPENCODE_EXEC_MAX_CAPTURE_CHARS, defaultValue: "200000", description: "OpenCode runtime output/diagnostics capture cap", category: "subagent" },
@@ -141,6 +141,7 @@ const subagentSchema = z.object({
141
141
  nameOverride: z.string().default(getEnvDefaultValue(ENV_KEYS.AGENT_OVERRIDE)).transform((value) => value.trim()),
142
142
  disableSpawn: booleanFlagEnv(ENV_KEYS.DISABLE_SUBAGENT_SPAWN),
143
143
  execIdleTimeoutMs: positiveIntEnv(ENV_KEYS.SUBAGENT_EXEC_IDLE_TIMEOUT_MS),
144
+ execMaxDurationMs: positiveIntEnv(ENV_KEYS.SUBAGENT_EXEC_MAX_DURATION_MS),
144
145
  });
145
146
  const codexSchema = z.object({
146
147
  execMaxCaptureChars: positiveIntEnv(ENV_KEYS.CODEX_EXEC_MAX_CAPTURE_CHARS),
@@ -175,6 +176,7 @@ const envSchema = z
175
176
  [ENV_KEYS.AGENT_OVERRIDE]: z.string().optional(),
176
177
  [ENV_KEYS.DISABLE_SUBAGENT_SPAWN]: z.string().optional(),
177
178
  [ENV_KEYS.SUBAGENT_EXEC_IDLE_TIMEOUT_MS]: z.string().optional(),
179
+ [ENV_KEYS.SUBAGENT_EXEC_MAX_DURATION_MS]: z.string().optional(),
178
180
  [ENV_KEYS.CODEX_EXEC_MAX_CAPTURE_CHARS]: z.string().optional(),
179
181
  [ENV_KEYS.CLAUDE_EXEC_MAX_CAPTURE_CHARS]: z.string().optional(),
180
182
  [ENV_KEYS.OPENCODE_EXEC_MAX_CAPTURE_CHARS]: z.string().optional(),
@@ -195,6 +197,7 @@ const envSchema = z
195
197
  nameOverride: rawEnv[ENV_KEYS.AGENT_OVERRIDE],
196
198
  disableSpawn: rawEnv[ENV_KEYS.DISABLE_SUBAGENT_SPAWN],
197
199
  execIdleTimeoutMs: rawEnv[ENV_KEYS.SUBAGENT_EXEC_IDLE_TIMEOUT_MS],
200
+ execMaxDurationMs: rawEnv[ENV_KEYS.SUBAGENT_EXEC_MAX_DURATION_MS],
198
201
  }),
199
202
  codex: codexSchema.parse({
200
203
  execMaxCaptureChars: rawEnv[ENV_KEYS.CODEX_EXEC_MAX_CAPTURE_CHARS],