@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,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, } from "../infrastructure/harness/artifact-store.js";
|
|
6
|
+
import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, writeDagRunJsonArtifact, } from "../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { captureWorkspaceCheckpoint } from "../workflows/dag/workspace-checkpoint.js";
|
|
7
8
|
import { truncateOutput } from "../shared/output-truncation.js";
|
|
8
9
|
import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
|
|
@@ -14,15 +15,25 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
|
|
|
14
15
|
import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
|
|
15
16
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
16
17
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
18
|
+
import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
|
|
17
19
|
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
20
|
+
import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
|
|
21
|
+
import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
|
|
18
22
|
import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
|
|
19
23
|
import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
|
|
20
24
|
import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
|
|
21
25
|
import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
|
|
22
26
|
import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
|
|
23
|
-
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
27
|
+
import { captureFrontendWorktreeBaseline, formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
28
|
+
import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
|
|
29
|
+
import { classifyFrontendFailure, } from "../workflows/dag/frontend-repair.js";
|
|
30
|
+
import { FRONTEND_CONTRACT_MAX_BYTES, formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
|
|
31
|
+
import { evaluateFrontendDesignPolicy, } from "../workflows/dag/frontend-design-policy.js";
|
|
32
|
+
import { deriveFrontendWriterAdmissionShell, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, } from "../workflows/dag/frontend-writer-admission.js";
|
|
33
|
+
import { deriveFrontendIntegrationFacts, buildFrontendTestHandoff, frontendCloseoutFactsSchema, renderFrontendCloseout, } from "../workflows/dag/frontend-closeout.js";
|
|
34
|
+
import { buildFrontendHumanDecisionEnvelope, evaluateHighAdmissionReceipt, routeVerificationConfig, } from "../workflows/dag/frontend-human-decision.js";
|
|
35
|
+
import { classifyFailureOwner, classifyIssueCategoryRestartPhase, classifyRestartPhase, } from "../workflows/dag/frontend-recovery-capsule.js";
|
|
36
|
+
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
26
37
|
import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
|
|
27
38
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
28
39
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
@@ -121,6 +132,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
|
|
|
121
132
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
122
133
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
123
134
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
135
|
+
const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
|
|
136
|
+
const TEST_TIMEOUT_MARKERS = [
|
|
137
|
+
/Test timed out in \d+ms/i,
|
|
138
|
+
/Timeout of \d+ms exceeded/i,
|
|
139
|
+
];
|
|
140
|
+
const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
|
|
141
|
+
/AssertionError/i,
|
|
142
|
+
/expected:?\s/i,
|
|
143
|
+
/received:?\s/i,
|
|
144
|
+
/Snapshot /i,
|
|
145
|
+
/toMatchSnapshot/i,
|
|
146
|
+
/SyntaxError/i,
|
|
147
|
+
/TS\d{4}/,
|
|
148
|
+
/error TS/i,
|
|
149
|
+
/eslint/i,
|
|
150
|
+
/compile error/i,
|
|
151
|
+
/Failed to compile/i,
|
|
152
|
+
];
|
|
153
|
+
function isIdentifiedTestCommand(command) {
|
|
154
|
+
return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
|
|
155
|
+
}
|
|
156
|
+
function classifyShellCommandFailure(input) {
|
|
157
|
+
if (input.processCategory !== "nonzero-exit")
|
|
158
|
+
return input.processCategory;
|
|
159
|
+
if (!isIdentifiedTestCommand(input.command))
|
|
160
|
+
return "nonzero-exit";
|
|
161
|
+
const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
|
|
162
|
+
const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
|
|
163
|
+
if (!hasTimeoutMarker)
|
|
164
|
+
return "nonzero-exit";
|
|
165
|
+
const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
|
|
166
|
+
return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
|
|
167
|
+
}
|
|
124
168
|
const NODE_STDOUT_MAX = 12_000;
|
|
125
169
|
const NODE_STDERR_MAX = 8_000;
|
|
126
170
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
@@ -327,16 +371,21 @@ export async function executeShellCommand(input) {
|
|
|
327
371
|
});
|
|
328
372
|
});
|
|
329
373
|
child.on("close", (code) => {
|
|
330
|
-
let
|
|
374
|
+
let processCategory = "nonzero-exit";
|
|
331
375
|
if (timedOut) {
|
|
332
|
-
|
|
376
|
+
processCategory = "timeout";
|
|
333
377
|
}
|
|
334
378
|
else if (code === 0) {
|
|
335
|
-
|
|
379
|
+
processCategory = "success";
|
|
336
380
|
}
|
|
337
381
|
finish({
|
|
338
382
|
exitCode: code,
|
|
339
|
-
failureCategory
|
|
383
|
+
failureCategory: classifyShellCommandFailure({
|
|
384
|
+
command: input.command,
|
|
385
|
+
processCategory,
|
|
386
|
+
stdout: formatBoundedOutput(stdout),
|
|
387
|
+
stderr: formatBoundedOutput(stderr),
|
|
388
|
+
}),
|
|
340
389
|
ok: code === 0 && !timedOut,
|
|
341
390
|
});
|
|
342
391
|
});
|
|
@@ -1740,6 +1789,45 @@ async function executePipelineCommands(input, meta, overrideCommands) {
|
|
|
1740
1789
|
}
|
|
1741
1790
|
return results;
|
|
1742
1791
|
}
|
|
1792
|
+
/** A+B (AC-008): derive unresolved verification entrypoints from committed plan
|
|
1793
|
+
* facts. A verification target is unresolved when its commandLabel does not
|
|
1794
|
+
* match any frozen command label from the verify shell evidence bundle.
|
|
1795
|
+
* Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
|
|
1796
|
+
* closed to [] so the router restarts the contract instead of inventing refs. */
|
|
1797
|
+
async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
|
|
1798
|
+
try {
|
|
1799
|
+
const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
1800
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
|
|
1801
|
+
const patch = restorePlanPatchFromCommittedFacts(records);
|
|
1802
|
+
const targets = patch
|
|
1803
|
+
?.verificationTargets;
|
|
1804
|
+
if (!Array.isArray(targets))
|
|
1805
|
+
return [];
|
|
1806
|
+
const frozen = new Set(frozenCommandLabels
|
|
1807
|
+
.map((label) => label.trim().toLowerCase())
|
|
1808
|
+
.filter(Boolean));
|
|
1809
|
+
const refs = [];
|
|
1810
|
+
for (const item of targets) {
|
|
1811
|
+
const target = item;
|
|
1812
|
+
const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
|
|
1813
|
+
if (commandLabel &&
|
|
1814
|
+
frozen.has(commandLabel.trim().toLowerCase())) {
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const file = typeof target.file === "string" ? target.file : "";
|
|
1818
|
+
const symbol = typeof target.symbol === "string" ? target.symbol : "";
|
|
1819
|
+
const id = typeof target.id === "string" ? target.id : "";
|
|
1820
|
+
const ref = file || symbol || id;
|
|
1821
|
+
if (!ref)
|
|
1822
|
+
continue;
|
|
1823
|
+
refs.push({ ref, digest: sha256Hex(ref) });
|
|
1824
|
+
}
|
|
1825
|
+
return refs;
|
|
1826
|
+
}
|
|
1827
|
+
catch {
|
|
1828
|
+
return [];
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1743
1831
|
async function executeFrontendVerificationBundle(input, meta) {
|
|
1744
1832
|
const started = Date.now();
|
|
1745
1833
|
const shell = input.task.shell;
|
|
@@ -1894,75 +1982,809 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1894
1982
|
catch (error) {
|
|
1895
1983
|
traceError = error instanceof Error ? error : new Error(String(error));
|
|
1896
1984
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1985
|
+
const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
|
|
1986
|
+
if (verifyFailed) {
|
|
1987
|
+
// M8: verify failure is TERMINAL. There is no same-run repair branch and
|
|
1988
|
+
// no frontend-repair-assessment.json; we only materialize failure
|
|
1989
|
+
// ownership facts (frontend-repair classification + recovery capsule
|
|
1990
|
+
// ownership) for the operation controller / recovery run.
|
|
1991
|
+
const failedNodeId = input.task.id;
|
|
1992
|
+
const failureCategory = firstFailure?.failureCategory ??
|
|
1993
|
+
lintSyntheticFailure?.failureCategory ??
|
|
1994
|
+
(traceError ? "invalid-output" : undefined) ??
|
|
1995
|
+
"invalid-output";
|
|
1996
|
+
const failureClass = classifyFrontendFailure({
|
|
1997
|
+
nodeId: failedNodeId,
|
|
1998
|
+
failureCategory,
|
|
1999
|
+
stdout: firstFailure?.stdout,
|
|
2000
|
+
stderr: firstFailure?.stderr ?? traceError?.message,
|
|
2001
|
+
});
|
|
2002
|
+
// A verify failure means the writer's candidate failed verification, so
|
|
2003
|
+
// the recovery ownership maps to the implement phase (frontend-recovery
|
|
2004
|
+
// capsule classification, fail-closed to implement-pi).
|
|
2005
|
+
const classifiedOwner = classifyFailureOwner({
|
|
2006
|
+
failureSource: "frontend-verify-shell",
|
|
2007
|
+
nodeId: failedNodeId,
|
|
2008
|
+
rawFailureCategory: failureCategory,
|
|
2009
|
+
});
|
|
2010
|
+
const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
|
|
2011
|
+
const restartPhase = classifyRestartPhase(failureOwner);
|
|
2012
|
+
// A+B (AC-008): verification-config routing. Produce structured
|
|
2013
|
+
// unresolved entrypoint refs from committed plan facts and the frozen
|
|
2014
|
+
// command labels; a non-empty unresolved set routes to
|
|
2015
|
+
// verification-config-decision and materializes the frozen
|
|
2016
|
+
// verification-config envelope. An empty/untyped signal fails closed to
|
|
2017
|
+
// contract-restart. The decision is recorded so the recovery controller
|
|
2018
|
+
// never re-classifies the same signal twice.
|
|
2019
|
+
let verificationConfigRoute;
|
|
2020
|
+
if (failureOwner === "verification-config") {
|
|
2021
|
+
const frozenCommandLabels = [
|
|
2022
|
+
...(bundle.staticEvidence.commandLabels ?? []),
|
|
2023
|
+
...(bundle.behaviorEvidence.commandLabels ?? []),
|
|
2024
|
+
...(bundle.mockEvidence?.commandLabels ?? []),
|
|
2025
|
+
];
|
|
2026
|
+
const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
|
|
2027
|
+
verificationConfigRoute = routeVerificationConfig({
|
|
2028
|
+
unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
|
|
2029
|
+
});
|
|
2030
|
+
const allowlistedResolvers = [
|
|
2031
|
+
...new Set(frozenCommandLabels
|
|
2032
|
+
.map((label) => label.trim())
|
|
2033
|
+
.filter(Boolean)),
|
|
2034
|
+
].sort();
|
|
2035
|
+
if (verificationConfigRoute.decision ===
|
|
2036
|
+
"verification-config-decision" &&
|
|
2037
|
+
unresolvedEntrypointRefs.length > 0 &&
|
|
2038
|
+
allowlistedResolvers.length > 0) {
|
|
2039
|
+
const envelope = buildFrontendHumanDecisionEnvelope({
|
|
2040
|
+
operationId: input.task.id,
|
|
2041
|
+
runId: meta.runId,
|
|
2042
|
+
operationRevision: 1,
|
|
2043
|
+
targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
|
|
2044
|
+
staleOnTargetDigestChange: true,
|
|
2045
|
+
kind: "verification-config",
|
|
2046
|
+
resumeAction: "select-verification-config",
|
|
2047
|
+
payload: {
|
|
2048
|
+
unresolvedEntrypointRefs,
|
|
2049
|
+
allowlistedResolvers,
|
|
2050
|
+
},
|
|
2051
|
+
});
|
|
2052
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
const verifyFailure = {
|
|
2056
|
+
schemaVersion: 1,
|
|
2057
|
+
schemaId: "frontend-verify-failure-v1",
|
|
2058
|
+
nodeId: failedNodeId,
|
|
2059
|
+
failureCategory,
|
|
2060
|
+
failureClass,
|
|
2061
|
+
failureOwner,
|
|
2062
|
+
restartPhase,
|
|
2063
|
+
...(verificationConfigRoute
|
|
2064
|
+
? { verificationConfigRoute }
|
|
2065
|
+
: {}),
|
|
2066
|
+
commandResults,
|
|
2067
|
+
failedAt: new Date().toISOString(),
|
|
2068
|
+
};
|
|
2069
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
|
|
2070
|
+
return {
|
|
2071
|
+
ok: false,
|
|
2072
|
+
stdout: summarizeCommandResults(results).stdout,
|
|
2073
|
+
stderr: firstFailure?.stderr ||
|
|
2074
|
+
lintSyntheticFailure?.stderr ||
|
|
2075
|
+
traceError?.message ||
|
|
2076
|
+
"frontend verification failed",
|
|
2077
|
+
failureCategory,
|
|
2078
|
+
durationMs: Date.now() - started,
|
|
2079
|
+
...{ commandResults },
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
// A+B (AC-008): when lint is `require-clean` but the assessment proves
|
|
2083
|
+
// baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
|
|
2084
|
+
// whose only positive resume is `accept-baseline-debt`. Evidence stays
|
|
2085
|
+
// `baseline-debt` and is never rewritten to `passed`.
|
|
2086
|
+
if (lintAssessment?.status === "baseline-debt") {
|
|
2087
|
+
const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
|
|
2088
|
+
.filter((ref) => ref?.path && ref?.sha256)
|
|
2089
|
+
.map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
|
|
2090
|
+
if (baselineEvidenceRefs.length > 0) {
|
|
2091
|
+
const envelope = buildFrontendHumanDecisionEnvelope({
|
|
2092
|
+
operationId: input.task.id,
|
|
2093
|
+
runId: meta.runId,
|
|
2094
|
+
operationRevision: 1,
|
|
2095
|
+
targetDigest: lintAssessment.commandIdentity.sha256,
|
|
2096
|
+
staleOnTargetDigestChange: true,
|
|
2097
|
+
kind: "baseline-debt-acceptance",
|
|
2098
|
+
resumeAction: "accept-baseline-debt",
|
|
2099
|
+
payload: {
|
|
2100
|
+
baselineEvidenceRefs,
|
|
2101
|
+
candidateDigest: lintAssessment.commandIdentity.sha256,
|
|
2102
|
+
commandDigest: lintAssessment.commandIdentity.sha256,
|
|
2103
|
+
resultingPolicy: "allow-non-regression",
|
|
2104
|
+
},
|
|
2105
|
+
});
|
|
2106
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
ok: true,
|
|
2111
|
+
stdout: lintAssessment?.status === "baseline-debt"
|
|
2112
|
+
? "Frontend verification bundle: pass with lint baseline-debt"
|
|
2113
|
+
: "Frontend verification bundle: pass",
|
|
2114
|
+
stderr: "",
|
|
2115
|
+
failureCategory: "success",
|
|
2116
|
+
durationMs: Date.now() - started,
|
|
2117
|
+
...{ commandResults },
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
/* ------------------------------------------------------------------ *
|
|
2121
|
+
* M8 deterministic frontend shell nodes: design policy, writer *
|
|
2122
|
+
* admission, closeout. These replace the prewrite-gate authorization *
|
|
2123
|
+
* path and the Pi closeout node. *
|
|
2124
|
+
* ------------------------------------------------------------------ */
|
|
2125
|
+
async function readRunDirJson(runDir, relativePath) {
|
|
2126
|
+
return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
|
|
2127
|
+
}
|
|
2128
|
+
/** Committed typed review terminal kinds from the review node's typed event
|
|
2129
|
+
* store (AC-003: typed terminal tools are the only authoritative verdict). */
|
|
2130
|
+
async function collectReviewTerminalKinds(runDir, reviewNodeId) {
|
|
2131
|
+
try {
|
|
2132
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
|
|
2133
|
+
return records
|
|
2134
|
+
.filter((record) => record.phase === "committed")
|
|
2135
|
+
.map((record) => {
|
|
2136
|
+
const kind = record.fact.kind;
|
|
2137
|
+
return typeof kind === "string" ? kind : "";
|
|
2138
|
+
})
|
|
2139
|
+
.filter(Boolean);
|
|
2140
|
+
}
|
|
2141
|
+
catch {
|
|
2142
|
+
return [];
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
/** Committed typed design terminal kinds from the design review node's typed
|
|
2146
|
+
* event store (M8: the committed approve_design / request_design_changes fact
|
|
2147
|
+
* is the only authoritative verdict; the legacy first-line VERDICT text is
|
|
2148
|
+
* never read for writer admission). Fail-closed on a missing/unreadable store. */
|
|
2149
|
+
async function collectDesignTerminalKinds(runDir, designNodeId) {
|
|
2150
|
+
try {
|
|
2151
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2152
|
+
return records
|
|
2153
|
+
.filter((record) => record.phase === "committed")
|
|
2154
|
+
.map((record) => {
|
|
2155
|
+
const kind = record.fact.kind;
|
|
2156
|
+
return typeof kind === "string" ? kind : "";
|
|
2157
|
+
})
|
|
2158
|
+
.filter(Boolean);
|
|
2159
|
+
}
|
|
2160
|
+
catch {
|
|
2161
|
+
return [];
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2165
|
+
* `request_review_changes` fact. The typed issueCategory is mapped through the
|
|
2166
|
+
* frozen classifier; `unknown` (or an unreadable store) fails closed to
|
|
2167
|
+
* `undefined` so the runtime never trusts an untyped category. */
|
|
2168
|
+
async function collectReviewRestartPhase(runDir, reviewNodeId) {
|
|
2169
|
+
try {
|
|
2170
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
|
|
2171
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2172
|
+
record.fact.kind === "request_review_changes");
|
|
2173
|
+
const issueCategory = request?.fact
|
|
2174
|
+
?.issueCategory;
|
|
2175
|
+
return typeof issueCategory === "string"
|
|
2176
|
+
? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
|
|
2177
|
+
: undefined;
|
|
2178
|
+
}
|
|
2179
|
+
catch {
|
|
2180
|
+
return undefined;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2184
|
+
* `request_design_changes` fact. Mirrors the review collector. */
|
|
2185
|
+
async function collectDesignRestartPhase(runDir, designNodeId) {
|
|
2186
|
+
try {
|
|
2187
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2188
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2189
|
+
record.fact.kind === "request_design_changes");
|
|
2190
|
+
const issueCategory = request?.fact
|
|
2191
|
+
?.issueCategory;
|
|
2192
|
+
return typeof issueCategory === "string"
|
|
2193
|
+
? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
|
|
2194
|
+
: undefined;
|
|
2195
|
+
}
|
|
2196
|
+
catch {
|
|
2197
|
+
return undefined;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Resolve the deterministic runtime skeleton for the frontend plan patch from
|
|
2202
|
+
* the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
|
|
2203
|
+
* (the only schema-recognized persistent channel); absence is a fail-closed
|
|
2204
|
+
* generation/configuration defect, never a reason to fall back to text.
|
|
2205
|
+
*/
|
|
2206
|
+
function resolveFrontendContractSkeleton(spec, planNodeId) {
|
|
2207
|
+
const planTask = spec.tasks.find((task) => task.id === planNodeId);
|
|
2208
|
+
const skeleton = planTask?.structuredContractOutput?.skeleton;
|
|
2209
|
+
if (!skeleton || Object.keys(skeleton).length === 0) {
|
|
2210
|
+
throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
|
|
2211
|
+
}
|
|
2212
|
+
return skeleton;
|
|
2213
|
+
}
|
|
2214
|
+
async function collectFrontendSourceFreshness(input) {
|
|
2215
|
+
const entries = [];
|
|
2216
|
+
for (const source of input.sourceBinding?.sources ?? []) {
|
|
2217
|
+
entries.push({ path: source.path, sha256: source.sha256, fresh: true });
|
|
2218
|
+
}
|
|
2219
|
+
for (const evidencePath of input.contract.designEvidence.paths) {
|
|
2220
|
+
if (entries.some((entry) => entry.path === evidencePath))
|
|
2221
|
+
continue;
|
|
2222
|
+
try {
|
|
2223
|
+
const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
|
|
2224
|
+
entries.push({
|
|
2225
|
+
path: evidencePath,
|
|
2226
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
2227
|
+
fresh: true,
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
catch {
|
|
2231
|
+
entries.push({
|
|
2232
|
+
path: evidencePath,
|
|
2233
|
+
sha256: "0".repeat(64),
|
|
2234
|
+
fresh: false,
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return entries;
|
|
2239
|
+
}
|
|
2240
|
+
/** R3/R4 openspec invariants: frozen candidates must exist and content
|
|
2241
|
+
* snapshots must not have drifted. Citation is enforced upstream by the
|
|
2242
|
+
* prewrite gate read-event evidence and ledger specReference conformance. */
|
|
2243
|
+
async function checkFrontendOpenspecFacts(input) {
|
|
2244
|
+
const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
2245
|
+
const candidateRoot = input.workspaceRoot;
|
|
2246
|
+
const missingCandidates = [];
|
|
2247
|
+
for (const candidate of frozenCandidatePaths) {
|
|
2248
|
+
try {
|
|
2249
|
+
await stat(path.join(candidateRoot, candidate));
|
|
2250
|
+
}
|
|
2251
|
+
catch {
|
|
2252
|
+
missingCandidates.push(candidate);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (missingCandidates.length > 0) {
|
|
2256
|
+
throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
|
|
2257
|
+
}
|
|
2258
|
+
const snapshots = input.config.openspecCandidateSnapshots;
|
|
2259
|
+
if (snapshots && snapshots.length > 0) {
|
|
2260
|
+
for (const snapshot of snapshots) {
|
|
2261
|
+
try {
|
|
2262
|
+
const bytes = await readFile(path.join(candidateRoot, snapshot.path));
|
|
2263
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
2264
|
+
if (actual !== snapshot.sha256) {
|
|
2265
|
+
throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
catch (error) {
|
|
2269
|
+
if (error instanceof Error &&
|
|
2270
|
+
error.message.startsWith("frontend design policy blocked")) {
|
|
2271
|
+
throw error;
|
|
2272
|
+
}
|
|
2273
|
+
throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
async function executeFrontendDesignPolicy(input, meta) {
|
|
2279
|
+
const started = Date.now();
|
|
2280
|
+
const shell = input.task.shell;
|
|
2281
|
+
const config = shell.frontendDesignPolicy;
|
|
2282
|
+
try {
|
|
2283
|
+
// 1. Materialize the canonical contract from committed typed facts. After
|
|
2284
|
+
// M9a the plan node's assistantText is a pure Markdown narrative; the
|
|
2285
|
+
// compile authority is the committed plan ledger ⊕ runtime skeleton.
|
|
2286
|
+
// Fail closed on missing committed facts — never fall back to text.
|
|
2287
|
+
const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
2288
|
+
// A+B (AC-001/003): fail-closed committed contract/scout typed facts.
|
|
2289
|
+
// Missing/uncommitted facts block here; never fall back to text parsing.
|
|
2290
|
+
const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
|
|
2291
|
+
const contractCheck = checkCommittedOriginFacts({
|
|
2292
|
+
records: committedContractFacts,
|
|
2293
|
+
origin: "contract",
|
|
2294
|
+
});
|
|
2295
|
+
if (!contractCheck.ok) {
|
|
2296
|
+
return {
|
|
2297
|
+
ok: false,
|
|
2298
|
+
stdout: "",
|
|
2299
|
+
stderr: `frontend design policy blocked: ${contractCheck.reason}`,
|
|
2300
|
+
failureCategory: "invalid-output",
|
|
2301
|
+
durationMs: Date.now() - started,
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
|
|
2305
|
+
const scoutCheck = checkCommittedOriginFacts({
|
|
2306
|
+
records: committedScoutFacts,
|
|
2307
|
+
origin: "scout",
|
|
2308
|
+
});
|
|
2309
|
+
if (!scoutCheck.ok) {
|
|
2310
|
+
return {
|
|
2311
|
+
ok: false,
|
|
2312
|
+
stdout: "",
|
|
2313
|
+
stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
|
|
2314
|
+
failureCategory: "invalid-output",
|
|
2315
|
+
durationMs: Date.now() - started,
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
|
|
2319
|
+
const assembled = await buildContractFromCommittedFacts({
|
|
2320
|
+
runDir: meta.runDir,
|
|
2321
|
+
planNodeId: config.planFromNodeId,
|
|
2322
|
+
contractNodeId: "frontend-contract-pi",
|
|
2323
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2324
|
+
skeleton,
|
|
2325
|
+
});
|
|
2326
|
+
if (!assembled.ok) {
|
|
2327
|
+
return {
|
|
2328
|
+
ok: false,
|
|
2329
|
+
stdout: "",
|
|
2330
|
+
stderr: `frontend design policy blocked: ${assembled.reason}`,
|
|
2331
|
+
failureCategory: "invalid-output",
|
|
2332
|
+
durationMs: Date.now() - started,
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
const contract = assembled.canonical;
|
|
2336
|
+
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(contract), "utf8");
|
|
2337
|
+
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
2338
|
+
return {
|
|
2339
|
+
ok: false,
|
|
2340
|
+
stdout: "",
|
|
2341
|
+
stderr: `frontend design policy blocked: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit`,
|
|
2342
|
+
failureCategory: "invalid-output",
|
|
2343
|
+
durationMs: Date.now() - started,
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
// 2. Requirement id retention (R11): search the canonical contract
|
|
2347
|
+
// serialization instead of the plan node narrative.
|
|
2348
|
+
const contractText = serializeDeterministicJson(contract);
|
|
2349
|
+
const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
|
|
2350
|
+
if (missingIds.length > 0) {
|
|
2351
|
+
return {
|
|
2352
|
+
ok: false,
|
|
2353
|
+
stdout: "",
|
|
2354
|
+
stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
|
|
2355
|
+
failureCategory: "invalid-output",
|
|
2356
|
+
durationMs: Date.now() - started,
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
// 3. Mock strategy allowed + frozen command binding (R1).
|
|
2360
|
+
const mockStrategy = contract.mockApi.strategy;
|
|
2361
|
+
if (!config.allowedMockStrategies.includes(mockStrategy)) {
|
|
2362
|
+
return {
|
|
2363
|
+
ok: false,
|
|
2364
|
+
stdout: "",
|
|
2365
|
+
stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
|
|
2366
|
+
failureCategory: "invalid-output",
|
|
2367
|
+
durationMs: Date.now() - started,
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
|
|
2371
|
+
return {
|
|
2372
|
+
ok: false,
|
|
2373
|
+
stdout: "",
|
|
2374
|
+
stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
|
|
2375
|
+
failureCategory: "invalid-output",
|
|
2376
|
+
durationMs: Date.now() - started,
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
// 4. writeSet containment.
|
|
2380
|
+
if (config.implementationWriteSet) {
|
|
2381
|
+
const writeSet = new Set(config.implementationWriteSet);
|
|
2382
|
+
const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
|
|
2383
|
+
if (uncoveredTargets.length > 0) {
|
|
2384
|
+
return {
|
|
2385
|
+
ok: false,
|
|
2386
|
+
stdout: "",
|
|
2387
|
+
stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
|
|
2388
|
+
failureCategory: "invalid-output",
|
|
2389
|
+
durationMs: Date.now() - started,
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
const uncoveredVTs = contract.verificationTargets
|
|
2393
|
+
.filter((vt) => vt.type !== "static")
|
|
2394
|
+
.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
2395
|
+
if (uncoveredVTs.length > 0) {
|
|
2396
|
+
return {
|
|
2397
|
+
ok: false,
|
|
2398
|
+
stdout: "",
|
|
2399
|
+
stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
|
|
2400
|
+
failureCategory: "invalid-output",
|
|
2401
|
+
durationMs: Date.now() - started,
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
// 5. Source freshness (R4) + freshness entries.
|
|
2406
|
+
if (config.requireSourceFreshness) {
|
|
2407
|
+
if (!meta.spec.sourceBinding) {
|
|
2408
|
+
throw new Error("frontend design policy requires sourceBinding for freshness check");
|
|
2409
|
+
}
|
|
2410
|
+
await assertFrontendSourceBindingFresh({
|
|
2411
|
+
workspaceRoot: input.cwd,
|
|
2412
|
+
binding: meta.spec.sourceBinding,
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
const sourceFreshness = await collectFrontendSourceFreshness({
|
|
2416
|
+
workspaceRoot: input.cwd,
|
|
2417
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2418
|
+
contract,
|
|
2419
|
+
});
|
|
2420
|
+
// 6. Openspec invariants (R3): frozen candidates exist and snapshots have
|
|
2421
|
+
// not drifted. Citation is superseded by prewrite-gate read evidence.
|
|
2422
|
+
await checkFrontendOpenspecFacts({
|
|
2423
|
+
workspaceRoot: input.cwd,
|
|
2424
|
+
config,
|
|
2425
|
+
});
|
|
2426
|
+
// 7. Deterministic design policy (M8 D2: designApproval is optional here;
|
|
2427
|
+
// the design verdict is enforced by the writer-admission shell).
|
|
2428
|
+
const policyResult = evaluateFrontendDesignPolicy({
|
|
2429
|
+
contract,
|
|
2430
|
+
allowedMockStrategies: config.allowedMockStrategies,
|
|
2431
|
+
sourceFreshness,
|
|
2432
|
+
componentSpecCandidatePaths: config.openspecCandidatePaths ??
|
|
2433
|
+
config.componentSpecCandidatePaths ??
|
|
2434
|
+
[],
|
|
2435
|
+
allowedDependencies: config.allowedDependencies,
|
|
2436
|
+
writeSetPatterns: config.implementationWriteSet ?? [],
|
|
2437
|
+
});
|
|
2438
|
+
// 8. Write canonical contract + policy result.
|
|
2439
|
+
await writeFrontendImplementationContractArtifact({
|
|
2440
|
+
runDir: meta.runDir,
|
|
2441
|
+
outputDir: config.outputDir,
|
|
2442
|
+
artifactName: config.artifactName,
|
|
2443
|
+
canonical: contract,
|
|
2444
|
+
});
|
|
2445
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
|
|
2446
|
+
if (policyResult.classification === "blocked") {
|
|
1899
2447
|
return {
|
|
1900
2448
|
ok: false,
|
|
1901
|
-
stdout:
|
|
1902
|
-
stderr:
|
|
1903
|
-
|
|
1904
|
-
traceError?.message ||
|
|
1905
|
-
"frontend reverify failed",
|
|
1906
|
-
failureCategory: firstFailure?.failureCategory ??
|
|
1907
|
-
lintSyntheticFailure?.failureCategory ??
|
|
1908
|
-
"invalid-output",
|
|
2449
|
+
stdout: "",
|
|
2450
|
+
stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
|
|
2451
|
+
failureCategory: "invalid-output",
|
|
1909
2452
|
durationMs: Date.now() - started,
|
|
1910
|
-
...{ commandResults },
|
|
1911
2453
|
};
|
|
1912
2454
|
}
|
|
1913
2455
|
return {
|
|
1914
2456
|
ok: true,
|
|
1915
|
-
stdout:
|
|
1916
|
-
? "Frontend reverify bundle: pass with lint baseline-debt"
|
|
1917
|
-
: "Frontend reverify bundle: pass",
|
|
2457
|
+
stdout: `Frontend design policy: approved findings=0\nContract: ${path.posix.join(config.outputDir, config.artifactName)}`,
|
|
1918
2458
|
stderr: "",
|
|
1919
2459
|
failureCategory: "success",
|
|
1920
2460
|
durationMs: Date.now() - started,
|
|
1921
|
-
...{ commandResults },
|
|
1922
2461
|
};
|
|
1923
2462
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
:
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
status: "FINISHED",
|
|
1933
|
-
failureCategory: firstFailure?.failureCategory ??
|
|
1934
|
-
lintSyntheticFailure?.failureCategory,
|
|
1935
|
-
// Keep classification scoped to the failed command. Aggregate
|
|
1936
|
-
// successful output may contain unrelated writeSet-like JSON.
|
|
1937
|
-
stdout: failureStdout,
|
|
1938
|
-
stderr: firstFailure?.stderr ?? lintSyntheticFailure?.stderr,
|
|
1939
|
-
commandResults,
|
|
1940
|
-
},
|
|
1941
|
-
});
|
|
2463
|
+
catch (error) {
|
|
2464
|
+
return {
|
|
2465
|
+
ok: false,
|
|
2466
|
+
stdout: "",
|
|
2467
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2468
|
+
failureCategory: "invalid-output",
|
|
2469
|
+
durationMs: Date.now() - started,
|
|
2470
|
+
};
|
|
1942
2471
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2472
|
+
}
|
|
2473
|
+
async function executeFrontendWriterAdmission(input, meta) {
|
|
2474
|
+
const started = Date.now();
|
|
2475
|
+
const shell = input.task.shell;
|
|
2476
|
+
const config = shell.frontendWriterAdmission;
|
|
2477
|
+
try {
|
|
2478
|
+
// 1. Design verdict pass is a hard admission condition (M8 D2). In small
|
|
2479
|
+
// topology the design review node is absent and the check is vacuous.
|
|
2480
|
+
// The verdict is read from the committed typed design terminal fact, not
|
|
2481
|
+
// from the legacy first-line VERDICT text protocol (AC-003/AC-004).
|
|
2482
|
+
let verdictPass = true;
|
|
2483
|
+
let designRestartPhase;
|
|
2484
|
+
if (config.designReviewFromNodeId) {
|
|
2485
|
+
const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
|
|
2486
|
+
verdictPass =
|
|
2487
|
+
designKinds.length === 1 && designKinds[0] === "approve_design";
|
|
2488
|
+
// A+B (AC-009): request_design_changes issueCategory maps to the
|
|
2489
|
+
// deterministic §4.10 restart phase; unknown fails closed to undefined.
|
|
2490
|
+
designRestartPhase = verdictPass
|
|
2491
|
+
? undefined
|
|
2492
|
+
: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId);
|
|
2493
|
+
}
|
|
2494
|
+
// 2. Read the canonical contract + design-policy result.
|
|
2495
|
+
const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
|
|
2496
|
+
const contract = frontendImplementationContractSchema.parse(contractRaw);
|
|
2497
|
+
const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
|
|
2498
|
+
if (policyRaw.schemaId !== "frontend-design-policy-v1") {
|
|
2499
|
+
throw new Error("frontend writer admission: design policy result artifact invalid");
|
|
2500
|
+
}
|
|
2501
|
+
const designPolicyResult = {
|
|
2502
|
+
schemaId: "frontend-design-policy-v1",
|
|
2503
|
+
classification: policyRaw.classification === "approved" ? "approved" : "blocked",
|
|
2504
|
+
findings: policyRaw.findings ?? [],
|
|
2505
|
+
digest: "",
|
|
2506
|
+
};
|
|
2507
|
+
// 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
|
|
2508
|
+
await captureFrontendWorktreeBaseline({
|
|
2509
|
+
runDir: meta.runDir,
|
|
2510
|
+
workspaceRoot: input.cwd,
|
|
1951
2511
|
});
|
|
2512
|
+
const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
|
|
2513
|
+
const baseline = (baselineRaw.files ?? [])
|
|
2514
|
+
.map((file) => ({
|
|
2515
|
+
path: file,
|
|
2516
|
+
sha256: baselineRaw.hashes?.[file] ?? null,
|
|
2517
|
+
}))
|
|
2518
|
+
.filter((entry) => entry.sha256 !== null);
|
|
2519
|
+
// 4. Writer-preceding lint baseline (R5) when lint is configured.
|
|
2520
|
+
let lintResults = [];
|
|
2521
|
+
if ((config.lintCommands?.length ?? 0) > 0) {
|
|
2522
|
+
for (const command of config.lintCommands) {
|
|
2523
|
+
const commandNumber = lintResults.length + 1;
|
|
2524
|
+
const result = await executeShellCommand({
|
|
2525
|
+
command,
|
|
2526
|
+
cwd: input.cwd,
|
|
2527
|
+
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
2528
|
+
envAllowlist: shell.envAllowlist,
|
|
2529
|
+
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2530
|
+
reportActivity: input.reportActivity,
|
|
2531
|
+
outputArtifacts: {
|
|
2532
|
+
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2533
|
+
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
2534
|
+
},
|
|
2535
|
+
});
|
|
2536
|
+
lintResults.push(result);
|
|
2537
|
+
}
|
|
2538
|
+
await materializeFrontendLintBaseline({
|
|
2539
|
+
runDir: meta.runDir,
|
|
2540
|
+
workspaceRoot: input.cwd,
|
|
2541
|
+
commands: config.lintCommands,
|
|
2542
|
+
results: lintResults,
|
|
2543
|
+
worktreeChanged: false,
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
// 5. Derive admission (writeSet + digests + findings). Glob entries in
|
|
2547
|
+
// the contract targets (e.g. `src/**` inherited from the skeleton) are
|
|
2548
|
+
// expanded against the concrete workspace files so the derived writeSet
|
|
2549
|
+
// contains only real paths the writer can touch.
|
|
2550
|
+
const workspaceFiles = await collectWorkspaceFiles(input.cwd);
|
|
2551
|
+
const admission = deriveFrontendWriterAdmissionShell({
|
|
2552
|
+
contract,
|
|
2553
|
+
designPolicyResult,
|
|
2554
|
+
baseline,
|
|
2555
|
+
frozenCommandLabels: config.frozenCommandLabels,
|
|
2556
|
+
allowedMockStrategies: config.allowedMockStrategies,
|
|
2557
|
+
workspaceFiles,
|
|
2558
|
+
});
|
|
2559
|
+
// 6. Design verdict enforcement: request-revision is a blocked admission.
|
|
2560
|
+
const failureSource = policyRaw.classification === "blocked"
|
|
2561
|
+
? "frontend-design-policy-shell"
|
|
2562
|
+
: "frontend-writer-admission-shell";
|
|
2563
|
+
const findings = verdictPass
|
|
2564
|
+
? admission.findings
|
|
2565
|
+
: [
|
|
2566
|
+
...admission.findings,
|
|
2567
|
+
{
|
|
2568
|
+
code: "design-approval-request-revision",
|
|
2569
|
+
message: designRestartPhase !== undefined
|
|
2570
|
+
? `design review typed terminal verdict is not approve_design (restartPhase=${designRestartPhase})`
|
|
2571
|
+
: "design review typed terminal verdict is not approve_design",
|
|
2572
|
+
},
|
|
2573
|
+
];
|
|
2574
|
+
const classification = findings.length === 0 && admission.classification === "accepted"
|
|
2575
|
+
? admission.classification
|
|
2576
|
+
: findings.length === 0 &&
|
|
2577
|
+
admission.classification === "requires-human-approval"
|
|
2578
|
+
? "requires-human-approval"
|
|
2579
|
+
: "blocked";
|
|
2580
|
+
const result = frontendWriterAdmissionResultV1Schema.parse({
|
|
2581
|
+
schemaVersion: 1,
|
|
2582
|
+
schemaId: "frontend-writer-admission-shell-v1",
|
|
2583
|
+
classification,
|
|
2584
|
+
writeSet: admission.writeSet,
|
|
2585
|
+
writeSetDigest: admission.writeSetDigest,
|
|
2586
|
+
baselineDigest: admission.baselineDigest,
|
|
2587
|
+
admissionDigest: admission.admissionDigest,
|
|
2588
|
+
...(admission.baselineDebtPolicy !== undefined
|
|
2589
|
+
? { baselineDebtPolicy: admission.baselineDebtPolicy }
|
|
2590
|
+
: {}),
|
|
2591
|
+
findings,
|
|
2592
|
+
failureSource,
|
|
2593
|
+
});
|
|
2594
|
+
await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
|
|
2595
|
+
// A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
|
|
2596
|
+
// catalog whose candidate digest must match the admission digest before
|
|
2597
|
+
// the gate can transition from `requires-human-approval` to `accepted`.
|
|
2598
|
+
// Any digest drift resolves to `stale` (never silently accepted).
|
|
2599
|
+
if (classification === "requires-human-approval") {
|
|
2600
|
+
const receipt = evaluateHighAdmissionReceipt({
|
|
2601
|
+
candidateDigest: result.admissionDigest,
|
|
2602
|
+
admissionDigest: result.admissionDigest,
|
|
2603
|
+
});
|
|
2604
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
|
|
2605
|
+
if (receipt.status !== "accepted") {
|
|
2606
|
+
return {
|
|
2607
|
+
ok: false,
|
|
2608
|
+
stdout: "",
|
|
2609
|
+
stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
|
|
2610
|
+
failureCategory: "invalid-output",
|
|
2611
|
+
durationMs: Date.now() - started,
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (classification === "blocked") {
|
|
2616
|
+
return {
|
|
2617
|
+
ok: false,
|
|
2618
|
+
stdout: "",
|
|
2619
|
+
stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
|
|
2620
|
+
failureCategory: "invalid-output",
|
|
2621
|
+
durationMs: Date.now() - started,
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
return {
|
|
2625
|
+
ok: true,
|
|
2626
|
+
stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
|
|
2627
|
+
stderr: "",
|
|
2628
|
+
failureCategory: "success",
|
|
2629
|
+
durationMs: Date.now() - started,
|
|
2630
|
+
};
|
|
1952
2631
|
}
|
|
2632
|
+
catch (error) {
|
|
2633
|
+
return {
|
|
2634
|
+
ok: false,
|
|
2635
|
+
stdout: "",
|
|
2636
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2637
|
+
failureCategory: "invalid-output",
|
|
2638
|
+
durationMs: Date.now() - started,
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
async function executeFrontendContractImport(input, meta) {
|
|
2643
|
+
const started = Date.now();
|
|
2644
|
+
const shell = input.task.shell;
|
|
2645
|
+
const config = shell.frontendContractImport;
|
|
1953
2646
|
try {
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
|
|
2647
|
+
const binding = meta.spec.sourceBinding;
|
|
2648
|
+
if (!binding) {
|
|
2649
|
+
throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
|
|
2650
|
+
}
|
|
2651
|
+
if (config.requireSourceFreshness) {
|
|
2652
|
+
await assertFrontendSourceBindingFresh({
|
|
2653
|
+
workspaceRoot: input.cwd,
|
|
2654
|
+
binding,
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
const digest = createHash("sha256")
|
|
2658
|
+
.update(JSON.stringify({
|
|
2659
|
+
taskId: binding.taskId,
|
|
2660
|
+
sources: binding.sources,
|
|
2661
|
+
artifactName: config.artifactName,
|
|
2662
|
+
}))
|
|
2663
|
+
.digest("hex");
|
|
2664
|
+
const facts = {
|
|
2665
|
+
schemaVersion: 1,
|
|
2666
|
+
schemaId: "frontend-contract-import-v1",
|
|
2667
|
+
artifactName: config.artifactName,
|
|
2668
|
+
sourceBindingTaskId: binding.taskId,
|
|
2669
|
+
sourceCount: binding.sources.length,
|
|
2670
|
+
fresh: true,
|
|
2671
|
+
digest,
|
|
2672
|
+
};
|
|
2673
|
+
return {
|
|
2674
|
+
ok: true,
|
|
2675
|
+
stdout: JSON.stringify(facts, null, 2),
|
|
2676
|
+
stderr: "",
|
|
2677
|
+
failureCategory: "success",
|
|
2678
|
+
durationMs: Date.now() - started,
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
catch (error) {
|
|
2682
|
+
return {
|
|
2683
|
+
ok: false,
|
|
2684
|
+
stdout: "",
|
|
2685
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2686
|
+
failureCategory: "invalid-output",
|
|
2687
|
+
durationMs: Date.now() - started,
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
async function executeFrontendCloseout(input, meta) {
|
|
2692
|
+
const started = Date.now();
|
|
2693
|
+
const shell = input.task.shell;
|
|
2694
|
+
const config = shell.frontendCloseout;
|
|
2695
|
+
try {
|
|
2696
|
+
const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
|
|
2697
|
+
const contract = frontendImplementationContractSchema.parse(context.contract);
|
|
2698
|
+
const trace = context.verificationTrace;
|
|
2699
|
+
const verificationTargets = (trace?.targets ?? []).map((target) => ({
|
|
2700
|
+
status: target.status === "ok" ? "ok" : "failed",
|
|
2701
|
+
}));
|
|
2702
|
+
const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
|
|
2703
|
+
typeof trace?.mockCommandNodeId === "string" &&
|
|
2704
|
+
trace.mockCommandNodeId.length > 0 &&
|
|
2705
|
+
Array.isArray(trace?.mockCommandLabels) &&
|
|
2706
|
+
trace.mockCommandLabels.length > 0 &&
|
|
2707
|
+
Array.isArray(trace?.mockCommandTexts) &&
|
|
2708
|
+
trace.mockCommandTexts.length === trace.mockCommandLabels.length;
|
|
2709
|
+
const integrationFacts = deriveFrontendIntegrationFacts({
|
|
2710
|
+
mockStrategy: contract.mockApi.strategy,
|
|
2711
|
+
realApiEvidence: { present: false, passed: false },
|
|
2712
|
+
mockEvidenceBound,
|
|
2713
|
+
verificationTargets,
|
|
1957
2714
|
});
|
|
1958
|
-
|
|
2715
|
+
// Typed review terminal (AC-003) from the committed typed event store.
|
|
2716
|
+
const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
|
|
2717
|
+
const typedVerdict = typedKinds.length === 1 &&
|
|
2718
|
+
(typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
|
|
2719
|
+
? typedKinds[0]
|
|
2720
|
+
: null;
|
|
2721
|
+
// A+B (AC-009): request_review_changes issueCategory maps to the
|
|
2722
|
+
// deterministic §4.10 restart phase. Recorded as a run-owned audit
|
|
2723
|
+
// artifact so the frozen closeout schema is never widened; `unknown`
|
|
2724
|
+
// fails closed to no restart phase.
|
|
2725
|
+
const reviewRestartPhase = typedVerdict === "request_review_changes"
|
|
2726
|
+
? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
|
|
2727
|
+
: undefined;
|
|
2728
|
+
if (reviewRestartPhase !== undefined) {
|
|
2729
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
|
|
2730
|
+
schemaVersion: 1,
|
|
2731
|
+
nodeId: config.reviewFromNodeId,
|
|
2732
|
+
restartPhase: reviewRestartPhase,
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
const facts = frontendCloseoutFactsSchema.parse({
|
|
2736
|
+
schemaVersion: 1,
|
|
2737
|
+
schemaId: "frontend-closeout-facts-v1",
|
|
2738
|
+
coverageMatrix: verificationTargets.map((target, index) => ({
|
|
2739
|
+
verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
|
|
2740
|
+
status: target.status === "ok" ? "passed" : "failed",
|
|
2741
|
+
})),
|
|
2742
|
+
lintStatus: "unavailable",
|
|
2743
|
+
integrationFacts,
|
|
2744
|
+
reviewApproval: {
|
|
2745
|
+
typedVerdict,
|
|
2746
|
+
legacyVerdict: null,
|
|
2747
|
+
match: true,
|
|
2748
|
+
},
|
|
2749
|
+
cumulativeDiff: {
|
|
2750
|
+
cumulativeDigest: (typeof context.verificationTrace === "object" &&
|
|
2751
|
+
context.verificationTrace !== null &&
|
|
2752
|
+
"schemaId" in context.verificationTrace
|
|
2753
|
+
? deterministicSha256({ verificationTrace: context.verificationTrace })
|
|
2754
|
+
: deterministicSha256({})),
|
|
2755
|
+
empty: false,
|
|
2756
|
+
},
|
|
2757
|
+
notRun: { browser: "not-run", visual: "not-run" },
|
|
2758
|
+
pendingRealIntegration: integrationFacts !== "real-api",
|
|
2759
|
+
});
|
|
2760
|
+
const rendered = renderFrontendCloseout(facts);
|
|
2761
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
|
|
2762
|
+
// A+B (AC-010): materialize the deterministic frontend-test handoff.
|
|
2763
|
+
// browser/visual/runtime-a11y are always `not-run` and the handoff is
|
|
2764
|
+
// always `deferred-to-frontend-test`, so it never blocks implementation
|
|
2765
|
+
// and can never be rewritten into a passed result.
|
|
2766
|
+
const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
|
|
2767
|
+
const sourceDigest = meta.spec.sourceBinding
|
|
2768
|
+
? createHash("sha256")
|
|
2769
|
+
.update(JSON.stringify({
|
|
2770
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
2771
|
+
sources: meta.spec.sourceBinding.sources,
|
|
2772
|
+
}))
|
|
2773
|
+
.digest("hex")
|
|
2774
|
+
: deterministicSha256({});
|
|
2775
|
+
const handoff = buildFrontendTestHandoff({
|
|
2776
|
+
taskId: handoffTaskId,
|
|
2777
|
+
sourceDigest,
|
|
2778
|
+
candidateHash: facts.cumulativeDiff.cumulativeDigest,
|
|
2779
|
+
});
|
|
2780
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
|
|
2781
|
+
await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
|
|
1959
2782
|
return {
|
|
1960
2783
|
ok: true,
|
|
1961
|
-
stdout:
|
|
2784
|
+
stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
|
|
1962
2785
|
stderr: "",
|
|
1963
2786
|
failureCategory: "success",
|
|
1964
2787
|
durationMs: Date.now() - started,
|
|
1965
|
-
...{ commandResults },
|
|
1966
2788
|
};
|
|
1967
2789
|
}
|
|
1968
2790
|
catch (error) {
|
|
@@ -1972,7 +2794,6 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1972
2794
|
stderr: error instanceof Error ? error.message : String(error),
|
|
1973
2795
|
failureCategory: "invalid-output",
|
|
1974
2796
|
durationMs: Date.now() - started,
|
|
1975
|
-
...{ commandResults },
|
|
1976
2797
|
};
|
|
1977
2798
|
}
|
|
1978
2799
|
}
|
|
@@ -2061,6 +2882,52 @@ async function executeFrontendTestHtmlReport(input, meta) {
|
|
|
2061
2882
|
};
|
|
2062
2883
|
}
|
|
2063
2884
|
}
|
|
2885
|
+
async function executeFrontendTestStandardScenarios(input) {
|
|
2886
|
+
const started = Date.now();
|
|
2887
|
+
try {
|
|
2888
|
+
const result = await copyFrontendTestStandardScenarios({
|
|
2889
|
+
workspaceRoot: input.cwd,
|
|
2890
|
+
});
|
|
2891
|
+
return {
|
|
2892
|
+
ok: true,
|
|
2893
|
+
stdout: JSON.stringify(result),
|
|
2894
|
+
stderr: "",
|
|
2895
|
+
failureCategory: "success",
|
|
2896
|
+
durationMs: Date.now() - started,
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
catch (error) {
|
|
2900
|
+
return {
|
|
2901
|
+
ok: false,
|
|
2902
|
+
stdout: "",
|
|
2903
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2904
|
+
failureCategory: "invalid-output",
|
|
2905
|
+
durationMs: Date.now() - started,
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
async function executeFrontendTestEnvironmentProbe(input) {
|
|
2910
|
+
const started = Date.now();
|
|
2911
|
+
const result = await probeFrontendTestEnvironment({
|
|
2912
|
+
workspaceRoot: input.cwd,
|
|
2913
|
+
});
|
|
2914
|
+
if (result.ok) {
|
|
2915
|
+
return {
|
|
2916
|
+
ok: true,
|
|
2917
|
+
stdout: result.stdout,
|
|
2918
|
+
stderr: "",
|
|
2919
|
+
failureCategory: "success",
|
|
2920
|
+
durationMs: Date.now() - started,
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
return {
|
|
2924
|
+
ok: false,
|
|
2925
|
+
stdout: "",
|
|
2926
|
+
stderr: result.stderr,
|
|
2927
|
+
failureCategory: "nonzero-exit",
|
|
2928
|
+
durationMs: Date.now() - started,
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2064
2931
|
async function executeFrontendTestEvidenceValidation(input) {
|
|
2065
2932
|
const started = Date.now();
|
|
2066
2933
|
try {
|
|
@@ -2238,69 +3105,76 @@ async function executeFrontendTestReports(input, meta) {
|
|
|
2238
3105
|
};
|
|
2239
3106
|
}
|
|
2240
3107
|
}
|
|
2241
|
-
async function
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
|
|
3108
|
+
async function writeShellPresetProcessReceipt(input) {
|
|
3109
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
|
|
3110
|
+
schemaVersion: 1,
|
|
3111
|
+
kind: "shell-preset",
|
|
3112
|
+
preset: input.preset,
|
|
3113
|
+
ok: input.ok,
|
|
3114
|
+
durationMs: input.durationMs,
|
|
3115
|
+
steps: input.steps,
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
async function ensureShellPresetProcessReceipt(input) {
|
|
3119
|
+
const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
|
|
3120
|
+
if (existsSync(receiptPath))
|
|
3121
|
+
return;
|
|
3122
|
+
if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
|
|
3123
|
+
return;
|
|
2252
3124
|
}
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
3125
|
+
const detail = input.result.ok ? input.result.stdout : input.result.stderr;
|
|
3126
|
+
await writeShellPresetProcessReceipt({
|
|
3127
|
+
runDir: input.runDir,
|
|
3128
|
+
nodeId: input.nodeId,
|
|
3129
|
+
preset: input.preset,
|
|
3130
|
+
ok: input.result.ok,
|
|
3131
|
+
durationMs: input.result.durationMs,
|
|
3132
|
+
steps: [
|
|
3133
|
+
{
|
|
3134
|
+
id: "run",
|
|
3135
|
+
label: input.preset,
|
|
3136
|
+
ok: input.result.ok,
|
|
3137
|
+
detail: detail?.slice(0, 8 * 1024),
|
|
3138
|
+
durationMs: input.result.durationMs,
|
|
2265
3139
|
},
|
|
2266
|
-
|
|
2267
|
-
}
|
|
2268
|
-
let afterStatus;
|
|
2269
|
-
try {
|
|
2270
|
-
afterStatus = await readGitStatusPorcelain(input.cwd);
|
|
2271
|
-
}
|
|
2272
|
-
catch {
|
|
2273
|
-
afterStatus = undefined;
|
|
2274
|
-
}
|
|
2275
|
-
try {
|
|
2276
|
-
const materialized = await materializeFrontendLintBaseline({
|
|
2277
|
-
runDir: meta.runDir,
|
|
2278
|
-
workspaceRoot: input.cwd,
|
|
2279
|
-
commands: baseline.lintCommands,
|
|
2280
|
-
results,
|
|
2281
|
-
worktreeChanged: beforeStatus === undefined ||
|
|
2282
|
-
afterStatus === undefined ||
|
|
2283
|
-
beforeStatus !== afterStatus,
|
|
2284
|
-
});
|
|
2285
|
-
return {
|
|
2286
|
-
ok: true,
|
|
2287
|
-
stdout: `Frontend lint baseline: ${materialized.artifact.status}\nArtifact: ${materialized.ref.path}\nDiagnostics: ${materialized.artifact.diagnostics.length}`,
|
|
2288
|
-
stderr: materialized.artifact.reason ?? "",
|
|
2289
|
-
failureCategory: "success",
|
|
2290
|
-
durationMs: Date.now() - started,
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
catch (error) {
|
|
2294
|
-
return {
|
|
2295
|
-
ok: false,
|
|
2296
|
-
stdout: "",
|
|
2297
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2298
|
-
failureCategory: "invalid-output",
|
|
2299
|
-
durationMs: Date.now() - started,
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
3140
|
+
],
|
|
3141
|
+
});
|
|
2302
3142
|
}
|
|
2303
|
-
|
|
3143
|
+
function namedShellHandler(shell) {
|
|
3144
|
+
if (!shell)
|
|
3145
|
+
return null;
|
|
3146
|
+
const record = shell;
|
|
3147
|
+
for (const key of [
|
|
3148
|
+
"finalWriteSetApprovalGate",
|
|
3149
|
+
"frontendTestStandardScenarios",
|
|
3150
|
+
"frontendTestEnvironmentProbe",
|
|
3151
|
+
"frontendBrowserToolPreflight",
|
|
3152
|
+
"frontendPrewriteGate",
|
|
3153
|
+
"frontendLintBaseline",
|
|
3154
|
+
"frontendVerificationBundle",
|
|
3155
|
+
"frontendReviewContext",
|
|
3156
|
+
"frontendTestCaseChecklist",
|
|
3157
|
+
"frontendTestCaseManifest",
|
|
3158
|
+
"frontendTestEvidenceValidation",
|
|
3159
|
+
"frontendTestResultFinalize",
|
|
3160
|
+
"frontendTestL5Report",
|
|
3161
|
+
"frontendTestHtmlReport",
|
|
3162
|
+
"frontendTestReports",
|
|
3163
|
+
"backendTestPipeline",
|
|
3164
|
+
"jsonArtifactGate",
|
|
3165
|
+
"projectGovernanceGate",
|
|
3166
|
+
"requirementCoverageGate",
|
|
3167
|
+
"verdictGate",
|
|
3168
|
+
"verifyEvidence",
|
|
3169
|
+
"repairArtifactGate",
|
|
3170
|
+
"jacocoCoverage",
|
|
3171
|
+
]) {
|
|
3172
|
+
if (record[key] != null)
|
|
3173
|
+
return key;
|
|
3174
|
+
}
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
async function executeDagShellNodeInner(input, meta) {
|
|
2304
3178
|
const shell = input.task.shell;
|
|
2305
3179
|
if (shell?.finalWriteSetApprovalGate) {
|
|
2306
3180
|
const started = Date.now();
|
|
@@ -2339,30 +3213,89 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2339
3213
|
};
|
|
2340
3214
|
}
|
|
2341
3215
|
}
|
|
3216
|
+
if (shell?.frontendTestStandardScenarios) {
|
|
3217
|
+
return executeFrontendTestStandardScenarios(input);
|
|
3218
|
+
}
|
|
3219
|
+
if (shell?.frontendTestEnvironmentProbe) {
|
|
3220
|
+
return executeFrontendTestEnvironmentProbe(input);
|
|
3221
|
+
}
|
|
2342
3222
|
if (shell?.frontendBrowserToolPreflight) {
|
|
2343
3223
|
const started = Date.now();
|
|
3224
|
+
const steps = [];
|
|
3225
|
+
const finish = async (result) => {
|
|
3226
|
+
await writeShellPresetProcessReceipt({
|
|
3227
|
+
runDir: meta.runDir,
|
|
3228
|
+
nodeId: input.task.id,
|
|
3229
|
+
preset: "frontendBrowserToolPreflight",
|
|
3230
|
+
ok: result.ok,
|
|
3231
|
+
durationMs: result.durationMs,
|
|
3232
|
+
steps: steps.length > 0
|
|
3233
|
+
? steps
|
|
3234
|
+
: [
|
|
3235
|
+
{
|
|
3236
|
+
id: "run",
|
|
3237
|
+
label: "浏览器工具能力探测",
|
|
3238
|
+
ok: result.ok,
|
|
3239
|
+
detail: result.ok ? result.stdout : result.stderr,
|
|
3240
|
+
durationMs: result.durationMs,
|
|
3241
|
+
},
|
|
3242
|
+
],
|
|
3243
|
+
});
|
|
3244
|
+
return result;
|
|
3245
|
+
};
|
|
2344
3246
|
try {
|
|
2345
3247
|
if (resolvePiBackend() === "cli-only") {
|
|
3248
|
+
steps.push({
|
|
3249
|
+
id: "pi-backend",
|
|
3250
|
+
label: "Pi SDK 后端可用",
|
|
3251
|
+
ok: false,
|
|
3252
|
+
detail: "CODE_AGENT_PI_BACKEND=cli-only",
|
|
3253
|
+
});
|
|
2346
3254
|
throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
|
|
2347
3255
|
}
|
|
3256
|
+
steps.push({
|
|
3257
|
+
id: "pi-backend",
|
|
3258
|
+
label: "Pi SDK 后端可用",
|
|
3259
|
+
ok: true,
|
|
3260
|
+
});
|
|
2348
3261
|
const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
|
|
2349
3262
|
if (!sdkCapability.ok) {
|
|
3263
|
+
steps.push({
|
|
3264
|
+
id: "sdk-custom-tool",
|
|
3265
|
+
label: "Pi SDK custom-tool 能力",
|
|
3266
|
+
ok: false,
|
|
3267
|
+
detail: sdkCapability.detail,
|
|
3268
|
+
});
|
|
2350
3269
|
throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
|
|
2351
3270
|
}
|
|
3271
|
+
steps.push({
|
|
3272
|
+
id: "sdk-custom-tool",
|
|
3273
|
+
label: "Pi SDK custom-tool 能力",
|
|
3274
|
+
ok: true,
|
|
3275
|
+
});
|
|
2352
3276
|
const toolProbe = await createPlaywrightCliTool({
|
|
2353
3277
|
repoRoot: input.cwd,
|
|
2354
3278
|
runDir: meta.runDir,
|
|
2355
3279
|
nodeId: input.task.id,
|
|
2356
3280
|
caseId: "frontend-browser-preflight",
|
|
2357
3281
|
evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
|
|
2358
|
-
// Capability-only registration probe. The tested URL is selected later by
|
|
2359
|
-
// retrieve-frontend-test-context-pi and validated by the environment shell.
|
|
2360
3282
|
baseUrl: "http://localhost:5173/",
|
|
2361
3283
|
inputRoot: "testcase/frontend/fixtures",
|
|
2362
3284
|
});
|
|
2363
3285
|
if (toolProbe.name !== "playwright_cli") {
|
|
3286
|
+
steps.push({
|
|
3287
|
+
id: "structured-tool",
|
|
3288
|
+
label: "注册 playwright_cli",
|
|
3289
|
+
ok: false,
|
|
3290
|
+
detail: toolProbe.name,
|
|
3291
|
+
});
|
|
2364
3292
|
throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
|
|
2365
3293
|
}
|
|
3294
|
+
steps.push({
|
|
3295
|
+
id: "structured-tool",
|
|
3296
|
+
label: "注册 playwright_cli",
|
|
3297
|
+
ok: true,
|
|
3298
|
+
});
|
|
2366
3299
|
const launcher = resolvePlaywrightCliLauncher(input.cwd);
|
|
2367
3300
|
const result = await defaultPlaywrightCliCommandRunner({
|
|
2368
3301
|
executable: launcher.executable,
|
|
@@ -2375,8 +3308,19 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2375
3308
|
if (result.exitCode !== 0 ||
|
|
2376
3309
|
result.timedOut ||
|
|
2377
3310
|
!["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
|
|
3311
|
+
steps.push({
|
|
3312
|
+
id: "playwright-cli-help",
|
|
3313
|
+
label: "playwright-cli --help 合同",
|
|
3314
|
+
ok: false,
|
|
3315
|
+
detail: help.slice(0, 8 * 1024),
|
|
3316
|
+
});
|
|
2378
3317
|
throw new Error("playwright-cli-contract-incompatible");
|
|
2379
3318
|
}
|
|
3319
|
+
steps.push({
|
|
3320
|
+
id: "playwright-cli-help",
|
|
3321
|
+
label: "playwright-cli --help 合同",
|
|
3322
|
+
ok: true,
|
|
3323
|
+
});
|
|
2380
3324
|
const capability = {
|
|
2381
3325
|
schemaVersion: 1,
|
|
2382
3326
|
status: "ready",
|
|
@@ -2387,24 +3331,36 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2387
3331
|
targetUrlAuthority: "retrieve-context-and-environment-probe",
|
|
2388
3332
|
};
|
|
2389
3333
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
|
|
2390
|
-
|
|
3334
|
+
steps.push({
|
|
3335
|
+
id: "capability-artifact",
|
|
3336
|
+
label: "写出 frontend-browser-capability.json",
|
|
3337
|
+
ok: true,
|
|
3338
|
+
detail: JSON.stringify(capability, null, 2),
|
|
3339
|
+
});
|
|
3340
|
+
return await finish({
|
|
2391
3341
|
ok: true,
|
|
2392
3342
|
stdout: JSON.stringify(capability),
|
|
2393
3343
|
stderr: "",
|
|
2394
3344
|
failureCategory: "success",
|
|
2395
3345
|
durationMs: Date.now() - started,
|
|
2396
|
-
};
|
|
3346
|
+
});
|
|
2397
3347
|
}
|
|
2398
3348
|
catch (error) {
|
|
2399
|
-
return {
|
|
3349
|
+
return await finish({
|
|
2400
3350
|
ok: false,
|
|
2401
3351
|
stdout: "",
|
|
2402
3352
|
stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
2403
3353
|
failureCategory: "tool-policy",
|
|
2404
3354
|
durationMs: Date.now() - started,
|
|
2405
|
-
};
|
|
3355
|
+
});
|
|
2406
3356
|
}
|
|
2407
3357
|
}
|
|
3358
|
+
if (shell?.frontendDesignPolicy) {
|
|
3359
|
+
return executeFrontendDesignPolicy(input, meta);
|
|
3360
|
+
}
|
|
3361
|
+
if (shell?.frontendWriterAdmission) {
|
|
3362
|
+
return executeFrontendWriterAdmission(input, meta);
|
|
3363
|
+
}
|
|
2408
3364
|
if (shell?.frontendPrewriteGate) {
|
|
2409
3365
|
const started = Date.now();
|
|
2410
3366
|
try {
|
|
@@ -2433,9 +3389,6 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2433
3389
|
};
|
|
2434
3390
|
}
|
|
2435
3391
|
}
|
|
2436
|
-
if (shell?.frontendLintBaseline) {
|
|
2437
|
-
return executeFrontendLintBaseline(input, meta);
|
|
2438
|
-
}
|
|
2439
3392
|
if (shell?.frontendVerificationBundle) {
|
|
2440
3393
|
return executeFrontendVerificationBundle(input, meta);
|
|
2441
3394
|
}
|
|
@@ -2465,6 +3418,12 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2465
3418
|
};
|
|
2466
3419
|
}
|
|
2467
3420
|
}
|
|
3421
|
+
if (shell?.frontendContractImport) {
|
|
3422
|
+
return executeFrontendContractImport(input, meta);
|
|
3423
|
+
}
|
|
3424
|
+
if (shell?.frontendCloseout) {
|
|
3425
|
+
return executeFrontendCloseout(input, meta);
|
|
3426
|
+
}
|
|
2468
3427
|
if (shell?.frontendTestCaseChecklist) {
|
|
2469
3428
|
return executeFrontendTestCaseChecklist(input, meta);
|
|
2470
3429
|
}
|
|
@@ -2686,53 +3645,23 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2686
3645
|
}
|
|
2687
3646
|
if (shell?.commands?.length === 1 &&
|
|
2688
3647
|
shell.commands[0] === "frontend-failure-assess-gate") {
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
stdout: formatFrontendFailureAssessStdout(result),
|
|
2697
|
-
stderr: "",
|
|
2698
|
-
failureCategory: "success",
|
|
2699
|
-
durationMs: Date.now() - started,
|
|
2700
|
-
};
|
|
2701
|
-
}
|
|
2702
|
-
catch (error) {
|
|
2703
|
-
return {
|
|
2704
|
-
ok: false,
|
|
2705
|
-
stdout: "",
|
|
2706
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2707
|
-
failureCategory: "invalid-output",
|
|
2708
|
-
durationMs: Date.now() - started,
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
3648
|
+
return {
|
|
3649
|
+
ok: false,
|
|
3650
|
+
stdout: "",
|
|
3651
|
+
stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
|
|
3652
|
+
failureCategory: "invalid-output",
|
|
3653
|
+
durationMs: 0,
|
|
3654
|
+
};
|
|
2711
3655
|
}
|
|
2712
3656
|
if (shell?.commands?.length === 1 &&
|
|
2713
3657
|
shell.commands[0] === "frontend-repair-contract-gate") {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
stdout: formatFrontendRepairContractStdout(result.assessment),
|
|
2722
|
-
stderr: "",
|
|
2723
|
-
failureCategory: "success",
|
|
2724
|
-
durationMs: Date.now() - started,
|
|
2725
|
-
};
|
|
2726
|
-
}
|
|
2727
|
-
catch (error) {
|
|
2728
|
-
return {
|
|
2729
|
-
ok: false,
|
|
2730
|
-
stdout: "",
|
|
2731
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2732
|
-
failureCategory: "invalid-output",
|
|
2733
|
-
durationMs: Date.now() - started,
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
3658
|
+
return {
|
|
3659
|
+
ok: false,
|
|
3660
|
+
stdout: "",
|
|
3661
|
+
stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
|
|
3662
|
+
failureCategory: "invalid-output",
|
|
3663
|
+
durationMs: 0,
|
|
3664
|
+
};
|
|
2736
3665
|
}
|
|
2737
3666
|
if (shell?.projectGovernanceGate) {
|
|
2738
3667
|
const started = Date.now();
|
|
@@ -2824,6 +3753,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2824
3753
|
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
2825
3754
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2826
3755
|
const failFast = shell.failFast !== false;
|
|
3756
|
+
const attemptNumber = input.attempt ?? 1;
|
|
2827
3757
|
const results = [];
|
|
2828
3758
|
const executedCommandKeys = new Set();
|
|
2829
3759
|
let beforeStatus;
|
|
@@ -2919,8 +3849,8 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2919
3849
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2920
3850
|
reportActivity: input.reportActivity,
|
|
2921
3851
|
outputArtifacts: {
|
|
2922
|
-
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2923
|
-
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
3852
|
+
stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
|
|
3853
|
+
stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
|
|
2924
3854
|
},
|
|
2925
3855
|
});
|
|
2926
3856
|
results.push(result);
|
|
@@ -2979,6 +3909,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2979
3909
|
schemaVersion: 1,
|
|
2980
3910
|
runId: meta.runId,
|
|
2981
3911
|
nodeId: input.task.id,
|
|
3912
|
+
attempt: attemptNumber,
|
|
2982
3913
|
workspaceFingerprint: workspaceFingerprint ?? null,
|
|
2983
3914
|
failFast,
|
|
2984
3915
|
commands: results.map((result) => ({
|
|
@@ -2997,6 +3928,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2997
3928
|
subchecks: parseCheckRepoSubcheckDurations(result),
|
|
2998
3929
|
})),
|
|
2999
3930
|
};
|
|
3931
|
+
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
|
|
3000
3932
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
|
|
3001
3933
|
const summaryMarkdown = buildShellResultSummaryMarkdown({
|
|
3002
3934
|
nodeId: input.task.id,
|
|
@@ -3048,8 +3980,57 @@ export async function executeDagShellNode(input, meta) {
|
|
|
3048
3980
|
stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
|
|
3049
3981
|
stderr: stderrParts.filter(Boolean).join("\n\n"),
|
|
3050
3982
|
failureCategory,
|
|
3983
|
+
...(firstFailure?.failureCategory
|
|
3984
|
+
? { rawFailureCategory: firstFailure.failureCategory }
|
|
3985
|
+
: {}),
|
|
3051
3986
|
durationMs: Date.now() - started,
|
|
3052
3987
|
// extra field consumed by node-execution if supported; also embedded in stdout marker
|
|
3053
3988
|
...{ commandResults },
|
|
3054
3989
|
};
|
|
3055
3990
|
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Enumerate the workspace's concrete file paths for glob writeSet expansion.
|
|
3993
|
+
*
|
|
3994
|
+
* Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
|
|
3995
|
+
* gitignored) so the expanded writeSet reflects real files the writer can
|
|
3996
|
+
* touch. Falls back to [] when the workspace is not a git repository or git
|
|
3997
|
+
* fails — in that case glob entries fail closed as non-concrete paths.
|
|
3998
|
+
*/
|
|
3999
|
+
async function collectWorkspaceFiles(workspaceRoot) {
|
|
4000
|
+
try {
|
|
4001
|
+
const files = await new Promise((resolve, reject) => {
|
|
4002
|
+
const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
|
|
4003
|
+
const chunks = [];
|
|
4004
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
4005
|
+
child.on("error", reject);
|
|
4006
|
+
child.on("close", (code) => {
|
|
4007
|
+
if (code === 0) {
|
|
4008
|
+
resolve(Buffer.concat(chunks)
|
|
4009
|
+
.toString("utf8")
|
|
4010
|
+
.split("\0")
|
|
4011
|
+
.filter(Boolean));
|
|
4012
|
+
}
|
|
4013
|
+
else {
|
|
4014
|
+
reject(new Error(`git ls-files exited ${String(code)}`));
|
|
4015
|
+
}
|
|
4016
|
+
});
|
|
4017
|
+
});
|
|
4018
|
+
return files.map((file) => file.replace(/\\/g, "/")).sort();
|
|
4019
|
+
}
|
|
4020
|
+
catch {
|
|
4021
|
+
return [];
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
export async function executeDagShellNode(input, meta) {
|
|
4025
|
+
const handler = namedShellHandler(input.task.shell);
|
|
4026
|
+
const result = await executeDagShellNodeInner(input, meta);
|
|
4027
|
+
if (handler) {
|
|
4028
|
+
await ensureShellPresetProcessReceipt({
|
|
4029
|
+
runDir: meta.runDir,
|
|
4030
|
+
nodeId: input.task.id,
|
|
4031
|
+
preset: handler,
|
|
4032
|
+
result,
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
return result;
|
|
4036
|
+
}
|