@tea-agent/loop-agent 0.44.0-next.8 → 0.44.0-next.9

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 (75) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/tools/design-terminal-tools.js +17 -7
  20. package/dist/executors/dag-pi/tools/review-terminal-tools.js +17 -7
  21. package/dist/executors/dag-pi-executor.js +2048 -125
  22. package/dist/executors/pi-executor.js +6 -1
  23. package/dist/executors/pi-sdk-executor.js +76 -11
  24. package/dist/executors/shell-executor.js +88 -12
  25. package/dist/infrastructure/console/operation-store.js +2 -0
  26. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  27. package/dist/shared/operator/capabilities.js +10 -5
  28. package/dist/task/config-types.js +22 -9
  29. package/dist/task/contract/project.js +13 -0
  30. package/dist/task/contract/schema.js +2 -8
  31. package/dist/task/source-prepare/parse-intent.js +124 -5
  32. package/dist/task/source-prepare/prepare.js +6 -1
  33. package/dist/task/source-prepare/semantic-intake.js +17 -2
  34. package/dist/task/source-prepare/source-execution.js +65 -0
  35. package/dist/task/source-prepare/source-fidelity-pi.js +21 -8
  36. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  37. package/dist/worker/console/operator-actions.js +0 -1
  38. package/dist/worker/console/operator-user-error.js +1 -1
  39. package/dist/worker/console/prd-intake-bridge.js +5 -15
  40. package/dist/workflows/dag/budget-enforcement.js +31 -2
  41. package/dist/workflows/dag/frontend-closeout.js +3 -1
  42. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  43. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  44. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  45. package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
  46. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  47. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  48. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  49. package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
  50. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  51. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  52. package/dist/workflows/dag/frontend-review-context.js +29 -1
  53. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  54. package/dist/workflows/dag/frontend-risk.js +8 -3
  55. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  56. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  57. package/dist/workflows/dag/frontend-session-context.js +10 -0
  58. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  59. package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
  60. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  61. package/dist/workflows/dag/frontend-writer-admission.js +2 -1
  62. package/dist/workflows/dag/init-hybrid.js +108 -37
  63. package/dist/workflows/dag/node-execution.js +11 -11
  64. package/dist/workflows/dag/rerun-feedback.js +35 -4
  65. package/dist/workflows/dag/rerun-task.js +62 -67
  66. package/dist/workflows/dag/runner.js +58 -12
  67. package/dist/workflows/dag/types.js +15 -8
  68. package/docs/architecture/runtime-boundaries.md +1 -1
  69. package/docs/templates/backend-test-dag.json +4 -4
  70. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  71. package/package.json +1 -1
  72. package/skills/frontend-design-review/SKILL.md +8 -11
  73. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  74. package/skills/frontend-review/SKILL.md +5 -1
  75. package/skills/loop-agent/references/command-reference.md +8 -0
