@tea-agent/loop-agent 0.44.0-next.7 → 0.44.0-next.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +166 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +166 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +198 -118
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +76 -11
- package/dist/executors/shell-executor.js +88 -12
- package/dist/infrastructure/console/operation-store.js +2 -0
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/task/config-types.js +22 -9
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -8
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/operator-actions.js +0 -1
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/prd-intake-bridge.js +5 -15
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DTuku_-7.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CQbCsSqb.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C-xUntTR.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DhNlwGAl.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B7j3nx8o.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCrcxEh2.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CsaePDb3.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H--SRQc1rN.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C5BFY2Po.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Caso6uN7.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AY2ZSAx2.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DiMqZQ_8.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-gpvvriOp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUdyoJhI.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bp1xW3T4.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-D-JIcOIT.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C-NKFflx.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CSOcvOAA.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BQSBltMy.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0LVC45G.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cypg3nqy.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BBDU88XJ.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BZusLqSb.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Cfolotcr.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CjfyUy0m.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DJdz7txE.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CQqgPO6y.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BOpGfS2m.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C6UCu0qx.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/{index-DWKejNVJ.js → index-CTMZsDmh.js} +98 -98
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWeaVEUB.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C8gmlBbk.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DF3bm8Bp.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D2fG2Vxi.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BGKgMc2T.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DF6mMPJ9.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-AMhePheC.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DN1Cl0W0.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cx9ngUeN.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BcYPTQJb.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BqHUZWnH.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CmqWZ0-B.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P--4YetH1y.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGD4VMfD.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-QVw9q7rh.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CuemV9pe.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C7oKzZKs.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C2Of60nT.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DHtpQHyL.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-COi9OhSy.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DPHQfRrt.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +2 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -67
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/backend-test-dag.json +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +0 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +0 -8
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** DAG Pi plan ledger 契约与来源继承装载:纯类型与纯函数,禁止反向依赖执行器入口。 */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { frontendExecutionSchema } from "../../../workflows/dag/frontend-execution-groups.js";
|
|
4
|
+
/**
|
|
5
|
+
* Source fidelity ledger (AC-005/AC-006): load the contract node's committed
|
|
6
|
+
* requirement facts and build a requirement-id → provenance map. The contract
|
|
7
|
+
* node declared sourceFragmentIds/sourceRefs from the ledger's
|
|
8
|
+
* requirement→fragment mapping; the plan inherits them by id so the compiled
|
|
9
|
+
* canonical contract carries authoritative provenance without the plan
|
|
10
|
+
* re-deriving (or fabricating) it. Best-effort: missing/unreadable contract
|
|
11
|
+
* ledger yields an empty map and the plan compiles as before (the design
|
|
12
|
+
* policy shell will then fail closed on missing provenance).
|
|
13
|
+
*/
|
|
14
|
+
export async function loadContractRequirementInheritance(runDir) {
|
|
15
|
+
const { readTypedEventStoreFromJsonl } = await import("../../../workflows/dag/frontend-typed-event-store.js");
|
|
16
|
+
let records;
|
|
17
|
+
try {
|
|
18
|
+
records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return new Map();
|
|
22
|
+
}
|
|
23
|
+
const byId = new Map();
|
|
24
|
+
for (const record of records) {
|
|
25
|
+
const fact = record.fact;
|
|
26
|
+
if (!fact || typeof fact !== "object")
|
|
27
|
+
continue;
|
|
28
|
+
const recordFact = fact;
|
|
29
|
+
if (recordFact.origin !== "contract" ||
|
|
30
|
+
recordFact.kind !== "requirement") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Contract requirement facts carry id/sourceFragmentIds/sourceRefs on
|
|
34
|
+
// the fact itself (origin=contract, kind=requirement, id, text, ...),
|
|
35
|
+
// not inside an `entry` wrapper.
|
|
36
|
+
const id = typeof recordFact.id === "string" ? recordFact.id : "";
|
|
37
|
+
if (!id)
|
|
38
|
+
continue;
|
|
39
|
+
const sourceFragmentIds = Array.isArray(recordFact.sourceFragmentIds)
|
|
40
|
+
? recordFact.sourceFragmentIds.filter((value) => typeof value === "string")
|
|
41
|
+
: undefined;
|
|
42
|
+
const sourceRefs = Array.isArray(recordFact.sourceRefs)
|
|
43
|
+
? recordFact.sourceRefs.filter((value) => typeof value === "string")
|
|
44
|
+
: undefined;
|
|
45
|
+
if (sourceFragmentIds || sourceRefs) {
|
|
46
|
+
byId.set(id, {
|
|
47
|
+
...(typeof recordFact.text === "string" ? { text: recordFact.text } : {}),
|
|
48
|
+
...(recordFact.execution !== undefined ? { execution: frontendExecutionSchema.parse(recordFact.execution) } : {}),
|
|
49
|
+
...(sourceFragmentIds ? { sourceFragmentIds } : {}),
|
|
50
|
+
...(sourceRefs ? { sourceRefs } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return byId;
|
|
55
|
+
}
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/** DAG Pi contract 工具工厂:origin=contract 增量契约工具与 vNext 审计装配,禁止反向依赖执行器入口。 */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createDurableFrontendTools } from "../../../workflows/dag/frontend-durable-tools.js";
|
|
5
|
+
import { writeJsonAtomic } from "../../../infrastructure/harness/atomic-write.js";
|
|
6
|
+
import { mapContractBlockedOwner } from "../../../workflows/dag/frontend-human-decision.js";
|
|
7
|
+
import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../../../workflows/dag/frontend-execution-groups.js";
|
|
8
|
+
import { frontendEvidenceExpectationSchema, validateFrontendRequiredDeliverables, } from "../../../workflows/dag/frontend-contract-facts.js";
|
|
9
|
+
import { isRecordObject } from "../guards.js";
|
|
10
|
+
/** A contract fact is source-mapped when it carries a non-empty `sourceSpan`
|
|
11
|
+
* (object or string), a non-empty `sourceRefs` array, or a non-empty `source`
|
|
12
|
+
* string. Anything else is an unmapped source segment (AC-001). */
|
|
13
|
+
function contractFactSourceSpan(fact) {
|
|
14
|
+
if (isRecordObject(fact.sourceSpan) || typeof fact.sourceSpan === "string") {
|
|
15
|
+
return fact.sourceSpan;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(fact.sourceRefs) && fact.sourceRefs.length > 0) {
|
|
18
|
+
return fact.sourceRefs;
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(fact.sourceFragmentIds) &&
|
|
21
|
+
fact.sourceFragmentIds.some((value) => typeof value === "string" && value.trim().length > 0)) {
|
|
22
|
+
return fact.sourceFragmentIds;
|
|
23
|
+
}
|
|
24
|
+
if (typeof fact.source === "string" && fact.source.trim().length > 0) {
|
|
25
|
+
return fact.source;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A+B (AC-001): deterministically assemble the read-only `frontend-task-contract-vNext`
|
|
31
|
+
* audit artifact from committed contract facts. It never rewrites the original
|
|
32
|
+
* task source: unmapped requirement/constraint segments are listed explicitly
|
|
33
|
+
* so Plan/Implement/Review keep binding to the raw source, not the model's
|
|
34
|
+
* summarized contract. The blocked disposition is projected through the frozen
|
|
35
|
+
* `mapContractBlockedOwner` mapping (AC-002).
|
|
36
|
+
*/
|
|
37
|
+
export function buildFrontendTaskContractVNext(records) {
|
|
38
|
+
const facts = records
|
|
39
|
+
.filter((record) => record.phase === "committed")
|
|
40
|
+
.map((record) => record.fact)
|
|
41
|
+
.filter(isRecordObject);
|
|
42
|
+
const finalized = facts.find((fact) => fact.kind === "contract-finalized");
|
|
43
|
+
const disposition = typeof finalized?.disposition === "string" ? finalized.disposition : null;
|
|
44
|
+
const blockingOwner = typeof finalized?.blockingOwner === "string"
|
|
45
|
+
? finalized.blockingOwner
|
|
46
|
+
: null;
|
|
47
|
+
const projected = mapContractBlockedOwner({
|
|
48
|
+
disposition: disposition ?? "",
|
|
49
|
+
blockingOwner: blockingOwner ?? undefined,
|
|
50
|
+
});
|
|
51
|
+
const byKind = (kind) => facts.filter((fact) => fact.kind === kind);
|
|
52
|
+
const requirements = byKind("requirement");
|
|
53
|
+
const unmappedSourceSegments = requirements
|
|
54
|
+
.filter((fact) => contractFactSourceSpan(fact) === undefined)
|
|
55
|
+
.map((fact) => ({
|
|
56
|
+
kind: "requirement",
|
|
57
|
+
id: typeof fact.id === "string"
|
|
58
|
+
? fact.id
|
|
59
|
+
: typeof fact.text === "string"
|
|
60
|
+
? fact.text
|
|
61
|
+
: undefined,
|
|
62
|
+
}))
|
|
63
|
+
.filter((segment) => segment.id !== undefined);
|
|
64
|
+
return {
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
schemaId: "frontend-task-contract-vNext",
|
|
67
|
+
disposition,
|
|
68
|
+
blockingOwner,
|
|
69
|
+
blockedOwner: projected === "not-blocked" ? null : projected,
|
|
70
|
+
requirements,
|
|
71
|
+
constraints: byKind("constraint"),
|
|
72
|
+
evidenceExpectations: byKind("evidence-expectation"),
|
|
73
|
+
deliverableDeclarations: byKind("required-deliverables").at(-1)?.items ?? [],
|
|
74
|
+
handoffIntents: byKind("handoff-intent"),
|
|
75
|
+
openQuestions: byKind("open-question"),
|
|
76
|
+
splitProposals: byKind("split-proposal"),
|
|
77
|
+
unmappedSourceSegments,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A+B: `frontend-contract-pi` incremental contract tools. The `record_*` tools
|
|
82
|
+
* commit origin=contract facts and `finalize_contract` commits the terminal
|
|
83
|
+
* disposition (ready | ready-with-assumptions | blocked + blockingOwner).
|
|
84
|
+
*/
|
|
85
|
+
export async function createFrontendContractTools(input) {
|
|
86
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
87
|
+
import("typebox"),
|
|
88
|
+
import("@earendil-works/pi-coding-agent"),
|
|
89
|
+
]);
|
|
90
|
+
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../../../workflows/dag/frontend-typed-event-store.js");
|
|
91
|
+
const { adoptTypedEventFact, stageTypedEventFact } = await import("../../../workflows/dag/frontend-typed-event-transaction.js");
|
|
92
|
+
let store = input.store;
|
|
93
|
+
const attemptId = input.attemptId;
|
|
94
|
+
let activeScope = null;
|
|
95
|
+
const committedRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement").map(r => String(r.fact.id)));
|
|
96
|
+
const completedScopeRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "contract-scope-completed").flatMap(r => Array.isArray(r.fact.requirementIds) ? r.fact.requirementIds.filter((id) => typeof id === "string") : []));
|
|
97
|
+
const receipt = (details) => ({
|
|
98
|
+
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
99
|
+
details,
|
|
100
|
+
});
|
|
101
|
+
async function adoptContractFact(kind, fact) {
|
|
102
|
+
try {
|
|
103
|
+
const requestId = `${attemptId}:${kind}:${randomUUID()}`;
|
|
104
|
+
const staged = stageTypedEventFact({
|
|
105
|
+
store,
|
|
106
|
+
requestId,
|
|
107
|
+
attemptId,
|
|
108
|
+
fact: fact,
|
|
109
|
+
});
|
|
110
|
+
const committed = await adoptTypedEventFact({
|
|
111
|
+
store,
|
|
112
|
+
requestId,
|
|
113
|
+
attemptId,
|
|
114
|
+
fact: fact,
|
|
115
|
+
eventId: staged.eventId,
|
|
116
|
+
expectedRevision: store.revision,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
kind,
|
|
121
|
+
eventId: committed.eventId,
|
|
122
|
+
revision: committed.revision,
|
|
123
|
+
error: "",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
kind,
|
|
130
|
+
code: error?.code,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const recordKinds = {
|
|
136
|
+
record_constraint: "constraint",
|
|
137
|
+
record_handoff_intent: "handoff-intent",
|
|
138
|
+
record_open_question: "open-question",
|
|
139
|
+
record_split_proposal: "split-proposal",
|
|
140
|
+
};
|
|
141
|
+
const recordTools = Object.entries(recordKinds).map(([name, kind]) => defineTool({
|
|
142
|
+
name,
|
|
143
|
+
label: name,
|
|
144
|
+
description: `Commit an origin=contract ${kind} fact. IMPORTANT: submit incrementally — batch up to 5 record_* calls per message, starting from the FIRST message; never attempt to emit the whole contract in one response (a single large dump will be truncated and rejected). Every message must make progress by committing at least one record_* fact.`,
|
|
145
|
+
promptSnippet: `Commit 1-5 origin=contract ${kind} facts (up to 5 per message).`,
|
|
146
|
+
parameters: Type.Object({
|
|
147
|
+
text: Type.String({ minLength: 1 }),
|
|
148
|
+
requirementIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
149
|
+
sourceFragmentIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
150
|
+
...(kind === "constraint" ? { category: Type.Optional(Type.Enum({ constraint: "constraint", "non-goal": "non-goal", risk: "risk" })) } : {}),
|
|
151
|
+
...(kind === "handoff-intent" ? { taskKind: Type.Literal("frontend-test"), blocking: Type.Optional(Type.Boolean()) } : {}),
|
|
152
|
+
}, { additionalProperties: false }),
|
|
153
|
+
async execute(_toolCallId, params) {
|
|
154
|
+
const data = params;
|
|
155
|
+
if (!data.text?.trim())
|
|
156
|
+
return receipt({ ok: false, code: "TOOL_SCHEMA_INVALID", error: `${name}: text must be non-empty` });
|
|
157
|
+
const knownFragments = new Set([...(input.canonicalRequirements?.values() ?? [])].flatMap(r => r.sourceFragmentIds));
|
|
158
|
+
if (data.requirementIds?.some(id => !input.canonicalRequirements?.has(id)) || data.sourceFragmentIds?.some(id => !knownFragments.has(id)))
|
|
159
|
+
return receipt({ ok: false, code: "CONTRACT_REFERENCE_UNKNOWN", error: `${name}: reference is outside the frozen source inventory` });
|
|
160
|
+
const result = await adoptContractFact(kind, {
|
|
161
|
+
...(params ?? {}),
|
|
162
|
+
kind,
|
|
163
|
+
origin: "contract",
|
|
164
|
+
});
|
|
165
|
+
return receipt(result);
|
|
166
|
+
},
|
|
167
|
+
}));
|
|
168
|
+
// record_requirement is runtime-owned by design: the requirement TEXT and
|
|
169
|
+
// sourceFragmentIds come from the frozen source-fidelity ledger, never from
|
|
170
|
+
// model-authored prose. The model only names the canonical id it confirms.
|
|
171
|
+
// This closes the free-shape hole (additionalProperties:true accepted
|
|
172
|
+
// `statement` rewrites and JSON-stringified `sourceFragmentIds` arrays,
|
|
173
|
+
// which then reached the planner as empty text + dead fragment bindings).
|
|
174
|
+
const recordRequirementTool = defineTool({
|
|
175
|
+
name: "record_requirement",
|
|
176
|
+
label: "record_requirement",
|
|
177
|
+
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass the canonical id and optional execution:{groupId,kind,summary} only. Reuse a group only when its behavior and all permission/threshold/error conditions agree; retain separate groups for differences. Constraints/exclusions do not require invented UI. The canonical id is listed in the <frontend_contract_input> inventory, e.g. {"id": "AC-001"} — the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself: free-form rewrites and JSON-stringified fragment arrays are rejected. IMPORTANT: batch up to 5 record_* calls per message, starting from the FIRST message.',
|
|
178
|
+
promptSnippet: "Confirm 1-5 canonical requirements by id (up to 5 per message).",
|
|
179
|
+
parameters: Type.Object({ id: Type.String({ description: "Canonical ledger requirement id (e.g. AC-001)" }), execution: Type.Optional(Type.Object({ groupId: Type.String({ minLength: 1 }), kind: Type.Union([Type.Literal("behavior"), Type.Literal("constraint"), Type.Literal("exclusion")]), summary: Type.String({ minLength: 1 }) }, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
180
|
+
async execute(_toolCallId, params) {
|
|
181
|
+
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
182
|
+
if (!id) {
|
|
183
|
+
return receipt({
|
|
184
|
+
ok: false,
|
|
185
|
+
kind: "requirement",
|
|
186
|
+
error: "record_requirement requires the canonical requirement id",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (activeScope && !activeScope.has(id))
|
|
190
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `requirement ${id} is outside the complete input scope of this session` });
|
|
191
|
+
const canonical = input.canonicalRequirements?.get(id);
|
|
192
|
+
if (!canonical) {
|
|
193
|
+
const known = [...(input.canonicalRequirements?.keys() ?? [])];
|
|
194
|
+
return receipt({
|
|
195
|
+
ok: false,
|
|
196
|
+
kind: "requirement",
|
|
197
|
+
error: `record_requirement id "${id}" is not a canonical ledger requirement; canonical ids are: ${known.join(", ") || "(none)"}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (params.execution) {
|
|
201
|
+
try {
|
|
202
|
+
collectFrontendExecutionGroups([...readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement" && r.fact.id !== id).map(r => ({ id: String(r.fact.id), execution: r.fact.execution })), { id, execution: params.execution }]);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return receipt({ ok: false, code: "EXECUTION_GROUP_CONFLICT", error: String(error) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const result = await adoptContractFact("requirement", {
|
|
209
|
+
kind: "requirement",
|
|
210
|
+
origin: "contract",
|
|
211
|
+
disposition: "explicit",
|
|
212
|
+
id,
|
|
213
|
+
text: canonical.text,
|
|
214
|
+
sourceFragmentIds: canonical.sourceFragmentIds,
|
|
215
|
+
...(params.execution ? { execution: params.execution } : {}),
|
|
216
|
+
});
|
|
217
|
+
return receipt(result);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
// Requirement identity/text/source are runtime-owned and seeded before the
|
|
221
|
+
// model starts. Execution grouping is still a model decision, so expose it
|
|
222
|
+
// as a small typed update instead of forcing the model to re-submit the same
|
|
223
|
+
// requirement just to attach execution metadata.
|
|
224
|
+
const recordRequirementExecutionTool = defineTool({
|
|
225
|
+
name: "record_requirement_execution",
|
|
226
|
+
label: "record_requirement_execution",
|
|
227
|
+
description: "Attach execution ownership to already-confirmed canonical requirements.",
|
|
228
|
+
promptSnippet: "Record execution group metadata for confirmed requirements.",
|
|
229
|
+
parameters: Type.Object({
|
|
230
|
+
requirementIds: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, uniqueItems: true }),
|
|
231
|
+
execution: Type.Object({
|
|
232
|
+
groupId: Type.String({ minLength: 1 }),
|
|
233
|
+
kind: Type.Union([Type.Literal("behavior"), Type.Literal("constraint"), Type.Literal("exclusion")]),
|
|
234
|
+
summary: Type.String({ minLength: 1 }),
|
|
235
|
+
}, { additionalProperties: false }),
|
|
236
|
+
}, { additionalProperties: false }),
|
|
237
|
+
async execute(callId, params) {
|
|
238
|
+
const ids = params.requirementIds;
|
|
239
|
+
if (activeScope && ids.some((id) => !activeScope?.has(id)))
|
|
240
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "execution metadata references a requirement outside the active contract scope" });
|
|
241
|
+
const canonical = input.canonicalRequirements;
|
|
242
|
+
const unknown = ids.filter((id) => !canonical?.has(id));
|
|
243
|
+
if (unknown.length > 0)
|
|
244
|
+
return receipt({ ok: false, code: "CONTRACT_REFERENCE_UNKNOWN", error: `record_requirement_execution references unknown canonical requirements: ${unknown.join(", ")}` });
|
|
245
|
+
const execution = frontendExecutionSchema.parse(params.execution);
|
|
246
|
+
const existing = readCommittedEvents(store, attemptId)
|
|
247
|
+
.filter((record) => record.fact.kind === "requirement")
|
|
248
|
+
.map((record) => ({ id: String(record.fact.id), execution: record.fact.execution }))
|
|
249
|
+
.filter((unit) => !ids.includes(unit.id));
|
|
250
|
+
try {
|
|
251
|
+
collectFrontendExecutionGroups([...existing, ...ids.map((id) => ({ id, execution }))]);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
return receipt({ ok: false, code: "EXECUTION_GROUP_CONFLICT", error: error instanceof Error ? error.message : String(error) });
|
|
255
|
+
}
|
|
256
|
+
let result = { ok: true };
|
|
257
|
+
for (const id of ids) {
|
|
258
|
+
const requirement = canonical.get(id);
|
|
259
|
+
result = await adoptContractFact("requirement", { kind: "requirement", origin: "contract", disposition: "explicit", id, text: requirement.text, sourceFragmentIds: requirement.sourceFragmentIds, execution });
|
|
260
|
+
if (result.ok !== true)
|
|
261
|
+
return receipt(result);
|
|
262
|
+
}
|
|
263
|
+
return receipt({ ...result, requestId: callId });
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
// Authoritative UI state declarations: the contract node extracts the
|
|
267
|
+
// PRD/reference UI-state table into structured facts so the planner binds
|
|
268
|
+
// uiStates to declared ids instead of inventing names (dogfood
|
|
269
|
+
// dag-1788504923861-0f7b17a9: 10 invented list-visibility variants, 0 of
|
|
270
|
+
// the 7 PRD states).
|
|
271
|
+
const recordUiStateTool = defineTool({
|
|
272
|
+
name: "record_ui_state",
|
|
273
|
+
label: "record_ui_state",
|
|
274
|
+
description: 'Declare ONE authoritative UI state extracted from the task source\'s UI-state table (origin=contract ui-state-declaration fact). Call once per declared state, exactly as the source names it: {"id": "<state id from the source table>", "trigger": "<when this state applies>", "observableOutcome": "<what the user can observe>"}. The planner must bind these ids later; do not rename or invent states.',
|
|
275
|
+
promptSnippet: "Declare 1-5 authoritative UI states (up to 5 per message).",
|
|
276
|
+
parameters: Type.Object({
|
|
277
|
+
id: Type.String({ description: "UI state id exactly as the source table declares it" }),
|
|
278
|
+
trigger: Type.String({ description: "When this state applies" }),
|
|
279
|
+
observableOutcome: Type.String({ description: "Observable result for the user" }),
|
|
280
|
+
}, { additionalProperties: false }),
|
|
281
|
+
async execute(_toolCallId, params) {
|
|
282
|
+
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
283
|
+
const trigger = typeof params?.trigger === "string" ? params.trigger.trim() : "";
|
|
284
|
+
const observableOutcome = typeof params?.observableOutcome === "string"
|
|
285
|
+
? params.observableOutcome.trim()
|
|
286
|
+
: "";
|
|
287
|
+
if (!id || !trigger || !observableOutcome) {
|
|
288
|
+
return receipt({
|
|
289
|
+
ok: false,
|
|
290
|
+
kind: "ui-state-declaration",
|
|
291
|
+
error: "record_ui_state requires non-empty id, trigger, and observableOutcome",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const result = await adoptContractFact("ui-state-declaration", {
|
|
295
|
+
kind: "ui-state-declaration",
|
|
296
|
+
origin: "contract",
|
|
297
|
+
id,
|
|
298
|
+
trigger,
|
|
299
|
+
observableOutcome,
|
|
300
|
+
});
|
|
301
|
+
return receipt(result);
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
const evidenceStatus = Type.Enum({
|
|
305
|
+
required: "required", optional: "optional", "not-applicable": "not-applicable",
|
|
306
|
+
});
|
|
307
|
+
const recordEvidenceExpectationTool = defineTool({
|
|
308
|
+
name: "record_evidence_expectation",
|
|
309
|
+
label: "record_evidence_expectation",
|
|
310
|
+
description: 'Record evidence requirements as {requirementId:"<canonical id>",evidence:{static:"required|optional|not-applicable",behavior:"required|optional|not-applicable",mock:"required|optional|not-applicable","real-integration":"required|optional|not-applicable"}}. Declare at least one lane; later calls replace only the lanes they name.',
|
|
311
|
+
parameters: Type.Object({
|
|
312
|
+
requirementId: Type.String(),
|
|
313
|
+
evidence: Type.Object({
|
|
314
|
+
static: Type.Optional(evidenceStatus),
|
|
315
|
+
behavior: Type.Optional(evidenceStatus),
|
|
316
|
+
mock: Type.Optional(evidenceStatus),
|
|
317
|
+
"real-integration": Type.Optional(evidenceStatus),
|
|
318
|
+
}, { additionalProperties: false }),
|
|
319
|
+
}, { additionalProperties: false }),
|
|
320
|
+
async execute(_toolCallId, params) {
|
|
321
|
+
try {
|
|
322
|
+
const fact = frontendEvidenceExpectationSchema.parse(params);
|
|
323
|
+
if (!input.canonicalRequirements?.has(fact.requirementId)) {
|
|
324
|
+
throw new Error(`unknown canonical requirement ${fact.requirementId}`);
|
|
325
|
+
}
|
|
326
|
+
return receipt(await adoptContractFact("evidence-expectation", {
|
|
327
|
+
kind: "evidence-expectation", origin: "contract", ...fact,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return receipt({
|
|
332
|
+
ok: false, kind: "evidence-expectation",
|
|
333
|
+
error: error instanceof Error ? error.message : String(error),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
const recordRequiredDeliverablesTool = defineTool({
|
|
339
|
+
name: "record_required_deliverables",
|
|
340
|
+
label: "record_required_deliverables",
|
|
341
|
+
description: 'Declare the complete source-required file deliverables as {items:[{path,requirementId,sourceFragmentId}]}. Interpret obligations from the original source, including lists/tables: permissions (allowedPaths/only allowed to modify), prohibitions, examples and read-only references are NOT delivery obligations. Each path must appear exactly in its frozen requirement-bound source fragment. Submit {items:[]} explicitly if no files are mandatory. Each call appends complete source-bound items; replace:true explicitly replaces the inventory. Required before finalize_contract ready.',
|
|
342
|
+
parameters: Type.Object({ replace: Type.Optional(Type.Boolean()), items: Type.Array(Type.Object({
|
|
343
|
+
path: Type.String(), requirementId: Type.String(), sourceFragmentId: Type.String(),
|
|
344
|
+
}, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
345
|
+
async execute(_toolCallId, params) {
|
|
346
|
+
try {
|
|
347
|
+
const declaration = validateFrontendRequiredDeliverables({ items: params.items }, input.canonicalRequirements ?? new Map());
|
|
348
|
+
const previous = params.replace ? [] : readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "required-deliverables").at(-1)?.fact.items;
|
|
349
|
+
const items = [...new Map([...(Array.isArray(previous) ? previous : []), ...declaration.items].map(item => [JSON.stringify(item), item])).values()];
|
|
350
|
+
return receipt(await adoptContractFact("required-deliverables", {
|
|
351
|
+
kind: "required-deliverables", origin: "contract", items,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return receipt({
|
|
356
|
+
ok: false, kind: "required-deliverables",
|
|
357
|
+
error: error instanceof Error ? error.message : String(error),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
// OpenSpec selection committed as individual typed facts (one path per
|
|
363
|
+
// call) so a large candidate set never exceeds a single model output
|
|
364
|
+
// budget: each tool call carries exactly one {path, disposition,
|
|
365
|
+
// rationale} row and the ledger accumulates them across calls. Only
|
|
366
|
+
// positive classifications (required | relevant) are legal; unmentioned
|
|
367
|
+
// candidates default to irrelevant at the prewrite gate.
|
|
368
|
+
const recordOpenspecSelectionTool = defineTool({
|
|
369
|
+
name: "record_openspec_selection",
|
|
370
|
+
label: "record_openspec_selection",
|
|
371
|
+
description: "Commit one OpenSpec candidate classification (origin=contract openspec-selection fact). Call once per path you actually use or consult: required (must be read and cited) or relevant (informs planning). Never call it for irrelevant candidates — unmentioned candidates default to irrelevant. You may call it many times; one row per call.",
|
|
372
|
+
promptSnippet: "Commit one OpenSpec candidate classification (required | relevant); one path per call; skip irrelevant candidates.",
|
|
373
|
+
parameters: Type.Object({
|
|
374
|
+
path: Type.String({
|
|
375
|
+
description: "Repo-relative candidate spec path, e.g. openspec/project-specs/ui/ucp-components-md/AdvancedSearch.md",
|
|
376
|
+
}),
|
|
377
|
+
disposition: Type.Enum({
|
|
378
|
+
required: "required",
|
|
379
|
+
relevant: "relevant",
|
|
380
|
+
}),
|
|
381
|
+
rationale: Type.String({}),
|
|
382
|
+
}, { additionalProperties: false }),
|
|
383
|
+
async execute(_toolCallId, params) {
|
|
384
|
+
const path = typeof params?.path === "string" ? params.path : "";
|
|
385
|
+
const disposition = params?.disposition;
|
|
386
|
+
const rationale = typeof params?.rationale === "string" ? params.rationale : "";
|
|
387
|
+
if (!path || !disposition || !rationale.trim()) {
|
|
388
|
+
return receipt({
|
|
389
|
+
ok: false,
|
|
390
|
+
kind: "openspec-selection",
|
|
391
|
+
error: "record_openspec_selection requires non-empty path, disposition (required|relevant), and rationale",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const result = await adoptContractFact("openspec-selection", {
|
|
395
|
+
kind: "openspec-selection",
|
|
396
|
+
origin: "contract",
|
|
397
|
+
path,
|
|
398
|
+
disposition,
|
|
399
|
+
rationale,
|
|
400
|
+
});
|
|
401
|
+
return receipt(result);
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
const finalizeContractTool = defineTool({
|
|
405
|
+
name: "finalize_contract",
|
|
406
|
+
label: "finalize_contract",
|
|
407
|
+
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner). A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
|
|
408
|
+
promptSnippet: "Commit the contract-finalized terminal (disposition + optional blockingOwner).",
|
|
409
|
+
parameters: Type.Object({
|
|
410
|
+
disposition: Type.Enum({
|
|
411
|
+
ready: "ready",
|
|
412
|
+
"ready-with-assumptions": "ready-with-assumptions",
|
|
413
|
+
blocked: "blocked",
|
|
414
|
+
}),
|
|
415
|
+
blockingOwner: Type.Optional(Type.Enum({
|
|
416
|
+
"blocked-human": "blocked-human",
|
|
417
|
+
"blocked-external": "blocked-external",
|
|
418
|
+
})),
|
|
419
|
+
assumptions: Type.Optional(Type.Array(Type.String({}))),
|
|
420
|
+
summary: Type.Optional(Type.String({})),
|
|
421
|
+
}, { additionalProperties: false }),
|
|
422
|
+
async execute(_toolCallId, params) {
|
|
423
|
+
const disposition = params?.disposition;
|
|
424
|
+
const blockingOwner = params?.blockingOwner;
|
|
425
|
+
if (disposition === "blocked" && !blockingOwner) {
|
|
426
|
+
return receipt({
|
|
427
|
+
ok: false,
|
|
428
|
+
kind: "finalize_contract",
|
|
429
|
+
error: "blocked disposition requires blockingOwner (blocked-human | blocked-external)",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (disposition !== "blocked" && input.canonicalRequirements?.size &&
|
|
433
|
+
!readCommittedEvents(store, attemptId).some((record) => record.fact.kind === "required-deliverables")) {
|
|
434
|
+
return receipt({
|
|
435
|
+
ok: false, kind: "finalize_contract",
|
|
436
|
+
error: "call record_required_deliverables with the complete source-bound inventory (or items:[] when none) before finalizing",
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const missing = [...(input.canonicalRequirements?.keys() ?? [])].filter(id => !committedRequirementIds().has(id) || (activeScope !== null && !completedScopeRequirementIds().has(id)));
|
|
440
|
+
if (disposition !== "blocked" && missing.length)
|
|
441
|
+
return receipt({ ok: false, code: "CONTRACT_REQUIREMENT_COVERAGE_MISSING", error: `Confirm all complete source obligations before finalizing: ${missing.join(", ")}` });
|
|
442
|
+
const blockedOwner = mapContractBlockedOwner({
|
|
443
|
+
disposition: disposition ?? "",
|
|
444
|
+
blockingOwner,
|
|
445
|
+
});
|
|
446
|
+
const result = await adoptContractFact("contract-finalized", {
|
|
447
|
+
kind: "contract-finalized",
|
|
448
|
+
origin: "contract",
|
|
449
|
+
disposition,
|
|
450
|
+
...(blockingOwner ? { blockingOwner } : {}),
|
|
451
|
+
...(blockedOwner !== "not-blocked" ? { blockedOwner } : {}),
|
|
452
|
+
...(Array.isArray(params?.assumptions)
|
|
453
|
+
? { assumptions: params.assumptions }
|
|
454
|
+
: {}),
|
|
455
|
+
...(typeof params?.summary === "string"
|
|
456
|
+
? { summary: params.summary }
|
|
457
|
+
: {}),
|
|
458
|
+
});
|
|
459
|
+
return receipt(result);
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
const completeScopeTool = defineTool({
|
|
463
|
+
name: "complete_contract_scope", label: "complete_contract_scope",
|
|
464
|
+
description: "After recording all requirements AND their evidence, constraints, questions and deliverables for this session, mark the scope complete. Confirming an ID alone does not complete its analysis. Do this before finalize_contract.",
|
|
465
|
+
parameters: Type.Object({ requirementIds: Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }) }, { additionalProperties: false }),
|
|
466
|
+
async execute(_id, params) {
|
|
467
|
+
const ids = params.requirementIds;
|
|
468
|
+
if (activeScope === null || ids.length !== activeScope.size || ids.some(id => !activeScope?.has(id) || !committedRequirementIds().has(id)))
|
|
469
|
+
return receipt({ ok: false, code: "CONTRACT_SCOPE_INCOMPLETE", error: "Complete exactly the active scope after confirming all its obligations" });
|
|
470
|
+
return receipt(await adoptContractFact("contract-scope-completed", { kind: "contract-scope-completed", origin: "contract", requirementIds: ids }));
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
const durable = await createDurableFrontendTools({
|
|
474
|
+
file: path.join(input.runDir, input.nodeId, "contract-typed-facts.jsonl"), attemptId, store: input.store,
|
|
475
|
+
setWorkingStore: next => { store = next; }, binding: { canonicalRequirements: input.canonicalRequirements, sourceDigest: input.sourceDigest },
|
|
476
|
+
tools: [
|
|
477
|
+
...recordTools,
|
|
478
|
+
recordRequirementTool,
|
|
479
|
+
recordRequirementExecutionTool,
|
|
480
|
+
recordEvidenceExpectationTool,
|
|
481
|
+
recordUiStateTool,
|
|
482
|
+
recordRequiredDeliverablesTool,
|
|
483
|
+
recordOpenspecSelectionTool,
|
|
484
|
+
completeScopeTool,
|
|
485
|
+
finalizeContractTool,
|
|
486
|
+
],
|
|
487
|
+
});
|
|
488
|
+
const durableRecordRequirementTool = durable.customTools.find((tool) => typeof tool === "object" && tool !== null && tool.name === "record_requirement");
|
|
489
|
+
if (!durableRecordRequirementTool)
|
|
490
|
+
throw new Error("frontend contract durable requirement tool unavailable");
|
|
491
|
+
return {
|
|
492
|
+
customTools: durable.customTools,
|
|
493
|
+
inputRequirements: () => [...(input.canonicalRequirements ?? [])].map(([id, value]) => ({ id, text: value.text, sourceFragmentIds: [...value.sourceFragmentIds] })),
|
|
494
|
+
seedCanonicalRequirements: async () => {
|
|
495
|
+
for (const id of input.canonicalRequirements?.keys() ?? []) {
|
|
496
|
+
if (committedRequirementIds().has(id))
|
|
497
|
+
continue;
|
|
498
|
+
const receipt = await durableRecordRequirementTool.execute(`${attemptId}:seed-requirement:${id}`, { id }, undefined, undefined, {});
|
|
499
|
+
if (receipt.details?.ok !== true) {
|
|
500
|
+
throw new Error(`runtime requirement seed rejected for ${id}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
await durable.flush();
|
|
504
|
+
},
|
|
505
|
+
completedScopeRequirementIds,
|
|
506
|
+
setActiveRequirementScope: ids => { activeScope = ids === null ? null : new Set(ids); },
|
|
507
|
+
committedRequirementIds,
|
|
508
|
+
committedFacts: () => readCommittedEvents(input.store, attemptId),
|
|
509
|
+
flush: async () => {
|
|
510
|
+
await durable.flush();
|
|
511
|
+
const committed = readCommittedEvents(input.store, attemptId);
|
|
512
|
+
await writeJsonAtomic(path.join(input.runDir, input.nodeId, "frontend-task-contract-vNext.json"), buildFrontendTaskContractVNext(committed));
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|