@uchibeke/gallivant 0.0.0-stage → 0.2.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 (168) hide show
  1. package/CONTRIBUTING.md +26 -0
  2. package/LICENSE +204 -0
  3. package/README.md +201 -2
  4. package/SECURITY.md +19 -0
  5. package/bin/gallivant.mjs +257 -0
  6. package/bin/package-plugin.mjs +16 -0
  7. package/bin/release-live.mjs +29 -0
  8. package/bin/release-package.mjs +23 -0
  9. package/bin/session-runtime.mjs +13 -0
  10. package/bin/task-delivery-state.mjs +12 -0
  11. package/docs/README.md +70 -0
  12. package/docs/agent-setup.md +214 -0
  13. package/docs/app.md +111 -0
  14. package/docs/architecture.md +265 -0
  15. package/docs/chains.md +102 -0
  16. package/docs/codex-cloud.md +284 -0
  17. package/docs/commands.md +620 -0
  18. package/docs/config.md +430 -0
  19. package/docs/execution.md +796 -0
  20. package/docs/guardrails.md +311 -0
  21. package/docs/install.md +338 -0
  22. package/docs/launch.md +42 -0
  23. package/docs/learning.md +369 -0
  24. package/docs/network-recovery.md +101 -0
  25. package/docs/openai-agents.md +457 -0
  26. package/docs/prd.md +246 -0
  27. package/docs/preparation.md +110 -0
  28. package/docs/release-qualification.md +196 -0
  29. package/docs/releasing.md +140 -0
  30. package/docs/runtime-environment.md +113 -0
  31. package/docs/task-delivery.md +476 -0
  32. package/docs/tasks.md +739 -0
  33. package/lib/aport.mjs +95 -0
  34. package/lib/attribution.mjs +95 -0
  35. package/lib/block.mjs +89 -0
  36. package/lib/capabilities.mjs +3 -0
  37. package/lib/chains.mjs +222 -0
  38. package/lib/codex_cloud.mjs +160 -0
  39. package/lib/codex_cloud_config.mjs +20 -0
  40. package/lib/codex_cloud_input.mjs +49 -0
  41. package/lib/codex_cloud_result.mjs +74 -0
  42. package/lib/commands/doctor.mjs +457 -0
  43. package/lib/commands/init.mjs +34 -0
  44. package/lib/commands/learn.mjs +54 -0
  45. package/lib/commands/new.mjs +47 -0
  46. package/lib/commands/ready.mjs +45 -0
  47. package/lib/commands/register.mjs +195 -0
  48. package/lib/commands/resume.mjs +224 -0
  49. package/lib/commands/serve.mjs +349 -0
  50. package/lib/commands/start.mjs +247 -0
  51. package/lib/commands/start_services.mjs +22 -0
  52. package/lib/commands/status.mjs +150 -0
  53. package/lib/commands/task.mjs +73 -0
  54. package/lib/commands/up.mjs +228 -0
  55. package/lib/commands/why.mjs +143 -0
  56. package/lib/config.mjs +461 -0
  57. package/lib/context.mjs +100 -0
  58. package/lib/control.mjs +202 -0
  59. package/lib/control_contract.mjs +16 -0
  60. package/lib/coordinator.mjs +19 -0
  61. package/lib/credential.mjs +161 -0
  62. package/lib/daemon.mjs +156 -0
  63. package/lib/deliverable.mjs +357 -0
  64. package/lib/events.mjs +261 -0
  65. package/lib/executable.mjs +31 -0
  66. package/lib/execution.mjs +631 -0
  67. package/lib/execution_contract.mjs +15 -0
  68. package/lib/execution_input.mjs +4 -0
  69. package/lib/execution_io.mjs +94 -0
  70. package/lib/execution_journal.mjs +69 -0
  71. package/lib/execution_preparation.mjs +29 -0
  72. package/lib/execution_result.mjs +266 -0
  73. package/lib/fs.mjs +133 -0
  74. package/lib/gh.mjs +289 -0
  75. package/lib/git.mjs +434 -0
  76. package/lib/harness.mjs +521 -0
  77. package/lib/harness_log.mjs +432 -0
  78. package/lib/hash.mjs +16 -0
  79. package/lib/identity.mjs +98 -0
  80. package/lib/learning.mjs +654 -0
  81. package/lib/learning_contract.mjs +2 -0
  82. package/lib/learning_data.mjs +135 -0
  83. package/lib/learning_input.mjs +111 -0
  84. package/lib/learning_qualification.mjs +67 -0
  85. package/lib/learning_signer.mjs +151 -0
  86. package/lib/learning_store.mjs +88 -0
  87. package/lib/log.mjs +53 -0
  88. package/lib/network_diagnostics.mjs +64 -0
  89. package/lib/network_recovery.mjs +42 -0
  90. package/lib/notify.mjs +58 -0
  91. package/lib/openai_agents.mjs +373 -0
  92. package/lib/openai_agents_config.mjs +14 -0
  93. package/lib/openai_agents_http.mjs +87 -0
  94. package/lib/openai_agents_input.mjs +15 -0
  95. package/lib/openai_agents_sources.mjs +112 -0
  96. package/lib/owner_delivery.mjs +443 -0
  97. package/lib/plugin_package.mjs +340 -0
  98. package/lib/prd.mjs +635 -0
  99. package/lib/preparation_agents.mjs +33 -0
  100. package/lib/preparation_codex.mjs +212 -0
  101. package/lib/probe.mjs +126 -0
  102. package/lib/prompt.mjs +238 -0
  103. package/lib/provider_health.mjs +55 -0
  104. package/lib/public-format.mjs +31 -0
  105. package/lib/public_output.mjs +49 -0
  106. package/lib/release_package.mjs +154 -0
  107. package/lib/repository_files.mjs +86 -0
  108. package/lib/run.mjs +385 -0
  109. package/lib/run_log.mjs +94 -0
  110. package/lib/runtime-environment.mjs +87 -0
  111. package/lib/runtime_dependencies.mjs +15 -0
  112. package/lib/runtime_node.mjs +107 -0
  113. package/lib/runtime_state.mjs +174 -0
  114. package/lib/sources/index.mjs +27 -0
  115. package/lib/sources/prd.mjs +50 -0
  116. package/lib/start_contract.mjs +37 -0
  117. package/lib/state.mjs +246 -0
  118. package/lib/task_allowance.mjs +31 -0
  119. package/lib/task_completion.mjs +61 -0
  120. package/lib/task_contract.mjs +450 -0
  121. package/lib/task_delivery.mjs +219 -0
  122. package/lib/task_delivery_cancel.mjs +34 -0
  123. package/lib/task_delivery_contract.mjs +61 -0
  124. package/lib/task_delivery_mcp.mjs +40 -0
  125. package/lib/task_delivery_store.mjs +43 -0
  126. package/lib/task_delivery_view.mjs +88 -0
  127. package/lib/task_environment.mjs +281 -0
  128. package/lib/task_prepare.mjs +224 -0
  129. package/lib/task_store.mjs +38 -0
  130. package/lib/tasks.mjs +513 -0
  131. package/lib/tick.mjs +1635 -0
  132. package/lib/time.mjs +36 -0
  133. package/lib/tracker/assets.mjs +43 -0
  134. package/lib/tracker/pages.mjs +320 -0
  135. package/lib/tracker/runs.mjs +102 -0
  136. package/lib/tracker/stream.mjs +74 -0
  137. package/lib/tracker_process.mjs +107 -0
  138. package/lib/verification_sandbox.mjs +181 -0
  139. package/node_modules/smol-toml/LICENSE +24 -0
  140. package/node_modules/smol-toml/README.md +605 -0
  141. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  142. package/node_modules/smol-toml/dist/date.js +156 -0
  143. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  144. package/node_modules/smol-toml/dist/error.js +67 -0
  145. package/node_modules/smol-toml/dist/extract.js +289 -0
  146. package/node_modules/smol-toml/dist/index.cjs +770 -0
  147. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  148. package/node_modules/smol-toml/dist/index.js +34 -0
  149. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  150. package/node_modules/smol-toml/dist/parse.js +164 -0
  151. package/node_modules/smol-toml/dist/primitive.js +151 -0
  152. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  153. package/node_modules/smol-toml/dist/stringify.js +219 -0
  154. package/node_modules/smol-toml/dist/struct.js +156 -0
  155. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  156. package/node_modules/smol-toml/dist/util.js +57 -0
  157. package/node_modules/smol-toml/package.json +68 -0
  158. package/npm-shrinkwrap.json +1823 -0
  159. package/package.json +105 -4
  160. package/templates/claude-settings.json +18 -0
  161. package/templates/passport.gallivant.json +83 -0
  162. package/templates/plugin/bundled-runtime.json +26 -0
  163. package/templates/plugin/schemas/mcp.schema.json +120 -0
  164. package/templates/plugin/schemas/plugin.schema.json +65 -0
  165. package/templates/plugin/task-delivery.SKILL.md +60 -0
  166. package/templates/prd.md +32 -0
  167. package/templates/tracker/tracker.css +301 -0
  168. package/templates/tracker/tracker.js +721 -0