@@ -0,0 +1,290 @@
1
+ import { completedSourceExecutionSchema } from "./source-execution.js";
2
+ import { currentFrontendSessionIdentity } from "../../workflows/dag/frontend-session-context.js";
3
+ import { sha256OfCanonicalJson } from "../contract/hash.js";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { lstat, mkdir, readdir, rmdir } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { sourceProviderBudgetSchema as ledgerSchema } from "../../application/evaluation/budget.js";
8
+ import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
9
+ import { readFileBoundarySafe } from "../../shared/path-safety.js";
10
+ import { getTaskPaths, loadTaskConfig } from "../runtime.js";
11
+ /** Read-only generation snapshot; never create a zero ledger over old receipts. */
12
+ export async function readTaskSourceProviderBudget(input) {
13
+ const root = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts");
14
+ const file = path.join(root, "source-provider-budget.json");
15
+ const exists = await lstat(file).then(() => true).catch(error => { if (error.code === "ENOENT")
16
+ return false; throw error; });
17
+ if (!exists) {
18
+ const files = await readdir(path.join(root, "session-budget")).catch(error => { if (error.code === "ENOENT")
19
+ return []; throw error; });
20
+ if (files.length || input.expectedBudgetId)
21
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: missing bound source budget");
22
+ return undefined;
23
+ }
24
+ const ledger = ledgerSchema.parse(JSON.parse((await readFileBoundarySafe({ boundaryRoot: root, targetPath: file, label: "source provider budget" })).toString("utf8")));
25
+ validateBinding(input, ledger);
26
+ return ledger;
27
+ }
28
+ function validateBinding(input, ledger) {
29
+ if (ledger.taskId !== input.taskId || ledger.maxProviderRequests !== input.maxProviderRequests || input.expectedBudgetId && ledger.budgetId !== input.expectedBudgetId || input.minimumReservedRequests !== undefined && ledger.reservedRequests < input.minimumReservedRequests)
30
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: source budget binding changed");
31
+ }
32
+ /** The short filesystem critical section spans read, admission and atomic
33
+ * persistence. A stale lock is never stolen on an age heuristic. */
34
+ async function transact(input, reserve) {
35
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(input.taskId) || !Number.isSafeInteger(input.maxProviderRequests) || input.maxProviderRequests < 1)
36
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: invalid source budget identity or limit");
37
+ await loadTaskConfig(input.repoRoot, input.taskId); // never recreate a missing task
38
+ const root = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts");
39
+ const rootStat = await lstat(root);
40
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
41
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: source artifact directory is not a real directory");
42
+ const file = path.join(root, "source-provider-budget.json");
43
+ const lock = path.join(root, ".source-provider-budget.lock");
44
+ let held = false;
45
+ try {
46
+ for (let attempt = 0; attempt < 100; attempt++) {
47
+ try {
48
+ await mkdir(lock);
49
+ held = true;
50
+ break;
51
+ }
52
+ catch (error) {
53
+ if (error.code !== "EEXIST")
54
+ throw error;
55
+ await new Promise(resolve => setTimeout(resolve, 10));
56
+ }
57
+ }
58
+ if (!held)
59
+ throw Error("FRONTEND_PROVIDER_BUDGET_BUSY: source budget lock unavailable");
60
+ let ledger;
61
+ try {
62
+ await lstat(file);
63
+ ledger = ledgerSchema.parse(JSON.parse((await readFileBoundarySafe({ boundaryRoot: root, targetPath: file, label: "source provider budget" })).toString("utf8")));
64
+ }
65
+ catch (error) {
66
+ if (error.code !== "ENOENT")
67
+ throw Error(`FRONTEND_PROVIDER_BUDGET_INVALID: ${error instanceof Error ? error.message : String(error)}`);
68
+ if (input.expectedBudgetId)
69
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: bound task budget is missing");
70
+ // Existing attempts cannot be silently imported as zero-cost history.
71
+ const previous = await readdir(path.join(root, "session-budget")).catch(error => {
72
+ if (error.code === "ENOENT")
73
+ return [];
74
+ throw error;
75
+ });
76
+ if (previous.length)
77
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: source receipts have no bound budget");
78
+ ledger = { schemaVersion: 1, taskId: input.taskId, budgetId: randomUUID(), maxProviderRequests: input.maxProviderRequests, reservedRequests: 0 };
79
+ }
80
+ validateBinding(input, ledger);
81
+ if (reserve) {
82
+ if (ledger.reservedRequests >= ledger.maxProviderRequests)
83
+ throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: source request quota reached");
84
+ const identity = currentFrontendSessionIdentity();
85
+ if (identity) {
86
+ if (identity.executionKind !== "model" || identity.taskId && identity.taskId !== input.taskId)
87
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: request belongs to another session/task");
88
+ const key = `${identity.runId ?? "task"}:${identity.sessionId}`;
89
+ const previous = ledger.requestSessions?.[key];
90
+ if (previous && sha256OfCanonicalJson(previous.identity) !== sha256OfCanonicalJson(identity))
91
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: session identity changed");
92
+ ledger.requestSessions ??= {};
93
+ ledger.requestSessions[key] = { identity, reservedRequests: (previous?.reservedRequests ?? 0) + 1 };
94
+ }
95
+ ledger.reservedRequests++;
96
+ }
97
+ // A failed write grants no admission. A successful reservation is never refunded.
98
+ await writeJsonAtomic(file, ledger, { repoRoot: input.repoRoot });
99
+ return ledger;
100
+ }
101
+ finally {
102
+ if (held)
103
+ await rmdir(lock);
104
+ }
105
+ }
106
+ export async function reserveTaskSourceProviderRequest(input) {
107
+ try {
108
+ await transact(input, true);
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ throw Error(message.startsWith("FRONTEND_PROVIDER_") ? message : `FRONTEND_PROVIDER_BUDGET_PERSIST_FAILED: ${message}`);
113
+ }
114
+ }
115
+ /** Initialize before the adapter writes its first incomplete session receipt. */
116
+ export async function openTaskSourceProviderBudget(input) {
117
+ await transact(input, false);
118
+ return () => reserveTaskSourceProviderRequest(input);
119
+ }
120
+ /** Check the closed set of source sessions registered by durable admission.
121
+ * Receipt copies deduplicate only when their contents are identical. */
122
+ export function summarizeBoundSourceUsage(ledger, receipts) {
123
+ ledgerSchema.parse(ledger);
124
+ const diagnostics = [];
125
+ const sessions = Object.values(ledger.requestSessions ?? {});
126
+ const sources = sessions.filter(session => session.identity.runId === null);
127
+ const unattributedRequests = ledger.reservedRequests - sessions.reduce((sum, session) => sum + session.reservedRequests, 0);
128
+ if (unattributedRequests)
129
+ diagnostics.push("unattributed-task-requests");
130
+ const byId = new Map();
131
+ for (const receipt of receipts) {
132
+ if (receipt.identity.runId !== null)
133
+ continue;
134
+ const key = receipt.identity.sessionId;
135
+ const previous = byId.get(key);
136
+ if (previous && sha256OfCanonicalJson(previous) !== sha256OfCanonicalJson(receipt))
137
+ diagnostics.push(`conflicting-source-receipt:${key}`);
138
+ byId.set(key, receipt);
139
+ if (!sources.some(session => session.identity.sessionId === key))
140
+ diagnostics.push(`unbound-source-receipt:${key}`);
141
+ }
142
+ let totalTokens = 0;
143
+ for (const session of sources) {
144
+ const receipt = byId.get(session.identity.sessionId);
145
+ if (!receipt || session.identity.taskId !== ledger.taskId || !session.identity.sourceDigest || sha256OfCanonicalJson(session.identity) !== sha256OfCanonicalJson(receipt.identity)) {
146
+ diagnostics.push(`source-session-binding:${session.identity.sessionId}`);
147
+ continue;
148
+ }
149
+ if (receipt.completion !== "complete" || !receipt.terminal || receipt.actual.requestCount !== session.reservedRequests)
150
+ diagnostics.push(`source-session-incomplete:${session.identity.sessionId}`);
151
+ const tokens = receipt.actual.totalTokens;
152
+ if (tokens === null || !Number.isFinite(tokens) || tokens < 0)
153
+ diagnostics.push(`source-tokens-unknown:${session.identity.sessionId}`);
154
+ else
155
+ totalTokens += tokens;
156
+ if (receipt.attempts.length && (receipt.attempts.some(attempt => attempt.completion !== "complete" || attempt.actual?.requestCount == null || attempt.actual?.totalTokens == null) || receipt.attempts.reduce((sum, attempt) => sum + (attempt.actual?.requestCount ?? 0), 0) !== session.reservedRequests || receipt.attempts.reduce((sum, attempt) => sum + (attempt.actual?.totalTokens ?? 0), 0) !== tokens))
157
+ diagnostics.push(`source-attempt-usage-mismatch:${session.identity.sessionId}`);
158
+ }
159
+ return { complete: diagnostics.length === 0, diagnostics, sourceSessions: sources.length, sourceRequests: sources.reduce((sum, session) => sum + session.reservedRequests, 0), unattributedRequests, totalTokens: diagnostics.length ? null : totalTokens };
160
+ }
161
+ const receiptHash = (bytes) => createHash("sha256").update(bytes).digest("hex");
162
+ /** Freeze actual bytes, not a caller-supplied token total. Concurrent changes
163
+ * remain diagnostic evidence rather than a cheaper snapshot. */
164
+ export async function freezeTaskSourceUsage(input, ledger) {
165
+ const root = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts");
166
+ const evidence = { receipts: [], executions: [], diagnostics: [] };
167
+ try {
168
+ const files = await readdir(path.join(root, "session-budget")).catch(error => { if (error.code === "ENOENT")
169
+ return []; throw error; });
170
+ for (const name of files.sort()) {
171
+ const relative = `session-budget/${name}`;
172
+ const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, relative), label: "source usage receipt" });
173
+ evidence.receipts.push({ path: relative, sha256: receiptHash(bytes), content: bytes.toString("utf8") });
174
+ }
175
+ const executionIds = new Set(Object.values(ledger.requestSessions ?? {}).filter(row => row.identity.runId === null).map(row => row.identity.sourceExecutionId).filter((id) => !!id));
176
+ for (const id of [...executionIds].sort()) {
177
+ const relative = `source-executions/${id}.json`;
178
+ const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, relative), label: "source execution" });
179
+ evidence.executions.push({ path: relative, sha256: receiptHash(bytes), content: bytes.toString("utf8") });
180
+ }
181
+ for (const ref of [...evidence.receipts, ...evidence.executions]) {
182
+ const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, ref.path), label: "source usage receipt" });
183
+ if (receiptHash(bytes) !== ref.sha256)
184
+ throw Error("source-receipt-changed-during-freeze");
185
+ }
186
+ const after = await readTaskSourceProviderBudget(input);
187
+ if (sha256OfCanonicalJson(after ?? null) !== sha256OfCanonicalJson(ledger))
188
+ throw Error("source-ledger-changed-during-freeze");
189
+ }
190
+ catch (error) {
191
+ evidence.diagnostics.push(error instanceof Error ? error.message : String(error));
192
+ }
193
+ return evidence;
194
+ }
195
+ export function summarizeFrozenSourceUsage(binding) {
196
+ const diagnostics = [...(binding.evidence?.diagnostics ?? [])];
197
+ const receipts = [];
198
+ if (!binding.evidence)
199
+ diagnostics.push("missing-frozen-source-evidence");
200
+ for (const ref of binding.evidence?.receipts ?? []) {
201
+ try {
202
+ if (!/^session-budget\/[^/\\]+\.json$/.test(ref.path) || receiptHash(ref.content) !== ref.sha256)
203
+ throw Error("source-receipt-hash-or-path");
204
+ const receipt = JSON.parse(ref.content);
205
+ if (receipt.schemaVersion !== 2 || receipt.policyVersion !== "frontend-session-observation-v2" || !receipt.identity || receipt.identity.runId !== null || !receipt.actual || !Array.isArray(receipt.attempts) || !Number.isFinite(Date.parse(receipt.startedAt)) || !receipt.finishedAt || Date.parse(receipt.finishedAt) < Date.parse(receipt.startedAt) || !Number.isFinite(Date.parse(receipt.finishedAt)))
206
+ throw Error("invalid-source-receipt");
207
+ receipts.push(receipt);
208
+ }
209
+ catch {
210
+ diagnostics.push(`invalid-frozen-source-receipt:${ref.path}`);
211
+ }
212
+ }
213
+ const executions = new Map();
214
+ const required = new Set(Object.values(binding.ledger.requestSessions ?? {}).filter(row => row.identity.runId === null).map(row => row.identity.sourceExecutionId).filter((id) => !!id));
215
+ for (const ref of binding.evidence?.executions ?? []) {
216
+ try {
217
+ if (receiptHash(ref.content) !== ref.sha256)
218
+ throw Error("execution hash");
219
+ const execution = completedSourceExecutionSchema.parse(JSON.parse(ref.content));
220
+ if (ref.path !== `source-executions/${execution.executionId}.json` || execution.taskId !== binding.ledger.taskId || !required.has(execution.executionId) || executions.has(execution.executionId))
221
+ throw Error("execution identity");
222
+ executions.set(execution.executionId, execution);
223
+ }
224
+ catch {
225
+ diagnostics.push(`invalid-source-execution:${ref.path}`);
226
+ }
227
+ }
228
+ for (const id of required)
229
+ if (!executions.has(id))
230
+ diagnostics.push(`missing-source-execution:${id}`);
231
+ for (const receipt of receipts) {
232
+ const id = receipt.identity.sourceExecutionId;
233
+ if (!id) {
234
+ diagnostics.push(`unregistered-source-execution:${receipt.identity.sessionId}`);
235
+ continue;
236
+ }
237
+ const execution = executions.get(id);
238
+ if (!execution || Date.parse(receipt.startedAt) < Date.parse(execution.startedAt) || Date.parse(receipt.finishedAt) > Date.parse(execution.finishedAt) || (execution.phase === "semantic-intake") !== (receipt.identity.phase === "semantic-intake"))
239
+ diagnostics.push(`source-execution-session-mismatch:${receipt.identity.sessionId}`);
240
+ }
241
+ const relation = (snapshot) => {
242
+ if (!binding.contract)
243
+ return "unbound";
244
+ if (snapshot.revision === null && snapshot.canonicalHash === null && snapshot.observedCanonicalHash === null)
245
+ return "pre-contract";
246
+ if (snapshot.revision === null || snapshot.canonicalHash === null || snapshot.observedCanonicalHash !== snapshot.canonicalHash)
247
+ return "conflict";
248
+ if (snapshot.revision > binding.contract.revision)
249
+ return "future-revision";
250
+ if (snapshot.revision < binding.contract.revision)
251
+ return "prior-revision";
252
+ return snapshot.canonicalHash === binding.contract.canonicalHash ? "current" : "conflict";
253
+ };
254
+ const executionContracts = [...executions.values()].map(execution => ({ executionId: execution.executionId, before: relation(execution.beforeContract), after: relation(execution.afterContract) }));
255
+ for (const row of executionContracts)
256
+ if ([row.before, row.after].some(value => ["unbound", "conflict", "future-revision"].includes(value)))
257
+ diagnostics.push(`source-execution-contract:${row.executionId}`);
258
+ try {
259
+ const usage = summarizeBoundSourceUsage(binding.ledger, receipts);
260
+ diagnostics.push(...usage.diagnostics);
261
+ // Non-source requests in a task-lifetime ledger have not been priced by
262
+ // this source-only proof. Keep their existing unknown-cost marker.
263
+ if (binding.ledger.reservedRequests !== usage.sourceRequests)
264
+ diagnostics.push("non-source-task-cost-unproven");
265
+ return { ...usage, executionContracts, complete: diagnostics.length === 0, diagnostics, totalTokens: diagnostics.length ? null : usage.totalTokens };
266
+ }
267
+ catch {
268
+ return { executionContracts, complete: false, diagnostics: [...diagnostics, "invalid-source-usage"], sourceSessions: null, sourceRequests: null, unattributedRequests: null, totalTokens: null };
269
+ }
270
+ }
271
+ /** Evaluation checks the original files against the frozen generation bytes.
272
+ * It never reads today's task ledger as yesterday's cost. */
273
+ export async function verifyFrozenSourceUsage(repoRoot, binding) {
274
+ const summary = summarizeFrozenSourceUsage(binding);
275
+ const diagnostics = [...summary.diagnostics];
276
+ const root = path.join(getTaskPaths(repoRoot, binding.ledger.taskId).taskDir, "artifacts");
277
+ for (const ref of [...(binding.evidence?.receipts ?? []), ...(binding.evidence?.executions ?? [])]) {
278
+ try {
279
+ if (!/^(?:session-budget|source-executions)\/[^/\\]+\.json$/.test(ref.path))
280
+ throw Error("invalid path");
281
+ const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, ref.path), label: "frozen source usage" });
282
+ if (receiptHash(bytes) !== ref.sha256)
283
+ throw Error("changed receipt");
284
+ }
285
+ catch {
286
+ diagnostics.push(`source-evidence-missing-or-changed:${ref.path}`);
287
+ }
288
+ }
289
+ return { ...summary, complete: diagnostics.length === 0, diagnostics, totalTokens: diagnostics.length ? null : summary.totalTokens };
290
+ }
@@ -2789,7 +2789,6 @@ export async function dispatchOperatorAction(ctx, req) {
2789
2789
  reconcileAction,
2790
2790
  "--reason",
2791
2791
  reason,
2792
- "--json",
2793
2792
  ],
