@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.10
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 +129 -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 +1214 -584
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +5 -2
- 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 +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -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 +78 -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 +213 -1181
- package/dist/worker/console/chat/routes.js +212 -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 +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -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-D_fNbWc_.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_HuqLW6m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-Djy2qY1m.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-bB-1TSyY.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-C5Hmghrz.js} +1 -1
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-Duyl39oy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-x6mdnSyf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-WlBuFj7W.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CTF429HX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-Dwd6r-SF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-C-Br82D1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CuGp0DeE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-CJhFX6Lh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-DpGOa_jT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-DXJaTynd.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-CRBZWjE4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-BdXcxtlb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-07UKsOts.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-D38p9__S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-nSiBNNjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BFOTot5D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-DypKaZ6k.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-B6yoOQeW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-BHYS6viX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-D2VD5Snj.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-E98mWxN1.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-CT1zxhqP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-6GcngNcy.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-DCL6NvKe.js} +1 -1
- package/dist/worker/console/static/assets/index-24jp3aBh.js +468 -0
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-C0627e-9.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DPDPv9HL.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-D3OHdWiO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DI88zD7n.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-p2l8IrWY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-B1bAX-oS.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-DwOnWxdD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-xRkTXnJC.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-_9UC5Zio.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-CgEUirWJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-Czbpwiyz.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-_j7_KiqY.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-BNJa6Hyi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-DedpKtOc.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BBvthr7c.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-Ds7umgEM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-CKa-p1nF.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-LoUNR56o.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-Bqbl6ym7.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-B8dUg4lf.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-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-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 +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/init-hybrid.js +16 -10
- 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 +5 -5
- package/package.json +6 -3
- 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 +9 -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
- 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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFrontendToolProgress } from "./frontend-durable-tools.js";
|
|
5
|
+
import { frontendModelCapabilities } from "../../shared/frontend-execution-policy.js";
|
|
6
|
+
/** Count returned text only after a successful native read. No filesystem reads or raw content retention. */
|
|
7
|
+
export function createFrontendReadObservation() {
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
const finished = new Set();
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
let bytes = 0, duplicates = 0, complete = 0, rejected = 0, unknown = false;
|
|
12
|
+
return {
|
|
13
|
+
observe(raw) {
|
|
14
|
+
const event = record(raw);
|
|
15
|
+
const id = event.toolCallId;
|
|
16
|
+
if (typeof id !== "string")
|
|
17
|
+
return;
|
|
18
|
+
if (event.type === "tool_execution_start" && event.toolName === "read")
|
|
19
|
+
pending.set(id, record(event.args ?? event.input));
|
|
20
|
+
if (event.type !== "tool_execution_end" || !pending.has(id) || finished.has(id))
|
|
21
|
+
return;
|
|
22
|
+
const args = pending.get(id);
|
|
23
|
+
pending.delete(id);
|
|
24
|
+
finished.add(id);
|
|
25
|
+
const result = record(event.result);
|
|
26
|
+
if (event.isError === true || result.isError === true || result.ok === false || result.error || record(result.details).ok === false) {
|
|
27
|
+
rejected++;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const content = result.content;
|
|
31
|
+
if (!Array.isArray(content) || content.some(item => record(item).type !== "text" || typeof record(item).text !== "string") || typeof args.path !== "string") {
|
|
32
|
+
unknown = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const text = content.map(item => record(item).text).join("\n");
|
|
36
|
+
const size = Buffer.byteLength(text);
|
|
37
|
+
const key = createHash("sha256").update(JSON.stringify([args.path.replaceAll("\\", "/").replace(/^\.\//, ""), args.offset ?? null, args.limit ?? null, text])).digest("hex");
|
|
38
|
+
bytes += size;
|
|
39
|
+
complete++;
|
|
40
|
+
if (seen.has(key))
|
|
41
|
+
duplicates += size;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
},
|
|
44
|
+
snapshot() { return { successfulReadBytes: unknown ? null : bytes, duplicateReadBytes: unknown ? null : duplicates, completedReads: complete, rejectedReads: rejected, incompleteReads: pending.size }; },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function classifyFrontendSessionFailure(result) {
|
|
48
|
+
const errorCode = /\b(?:FRONTEND_[A-Z_]+|TOOL_SCHEMA_INVALID|FACT_IDENTITY_CONFLICT|REQUEST_ID_REUSE_CONFLICT|TERMINAL_CONFLICT|[A-Z_]+_INCOMPLETE|[A-Z_]+_EXHAUSTED)\b/.exec(result.stderr)?.[0] ?? null;
|
|
49
|
+
const category = result.failureCategory;
|
|
50
|
+
const failureLayer = result.stopReason === "length" || /context|budget-exhausted/.test(category) ? "capacity"
|
|
51
|
+
: result.ok ? "none"
|
|
52
|
+
: errorCode?.includes("LEDGER") || errorCode === "FRONTEND_COMMIT_FAILED" ? "persistence"
|
|
53
|
+
: errorCode === "TOOL_SCHEMA_INVALID" ? "schema"
|
|
54
|
+
: /binding|source|permission|write-guard|tool-policy/.test(category) ? "governance"
|
|
55
|
+
: /timeout|rate-limit|provider|transport|network|auth/.test(category) ? "provider"
|
|
56
|
+
: /invalid-output|structured-output/.test(category) ? "protocol-or-coverage"
|
|
57
|
+
: "unknown";
|
|
58
|
+
return { failureLayer, errorCode };
|
|
59
|
+
}
|
|
60
|
+
const fields = ["inputTokens", "outputTokens", "reasoningTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens", "peakRequestContextTokens"];
|
|
61
|
+
const record = (v) => v !== null && typeof v === "object" ? v : {};
|
|
62
|
+
const number = (r, ...keys) => {
|
|
63
|
+
for (const key of keys)
|
|
64
|
+
if (typeof r[key] === "number" && Number.isFinite(r[key]) && r[key] >= 0)
|
|
65
|
+
return r[key];
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
const sumKnown = (values) => values.length && values.every(v => v !== null) ? values.reduce((s, v) => s + v, 0) : null;
|
|
69
|
+
/** Observation only. Never feed this projection into the SDK execution guard. */
|
|
70
|
+
export function collectFrontendUsage(events) {
|
|
71
|
+
const samples = new Map();
|
|
72
|
+
const aliases = new Map();
|
|
73
|
+
let anonymous = false;
|
|
74
|
+
let observed = false;
|
|
75
|
+
for (const raw of events) {
|
|
76
|
+
const event = record(raw);
|
|
77
|
+
const message = record(event.message);
|
|
78
|
+
if (typeof message.role === "string" && message.role !== "assistant")
|
|
79
|
+
continue;
|
|
80
|
+
const usage = record(event.usage ?? message.usage ?? event.tokenUsage);
|
|
81
|
+
if (!Object.keys(usage).length)
|
|
82
|
+
continue;
|
|
83
|
+
observed = true;
|
|
84
|
+
const ids = [event.responseId, event.messageId, message.responseId, message.id].filter((v) => typeof v === "string" && v.length > 0);
|
|
85
|
+
const roots = [...new Set(ids.map(id => aliases.get(id) ?? id))];
|
|
86
|
+
const key = roots[0];
|
|
87
|
+
if (!key) {
|
|
88
|
+
anonymous = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const input = number(usage, "input", "input_tokens", "inputTokens", "prompt_tokens", "promptTokens");
|
|
92
|
+
const output = number(usage, "output", "output_tokens", "outputTokens", "completion_tokens", "completionTokens");
|
|
93
|
+
const cacheRead = number(usage, "cacheRead", "cache_read_input_tokens");
|
|
94
|
+
const cacheWrite = number(usage, "cacheWrite", "cache_creation_input_tokens");
|
|
95
|
+
// Pi's native input excludes cache tokens; OpenAI-compatible input includes them.
|
|
96
|
+
const context = input === null ? null : "input" in usage ? sumKnown([input, cacheRead, cacheWrite]) : input;
|
|
97
|
+
const sample = {
|
|
98
|
+
inputTokens: input, outputTokens: output,
|
|
99
|
+
reasoningTokens: number(usage, "reasoning", "reasoning_tokens", "reasoningTokens") ?? number(record(usage.completion_tokens_details ?? usage.output_tokens_details), "reasoning_tokens"),
|
|
100
|
+
cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite,
|
|
101
|
+
totalTokens: number(usage, "total_tokens", "totalTokens") ?? sumKnown([context, output]),
|
|
102
|
+
peakRequestContextTokens: context,
|
|
103
|
+
};
|
|
104
|
+
for (const root of roots) {
|
|
105
|
+
const previous = samples.get(root);
|
|
106
|
+
if (previous)
|
|
107
|
+
for (const field of fields)
|
|
108
|
+
sample[field] = previous[field] === null ? sample[field] : sample[field] === null ? previous[field] : Math.max(previous[field], sample[field]);
|
|
109
|
+
samples.delete(root);
|
|
110
|
+
}
|
|
111
|
+
for (const [alias, root] of aliases)
|
|
112
|
+
if (roots.includes(root))
|
|
113
|
+
aliases.set(alias, key);
|
|
114
|
+
for (const id of ids)
|
|
115
|
+
aliases.set(id, key);
|
|
116
|
+
samples.set(key, sample);
|
|
117
|
+
}
|
|
118
|
+
const result = Object.fromEntries(fields.map(field => [field, anonymous ? null : field === "peakRequestContextTokens" ? (samples.size && [...samples.values()].every(s => s[field] !== null) ? Math.max(...[...samples.values()].map(s => s[field])) : null) : sumKnown([...samples.values()].map(s => s[field]))]));
|
|
119
|
+
return { ...result, requestCount: anonymous || !observed ? null : samples.size,
|
|
120
|
+
completeness: !observed ? "unknown" : !anonymous && result.inputTokens !== null && result.outputTokens !== null && result.totalTokens !== null ? "complete" : "partial" };
|
|
121
|
+
}
|
|
122
|
+
function aggregateAttempts(attempts) {
|
|
123
|
+
const usages = attempts.map(a => a.actual ?? collectFrontendUsage([]));
|
|
124
|
+
const totals = Object.fromEntries(fields.map(field => [field, field === "peakRequestContextTokens"
|
|
125
|
+
? usages.every(u => u[field] !== null) ? Math.max(...usages.map(u => u[field])) : null
|
|
126
|
+
: sumKnown(usages.map(u => u[field]))]));
|
|
127
|
+
return { ...totals, requestCount: sumKnown(usages.map(u => u.requestCount)), completeness: usages.every(u => u.completeness === "complete") ? "complete" : usages.every(u => u.completeness === "unknown") ? "unknown" : "partial" };
|
|
128
|
+
}
|
|
129
|
+
/** Captures every transport attempt, including an SDK failure before CLI fallback. */
|
|
130
|
+
export async function observeFrontendAttempt(input, execute) {
|
|
131
|
+
const event = { id: randomUUID(), backend: input.backend, model: input.model, startedAt: new Date().toISOString(), completion: "incomplete" };
|
|
132
|
+
const notify = () => { try {
|
|
133
|
+
input.observer?.(structuredClone(event));
|
|
134
|
+
}
|
|
135
|
+
catch { /* observation cannot change execution */ } };
|
|
136
|
+
notify();
|
|
137
|
+
const result = await execute();
|
|
138
|
+
event.completion = "complete";
|
|
139
|
+
event.actual = result.usageObservation ?? collectFrontendUsage([]);
|
|
140
|
+
event.reads = result.readObservation;
|
|
141
|
+
event.capabilities = result.capabilitiesObservation;
|
|
142
|
+
event.requestEnvelope = result.requestEnvelopeObservation;
|
|
143
|
+
event.terminal = { ok: result.ok, failureCategory: result.failureCategory, stopReason: result.stopReason ?? null, durationMs: result.durationMs };
|
|
144
|
+
notify();
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
const measure = (text) => ({ sha256: createHash("sha256").update(text).digest("hex"), chars: text.length, bytes: Buffer.byteLength(text), estimatedTokens: Math.ceil(Buffer.byteLength(text) / 3) });
|
|
148
|
+
function safeObservation(read, fallback) { try {
|
|
149
|
+
return read();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return fallback;
|
|
153
|
+
} }
|
|
154
|
+
/** One logical dispatch and its real transport attempts; no raw prompt or tool output is stored. */
|
|
155
|
+
export async function observeFrontendSession(input, execute) {
|
|
156
|
+
const durableBefore = safeObservation(() => input.durableCommittedCount?.(), undefined);
|
|
157
|
+
const before = safeObservation(() => input.committedCount?.(), undefined);
|
|
158
|
+
const toolsBefore = readFrontendToolProgress(input.customTools);
|
|
159
|
+
const receipt = {
|
|
160
|
+
schemaVersion: 1, policyVersion: "frontend-session-observation-v1",
|
|
161
|
+
identity: { sessionId: randomUUID(), phase: input.phase, scopeIds: input.scopeIds ?? [], taskId: input.taskId ?? null, runId: input.runId ?? null, nodeId: input.nodeId ?? null, attempt: input.attempt ?? null, sourceDigest: input.sourceDigest ?? null, contractDigest: input.contractDigest ?? null },
|
|
162
|
+
startedAt: new Date().toISOString(), finishedAt: null, completion: "incomplete",
|
|
163
|
+
planned: { prompt: measure(input.prompt), userMessage: measure(input.userMessage ?? ""), toolSchemas: input.customTools ? safeObservation(() => measure(JSON.stringify(input.customTools.map(t => { const tool = record(t); return { name: tool.name, description: tool.description, parameters: tool.parameters }; }))), null) : null,
|
|
164
|
+
estimatorVersion: "utf8-bytes-div3-v1-estimate", capabilities: { ...frontendModelCapabilities(undefined), model: input.model ?? null }, unmeasuredComponents: ["sdk-system-overhead", "built-in-tools", "history", "tool-results", "attachments"] },
|
|
165
|
+
actual: collectFrontendUsage([]), attempts: [],
|
|
166
|
+
progress: { memoryCommittedDelta: null, durableCommittedDelta: null, failedRecords: null, duplicateRecords: null, durableSubmissions: null, durableReplacements: null, successfulReadBytes: null, duplicateReadBytes: null }, terminal: null,
|
|
167
|
+
};
|
|
168
|
+
let writes = Promise.resolve();
|
|
169
|
+
let abandoned = false;
|
|
170
|
+
const writeObservation = async (file, snapshot) => {
|
|
171
|
+
// Never recreate a run that the runner has already archived.
|
|
172
|
+
await mkdir(path.dirname(file)).catch(error => { if (error.code !== "EEXIST")
|
|
173
|
+
throw error; });
|
|
174
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
175
|
+
try {
|
|
176
|
+
await writeFile(temporary, JSON.stringify(snapshot), { flag: "wx" });
|
|
177
|
+
await rename(temporary, file);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const save = () => {
|
|
184
|
+
const snapshot = structuredClone(receipt);
|
|
185
|
+
writes = writes.then(async () => {
|
|
186
|
+
if (abandoned)
|
|
187
|
+
return;
|
|
188
|
+
if (input.sink)
|
|
189
|
+
await input.sink(snapshot);
|
|
190
|
+
else if (input.artifactPath)
|
|
191
|
+
await writeObservation(input.artifactPath, snapshot);
|
|
192
|
+
}).catch(() => { });
|
|
193
|
+
return writes;
|
|
194
|
+
};
|
|
195
|
+
const waitForWrite = async (pending) => {
|
|
196
|
+
let timer;
|
|
197
|
+
try {
|
|
198
|
+
await Promise.race([pending, new Promise(resolve => { timer = setTimeout(() => { abandoned = true; resolve(); }, 100); })]);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
if (timer)
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
await waitForWrite(save());
|
|
206
|
+
const result = await execute(event => {
|
|
207
|
+
const index = receipt.attempts.findIndex(a => a.id === event.id);
|
|
208
|
+
if (index < 0)
|
|
209
|
+
receipt.attempts.push(event);
|
|
210
|
+
else
|
|
211
|
+
receipt.attempts[index] = event;
|
|
212
|
+
void save();
|
|
213
|
+
});
|
|
214
|
+
receipt.finishedAt = new Date().toISOString();
|
|
215
|
+
receipt.completion = "complete";
|
|
216
|
+
receipt.actual = receipt.attempts.length ? aggregateAttempts(receipt.attempts) : result.usageObservation ?? collectFrontendUsage([]);
|
|
217
|
+
receipt.planned.capabilities = receipt.attempts[0]?.capabilities ?? result.capabilitiesObservation ?? receipt.planned.capabilities;
|
|
218
|
+
receipt.progress.memoryCommittedDelta = before !== undefined && input.committedCount ? safeObservation(() => Math.max(0, input.committedCount() - before), null) : null;
|
|
219
|
+
const durableAfter = safeObservation(() => input.durableCommittedCount?.(), undefined);
|
|
220
|
+
receipt.progress.durableCommittedDelta = durableBefore !== undefined && durableAfter !== undefined ? Math.max(0, durableAfter - durableBefore) : null;
|
|
221
|
+
const toolsAfter = readFrontendToolProgress(input.customTools);
|
|
222
|
+
if (toolsBefore && toolsAfter)
|
|
223
|
+
for (const key of ["failedRecords", "duplicateRecords", "durableSubmissions", "durableReplacements"])
|
|
224
|
+
receipt.progress[key] = Math.max(0, toolsAfter[key] - toolsBefore[key]);
|
|
225
|
+
const reads = receipt.attempts.length ? receipt.attempts.map(a => a.reads) : [result.readObservation];
|
|
226
|
+
for (const key of ["successfulReadBytes", "duplicateReadBytes"])
|
|
227
|
+
receipt.progress[key] = sumKnown(reads.map(r => r?.[key] ?? null));
|
|
228
|
+
receipt.terminal = { ok: result.ok, failureCategory: result.failureCategory, stopReason: result.stopReason ?? null, durationMs: Date.parse(receipt.finishedAt) - Date.parse(receipt.startedAt), ...classifyFrontendSessionFailure(result) };
|
|
229
|
+
await waitForWrite(save());
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
export function summarizeFrontendSessions(receipts) {
|
|
233
|
+
const byId = new Map();
|
|
234
|
+
for (const receipt of receipts) {
|
|
235
|
+
const previous = byId.get(receipt.identity.sessionId);
|
|
236
|
+
const rank = (r) => [r.completion === "complete" ? 1 : 0, Date.parse(r.finishedAt ?? r.startedAt), r.attempts.filter(a => a.completion === "complete").length, r.attempts.length];
|
|
237
|
+
const candidateRank = rank(receipt);
|
|
238
|
+
const previousRank = previous ? rank(previous) : [];
|
|
239
|
+
const difference = candidateRank.map((v, i) => v - (previousRank[i] ?? 0)).find(v => v !== 0) ?? 0;
|
|
240
|
+
if (!previous || difference > 0)
|
|
241
|
+
byId.set(receipt.identity.sessionId, receipt);
|
|
242
|
+
}
|
|
243
|
+
const unique = [...byId.values()];
|
|
244
|
+
return { sessions: unique.length, failedSessions: unique.filter(r => r.terminal?.ok === false).length, incompleteSessions: unique.filter(r => r.completion === "incomplete").length,
|
|
245
|
+
failureLayers: Object.fromEntries([...new Set(unique.flatMap(r => r.terminal && r.terminal.failureLayer !== "none" ? [r.terminal.failureLayer] : []))].sort().map(layer => [layer, unique.filter(r => r.terminal?.failureLayer === layer).length])),
|
|
246
|
+
progress: Object.fromEntries(["durableCommittedDelta", "durableSubmissions", "durableReplacements", "failedRecords", "duplicateRecords", "successfulReadBytes", "duplicateReadBytes"].map(key => [key, sumKnown(unique.map(r => r.progress[key]))])),
|
|
247
|
+
totalTokens: sumKnown(unique.flatMap(r => r.attempts.length ? r.attempts.map(a => a.actual?.totalTokens ?? null) : [r.actual.totalTokens])),
|
|
248
|
+
sessionDurationSumMs: sumKnown(unique.map(r => r.terminal?.durationMs ?? null)) };
|
|
249
|
+
}
|
|
@@ -236,6 +236,7 @@ export const PLAN_LEDGER_FACT_KINDS = [
|
|
|
236
236
|
"plan-requirement",
|
|
237
237
|
"plan-verification-target",
|
|
238
238
|
"plan-evidence-gap",
|
|
239
|
+
"mock-endpoint",
|
|
239
240
|
];
|
|
240
241
|
function skeletonTargetFiles(skeleton) {
|
|
241
242
|
if (!isRecord(skeleton))
|
|
@@ -386,8 +387,25 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
386
387
|
patch.interactions = [...interactionByName.values(), ...unnamedInteractions];
|
|
387
388
|
}
|
|
388
389
|
const mockApi = lastByKind("mock-api");
|
|
390
|
+
if (!mockApi && facts.some(f => f.kind === "mock-endpoint" && f.removed !== true))
|
|
391
|
+
throw Error("MOCK_POLICY_MISSING: endpoint has no policy");
|
|
389
392
|
if (mockApi && isRecord(mockApi.mockApi)) {
|
|
390
|
-
|
|
393
|
+
const endpoints = new Map();
|
|
394
|
+
for (const endpoint of Array.isArray(mockApi.mockApi.endpoints) ? mockApi.mockApi.endpoints : []) {
|
|
395
|
+
if (isRecord(endpoint))
|
|
396
|
+
endpoints.set(`${endpoint.method}:${endpoint.path}`, endpoint);
|
|
397
|
+
}
|
|
398
|
+
for (const fact of facts)
|
|
399
|
+
if (fact.kind === "mock-endpoint" && isRecord(fact.endpoint)) {
|
|
400
|
+
const key = `${fact.endpoint.method}:${fact.endpoint.path}`;
|
|
401
|
+
if (fact.removed === true)
|
|
402
|
+
endpoints.delete(key);
|
|
403
|
+
else
|
|
404
|
+
endpoints.set(key, fact.endpoint);
|
|
405
|
+
}
|
|
406
|
+
if (mockApi.mockApi.strategy === "not-needed" && endpoints.size)
|
|
407
|
+
throw Error("MOCK_POLICY_CONFLICT: withdraw endpoints before declaring Mock/API not needed");
|
|
408
|
+
patch.mockApi = { ...mockApi.mockApi, endpoints: [...endpoints.values()] };
|
|
391
409
|
}
|
|
392
410
|
const deviation = lastByKind("design-deviation");
|
|
393
411
|
if (deviation) {
|
|
@@ -433,7 +451,7 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
|
|
|
433
451
|
const inherited = contractRequirements.get(id);
|
|
434
452
|
if (!inherited)
|
|
435
453
|
return entry;
|
|
436
|
-
const merged = { ...entry };
|
|
454
|
+
const merged = { ...entry, ...(inherited.text ? { expectedOutcome: inherited.text } : {}), ...(inherited.execution ? { execution: inherited.execution } : {}) };
|
|
437
455
|
// Inherit the contract-declared provenance only when the plan did
|
|
438
456
|
// not already declare it (model override wins; missing inherits).
|
|
439
457
|
if (!Array.isArray(entry.sourceFragmentIds)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
2
|
+
import { FRONTEND_MAX_IMPLEMENTATION_FILES, frontendConcreteDeliveryFiles } from "./frontend-capacity.js";
|
|
2
3
|
import { buildFrontendShapeTransitionCapsule, deriveFrontendRuntimeShapeFacts, evaluateFrontendShapeEscalation, parseFrontendShapeTransitionCapsule, } from "./frontend-shape.js";
|
|
3
4
|
import { createTypedEventStore, readCommittedEvents, } from "./frontend-typed-event-store.js";
|
|
4
5
|
import { adoptTypedEventFact, stageTypedEventFact, } from "./frontend-typed-event-transaction.js";
|
|
@@ -222,7 +223,13 @@ function extractRuntimeShapeEvidence(input) {
|
|
|
222
223
|
!sourceText.includes(path) &&
|
|
223
224
|
/(?:\.[A-Za-z0-9]+|\*\*)$/.test(path));
|
|
224
225
|
const routes = canonicalStrings(text.match(/\/(?:[A-Za-z0-9_:[\]-]+\/?)+/g) ?? []);
|
|
225
|
-
|
|
226
|
+
// The frozen delivery inventory is available before Plan. Do not spend
|
|
227
|
+
// coverage/UX sessions building a contract the same capacity gate will reject.
|
|
228
|
+
// allowedPaths and text mentions are permissions/evidence, never this count.
|
|
229
|
+
const skeleton = input.tasksById.get("frontend-plan-pi")?.structuredContractOutput?.skeleton;
|
|
230
|
+
const deliveryFiles = frontendConcreteDeliveryFiles(skeleton?.targets?.files);
|
|
231
|
+
const capacitySplit = deliveryFiles.length > FRONTEND_MAX_IMPLEMENTATION_FILES;
|
|
232
|
+
const splitRequired = capacitySplit || /["']splitRequired["']\s*:\s*true|\bsplit-required\s+shape\b/i.test(text);
|
|
226
233
|
return {
|
|
227
234
|
sourceNodeId: input.sourceNodeId,
|
|
228
235
|
sourceNodeDigest: computeFrontendShapeNodeDigest(node),
|
|
@@ -232,7 +239,10 @@ function extractRuntimeShapeEvidence(input) {
|
|
|
232
239
|
targetPaths,
|
|
233
240
|
routes,
|
|
234
241
|
splitRequired,
|
|
235
|
-
evidenceRefs: [
|
|
242
|
+
evidenceRefs: [
|
|
243
|
+
...(capacitySplit ? ["dag:frontend-plan-pi:structuredContractOutput.skeleton.targets.files"] : []),
|
|
244
|
+
`node:${input.sourceNodeId}:stdout`,
|
|
245
|
+
],
|
|
236
246
|
};
|
|
237
247
|
}
|
|
238
248
|
/** Commit a typed, structured runtime fact immediately after a canonical source
|
|
@@ -61,7 +61,7 @@ function pathSpreadScore(paths) {
|
|
|
61
61
|
function collectEscalationSignals(blob, supervised) {
|
|
62
62
|
const signals = [];
|
|
63
63
|
for (const pattern of SHAPE_ESCALATION_PATTERNS) {
|
|
64
|
-
if (
|
|
64
|
+
if (hasAffirmativeShapeSignal(blob, pattern))
|
|
65
65
|
signals.push(pattern.id);
|
|
66
66
|
}
|
|
67
67
|
if (supervised)
|
|
@@ -72,6 +72,35 @@ function collectEscalationSignals(blob, supervised) {
|
|
|
72
72
|
}
|
|
73
73
|
return [...new Set(signals)];
|
|
74
74
|
}
|
|
75
|
+
function isExplicitShapeExclusion(clause, pattern) {
|
|
76
|
+
const match = new RegExp(pattern.re.source, "i").exec(clause);
|
|
77
|
+
if (!match)
|
|
78
|
+
return false;
|
|
79
|
+
const prefix = clause.slice(0, match.index).trim();
|
|
80
|
+
const suffix = clause.slice(match.index + match[0].length).trim();
|
|
81
|
+
const actionInSignal = ["new-route", "new-dependency"].includes(pattern.id);
|
|
82
|
+
if (suffix && !(actionInSignal && /^no$/i.test(prefix) && /^is\s+(?:required|needed)$/i.test(suffix)))
|
|
83
|
+
return false;
|
|
84
|
+
if (/^(?:不涉及|不(?:修改|变更|调整|改变|新增|添加)|无需(?:修改|变更|调整|改变|新增|添加)|no\s+changes?\s+to|(?:do\s+not|don't)\s+(?:change|modify))$/i.test(prefix))
|
|
85
|
+
return true;
|
|
86
|
+
return actionInSignal && /^(?:不|禁止|无需|无须|不需要|no|without|do\s+not|don't)$/i.test(prefix);
|
|
87
|
+
}
|
|
88
|
+
/** Only a complete, independent scope exclusion can suppress a risk signal.
|
|
89
|
+
* Nested negation, conditions and trailing consequences remain risk evidence;
|
|
90
|
+
* an excluded occurrence never cancels another occurrence elsewhere. */
|
|
91
|
+
function hasAffirmativeShapeSignal(blob, pattern) {
|
|
92
|
+
for (const sentence of blob.split(/[\n。.;;!?!?]/)) {
|
|
93
|
+
if (!pattern.re.test(sentence))
|
|
94
|
+
continue;
|
|
95
|
+
// Commas can attach a condition to an exclusion. A comma-separated list
|
|
96
|
+
// is independent only when every item is itself an explicit exclusion.
|
|
97
|
+
const clauses = sentence.split(/[,,、]/).map(part => part.trim().replace(/^(?:[-*+]|\d+\))\s+/, ""));
|
|
98
|
+
if (clauses.every(clause => SHAPE_ESCALATION_PATTERNS.some(candidate => isExplicitShapeExclusion(clause, candidate))))
|
|
99
|
+
continue;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
75
104
|
/** Negation applies to the API mention itself, never to unrelated Mock policy. */
|
|
76
105
|
function hasFrontendRemoteSignal(blob) {
|
|
77
106
|
const remaining = blob
|
|
@@ -39,9 +39,10 @@ const resultSchema = z.object({
|
|
|
39
39
|
})),
|
|
40
40
|
})),
|
|
41
41
|
});
|
|
42
|
-
|
|
42
|
+
/** Resolve runner paths against its cwd, then use the contract workspace coordinates. */
|
|
43
|
+
export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd) {
|
|
43
44
|
return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
|
|
44
|
-
file: path.relative(
|
|
45
|
+
file: path.relative(workspaceRoot, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
|
|
45
46
|
title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
|
|
46
47
|
status: test.status,
|
|
47
48
|
})));
|
|
@@ -128,6 +128,7 @@ export const PLAN_RECORD_FACT_KINDS = [
|
|
|
128
128
|
"plan-evidence-gap",
|
|
129
129
|
"state-registry",
|
|
130
130
|
];
|
|
131
|
+
export const INCREMENTAL_DETAIL_FACT_KINDS = ["mock-endpoint", "review-finding", "design-finding", "contract-scope-completed", "plan-group-coverage", "scout-scope", "review-scope-completed", "design-scope-completed"];
|
|
131
132
|
/**
|
|
132
133
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
133
134
|
* requests. The five-value enum replaces free-form issueCategory strings.
|
|
@@ -159,6 +160,7 @@ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
|
|
|
159
160
|
...CONTRACT_FACT_KINDS,
|
|
160
161
|
...PLAN_TERMINAL_FACT_KINDS,
|
|
161
162
|
...PLAN_RECORD_FACT_KINDS,
|
|
163
|
+
...INCREMENTAL_DETAIL_FACT_KINDS,
|
|
162
164
|
]),
|
|
163
165
|
];
|
|
164
166
|
export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
|
|
@@ -261,6 +263,15 @@ export const typedEventRecordSchema = z
|
|
|
261
263
|
revision: z.number().int().nonnegative(),
|
|
262
264
|
recordedAt: z.string().min(1),
|
|
263
265
|
quarantineReason: z.string().min(1).optional(),
|
|
266
|
+
submission: z.object({
|
|
267
|
+
version: z.literal(1),
|
|
268
|
+
bindingSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
269
|
+
tool: z.string(),
|
|
270
|
+
callId: z.string(),
|
|
271
|
+
identity: z.string(),
|
|
272
|
+
paramsSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
273
|
+
receipt: z.record(z.unknown()).optional(),
|
|
274
|
+
}).strict().optional(),
|
|
264
275
|
})
|
|
265
276
|
.strict()
|
|
266
277
|
.superRefine((value, ctx) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { frontendExecutionPolicySchema } from "../../shared/frontend-execution-policy.js";
|
|
2
3
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { deflateRawSync } from "node:zlib";
|
|
@@ -3061,7 +3062,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3061
3062
|
.filter(Boolean)
|
|
3062
3063
|
.join("\n\n"),
|
|
3063
3064
|
scout: [
|
|
3064
|
-
buildSourceContextBlock(sources, { includeReferenceDocuments: false }),
|
|
3065
|
+
buildSourceContextBlock(sources, { includeRequirementExcerpt: false, includeConstraintExcerpt: false, includeReferenceDocuments: false }),
|
|
3065
3066
|
capabilityContextBlock,
|
|
3066
3067
|
]
|
|
3067
3068
|
.filter(Boolean)
|
|
@@ -3342,11 +3343,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3342
3343
|
allowedPaths: readOnlyPaths,
|
|
3343
3344
|
forbiddenPaths,
|
|
3344
3345
|
skills: FRONTEND_CONTRACT_SKILLS,
|
|
3345
|
-
outputContract: "
|
|
3346
|
+
outputContract: "Incremental typed requirement facts; narrative is display-only. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_ui_state / record_required_deliverables / record_openspec_selection, then complete each input scope with complete_contract_scope, then call finalize_contract; correct rejected calls until one successful terminal. record_requirement takes the canonical ledger requirement id and optional execution:{groupId,kind,summary} — the runtime owns the authoritative text, source spans, fragment bindings, and disposition. UI-visible or interactive requirements register a non-blocking frontend-test handoff intent, and any source-declared UI-state table is extracted verbatim through record_ui_state. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
|
|
3346
3347
|
subtask_prompt: [
|
|
3347
|
-
"OUTPUT BUDGET DISCIPLINE
|
|
3348
|
-
"
|
|
3349
|
-
"Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes
|
|
3348
|
+
"OUTPUT BUDGET DISCIPLINE: provider capacity is discovered at runtime; use small records — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
|
|
3349
|
+
"Consume the complete injected input scope and produce a concise frontend implementation contract as typed requirement facts from complete injected scopes.",
|
|
3350
|
+
"Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes the canonical id and optional execution:{groupId,kind,summary}; the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself — model rewrites and JSON-stringified fragment arrays are rejected.",
|
|
3350
3351
|
"Requirement semantics, source spans, dispositions, and fragment bindings are ledger/runtime-owned. If a canonical requirement is genuinely blocked, say so in the Markdown contract narrative and finalize with the matching disposition instead of trying to encode it in the requirement fact.",
|
|
3351
3352
|
"Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
|
|
3352
3353
|
'Use record_evidence_expectation with {requirementId,evidence:{static,behavior,mock,"real-integration"}}; every lane is required | optional | not-applicable. Requirement text and provenance remain runtime-owned.',
|
|
@@ -3406,17 +3407,17 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3406
3407
|
retryOnInvalid: true,
|
|
3407
3408
|
skeleton: frontendContractSkeleton,
|
|
3408
3409
|
},
|
|
3409
|
-
outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan
|
|
3410
|
+
outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan; correct rejected facts until one successful terminal. Contract owns requirement semantics; Scout owns repository discovery; deterministic runtime owns schema, protected fields, path containment, and command validation. No Markdown narrative or file writes.",
|
|
3410
3411
|
subtask_prompt: [
|
|
3411
3412
|
"Plan only the delta between the frozen frontend-contract-pi facts and frontend-scout-pi target surface. Do not reinterpret the task, repeat requirements, search the repository, or choose implementation order.",
|
|
3412
3413
|
"Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
|
|
3413
3414
|
"Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
|
|
3414
3415
|
`Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a frozen commandId from the directory above plus a Scout-confirmed file. Behavior commands prove observable behavior: one target may cover multiple related requirementIds when one test behavior proves them together; do not mechanically create one target per requirement. A behavior target id is the stable machine trace token and its file must be a test file. Static commands are project-wide checks traced by file and command only.`,
|
|
3415
|
-
"UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept.
|
|
3416
|
+
"UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept. Details consume complete execution-group scopes and reuse the same global names across scopes. Constraints/exclusions must not manufacture UI. Then record_state_flow entries whose names all come from that registry; uiState names must use the contract's declared authoritative ids (declaredUiStates in the plan input) when present. Retry attempts see committedUx in this input — reuse those exact names. Components: one choice may cover many state/interaction ids via covers; reuse-existing requires evidencePath naming an existing repo file (greenfield must be decision=new).",
|
|
3416
3417
|
...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
|
|
3417
|
-
"Call finalize_plan
|
|
3418
|
-
"TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
|
|
3419
|
-
"
|
|
3418
|
+
"Call finalize_plan; correct rejected facts until one successful terminal after the necessary typed facts. Return no Markdown narrative.",
|
|
3419
|
+
"Group members retain full source text and independent ACs. Use record_plan_group_coverage only for actually shared references; do not create components for exclusions. TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
|
|
3420
|
+
"Incremental output protocol: process the current complete scope, committing small records immediately. Runtime packs full input and estimated work without assuming a model capacity from its name. Use record_plan_group_coverage for shared references, record_mock_endpoint per endpoint, and finalize only when all coverage is complete. On exhaustion, durable progress survives and remaining work is reduced; never omit source conditions or repeatedly retry the same exhausted scope.",
|
|
3420
3421
|
fixedVerificationContext,
|
|
3421
3422
|
scopedOpenspecContext,
|
|
3422
3423
|
mockContextBlock,
|
|
@@ -3500,6 +3501,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3500
3501
|
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
3501
3502
|
outputContract: "Authoritative typed design terminal via approve_design / request_design_changes tools. No JSON verdict; the committed typed design fact is the only authority. No file writes.",
|
|
3502
3503
|
subtask_prompt: [
|
|
3504
|
+
"Submit findings individually with record_design_finding and stable IDs; terminal tools aggregate saved findings. Correct rejected calls, stop after a successful terminal. Check execution groups against every member source outcome, including permission, threshold and failure-path differences; shared verification is valid only when it proves each independent AC.",
|
|
3503
3505
|
"Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
|
|
3504
3506
|
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
|
|
3505
3507
|
"request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
@@ -3665,6 +3667,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3665
3667
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
3666
3668
|
outputContract: 'Authoritative typed review terminal via approve_review / request_review_changes tools. No JSON verdict is required in the response text; the typed terminal fact is the only authority. No file writes.',
|
|
3667
3669
|
subtask_prompt: [
|
|
3670
|
+
"Submit findings individually with record_review_finding and stable IDs; terminal tools aggregate saved findings. Correct rejected calls, stop after a successful terminal. Check execution groups against every member source outcome, including permission, threshold and failure-path differences; shared verification is valid only when it proves each independent AC.",
|
|
3668
3671
|
"Review the frontend implementation and verification evidence.",
|
|
3669
3672
|
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_review or request_review_changes. Call it once and do not call the other afterwards.",
|
|
3670
3673
|
"approve_review means the implementation passes; it must not carry Critical or Important findings. request_review_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
@@ -3751,6 +3754,9 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3751
3754
|
// runner can bound M6 auto-recovery without re-reading the task config.
|
|
3752
3755
|
const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
|
|
3753
3756
|
spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
|
|
3757
|
+
// Configurable execution safety quota, independent of model context/output capacity.
|
|
3758
|
+
spec.budget = { schemaVersion: 1, mode: "hard", limits: { maxProviderRequests: sources.taskConfig.frontendRecovery?.maxProviderRequests ?? 1024 } };
|
|
3759
|
+
spec.frontendExecutionPolicy = frontendExecutionPolicySchema.parse(sources.taskConfig.frontendExecutionPolicy ?? {});
|
|
3754
3760
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
3755
3761
|
stampGeneratedArtifactBindings(spec);
|
|
3756
3762
|
parseDagSpec(spec);
|