@tt-a1i/openpi 0.1.0

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 (114) hide show
  1. package/README.md +643 -0
  2. package/SETUP.md +74 -0
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-package.png +0 -0
  5. package/assets/readme-hero-mobile.svg +72 -0
  6. package/assets/readme-hero.svg +118 -0
  7. package/assets/readme-runtime-mobile.svg +91 -0
  8. package/assets/readme-runtime.svg +111 -0
  9. package/extensions/ask-user/handoff.ts +205 -0
  10. package/extensions/ask-user/index.ts +1110 -0
  11. package/extensions/ask-user/limits.ts +89 -0
  12. package/extensions/ask-user/prompt.ts +76 -0
  13. package/extensions/background-terminals/index.ts +653 -0
  14. package/extensions/background-terminals/src/domain.ts +99 -0
  15. package/extensions/background-terminals/src/manager.ts +989 -0
  16. package/extensions/background-terminals/src/output.ts +84 -0
  17. package/extensions/background-terminals/src/prompt.ts +195 -0
  18. package/extensions/background-terminals/src/result-delivery.ts +43 -0
  19. package/extensions/background-terminals/src/runtime.ts +36 -0
  20. package/extensions/background-terminals/src/ui/output-view.ts +55 -0
  21. package/extensions/background-terminals/src/ui/ps.ts +642 -0
  22. package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
  23. package/extensions/background-terminals/src/watch.ts +192 -0
  24. package/extensions/context-pivot/index.ts +222 -0
  25. package/extensions/copy-all/index.ts +65 -0
  26. package/extensions/cron/index.ts +173 -0
  27. package/extensions/cron/schedule.ts +127 -0
  28. package/extensions/file-mutation-display/index.ts +105 -0
  29. package/extensions/file-mutation-display/render.ts +107 -0
  30. package/extensions/file-search/index.ts +515 -0
  31. package/extensions/file-search/src/args.ts +129 -0
  32. package/extensions/file-search/src/binaries.ts +419 -0
  33. package/extensions/file-search/src/output.ts +142 -0
  34. package/extensions/file-search/src/process.ts +309 -0
  35. package/extensions/file-search/src/prompt.ts +53 -0
  36. package/extensions/git-info/index.ts +272 -0
  37. package/extensions/git-info/src/changed-files-view.ts +414 -0
  38. package/extensions/git-info/src/process.ts +107 -0
  39. package/extensions/git-info/src/refresh-coordinator.ts +13 -0
  40. package/extensions/git-info/src/runtime.ts +28 -0
  41. package/extensions/goal/controller.ts +794 -0
  42. package/extensions/goal/index.ts +521 -0
  43. package/extensions/goal/prompts.ts +122 -0
  44. package/extensions/goal/state.ts +763 -0
  45. package/extensions/goal/ui.ts +158 -0
  46. package/extensions/model-info/index.ts +234 -0
  47. package/extensions/plan-mode/bash-policy.ts +313 -0
  48. package/extensions/plan-mode/index.ts +539 -0
  49. package/extensions/post-edit/index.ts +129 -0
  50. package/extensions/sessions/LICENSE.upstream +21 -0
  51. package/extensions/sessions/git-stats.ts +226 -0
  52. package/extensions/sessions/index.ts +1092 -0
  53. package/extensions/sessions/sessions.ts +385 -0
  54. package/extensions/setup/index.ts +408 -0
  55. package/extensions/shared/activity-status.ts +65 -0
  56. package/extensions/shared/below-editor-navigation.ts +343 -0
  57. package/extensions/shared/child-session.ts +352 -0
  58. package/extensions/shared/context-utilization.ts +47 -0
  59. package/extensions/shared/dashboard-state.ts +102 -0
  60. package/extensions/shared/plan-mode-state.ts +65 -0
  61. package/extensions/shared/setup-config.ts +971 -0
  62. package/extensions/shared/subagent-roles.ts +22 -0
  63. package/extensions/shared/terminal-text.ts +38 -0
  64. package/extensions/shared/tool-call-timeout.ts +104 -0
  65. package/extensions/shared/worktree.ts +526 -0
  66. package/extensions/subagents/index.ts +1225 -0
  67. package/extensions/subagents/navigation.ts +121 -0
  68. package/extensions/subagents/src/agent-types.ts +543 -0
  69. package/extensions/subagents/src/backend.ts +63 -0
  70. package/extensions/subagents/src/backends/pi.ts +493 -0
  71. package/extensions/subagents/src/backends/stub.ts +296 -0
  72. package/extensions/subagents/src/by-the-way.ts +21 -0
  73. package/extensions/subagents/src/domain.ts +271 -0
  74. package/extensions/subagents/src/format.ts +48 -0
  75. package/extensions/subagents/src/manager.ts +769 -0
  76. package/extensions/subagents/src/prompt.ts +190 -0
  77. package/extensions/subagents/src/result-delivery.ts +20 -0
  78. package/extensions/subagents/src/runtime.ts +51 -0
  79. package/extensions/subagents/src/ui/takeover.ts +615 -0
  80. package/extensions/subagents/src/ui/transcript.ts +293 -0
  81. package/extensions/subagents/src/ui/wait-result.ts +89 -0
  82. package/extensions/suggestions/index.ts +172 -0
  83. package/extensions/suggestions/src/config.ts +12 -0
  84. package/extensions/suggestions/src/predictor.ts +147 -0
  85. package/extensions/suggestions/src/prompt.ts +20 -0
  86. package/extensions/suggestions/src/transcript.ts +233 -0
  87. package/extensions/suggestions/src/ui.ts +224 -0
  88. package/extensions/tasks/index.ts +512 -0
  89. package/extensions/tasks/tasks.ts +649 -0
  90. package/extensions/tasks/ui.ts +421 -0
  91. package/extensions/turn-time/index.ts +61 -0
  92. package/extensions/ui-customization/footer.ts +512 -0
  93. package/extensions/ui-customization/index.ts +217 -0
  94. package/extensions/workflows/acceptance.ts +298 -0
  95. package/extensions/workflows/artifacts.ts +225 -0
  96. package/extensions/workflows/controller.ts +210 -0
  97. package/extensions/workflows/dashboard.ts +1226 -0
  98. package/extensions/workflows/index.ts +1884 -0
  99. package/extensions/workflows/journal.ts +188 -0
  100. package/extensions/workflows/meta.ts +250 -0
  101. package/extensions/workflows/model.ts +423 -0
  102. package/extensions/workflows/navigation.ts +93 -0
  103. package/extensions/workflows/prompt.ts +212 -0
  104. package/extensions/workflows/replay-safety.ts +577 -0
  105. package/extensions/workflows/runner.ts +786 -0
  106. package/extensions/workflows/sandbox-child.cjs +402 -0
  107. package/extensions/workflows/sandbox.ts +397 -0
  108. package/extensions/workflows/serialization.ts +162 -0
  109. package/extensions/workflows/worktree-handoff.ts +216 -0
  110. package/package.json +87 -0
  111. package/scripts/prepare-effect-tsgo.mjs +16 -0
  112. package/skills/background-terminals/SKILL.md +30 -0
  113. package/skills/subagents/SKILL.md +15 -0
  114. package/themes/github-dark-default.json +89 -0