2794
2793
  });
2795
2794
  await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
@@ -114,7 +114,7 @@ const CONTEXT_TRANSFER_CODE_COPY = [
114
114
  },
115
115
  {
116
116
  pattern: /\bOPERATION_NEEDS_RECONCILE\b/,
117
- copy: "当前操作的执行结果尚未确认,需要先核对(OPERATION_NEEDS_RECONCILE)。请查看操作记录和相关任务状态,核对实际执行结果后再决定后续操作;确认前请勿重复提交。",
117
+ copy: "当前操作的执行结果尚未确认,需要先核对(OPERATION_NEEDS_RECONCILE)。请查看操作记录和相关任务状态,核对实际执行结果后再决定后续操作;确认前请勿重复提交。"
118
118
  },
119
119
  {
120
120
  pattern: /\bCONTEXT_EXPORT_FAILED\b/,
@@ -1,3 +1,4 @@
1
+ import { capabilityBoundarySchema } from "../../task/config-types.js";
1
2
  /**
2
3
  * Console PRD Intake Bridge (design 2026-08-11-console-prd-intake-bridge).
3
4
  * Maps task-side intake artifacts / projected source into Console DraftStore
@@ -119,20 +120,9 @@ export function parseEngineeringBoundaryFromParams(p) {
119
120
  };
120
121
  const allowedPaths = asList(p.allowedPaths ?? p.allowedPath);
121
122
  const forbiddenPaths = asList(p.forbiddenPaths ?? p.forbiddenPath);
122
- const capabilityBoundaryRaw = p.capabilityBoundary;
123
- const capabilityBoundary = capabilityBoundaryRaw && typeof capabilityBoundaryRaw === "object" && !Array.isArray(capabilityBoundaryRaw)
124
- ? {
125
- allowed: Array.isArray(capabilityBoundaryRaw.allowed)
126
- ? capabilityBoundaryRaw.allowed.filter((x) => typeof x === "string" && x.trim().length > 0)
127
- : undefined,
128
- forbidden: Array.isArray(capabilityBoundaryRaw.forbidden)
129
- ? capabilityBoundaryRaw.forbidden.filter((x) => typeof x === "string" && x.trim().length > 0)
130
- : undefined,
131
- requiredClauses: Array.isArray(capabilityBoundaryRaw.requiredClauses)
132
- ? capabilityBoundaryRaw.requiredClauses.filter((x) => typeof x === "string" && x.trim().length > 0)
133
- : undefined,
134
- }
135
- : undefined;
123
+ const capabilityBoundary = p.capabilityBoundary === undefined
124
+ ? undefined
125
+ : capabilityBoundarySchema.parse(p.capabilityBoundary);
136
126
  const interactionIds = asList(p.interactionIds ?? p.interactionId);
137
127
  const requirementOwnership = Array.isArray(p.requirementOwnership)
138
128
  ? p.requirementOwnership
@@ -206,7 +196,7 @@ export function parseEngineeringBoundaryFromParams(p) {
206
196
  ...(forbiddenPaths.length > 0 ? { forbiddenPaths } : {}),
207
197
  ...(verifyCommands.length > 0 ? { verifyCommands } : {}),
208
198
  ...(requirementOwnership?.length ? { requirementOwnership } : {}),
209
- ...(capabilityBoundary && ((capabilityBoundary.allowed?.length ?? 0) + (capabilityBoundary.forbidden?.length ?? 0)) > 0
199
+ ...(capabilityBoundary
210
200
  ? { capabilityBoundary }
211
201
  : {}),
212
202
  ...(interactionIds.length > 0 ? { interactionIds } : {}),
@@ -1,6 +1,6 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { checkBudgetPreNode, createBudgetLedger, formatBudgetReportMarkdown, isHardBudgetBreached, recordNodeBudgetSample, reserveProviderBudget, skipPendingNodesForBudgetBreach, } from "../../application/evaluation/budget.js";
3
+ import { campaignBudgetSchema, checkBudgetPreNode, createBudgetLedger, formatBudgetReportMarkdown, isHardBudgetBreached, recordNodeBudgetSample, reserveProviderBudget, skipPendingNodesForBudgetBreach, } from "../../application/evaluation/budget.js";
4
4
  import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
5
5
  /** A continuation already contains its ancestors' consumption. Never sum both. */
6
6
  export function validateCumulativeBudgetSuccessor(parent, child) {
@@ -9,9 +9,33 @@ export function validateCumulativeBudgetSuccessor(parent, child) {
9
9
  const next = child.budgetLedger;
10
10
  if (!previous || !next || !lineage || (child.continuation?.parentRunId ?? child.frontendRecoveryState?.parentRunId) !== parent.runId || lineage.parentLedgerSha256 !== sha256OfCanonicalJson(previous) || sha256OfCanonicalJson(lineage.inheritedConsumed) !== sha256OfCanonicalJson(previous.consumed) || sha256OfCanonicalJson(parent.budget ?? null) !== sha256OfCanonicalJson(child.budget ?? null))
11
11
  throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: unbound cumulative continuation");
12
+ const invalid = (reason) => { throw Error(`FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: ${reason}`); };
13
+ for (const [state, ledger] of [[parent, previous], [child, next]]) {
14
+ const parsed = campaignBudgetSchema.safeParse(state.budget);
15
+ if (!parsed.success || ledger.schemaVersion !== 1 || ledger.mode !== parsed.data.mode || sha256OfCanonicalJson(ledger.limits) !== sha256OfCanonicalJson(parsed.data.limits))
16
+ invalid("ledger differs from frozen budget");
17
+ const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
18
+ for (const key of ["executorCalls", "wallTimeMs", "repairPasses", "peakContextChars"])
19
+ if (!count(ledger.consumed[key]))
20
+ invalid("invalid consumption");
21
+ if (ledger.consumed.tokens !== null && !count(ledger.consumed.tokens))
22
+ invalid("invalid token consumption");
23
+ if ((ledger.mode === "hard" && ledger.limits.maxProviderRequests !== undefined || ledger.consumed.providerRequests !== undefined) && !count(ledger.consumed.providerRequests))
24
+ invalid("invalid request consumption");
25
+ if (!Array.isArray(ledger.consumed.missingTokenNodeIds) || ledger.consumed.missingTokenNodeIds.some(id => typeof id !== "string" || !id))
26
+ invalid("invalid missing token records");
27
+ if (!Array.isArray(ledger.breaches) || (ledger.mode === "record-only" ? ledger.status !== "record-only" : !["ok", "breached"].includes(ledger.status)))
28
+ invalid("invalid ledger status");
29
+ }
12
30
  for (const key of ["executorCalls", "providerRequests", "wallTimeMs", "repairPasses", "peakContextChars"])
13
31
  if ((next.consumed[key] ?? 0) < (previous.consumed[key] ?? 0))
14
- throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: consumption decreased");
32
+ invalid("consumption decreased");
33
+ if (previous.consumed.tokens !== null && (next.consumed.tokens === null || next.consumed.tokens < previous.consumed.tokens))
34
+ invalid("token consumption decreased");
35
+ if (previous.consumed.missingTokenNodeIds.some(id => !next.consumed.missingTokenNodeIds.includes(id)))
36
+ invalid("missing token records lost");
37
+ if (previous.status === "breached" && next.status !== "breached" || previous.breaches.some(breach => !next.breaches.some(value => sha256OfCanonicalJson(value) === sha256OfCanonicalJson(breach))))
38
+ invalid("budget breach lost");
15
39
  return next;
16
40
  }
17
41
  const requestQueues = new WeakMap();
@@ -27,6 +51,11 @@ export async function reserveDagProviderRequest(input) {
27
51
  throw Error("FRONTEND_PROVIDER_BUDGET_STALE: request belongs to an inactive attempt");
28
52
  if (isHardBudgetBreached(ledger))
29
53
  throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: run budget already breached");
54
+ if ((ledger.consumed.providerRequests ?? 0) < ledger.limits.maxProviderRequests && input.reserveTaskRequest) {
55
+ await input.reserveTaskRequest();
56
+ if (!active())
57
+ throw Error("FRONTEND_PROVIDER_BUDGET_STALE: attempt stopped during task reservation");
58
+ }
30
59
  const breach = reserveProviderBudget(ledger, input.nodeId);
31
60
  if (breach)
32
61
  applyHardBudgetBreach(input.state, breach);
@@ -44,6 +44,8 @@ export const frontendCloseoutFactsSchema = z
44
44
  status: frontendCoverageStatusSchema,
45
45
  evidence: z.enum(["file-exists", "command-execution", "test-file-execution", "unavailable"]).optional(),
46
46
  behaviorCoverage: z.enum(["unconfirmed"]).optional(),
47
+ plannedEvidenceLevel: z.enum(["unit", "mounted", "real-integration", "unknown"]).optional(),
48
+ boundary: z.string().min(1).optional(),
47
49
  })
48
50
  .strict()),
