@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11
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 +144 -9
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1237 -586
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +8 -4
- package/dist/shared/frontend-execution-policy.js +23 -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 +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +113 -0
- package/dist/worker/console/chat/browser-routes.js +140 -0
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +207 -1178
- package/dist/worker/console/chat/routes.js +208 -71
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +49 -0
- package/dist/worker/console/chat/session-mode.js +219 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +348 -0
- package/dist/worker/console/chat/terminal-tools.js +211 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
- package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
- package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
- package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- 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 +263 -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 +742 -268
- 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 +49 -11
- 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-capacity.js +9 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- 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 +13 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -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-context.js +2 -1
- package/dist/workflows/dag/frontend-review-scopes.js +139 -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-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +30 -1
- 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/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +65 -167
- package/dist/workflows/dag/node-execution.js +38 -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 +14 -13
- package/package.json +6 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +22 -4
- package/skills/frontend-design-review/SKILL.md +22 -16
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +21 -2
- package/skills/frontend-plan/references/decision-contract.md +42 -23
- package/skills/frontend-review/SKILL.md +22 -29
- package/skills/frontend-review/references/review-findings.md +24 -43
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
|
@@ -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,9 @@
|
|
|
1
|
+
/** Shared Contract/Plan bound. Permissions and directory globs are not files. */
|
|
2
|
+
export const FRONTEND_MAX_IMPLEMENTATION_FILES = 15;
|
|
3
|
+
export function frontendConcreteDeliveryFiles(files) {
|
|
4
|
+
if (!Array.isArray(files))
|
|
5
|
+
return [];
|
|
6
|
+
return [...new Set(files.filter((file) => typeof file === "string")
|
|
7
|
+
.map(file => file.trim().replace(/\\/g, "/").replace(/^(\.\/)+/, ""))
|
|
8
|
+
.filter(file => file.length > 0 && !/[*?{}]/.test(file) && !file.endsWith("/")))];
|
|
9
|
+
}
|
|
@@ -114,7 +114,7 @@ export function buildFrontendTestHandoff(input) {
|
|
|
114
114
|
* required verification evidence. Anything else downgrades to pending.
|
|
115
115
|
*/
|
|
116
116
|
export function deriveFrontendIntegrationFacts(input) {
|
|
117
|
-
const requiredEvidenceSatisfied = input.verificationTargets.
|
|
117
|
+
const requiredEvidenceSatisfied = input.verificationTargets.some((target) => target.mode === "behavior") &&
|
|
118
118
|
input.verificationTargets.every((target) => target.status === "ok");
|
|
119
119
|
if (input.realApiEvidence.present &&
|
|
120
120
|
input.realApiEvidence.passed &&
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { deterministicSha256, } from "./frontend-implementation-contract.js";
|
|
2
2
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
3
|
+
import { FRONTEND_MAX_IMPLEMENTATION_FILES } from "./frontend-capacity.js";
|
|
3
4
|
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
4
5
|
const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
5
6
|
/** Max concrete implementation files a single writeSet may carry before the
|
|
6
7
|
* plan is asked to split. Kept at 15: beyond this, a single implement node's
|
|
7
8
|
* read/edit loop inflates provider context toward overflow. */
|
|
8
|
-
const MAX_WRITE_SET_FILES =
|
|
9
|
+
const MAX_WRITE_SET_FILES = FRONTEND_MAX_IMPLEMENTATION_FILES;
|
|
9
10
|
function normalizeRelativePath(value) {
|
|
10
11
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
11
12
|
}
|
|
@@ -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) {
|
|
@@ -2500,6 +2509,10 @@ function splitFrontendContractReviewGroup(canonical, fields) {
|
|
|
2500
2509
|
if (!Object.hasOwn(canonical, field))
|
|
2501
2510
|
continue;
|
|
2502
2511
|
const fieldValue = canonical[field];
|
|
2512
|
+
// JSON omits undefined object properties; the index must describe the
|
|
2513
|
+
// persisted contract, not phantom keys retained by normalization.
|
|
2514
|
+
if (fieldValue === undefined)
|
|
2515
|
+
continue;
|
|
2503
2516
|
if (!Array.isArray(fieldValue)) {
|
|
2504
2517
|
const candidate = { ...data, [field]: fieldValue };
|
|
2505
2518
|
if (Object.keys(data).length > 0 &&
|