@@ -0,0 +1,219 @@
1
+ // APort owns OAuth, audience/region/repository authorization. This adapter has no
2
+ // anonymous fallback; every operation reauthorizes before touching scoped state.
3
+ import Ajv from "ajv";
4
+ import { MAX_DELIVERY_OPERATIONS, receiptCapacityError } from "./task_delivery_store.mjs";
5
+ import { createHash } from "node:crypto";
6
+ import { join } from "node:path";
7
+ import { createTaskService } from "./tasks.mjs";
8
+ import { executionCapabilities, capabilitiesFor } from "./harness.mjs";
9
+ import { unsubmittedInitialDispatch } from "./execution.mjs";
10
+ import { runtimePinManifest, assertRuntimePin } from "./runtime_state.mjs";
11
+ import { acquireRuntimeOwner } from "./daemon.mjs";
12
+ import { requestControl } from "./control.mjs";
13
+ import { reconcileDeliveryCancellation } from "./task_delivery_cancel.mjs";
14
+ import { controlAvailability } from "./control_contract.mjs";
15
+ import { logWindow } from "./run_log.mjs";
16
+ import { idSlug } from "./state.mjs";
17
+ import { redact } from "./execution_io.mjs";
18
+ import { canonical } from "./learning_data.mjs";
19
+ import { scopeKey } from "./task_delivery_store.mjs";
20
+ import { TASK_OPERATIONS, REPOSITORY_ID_SCHEMA, DELIVERY_VERSION, deliveryError, settledTaskRejection } from "./task_delivery_contract.mjs";
21
+ import { taskView } from "./task_delivery_view.mjs";
22
+ import { readJson } from "./fs.mjs";
23
+ import { COMPLETION_CAPABILITIES } from "./task_completion.mjs";
24
+ const validators = new Map(TASK_OPERATIONS.map(op => [op.name, new Ajv({ strict: true, allErrors: true }).compile(op.inputSchema)]));
25
+ const validRepositoryId = new Ajv({ strict: true }).compile(REPOSITORY_ID_SCHEMA);
26
+ const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
27
+
28
+ export function createTaskDelivery({ authorize, contextFor, store, schedule, manifest, secretsFor = () => [] }) {
29
+ if (typeof authorize !== "function" || typeof contextFor !== "function" || !store?.transaction || !store?.read || typeof schedule !== "function") throw deliveryError("DELIVERY_CONFIG", "Supply APort authorization, scoped context, durable receipt store and wake-up scheduling adapters");
30
+ manifest = runtimePinManifest(manifest);
31
+ const owned = (data, taskId) => {
32
+ if (!Object.hasOwn(data.tasks, taskId)) throw deliveryError("DELIVERY_NOT_FOUND", "Task is unavailable in this account");
33
+ return data.tasks[taskId];
34
+ };
35
+ async function dispatch(name, input, identity) {
36
+ const op = TASK_OPERATIONS.find(o => o.name === name), validate = validators.get(name);
37
+ if (!op || !validate(input)) throw deliveryError("DELIVERY_INPUT", `Invalid ${name} request; use its documented schema (no paths, endpoints or credentials)`);
38
+ const scope = await authorize({ identity, operation: name, requiredScope: op.scope, taskId: input.taskId, repositoryId: input.repositoryId });
39
+ scopeKey(scope);
40
+ const secrets = secretsFor(scope);
41
+ const serialized = JSON.stringify(input);
42
+ if (serialized !== redact(serialized, secrets)) throw deliveryError("DELIVERY_SECRET", "Remove credential material from task text, answers or notes before sending them to a model");
43
+ if (!Array.isArray(scope.scopes) || !scope.scopes.includes(op.scope) || !Array.isArray(scope.repositories)) throw deliveryError("DELIVERY_AUTHORIZATION", "APort did not grant the required scope and repository set");
44
+ if (!scope.repositories.every(id => validRepositoryId(id))) throw deliveryError("DELIVERY_CONFIG", "APort returned an unsupported repository ID; use the bounded repository-ID schema");
45
+ const allowedRepo = id => { if (!scope.repositories.includes(id)) throw deliveryError("DELIVERY_AUTHORIZATION", "Repository is unavailable in this account"); };
46
+ const project = (ctx, task) => {
47
+ const entry = task.implementation && ctx.state.get(task.implementation.id);
48
+ const execution = entry && (ctx.executionFor ? ctx.executionFor(task.implementation.id, task.harness) : ctx.runnerFor(task.implementation.id));
49
+ const run = execution?.current();
50
+ const result = run?.id && execution.result?.(run.id);
51
+ const verification = run?.id && /^[A-Za-z0-9_-]+$/.test(run.id) ? readJson(join(ctx.home, "runs", idSlug(task.implementation.id), run.id, "verification.json")) : null;
52
+ const capability = execution?.capabilities ?? executionCapabilities().find(c => c.name === task.harness);
53
+ let resumable = Boolean(entry?.session);
54
+ if (entry && execution && !resumable) {
55
+ try { resumable = Boolean(unsubmittedInitialDispatch(entry, execution)); }
56
+ catch (error) { if (!["EXECUTION_STOPPED", "EXECUTION_UNKNOWN"].includes(error.code)) throw error; }
57
+ }
58
+ return taskView(task, { entry, run, result, verification, capabilities: capability, controls: controlAvailability(entry, run, capability, { resumable }), secrets: [...secretsFor(scope), ctx.home, task.checkout].filter(Boolean) });
59
+ };
60
+ const getContext = async (repoId, allowance) => {
61
+ allowedRepo(repoId);
62
+ // Server configuration resolves all filesystem paths and transport URLs.
63
+ const bound = await contextFor(scope, repoId);
64
+ if (!bound?.ctx || !bound.repository?.dir || !bound.repository?.repo) throw deliveryError("DELIVERY_CONFIG", "APort must bind the authorized repository to a scoped runtime context");
65
+ const ctx = bound.ctx;
66
+ let scopedCtx = { ...ctx, taskSecrets: secrets, enforceTaskAllowance: true };
67
+ if (allowance) {
68
+ if (allowance.budgetUsd > ctx.config.max_budget_usd || allowance.runtimeMs > ctx.config.run_timeout_minutes * 60000) throw deliveryError("DELIVERY_ALLOWANCE", "Requested allowance exceeds the configured account limit");
69
+ // Narrow a copy: shared account settings are never mutated by requests.
70
+ scopedCtx = { ...scopedCtx, config: { ...ctx.config, max_budget_usd: allowance.budgetUsd, run_timeout_minutes: allowance.runtimeMs / 60000 } };
71
+ }
72
+ return { ...bound, ctx: scopedCtx, service: createTaskService(scopedCtx, bound.taskOptions) };
73
+ };
74
+ if (name === "capabilities") {
75
+ const repositories = [];
76
+ const ids = [...new Set(scope.repositories)].sort().filter(id => !input.cursor || id > input.cursor);
77
+ for (const id of ids.slice(0, 100)) {
78
+ const { ctx } = await getContext(id);
79
+ repositories.push({ id, completion: COMPLETION_CAPABILITIES, providers: executionCapabilities().map(capability => ({ ...capability,
80
+ configured: Boolean(ctx.config.harness[capability.name]),
81
+ available: capability.available && Boolean(ctx.config.harness[capability.name]),
82
+ accountAccess: "unverified-until-setup-check", setup: capability.reason || (!ctx.config.harness[capability.name] ? "APort must configure this provider for the authorized account" : "Dispatch validates the configured account/model/environment and allowance"),
83
+ canPrepare: Boolean(capability.preparation && capability.available && ctx.config.harness[capability.name]),
84
+ preparationSetup: capability.preparation ? "Uses the declared bounded preparation adapter" : "Bounded preparation is unavailable; use the existing hand-authored ready/start workflow until this provider supports an enforced tool-free boundary",
85
+ })) });
86
+ }
87
+ return redact({ version: DELIVERY_VERSION, providers: executionCapabilities(), repositories,
88
+ truncated: ids.length > 100, nextCursor: ids.length > 100 ? repositories.at(-1).id : null }, secretsFor(scope));
89
+ }
90
+ if (op.annotations.readOnlyHint) {
91
+ const data = await store.read(scope);
92
+ if (name === "list") {
93
+ const tasks = [], authorized = Object.entries(data.tasks).filter(([, record]) => scope.repositories.includes(record.repositoryId)).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).filter(([id]) => !input.cursor || id > input.cursor);
94
+ for (const [taskId, record] of authorized.slice(0, 100)) {
95
+ const { ctx, service } = await getContext(record.repositoryId);
96
+ tasks.push(project(ctx, await service.inspect({ taskId })));
97
+ }
98
+ return { version: DELIVERY_VERSION, tasks, truncated: authorized.length > 100, nextCursor: authorized.length > 100 ? tasks.at(-1).taskId : null };
99
+ }
100
+ const record = owned(data, input.taskId), { ctx, service } = await getContext(record.repositoryId);
101
+ const task = await service.inspect({ taskId: input.taskId }), view = project(ctx, task);
102
+ if (name === "logs") {
103
+ if (!task.implementation?.run) return { version: DELIVERY_VERSION, taskId: view.taskId, revision: view.revision, lines: [], ids: [], total: 0, from: 1, to: 0, gap: null };
104
+ const runId = view.implementation.run;
105
+ if (!/^[A-Za-z0-9_-]+$/.test(runId || "")) throw deliveryError("DELIVERY_STATE", "Saved run identity is invalid; repair the scoped runtime record");
106
+ const root = join(ctx.home, "runs", idSlug(task.implementation.id), runId);
107
+ return redact({ version: DELIVERY_VERSION, taskId: view.taskId, revision: view.revision, ...logWindow(root, { after: input.after, count: input.count || 100 }) }, [...secretsFor(scope), ctx.home, task.checkout].filter(Boolean));
108
+ }
109
+ if (name === "evidence") return { version: DELIVERY_VERSION, taskId: view.taskId, revision: view.revision, completion: view.completion, brief: view.brief, sources: view.sources, artifacts: view.artifacts, readiness: view.readiness, implementation: view.implementation, evidence: view.evidence };
110
+ return view;
111
+ }
112
+ return store.transaction(scope, async (data, save) => {
113
+ const key = input.operationId, signature = "canonical-v1:" + createHash("sha256").update(canonical(JSON.parse(JSON.stringify([name, input])))).digest("hex");
114
+ const cancellation = name === "control" && input.action === "cancel";
115
+ const cancellations = data.cancellations || {};
116
+ const previous = Object.hasOwn(data.operations, key) ? data.operations[key] : Object.hasOwn(cancellations, key) ? cancellations[key] : null;
117
+ // One compact safety receipt per retained task, independent of ordinary
118
+ // count/byte capacity. Never evict identities or allow one task to consume
119
+ // another task's cancellation reserve.
120
+ if (!previous && cancellation && Object.values(cancellations).some(receipt => receipt.taskId === input.taskId && !(receipt.status === "acknowledged" && receipt.rejection))) throw deliveryError("DELIVERY_CAPACITY", "This task already has a retained cancellation; retry its original operation ID or inspect the task's cancellation status");
121
+ if (!previous && !cancellation && Object.keys(data.operations).length >= MAX_DELIVERY_OPERATIONS) throw receiptCapacityError();
122
+ if (!previous && cancellation && Object.keys(cancellations).length >= MAX_DELIVERY_OPERATIONS) throw receiptCapacityError();
123
+ const reserved = cancellation && !Object.hasOwn(data.operations, key);
124
+ const receipts = reserved ? cancellations : data.operations;
125
+ if (previous && previous.signature !== signature && previous.signature !== hash([name, input])) throw deliveryError("DELIVERY_CONFLICT", "Operation identity already belongs to different input");
126
+ // Only the original request may advance a task while its intent is open.
127
+ // Preparation IDs deterministically allocate distinct tasks; an unpublished
128
+ // preparation has no owned task binding available to competing requests.
129
+ if (!cancellation && Object.entries(data.operations).some(([id, receipt]) => id !== key && receipt.status === "intent" &&
130
+ (input.taskId && receipt.taskId === input.taskId))) throw deliveryError("DELIVERY_PENDING", "Retry the unresolved operation with its original identity before another task mutation; cancellation remains available");
131
+ const record = name === "prepare" ? { repositoryId: input.repositoryId, allowance: input.allowance } : owned(data, input.taskId);
132
+ const { ctx, taskOptions, repository } = await getContext(record.repositoryId, record.allowance);
133
+ // Share ownership with coordinator wakes, backup and migration. Claim
134
+ // before durable intent or task effects, and retain it through receipt saves.
135
+ const runtimeOwner = acquireRuntimeOwner(ctx.home);
136
+ try {
137
+ assertRuntimePin({ home: ctx.home, manifest });
138
+ const service = createTaskService(ctx, { ...taskOptions, mutationOwner: runtimeOwner });
139
+ if (previous?.status === "acknowledged") {
140
+ if (previous.rejection) throw settledTaskRejection(previous.rejection.code, previous.rejection.message);
141
+ if (cancellation) {
142
+ const task = await service.inspect({ taskId: previous.taskId });
143
+ if (!task.implementation) throw deliveryError("DELIVERY_CONTROL", "No implementation exists to control");
144
+ previous.wakePending = true; await save(data);
145
+ const cancellationPending = await reconcileDeliveryCancellation(ctx, task.implementation.id, hash([scopeKey(scope), key]));
146
+ await schedule(scope); previous.wakePending = false; await save(data);
147
+ return { ...project(ctx, await service.inspect({ taskId: previous.taskId })), cancellationPending };
148
+ }
149
+ if (previous.wakePending) { await schedule(scope); previous.wakePending = false; await save(data); }
150
+ return previous.result ?? project(ctx, await service.inspect({ taskId: previous.taskId }));
151
+ }
152
+ const task = name === "prepare" ? null : await service.inspect({ taskId: input.taskId });
153
+ const rejectWithoutEffects = async error => {
154
+ const rejection = { code: error.code, message: redact(String(error.message), [...secretsFor(scope), ctx.home, task?.checkout].filter(Boolean)).slice(0, 500) };
155
+ receipts[key] = { signature, name, taskId: input.taskId || null, status: "acknowledged", wakePending: false, rejection };
156
+ if (reserved) data.cancellations = receipts;
157
+ await save(data);
158
+ throw settledTaskRejection(rejection.code, rejection.message);
159
+ };
160
+ // Only this pre-intent check proves a stale request had no effects.
161
+ // Keep its identity forever; a new cancellation may use a fresh ID,
162
+ // but the retained receipt still counts against bounded capacity.
163
+ if (!previous && task?.revision !== input.expectedRevision && name !== "prepare") return await rejectWithoutEffects(deliveryError("DELIVERY_CONFLICT", "Task revision changed; inspect before retrying"));
164
+ if (cancellation && !task.implementation) throw deliveryError("DELIVERY_CONTROL", "No implementation exists to control");
165
+ const ctxPaths = (input.contextIds || []).map(id => {
166
+ if (!Object.hasOwn(repository.context || {}, id)) throw deliveryError("DELIVERY_INPUT", "Selected context ID is unavailable");
167
+ return repository.context[id];
168
+ });
169
+ if (!previous) Object.defineProperty(receipts, key, { value: { signature, status: "intent", mutationFence: true, name, taskId: input.taskId || null }, enumerable: true, writable: true, configurable: true });
170
+ if (reserved) data.cancellations = receipts;
171
+ // A compact acknowledgement plus the bounded repository/task binding
172
+ // must fit before effects. Legacy intents also reserve before recovery.
173
+ if (!reserved) receipts[key].acknowledgementReserve = " ".repeat(4096);
174
+ await save(data); // crash-safe intent before any inference, readiness or execution
175
+ let result, cancellationPending;
176
+ const shared = { taskId: input.taskId, expectedRevision: input.expectedRevision, noDaemon: true };
177
+ try {
178
+ if (name === "prepare") result = await service.prepare({ text: input.text, dir: repository.dir, repo: repository.repo, harness: input.harness, context: ctxPaths, verifyCommands: input.verifyCommands || [], completion: input.completion, requestId: hash([scopeKey(scope), key]), noDaemon: true });
179
+ else if (name === "answer") result = await service.answer({ ...shared, answers: input.answers, verifyCommands: input.verifyCommands || [], operationId: key });
180
+ else if (name === "control") {
181
+ if (!task.implementation) throw deliveryError("DELIVERY_CONTROL", "No implementation exists to control");
182
+ const requestId = hash([scopeKey(scope), key]);
183
+ const entry = ctx.state.get(task.implementation.id);
184
+ // Cancellation can safely be re-enqueued with its deterministic ID
185
+ // after intent-only crashes; it only fences dispatch, never starts work.
186
+ // Other controls remain ambiguous when their bounded history is gone.
187
+ if (previous && !cancellation && ![entry?.control, ...(entry?.control_history || [])].some(c => c?.id === requestId)) throw deliveryError("DELIVERY_UNKNOWN", "Interrupted control has no retained acknowledgement; inspect its effects before authorizing a new request identity");
188
+ requestControl(ctx, { handle: task.implementation.id, action: input.action, note: input.note || "", interrupt: input.interrupt || false, requestId });
189
+ if (cancellation) cancellationPending = await reconcileDeliveryCancellation(ctx, task.implementation.id, requestId);
190
+ result = await service.inspect({ taskId: input.taskId });
191
+ } else {
192
+ // A moved revision settles an interrupted request with the current
193
+ // saved outcome, including denial or incomplete preparation. Never
194
+ // repeat effects or grant authority from its stale revision; further
195
+ // recovery requires a new request against the inspected revision.
196
+ if (previous && task.revision !== input.expectedRevision) {
197
+ if (!previous.mutationFence) throw deliveryError("DELIVERY_UNKNOWN", "Legacy intent lacks exclusive mutation evidence; preserve it for reconciliation with its original runtime");
198
+ result = task;
199
+ }
200
+ else result = await service[name]({ ...shared, ...(name === "start" ? { run: true } : {}) });
201
+ }
202
+ } catch (error) {
203
+ if (error.taskEffects === "none") {
204
+ return await rejectWithoutEffects(error);
205
+ }
206
+ throw error;
207
+ }
208
+ data.tasks[result.taskId] = record;
209
+ receipts[key] = { signature, name, taskId: result.taskId, status: "acknowledged", wakePending: true };
210
+ await save(data);
211
+ await schedule(scope); // adapter enqueues/alarm; never runs a second poll loop
212
+ receipts[key].wakePending = false;
213
+ await save(data);
214
+ return { ...project(ctx, result), ...(cancellation ? { cancellationPending } : {}) };
215
+ } finally { runtimeOwner.release(); }
216
+ });
217
+ }
218
+ return { version: DELIVERY_VERSION, operations: TASK_OPERATIONS, dispatch };
219
+ }
@@ -0,0 +1,34 @@
1
+ import { cancelControl, cancellationStatus } from "./control.mjs";
2
+ import { executionRunner, executionLive } from "./execution.mjs";
3
+ import { deliveryError } from "./task_delivery_contract.mjs";
4
+ import { nowIso } from "./time.mjs";
5
+
6
+ // A cancellation-scoped host may replay only this exact retained control. It
7
+ // never runs a coordinator tick, advances another task, or collects/publishes a
8
+ // late result. Provider ownership, backoff and idempotency remain in execution.
9
+ export async function reconcileDeliveryCancellation(ctx, id, requestId) {
10
+ let entry = ctx.state.get(id);
11
+ const request = [entry?.control, ...(entry?.control_history || [])].find(control => control?.id === requestId);
12
+ if (request?.action !== "cancel" || !entry.dispatch_stopped) {
13
+ throw deliveryError("DELIVERY_CONTROL", "The retained cancellation no longer owns this task control; inspect before recovery");
14
+ }
15
+ if (entry.control?.id === requestId) await cancelControl(ctx, id);
16
+ entry = ctx.state.get(id);
17
+ if (entry.control?.id === requestId && entry.control.status === "queued") return true; // another execution owner
18
+ if (entry.reason === "cancelled") return false;
19
+ const runner = executionRunner(ctx, id);
20
+ // Legacy local runners retain their existing verified-stop behavior. Remote
21
+ // execution must have its durable stop before reconcile is allowed to run.
22
+ if (!runner.state || !runner.reconcile) return true;
23
+ if (!runner.state().stopped) await runner.cancel();
24
+ if (!runner.state().stopped) throw deliveryError("DELIVERY_CONTROL", "Cancellation has not durably fenced provider dispatch");
25
+ await runner.reconcile();
26
+ const run = runner.current();
27
+ const pending = Boolean(run && executionLive(run));
28
+ if (!pending) {
29
+ const status = cancellationStatus({ confirmed: !run || run.execution?.providerState === "cancelled", terminal: Boolean(run) });
30
+ ctx.state.update(id, { state: "needs-human", reason: status.reason,
31
+ ...(entry.control?.id === requestId ? { control: { ...entry.control, status: "applied", message: status.message, finished_at: nowIso(ctx.clock) } } : {}) });
32
+ }
33
+ return pending;
34
+ }
@@ -0,0 +1,61 @@
1
+ // Portable wire contract. No filesystem, credentials, configuration or providers.
2
+ import { COMPLETION_SCHEMA } from "./task_completion.mjs";
3
+ export { COMPLETION_SCHEMA, COMPLETION_CAPABILITIES, coordinatorEntryActive } from "./task_completion.mjs";
4
+ export const DELIVERY_VERSION = 1;
5
+ export const CORE_VERSION = "0.2.0";
6
+ // One byte limit for the built APort MCP resource, package and runtime reader.
7
+ export const MAX_TASK_UI_BYTES = 2_000_000;
8
+ export const deliveryError = (code, message) => Object.assign(new Error(message), { code, version: DELIVERY_VERSION });
9
+ // Only the delivery receipt boundary may label a bounded/redacted rejection.
10
+ // Arbitrary provider errors with a TASK_* code must not expose their message.
11
+ const settledRejections = new WeakMap();
12
+ const safeRejectionCodes = new Set(["DELIVERY_CONFLICT", "TASK_INPUT", "TASK_DELIVERY_SCOPE", "TASK_CONFLICT", "TASK_NOT_READY", "TASK_AUTHORIZATION", "TASK_SECRET", "TASK_PREPARATION_BOUNDARY"]);
13
+ export function settledTaskRejection(code, message) {
14
+ const error = deliveryError(code, message);
15
+ if (safeRejectionCodes.has(code) && typeof message === "string") settledRejections.set(error, { code, message: message.slice(0, 500) });
16
+ return error;
17
+ }
18
+ export const taskRejectionForModel = error => settledRejections.get(error) || null;
19
+
20
+ const id = { type: "string", pattern: "^[A-Za-z0-9_-]{1,128}$" };
21
+ export const REPOSITORY_ID_SCHEMA = { type: "string", maxLength: 128, pattern: "^[A-Za-z0-9_-][A-Za-z0-9_.-]*(/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$" };
22
+ const taskId = { type: "string", pattern: "^task-[a-f0-9]{24}$" };
23
+ const text = { type: "string", minLength: 1, maxLength: 32768 };
24
+ const revision = { type: "integer", minimum: 1 };
25
+ const verifyCommands = { type: "array", maxItems: 32, uniqueItems: true, items: { type: "string", minLength: 1, maxLength: 500 } };
26
+ const object = (properties, required = Object.keys(properties)) => ({ type: "object", properties, required, additionalProperties: false });
27
+ const mutation = (properties, required = Object.keys(properties)) => object({ ...properties, operationId: id, expectedRevision: revision }, [...required, "operationId", "expectedRevision"]);
28
+ const definition = (name, description, inputSchema, { write = false, destructive = false, open = false } = {}) => ({
29
+ name, description, inputSchema, scope: write ? "task-delivery:write" : "task-delivery:read",
30
+ annotations: { readOnlyHint: !write, destructiveHint: destructive, idempotentHint: true, openWorldHint: open },
31
+ });
32
+ export const TASK_OPERATIONS = Object.freeze([
33
+ definition("capabilities", "Read up to 100 authorized repositories and configured harness capabilities; pass nextCursor as cursor for more. Does not probe providers.", object({ cursor: REPOSITORY_ID_SCHEMA }, [])),
34
+ definition("prepare", "Prepare requirements using an authorized repository and allowance; may use paid inference.", object({
35
+ repositoryId: REPOSITORY_ID_SCHEMA, text, harness: id, contextIds: { type: "array", items: id, maxItems: 100, uniqueItems: true }, verifyCommands, completion: COMPLETION_SCHEMA,
36
+ allowance: object({ budgetUsd: { type: "number", exclusiveMinimum: 0 }, runtimeMs: { type: "integer", minimum: 1 } }),
37
+ operationId: id, expectedRevision: { type: "integer", const: 0 },
38
+ }, ["repositoryId", "text", "harness", "allowance", "operationId", "expectedRevision"]), { write: true, open: true }),
39
+ definition("answer", "Answer saved question IDs and optionally select verification commands on the expected revision; may continue paid preparation.", mutation({ taskId, answers: { type: "object", minProperties: 1, maxProperties: 100, propertyNames: id, additionalProperties: text }, verifyCommands }, ["taskId", "answers"]), { write: true, open: true }),
40
+ definition("resume", "Resume owned preparation without allocating another task.", mutation({ taskId }), { write: true, open: true }),
41
+ definition("validate", "Run the canonical readiness gate and preserve its decision.", mutation({ taskId }), { write: true, open: true }),
42
+ definition("start", "Start the exact approved task revision with explicit execution authorization.", mutation({ taskId, run: { type: "boolean", const: true } }), { write: true, open: true }),
43
+ definition("list", "Read up to 100 authorized saved tasks; pass nextCursor as cursor for more. Does not poll providers.", object({ cursor: taskId }, [])),
44
+ definition("inspect", "Read a saved task, brief, progress and supported controls.", object({ taskId })),
45
+ definition("evidence", "Read bounded saved evidence and artifact identities; does not reconcile providers.", object({ taskId })),
46
+ definition("logs", "Read one bounded saved log window; does not create a provider poller.", object({ taskId, after: { type: "integer", minimum: 0 }, count: { type: "integer", minimum: 1, maximum: 100 } }, ["taskId"])),
47
+ definition("control", "Queue a supported control for the existing coordinator; cancellation may leave remote compute running.", mutation({ taskId, action: { type: "string", enum: ["recheck", "resume", "message", "cancel"] }, note: { type: "string", maxLength: 4000 }, interrupt: { type: "boolean" } }, ["taskId", "action"]), { write: true, destructive: true, open: true }),
48
+ ]);
49
+
50
+ export function assertCompatible({ coreVersion, operationVersion, taskVersion, executionVersion }) {
51
+ if (!/^0\.2\.\d+$/.test(coreVersion || "") || operationVersion !== DELIVERY_VERSION || taskVersion !== 1 || executionVersion !== 1) {
52
+ throw deliveryError("DELIVERY_VERSION", "Incompatible core or contract. Stop wake-ups, back up the durable namespace, migrate with a compatible package, or restore the matching package and state backup. Never replace active-job code.");
53
+ }
54
+ }
55
+
56
+ export function uiResourceConfiguration({ uri, domain, connectDomains = [], resourceDomains = [] }) {
57
+ const origin = value => { const u = new URL(value); if (u.protocol !== "https:" || u.origin !== value || u.username || u.password) throw deliveryError("DELIVERY_RESOURCE", "UI configuration needs exact HTTPS origins"); return value; };
58
+ if (typeof uri !== "string" || !/^ui:\/\/[A-Za-z0-9/-]+$/.test(uri)) throw deliveryError("DELIVERY_RESOURCE", "Configure a registered ui:// resource URI");
59
+ return { uri, mimeType: "text/html;profile=mcp-app", toolMeta: { ui: { resourceUri: uri }, "openai/ui": { entrypoints: [{ type: "global" }, { type: "thread" }] } },
60
+ resourceMeta: { ui: { domain: origin(domain), csp: { connectDomains: connectDomains.map(origin), resourceDomains: resourceDomains.map(origin) } } } };
61
+ }
@@ -0,0 +1,40 @@
1
+ // Transport registration only. Production identity and OAuth stay with APort.
2
+ import { TASK_OPERATIONS, MAX_TASK_UI_BYTES, taskRejectionForModel } from "./task_delivery_contract.mjs";
3
+ import { modelSummary, taskListSummary, capabilitySummary, evidenceSummary, logSummary } from "./task_delivery_view.mjs";
4
+
5
+ export async function registerTaskDelivery(server, { delivery, identityFor, resource = null, readResource = null }) {
6
+ if (!delivery?.dispatch || typeof identityFor !== "function") throw new Error("Supply the scoped dispatcher and APort's authenticated request identity resolver");
7
+ const { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
8
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TASK_OPERATIONS.map(op => ({ name: `task_${op.name}`, description: op.description,
9
+ inputSchema: op.inputSchema, annotations: op.annotations, ...(resource ? { _meta: resource.toolMeta } : {}) })) }));
10
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
11
+ const name = request.params.name.replace(/^task_/, "");
12
+ try {
13
+ const view = await delivery.dispatch(name, request.params.arguments || {}, await identityFor(extra));
14
+ const summary = name === "capabilities" ? capabilitySummary(view) : name === "evidence" ? evidenceSummary(view) : name === "logs" ? logSummary(view) : view.tasks ? { version: view.version, tasks: view.tasks.map(taskListSummary), truncated: view.truncated, nextCursor: view.nextCursor } : view.taskId && view.status ? modelSummary(view) : { version: view.version, available: true };
15
+ return { structuredContent: summary, content: [{ type: "text", text: JSON.stringify(summary) }], _meta: { taskDelivery: name === "list" ? summary : view } };
16
+ } catch (error) {
17
+ const rejection = taskRejectionForModel(error);
18
+ if (rejection) return { isError: true, content: [{ type: "text", text: `${rejection.code}: ${rejection.message}` }] };
19
+ // Authentication challenges are owned by APort's transport layer. Never
20
+ // serialize a raw credential/provider/filesystem error into model context.
21
+ return { isError: true, content: [{ type: "text", text: `${/^DELIVERY_[A-Z_]+$/.test(error.code || "") ? error.code : "DELIVERY_FAILED"}: Inspect the authorized task or reconnect your account before retrying.` }] };
22
+ }
23
+ });
24
+ if (resource) {
25
+ if (typeof readResource !== "function") throw new Error("Supply the built APort resource reader");
26
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [{ uri: resource.uri, name: "Task delivery", mimeType: resource.mimeType }] }));
27
+ server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
28
+ try {
29
+ if (request.params.uri !== resource.uri) throw new Error("Resource unavailable");
30
+ const identity = await identityFor(extra);
31
+ // The supplied asset callback must authorize the identity as well as load
32
+ // built assets. It receives no path or user-supplied URL.
33
+ const text = await readResource(identity);
34
+ if (typeof text !== "string" || Buffer.byteLength(text) > MAX_TASK_UI_BYTES) throw new Error("Built UI resource exceeds the shared byte limit or is unavailable");
35
+ return { contents: [{ uri: resource.uri, mimeType: resource.mimeType, text, _meta: resource.resourceMeta }] };
36
+ } catch { throw new Error("Task resource unavailable; reconnect the authorized account or repair its built UI asset"); }
37
+ });
38
+ }
39
+ return server;
40
+ }
@@ -0,0 +1,43 @@
1
+ // Durable single-host adapter. Its root MUST live on the durable coordinator volume.
2
+ import { statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { ensureDir, readJson, writeJsonAtomic } from "./fs.mjs";
6
+ import { acquireProcessLock } from "./daemon.mjs";
7
+ import { deliveryError } from "./task_delivery_contract.mjs";
8
+ export const MAX_DELIVERY_OPERATIONS = 256;
9
+ export const MAX_DELIVERY_STORE_BYTES = 32 * 1024 * 1024;
10
+ export const MAX_CANCELLATION_STORE_BYTES = 1024 * 1024;
11
+ const bytes = value => Buffer.byteLength(JSON.stringify(value, null, 2) + "\n");
12
+ const checkSize = data => {
13
+ const { cancellations, ...ordinary } = data;
14
+ if (bytes(ordinary) > MAX_DELIVERY_STORE_BYTES || (cancellations && bytes({ cancellations }) > MAX_CANCELLATION_STORE_BYTES)) throw receiptCapacityError();
15
+ };
16
+ export const receiptCapacityError = () => deliveryError("DELIVERY_CAPACITY", "Delivery receipt capacity reached; retain this namespace for retries, drain active work and archive it before provisioning a separate namespace for new operations. Never resend an old operation ID in a new namespace.");
17
+ export const scopeKey = scope => {
18
+ for (const key of ["tenant", "account", "environment", "region"]) if (typeof scope?.[key] !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(scope[key])) throw deliveryError("DELIVERY_SCOPE", `APort must supply validated ${key}`);
19
+ return createHash("sha256").update(JSON.stringify([scope.tenant, scope.account, scope.environment, scope.region])).digest("hex");
20
+ };
21
+ export function createDeliveryStore(root) {
22
+ const folder = scope => ensureDir(join(root, scopeKey(scope)));
23
+ const read = scope => {
24
+ const file = join(folder(scope), "delivery.json");
25
+ try { if (statSync(file).size > MAX_DELIVERY_STORE_BYTES + MAX_CANCELLATION_STORE_BYTES) throw receiptCapacityError(); }
26
+ catch (error) { if (error.code !== "ENOENT") throw error; }
27
+ const data = readJson(file, { version: 1, tasks: {}, operations: {} });
28
+ if (data.version !== 1) throw deliveryError("DELIVERY_VERSION", "Back up the delivery store and restore a compatible version before reopening tasks");
29
+ checkSize(data);
30
+ return data;
31
+ };
32
+ return {
33
+ read,
34
+ transaction: async (scope, fn) => {
35
+ const dir = folder(scope), lock = acquireProcessLock(join(dir, "delivery.lock"), "task delivery");
36
+ try { return await fn(read(scope), data => {
37
+ checkSize(data);
38
+ return writeJsonAtomic(join(dir, "delivery.json"), data);
39
+ }); }
40
+ finally { lock.release(); }
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,88 @@
1
+ import { DELIVERY_VERSION } from "./task_delivery_contract.mjs";
2
+ import { redact } from "./execution_io.mjs";
3
+
4
+ // All inputs are saved observations, never a transport or live credential.
5
+ export function taskView(task, { entry = null, run = null, result = null, verification = null, capabilities = {}, controls = {}, secrets = [] } = {}) {
6
+ // Preparation is real provider work too. Keep its saved status visible until
7
+ // the implementation owns an execution, without polling from the projection.
8
+ const ex = run?.execution || entry?.execution || task.preparation?.attempts?.at(-1)?.execution || {};
9
+ const pr = task.implementation?.pr || entry?.pr || null;
10
+ const url = pr && /^[-\w.]+\/[-\w.]+$/.test(task.repo) ? `https://github.com/${task.repo}/pull/${Number(pr)}` : null;
11
+ return redact({ version: DELIVERY_VERSION, taskId: task.taskId, revision: task.revision, status: task.status,
12
+ repo: task.repo, base: task.base, harness: task.harness, original: task.original,
13
+ questions: task.questions, answers: task.answers, requirements: task.requirements,
14
+ assumptions: task.assumptions, mapping: task.mapping, stories: task.stories,
15
+ brief: task.draft ? { hash: task.draft.hash, revision: task.draft.revision, current: task.draft.current, text: task.draft.text } : null,
16
+ readiness: task.readiness, validation: task.validation, allowance: task.preparation?.allowance, completion: task.completion,
17
+ failures: task.failures, errorCode: task.errorCode, nextAction: task.nextAction,
18
+ progress: { workflowState: entry?.state || task.status, providerState: ex.providerState || null,
19
+ observedAt: ex.observedAt || null, reconciledAt: ex.reconciledAt || null, nextAttemptAt: ex.nextAttemptAt || null,
20
+ recoveryAction: ex.recoveryAction || null, costUsd: ex.costUsd ?? null, continuity: capabilities.continuation || null },
21
+ implementation: task.implementation ? { id: task.implementation.id, run: run?.id || entry?.last?.run || task.implementation.run, state: entry?.state || task.implementation.state, pr, url } : null,
22
+ sources: task.sources, artifacts: [
23
+ ...(task.artifacts || []).filter(a => a.id === "brief").slice(0, 1).map(a => ({ id: a.id, hash: a.hash })),
24
+ ...(result?.files || []).slice(0, 100).map((a, i) => ({ id: `result-${i}`, path: a.path, hash: a.afterHash || a.hash || null })),
25
+ ...(task.artifacts || []).filter(a => a.id !== "brief").slice(0, 100).map(a => ({ id: a.id, hash: a.hash })),
26
+ ].slice(0, 100),
27
+ evidence: { providerTests: (result?.tests || []).slice(0, 100), verification: verification ? { inputHead: verification.inputHead, head: verification.head, ok: verification.ok, commands: verification.commands, status: verification.status, observations: verification.evidence } : null },
28
+ controls, capability: capabilities,
29
+ }, secrets);
30
+ }
31
+
32
+ export function modelSummary(view) {
33
+ // Only bounded redacted recovery reasons enter model context; raw diagnostics,
34
+ // briefs and logs remain in the explicit UI/evidence projection.
35
+ return { version: view.version, taskId: view.taskId, revision: view.revision, status: view.status, harness: view.harness, repo: boundedText(view.repo, 256), completion: view.completion,
36
+ recovery: view.status === "blocked" ? {
37
+ code: /^TASK_[A-Z_]{1,64}$/.test(view.errorCode || "") ? view.errorCode : "TASK_BLOCKED",
38
+ reasons: (Array.isArray(view.failures) ? view.failures : []).slice(0, 5).map(reason => boundedText(reason, 500)).filter(Boolean),
39
+ nextAction: boundedText(view.nextAction, 500),
40
+ } : null,
41
+ controls: Object.fromEntries(["recheck", "resume", "message", "cancel", "interrupt"].map(key => [key, view.controls?.[key] === true])),
42
+ cancellation: ["stop-future-dispatch; remote-compute-may-continue", "requires-provider-confirmation"].includes(view.controls?.cancellation) ? view.controls.cancellation : null,
43
+ questionIds: (view.questions || []).map(q => q.id),
44
+ questions: (view.questions || []).map(q => ({ id: q.id, text: q.text })), workflowState: view.progress?.workflowState, pr: view.implementation?.url || null };
45
+ }
46
+
47
+ // Discovery must reach the model as well as the UI. Only public capability
48
+ // fields are projected; server config, paths and arbitrary adapter extras stay out.
49
+ export function capabilitySummary(view) {
50
+ return { version: view.version, repositories: view.repositories.slice(0, 100).map(repo => ({
51
+ id: repo.id, completion: repo.completion, providers: repo.providers.map(provider => ({
52
+ name: provider.name, configured: provider.configured, available: provider.available,
53
+ transport: provider.transport, continuation: provider.continuation,
54
+ controls: { cancel: provider.controls?.cancel, interrupt: provider.controls?.interrupt },
55
+ cost: { source: provider.cost?.source, hardCap: provider.cost?.hardCap },
56
+ canPrepare: provider.canPrepare, accountAccess: provider.accountAccess,
57
+ setup: provider.setup, preparationSetup: provider.preparationSetup,
58
+ })),
59
+ })), truncated: view.truncated, nextCursor: view.nextCursor };
60
+ }
61
+
62
+
63
+ // Explicit requested evidence only: no raw brief, source paths, logs or diagnostics.
64
+ const boundedText = (value, limit = 300) => typeof value === "string" ? value.slice(0, limit) : null;
65
+ export function evidenceSummary(view) {
66
+ const verification = view.evidence?.verification;
67
+ return { version: view.version, taskId: view.taskId, revision: view.revision, completion: view.completion,
68
+ artifacts: (view.artifacts || []).slice(0, 100).map(a => ({ id: boundedText(a.id, 128), hash: boundedText(a.hash, 128) })),
69
+ providerTests: (view.evidence?.providerTests || []).slice(0, 100).map(t => ({ command: boundedText(t.command), inputHead: boundedText(t.inputHead, 128), exit: Number.isInteger(t.exit) ? t.exit : null })),
70
+ verification: verification ? { ok: typeof verification.ok === "boolean" ? verification.ok : null, status: boundedText(verification.status), inputHead: boundedText(verification.inputHead, 128), head: boundedText(verification.head, 128),
71
+ observations: (verification.observations || []).slice(0, 100).map(t => ({ command: boundedText(t.command), head: boundedText(t.head, 128), exit: Number.isInteger(t.exit) ? t.exit : null })),
72
+ commands: (Array.isArray(verification.commands) ? verification.commands : []).slice(0, 100).map(command => boundedText(command)) } : null,
73
+ pr: view.implementation?.url || null };
74
+ }
75
+ export function logSummary(view) {
76
+ return { version: view.version, taskId: view.taskId, revision: view.revision,
77
+ total: view.total ?? 0, from: view.from ?? null, to: view.to ?? null, nextAfter: view.to ?? null,
78
+ returnedLines: view.lines?.length || 0, gap: boundedText(view.gap), rawLogsInUi: true };
79
+ }
80
+
81
+
82
+ // List pages identify tasks; inspect supplies question text and recovery detail.
83
+ export function taskListSummary(view) {
84
+ return { version: view.version, taskId: view.taskId, revision: view.revision,
85
+ status: boundedText(view.status, 64), harness: boundedText(view.harness, 128),
86
+ repo: boundedText(view.repo, 256), questionCount: view.questions?.length || 0,
87
+ workflowState: boundedText(view.progress?.workflowState, 64), completion: view.completion, pr: boundedText(view.implementation?.url, 512) };
88
+ }