49
51
  lintStatus: frontendLintStatusSchema,
@@ -145,7 +147,7 @@ function bulletList(items) {
145
147
  return items.map((item) => `- ${item}`).join("\n");
146
148
  }
147
149
  function coverageLines(facts) {
148
- const lines = facts.coverageMatrix.map((entry) => `- ${entry.verificationTargetId}: execution=${entry.status}; evidence=${entry.evidence ?? "unspecified"}; behavior-coverage=${entry.behaviorCoverage ?? "unconfirmed"}`);
150
+ const lines = facts.coverageMatrix.map((entry) => `- ${entry.verificationTargetId}: execution=${entry.status}; evidence=${entry.evidence ?? "unspecified"}; behavior-coverage=${entry.behaviorCoverage ?? "unconfirmed"}; planned-evidence=${entry.plannedEvidenceLevel ?? "unknown"}${entry.boundary ? `; boundary=${entry.boundary}` : ""}`);
149
151
  return lines.length > 0 ? lines.join("\n") : "_(none)_";
150
152
  }
151
153
  function frontendStatus(facts) {
@@ -5,7 +5,8 @@ function object(value) {
5
5
  ? value
6
6
  : undefined;
7
7
  }
8
- /** Join requirement-owned identity with separately committed evidence by id.
8
+ /** Project the latest requirement per id while retaining first-seen order.
9
+ * The append-only ledger also contains earlier execution metadata revisions.
9
10
  * Later evidence declarations replace only the lanes they explicitly name. */
10
11
  export function resolveFrontendContractRequirements(facts) {
11
12
  const contractFacts = facts.map(object).filter((fact) => fact?.origin === "contract");
@@ -19,13 +20,15 @@ export function resolveFrontendContractRequirements(facts) {
19
20
  ...object(fact.evidence),
20
21
  });
21
22
  }
