@astrosheep/keiyaku 2.9.4 → 2.9.5

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 (126) 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 +21 -1
  5. package/build/agents/harness/projection.js +147 -75
  6. package/build/agents/providers/codex-app-server/adapter.js +32 -0
  7. package/build/agents/providers/codex-app-server/events.js +23 -0
  8. package/build/agents/providers/pi/events.js +4 -3
  9. package/build/agents/types.js +2 -0
  10. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  11. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  12. package/build/cli/commands/akuma/view/meta.js +1 -1
  13. package/build/cli/commands/contract/amend/meta.js +4 -4
  14. package/build/cli/commands/contract/arc/handler.js +1 -1
  15. package/build/cli/commands/contract/arc/meta.js +4 -4
  16. package/build/cli/commands/contract/audit/handler.js +16 -0
  17. package/build/cli/commands/contract/audit/meta.js +16 -0
  18. package/build/cli/commands/contract/bind/meta.js +20 -11
  19. package/build/cli/commands/contract/catalog.js +2 -0
  20. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  21. package/build/cli/commands/contract/log/meta.js +1 -1
  22. package/build/cli/commands/contract/petition/handler.js +1 -2
  23. package/build/cli/commands/contract/petition/meta.js +6 -6
  24. package/build/cli/commands/contract/renew/handler.js +2 -2
  25. package/build/cli/commands/contract/renew/meta.js +4 -4
  26. package/build/cli/commands/metadata.js +3 -2
  27. package/build/cli/commands/projection/call/meta.js +4 -1
  28. package/build/cli/commands/projection/kill/meta.js +1 -1
  29. package/build/cli/commands/projection/revive/meta.js +4 -1
  30. package/build/cli/commands/projection/status/meta.js +1 -1
  31. package/build/cli/commands/projection/tell/handler.js +1 -1
  32. package/build/cli/commands/projection/tell/meta.js +4 -1
  33. package/build/cli/commands/projection/wait/meta.js +1 -1
  34. package/build/cli/commands/skills/install/meta.js +1 -1
  35. package/build/cli/commands/skills/skills/meta.js +1 -1
  36. package/build/cli/commands/system/completion/meta.js +1 -1
  37. package/build/cli/commands/system/dump-env/meta.js +1 -1
  38. package/build/cli/commands/system/guide/meta.js +1 -1
  39. package/build/cli/commands/task/add/handler.js +2 -2
  40. package/build/cli/commands/task/add/meta.js +4 -1
  41. package/build/cli/commands/task/doctor/meta.js +1 -1
  42. package/build/cli/commands/task/done/meta.js +1 -1
  43. package/build/cli/commands/task/drop/meta.js +1 -1
  44. package/build/cli/commands/task/ls/handler.js +10 -4
  45. package/build/cli/commands/task/ls/meta.js +3 -3
  46. package/build/cli/commands/task/show/meta.js +1 -1
  47. package/build/cli/commands/task/start/meta.js +1 -1
  48. package/build/cli/commands/task/stop/meta.js +1 -1
  49. package/build/cli/commands/task/task/meta.js +1 -1
  50. package/build/cli/commands/task/update/handler.js +2 -3
  51. package/build/cli/commands/task/update/meta.js +4 -1
  52. package/build/cli/flags.js +18 -3
  53. package/build/cli/help.js +3 -3
  54. package/build/cli/index.js +59 -75
  55. package/build/cli/output-stream-error-policy.js +19 -5
  56. package/build/cli/parse-flags.js +19 -2
  57. package/build/cli/parse-metadata.js +1 -7
  58. package/build/cli/parse-selectors.js +1 -0
  59. package/build/cli/parse.js +16 -7
  60. package/build/cli/projection-address.js +6 -1
  61. package/build/cli/render/audit.js +121 -0
  62. package/build/cli/render/misc.js +3 -1
  63. package/build/cli/render/projection-activity.js +12 -6
  64. package/build/cli/render/status.js +28 -18
  65. package/build/cli/render/success-response.js +6 -5
  66. package/build/cli/render/task.js +12 -43
  67. package/build/cli/render/terminal-failure.js +7 -2
  68. package/build/cli/render/wait.js +36 -8
  69. package/build/config/env-keys.js +2 -0
  70. package/build/config/env.js +3 -0
  71. package/build/core/amend.js +20 -94
  72. package/build/core/arc.js +14 -6
  73. package/build/core/audit.js +650 -0
  74. package/build/core/bind.js +1 -1
  75. package/build/core/call/execution.js +1 -0
  76. package/build/core/contract-view.js +2 -3
  77. package/build/core/draft.js +33 -20
  78. package/build/core/entry.js +3 -4
  79. package/build/core/markdown/lex.js +2 -1
  80. package/build/core/projection/generation/database.js +41 -16
  81. package/build/core/projection/generation/model.js +22 -2
  82. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  83. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  84. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  85. package/build/core/projection/generation/store.js +1 -1
  86. package/build/core/projection/generation/transitions.js +19 -3
  87. package/build/core/projection/index.js +2 -2
  88. package/build/core/projection/projection-execution-observer.js +35 -21
  89. package/build/core/projection/projection-life-observer.js +16 -2
  90. package/build/core/projection/projection-status.js +67 -22
  91. package/build/core/projection/projection-terminal-failure.js +2 -0
  92. package/build/core/projection/projection-wait.js +6 -9
  93. package/build/core/projection/projection-wake.js +30 -6
  94. package/build/core/renew.js +41 -22
  95. package/build/core/scope.js +129 -2
  96. package/build/core/seal.js +36 -15
  97. package/build/core/settlement/petition-claim-gates.js +4 -33
  98. package/build/core/settlement/petition-preview.js +1 -1
  99. package/build/core/settlement/petition.js +10 -3
  100. package/build/core/settlement/settlement.js +3 -10
  101. package/build/core/settlement/verification.js +59 -0
  102. package/build/core/status/board.js +48 -28
  103. package/build/core/stored-agent-event.js +3 -2
  104. package/build/core/task/board.js +1 -0
  105. package/build/core/task/commands.js +20 -12
  106. package/build/core/task/document.js +5 -0
  107. package/build/core/task/index.js +1 -1
  108. package/build/core/task/model.js +8 -1
  109. package/build/core/task/query.js +172 -0
  110. package/build/core/task/settlement-git.js +115 -16
  111. package/build/core/task/settlement-policy.js +17 -9
  112. package/build/core/task/source-board.js +1 -0
  113. package/build/core/task/task-contract.js +125 -51
  114. package/build/core/task/task-store-repository.js +1 -0
  115. package/build/core/task/task.js +2 -0
  116. package/build/flow-error.js +2 -4
  117. package/build/generated/version.js +2 -2
  118. package/build/git/diff/parsers.js +23 -13
  119. package/build/git/diff/structured.js +127 -0
  120. package/build/index.js +7 -7
  121. package/build/telemetry/logger.js +4 -2
  122. package/package.json +1 -1
  123. package/skills/keiyaku/SKILL.md +2 -2
  124. package/skills/keiyaku-akuma/SKILL.md +17 -6
  125. package/skills/keiyaku-task/SKILL.md +16 -7
  126. package/skills/keiyaku-workflow/SKILL.md +42 -13
