@tea-agent/loop-agent 0.42.0 → 0.43.0-next.2
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.
- package/CHANGELOG.md +34 -7
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +762 -252
- package/dist/executors/pi-executor.js +20 -1
- package/dist/executors/pi-sdk-executor.js +64 -1
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +22 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +260 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +746 -265
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +48 -10
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-scopes.js +117 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +16 -10
- package/dist/workflows/dag/node-execution.js +32 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/package.json +1 -1
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +2 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspectRouteStates } from "../state.js";
|
|
2
|
+
import { mergeTaskHistoryPage } from "../task-history.js";
|
|
1
3
|
/** Observe UI view module (R5). */
|
|
2
4
|
import {
|
|
3
5
|
UI_TEXT,
|
|
@@ -16,7 +18,7 @@ import {
|
|
|
16
18
|
DAG_EFFECTIVE_STATUS_LABELS,
|
|
17
19
|
STATUS_LABELS,
|
|
18
20
|
} from "../constants.js";
|
|
19
|
-
import { clearNode, el, metaGrid, buildTable } from "../dom.js";
|
|
21
|
+
import { clearNode, el, metaGrid, buildTable, reconcileKeyed } from "../dom.js";
|
|
20
22
|
import {
|
|
21
23
|
badge,
|
|
22
24
|
badgeClass,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
import {
|
|
46
48
|
fetchJson,
|
|
47
49
|
fetchJsonResult,
|
|
50
|
+
summarizeFetchError,
|
|
48
51
|
artifactUrl,
|
|
49
52
|
parseArtifactPreviewResponse,
|
|
50
53
|
} from "../api.js";
|
|
@@ -66,7 +69,7 @@ import {
|
|
|
66
69
|
renderViewState,
|
|
67
70
|
mountViewState,
|
|
68
71
|
} from "../state.js";
|
|
69
|
-
import { uiState } from "../state.js";
|
|
72
|
+
import { uiState, taskHistoryStates, workspaceScope } from "../state.js";
|
|
70
73
|
import { sortPoolTasks } from "../format-pool.js";
|
|
71
74
|
import {
|
|
72
75
|
dagMetaVisibility,
|
|
@@ -75,7 +78,7 @@ import {
|
|
|
75
78
|
findDagForRun,
|
|
76
79
|
} from "../dag-model.js";
|
|
77
80
|
import { layoutDag } from "../dag-layout.js";
|
|
78
|
-
import { showView, setBreadcrumb, updateHeaderRefresh, mountRelationBar } from "../shell-chrome.js";
|
|
81
|
+
import { showView, setBreadcrumb, updateHeaderRefresh, mountRelationBar, bindManualViewRefresh } from "../shell-chrome.js";
|
|
79
82
|
import { artifactLink } from "../kpi.js";
|
|
80
83
|
import { findBatchRunIdForTask, taskRunHistoryRow } from "../run-processing.js";
|
|
81
84
|
|
|
@@ -91,7 +94,7 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
91
94
|
showView("task");
|
|
92
95
|
setBreadcrumb([
|
|
93
96
|
{ label: UI_TEXT.dashboard, href: "#/" },
|
|
94
|
-
{ label: "资源池", href: "/#/inspect/pool" },
|
|
97
|
+
{ label: "资源池", href: inspectRouteStates.get("return-list") ?? "/#/inspect/pool" },
|
|
95
98
|
{ label: displayLabel },
|
|
96
99
|
]);
|
|
97
100
|
|
|
@@ -103,13 +106,29 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
103
106
|
const ledgerEl = document.getElementById("task-ledger");
|
|
104
107
|
if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
|
|
105
108
|
if (titleEl) titleEl.textContent = displayLabel;
|
|
106
|
-
|
|
109
|
+
const identity = `${workspaceScope() ?? ""}:${featureId ?? ""}:${taskId}`;
|
|
110
|
+
const requestGeneration = uiState.pollingGeneration;
|
|
111
|
+
if (metaEl.dataset.taskIdentity !== identity) {
|
|
112
|
+
metaEl.dataset.taskIdentity = identity;
|
|
113
|
+
mountViewState(metaEl, "loading", "加载 Task…");
|
|
114
|
+
clearNode(runsEl);
|
|
115
|
+
for (const element of [linksEl, evidenceEl, ledgerEl]) if (element) clearNode(element);
|
|
116
|
+
}
|
|
117
|
+
if (!taskHistoryStates.has(identity)) taskHistoryStates.set(identity, { runs: [], nextBefore: null, loadedOlder: false, loadingOlder: false });
|
|
118
|
+
const historyState = taskHistoryStates.get(identity);
|
|
107
119
|
|
|
108
120
|
const [taskResult, historyResult, snapshot] = await Promise.all([
|
|
109
121
|
fetchJsonResult(taskApi),
|
|
110
122
|
fetchJsonResult(`${taskApi}/runs?limit=20`),
|
|
111
123
|
fetchJson("/api/snapshot"),
|
|
112
124
|
]);
|
|
125
|
+
if (requestGeneration !== uiState.pollingGeneration || metaEl.dataset.taskIdentity !== identity) return false;
|
|
126
|
+
if (!taskResult.ok && ![404, 409].includes(taskResult.status)) {
|
|
127
|
+
let error = metaEl.querySelector(".task-refresh-error");
|
|
128
|
+
if (!error) { error = el("p", "task-refresh-error node-input-error"); metaEl.appendChild(error); }
|
|
129
|
+
error.textContent = `更新失败,保留上次内容。${summarizeFetchError(taskResult)}`;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
113
132
|
const task = taskResult.ok ? taskResult.body : null;
|
|
114
133
|
const history = historyResult.ok ? historyResult.body : null;
|
|
115
134
|
if (snapshot) {
|
|
@@ -120,7 +139,6 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
120
139
|
clearNode(metaEl);
|
|
121
140
|
clearNode(linksEl);
|
|
122
141
|
clearNode(evidenceEl);
|
|
123
|
-
clearNode(runsEl);
|
|
124
142
|
|
|
125
143
|
if (!task) {
|
|
126
144
|
if (taskResult.status === 409) {
|
|
@@ -234,27 +252,9 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
234
252
|
evidenceEl.appendChild(evidenceLinks);
|
|
235
253
|
}
|
|
236
254
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
mountViewState(runsEl, "empty", "暂无 ledger 运行记录。");
|
|
241
|
-
} else {
|
|
242
|
-
const rows = runs.map(taskRunHistoryRow);
|
|
243
|
-
runsEl.appendChild(
|
|
244
|
-
buildTable(
|
|
245
|
-
["worker run", "状态", "时间", "batch", "retryOf", "失败分类"],
|
|
246
|
-
rows,
|
|
247
|
-
),
|
|
248
|
-
);
|
|
249
|
-
if (history?.nextBefore) {
|
|
250
|
-
const more = el("button", "link-button", "加载更早的运行");
|
|
251
|
-
more.type = "button";
|
|
252
|
-
more.addEventListener("click", () => {
|
|
253
|
-
void loadOlderTaskRuns(featureId, taskId, history.nextBefore, runsEl);
|
|
254
|
-
});
|
|
255
|
-
runsEl.appendChild(more);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
255
|
+
if (history) mergeTaskHistoryPage(historyState, history);
|
|
256
|
+
else historyState.error = summarizeFetchError(historyResult);
|
|
257
|
+
renderTaskHistory(runsEl, historyState, featureId, taskId, identity);
|
|
258
258
|
|
|
259
259
|
const active = ["running", "pending", "blocked", "stale"].includes(
|
|
260
260
|
(task.status ?? "").toLowerCase(),
|
|
@@ -262,6 +262,8 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
262
262
|
if (ledgerEl) {
|
|
263
263
|
void renderTaskLedger(ledgerEl, taskId);
|
|
264
264
|
}
|
|
265
|
+
updateHeaderRefresh(snapshot?.generatedAt ?? uiState.lastSnapshot?.generatedAt, { mode: active ? "auto" : "ended" });
|
|
266
|
+
bindManualViewRefresh("task", () => renderTaskDetail(featureId, taskId));
|
|
265
267
|
return active;
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -315,42 +317,59 @@ async function renderTaskLedger(ledgerEl, taskId) {
|
|
|
315
317
|
ledgerEl.appendChild(pre);
|
|
316
318
|
}
|
|
317
319
|
|
|
318
|
-
|
|
319
|
-
if (runsEl
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
function renderTaskHistory(runsEl, state, featureId, taskId, identity) {
|
|
321
|
+
if (runsEl.dataset.historyIdentity !== identity) { clearNode(runsEl); runsEl.dataset.historyIdentity = identity; }
|
|
322
|
+
let table = runsEl.querySelector("table");
|
|
323
|
+
if (!table) {
|
|
324
|
+
runsEl.appendChild(el("h3", "section-heading", "运行历史"));
|
|
325
|
+
table = buildTable(["worker run", "状态", "时间", "batch", "retryOf", "失败分类"], []);
|
|
326
|
+
runsEl.appendChild(table);
|
|
327
|
+
const status = el("p", "muted task-history-status");
|
|
328
|
+
status.setAttribute("role", "status");
|
|
329
|
+
runsEl.appendChild(status);
|
|
330
|
+
const more = el("button", "link-button", "加载更早的运行");
|
|
331
|
+
more.type = "button";
|
|
332
|
+
more.addEventListener("click", () => { void loadOlderTaskRuns(featureId, taskId, state.nextBefore, runsEl); });
|
|
333
|
+
runsEl.appendChild(more);
|
|
324
334
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
335
|
+
reconcileKeyed(table.querySelector("tbody"), state.runs, {
|
|
336
|
+
getKey: (run) => run.workerRunId,
|
|
337
|
+
create: () => el("tr"),
|
|
338
|
+
update: (row, run) => {
|
|
339
|
+
const signature = JSON.stringify(run);
|
|
340
|
+
if (row.dataset.signature === signature) return;
|
|
341
|
+
clearNode(row);
|
|
342
|
+
for (const cell of taskRunHistoryRow(run).cells) {
|
|
343
|
+
const td = el("td");
|
|
344
|
+
if (cell instanceof Node) td.appendChild(cell); else td.textContent = cell ?? "—";
|
|
345
|
+
row.appendChild(td);
|
|
346
|
+
}
|
|
347
|
+
row.dataset.signature = signature;
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
const status = runsEl.querySelector(".task-history-status");
|
|
351
|
+
status.textContent = state.error ? `历史加载失败:${state.error}` : state.runs.length ? `已加载 ${state.runs.length} 次运行` : "暂无 ledger 运行记录。";
|
|
352
|
+
const more = runsEl.querySelector("button.link-button");
|
|
353
|
+
more.hidden = !state.nextBefore && !state.error;
|
|
354
|
+
more.disabled = state.loadingOlder;
|
|
355
|
+
more.textContent = state.loadingOlder ? "正在加载…" : state.error ? "重试加载" : "加载更早的运行";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
|
|
359
|
+
const identity = `${workspaceScope() ?? ""}:${featureId ?? ""}:${taskId}`;
|
|
360
|
+
const state = taskHistoryStates.get(identity);
|
|
361
|
+
if (!state || state.loadingOlder) return;
|
|
362
|
+
state.loadingOlder = true;
|
|
363
|
+
renderTaskHistory(runsEl, state, featureId, taskId, identity);
|
|
364
|
+
const taskApi = featureId ? `/api/features/${encodeURIComponent(featureId)}/tasks/${encodeURIComponent(taskId)}` : `/api/tasks/${encodeURIComponent(taskId)}`;
|
|
365
|
+
try {
|
|
366
|
+
const result = await fetchJsonResult(`${taskApi}/runs?limit=20${state.nextBefore ? `&before=${encodeURIComponent(state.nextBefore)}` : ""}`);
|
|
367
|
+
if (taskHistoryStates.get(identity) !== state) return;
|
|
368
|
+
if (result.ok && result.body) mergeTaskHistoryPage(state, result.body, Boolean(state.nextBefore));
|
|
369
|
+
else state.error = summarizeFetchError(result);
|
|
370
|
+
} finally {
|
|
371
|
+
state.loadingOlder = false;
|
|
372
|
+
if (runsEl.dataset.historyIdentity === identity && taskHistoryStates.get(identity) === state) renderTaskHistory(runsEl, state, featureId, taskId, identity);
|
|
354
373
|
}
|
|
355
374
|
}
|
|
356
375
|
|
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { checkBudgetPreNode, createBudgetLedger, formatBudgetReportMarkdown, isHardBudgetBreached, recordNodeBudgetSample, skipPendingNodesForBudgetBreach, } from "../../application/evaluation/budget.js";
|
|
3
|
+
import { checkBudgetPreNode, createBudgetLedger, formatBudgetReportMarkdown, isHardBudgetBreached, recordNodeBudgetSample, reserveProviderBudget, skipPendingNodesForBudgetBreach, } from "../../application/evaluation/budget.js";
|
|
4
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
5
|
+
/** A continuation already contains its ancestors' consumption. Never sum both. */
|
|
6
|
+
export function validateCumulativeBudgetSuccessor(parent, child) {
|
|
7
|
+
const lineage = child.continuation?.budgetLineage ?? child.frontendRecoveryState?.budgetLineage;
|
|
8
|
+
const previous = parent.budgetLedger;
|
|
9
|
+
const next = child.budgetLedger;
|
|
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
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: unbound cumulative continuation");
|
|
12
|
+
for (const key of ["executorCalls", "providerRequests", "wallTimeMs", "repairPasses", "peakContextChars"])
|
|
13
|
+
if ((next.consumed[key] ?? 0) < (previous.consumed[key] ?? 0))
|
|
14
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: consumption decreased");
|
|
15
|
+
return next;
|
|
16
|
+
}
|
|
17
|
+
const requestQueues = new WeakMap();
|
|
18
|
+
/** One shared, attempt-fenced, durable admission queue for every request in a run. */
|
|
19
|
+
export async function reserveDagProviderRequest(input) {
|
|
20
|
+
const ledger = input.state.budgetLedger;
|
|
21
|
+
if (!ledger || ledger.limits.maxProviderRequests === undefined)
|
|
22
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: frozen request budget unavailable");
|
|
23
|
+
const previous = requestQueues.get(ledger) ?? Promise.resolve();
|
|
24
|
+
const current = previous.catch(() => { }).then(async () => {
|
|
25
|
+
const active = () => input.state.status === "running" && input.state.nodes[input.nodeId]?.status === "RUNNING" && input.state.nodes[input.nodeId]?.currentAttempt === input.attempt;
|
|
26
|
+
if (!active())
|
|
27
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_STALE: request belongs to an inactive attempt");
|
|
28
|
+
if (isHardBudgetBreached(ledger))
|
|
29
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: run budget already breached");
|
|
30
|
+
const breach = reserveProviderBudget(ledger, input.nodeId);
|
|
31
|
+
if (breach)
|
|
32
|
+
applyHardBudgetBreach(input.state, breach);
|
|
33
|
+
try {
|
|
34
|
+
await input.persist();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
ledger.status = "breached";
|
|
38
|
+
throw Error(`FRONTEND_PROVIDER_BUDGET_PERSIST_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
if (breach)
|
|
41
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: request quota reached");
|
|
42
|
+
if (!active())
|
|
43
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_STALE: attempt stopped while reserving request");
|
|
44
|
+
});
|
|
45
|
+
requestQueues.set(ledger, current);
|
|
46
|
+
try {
|
|
47
|
+
await current;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (requestQueues.get(ledger) === current)
|
|
51
|
+
requestQueues.delete(ledger);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
4
54
|
export function initRunBudgetLedger(state, budget) {
|
|
5
55
|
if (!budget)
|
|
6
56
|
return;
|
|
@@ -8,7 +58,7 @@ export function initRunBudgetLedger(state, budget) {
|
|
|
8
58
|
state.budgetLedger = createBudgetLedger(budget);
|
|
9
59
|
}
|
|
10
60
|
function wallTimeMs(state, now = Date.now()) {
|
|
11
|
-
const lineage = state.continuation?.budgetLineage;
|
|
61
|
+
const lineage = state.continuation?.budgetLineage ?? state.frontendRecoveryState?.budgetLineage;
|
|
12
62
|
if (lineage) {
|
|
13
63
|
return (lineage.inheritedConsumed.wallTimeMs +
|
|
14
64
|
Math.max(0, now - new Date(lineage.inheritedAt).getTime()));
|
|
@@ -16,7 +66,7 @@ function wallTimeMs(state, now = Date.now()) {
|
|
|
16
66
|
return Math.max(0, now - new Date(state.startedAt).getTime());
|
|
17
67
|
}
|
|
18
68
|
function repairPasses(state) {
|
|
19
|
-
const lineage = state.continuation?.budgetLineage;
|
|
69
|
+
const lineage = state.continuation?.budgetLineage ?? state.frontendRecoveryState?.budgetLineage;
|
|
20
70
|
if (lineage) {
|
|
21
71
|
return (lineage.inheritedConsumed.repairPasses +
|
|
22
72
|
Math.max(0, (state.convergence?.currentPass ?? 1) - 1));
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Check, Errors } from "typebox/value";
|
|
5
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
6
|
+
import { assertHarnessWriteAllowed } from "../../infrastructure/harness/completed-facts-guard.js";
|
|
7
|
+
import { readTypedEventStoreFromJsonl, typedEventPayloadSha256, hasCommittedTerminalEvent } from "./frontend-typed-event-store.js";
|
|
8
|
+
const queues = new Map();
|
|
9
|
+
const progressByTool = new WeakMap();
|
|
10
|
+
/** Process-local telemetry only; durable facts remain the authority. */
|
|
11
|
+
export function readFrontendToolProgress(tools) {
|
|
12
|
+
const counters = [...new Set((tools ?? []).flatMap(tool => tool && typeof tool === "object" && progressByTool.has(tool) ? [progressByTool.get(tool)] : []))];
|
|
13
|
+
if (!counters.length)
|
|
14
|
+
return undefined;
|
|
15
|
+
return counters.reduce((sum, item) => ({ failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
|
|
16
|
+
}
|
|
17
|
+
async function exclusive(key, work) {
|
|
18
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
19
|
+
const current = previous.catch(() => { }).then(work);
|
|
20
|
+
queues.set(key, current);
|
|
21
|
+
try {
|
|
22
|
+
return await current;
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
if (queues.get(key) === current)
|
|
26
|
+
queues.delete(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const hash = (value) => sha256OfCanonicalJson(JSON.parse(JSON.stringify(value, (_key, v) => v instanceof Map ? [...v.entries()].sort(([a], [b]) => String(a).localeCompare(String(b))) : v)));
|
|
30
|
+
const ledgerError = (code, message) => Object.assign(new Error(message), { code });
|
|
31
|
+
const failure = (code, error) => ({ content: [{ type: "text", text: JSON.stringify({ ok: false, code, error }) }], details: { ok: false, code, error } });
|
|
32
|
+
/** A failed rename must never fall back to copying over a live ledger. */
|
|
33
|
+
async function persist(file, records) {
|
|
34
|
+
assertHarnessWriteAllowed(file);
|
|
35
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
36
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
37
|
+
try {
|
|
38
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
39
|
+
try {
|
|
40
|
+
await handle.writeFile(records.map(r => JSON.stringify(r)).join("\n") + (records.length ? "\n" : ""));
|
|
41
|
+
await handle.sync();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await handle.close();
|
|
45
|
+
}
|
|
46
|
+
await rename(temporary, file);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** One queue covers schema checks, semantic checks, all facts in a tool call,
|
|
53
|
+
* atomic persistence, and acknowledgement. The model never sees a draft as saved. */
|
|
54
|
+
export async function createDurableFrontendTools(input) {
|
|
55
|
+
const file = path.resolve(input.file);
|
|
56
|
+
const tools = input.tools.map(tool => {
|
|
57
|
+
const schema = tool.parameters;
|
|
58
|
+
return { ...tool, parameters: { ...schema, properties: { ...schema.properties, replace: { type: "boolean", description: "Explicitly revise the current fact with the same identity. Omit for new facts or identical replay." } } } };
|
|
59
|
+
});
|
|
60
|
+
const bindingSha256 = hash({ version: 1, attemptId: input.attemptId, binding: input.binding, schemas: tools.map(t => ({ name: t.name, parameters: t.parameters })) });
|
|
61
|
+
const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store); };
|
|
62
|
+
const validate = async (records) => {
|
|
63
|
+
let revision = 0;
|
|
64
|
+
let terminal = false;
|
|
65
|
+
const ids = new Set();
|
|
66
|
+
const requests = new Set();
|
|
67
|
+
for (const r of records) {
|
|
68
|
+
if (r.attemptId !== input.attemptId || r.submission?.bindingSha256 !== bindingSha256)
|
|
69
|
+
throw ledgerError("FRONTEND_LEDGER_BINDING_MISMATCH", "frontend ledger binding/scope mismatch; preserve evidence and restart its owning phase");
|
|
70
|
+
if (r.phase !== "committed" || r.payloadSha256 !== typedEventPayloadSha256(r.fact) || r.revision <= revision || ids.has(r.eventId) || requests.has(r.requestId) || terminal)
|
|
71
|
+
throw ledgerError("FRONTEND_LEDGER_INTEGRITY_INVALID", "frontend ledger integrity/revision mismatch");
|
|
72
|
+
ids.add(r.eventId);
|
|
73
|
+
requests.add(r.requestId);
|
|
74
|
+
revision = r.revision;
|
|
75
|
+
terminal = hasCommittedTerminalEvent({ records: [r], revision }, input.attemptId);
|
|
76
|
+
}
|
|
77
|
+
await input.validateRestored?.(records);
|
|
78
|
+
};
|
|
79
|
+
const restore = async () => {
|
|
80
|
+
const records = await readTypedEventStoreFromJsonl(file);
|
|
81
|
+
if (records.length) {
|
|
82
|
+
await validate(records);
|
|
83
|
+
publish({ records, revision: records.at(-1).revision });
|
|
84
|
+
}
|
|
85
|
+
else if (input.store.records.some(r => r.submission))
|
|
86
|
+
throw ledgerError("FRONTEND_LEDGER_MISSING", "frontend durable ledger disappeared or became empty; refusing an unproven acknowledgement");
|
|
87
|
+
};
|
|
88
|
+
await exclusive(file, restore);
|
|
89
|
+
const progress = { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
|
|
90
|
+
const replay = (receipt) => { progress.duplicateRecords++; return receipt; };
|
|
91
|
+
const wrapped = tools.map(tool => ({ ...tool, execute: (...args) => exclusive(file, async () => {
|
|
92
|
+
const [callId, params] = args;
|
|
93
|
+
const valid = Check(tool.parameters, params);
|
|
94
|
+
if (!valid) {
|
|
95
|
+
const errors = [...Errors(tool.parameters, params)];
|
|
96
|
+
return failure("TOOL_SCHEMA_INVALID", `${tool.name}: ${errors.slice(0, 8).map(e => `${e.instancePath || "/"}: ${e.message}`).join("; ")}${errors.length > 8 ? `; ${errors.length - 8} more fields` : ""}`);
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await restore();
|
|
100
|
+
const paramsSha256 = hash(params);
|
|
101
|
+
const singleton = new Set(["record_route_selection", "record_state_registry", "record_mock_api", "record_data_flow", "record_dependency", "record_design_deviation", "record_required_deliverables", "record_target_surface", "finalize_contract", "finalize_plan"]);
|
|
102
|
+
const naturalId = params?.id ?? params?.scopeId ?? params?.entry?.id ?? params?.requirementId ?? params?.choice?.purpose ?? (tool.name === "record_openspec_selection" ? params?.path : undefined) ?? (params?.endpoint ? `${params.endpoint.method}:${params.endpoint.path}` : undefined) ?? (singleton.has(tool.name) ? "singleton" : undefined);
|
|
103
|
+
const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
|
|
104
|
+
const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
|
|
105
|
+
const previousCall = submissions.find(s => s.callId === callId);
|
|
106
|
+
if (previousCall)
|
|
107
|
+
return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
|
|
108
|
+
const previousFact = submissions.filter(s => s.identity === identity).at(-1);
|
|
109
|
+
if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
|
|
110
|
+
return replay(previousFact.receipt);
|
|
111
|
+
if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
|
|
112
|
+
return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
|
|
113
|
+
if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
|
|
114
|
+
return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
|
|
115
|
+
const draft = structuredClone(input.store);
|
|
116
|
+
input.setWorkingStore(draft);
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
// Protocol controls must not be spread into domain facts by permissive callers.
|
|
120
|
+
const original = input.tools.find(t => t.name === tool.name);
|
|
121
|
+
const domainParams = { ...params };
|
|
122
|
+
if (!original.parameters.properties?.replace)
|
|
123
|
+
delete domainParams.replace;
|
|
124
|
+
result = await tool.execute(args[0], domainParams, ...args.slice(2));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
input.setWorkingStore(input.store);
|
|
128
|
+
}
|
|
129
|
+
if (result.details?.ok !== true)
|
|
130
|
+
return result;
|
|
131
|
+
const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
|
|
132
|
+
if (!newRecords.length)
|
|
133
|
+
return replay(result);
|
|
134
|
+
for (const record of draft.records) {
|
|
135
|
+
record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
|
|
136
|
+
}
|
|
137
|
+
for (const record of newRecords)
|
|
138
|
+
record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
|
|
139
|
+
newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
|
|
140
|
+
const committed = draft.records.filter(r => r.phase === "committed");
|
|
141
|
+
await validate(committed);
|
|
142
|
+
await persist(file, committed);
|
|
143
|
+
publish(draft);
|
|
144
|
+
progress.durableSubmissions++;
|
|
145
|
+
if (previousFact)
|
|
146
|
+
progress.durableReplacements++;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
input.setWorkingStore(input.store);
|
|
151
|
+
return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
|
|
152
|
+
}
|
|
153
|
+
}).then(result => { if (result.details?.ok !== true)
|
|
154
|
+
progress.failedRecords++; return result; }) }));
|
|
155
|
+
for (const tool of wrapped)
|
|
156
|
+
progressByTool.set(tool, progress);
|
|
157
|
+
return {
|
|
158
|
+
customTools: wrapped,
|
|
159
|
+
commitExternal: (work) => exclusive(file, async () => {
|
|
160
|
+
await restore();
|
|
161
|
+
const draft = structuredClone(input.store);
|
|
162
|
+
input.setWorkingStore(draft);
|
|
163
|
+
try {
|
|
164
|
+
await work();
|
|
165
|
+
const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
|
|
166
|
+
for (const record of draft.records) {
|
|
167
|
+
record.submission ??= {
|
|
168
|
+
version: 1,
|
|
169
|
+
bindingSha256,
|
|
170
|
+
tool: "external-adoption",
|
|
171
|
+
callId: record.requestId,
|
|
172
|
+
identity: record.eventId,
|
|
173
|
+
paramsSha256: record.payloadSha256,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const committed = draft.records.filter(r => r.phase === "committed");
|
|
177
|
+
await validate(committed);
|
|
178
|
+
await persist(file, committed);
|
|
179
|
+
publish(draft);
|
|
180
|
+
progress.durableSubmissions += newRecords.length;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
input.setWorkingStore(input.store);
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
flush: () => exclusive(file, async () => {
|
|
187
|
+
await restore();
|
|
188
|
+
const committed = input.store.records.filter(r => r.phase === "committed");
|
|
189
|
+
await validate(committed);
|
|
190
|
+
await persist(file, committed);
|
|
191
|
+
}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Model-authored execution ownership; never replaces a source obligation. */
|
|
3
|
+
export const frontendExecutionSchema = z.object({
|
|
4
|
+
groupId: z.string().trim().min(1),
|
|
5
|
+
kind: z.enum(["behavior", "constraint", "exclusion"]),
|
|
6
|
+
summary: z.string().trim().min(1),
|
|
7
|
+
}).strict();
|
|
8
|
+
/** Latest canonical member wins; joining a group requires consistent metadata.
|
|
9
|
+
* No lexical/semantic similarity heuristic can erase a source ID or condition. */
|
|
10
|
+
export function collectFrontendExecutionGroups(units) {
|
|
11
|
+
const groups = new Map();
|
|
12
|
+
for (const unit of new Map(units.map(unit => [unit.id, unit])).values()) {
|
|
13
|
+
const execution = unit.execution === undefined ? undefined : frontendExecutionSchema.parse(unit.execution);
|
|
14
|
+
const key = execution ? `group:${execution.groupId}` : `requirement:${unit.id}`;
|
|
15
|
+
const previous = groups.get(key);
|
|
16
|
+
if (previous && execution && (previous.kind !== execution.kind || previous.summary !== execution.summary))
|
|
17
|
+
throw Error(`EXECUTION_GROUP_CONFLICT: ${execution.groupId}; preserve differing permissions, thresholds and error paths as separate obligations/groups`);
|
|
18
|
+
if (previous)
|
|
19
|
+
previous.requirementIds.push(unit.id);
|
|
20
|
+
else
|
|
21
|
+
groups.set(key, { id: execution?.groupId ?? unit.id, kind: execution?.kind ?? "unclassified", summary: execution?.summary ?? `Independent requirement ${unit.id}`, requirementIds: [unit.id] });
|
|
22
|
+
}
|
|
23
|
+
return [...groups.values()];
|
|
24
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -711,6 +712,7 @@ export const frontendImplementationContractSchema = z
|
|
|
711
712
|
.object({
|
|
712
713
|
id,
|
|
713
714
|
expectedOutcome: z.string().min(1),
|
|
715
|
+
execution: frontendExecutionSchema.optional(),
|
|
714
716
|
implementationTargets: z.array(safePath),
|
|
715
717
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
716
718
|
evidenceGap: gap.optional(),
|
|
@@ -806,6 +808,12 @@ export const frontendImplementationContractSchema = z
|
|
|
806
808
|
})
|
|
807
809
|
.strict()
|
|
808
810
|
.superRefine((value, ctx) => {
|
|
811
|
+
try {
|
|
812
|
+
collectFrontendExecutionGroups(value.requirements);
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
ctx.addIssue({ code: "custom", path: ["requirements"], message: String(error) });
|
|
816
|
+
}
|
|
809
817
|
const stableIdentities = [
|
|
810
818
|
{
|
|
811
819
|
path: "requirements",
|
|
@@ -2063,6 +2071,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
2063
2071
|
? { sourceFragmentIds: req.sourceFragmentIds }
|
|
2064
2072
|
: {}),
|
|
2065
2073
|
...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
|
|
2074
|
+
...(req.execution !== undefined ? { execution: req.execution } : {}),
|
|
2066
2075
|
};
|
|
2067
2076
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
2068
2077
|
if (gapText) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
2
|
+
/** An execution packing target, not a claim about any model's context limit. */
|
|
3
|
+
export const FRONTEND_SCOPE_TARGET_BYTES = 24_000;
|
|
4
|
+
export function packFrontendInputUnits(units, options = {}) {
|
|
5
|
+
const target = options.targetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
6
|
+
const maxUnits = options.maxUnits ?? 4;
|
|
7
|
+
if (!Number.isFinite(target) || target <= 0 || !Number.isInteger(maxUnits) || maxUnits < 1)
|
|
8
|
+
throw Error("FRONTEND_INPUT_POLICY_INVALID");
|
|
9
|
+
const batches = [];
|
|
10
|
+
let batch = [];
|
|
11
|
+
let bytes = 2;
|
|
12
|
+
let cost = 0;
|
|
13
|
+
const ids = new Set();
|
|
14
|
+
for (const unit of units) {
|
|
15
|
+
if (!unit.id || ids.has(unit.id))
|
|
16
|
+
throw Error(`FRONTEND_INPUT_ID_CONFLICT: ${unit.id}`);
|
|
17
|
+
ids.add(unit.id);
|
|
18
|
+
const size = Buffer.byteLength(JSON.stringify(unit), "utf8") + 1;
|
|
19
|
+
if (options.maxUnitBytes !== undefined && size > options.maxUnitBytes)
|
|
20
|
+
throw Error(`FRONTEND_INPUT_UNIT_TOO_LARGE: ${unit.id} requires ${size} bytes; split the source obligation without dropping conditions`);
|
|
21
|
+
const unitCost = options.cost?.(unit) ?? 1;
|
|
22
|
+
if (batch.length && (batch.length >= maxUnits || bytes + size > target || cost + unitCost > (options.maxCost ?? Infinity))) {
|
|
23
|
+
batches.push(batch);
|
|
24
|
+
batch = [];
|
|
25
|
+
bytes = 2;
|
|
26
|
+
cost = 0;
|
|
27
|
+
}
|
|
28
|
+
batch.push(unit);
|
|
29
|
+
bytes += size;
|
|
30
|
+
cost += unitCost;
|
|
31
|
+
}
|
|
32
|
+
if (batch.length)
|
|
33
|
+
batches.push(batch);
|
|
34
|
+
return batches;
|
|
35
|
+
}
|
|
36
|
+
/** Select whole obligations before serialization; never clip identities or text. */
|
|
37
|
+
export function projectFrontendInputScope(payload, scopeIds) {
|
|
38
|
+
const byId = new Map(payload.requirements.map(r => [r.id, r]));
|
|
39
|
+
const requirements = [...new Set(scopeIds)].map(id => {
|
|
40
|
+
const unit = byId.get(id);
|
|
41
|
+
if (!unit)
|
|
42
|
+
throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${id}`);
|
|
43
|
+
return unit;
|
|
44
|
+
});
|
|
45
|
+
const fragments = new Set(requirements.flatMap(r => Array.isArray(r.sourceFragmentIds) ? r.sourceFragmentIds : []));
|
|
46
|
+
const inScope = (fact) => {
|
|
47
|
+
const ids = typeof fact.requirementId === "string" ? [fact.requirementId] : Array.isArray(fact.requirementIds) ? fact.requirementIds : [];
|
|
48
|
+
return !ids.length || ids.some(id => scopeIds.includes(String(id)));
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
...payload, requirements,
|
|
52
|
+
...(Array.isArray(payload.sharedFacts) ? { sharedFacts: payload.sharedFacts.filter(f => f && typeof f === "object" && inScope(f)).map(f => f.kind === "required-deliverables" && Array.isArray(f.items) ? { ...f, items: f.items.filter((item) => inScope(item)) } : f) } : {}),
|
|
53
|
+
...(Array.isArray(payload.fragments) ? { fragments: payload.fragments.filter(f => f && typeof f === "object" && fragments.has(f.id)) } : {}),
|
|
54
|
+
inputManifest: { schemaVersion: 1, semantics: "full", sourceSha256: sha256OfCanonicalJson(payload), scopeIds: requirements.map(r => r.id), excludedIds: payload.requirements.filter(r => !scopeIds.includes(r.id)).map(r => r.id) },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function parseFrontendInputBlock(prompt, phase) {
|
|
58
|
+
const block = new RegExp(`<frontend_${phase}_input>[\\s\\S]*?<\\/frontend_${phase}_input>`).exec(prompt)?.[0];
|
|
59
|
+
if (!block)
|
|
60
|
+
return undefined;
|
|
61
|
+
for (const line of block.split(/\r?\n/)) {
|
|
62
|
+
try {
|
|
63
|
+
const payload = JSON.parse(line);
|
|
64
|
+
if (Array.isArray(payload.requirements))
|
|
65
|
+
return { block, payload };
|
|
66
|
+
}
|
|
67
|
+
catch { /* wrapper prose is not JSON */ }
|
|
68
|
+
}
|
|
69
|
+
throw Error(`FRONTEND_INPUT_INVALID: ${phase} compiled inventory is not parseable`);
|
|
70
|
+
}
|
|
71
|
+
export function projectFrontendContractPrompt(prompt, scopeIds) {
|
|
72
|
+
const input = parseFrontendInputBlock(prompt, "contract");
|
|
73
|
+
if (!input)
|
|
74
|
+
throw Error("FRONTEND_INPUT_MISSING: contract compiled inventory unavailable");
|
|
75
|
+
return prompt.replace(input.block, ["<frontend_contract_input>", "Complete canonical obligations for this session only. Confirm each id with record_requirement; preserve all independent conditions.", JSON.stringify(projectFrontendInputScope(input.payload, scopeIds)), "</frontend_contract_input>"].join("\n"));
|
|
76
|
+
}
|