22
- return contractFacts.flatMap((fact) => fact.kind === "requirement" && typeof fact.id === "string"
23
- ? [{
24
- ...fact,
25
- id: fact.id,
26
- evidence: { ...object(fact.evidence), ...evidenceById.get(fact.id) },
27
- }]
28
- : []);
23
+ const requirements = new Map();
24
+ for (const fact of contractFacts) {
25
+ if (fact.kind === "requirement" && typeof fact.id === "string")
26
+ requirements.set(fact.id, fact);
27
+ }
28
+ return [...requirements].map(([id, fact]) => ({
29
+ ...fact, id,
30
+ evidence: { ...object(fact.evidence), ...evidenceById.get(id) },
31
+ }));
29
32
  }
30
33
  function names(value) {
31
34
  return Array.isArray(value)
@@ -243,9 +243,9 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths, opti
243
243
  });
244
244
  return findings;
245
245
  }
246
- /** 5. Every implementation target and every non-static verification target
247
- * must fall inside the declared writeSet patterns. */
248
- export function checkTargetPaths(contract, writeSetPatterns) {
246
+ /** 5. Implementation targets require write permission. Existing, safely
247
+ * resolved verification files may remain read-only outside that writeSet. */
248
+ export function checkTargetPaths(contract, writeSetPatterns, existingVerificationPaths = []) {
249
249
  const findings = [];
250
250
  // No frozen writeSet patterns (legacy / unconfigured run) → nothing to
251
251
  // check; an empty pattern set must not report every file as out of scope.
@@ -275,7 +275,7 @@ export function checkTargetPaths(contract, writeSetPatterns) {
275
275
  for (const target of contract.verificationTargets) {
276
276
  if (target.mode === "static")
277
277
  continue;
278
- if (!matches(target.file)) {
278
+ if (!matches(target.file) && !existingVerificationPaths.includes(target.file)) {
279
279
  findings.push({
280
280
  code: "target-path-outside-write-set",
281
281
  message: `verification target "${target.id}" file is outside the writeSet patterns: ${target.file}`,
@@ -442,7 +442,7 @@ export function checkRequiredDeliverables(contract) {
442
442
  return [];
443
443
  const planned = new Set([
444
444
  ...contract.targets.files,
445
- ...contract.requirements.flatMap((requirement) => requirement.implementationTargets),
445
+ ...contract.requirements.flatMap((requirement) => [...requirement.implementationTargets, ...(requirement.deliverableTargets ?? [])]),
446
446
  ...contract.verificationTargets.map((target) => target.file),
447
447
  ]);
448
448
  return required
@@ -464,7 +464,7 @@ export function evaluateFrontendDesignPolicy(input) {
464
464
  ...checkComponentChoice(input.contract, input.componentSpecCandidatePaths, input.hasFreshPathEvidence
465
465
  ? { hasFreshPathEvidence: input.hasFreshPathEvidence }
466
466
  : undefined),
467
- ...checkTargetPaths(input.contract, input.writeSetPatterns),
467
+ ...checkTargetPaths(input.contract, input.writeSetPatterns, input.existingVerificationPaths),
468
468
  ...checkDependencies(input.contract, input.allowedDependencies),
469
469
  ...checkUiStateAttribution(input.contract),
470
470
  ...checkUxNameCollisions(input.contract),