@@ -0,0 +1,217 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ ReadonlyFooterDataProvider,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ getCapabilities,
8
+ hyperlink,
9
+ truncateToWidth,
10
+ visibleWidth,
11
+ } from "@earendil-works/pi-tui";
12
+ import {
13
+ loadSetupConfig,
14
+ SETUP_CONFIG_CHANGED_CHANNEL,
15
+ } from "../shared/setup-config.ts";
16
+ import {
17
+ emptyGitInfoState,
18
+ emptyModelInfoState,
19
+ GIT_INFO_CHANNEL,
20
+ MODEL_INFO_CHANNEL,
21
+ REFRESH_CHANNEL,
22
+ isGitInfoState,
23
+ isModelInfoState,
24
+ type GitInfoState,
25
+ type ModelInfoState,
26
+ } from "../shared/dashboard-state.ts";
27
+ import { formatDirectory, renderFooter } from "./footer.ts";
28
+
29
+ export { buildFooterContent, formatDirectory, renderFooter } from "./footer.ts";
30
+
31
+ type Rgb = [number, number, number];
32
+
33
+ const RESET = "\x1b[0m";
34
+ const BOLD = "\x1b[1m";
35
+ const PALETTE: Rgb[] = [
36
+ [22, 83, 189],
37
+ [48, 129, 247],
38
+ [93, 171, 255],
39
+ [151, 205, 255],
40
+ [93, 171, 255],
41
+ [48, 129, 247],
42
+ ];
43
+ const TITLE_LINES = [
44
+ " ██████╗ ██╗ ",
45
+ " ██╔══██╗ ██║ ",
46
+ " ██████╔╝ ██║ ",
47
+ " ██╔═══╝ ██║ ",
48
+ " ██║ ██║ ",
49
+ " ╚═╝ ╚═╝ ",
50
+ ];
51
+
52
+ function mix(a: number, b: number, amount: number) {
53
+ return Math.round(a + (b - a) * amount);
54
+ }
55
+
56
+ function sampleGradient(position: number) {
57
+ const wrapped = ((position % 1) + 1) % 1;
58
+ const scaled = wrapped * PALETTE.length;
59
+ const index = Math.floor(scaled);
60
+ const nextIndex = (index + 1) % PALETTE.length;
61
+ const amount = scaled - index;
62
+ const start = PALETTE[index]!;
63
+ const end = PALETTE[nextIndex]!;
64
+
65
+ return [
66
+ mix(start[0], end[0], amount),
67
+ mix(start[1], end[1], amount),
68
+ mix(start[2], end[2], amount),
69
+ ] satisfies Rgb;
70
+ }
71
+
72
+ function foreground([red, green, blue]: Rgb, text: string) {
73
+ return `\x1b[38;2;${red};${green};${blue}m${text}${RESET}`;
74
+ }
75
+
76
+ function gradientText(text: string, phase: number) {
77
+ const characters = [...text];
78
+ const span = Math.max(characters.length - 1, 1);
79
+
80
+ return characters
81
+ .map((character, index) =>
82
+ character === " "
83
+ ? character
84
+ : foreground(sampleGradient(index / span + phase), character),
85
+ )
86
+ .join("");
87
+ }
88
+
89
+ function center(text: string, width: number) {
90
+ const padding = Math.max(0, Math.floor((width - visibleWidth(text)) / 2));
91
+ return truncateToWidth(`${" ".repeat(padding)}${text}`, width);
92
+ }
93
+
94
+ export default function uiCustomization(pi: ExtensionAPI) {
95
+ let title = "pi";
96
+ let modelInfo = emptyModelInfoState();
97
+ let gitInfo = emptyGitInfoState();
98
+ let requestRender: (() => void) | undefined;
99
+ let activeSession: ExtensionContext | undefined;
100
+
101
+ let stopModelListener: (() => void) | undefined;
102
+ let stopGitListener: (() => void) | undefined;
103
+
104
+ const stopDashboardListeners = () => {
105
+ stopModelListener?.();
106
+ stopGitListener?.();
107
+ stopModelListener = undefined;
108
+ stopGitListener = undefined;
109
+ };
110
+
111
+ const startDashboardListeners = () => {
112
+ stopDashboardListeners();
113
+ stopModelListener = pi.events.on(MODEL_INFO_CHANNEL, (value) => {
114
+ if (!isModelInfoState(value)) return;
115
+ modelInfo = value;
116
+ requestRender?.();
117
+ });
118
+ stopGitListener = pi.events.on(GIT_INFO_CHANNEL, (value) => {
119
+ if (!isGitInfoState(value)) return;
120
+ gitInfo = value;
121
+ requestRender?.();
122
+ });
123
+ };
124
+
125
+ // Kept for process lifetime so configure_my_pi_setup still refreshes later sessions.
126
+ pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, () => {
127
+ if (!activeSession || activeSession.mode !== "tui") return;
128
+ install(activeSession);
129
+ requestRender?.();
130
+ });
131
+
132
+ function install(ctx: ExtensionContext) {
133
+ if (ctx.mode !== "tui") return;
134
+ const config = loadSetupConfig().ui;
135
+
136
+ if (config.showHeader) {
137
+ ctx.ui.setHeader((tui) => {
138
+ requestRender = () => tui.requestRender();
139
+
140
+ return {
141
+ render(width: number) {
142
+ const art = TITLE_LINES.map((line, row) =>
143
+ center(gradientText(line, row * 0.045), width),
144
+ );
145
+ const subtitle = center(
146
+ `${BOLD}${gradientText(title, 0.18)}${RESET}`,
147
+ width,
148
+ );
149
+ return ["", ...art, subtitle, ""];
150
+ },
151
+ invalidate() {},
152
+ };
153
+ });
154
+ } else {
155
+ ctx.ui.setHeader(undefined);
156
+ }
157
+
158
+ if (config.customFooter) {
159
+ ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
160
+ requestRender = () => tui.requestRender();
161
+
162
+ return {
163
+ invalidate() {},
164
+ render(width: number) {
165
+ const statuses = footerData.getExtensionStatuses();
166
+ const statusLines = Array.from(statuses.entries())
167
+ .sort(([a], [b]) => a.localeCompare(b))
168
+ .flatMap(([, text]) => text.split("\n"));
169
+
170
+ return renderFooter({
171
+ cwd: ctx.cwd,
172
+ modelInfo,
173
+ gitInfo,
174
+ style: config.footerStyle,
175
+ lines: config.footerLines,
176
+ width,
177
+ theme,
178
+ formatPullRequest: (number, url) => {
179
+ const label = `PR #${number}`;
180
+ return getCapabilities().hyperlinks
181
+ ? hyperlink(label, url)
182
+ : label;
183
+ },
184
+ statuses: statusLines,
185
+ });
186
+ },
187
+ };
188
+ });
189
+ } else {
190
+ ctx.ui.setFooter(undefined);
191
+ }
192
+
193
+ ctx.ui.setTitle(`pi · ${title}`);
194
+ pi.events.emit(REFRESH_CHANNEL, undefined);
195
+ }
196
+
197
+ pi.on("session_start", (_event, ctx) => {
198
+ title = formatDirectory(ctx.cwd);
199
+ modelInfo = emptyModelInfoState();
200
+ gitInfo = emptyGitInfoState();
201
+ activeSession = ctx;
202
+ startDashboardListeners();
203
+ install(ctx);
204
+ });
205
+
206
+ pi.on("session_shutdown", (_event, ctx) => {
207
+ // Drop the session pointer first so a late config event cannot reinstall
208
+ // against a shut-down context. The config listener stays armed for later sessions.
209
+ activeSession = undefined;
210
+ stopDashboardListeners();
211
+ requestRender = undefined;
212
+ if (ctx.mode === "tui") {
213
+ ctx.ui.setHeader(undefined);
214
+ ctx.ui.setFooter(undefined);
215
+ }
216
+ });
217
+ }
@@ -0,0 +1,298 @@
1
+ export const ACCEPTANCE_STATUSES = ["accepted", "rejected"] as const;
2
+ export type AcceptanceStatus = (typeof ACCEPTANCE_STATUSES)[number];
3
+
4
+ export interface AcceptanceCriterion {
5
+ readonly id: string;
6
+ readonly description: string;
7
+ readonly requiredEvidence?: readonly string[];
8
+ }
9
+
10
+ export interface AcceptanceContract {
11
+ readonly criteria: readonly AcceptanceCriterion[];
12
+ }
13
+
14
+ export interface AcceptanceCriterionResult {
15
+ readonly id: string;
16
+ readonly status: AcceptanceStatus;
17
+ readonly evidence: readonly string[];
18
+ readonly note?: string;
19
+ }
20
+
21
+ export interface AcceptanceLedger {
22
+ readonly status: "accepted" | "rejected" | "missing" | "malformed";
23
+ readonly criteria: readonly AcceptanceCriterionResult[];
24
+ readonly errors: readonly string[];
25
+ }
26
+
27
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
28
+ const MAX_CRITERIA = 32;
29
+
30
+ function record(value: unknown): value is Record<string, unknown> {
31
+ return !!value && typeof value === "object" && !Array.isArray(value);
32
+ }
33
+
34
+ export function parseAcceptanceContract(
35
+ value: unknown,
36
+ ): AcceptanceContract | undefined {
37
+ if (value === undefined) return undefined;
38
+ if (!record(value) || !Array.isArray(value.criteria)) {
39
+ throw new Error("acceptance must be { criteria: [...] }");
40
+ }
41
+ if (value.criteria.length === 0 || value.criteria.length > MAX_CRITERIA) {
42
+ throw new Error(`acceptance.criteria must contain 1-${MAX_CRITERIA} items`);
43
+ }
44
+ const seen = new Set<string>();
45
+ const criteria = value.criteria.map((raw, index): AcceptanceCriterion => {
46
+ if (!record(raw))
47
+ throw new Error(`acceptance criterion ${index + 1} must be an object`);
48
+ const id = typeof raw.id === "string" ? raw.id.trim() : "";
49
+ const description =
50
+ typeof raw.description === "string" ? raw.description.trim() : "";
51
+ if (!ID_PATTERN.test(id))
52
+ throw new Error(`acceptance criterion ${index + 1} has an invalid id`);
53
+ if (seen.has(id))
54
+ throw new Error(`acceptance criterion id "${id}" is duplicated`);
55
+ seen.add(id);
56
+ if (!description || description.length > 500)
57
+ throw new Error(
58
+ `acceptance criterion "${id}" needs a 1-500 character description`,
59
+ );
60
+ let requiredEvidence: string[] | undefined;
61
+ if (raw.requiredEvidence !== undefined) {
62
+ if (
63
+ !Array.isArray(raw.requiredEvidence) ||
64
+ raw.requiredEvidence.length > 16
65
+ ) {
66
+ throw new Error(
67
+ `acceptance criterion "${id}" requiredEvidence must be an array of at most 16 labels`,
68
+ );
69
+ }
70
+ requiredEvidence = raw.requiredEvidence.map((entry) => {
71
+ if (typeof entry !== "string" || !entry.trim() || entry.length > 120) {
72
+ throw new Error(
73
+ `acceptance criterion "${id}" has an invalid evidence label`,
74
+ );
75
+ }
76
+ return entry.trim();
77
+ });
78
+ }
79
+ return {
80
+ id,
81
+ description,
82
+ ...(requiredEvidence?.length ? { requiredEvidence } : {}),
83
+ };
84
+ });
85
+ return { criteria };
86
+ }
87
+
88
+ export function acceptanceSchema(
89
+ schema: unknown,
90
+ contract: AcceptanceContract,
91
+ ) {
92
+ const ledger = {
93
+ type: "object",
94
+ additionalProperties: false,
95
+ properties: {
96
+ criteria: {
97
+ type: "array",
98
+ minItems: contract.criteria.length,
99
+ maxItems: contract.criteria.length,
100
+ items: {
101
+ type: "object",
102
+ additionalProperties: false,
103
+ properties: {
104
+ id: { type: "string" },
105
+ status: { type: "string", enum: [...ACCEPTANCE_STATUSES] },
106
+ evidence: {
107
+ type: "array",
108
+ items: { type: "string" },
109
+ maxItems: 64,
110
+ },
111
+ note: { type: "string", maxLength: 1000 },
112
+ },
113
+ required: ["id", "status", "evidence"],
114
+ },
115
+ },
116
+ },
117
+ required: ["criteria"],
118
+ };
119
+ if (schema === undefined) {
120
+ return {
121
+ type: "object",
122
+ properties: { acceptance: ledger },
123
+ required: ["acceptance"],
124
+ };
125
+ }
126
+ if (!record(schema) || schema.type !== "object") {
127
+ throw new Error("acceptance can only compose with an object JSON Schema");
128
+ }
129
+ const properties =
130
+ schema.properties === undefined
131
+ ? {}
132
+ : record(schema.properties)
133
+ ? schema.properties
134
+ : undefined;
135
+ if (!properties) {
136
+ throw new Error("acceptance schema properties must be an object");
137
+ }
138
+ if (Object.hasOwn(properties, "acceptance")) {
139
+ throw new Error(
140
+ 'the caller schema already defines reserved property "acceptance"',
141
+ );
142
+ }
143
+ const required =
144
+ schema.required === undefined
145
+ ? []
146
+ : Array.isArray(schema.required) &&
147
+ schema.required.every((entry) => typeof entry === "string")
148
+ ? schema.required
149
+ : undefined;
150
+ if (!required)
151
+ throw new Error("acceptance schema required must be a string array");
152
+ return {
153
+ ...schema,
154
+ properties: { ...properties, acceptance: ledger },
155
+ required: [...new Set([...required, "acceptance"])],
156
+ };
157
+ }
158
+
159
+ export function isAcceptanceLedger(value: unknown): value is AcceptanceLedger {
160
+ if (
161
+ !record(value) ||
162
+ !Array.isArray(value.criteria) ||
163
+ !Array.isArray(value.errors)
164
+ ) {
165
+ return false;
166
+ }
167
+ return (
168
+ (value.status === "accepted" ||
169
+ value.status === "rejected" ||
170
+ value.status === "missing" ||
171
+ value.status === "malformed") &&
172
+ value.errors.every((error) => typeof error === "string") &&
173
+ value.criteria.every(
174
+ (criterion) =>
175
+ record(criterion) &&
176
+ typeof criterion.id === "string" &&
177
+ (criterion.status === "accepted" || criterion.status === "rejected") &&
178
+ Array.isArray(criterion.evidence) &&
179
+ criterion.evidence.every((entry) => typeof entry === "string"),
180
+ )
181
+ );
182
+ }
183
+
184
+ export function acceptanceInstruction(contract: AcceptanceContract) {
185
+ const criteria = contract.criteria.map(
186
+ (criterion) =>
187
+ `- ${criterion.id}: ${criterion.description}${criterion.requiredEvidence?.length ? `; required evidence labels: ${criterion.requiredEvidence.join(", ")}` : ""}`,
188
+ );
189
+ return [
190
+ "Acceptance is explicit evidence, not a self-awarded success claim.",
191
+ "Include an `acceptance.criteria` array in structured_output with exactly these ids. Mark rejected when the criterion is not demonstrated. Evidence entries must be concise labels or concrete references; do not invent evidence.",
192
+ ...criteria,
193
+ ].join("\n");
194
+ }
195
+
196
+ export function evaluateAcceptance(
197
+ contract: AcceptanceContract,
198
+ structured: unknown,
199
+ ): AcceptanceLedger {
200
+ if (!record(structured) || !record(structured.acceptance)) {
201
+ return {
202
+ status: "missing",
203
+ criteria: [],
204
+ errors: ["structured result omitted acceptance"],
205
+ };
206
+ }
207
+ const rawCriteria = structured.acceptance.criteria;
208
+ if (!Array.isArray(rawCriteria)) {
209
+ return {
210
+ status: "malformed",
211
+ criteria: [],
212
+ errors: ["acceptance.criteria is not an array"],
213
+ };
214
+ }
215
+ const errors: string[] = [];
216
+ const byId = new Map<string, AcceptanceCriterionResult>();
217
+ for (const raw of rawCriteria) {
218
+ if (!record(raw) || typeof raw.id !== "string") {
219
+ errors.push("acceptance contains a criterion without an id");
220
+ continue;
221
+ }
222
+ if (byId.has(raw.id)) {
223
+ errors.push(`acceptance criterion "${raw.id}" is duplicated`);
224
+ continue;
225
+ }
226
+ const status = raw.status;
227
+ const evidence = Array.isArray(raw.evidence)
228
+ ? raw.evidence
229
+ .filter((entry): entry is string => typeof entry === "string")
230
+ .map((entry) => entry.trim())
231
+ .filter(Boolean)
232
+ : [];
233
+ if (status !== "accepted" && status !== "rejected") {
234
+ errors.push(`acceptance criterion "${raw.id}" has an invalid status`);
235
+ continue;
236
+ }
237
+ byId.set(raw.id, {
238
+ id: raw.id,
239
+ status,
240
+ evidence,
241
+ ...(typeof raw.note === "string" && raw.note.trim()
242
+ ? { note: raw.note.trim().slice(0, 1000) }
243
+ : {}),
244
+ });
245
+ }
246
+ const results: AcceptanceCriterionResult[] = [];
247
+ for (const criterion of contract.criteria) {
248
+ const result = byId.get(criterion.id);
249
+ if (!result) {
250
+ errors.push(`acceptance criterion "${criterion.id}" is missing`);
251
+ continue;
252
+ }
253
+ const missingEvidence = (criterion.requiredEvidence ?? []).filter(
254
+ (required) => !result.evidence.includes(required),
255
+ );
256
+ if (missingEvidence.length) {
257
+ errors.push(
258
+ `acceptance criterion "${criterion.id}" lacks evidence: ${missingEvidence.join(", ")}`,
259
+ );
260
+ }
261
+ results.push(result);
262
+ }
263
+ for (const id of byId.keys()) {
264
+ if (!contract.criteria.some((criterion) => criterion.id === id)) {
265
+ errors.push(`unexpected acceptance criterion "${id}"`);
266
+ }
267
+ }
268
+ if (errors.length) return { status: "malformed", criteria: results, errors };
269
+ return {
270
+ status: results.every((result) => result.status === "accepted")
271
+ ? "accepted"
272
+ : "rejected",
273
+ criteria: results,
274
+ errors: [],
275
+ };
276
+ }
277
+
278
+ export function applyAcceptance(options: {
279
+ contract?: AcceptanceContract;
280
+ structured: unknown;
281
+ agentOk: boolean;
282
+ agentError?: string;
283
+ }) {
284
+ const ledger = options.contract
285
+ ? evaluateAcceptance(options.contract, options.structured)
286
+ : undefined;
287
+ const acceptanceError =
288
+ ledger && ledger.status !== "accepted"
289
+ ? `Acceptance ${ledger.status}${ledger.errors.length ? `: ${ledger.errors.join("; ")}` : ": one or more criteria were rejected"}`
290
+ : undefined;
291
+ const ok = options.agentOk && !acceptanceError;
292
+ const error = ok
293
+ ? undefined
294
+ : options.agentError
295
+ ? `${options.agentError}${acceptanceError ? `; ${acceptanceError}` : ""}`
296
+ : (acceptanceError ?? "Agent failed");
297
+ return { ok, ...(ledger ? { ledger } : {}), ...(error ? { error } : {}) };
298
+ }