@@ -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],
@@ -5,91 +5,33 @@ import { refreshContractView } from "./contract-view.js";
5
5
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
6
6
  import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
7
7
  import { resolveDefaultBranch } from "./status/drift.js";
8
- import { isBlankScopePatternLine, normalizeScopePattern, ScopePatternError } from "./scope.js";
8
+ import { markdownSourceSections, normalizeScopePattern, validateScopeFragmentLines } from "./scope.js";
9
9
  import { normalizeSectionTitle } from "./markdown/titles.js";
10
10
  import { resolveContractDeliveryCwd } from "./worktree-path.js";
11
11
  function amendScopeHeading(name) {
12
12
  return `## Scope ${name[0].toUpperCase()}${name.slice(1)}`;
13
13
  }
14
- function sourceLines(markdown) {
15
- const lines = [];
16
- let start = 0;
17
- while (start < markdown.length) {
18
- const newline = markdown.indexOf("\n", start);
19
- const end = newline === -1 ? markdown.length : newline;
20
- const next = newline === -1 ? markdown.length : newline + 1;
21
- const raw = markdown.slice(start, end);
22
- lines.push({ text: raw.endsWith("\r") ? raw.slice(0, -1) : raw, start, end, next });
23
- start = next;
24
- }
25
- return lines;
26
- }
27
- function amendHeading(line) {
28
- const match = line.match(/^ {0,3}(#{1,2})(?!#)[ \t]+(.+)$/);
29
- if (!match)
30
- return null;
31
- const level = match[1].length;
32
- const title = normalizeSectionTitle(match[2]);
33
- if (level !== 2)
34
- return { level };
35
- if (title === "scope add")
36
- return { level, name: "add" };
37
- return { level };
38
- }
39
- function amendFence(line) {
40
- const match = line.match(/^ {0,3}(`{3,}|~{3,})/);
41
- if (!match)
42
- return null;
43
- const marker = match[1];
44
- return { delimiter: marker[0], length: marker.length };
45
- }
46
14
  function parseAmendScopeSections(markdown) {
47
- const lines = sourceLines(markdown);
48
15
  const sections = {};
49
- let active;
50
- let fence;
51
- let frontMatterEnd = -1;
52
- if (lines[0]?.text.trim() === "---") {
53
- for (let index = 1; index < lines.length; index += 1) {
54
- if (lines[index].text.trim() === "---") {
55
- frontMatterEnd = index;
56
- break;
57
- }
58
- }
59
- }
60
- const finishActive = (end) => {
61
- if (!active)
62
- return;
63
- sections[active.name] = { ...active, end };
64
- active = undefined;
65
- };
66
- for (let index = 0; index < lines.length; index += 1) {
67
- const line = lines[index];
68
- if (index <= frontMatterEnd)
69
- continue;
70
- const marker = amendFence(line.text);
71
- if (fence) {
72
- if (marker && marker.delimiter === fence.delimiter && marker.length >= fence.length) {
73
- fence = undefined;
74
- }
16
+ for (const section of markdownSourceSections(markdown)) {
17
+ if (section.level !== 2)
75
18
  continue;
19
+ const title = normalizeSectionTitle(section.title);
20
+ if (title === "scope add") {
21
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", "Scope Add is obsolete; use ## Scope Append with a fenced gitignore-subset block");
76
22
  }
77
- if (marker) {
78
- fence = marker;
23
+ if (title !== "scope append")
79
24
  continue;
25
+ if (sections.append) {
26
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input may contain ${amendScopeHeading("append")} at most once`);
80
27
  }
81
- const heading = amendHeading(line.text);
82
- if (!heading)
83
- continue;
84
- finishActive(line.start);
85
- if (!heading.name)
86
- continue;
87
- if (sections[heading.name]) {
88
- throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input may contain ${amendScopeHeading(heading.name)} at most once`);
89
- }
90
- active = { name: heading.name, start: line.start, contentStart: line.next };
28
+ sections.append = {
29
+ name: "append",
30
+ start: section.start,
31
+ contentStart: section.contentStart,
32
+ end: section.end,
33
+ };
91
34
  }
92
- finishActive(markdown.length);
93
35
  let prose = markdown;
94
36
  for (const section of Object.values(sections).sort((left, right) => right.start - left.start)) {
95
37
  prose = `${prose.slice(0, section.start)}${prose.slice(section.end)}`;
@@ -97,30 +39,14 @@ function parseAmendScopeSections(markdown) {
97
39
  return { sections, prose };
98
40
  }
99
41
  function scopePatterns(markdown, section) {
100
- const patterns = markdown
101
- .slice(section.contentStart, section.end)
102
- .split(/\r?\n/)
103
- .filter((line) => !isBlankScopePatternLine(line))
104
- .map((pattern) => {
105
- try {
106
- // Do not trim() here: gitignore treats trailing `\ ` as a significant space.
107
- return normalizeScopePattern(pattern);
108
- }
109
- catch (error) {
110
- if (error instanceof ScopePatternError) {
111
- throw new FlowError("INVALID_KEIYAKU_DRAFT", error.message);
112
- }
113
- throw error;
114
- }
42
+ return validateScopeFragmentLines(markdown.slice(section.contentStart, section.end).split(/\r?\n/), {
43
+ heading: amendScopeHeading(section.name),
44
+ emptyMessage: `amend input has an empty ${amendScopeHeading(section.name)} section`,
115
45
  });
116
- if (patterns.length === 0) {
117
- throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input has an empty ${amendScopeHeading(section.name)} section`);
118
- }
119
- return patterns;
120
46
  }
121
47
  export function buildAmendInput(markdown, cwd, contractId, contractAddressSource) {
122
48
  const { sections, prose } = parseAmendScopeSections(markdown);
123
- const add = sections.add ? scopePatterns(markdown, sections.add) : [];
49
+ const append = sections.append ? scopePatterns(markdown, sections.append) : [];
124
50
  if (!prose.trim()) {
125
51
  throw new FlowError("EMPTY_PARAM", "amendment cannot be empty");
126
52
  }
@@ -129,7 +55,7 @@ export function buildAmendInput(markdown, cwd, contractId, contractAddressSource
129
55
  ...(contractId ? { contractId } : {}),
130
56
  ...(contractAddressSource ? { contractAddressSource } : {}),
131
57
  amendment: prose,
132
- ...(sections.add ? { scopeDelta: { add } } : {}),
58
+ ...(sections.append ? { scopeDelta: { add: append } } : {}),
133
59
  };
134
60
  }
135
61
  function requireAmendment(value) {
package/build/core/arc.js CHANGED
@@ -9,7 +9,7 @@ import { parseCommandMarkdown, requireMarkdownTextSection, requireMarkdownTitle,
9
9
  import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
10
10
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
11
11
  import { refreshContractView } from "./contract-view.js";
12
- import { findOpenArc, getMaxArc, sealCurrentArc } from "./seal.js";
12
+ import { appendScopeTerms, findOpenArc, getMaxArc, sealCurrentArc } from "./seal.js";
13
13
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
14
14
  import { resolveDefaultBranch } from "./status/drift.js";
15
15
  import { persistResponseHistory } from "./transcripts.js";
@@ -61,6 +61,14 @@ export async function openArc(input) {
61
61
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
62
62
  }
63
63
  assertArcOpenable(input.contractId, ledger.entries);
64
+ await appendScopeTerms({
65
+ cwd: input.cwd,
66
+ renderCwd,
67
+ contractId: input.contractId,
68
+ patterns: input.appendScope,
69
+ actor: input.actor,
70
+ nowMs: input.nowMs,
71
+ });
64
72
  // Seal capture needs delivery HEAD; ledger append may use hub or delivery (shared refs).
65
73
  const sealed = await sealCurrentArc({
66
74
  cwd: renderCwd,
@@ -68,7 +76,6 @@ export async function openArc(input) {
68
76
  contractId: input.contractId,
69
77
  sealedBy: "arc",
70
78
  nowMs: input.nowMs,
71
- waivePaths: input.waivePaths,
72
79
  });
73
80
  const at = new Date(input.nowMs).toISOString();
74
81
  let arcNumber = 0;
@@ -93,7 +100,7 @@ export async function openArc(input) {
93
100
  await refreshContractView({ ledgerCwd: input.cwd, renderCwd, contractId: input.contractId });
94
101
  return { contractId: input.contractId, arc: arcNumber, ledgerHead: result.head, sealed };
95
102
  }
96
- export function buildArcInput(content, cwd, waivePaths = [], contractId, contractAddressSource) {
103
+ export function buildArcInput(content, cwd, appendScope = [], contractId, contractAddressSource) {
97
104
  const parts = parseCommandMarkdown(content);
98
105
  return {
99
106
  cwd,
@@ -102,7 +109,7 @@ export function buildArcInput(content, cwd, waivePaths = [], contractId, contrac
102
109
  title: requireMarkdownTitle(parts),
103
110
  objective: requireMarkdownTextSection(parts, "Objective"),
104
111
  brief: requireMarkdownTextSection(parts, "Brief"),
105
- waivePaths,
112
+ appendScope,
106
113
  };
107
114
  }
108
115
  async function readBranchLabels(cwd) {
@@ -126,16 +133,17 @@ export async function arcKeiyaku(input) {
126
133
  const title = normalizeInlineTitle(requireText("title", input.title));
127
134
  const objective = requireText("objective", input.objective);
128
135
  const brief = requireText("brief", input.brief);
136
+ const nowMs = Date.now();
129
137
  const opened = await openArc({
130
138
  cwd: input.cwd,
131
139
  renderCwd,
132
140
  contractId,
133
141
  actor: ACTOR_IDENTITY,
134
- nowMs: Date.now(),
142
+ nowMs,
135
143
  title,
136
144
  objective,
137
145
  brief,
138
- waivePaths: input.waivePaths,
146
+ appendScope: input.appendScope,
139
147
  });
140
148
  const { currentBranch, baseBranch } = await readBranchLabels(renderCwd);
141
149
  const summary = opened.sealed