@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.4
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/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
5
|
-
import {
|
|
5
|
+
import { writeJsonAtomic } from "../infrastructure/harness/atomic-write.js";
|
|
6
|
+
import { mapContractBlockedOwner } from "../workflows/dag/frontend-human-decision.js";
|
|
7
|
+
import { routeFrontendProviderCapability } from "../workflows/dag/frontend-provider-capability-matrix.js";
|
|
8
|
+
import { executePiStep, resolvePiBackend, } from "./pi-executor.js";
|
|
9
|
+
import { resolveDagPiExtensions, } from "./pi-extension-resolver.js";
|
|
6
10
|
import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
|
|
7
11
|
import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
|
|
8
12
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
9
13
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
10
14
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
11
|
-
import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
15
|
+
import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
12
16
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
13
17
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
14
18
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
@@ -163,6 +167,14 @@ function resolvePiExecutorStep(persona) {
|
|
|
163
167
|
function isDagPiWriteTask(task) {
|
|
164
168
|
return task.executor === "pi" && task.toolProfile === "write";
|
|
165
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* M4: `frontend-implement-pi` derives its status from mechanical facts instead
|
|
172
|
+
* of the IMPLEMENTATION_OUTCOME first line. Everything else (backend/README/
|
|
173
|
+
* module writers + frontend-repair-pi) keeps the legacy protocol face.
|
|
174
|
+
*/
|
|
175
|
+
export function isFrontendFactsWriter(task) {
|
|
176
|
+
return task.writerOutcomePolicy?.type === "frontend-facts-v1";
|
|
177
|
+
}
|
|
166
178
|
/** Map DAG `piStep` / `role` to a safe workflow step for read-only Pi or bounded write. */
|
|
167
179
|
export function resolveDagPiStepName(task) {
|
|
168
180
|
if (isDagPiWriteTask(task)) {
|
|
@@ -178,7 +190,94 @@ export function resolveDagPiStepName(task) {
|
|
|
178
190
|
}
|
|
179
191
|
return resolvePiExecutorStep(resolveDagPiPersona(task));
|
|
180
192
|
}
|
|
193
|
+
/** A+B: `frontend-contract-pi` incremental record tools (origin=contract). */
|
|
194
|
+
export const FRONTEND_CONTRACT_RECORD_TOOL_NAMES = [
|
|
195
|
+
"record_requirement",
|
|
196
|
+
"record_constraint",
|
|
197
|
+
"record_evidence_expectation",
|
|
198
|
+
"record_handoff_intent",
|
|
199
|
+
"record_open_question",
|
|
200
|
+
"record_split_proposal",
|
|
201
|
+
];
|
|
202
|
+
export const FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES = [
|
|
203
|
+
"finalize_contract",
|
|
204
|
+
];
|
|
205
|
+
/** A+B: `frontend-scout-pi` incremental evidence tools (origin=scout). */
|
|
206
|
+
export const FRONTEND_SCOUT_EVIDENCE_TOOL_NAMES = [
|
|
207
|
+
"record_target_surface",
|
|
208
|
+
"record_design_evidence",
|
|
209
|
+
];
|
|
210
|
+
/** A+B: `frontend-plan-pi` incremental record tools (origin=plan). */
|
|
211
|
+
export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
212
|
+
"record_target_surface",
|
|
213
|
+
"record_component_choice",
|
|
214
|
+
"record_state_flow",
|
|
215
|
+
"record_data_flow",
|
|
216
|
+
"record_mock_api",
|
|
217
|
+
"record_design_deviation",
|
|
218
|
+
"record_dependency",
|
|
219
|
+
];
|
|
220
|
+
export const FRONTEND_PLAN_TERMINAL_TOOL_NAMES = ["finalize_plan"];
|
|
221
|
+
export const FRONTEND_PLAN_ADOPT_TOOL_NAMES = ["adopt_staged_fact"];
|
|
222
|
+
/** M5: `frontend-review-pi` emits its authoritative terminal verdict through
|
|
223
|
+
* committed typed tools instead of the legacy JSON verdict parse. */
|
|
224
|
+
export function isFrontendReviewTypedTerminalNode(task) {
|
|
225
|
+
return task.id === "frontend-review-pi";
|
|
226
|
+
}
|
|
227
|
+
/** M8: `frontend-design-review-pi` emits its authoritative terminal verdict
|
|
228
|
+
* through committed typed tools (`approve_design` / `request_design_changes`)
|
|
229
|
+
* instead of the legacy first-line `VERDICT: pass|request-revision` text. */
|
|
230
|
+
export function isFrontendDesignTypedTerminalNode(task) {
|
|
231
|
+
return task.id === "frontend-design-review-pi";
|
|
232
|
+
}
|
|
233
|
+
/** A+B: `frontend-contract-pi` submits its contract through incremental typed
|
|
234
|
+
* tools + the `finalize_contract` terminal (origin=contract). */
|
|
235
|
+
export function isFrontendContractTypedNode(task) {
|
|
236
|
+
return task.id === "frontend-contract-pi";
|
|
237
|
+
}
|
|
238
|
+
/** A+B: `frontend-scout-pi` submits target-surface/design-evidence through
|
|
239
|
+
* incremental evidence tools (origin=scout). */
|
|
240
|
+
export function isFrontendScoutEvidenceNode(task) {
|
|
241
|
+
return task.id === "frontend-scout-pi";
|
|
242
|
+
}
|
|
243
|
+
/** A+B: `frontend-plan-pi` records its decision ledger through seven
|
|
244
|
+
* incremental `record_*` tools and closes with `finalize_plan`. */
|
|
245
|
+
export function isFrontendPlanLedgerNode(task) {
|
|
246
|
+
return task.id === "frontend-plan-pi";
|
|
247
|
+
}
|
|
181
248
|
export function resolveDagPiToolNames(task) {
|
|
249
|
+
if (isFrontendReviewTypedTerminalNode(task)) {
|
|
250
|
+
return [
|
|
251
|
+
...DAG_PI_READONLY_TOOLS,
|
|
252
|
+
"approve_review",
|
|
253
|
+
"request_review_changes",
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
if (isFrontendDesignTypedTerminalNode(task)) {
|
|
257
|
+
return [
|
|
258
|
+
...DAG_PI_READONLY_TOOLS,
|
|
259
|
+
"approve_design",
|
|
260
|
+
"request_design_changes",
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
if (isFrontendContractTypedNode(task)) {
|
|
264
|
+
return [
|
|
265
|
+
...DAG_PI_READONLY_TOOLS,
|
|
266
|
+
...FRONTEND_CONTRACT_RECORD_TOOL_NAMES,
|
|
267
|
+
...FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES,
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
if (isFrontendScoutEvidenceNode(task)) {
|
|
271
|
+
return [...DAG_PI_READONLY_TOOLS, ...FRONTEND_SCOUT_EVIDENCE_TOOL_NAMES];
|
|
272
|
+
}
|
|
273
|
+
if (isFrontendPlanLedgerNode(task)) {
|
|
274
|
+
return [
|
|
275
|
+
...DAG_PI_READONLY_TOOLS,
|
|
276
|
+
...FRONTEND_PLAN_RECORD_TOOL_NAMES,
|
|
277
|
+
...FRONTEND_PLAN_TERMINAL_TOOL_NAMES,
|
|
278
|
+
...FRONTEND_PLAN_ADOPT_TOOL_NAMES,
|
|
279
|
+
];
|
|
280
|
+
}
|
|
182
281
|
if (!isDagPiWriteTask(task))
|
|
183
282
|
return [...DAG_PI_READONLY_TOOLS];
|
|
184
283
|
const tools = [...DAG_PI_WRITE_TOOLS];
|
|
@@ -189,20 +288,1157 @@ export function resolveDagPiToolNames(task) {
|
|
|
189
288
|
}
|
|
190
289
|
return tools;
|
|
191
290
|
}
|
|
291
|
+
export const FRONTEND_REVIEW_TERMINAL_TOOL_NAMES = new Set([
|
|
292
|
+
"approve_review",
|
|
293
|
+
"request_review_changes",
|
|
294
|
+
]);
|
|
295
|
+
/**
|
|
296
|
+
* Fail-closed scanner for the two committed review terminal tools. Mirrors
|
|
297
|
+
* M4's `countWriteToolEventsFromSessionEvents` pattern: an unparseable line
|
|
298
|
+
* never counts as a terminal fact, so a corrupt/empty log cannot inflate the
|
|
299
|
+
* typed verdict.
|
|
300
|
+
*/
|
|
301
|
+
export function scanReviewTerminalKindsFromSessionEvents(content) {
|
|
302
|
+
const kinds = [];
|
|
303
|
+
for (const line of content.split("\n")) {
|
|
304
|
+
const trimmed = line.trim();
|
|
305
|
+
if (!trimmed)
|
|
306
|
+
continue;
|
|
307
|
+
let event;
|
|
308
|
+
try {
|
|
309
|
+
event = JSON.parse(trimmed);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (event !== null &&
|
|
315
|
+
typeof event === "object" &&
|
|
316
|
+
event.type === "tool_execution_start" &&
|
|
317
|
+
typeof event.toolName === "string" &&
|
|
318
|
+
FRONTEND_REVIEW_TERMINAL_TOOL_NAMES.has(event.toolName)) {
|
|
319
|
+
kinds.push(event.toolName);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return kinds;
|
|
323
|
+
}
|
|
324
|
+
export const FRONTEND_DESIGN_TERMINAL_TOOL_NAMES = new Set([
|
|
325
|
+
"approve_design",
|
|
326
|
+
"request_design_changes",
|
|
327
|
+
]);
|
|
328
|
+
/**
|
|
329
|
+
* Fail-closed scanner for the two committed design terminal tools. Mirrors the
|
|
330
|
+
* review scanner: an unparseable line never counts as a terminal fact, so a
|
|
331
|
+
* corrupt/empty log cannot inflate the typed verdict.
|
|
332
|
+
*/
|
|
333
|
+
export function scanDesignTerminalKindsFromSessionEvents(content) {
|
|
334
|
+
const kinds = [];
|
|
335
|
+
for (const line of content.split("\n")) {
|
|
336
|
+
const trimmed = line.trim();
|
|
337
|
+
if (!trimmed)
|
|
338
|
+
continue;
|
|
339
|
+
let event;
|
|
340
|
+
try {
|
|
341
|
+
event = JSON.parse(trimmed);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (event !== null &&
|
|
347
|
+
typeof event === "object" &&
|
|
348
|
+
event.type === "tool_execution_start" &&
|
|
349
|
+
typeof event.toolName === "string" &&
|
|
350
|
+
FRONTEND_DESIGN_TERMINAL_TOOL_NAMES.has(event.toolName)) {
|
|
351
|
+
kinds.push(event.toolName);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return kinds;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* M5: build the two committed typed review terminal tools (approve_review /
|
|
358
|
+
* request_review_changes). Each tool validates its parameters with the review
|
|
359
|
+
* fact zod schemas, stages + adopts a terminal fact into the typed event
|
|
360
|
+
* store, and returns a structured receipt. Terminal conflicts (a second
|
|
361
|
+
* terminal commit) are caught inside execute and returned as an error receipt
|
|
362
|
+
* rather than crashing the node.
|
|
363
|
+
*/
|
|
364
|
+
export async function createFrontendReviewTerminalTools(input) {
|
|
365
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
366
|
+
import("typebox"),
|
|
367
|
+
import("@earendil-works/pi-coding-agent"),
|
|
368
|
+
]);
|
|
369
|
+
const { approveReviewFactSchema, readCommittedEvents, requestReviewChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
370
|
+
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
371
|
+
const store = input.store;
|
|
372
|
+
const attemptId = input.attemptId;
|
|
373
|
+
const findingSchema = Type.Object({
|
|
374
|
+
severity: Type.String({
|
|
375
|
+
description: "Critical | Important | Minor | Info",
|
|
376
|
+
}),
|
|
377
|
+
file: Type.Optional(Type.String({})),
|
|
378
|
+
line: Type.Optional(Type.Number({})),
|
|
379
|
+
issue: Type.String({}),
|
|
380
|
+
requiredChange: Type.Optional(Type.String({})),
|
|
381
|
+
}, { additionalProperties: false });
|
|
382
|
+
const approveParameters = Type.Object({
|
|
383
|
+
findings: Type.Array(findingSchema, {
|
|
384
|
+
description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
|
|
385
|
+
}),
|
|
386
|
+
}, { additionalProperties: false });
|
|
387
|
+
const requestParameters = Type.Object({
|
|
388
|
+
issueCategory: Type.Enum({
|
|
389
|
+
"implementation-mismatch": "implementation-mismatch",
|
|
390
|
+
"approved-design-defect": "approved-design-defect",
|
|
391
|
+
"target-surface-defect": "target-surface-defect",
|
|
392
|
+
"contract-requirement-gap": "contract-requirement-gap",
|
|
393
|
+
"unknown": "unknown",
|
|
394
|
+
}, { description: "Typed issue category (five-value enum)" }),
|
|
395
|
+
evidenceRefs: Type.Array(Type.String({}), {
|
|
396
|
+
description: "Evidence refs (paths or artifact ids); at least one",
|
|
397
|
+
}),
|
|
398
|
+
findings: Type.Array(findingSchema, {
|
|
399
|
+
description: "At least one finding",
|
|
400
|
+
}),
|
|
401
|
+
}, { additionalProperties: false });
|
|
402
|
+
async function adoptReviewFact(kind, fact) {
|
|
403
|
+
const requestId = randomUUID();
|
|
404
|
+
try {
|
|
405
|
+
const parsed = kind === "approve_review"
|
|
406
|
+
? approveReviewFactSchema.parse(fact)
|
|
407
|
+
: requestReviewChangesFactSchema.parse(fact);
|
|
408
|
+
const staged = stageTypedEventFact({
|
|
409
|
+
store,
|
|
410
|
+
requestId,
|
|
411
|
+
attemptId,
|
|
412
|
+
fact: parsed,
|
|
413
|
+
});
|
|
414
|
+
const adopted = await adoptTypedEventFact({
|
|
415
|
+
store,
|
|
416
|
+
requestId,
|
|
417
|
+
attemptId,
|
|
418
|
+
fact: parsed,
|
|
419
|
+
eventId: staged.eventId,
|
|
420
|
+
expectedRevision: store.revision,
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: "text",
|
|
426
|
+
text: JSON.stringify({
|
|
427
|
+
ok: true,
|
|
428
|
+
kind,
|
|
429
|
+
eventId: adopted.eventId,
|
|
430
|
+
revision: adopted.revision,
|
|
431
|
+
}),
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
details: {
|
|
435
|
+
ok: true,
|
|
436
|
+
kind,
|
|
437
|
+
eventId: adopted.eventId,
|
|
438
|
+
revision: adopted.revision,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
const code = error?.code;
|
|
444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
445
|
+
return {
|
|
446
|
+
content: [
|
|
447
|
+
{
|
|
448
|
+
type: "text",
|
|
449
|
+
text: JSON.stringify({ ok: false, kind, code, error: message }),
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
details: { ok: false, kind, code, error: message },
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const approveReviewTool = defineTool({
|
|
457
|
+
name: "approve_review",
|
|
458
|
+
label: "approve_review",
|
|
459
|
+
description: "Commit the authoritative approve_review terminal fact. Use only when the implementation passes review with no Critical/Important findings.",
|
|
460
|
+
promptSnippet: "Commit the authoritative approve_review terminal verdict (no Critical/Important findings).",
|
|
461
|
+
parameters: approveParameters,
|
|
462
|
+
async execute(_toolCallId, params) {
|
|
463
|
+
return adoptReviewFact("approve_review", {
|
|
464
|
+
kind: "approve_review",
|
|
465
|
+
verdict: "approve_review",
|
|
466
|
+
findings: params?.findings ?? [],
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
const requestReviewChangesTool = defineTool({
|
|
471
|
+
name: "request_review_changes",
|
|
472
|
+
label: "request_review_changes",
|
|
473
|
+
description: "Commit the authoritative request_review_changes terminal fact. Requires a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
474
|
+
promptSnippet: "Commit the authoritative request_review_changes terminal verdict (issueCategory + evidenceRefs + findings required).",
|
|
475
|
+
parameters: requestParameters,
|
|
476
|
+
async execute(_toolCallId, params) {
|
|
477
|
+
return adoptReviewFact("request_review_changes", {
|
|
478
|
+
kind: "request_review_changes",
|
|
479
|
+
verdict: "request_review_changes",
|
|
480
|
+
issueCategory: params?.issueCategory,
|
|
481
|
+
evidenceRefs: params?.evidenceRefs,
|
|
482
|
+
findings: params?.findings,
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
return {
|
|
487
|
+
customTools: [approveReviewTool, requestReviewChangesTool],
|
|
488
|
+
flush: async () => {
|
|
489
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
490
|
+
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "review-typed-facts.jsonl"), committed);
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* M8: build the two committed typed design terminal tools (approve_design /
|
|
496
|
+
* request_design_changes). Each tool validates its parameters with the design
|
|
497
|
+
* fact zod schemas, stages + adopts a terminal fact into the typed event
|
|
498
|
+
* store, and returns a structured receipt. Terminal conflicts are caught
|
|
499
|
+
* inside execute and returned as an error receipt rather than crashing the
|
|
500
|
+
* node.
|
|
501
|
+
*/
|
|
502
|
+
export async function createFrontendDesignTerminalTools(input) {
|
|
503
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
504
|
+
import("typebox"),
|
|
505
|
+
import("@earendil-works/pi-coding-agent"),
|
|
506
|
+
]);
|
|
507
|
+
const { approveDesignFactSchema, readCommittedEvents, requestDesignChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
508
|
+
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
509
|
+
const store = input.store;
|
|
510
|
+
const attemptId = input.attemptId;
|
|
511
|
+
const findingSchema = Type.Object({
|
|
512
|
+
severity: Type.String({
|
|
513
|
+
description: "Critical | Important | Minor | Info",
|
|
514
|
+
}),
|
|
515
|
+
file: Type.Optional(Type.String({})),
|
|
516
|
+
line: Type.Optional(Type.Number({})),
|
|
517
|
+
issue: Type.String({}),
|
|
518
|
+
requiredChange: Type.Optional(Type.String({})),
|
|
519
|
+
}, { additionalProperties: false });
|
|
520
|
+
const approveParameters = Type.Object({
|
|
521
|
+
findings: Type.Array(findingSchema, {
|
|
522
|
+
description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
|
|
523
|
+
}),
|
|
524
|
+
}, { additionalProperties: false });
|
|
525
|
+
const requestParameters = Type.Object({
|
|
526
|
+
issueCategory: Type.Enum({
|
|
527
|
+
"implementation-mismatch": "implementation-mismatch",
|
|
528
|
+
"approved-design-defect": "approved-design-defect",
|
|
529
|
+
"target-surface-defect": "target-surface-defect",
|
|
530
|
+
"contract-requirement-gap": "contract-requirement-gap",
|
|
531
|
+
"unknown": "unknown",
|
|
532
|
+
}, { description: "Typed issue category (five-value enum)" }),
|
|
533
|
+
evidenceRefs: Type.Array(Type.String({}), {
|
|
534
|
+
description: "Evidence refs (paths or artifact ids); at least one",
|
|
535
|
+
}),
|
|
536
|
+
findings: Type.Array(findingSchema, {
|
|
537
|
+
description: "At least one finding",
|
|
538
|
+
}),
|
|
539
|
+
}, { additionalProperties: false });
|
|
540
|
+
async function adoptDesignFact(kind, fact) {
|
|
541
|
+
const requestId = randomUUID();
|
|
542
|
+
try {
|
|
543
|
+
const parsed = kind === "approve_design"
|
|
544
|
+
? approveDesignFactSchema.parse(fact)
|
|
545
|
+
: requestDesignChangesFactSchema.parse(fact);
|
|
546
|
+
const staged = stageTypedEventFact({
|
|
547
|
+
store,
|
|
548
|
+
requestId,
|
|
549
|
+
attemptId,
|
|
550
|
+
fact: parsed,
|
|
551
|
+
});
|
|
552
|
+
const adopted = await adoptTypedEventFact({
|
|
553
|
+
store,
|
|
554
|
+
requestId,
|
|
555
|
+
attemptId,
|
|
556
|
+
fact: parsed,
|
|
557
|
+
eventId: staged.eventId,
|
|
558
|
+
expectedRevision: store.revision,
|
|
559
|
+
});
|
|
560
|
+
return {
|
|
561
|
+
content: [
|
|
562
|
+
{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: JSON.stringify({
|
|
565
|
+
ok: true,
|
|
566
|
+
kind,
|
|
567
|
+
eventId: adopted.eventId,
|
|
568
|
+
revision: adopted.revision,
|
|
569
|
+
}),
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
details: {
|
|
573
|
+
ok: true,
|
|
574
|
+
kind,
|
|
575
|
+
eventId: adopted.eventId,
|
|
576
|
+
revision: adopted.revision,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
const code = error?.code;
|
|
582
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
583
|
+
return {
|
|
584
|
+
content: [
|
|
585
|
+
{
|
|
586
|
+
type: "text",
|
|
587
|
+
text: JSON.stringify({ ok: false, kind, code, error: message }),
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
details: { ok: false, kind, code, error: message },
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const approveDesignTool = defineTool({
|
|
595
|
+
name: "approve_design",
|
|
596
|
+
label: "approve_design",
|
|
597
|
+
description: "Commit the authoritative approve_design terminal fact. Use only when the plan passes design review with no Critical/Important findings.",
|
|
598
|
+
promptSnippet: "Commit the authoritative approve_design terminal verdict (no Critical/Important findings).",
|
|
599
|
+
parameters: approveParameters,
|
|
600
|
+
async execute(_toolCallId, params) {
|
|
601
|
+
return adoptDesignFact("approve_design", {
|
|
602
|
+
kind: "approve_design",
|
|
603
|
+
verdict: "approve_design",
|
|
604
|
+
findings: params?.findings ?? [],
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
const requestDesignChangesTool = defineTool({
|
|
609
|
+
name: "request_design_changes",
|
|
610
|
+
label: "request_design_changes",
|
|
611
|
+
description: "Commit the authoritative request_design_changes terminal fact. Requires a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
612
|
+
promptSnippet: "Commit the authoritative request_design_changes terminal verdict (issueCategory + evidenceRefs + findings required).",
|
|
613
|
+
parameters: requestParameters,
|
|
614
|
+
async execute(_toolCallId, params) {
|
|
615
|
+
return adoptDesignFact("request_design_changes", {
|
|
616
|
+
kind: "request_design_changes",
|
|
617
|
+
verdict: "request_design_changes",
|
|
618
|
+
issueCategory: params?.issueCategory,
|
|
619
|
+
evidenceRefs: params?.evidenceRefs,
|
|
620
|
+
findings: params?.findings,
|
|
621
|
+
});
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
return {
|
|
625
|
+
customTools: [approveDesignTool, requestDesignChangesTool],
|
|
626
|
+
flush: async () => {
|
|
627
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
628
|
+
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "design-typed-facts.jsonl"), committed);
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* A+B: `frontend-plan-pi` now records its decision ledger through seven
|
|
634
|
+
* incremental `record_*` tools (origin=plan) and closes with exactly one
|
|
635
|
+
* `finalize_plan` terminal. A later attempt may explicitly adopt a quarantined
|
|
636
|
+
* fact via `adopt_staged_fact`. Flush writes `plan-typed-facts.jsonl` for the
|
|
637
|
+
* node validator / compile authority.
|
|
638
|
+
*/
|
|
639
|
+
export async function createFrontendPlanLedgerTools(input) {
|
|
640
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
641
|
+
import("typebox"),
|
|
642
|
+
import("@earendil-works/pi-coding-agent"),
|
|
643
|
+
]);
|
|
644
|
+
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
645
|
+
const { adoptStagedFact, adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
646
|
+
const { assemblePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
647
|
+
const store = input.store;
|
|
648
|
+
const attemptId = input.attemptId;
|
|
649
|
+
const stringArray = Type.Array(Type.String({}));
|
|
650
|
+
const optionalString = Type.Optional(Type.String({}));
|
|
651
|
+
const optionalStringArray = Type.Optional(stringArray);
|
|
652
|
+
const requirementSchema = Type.Object({
|
|
653
|
+
id: Type.String({}),
|
|
654
|
+
expectedOutcome: Type.String({}),
|
|
655
|
+
implementationTargets: stringArray,
|
|
656
|
+
verificationTargetIds: stringArray,
|
|
657
|
+
evidenceGap: Type.Optional(Type.Object({
|
|
658
|
+
requirementId: optionalString,
|
|
659
|
+
description: Type.String({}),
|
|
660
|
+
blocking: Type.Boolean(),
|
|
661
|
+
}, { additionalProperties: false })),
|
|
662
|
+
}, { additionalProperties: false });
|
|
663
|
+
const uiStateSchema = Type.Object({
|
|
664
|
+
name: Type.String({}),
|
|
665
|
+
applicable: Type.Boolean(),
|
|
666
|
+
expectedBehavior: optionalString,
|
|
667
|
+
implementationTargets: optionalStringArray,
|
|
668
|
+
verificationTargetIds: optionalStringArray,
|
|
669
|
+
notApplicableReason: optionalString,
|
|
670
|
+
}, { additionalProperties: false });
|
|
671
|
+
const interactionSchema = Type.Object({
|
|
672
|
+
name: Type.String({}),
|
|
673
|
+
trigger: Type.String({}),
|
|
674
|
+
expectedBehavior: Type.String({}),
|
|
675
|
+
implementationTargets: stringArray,
|
|
676
|
+
verificationTargetIds: stringArray,
|
|
677
|
+
}, { additionalProperties: false });
|
|
678
|
+
const mockEndpointSchema = Type.Object({
|
|
679
|
+
method: Type.String({
|
|
680
|
+
description: "GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS",
|
|
681
|
+
}),
|
|
682
|
+
path: Type.String({}),
|
|
683
|
+
fixture: optionalString,
|
|
684
|
+
consumer: optionalString,
|
|
685
|
+
}, { additionalProperties: false });
|
|
686
|
+
const mockApiSchema = Type.Object({
|
|
687
|
+
strategy: Type.String({
|
|
688
|
+
description: "native | browser-intercept | request-adapter | not-needed",
|
|
689
|
+
}),
|
|
690
|
+
activation: Type.String({}),
|
|
691
|
+
endpoints: Type.Array(mockEndpointSchema),
|
|
692
|
+
}, { additionalProperties: false });
|
|
693
|
+
const designEvidenceSchema = Type.Object({
|
|
694
|
+
source: Type.String({}),
|
|
695
|
+
paths: stringArray,
|
|
696
|
+
conflicts: stringArray,
|
|
697
|
+
}, { additionalProperties: false });
|
|
698
|
+
const verificationTargetSchema = Type.Object({
|
|
699
|
+
id: Type.String({}),
|
|
700
|
+
type: Type.String({
|
|
701
|
+
description: "static | unit | component | integration | mock",
|
|
702
|
+
}),
|
|
703
|
+
commandLabel: Type.String({}),
|
|
704
|
+
file: Type.String({}),
|
|
705
|
+
symbol: Type.Optional(Type.String({
|
|
706
|
+
description: "Real exported identifier or describe/it title in the target file. Forbidden for type=static (runtime strips it); required for non-static targets.",
|
|
707
|
+
})),
|
|
708
|
+
requirementIds: stringArray,
|
|
709
|
+
uiStates: stringArray,
|
|
710
|
+
}, { additionalProperties: false });
|
|
711
|
+
const evidenceGapSchema = Type.Object({
|
|
712
|
+
requirementId: optionalString,
|
|
713
|
+
description: Type.String({}),
|
|
714
|
+
blocking: Type.Boolean(),
|
|
715
|
+
}, { additionalProperties: false });
|
|
716
|
+
const uiComponentChoiceSchema = Type.Object({
|
|
717
|
+
purpose: Type.String({}),
|
|
718
|
+
component: Type.String({}),
|
|
719
|
+
decision: Type.String({
|
|
720
|
+
description: "specified | reuse-existing | new",
|
|
721
|
+
}),
|
|
722
|
+
specReference: Type.Optional(Type.Object({
|
|
723
|
+
path: Type.String({}),
|
|
724
|
+
section: Type.String({}),
|
|
725
|
+
line: Type.Optional(Type.Number({})),
|
|
726
|
+
}, { additionalProperties: false })),
|
|
727
|
+
rationale: Type.String({}),
|
|
728
|
+
}, { additionalProperties: false });
|
|
729
|
+
const targetsSchema = Type.Object({
|
|
730
|
+
routes: optionalStringArray,
|
|
731
|
+
publicApiChanges: optionalStringArray,
|
|
732
|
+
}, { additionalProperties: false });
|
|
733
|
+
const stringList = (value) => Array.isArray(value)
|
|
734
|
+
? value.filter((item) => typeof item === "string")
|
|
735
|
+
: [];
|
|
736
|
+
const planToolReceipt = (details) => ({
|
|
737
|
+
content: [
|
|
738
|
+
{
|
|
739
|
+
type: "text",
|
|
740
|
+
text: JSON.stringify(details),
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
details,
|
|
744
|
+
});
|
|
745
|
+
async function adoptPlanFact(kind, requestId, fact) {
|
|
746
|
+
// A+B (AC-005): a provider-capability fact kind reaching the plan ledger
|
|
747
|
+
// is out of route — it belongs to the shadow provider capability channel,
|
|
748
|
+
// not the plan decision ledger. Route it through the frozen seven-kind
|
|
749
|
+
// matrix and fail closed to `unsupported-provider-capability` instead of
|
|
750
|
+
// silently widening the plan catalog.
|
|
751
|
+
const capabilityRoute = routeFrontendProviderCapability({ factKind: kind });
|
|
752
|
+
if (capabilityRoute.ok) {
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
kind,
|
|
756
|
+
code: "unsupported-provider-capability",
|
|
757
|
+
error: `plan ledger cannot adopt provider capability fact kind: ${kind}`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
const staged = stageTypedEventFact({
|
|
762
|
+
store,
|
|
763
|
+
requestId,
|
|
764
|
+
attemptId,
|
|
765
|
+
fact: fact,
|
|
766
|
+
});
|
|
767
|
+
const committed = await adoptTypedEventFact({
|
|
768
|
+
store,
|
|
769
|
+
requestId,
|
|
770
|
+
attemptId,
|
|
771
|
+
fact: fact,
|
|
772
|
+
eventId: staged.eventId,
|
|
773
|
+
expectedRevision: store.revision,
|
|
774
|
+
});
|
|
775
|
+
return {
|
|
776
|
+
ok: true,
|
|
777
|
+
kind,
|
|
778
|
+
eventId: committed.eventId,
|
|
779
|
+
revision: committed.revision,
|
|
780
|
+
error: "",
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
return {
|
|
785
|
+
ok: false,
|
|
786
|
+
kind,
|
|
787
|
+
code: error?.code,
|
|
788
|
+
error: error instanceof Error ? error.message : String(error),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const recordTargetSurfaceTool = defineTool({
|
|
793
|
+
name: "record_target_surface",
|
|
794
|
+
label: "record_target_surface",
|
|
795
|
+
description: "Record the plan target surface (routes + files) as an origin=plan target-surface fact.",
|
|
796
|
+
promptSnippet: "Record the plan target surface fact.",
|
|
797
|
+
parameters: Type.Object({ targets: Type.Optional(targetsSchema), files: stringArray }, { additionalProperties: false }),
|
|
798
|
+
async execute(_toolCallId, params) {
|
|
799
|
+
const routes = stringList(params?.targets?.routes);
|
|
800
|
+
const files = stringList(params?.files);
|
|
801
|
+
const result = await adoptPlanFact("target-surface", `${attemptId}:record_target_surface:${randomUUID()}`, { kind: "target-surface", origin: "plan", files, routes });
|
|
802
|
+
return planToolReceipt(result);
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
const recordComponentChoiceTool = defineTool({
|
|
806
|
+
name: "record_component_choice",
|
|
807
|
+
label: "record_component_choice",
|
|
808
|
+
description: "Record component choices and styling strategy as an origin=plan component-choice fact.",
|
|
809
|
+
promptSnippet: "Record the plan component-choice fact.",
|
|
810
|
+
parameters: Type.Object({
|
|
811
|
+
uiComponentChoices: Type.Optional(Type.Array(uiComponentChoiceSchema)),
|
|
812
|
+
stylingStrategy: optionalString,
|
|
813
|
+
}, { additionalProperties: false }),
|
|
814
|
+
async execute(_toolCallId, params) {
|
|
815
|
+
const uiComponentChoices = Array.isArray(params?.uiComponentChoices)
|
|
816
|
+
? params.uiComponentChoices
|
|
817
|
+
: [];
|
|
818
|
+
const components = uiComponentChoices
|
|
819
|
+
.map((choice) => typeof choice?.component === "string" ? choice.component : "")
|
|
820
|
+
.filter(Boolean);
|
|
821
|
+
const result = await adoptPlanFact("component-choice", `${attemptId}:record_component_choice:${randomUUID()}`, {
|
|
822
|
+
kind: "component-choice",
|
|
823
|
+
origin: "plan",
|
|
824
|
+
components,
|
|
825
|
+
uiComponentChoices,
|
|
826
|
+
...(params?.stylingStrategy
|
|
827
|
+
? { stylingStrategy: params.stylingStrategy }
|
|
828
|
+
: {}),
|
|
829
|
+
});
|
|
830
|
+
return planToolReceipt(result);
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
const recordStateFlowTool = defineTool({
|
|
834
|
+
name: "record_state_flow",
|
|
835
|
+
label: "record_state_flow",
|
|
836
|
+
description: "Record UI states and interactions as an origin=plan state-flow fact.",
|
|
837
|
+
promptSnippet: "Record the plan state-flow fact.",
|
|
838
|
+
parameters: Type.Object({
|
|
839
|
+
uiStates: Type.Array(uiStateSchema),
|
|
840
|
+
interactions: Type.Array(interactionSchema),
|
|
841
|
+
}, { additionalProperties: false }),
|
|
842
|
+
async execute(_toolCallId, params) {
|
|
843
|
+
const uiStates = Array.isArray(params?.uiStates) ? params.uiStates : [];
|
|
844
|
+
const interactions = Array.isArray(params?.interactions)
|
|
845
|
+
? params.interactions
|
|
846
|
+
: [];
|
|
847
|
+
const states = uiStates
|
|
848
|
+
.map((state) => (typeof state?.name === "string" ? state.name : ""))
|
|
849
|
+
.filter(Boolean);
|
|
850
|
+
const result = await adoptPlanFact("state-flow", `${attemptId}:record_state_flow:${randomUUID()}`, {
|
|
851
|
+
kind: "state-flow",
|
|
852
|
+
origin: "plan",
|
|
853
|
+
states,
|
|
854
|
+
uiStates,
|
|
855
|
+
interactions,
|
|
856
|
+
});
|
|
857
|
+
return planToolReceipt(result);
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
const recordDataFlowTool = defineTool({
|
|
861
|
+
name: "record_data_flow",
|
|
862
|
+
label: "record_data_flow",
|
|
863
|
+
description: "Record interaction/endpoint data flow as an origin=plan data-flow fact.",
|
|
864
|
+
promptSnippet: "Record the plan data-flow fact.",
|
|
865
|
+
parameters: Type.Object({ interactions: stringArray, endpoints: stringArray }, { additionalProperties: false }),
|
|
866
|
+
async execute(_toolCallId, params) {
|
|
867
|
+
const result = await adoptPlanFact("data-flow", `${attemptId}:record_data_flow:${randomUUID()}`, {
|
|
868
|
+
kind: "data-flow",
|
|
869
|
+
origin: "plan",
|
|
870
|
+
interactions: stringList(params?.interactions),
|
|
871
|
+
endpoints: stringList(params?.endpoints),
|
|
872
|
+
});
|
|
873
|
+
return planToolReceipt(result);
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
const recordMockApiTool = defineTool({
|
|
877
|
+
name: "record_mock_api",
|
|
878
|
+
label: "record_mock_api",
|
|
879
|
+
description: "Record the Mock/API strategy as an origin=plan mock-api fact.",
|
|
880
|
+
promptSnippet: "Record the plan mock-api fact.",
|
|
881
|
+
parameters: Type.Object({ mockApi: mockApiSchema }, { additionalProperties: false }),
|
|
882
|
+
async execute(_toolCallId, params) {
|
|
883
|
+
const mockApi = params?.mockApi ?? {
|
|
884
|
+
strategy: "not-needed",
|
|
885
|
+
activation: "",
|
|
886
|
+
endpoints: [],
|
|
887
|
+
};
|
|
888
|
+
const endpoints = stringList((Array.isArray(mockApi?.endpoints) ? mockApi.endpoints : []).map((endpoint) => `${typeof endpoint?.method === "string" ? endpoint.method : ""} ${typeof endpoint?.path === "string" ? endpoint.path : ""}`.trim()));
|
|
889
|
+
const result = await adoptPlanFact("mock-api", `${attemptId}:record_mock_api:${randomUUID()}`, {
|
|
890
|
+
kind: "mock-api",
|
|
891
|
+
origin: "plan",
|
|
892
|
+
strategy: typeof mockApi?.strategy === "string" ? mockApi.strategy : "",
|
|
893
|
+
endpoints,
|
|
894
|
+
mockApi,
|
|
895
|
+
});
|
|
896
|
+
return planToolReceipt(result);
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
const recordDesignDeviationTool = defineTool({
|
|
900
|
+
name: "record_design_deviation",
|
|
901
|
+
label: "record_design_deviation",
|
|
902
|
+
description: "Record design evidence conflicts as an origin=plan design-deviation fact.",
|
|
903
|
+
promptSnippet: "Record the plan design-deviation fact.",
|
|
904
|
+
parameters: Type.Object({ designEvidence: designEvidenceSchema }, { additionalProperties: false }),
|
|
905
|
+
async execute(_toolCallId, params) {
|
|
906
|
+
const conflicts = stringList(params?.designEvidence?.conflicts);
|
|
907
|
+
const result = await adoptPlanFact("design-deviation", `${attemptId}:record_design_deviation:${randomUUID()}`, { kind: "design-deviation", origin: "plan", conflicts });
|
|
908
|
+
return planToolReceipt(result);
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
const recordDependencyTool = defineTool({
|
|
912
|
+
name: "record_dependency",
|
|
913
|
+
label: "record_dependency",
|
|
914
|
+
description: "Record the dependency policy as an origin=plan dependency fact.",
|
|
915
|
+
promptSnippet: "Record the plan dependency fact.",
|
|
916
|
+
parameters: Type.Object({ policy: Type.String({}) }, { additionalProperties: false }),
|
|
917
|
+
async execute(_toolCallId, params) {
|
|
918
|
+
const result = await adoptPlanFact("dependency", `${attemptId}:record_dependency:${randomUUID()}`, {
|
|
919
|
+
kind: "dependency",
|
|
920
|
+
origin: "plan",
|
|
921
|
+
policy: typeof params?.policy === "string" ? params.policy : "",
|
|
922
|
+
});
|
|
923
|
+
return planToolReceipt(result);
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
const finalizePlanTool = defineTool({
|
|
927
|
+
name: "finalize_plan",
|
|
928
|
+
label: "finalize_plan",
|
|
929
|
+
description: "Assemble the canonical editable plan patch from committed record facts plus these remaining fields, publish it on a target-surface fact, and commit the finalize_plan terminal. Call exactly once.",
|
|
930
|
+
promptSnippet: "Assemble and commit the finalize_plan terminal (requirements + verificationTargets + evidenceGaps + optional fields).",
|
|
931
|
+
parameters: Type.Object({
|
|
932
|
+
requirements: Type.Array(requirementSchema),
|
|
933
|
+
verificationTargets: Type.Array(verificationTargetSchema),
|
|
934
|
+
evidenceGaps: Type.Array(evidenceGapSchema),
|
|
935
|
+
implementationSteps: optionalStringArray,
|
|
936
|
+
residualRisks: optionalStringArray,
|
|
937
|
+
realIntegrationGap: optionalString,
|
|
938
|
+
}, { additionalProperties: false }),
|
|
939
|
+
async execute(_toolCallId, params) {
|
|
940
|
+
try {
|
|
941
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
942
|
+
const fragment = assemblePlanPatchFromCommittedFacts(committed) ?? {};
|
|
943
|
+
const patch = {
|
|
944
|
+
...fragment,
|
|
945
|
+
requirements: params?.requirements ?? [],
|
|
946
|
+
verificationTargets: params?.verificationTargets ?? [],
|
|
947
|
+
evidenceGaps: params?.evidenceGaps ?? [],
|
|
948
|
+
...(params?.implementationSteps
|
|
949
|
+
? { implementationSteps: params.implementationSteps }
|
|
950
|
+
: {}),
|
|
951
|
+
...(params?.residualRisks
|
|
952
|
+
? { residualRisks: params.residualRisks }
|
|
953
|
+
: {}),
|
|
954
|
+
...(params?.realIntegrationGap
|
|
955
|
+
? { realIntegrationGap: params.realIntegrationGap }
|
|
956
|
+
: {}),
|
|
957
|
+
};
|
|
958
|
+
const patchResult = await adoptPlanFact("target-surface", `${attemptId}:finalize_plan:patch:${randomUUID()}`, { kind: "target-surface", origin: "plan", patch });
|
|
959
|
+
if (!patchResult.ok) {
|
|
960
|
+
return planToolReceipt({
|
|
961
|
+
ok: false,
|
|
962
|
+
kind: "finalize_plan",
|
|
963
|
+
error: patchResult.error,
|
|
964
|
+
code: patchResult.code,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
const terminal = await adoptPlanFact("finalize_plan", `${attemptId}:finalize_plan:terminal:${randomUUID()}`, { kind: "finalize_plan", origin: "plan", patch });
|
|
968
|
+
return planToolReceipt(terminal);
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
return planToolReceipt({
|
|
972
|
+
ok: false,
|
|
973
|
+
kind: "finalize_plan",
|
|
974
|
+
error: error instanceof Error ? error.message : String(error),
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
const adoptStagedFactTool = defineTool({
|
|
980
|
+
name: "adopt_staged_fact",
|
|
981
|
+
label: "adopt_staged_fact",
|
|
982
|
+
description: "Explicitly adopt a quarantined fact from a prior attempt into the committed ledger (idempotent by requestId).",
|
|
983
|
+
promptSnippet: "Adopt a quarantined fact into the committed ledger.",
|
|
984
|
+
parameters: Type.Object({
|
|
985
|
+
requestId: Type.String({}),
|
|
986
|
+
eventId: Type.String({}),
|
|
987
|
+
expectedRevision: Type.Number({}),
|
|
988
|
+
}, { additionalProperties: false }),
|
|
989
|
+
async execute(_toolCallId, params) {
|
|
990
|
+
try {
|
|
991
|
+
const adopted = await adoptStagedFact({
|
|
992
|
+
store,
|
|
993
|
+
requestId: typeof params?.requestId === "string" ? params.requestId : "",
|
|
994
|
+
attemptId,
|
|
995
|
+
eventId: typeof params?.eventId === "string" ? params.eventId : "",
|
|
996
|
+
expectedRevision: typeof params?.expectedRevision === "number"
|
|
997
|
+
? params.expectedRevision
|
|
998
|
+
: store.revision,
|
|
999
|
+
});
|
|
1000
|
+
return planToolReceipt({
|
|
1001
|
+
ok: true,
|
|
1002
|
+
kind: "adopt_staged_fact",
|
|
1003
|
+
eventId: adopted.eventId,
|
|
1004
|
+
revision: adopted.revision,
|
|
1005
|
+
error: "",
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
return planToolReceipt({
|
|
1010
|
+
ok: false,
|
|
1011
|
+
kind: "adopt_staged_fact",
|
|
1012
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1013
|
+
code: error?.code,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
customTools: [
|
|
1020
|
+
recordTargetSurfaceTool,
|
|
1021
|
+
recordComponentChoiceTool,
|
|
1022
|
+
recordStateFlowTool,
|
|
1023
|
+
recordDataFlowTool,
|
|
1024
|
+
recordMockApiTool,
|
|
1025
|
+
recordDesignDeviationTool,
|
|
1026
|
+
recordDependencyTool,
|
|
1027
|
+
adoptStagedFactTool,
|
|
1028
|
+
finalizePlanTool,
|
|
1029
|
+
],
|
|
1030
|
+
flush: async () => {
|
|
1031
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
1032
|
+
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), committed);
|
|
1033
|
+
},
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function isRecordObject(value) {
|
|
1037
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1038
|
+
}
|
|
1039
|
+
/** A contract fact is source-mapped when it carries a non-empty `sourceSpan`
|
|
1040
|
+
* (object or string), a non-empty `sourceRefs` array, or a non-empty `source`
|
|
1041
|
+
* string. Anything else is an unmapped source segment (AC-001). */
|
|
1042
|
+
function contractFactSourceSpan(fact) {
|
|
1043
|
+
if (isRecordObject(fact.sourceSpan) || typeof fact.sourceSpan === "string") {
|
|
1044
|
+
return fact.sourceSpan;
|
|
1045
|
+
}
|
|
1046
|
+
if (Array.isArray(fact.sourceRefs) && fact.sourceRefs.length > 0) {
|
|
1047
|
+
return fact.sourceRefs;
|
|
1048
|
+
}
|
|
1049
|
+
if (typeof fact.source === "string" && fact.source.trim().length > 0) {
|
|
1050
|
+
return fact.source;
|
|
1051
|
+
}
|
|
1052
|
+
return undefined;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* A+B (AC-001): deterministically assemble the read-only `frontend-task-contract-vNext`
|
|
1056
|
+
* audit artifact from committed contract facts. It never rewrites the original
|
|
1057
|
+
* task source: unmapped requirement/constraint segments are listed explicitly
|
|
1058
|
+
* so Plan/Implement/Review keep binding to the raw source, not the model's
|
|
1059
|
+
* summarized contract. The blocked disposition is projected through the frozen
|
|
1060
|
+
* `mapContractBlockedOwner` mapping (AC-002).
|
|
1061
|
+
*/
|
|
1062
|
+
export function buildFrontendTaskContractVNext(records) {
|
|
1063
|
+
const facts = records
|
|
1064
|
+
.filter((record) => record.phase === "committed")
|
|
1065
|
+
.map((record) => record.fact)
|
|
1066
|
+
.filter(isRecordObject);
|
|
1067
|
+
const finalized = facts.find((fact) => fact.kind === "contract-finalized");
|
|
1068
|
+
const disposition = typeof finalized?.disposition === "string" ? finalized.disposition : null;
|
|
1069
|
+
const blockingOwner = typeof finalized?.blockingOwner === "string"
|
|
1070
|
+
? finalized.blockingOwner
|
|
1071
|
+
: null;
|
|
1072
|
+
const projected = mapContractBlockedOwner({
|
|
1073
|
+
disposition: disposition ?? "",
|
|
1074
|
+
blockingOwner: blockingOwner ?? undefined,
|
|
1075
|
+
});
|
|
1076
|
+
const byKind = (kind) => facts.filter((fact) => fact.kind === kind);
|
|
1077
|
+
const requirements = byKind("requirement");
|
|
1078
|
+
const unmappedSourceSegments = requirements
|
|
1079
|
+
.filter((fact) => contractFactSourceSpan(fact) === undefined)
|
|
1080
|
+
.map((fact) => ({
|
|
1081
|
+
kind: "requirement",
|
|
1082
|
+
id: typeof fact.id === "string"
|
|
1083
|
+
? fact.id
|
|
1084
|
+
: typeof fact.text === "string"
|
|
1085
|
+
? fact.text
|
|
1086
|
+
: undefined,
|
|
1087
|
+
}))
|
|
1088
|
+
.filter((segment) => segment.id !== undefined);
|
|
1089
|
+
return {
|
|
1090
|
+
schemaVersion: 1,
|
|
1091
|
+
schemaId: "frontend-task-contract-vNext",
|
|
1092
|
+
disposition,
|
|
1093
|
+
blockingOwner,
|
|
1094
|
+
blockedOwner: projected === "not-blocked" ? null : projected,
|
|
1095
|
+
requirements,
|
|
1096
|
+
constraints: byKind("constraint"),
|
|
1097
|
+
evidenceExpectations: byKind("evidence-expectation"),
|
|
1098
|
+
handoffIntents: byKind("handoff-intent"),
|
|
1099
|
+
openQuestions: byKind("open-question"),
|
|
1100
|
+
splitProposals: byKind("split-proposal"),
|
|
1101
|
+
unmappedSourceSegments,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* A+B: `frontend-contract-pi` incremental contract tools. Six `record_*` tools
|
|
1106
|
+
* commit origin=contract facts and `finalize_contract` commits the terminal
|
|
1107
|
+
* disposition (ready | ready-with-assumptions | blocked + blockingOwner).
|
|
1108
|
+
*/
|
|
1109
|
+
export async function createFrontendContractTools(input) {
|
|
1110
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
1111
|
+
import("typebox"),
|
|
1112
|
+
import("@earendil-works/pi-coding-agent"),
|
|
1113
|
+
]);
|
|
1114
|
+
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1115
|
+
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
1116
|
+
const store = input.store;
|
|
1117
|
+
const attemptId = input.attemptId;
|
|
1118
|
+
const receipt = (details) => ({
|
|
1119
|
+
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
1120
|
+
details,
|
|
1121
|
+
});
|
|
1122
|
+
async function adoptContractFact(kind, fact) {
|
|
1123
|
+
try {
|
|
1124
|
+
const requestId = `${attemptId}:${kind}:${randomUUID()}`;
|
|
1125
|
+
const staged = stageTypedEventFact({
|
|
1126
|
+
store,
|
|
1127
|
+
requestId,
|
|
1128
|
+
attemptId,
|
|
1129
|
+
fact: fact,
|
|
1130
|
+
});
|
|
1131
|
+
const committed = await adoptTypedEventFact({
|
|
1132
|
+
store,
|
|
1133
|
+
requestId,
|
|
1134
|
+
attemptId,
|
|
1135
|
+
fact: fact,
|
|
1136
|
+
eventId: staged.eventId,
|
|
1137
|
+
expectedRevision: store.revision,
|
|
1138
|
+
});
|
|
1139
|
+
return {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
kind,
|
|
1142
|
+
eventId: committed.eventId,
|
|
1143
|
+
revision: committed.revision,
|
|
1144
|
+
error: "",
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
return {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
kind,
|
|
1151
|
+
code: error?.code,
|
|
1152
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const recordKinds = {
|
|
1157
|
+
record_requirement: "requirement",
|
|
1158
|
+
record_constraint: "constraint",
|
|
1159
|
+
record_evidence_expectation: "evidence-expectation",
|
|
1160
|
+
record_handoff_intent: "handoff-intent",
|
|
1161
|
+
record_open_question: "open-question",
|
|
1162
|
+
record_split_proposal: "split-proposal",
|
|
1163
|
+
};
|
|
1164
|
+
const recordTools = Object.entries(recordKinds).map(([name, kind]) => defineTool({
|
|
1165
|
+
name,
|
|
1166
|
+
label: name,
|
|
1167
|
+
description: `Commit an origin=contract ${kind} fact.`,
|
|
1168
|
+
promptSnippet: `Commit an origin=contract ${kind} fact.`,
|
|
1169
|
+
parameters: Type.Object({}, { additionalProperties: true }),
|
|
1170
|
+
async execute(_toolCallId, params) {
|
|
1171
|
+
const result = await adoptContractFact(kind, {
|
|
1172
|
+
kind,
|
|
1173
|
+
origin: "contract",
|
|
1174
|
+
...(params ?? {}),
|
|
1175
|
+
});
|
|
1176
|
+
return receipt(result);
|
|
1177
|
+
},
|
|
1178
|
+
}));
|
|
1179
|
+
// OpenSpec selection committed as individual typed facts (one path per
|
|
1180
|
+
// call) so a large candidate set never exceeds a single model output
|
|
1181
|
+
// budget: each tool call carries exactly one {path, disposition,
|
|
1182
|
+
// rationale} row and the ledger accumulates them across calls. Only
|
|
1183
|
+
// positive classifications (required | relevant) are legal; unmentioned
|
|
1184
|
+
// candidates default to irrelevant at the prewrite gate.
|
|
1185
|
+
const recordOpenspecSelectionTool = defineTool({
|
|
1186
|
+
name: "record_openspec_selection",
|
|
1187
|
+
label: "record_openspec_selection",
|
|
1188
|
+
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.",
|
|
1189
|
+
promptSnippet: "Commit one OpenSpec candidate classification (required | relevant); one path per call; skip irrelevant candidates.",
|
|
1190
|
+
parameters: Type.Object({
|
|
1191
|
+
path: Type.String({
|
|
1192
|
+
description: "Repo-relative candidate spec path, e.g. openspec/project-specs/ui/ucp-components-md/AdvancedSearch.md",
|
|
1193
|
+
}),
|
|
1194
|
+
disposition: Type.Enum({
|
|
1195
|
+
required: "required",
|
|
1196
|
+
relevant: "relevant",
|
|
1197
|
+
}),
|
|
1198
|
+
rationale: Type.String({}),
|
|
1199
|
+
}, { additionalProperties: false }),
|
|
1200
|
+
async execute(_toolCallId, params) {
|
|
1201
|
+
const path = typeof params?.path === "string" ? params.path : "";
|
|
1202
|
+
const disposition = params?.disposition;
|
|
1203
|
+
const rationale = typeof params?.rationale === "string" ? params.rationale : "";
|
|
1204
|
+
if (!path || !disposition || !rationale.trim()) {
|
|
1205
|
+
return receipt({
|
|
1206
|
+
ok: false,
|
|
1207
|
+
kind: "openspec-selection",
|
|
1208
|
+
error: "record_openspec_selection requires non-empty path, disposition (required|relevant), and rationale",
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
const result = await adoptContractFact("openspec-selection", {
|
|
1212
|
+
kind: "openspec-selection",
|
|
1213
|
+
origin: "contract",
|
|
1214
|
+
path,
|
|
1215
|
+
disposition,
|
|
1216
|
+
rationale,
|
|
1217
|
+
});
|
|
1218
|
+
return receipt(result);
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
const finalizeContractTool = defineTool({
|
|
1222
|
+
name: "finalize_contract",
|
|
1223
|
+
label: "finalize_contract",
|
|
1224
|
+
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner). Call exactly once.",
|
|
1225
|
+
promptSnippet: "Commit the contract-finalized terminal (disposition + optional blockingOwner).",
|
|
1226
|
+
parameters: Type.Object({
|
|
1227
|
+
disposition: Type.Enum({
|
|
1228
|
+
ready: "ready",
|
|
1229
|
+
"ready-with-assumptions": "ready-with-assumptions",
|
|
1230
|
+
blocked: "blocked",
|
|
1231
|
+
}),
|
|
1232
|
+
blockingOwner: Type.Optional(Type.Enum({
|
|
1233
|
+
"blocked-human": "blocked-human",
|
|
1234
|
+
"blocked-external": "blocked-external",
|
|
1235
|
+
})),
|
|
1236
|
+
assumptions: Type.Optional(Type.Array(Type.String({}))),
|
|
1237
|
+
summary: Type.Optional(Type.String({})),
|
|
1238
|
+
}, { additionalProperties: false }),
|
|
1239
|
+
async execute(_toolCallId, params) {
|
|
1240
|
+
const disposition = params?.disposition;
|
|
1241
|
+
const blockingOwner = params?.blockingOwner;
|
|
1242
|
+
if (disposition === "blocked" && !blockingOwner) {
|
|
1243
|
+
return receipt({
|
|
1244
|
+
ok: false,
|
|
1245
|
+
kind: "finalize_contract",
|
|
1246
|
+
error: "blocked disposition requires blockingOwner (blocked-human | blocked-external)",
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
const blockedOwner = mapContractBlockedOwner({
|
|
1250
|
+
disposition: disposition ?? "",
|
|
1251
|
+
blockingOwner,
|
|
1252
|
+
});
|
|
1253
|
+
const result = await adoptContractFact("contract-finalized", {
|
|
1254
|
+
kind: "contract-finalized",
|
|
1255
|
+
origin: "contract",
|
|
1256
|
+
disposition,
|
|
1257
|
+
...(blockingOwner ? { blockingOwner } : {}),
|
|
1258
|
+
...(blockedOwner !== "not-blocked" ? { blockedOwner } : {}),
|
|
1259
|
+
...(Array.isArray(params?.assumptions)
|
|
1260
|
+
? { assumptions: params.assumptions }
|
|
1261
|
+
: {}),
|
|
1262
|
+
...(typeof params?.summary === "string"
|
|
1263
|
+
? { summary: params.summary }
|
|
1264
|
+
: {}),
|
|
1265
|
+
});
|
|
1266
|
+
return receipt(result);
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
return {
|
|
1270
|
+
customTools: [
|
|
1271
|
+
...recordTools,
|
|
1272
|
+
recordOpenspecSelectionTool,
|
|
1273
|
+
finalizeContractTool,
|
|
1274
|
+
],
|
|
1275
|
+
flush: async () => {
|
|
1276
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
1277
|
+
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "contract-typed-facts.jsonl"), committed);
|
|
1278
|
+
await writeJsonAtomic(path.join(input.runDir, input.nodeId, "frontend-task-contract-vNext.json"), buildFrontendTaskContractVNext(committed));
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* A+B: `frontend-scout-pi` incremental evidence tools (origin=scout). Runtime
|
|
1284
|
+
* enriches committed target-surface / design-evidence facts with hash/section/
|
|
1285
|
+
* freshness from real read events; the tool itself never trusts model self-report.
|
|
1286
|
+
*/
|
|
1287
|
+
export async function createFrontendScoutEvidenceTools(input) {
|
|
1288
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
1289
|
+
import("typebox"),
|
|
1290
|
+
import("@earendil-works/pi-coding-agent"),
|
|
1291
|
+
]);
|
|
1292
|
+
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1293
|
+
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
1294
|
+
const store = input.store;
|
|
1295
|
+
const attemptId = input.attemptId;
|
|
1296
|
+
const stringArray = Type.Array(Type.String({}));
|
|
1297
|
+
const optionalString = Type.Optional(Type.String({}));
|
|
1298
|
+
const receipt = (details) => ({
|
|
1299
|
+
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
1300
|
+
details,
|
|
1301
|
+
});
|
|
1302
|
+
// A+B (AC-003): runtime enriches declared scout paths with hash/freshness
|
|
1303
|
+
// from the real filesystem. The model's self-reported path list is never
|
|
1304
|
+
// trusted for content identity; a missing file fails closed to fresh=false
|
|
1305
|
+
// with a zero hash instead of inventing content.
|
|
1306
|
+
const enrichScoutPathEvidence = async (paths) => {
|
|
1307
|
+
if (!input.workspaceRoot)
|
|
1308
|
+
return [];
|
|
1309
|
+
const evidence = [];
|
|
1310
|
+
for (const relative of new Set(paths)) {
|
|
1311
|
+
try {
|
|
1312
|
+
const bytes = await readFile(path.join(input.workspaceRoot, relative));
|
|
1313
|
+
evidence.push({
|
|
1314
|
+
path: relative,
|
|
1315
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1316
|
+
fresh: true,
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return evidence;
|
|
1324
|
+
};
|
|
1325
|
+
async function adoptScoutFact(kind, fact) {
|
|
1326
|
+
try {
|
|
1327
|
+
const requestId = `${attemptId}:${kind}:${randomUUID()}`;
|
|
1328
|
+
const staged = stageTypedEventFact({
|
|
1329
|
+
store,
|
|
1330
|
+
requestId,
|
|
1331
|
+
attemptId,
|
|
1332
|
+
fact: fact,
|
|
1333
|
+
});
|
|
1334
|
+
const committed = await adoptTypedEventFact({
|
|
1335
|
+
store,
|
|
1336
|
+
requestId,
|
|
1337
|
+
attemptId,
|
|
1338
|
+
fact: fact,
|
|
1339
|
+
eventId: staged.eventId,
|
|
1340
|
+
expectedRevision: store.revision,
|
|
1341
|
+
});
|
|
1342
|
+
return {
|
|
1343
|
+
ok: true,
|
|
1344
|
+
kind,
|
|
1345
|
+
eventId: committed.eventId,
|
|
1346
|
+
revision: committed.revision,
|
|
1347
|
+
error: "",
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
return {
|
|
1352
|
+
ok: false,
|
|
1353
|
+
kind,
|
|
1354
|
+
code: error?.code,
|
|
1355
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const recordTargetSurfaceTool = defineTool({
|
|
1360
|
+
name: "record_target_surface",
|
|
1361
|
+
label: "record_target_surface",
|
|
1362
|
+
description: "Commit an origin=scout target-surface fact (entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts).",
|
|
1363
|
+
promptSnippet: "Commit an origin=scout target-surface fact.",
|
|
1364
|
+
parameters: Type.Object({
|
|
1365
|
+
entrypoint: optionalString,
|
|
1366
|
+
routeOrMount: optionalString,
|
|
1367
|
+
implementationPaths: stringArray,
|
|
1368
|
+
testPaths: stringArray,
|
|
1369
|
+
dataSource: optionalString,
|
|
1370
|
+
allowedPathConflicts: stringArray,
|
|
1371
|
+
}, { additionalProperties: false }),
|
|
1372
|
+
async execute(_toolCallId, params) {
|
|
1373
|
+
const implementationPaths = params?.implementationPaths ?? [];
|
|
1374
|
+
const testPaths = params?.testPaths ?? [];
|
|
1375
|
+
const pathEvidence = await enrichScoutPathEvidence([
|
|
1376
|
+
...(params?.entrypoint ? [params.entrypoint] : []),
|
|
1377
|
+
...implementationPaths,
|
|
1378
|
+
...testPaths,
|
|
1379
|
+
]);
|
|
1380
|
+
const result = await adoptScoutFact("target-surface", {
|
|
1381
|
+
kind: "target-surface",
|
|
1382
|
+
origin: "scout",
|
|
1383
|
+
entrypoint: params?.entrypoint ?? "",
|
|
1384
|
+
routeOrMount: params?.routeOrMount ?? "",
|
|
1385
|
+
implementationPaths,
|
|
1386
|
+
testPaths,
|
|
1387
|
+
dataSource: params?.dataSource ?? "",
|
|
1388
|
+
allowedPathConflicts: params?.allowedPathConflicts ?? [],
|
|
1389
|
+
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
1390
|
+
});
|
|
1391
|
+
return receipt(result);
|
|
1392
|
+
},
|
|
1393
|
+
});
|
|
1394
|
+
const recordDesignEvidenceTool = defineTool({
|
|
1395
|
+
name: "record_design_evidence",
|
|
1396
|
+
label: "record_design_evidence",
|
|
1397
|
+
description: "Commit an origin=scout design-evidence fact (source, paths, conflicts).",
|
|
1398
|
+
promptSnippet: "Commit an origin=scout design-evidence fact.",
|
|
1399
|
+
parameters: Type.Object({ source: Type.String({}), paths: stringArray, conflicts: stringArray }, { additionalProperties: false }),
|
|
1400
|
+
async execute(_toolCallId, params) {
|
|
1401
|
+
const paths = params?.paths ?? [];
|
|
1402
|
+
const pathEvidence = await enrichScoutPathEvidence(paths);
|
|
1403
|
+
const result = await adoptScoutFact("design-evidence", {
|
|
1404
|
+
kind: "design-evidence",
|
|
1405
|
+
origin: "scout",
|
|
1406
|
+
source: params?.source ?? "",
|
|
1407
|
+
paths,
|
|
1408
|
+
conflicts: params?.conflicts ?? [],
|
|
1409
|
+
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
1410
|
+
});
|
|
1411
|
+
return receipt(result);
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1414
|
+
return {
|
|
1415
|
+
customTools: [recordTargetSurfaceTool, recordDesignEvidenceTool],
|
|
1416
|
+
flush: async () => {
|
|
1417
|
+
const committed = readCommittedEvents(store, attemptId);
|
|
1418
|
+
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "scout-typed-facts.jsonl"), committed);
|
|
1419
|
+
},
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
192
1422
|
export function buildDagPiUserMessage(task, persona, step) {
|
|
193
1423
|
const role = task.role ?? "unspecified";
|
|
194
1424
|
const writePolicy = task.writePolicy ?? "read-only (default)";
|
|
195
1425
|
if (isDagPiWriteTask(task)) {
|
|
196
|
-
const outcomeInstruction = task
|
|
1426
|
+
const outcomeInstruction = isFrontendFactsWriter(task)
|
|
197
1427
|
? [
|
|
198
|
-
"
|
|
199
|
-
task.writerOutcomePolicy.requireChangedFiles
|
|
200
|
-
? "This generation node requires a non-empty bounded diff; already-satisfied cannot complete it successfully."
|
|
201
|
-
: undefined,
|
|
1428
|
+
"Your implementation status is derived by the executor from mechanical facts (persisted write-tool events, run delta, write guard, requirement coverage, focused-check failures) — never from an IMPLEMENTATION_OUTCOME first line. Do not emit an IMPLEMENTATION_OUTCOME first line. Make real write/edit tool calls that persist files to disk: a response-only change is an empty diff that fails this node. If the contract is already satisfied, prove it with concrete target/verification evidence; a bare self-report cannot authorize an empty implementation.",
|
|
202
1429
|
]
|
|
203
1430
|
.filter((value) => Boolean(value))
|
|
204
1431
|
.join(" ")
|
|
205
|
-
:
|
|
1432
|
+
: task.writerOutcomePolicy
|
|
1433
|
+
? [
|
|
1434
|
+
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked. The runner measures your diff mechanically from git status snapshots taken before and after this node: code pasted into the response text is NOT an implementation and yields an empty diff. Use changed only after actually calling write/edit tools that persist files to disk; use already-satisfied only when the contract is already met and no file changed; use blocked when implementation cannot proceed. Claiming changed without a persisted diff fails this node as invalid-output.",
|
|
1435
|
+
task.writerOutcomePolicy.requireChangedFiles
|
|
1436
|
+
? "This generation node requires a non-empty bounded diff; already-satisfied cannot complete it successfully."
|
|
1437
|
+
: undefined,
|
|
1438
|
+
]
|
|
1439
|
+
.filter((value) => Boolean(value))
|
|
1440
|
+
.join(" ")
|
|
1441
|
+
: undefined;
|
|
206
1442
|
return [
|
|
207
1443
|
`You are executing hybrid DAG node "${task.id}" (role=${role}, piStep=${step}, writePolicy=${writePolicy}).`,
|
|
208
1444
|
"The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
|
|
@@ -327,6 +1563,36 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
|
|
|
327
1563
|
await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
|
|
328
1564
|
return { promptPath, summaryPath };
|
|
329
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Run-owned per-node Pi extension facts (plan 2026-08-21): requested ids,
|
|
1568
|
+
* resolved packages (with absolute entry paths), and degraded/missing ids.
|
|
1569
|
+
* Written only for nodes that declare `piExtensions`; never into the repo-level
|
|
1570
|
+
* DAG template. Best-effort: artifact failures never fail the node.
|
|
1571
|
+
*/
|
|
1572
|
+
async function writePiExtensionsArtifact(nodeArtifactsDir, requested, resolved, missing) {
|
|
1573
|
+
try {
|
|
1574
|
+
const runDir = path.dirname(nodeArtifactsDir);
|
|
1575
|
+
const nodeId = path.basename(nodeArtifactsDir);
|
|
1576
|
+
await writeDagNodeJsonArtifact(runDir, nodeId, "pi-extensions.json", {
|
|
1577
|
+
schemaVersion: 1,
|
|
1578
|
+
requested: [...requested],
|
|
1579
|
+
resolved: resolved.map((entry) => ({
|
|
1580
|
+
id: entry.id,
|
|
1581
|
+
packageName: entry.packageName,
|
|
1582
|
+
version: entry.version,
|
|
1583
|
+
path: entry.entryPaths.join(", "),
|
|
1584
|
+
})),
|
|
1585
|
+
missing: missing.map((entry) => ({
|
|
1586
|
+
id: entry.id,
|
|
1587
|
+
reason: entry.reason,
|
|
1588
|
+
...(entry.detail ? { detail: entry.detail } : {}),
|
|
1589
|
+
})),
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
// best-effort run fact; node execution continues
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
330
1596
|
async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
|
|
331
1597
|
try {
|
|
332
1598
|
const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
|
|
@@ -383,6 +1649,136 @@ const DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES = {
|
|
|
383
1649
|
readGitStatusPorcelain,
|
|
384
1650
|
recoverRootNulArtifact,
|
|
385
1651
|
};
|
|
1652
|
+
/**
|
|
1653
|
+
* M5 shadow pass for `frontend-review-pi`: extract the committed typed terminal
|
|
1654
|
+
* facts from session events, parse the legacy JSON verdict from the response
|
|
1655
|
+
* text, compare them (audit-only), and persist the audit artifact. Missing
|
|
1656
|
+
* typed terminal facts fail the node closed (AC-001); a shadow mismatch never
|
|
1657
|
+
* blocks the node.
|
|
1658
|
+
*/
|
|
1659
|
+
async function runFrontendReviewTerminalShadow(input) {
|
|
1660
|
+
const { compareTypedReviewToLegacyJsonVerdict } = await import("../workflows/dag/frontend-review-context.js");
|
|
1661
|
+
const { parseJsonReviewVerdict } = await import("../workflows/dag/output-protocol.js");
|
|
1662
|
+
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
1663
|
+
let typedKinds = [];
|
|
1664
|
+
try {
|
|
1665
|
+
const content = await readFile(sessionEventsPath, "utf8");
|
|
1666
|
+
typedKinds = scanReviewTerminalKindsFromSessionEvents(content);
|
|
1667
|
+
}
|
|
1668
|
+
catch {
|
|
1669
|
+
// Missing/unreadable session log → fail-closed at zero terminal facts.
|
|
1670
|
+
typedKinds = [];
|
|
1671
|
+
}
|
|
1672
|
+
let legacyVerdict;
|
|
1673
|
+
try {
|
|
1674
|
+
const parsed = parseJsonReviewVerdict(input.mapped.assistantText ?? input.mapped.stdout);
|
|
1675
|
+
if (parsed.ok)
|
|
1676
|
+
legacyVerdict = parsed.verdict;
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
legacyVerdict = undefined;
|
|
1680
|
+
}
|
|
1681
|
+
let comparison;
|
|
1682
|
+
try {
|
|
1683
|
+
comparison = compareTypedReviewToLegacyJsonVerdict({
|
|
1684
|
+
typedKinds,
|
|
1685
|
+
legacyVerdict,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
catch (error) {
|
|
1689
|
+
comparison = {
|
|
1690
|
+
typedVerdict: undefined,
|
|
1691
|
+
legacyVerdict: undefined,
|
|
1692
|
+
match: false,
|
|
1693
|
+
reason: `typed review equivalence comparison crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
// Audit-only flush + artifact. Neither blocks the node.
|
|
1697
|
+
try {
|
|
1698
|
+
await input.tools?.flush?.();
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
// best-effort
|
|
1702
|
+
}
|
|
1703
|
+
try {
|
|
1704
|
+
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-review-status.json", {
|
|
1705
|
+
schemaVersion: 1,
|
|
1706
|
+
nodeId: input.task.id,
|
|
1707
|
+
typedKinds,
|
|
1708
|
+
legacyVerdict,
|
|
1709
|
+
comparison,
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
catch {
|
|
1713
|
+
// best-effort audit artifact
|
|
1714
|
+
}
|
|
1715
|
+
if (typedKinds.length === 0) {
|
|
1716
|
+
return {
|
|
1717
|
+
...input.mapped,
|
|
1718
|
+
ok: false,
|
|
1719
|
+
failureCategory: "review-terminal-missing",
|
|
1720
|
+
stderr: [
|
|
1721
|
+
input.mapped.stderr,
|
|
1722
|
+
"frontend review typed terminal fact missing: no approve_review/request_review_changes tool call was committed",
|
|
1723
|
+
]
|
|
1724
|
+
.filter(Boolean)
|
|
1725
|
+
.join("\n\n"),
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
return input.mapped;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* M8 shadow pass for `frontend-design-review-pi`: extract the committed typed
|
|
1732
|
+
* design terminal facts from session events, flush them, and persist the audit
|
|
1733
|
+
* artifact. The design review's legacy output was a first-line
|
|
1734
|
+
* `VERDICT: pass|request-revision` text protocol (not a JSON verdict), so
|
|
1735
|
+
* there is no JSON equivalence comparison here. Missing typed terminal facts
|
|
1736
|
+
* fail the node closed; writer admission later reads the committed
|
|
1737
|
+
* `design-typed-facts.jsonl` as the only authoritative verdict.
|
|
1738
|
+
*/
|
|
1739
|
+
async function runFrontendDesignTerminalShadow(input) {
|
|
1740
|
+
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
1741
|
+
let typedKinds = [];
|
|
1742
|
+
try {
|
|
1743
|
+
const content = await readFile(sessionEventsPath, "utf8");
|
|
1744
|
+
typedKinds = scanDesignTerminalKindsFromSessionEvents(content);
|
|
1745
|
+
}
|
|
1746
|
+
catch {
|
|
1747
|
+
// Missing/unreadable session log → fail-closed at zero terminal facts.
|
|
1748
|
+
typedKinds = [];
|
|
1749
|
+
}
|
|
1750
|
+
// Audit-only flush + artifact. Neither blocks the node.
|
|
1751
|
+
try {
|
|
1752
|
+
await input.tools?.flush?.();
|
|
1753
|
+
}
|
|
1754
|
+
catch {
|
|
1755
|
+
// best-effort
|
|
1756
|
+
}
|
|
1757
|
+
try {
|
|
1758
|
+
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-design-status.json", {
|
|
1759
|
+
schemaVersion: 1,
|
|
1760
|
+
nodeId: input.task.id,
|
|
1761
|
+
typedKinds,
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
catch {
|
|
1765
|
+
// best-effort audit artifact
|
|
1766
|
+
}
|
|
1767
|
+
if (typedKinds.length === 0) {
|
|
1768
|
+
return {
|
|
1769
|
+
...input.mapped,
|
|
1770
|
+
ok: false,
|
|
1771
|
+
failureCategory: "design-terminal-missing",
|
|
1772
|
+
stderr: [
|
|
1773
|
+
input.mapped.stderr,
|
|
1774
|
+
"frontend design typed terminal fact missing: no approve_design/request_design_changes tool call was committed",
|
|
1775
|
+
]
|
|
1776
|
+
.filter(Boolean)
|
|
1777
|
+
.join("\n\n"),
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
return input.mapped;
|
|
1781
|
+
}
|
|
386
1782
|
export async function executeDagPiNode(input, meta, piStepFn = executePiStep, writeGuardDependencies = DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES) {
|
|
387
1783
|
const started = Date.now();
|
|
388
1784
|
const persona = resolveDagPiPersona(input.task);
|
|
@@ -453,6 +1849,11 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
453
1849
|
}
|
|
454
1850
|
: undefined;
|
|
455
1851
|
let writerToolPolicy;
|
|
1852
|
+
let reviewTerminalTools;
|
|
1853
|
+
let designTerminalTools;
|
|
1854
|
+
let planLedgerTools;
|
|
1855
|
+
let contractTools;
|
|
1856
|
+
let scoutEvidenceTools;
|
|
456
1857
|
const commandPolicy = resolveDagCommandPolicy(input.task.commandPolicy);
|
|
457
1858
|
const allowsPlaywrightCli = dagCommandPolicyAllows(input.task.commandPolicy, "playwright-cli");
|
|
458
1859
|
let playwrightToolContext;
|
|
@@ -521,6 +1922,133 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
521
1922
|
};
|
|
522
1923
|
}
|
|
523
1924
|
}
|
|
1925
|
+
if (isFrontendReviewTypedTerminalNode(input.task)) {
|
|
1926
|
+
try {
|
|
1927
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1928
|
+
const store = createTypedEventStore();
|
|
1929
|
+
reviewTerminalTools = await createFrontendReviewTerminalTools({
|
|
1930
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
1931
|
+
store,
|
|
1932
|
+
runDir: meta.runDir,
|
|
1933
|
+
nodeId: input.task.id,
|
|
1934
|
+
});
|
|
1935
|
+
writerToolPolicy = {
|
|
1936
|
+
requireSdk: true,
|
|
1937
|
+
customTools: reviewTerminalTools.customTools,
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
catch (error) {
|
|
1941
|
+
return {
|
|
1942
|
+
ok: false,
|
|
1943
|
+
stdout: "",
|
|
1944
|
+
stderr: `pi review terminal tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
1945
|
+
failureCategory: "tool-policy",
|
|
1946
|
+
durationMs: Date.now() - started,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (isFrontendDesignTypedTerminalNode(input.task)) {
|
|
1951
|
+
try {
|
|
1952
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1953
|
+
const store = createTypedEventStore();
|
|
1954
|
+
designTerminalTools = await createFrontendDesignTerminalTools({
|
|
1955
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
1956
|
+
store,
|
|
1957
|
+
runDir: meta.runDir,
|
|
1958
|
+
nodeId: input.task.id,
|
|
1959
|
+
});
|
|
1960
|
+
writerToolPolicy = {
|
|
1961
|
+
requireSdk: true,
|
|
1962
|
+
customTools: designTerminalTools.customTools,
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
catch (error) {
|
|
1966
|
+
return {
|
|
1967
|
+
ok: false,
|
|
1968
|
+
stdout: "",
|
|
1969
|
+
stderr: `pi design terminal tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
1970
|
+
failureCategory: "tool-policy",
|
|
1971
|
+
durationMs: Date.now() - started,
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (isFrontendContractTypedNode(input.task)) {
|
|
1976
|
+
try {
|
|
1977
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1978
|
+
const store = createTypedEventStore();
|
|
1979
|
+
contractTools = await createFrontendContractTools({
|
|
1980
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
1981
|
+
store,
|
|
1982
|
+
runDir: meta.runDir,
|
|
1983
|
+
nodeId: input.task.id,
|
|
1984
|
+
});
|
|
1985
|
+
writerToolPolicy = {
|
|
1986
|
+
requireSdk: true,
|
|
1987
|
+
customTools: contractTools.customTools,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
catch (error) {
|
|
1991
|
+
return {
|
|
1992
|
+
ok: false,
|
|
1993
|
+
stdout: "",
|
|
1994
|
+
stderr: `pi contract tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
1995
|
+
failureCategory: "tool-policy",
|
|
1996
|
+
durationMs: Date.now() - started,
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (isFrontendScoutEvidenceNode(input.task)) {
|
|
2001
|
+
try {
|
|
2002
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2003
|
+
const store = createTypedEventStore();
|
|
2004
|
+
scoutEvidenceTools = await createFrontendScoutEvidenceTools({
|
|
2005
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
2006
|
+
store,
|
|
2007
|
+
runDir: meta.runDir,
|
|
2008
|
+
nodeId: input.task.id,
|
|
2009
|
+
workspaceRoot: input.cwd,
|
|
2010
|
+
});
|
|
2011
|
+
writerToolPolicy = {
|
|
2012
|
+
requireSdk: true,
|
|
2013
|
+
customTools: scoutEvidenceTools.customTools,
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
catch (error) {
|
|
2017
|
+
return {
|
|
2018
|
+
ok: false,
|
|
2019
|
+
stdout: "",
|
|
2020
|
+
stderr: `pi scout evidence tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
2021
|
+
failureCategory: "tool-policy",
|
|
2022
|
+
durationMs: Date.now() - started,
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (isFrontendPlanLedgerNode(input.task)) {
|
|
2027
|
+
try {
|
|
2028
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2029
|
+
const store = createTypedEventStore();
|
|
2030
|
+
planLedgerTools = await createFrontendPlanLedgerTools({
|
|
2031
|
+
attemptId: `${meta.runId}:${input.task.id}`,
|
|
2032
|
+
store,
|
|
2033
|
+
runDir: meta.runDir,
|
|
2034
|
+
nodeId: input.task.id,
|
|
2035
|
+
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
2036
|
+
});
|
|
2037
|
+
writerToolPolicy = {
|
|
2038
|
+
requireSdk: true,
|
|
2039
|
+
customTools: planLedgerTools.customTools,
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
catch (error) {
|
|
2043
|
+
return {
|
|
2044
|
+
ok: false,
|
|
2045
|
+
stdout: "",
|
|
2046
|
+
stderr: `pi plan ledger tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
2047
|
+
failureCategory: "tool-policy",
|
|
2048
|
+
durationMs: Date.now() - started,
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
524
2052
|
let result;
|
|
525
2053
|
try {
|
|
526
2054
|
// Phase 5 (P1-9): capture the writeSet baseline BEFORE the writer provider
|
|
@@ -529,8 +2057,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
529
2057
|
// hard fail (no baseline → no rollback → no recovery). Dynamic import avoids
|
|
530
2058
|
// an executor ↔ scheduler static cycle.
|
|
531
2059
|
if (isWriteTask &&
|
|
532
|
-
|
|
533
|
-
input.task.id === "frontend-repair-pi") &&
|
|
2060
|
+
input.task.id === "frontend-implement-pi" &&
|
|
534
2061
|
(input.task.writeSet?.length ?? 0) > 0) {
|
|
535
2062
|
try {
|
|
536
2063
|
const { captureFrontendWriterAttemptIntent } = await import("../workflows/dag/frontend-writer-recovery.js");
|
|
@@ -550,6 +2077,29 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
550
2077
|
};
|
|
551
2078
|
}
|
|
552
2079
|
}
|
|
2080
|
+
// Plan 2026-08-21 D4/D6: resolve the node's explicit Pi extension
|
|
2081
|
+
// allowlist against the settings inventory. Degrade on missing packages —
|
|
2082
|
+
// never fail the node — and persist requested/resolved/missing facts.
|
|
2083
|
+
const piExtensionsResolution = input.task.piExtensions
|
|
2084
|
+
? resolveDagPiExtensions({
|
|
2085
|
+
repoRoot: input.cwd,
|
|
2086
|
+
ids: input.task.piExtensions,
|
|
2087
|
+
})
|
|
2088
|
+
: undefined;
|
|
2089
|
+
if (piExtensionsResolution) {
|
|
2090
|
+
const piBackend = resolvePiBackend();
|
|
2091
|
+
const cliDegraded = piBackend === "cli-only";
|
|
2092
|
+
await writePiExtensionsArtifact(path.join(meta.runDir, input.task.id), input.task.piExtensions, cliDegraded ? [] : piExtensionsResolution.resolved, cliDegraded
|
|
2093
|
+
? input.task.piExtensions.map((id) => ({
|
|
2094
|
+
id,
|
|
2095
|
+
reason: "not-installed",
|
|
2096
|
+
detail: "cli backend does not load pi extensions (sdk-only v1)",
|
|
2097
|
+
}))
|
|
2098
|
+
: piExtensionsResolution.missing);
|
|
2099
|
+
}
|
|
2100
|
+
const piExtensionPaths = piExtensionsResolution && resolvePiBackend() !== "cli-only"
|
|
2101
|
+
? piExtensionsResolution.resolved.flatMap((entry) => entry.entryPaths)
|
|
2102
|
+
: undefined;
|
|
553
2103
|
result = await piStepFn({
|
|
554
2104
|
attachedFiles: [],
|
|
555
2105
|
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
@@ -564,6 +2114,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
564
2114
|
stallTimeoutMs: input.stallTimeoutMs,
|
|
565
2115
|
abortGraceMs: input.abortGraceMs,
|
|
566
2116
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
2117
|
+
...(piExtensionPaths && piExtensionPaths.length > 0
|
|
2118
|
+
? { piExtensionPaths }
|
|
2119
|
+
: {}),
|
|
567
2120
|
onActivity: bridgeActivity
|
|
568
2121
|
? (activity) => {
|
|
569
2122
|
if (activity.kind === "lease" ||
|
|
@@ -608,9 +2161,51 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
608
2161
|
persona,
|
|
609
2162
|
step,
|
|
610
2163
|
});
|
|
611
|
-
const mapped = mapPiResultToDagNodeResult(result, input.task
|
|
612
|
-
?
|
|
613
|
-
: input.task.
|
|
2164
|
+
const mapped = mapPiResultToDagNodeResult(result, isFrontendFactsWriter(input.task)
|
|
2165
|
+
? undefined
|
|
2166
|
+
: input.task.writerOutcomePolicy
|
|
2167
|
+
? WRITER_OUTCOME_PROTOCOL_LINE
|
|
2168
|
+
: input.task.firstProtocolLine);
|
|
2169
|
+
if (isFrontendReviewTypedTerminalNode(input.task)) {
|
|
2170
|
+
return await runFrontendReviewTerminalShadow({
|
|
2171
|
+
task: input.task,
|
|
2172
|
+
meta,
|
|
2173
|
+
mapped,
|
|
2174
|
+
tools: reviewTerminalTools,
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
if (isFrontendDesignTypedTerminalNode(input.task)) {
|
|
2178
|
+
return await runFrontendDesignTerminalShadow({
|
|
2179
|
+
task: input.task,
|
|
2180
|
+
meta,
|
|
2181
|
+
mapped,
|
|
2182
|
+
tools: designTerminalTools,
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
if (isFrontendContractTypedNode(input.task)) {
|
|
2186
|
+
try {
|
|
2187
|
+
await contractTools?.flush?.();
|
|
2188
|
+
}
|
|
2189
|
+
catch {
|
|
2190
|
+
// best-effort flush
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (isFrontendScoutEvidenceNode(input.task)) {
|
|
2194
|
+
try {
|
|
2195
|
+
await scoutEvidenceTools?.flush?.();
|
|
2196
|
+
}
|
|
2197
|
+
catch {
|
|
2198
|
+
// best-effort flush
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if (isFrontendPlanLedgerNode(input.task)) {
|
|
2202
|
+
try {
|
|
2203
|
+
await planLedgerTools?.flush?.();
|
|
2204
|
+
}
|
|
2205
|
+
catch {
|
|
2206
|
+
// best-effort flush; missing ledger still fails at the node validator
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
614
2209
|
if (!isWriteTask) {
|
|
615
2210
|
return mapped;
|
|
616
2211
|
}
|
|
@@ -723,8 +2318,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
723
2318
|
changeManifestChangedFiles = changedFiles;
|
|
724
2319
|
// Phase 5: record the attempt changed paths/hashes into the rollback
|
|
725
2320
|
// intent so a transient partial write can be CAS-restored.
|
|
726
|
-
if (
|
|
727
|
-
input.task.id === "frontend-repair-pi") &&
|
|
2321
|
+
if (input.task.id === "frontend-implement-pi" &&
|
|
728
2322
|
(input.task.writeSet?.length ?? 0) > 0) {
|
|
729
2323
|
try {
|
|
730
2324
|
const { recordFrontendWriterAttempt } = await import("../workflows/dag/frontend-writer-recovery.js");
|
|
@@ -774,8 +2368,65 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
774
2368
|
}
|
|
775
2369
|
}
|
|
776
2370
|
let writerOutcomeViolation;
|
|
2371
|
+
let factDerivedStatus;
|
|
777
2372
|
if (mapped.ok && input.task.writerOutcomePolicy) {
|
|
778
|
-
if (
|
|
2373
|
+
if (isFrontendFactsWriter(input.task)) {
|
|
2374
|
+
// AC-001/AC-003: facts-derived status. The first line is never read;
|
|
2375
|
+
// the legacy validator still runs for the shadow comparison only.
|
|
2376
|
+
try {
|
|
2377
|
+
const { collectFrontendWriterFacts, deriveFrontendWriterStatus, computeFailureFingerprint, compareFactStatusToLegacyOutcome, } = await import("../workflows/dag/frontend-writer-status.js");
|
|
2378
|
+
const facts = await collectFrontendWriterFacts({
|
|
2379
|
+
sessionEventsPath: path.join(meta.runDir, input.task.id, "session-events.jsonl"),
|
|
2380
|
+
changedFiles: changeManifestChangedFiles ?? [],
|
|
2381
|
+
writeGuard: { ok: writeGuardOk, violations: writeGuardViolations },
|
|
2382
|
+
requirementTargets: [],
|
|
2383
|
+
coveredRequirementIds: [],
|
|
2384
|
+
focusedCheckFailures: [],
|
|
2385
|
+
tokensUsed: result.tokensUsed,
|
|
2386
|
+
wallTimeMs: Date.now() - started,
|
|
2387
|
+
rounds: 1,
|
|
2388
|
+
writeAttempts: attempt,
|
|
2389
|
+
firstLineText: mapped.assistantText,
|
|
2390
|
+
});
|
|
2391
|
+
const derived = deriveFrontendWriterStatus(facts);
|
|
2392
|
+
factDerivedStatus = derived.status;
|
|
2393
|
+
const legacyValidation = validateWriterImplementationOutcome(mapped.assistantText || mapped.stdout, changeManifestChangedFiles ?? [], {
|
|
2394
|
+
requireChangedFiles: false,
|
|
2395
|
+
allowMissingChangedOutcomeWhenDiffPresent: false,
|
|
2396
|
+
});
|
|
2397
|
+
const legacyOutcome = legacyValidation.ok
|
|
2398
|
+
? legacyValidation.outcome
|
|
2399
|
+
: legacyValidation.reason.includes("blocked")
|
|
2400
|
+
? "blocked"
|
|
2401
|
+
: "missing";
|
|
2402
|
+
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "fact-implementation-status.json", {
|
|
2403
|
+
schemaVersion: 1,
|
|
2404
|
+
nodeId: input.task.id,
|
|
2405
|
+
status: derived.status,
|
|
2406
|
+
reason: derived.reason,
|
|
2407
|
+
changedFiles: facts.changedFiles,
|
|
2408
|
+
writeToolEvents: facts.writeToolEvents,
|
|
2409
|
+
writeGuardOk: facts.writeGuardOk,
|
|
2410
|
+
writeGuardViolations: facts.writeGuardViolations,
|
|
2411
|
+
focusedCheckFailures: facts.focusedCheckFailures,
|
|
2412
|
+
alreadySatisfiedEvidence: facts.alreadySatisfiedEvidence,
|
|
2413
|
+
tokensUsed: facts.tokensUsed,
|
|
2414
|
+
wallTimeMs: facts.wallTimeMs,
|
|
2415
|
+
rounds: facts.rounds,
|
|
2416
|
+
writeAttempts: facts.writeAttempts,
|
|
2417
|
+
failureFingerprint: computeFailureFingerprint(facts),
|
|
2418
|
+
shadow: compareFactStatusToLegacyOutcome(derived.status, legacyOutcome),
|
|
2419
|
+
});
|
|
2420
|
+
if (derived.status !== "changed" &&
|
|
2421
|
+
derived.status !== "already-satisfied") {
|
|
2422
|
+
writerOutcomeViolation = `fact-derived status ${derived.status}: ${derived.reason}`;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
catch (error) {
|
|
2426
|
+
writerOutcomeViolation = `frontend facts derivation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
else if (changeManifestChangedFiles === undefined) {
|
|
779
2430
|
writerOutcomeViolation =
|
|
780
2431
|
"writer outcome validation failed: actual diff is unavailable";
|
|
781
2432
|
}
|
|
@@ -885,31 +2536,42 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
885
2536
|
if (meta.writeGuardAttribution === "best-effort") {
|
|
886
2537
|
stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
|
|
887
2538
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const writerCleanTimeout = !mapped.ok &&
|
|
892
|
-
mapped.failureCategory === "timeout" &&
|
|
893
|
-
writeGuardOk &&
|
|
2539
|
+
const rawFailureCategory = mapped.failureCategory;
|
|
2540
|
+
const writeToolCallCount = readWriterThinkingExhaustionEvidence(result).writeToolCallCount;
|
|
2541
|
+
const zeroSideEffects = writeGuardOk &&
|
|
894
2542
|
!completenessFailure &&
|
|
895
2543
|
!writerThinkingExhausted &&
|
|
896
|
-
|
|
2544
|
+
!writerOutcomeViolation &&
|
|
897
2545
|
changeManifestChangedFiles !== undefined &&
|
|
898
2546
|
changeManifestChangedFiles.length === 0 &&
|
|
899
|
-
|
|
2547
|
+
writeToolCallCount === 0;
|
|
2548
|
+
const targetCleanTransport = !mapped.ok &&
|
|
2549
|
+
isTargetTemplateTransientRetryNode(input.task) &&
|
|
2550
|
+
rawFailureCategory !== undefined &&
|
|
2551
|
+
["timeout", "network", "rate-limit", "unavailable"].includes(rawFailureCategory) &&
|
|
2552
|
+
zeroSideEffects;
|
|
2553
|
+
const frontendCleanTimeout = !mapped.ok &&
|
|
2554
|
+
rawFailureCategory === "timeout" &&
|
|
2555
|
+
isWriterTransportRetryCandidate(input.task) &&
|
|
2556
|
+
!isTargetTemplateTransientRetryNode(input.task) &&
|
|
2557
|
+
zeroSideEffects;
|
|
2558
|
+
const writerCleanTimeout = targetCleanTransport || frontendCleanTimeout;
|
|
900
2559
|
return {
|
|
901
2560
|
...mapped,
|
|
902
2561
|
ok: false,
|
|
903
2562
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
2563
|
+
...(rawFailureCategory ? { rawFailureCategory } : {}),
|
|
904
2564
|
failureCategory: mapped.ok
|
|
905
2565
|
? writeGuardOk
|
|
906
2566
|
? completenessFailure
|
|
907
2567
|
? completenessFailure.failureCategory
|
|
908
|
-
:
|
|
909
|
-
isWriterEmptyDiffRetryCandidate(input.task) &&
|
|
910
|
-
isChangedWriterImplementationOutcome(mapped.assistantText || mapped.stdout)
|
|
2568
|
+
: factDerivedStatus === "empty-diff"
|
|
911
2569
|
? WRITER_EMPTY_DIFF_RETRY_CATEGORY
|
|
912
|
-
:
|
|
2570
|
+
: changeManifestChangedFiles?.length === 0 &&
|
|
2571
|
+
isWriterEmptyDiffRetryCandidate(input.task) &&
|
|
2572
|
+
isChangedWriterImplementationOutcome(mapped.assistantText || mapped.stdout)
|
|
2573
|
+
? WRITER_EMPTY_DIFF_RETRY_CATEGORY
|
|
2574
|
+
: "invalid-output"
|
|
913
2575
|
: "write-guard"
|
|
914
2576
|
: // When the attempt already failed with a writer-style category
|
|
915
2577
|
// (empty-output / invalid-output / writer-empty-diff) but the workspace
|