@tea-agent/loop-agent 0.41.0 → 0.41.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +267 -33
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +945 -217
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -7
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/pi-runtime.js +2 -3
- package/dist/worker/console/chat/routes.js +3 -12
- package/dist/worker/console/chat/semantic-activity.js +11 -12
- package/dist/worker/console/chat/session-stats.js +0 -96
- package/dist/worker/console/chat/turn-process.js +4 -28
- 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 +169 -7
- package/dist/worker/console/pi-plugins.js +1 -3
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-Bho4we4c.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-TBDkTeK0.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-BF_fZrn2.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-CCnPgUe2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-BCYZJLAT.js} +1 -1
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-D8K6adhU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-DkT1Q78b.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-DuhvpIr7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-BnK6dC49.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-DPz__z7r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-0kkppM2j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-BB41iK5t.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-CiVrxKHU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-C2h9Xje1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-3iOSHdbM.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-BN3A7tkk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-CTuBy0Or.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-wb6tUlMN.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-GP1Ivx8B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-ejrcK6L0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-YFkCCvUU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-B4rrvIaL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-C-YIZcUf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-BPF5wcL1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-CU5SzXjO.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-CpL414DE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-MrhLzNau.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-Bg4qFuUo.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-ColxSXLR.js} +1 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +1 -0
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +437 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-Czvtm6gc.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-DWYZcvph.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-KTLZRUjx.js} +1 -1
- package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-_Exn7bfl.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-Cxg8kX8V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-oKrlo2Bt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-XAeteiNd.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BTf1K-lp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BVbKtZhW.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-C5e4JFcr.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-Cz30hc29.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-DS33n6d0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-46w7v-9T.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-pbcD5SsZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-DuivYBSj.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-Cb9VZVIl.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-D2-mfOgR.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-DeS2aC76.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-WGsrFjxH.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/app/useRunProgress.js +2 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +0 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +3 -28
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +0 -106
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observability/read-model.js +0 -4
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/worker/observe/static/views/dag-inspector.js +7 -2
- package/dist/worker/observe/static/views/session-timeline.js +15 -46
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +932 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-read-budgets.js +12 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -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 +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -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 +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -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-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +685 -494
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +10 -2
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
- package/dist/worker/console/static/assets/index-Bih9w9h9.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +0 -22
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -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,6 +15,8 @@ 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, serializeCompactDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
|
|
19
|
+
import { checkFrontendSourceFidelityLedger } from "../workflows/dag/frontend-source-fidelity-ledger.js";
|
|
17
20
|
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
18
21
|
import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
|
|
19
22
|
import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
|
|
@@ -22,9 +25,15 @@ import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-ca
|
|
|
22
25
|
import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
|
|
23
26
|
import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
|
|
24
27
|
import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
|
|
25
|
-
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
+
import { captureFrontendWorktreeBaseline, formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
29
|
+
import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
|
|
30
|
+
import { classifyFrontendFailure, } from "../workflows/dag/frontend-repair.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";
|
|
28
37
|
import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
|
|
29
38
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
30
39
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
@@ -1230,33 +1239,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1230
1239
|
const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
|
|
1231
1240
|
const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
|
|
1232
1241
|
await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
|
|
1233
|
-
const
|
|
1234
|
-
const pytestLauncherAbsolute = path.join(meta.runDir, pytestLauncherRelative);
|
|
1235
|
-
await writeFile(pytestLauncherAbsolute, [
|
|
1236
|
-
"import os",
|
|
1237
|
-
"import pathlib",
|
|
1238
|
-
"import sys",
|
|
1239
|
-
"",
|
|
1240
|
-
"sys.path[0] = os.getcwd()",
|
|
1241
|
-
"import pytest",
|
|
1242
|
-
"",
|
|
1243
|
-
"if len(sys.argv) != 3:",
|
|
1244
|
-
" print('usage: backend-test-pytest-launcher.py <targets-file> <html-file>', file=sys.stderr)",
|
|
1245
|
-
" raise SystemExit(4)",
|
|
1246
|
-
"targets_path = pathlib.Path(sys.argv[1])",
|
|
1247
|
-
"html_path = pathlib.Path(sys.argv[2])",
|
|
1248
|
-
"try:",
|
|
1249
|
-
" targets = targets_path.read_text(encoding='utf-8').splitlines()",
|
|
1250
|
-
"except (OSError, UnicodeError) as error:",
|
|
1251
|
-
" print(f'backend-test targets unreadable: {error}', file=sys.stderr)",
|
|
1252
|
-
" raise SystemExit(4)",
|
|
1253
|
-
"if not targets or any(not target or target != target.strip() for target in targets):",
|
|
1254
|
-
" print('backend-test targets must contain non-empty exact pytest node IDs', file=sys.stderr)",
|
|
1255
|
-
" raise SystemExit(4)",
|
|
1256
|
-
"pytest_args = [*targets, '-v', '-p', 'no:cacheprovider', f'--html={html_path}', '--self-contained-html']",
|
|
1257
|
-
"raise SystemExit(pytest.main(pytest_args))",
|
|
1258
|
-
"",
|
|
1259
|
-
].join("\n"), "utf8");
|
|
1242
|
+
const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
|
|
1260
1243
|
// Split into short bash -c commands (aligned with markdown-environment).
|
|
1261
1244
|
// A single ultra-long compound command has been observed on Windows to
|
|
1262
1245
|
// exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
|
|
@@ -1333,11 +1316,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1333
1316
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
1334
1317
|
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
1335
1318
|
'echo "STEP=pytest-run"',
|
|
1336
|
-
`echo "- pytestCommand:
|
|
1319
|
+
`echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
1337
1320
|
`echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
|
|
1338
1321
|
`echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
|
|
1339
|
-
|
|
1340
|
-
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" "\${HARNESS_DAG_RUN_DIR}/${pytestLauncherRelative}" "\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}" "\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html"`,
|
|
1322
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
1341
1323
|
"STATUS=$?",
|
|
1342
1324
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
1343
1325
|
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
@@ -1895,6 +1877,45 @@ async function executePipelineCommands(input, meta, overrideCommands) {
|
|
|
1895
1877
|
}
|
|
1896
1878
|
return results;
|
|
1897
1879
|
}
|
|
1880
|
+
/** A+B (AC-008): derive unresolved verification entrypoints from committed plan
|
|
1881
|
+
* facts. A verification target is unresolved when its commandLabel does not
|
|
1882
|
+
* match any frozen command label from the verify shell evidence bundle.
|
|
1883
|
+
* Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
|
|
1884
|
+
* closed to [] so the router restarts the contract instead of inventing refs. */
|
|
1885
|
+
async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
|
|
1886
|
+
try {
|
|
1887
|
+
const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
1888
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
|
|
1889
|
+
const patch = restorePlanPatchFromCommittedFacts(records);
|
|
1890
|
+
const targets = patch
|
|
1891
|
+
?.verificationTargets;
|
|
1892
|
+
if (!Array.isArray(targets))
|
|
1893
|
+
return [];
|
|
1894
|
+
const frozen = new Set(frozenCommandLabels
|
|
1895
|
+
.map((label) => label.trim().toLowerCase())
|
|
1896
|
+
.filter(Boolean));
|
|
1897
|
+
const refs = [];
|
|
1898
|
+
for (const item of targets) {
|
|
1899
|
+
const target = item;
|
|
1900
|
+
const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
|
|
1901
|
+
if (commandLabel &&
|
|
1902
|
+
frozen.has(commandLabel.trim().toLowerCase())) {
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
const file = typeof target.file === "string" ? target.file : "";
|
|
1906
|
+
const symbol = typeof target.symbol === "string" ? target.symbol : "";
|
|
1907
|
+
const id = typeof target.id === "string" ? target.id : "";
|
|
1908
|
+
const ref = file || symbol || id;
|
|
1909
|
+
if (!ref)
|
|
1910
|
+
continue;
|
|
1911
|
+
refs.push({ ref, digest: sha256Hex(ref) });
|
|
1912
|
+
}
|
|
1913
|
+
return refs;
|
|
1914
|
+
}
|
|
1915
|
+
catch {
|
|
1916
|
+
return [];
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1898
1919
|
async function executeFrontendVerificationBundle(input, meta) {
|
|
1899
1920
|
const started = Date.now();
|
|
1900
1921
|
const shell = input.task.shell;
|
|
@@ -2049,75 +2070,861 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
2049
2070
|
catch (error) {
|
|
2050
2071
|
traceError = error instanceof Error ? error : new Error(String(error));
|
|
2051
2072
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2073
|
+
const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
|
|
2074
|
+
if (verifyFailed) {
|
|
2075
|
+
// M8: verify failure is TERMINAL. There is no same-run repair branch and
|
|
2076
|
+
// no frontend-repair-assessment.json; we only materialize failure
|
|
2077
|
+
// ownership facts (frontend-repair classification + recovery capsule
|
|
2078
|
+
// ownership) for the operation controller / recovery run.
|
|
2079
|
+
const failedNodeId = input.task.id;
|
|
2080
|
+
const failureCategory = firstFailure?.failureCategory ??
|
|
2081
|
+
lintSyntheticFailure?.failureCategory ??
|
|
2082
|
+
(traceError ? "invalid-output" : undefined) ??
|
|
2083
|
+
"invalid-output";
|
|
2084
|
+
const failureClass = classifyFrontendFailure({
|
|
2085
|
+
nodeId: failedNodeId,
|
|
2086
|
+
failureCategory,
|
|
2087
|
+
stdout: firstFailure?.stdout,
|
|
2088
|
+
stderr: firstFailure?.stderr ?? traceError?.message,
|
|
2089
|
+
});
|
|
2090
|
+
// A verify failure means the writer's candidate failed verification, so
|
|
2091
|
+
// the recovery ownership maps to the implement phase (frontend-recovery
|
|
2092
|
+
// capsule classification, fail-closed to implement-pi).
|
|
2093
|
+
const classifiedOwner = classifyFailureOwner({
|
|
2094
|
+
failureSource: "frontend-verify-shell",
|
|
2095
|
+
nodeId: failedNodeId,
|
|
2096
|
+
rawFailureCategory: failureCategory,
|
|
2097
|
+
});
|
|
2098
|
+
const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
|
|
2099
|
+
const restartPhase = classifyRestartPhase(failureOwner);
|
|
2100
|
+
// A+B (AC-008): verification-config routing. Produce structured
|
|
2101
|
+
// unresolved entrypoint refs from committed plan facts and the frozen
|
|
2102
|
+
// command labels; a non-empty unresolved set routes to
|
|
2103
|
+
// verification-config-decision and materializes the frozen
|
|
2104
|
+
// verification-config envelope. An empty/untyped signal fails closed to
|
|
2105
|
+
// contract-restart. The decision is recorded so the recovery controller
|
|
2106
|
+
// never re-classifies the same signal twice.
|
|
2107
|
+
let verificationConfigRoute;
|
|
2108
|
+
if (failureOwner === "verification-config") {
|
|
2109
|
+
const frozenCommandLabels = [
|
|
2110
|
+
...(bundle.staticEvidence.commandLabels ?? []),
|
|
2111
|
+
...(bundle.behaviorEvidence.commandLabels ?? []),
|
|
2112
|
+
...(bundle.mockEvidence?.commandLabels ?? []),
|
|
2113
|
+
];
|
|
2114
|
+
const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
|
|
2115
|
+
verificationConfigRoute = routeVerificationConfig({
|
|
2116
|
+
unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
|
|
2117
|
+
});
|
|
2118
|
+
const allowlistedResolvers = [
|
|
2119
|
+
...new Set(frozenCommandLabels
|
|
2120
|
+
.map((label) => label.trim())
|
|
2121
|
+
.filter(Boolean)),
|
|
2122
|
+
].sort();
|
|
2123
|
+
if (verificationConfigRoute.decision ===
|
|
2124
|
+
"verification-config-decision" &&
|
|
2125
|
+
unresolvedEntrypointRefs.length > 0 &&
|
|
2126
|
+
allowlistedResolvers.length > 0) {
|
|
2127
|
+
const envelope = buildFrontendHumanDecisionEnvelope({
|
|
2128
|
+
operationId: input.task.id,
|
|
2129
|
+
runId: meta.runId,
|
|
2130
|
+
operationRevision: 1,
|
|
2131
|
+
targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
|
|
2132
|
+
staleOnTargetDigestChange: true,
|
|
2133
|
+
kind: "verification-config",
|
|
2134
|
+
resumeAction: "select-verification-config",
|
|
2135
|
+
payload: {
|
|
2136
|
+
unresolvedEntrypointRefs,
|
|
2137
|
+
allowlistedResolvers,
|
|
2138
|
+
},
|
|
2139
|
+
});
|
|
2140
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const verifyFailure = {
|
|
2144
|
+
schemaVersion: 1,
|
|
2145
|
+
schemaId: "frontend-verify-failure-v1",
|
|
2146
|
+
nodeId: failedNodeId,
|
|
2147
|
+
failureCategory,
|
|
2148
|
+
failureClass,
|
|
2149
|
+
failureOwner,
|
|
2150
|
+
restartPhase,
|
|
2151
|
+
...(verificationConfigRoute
|
|
2152
|
+
? { verificationConfigRoute }
|
|
2153
|
+
: {}),
|
|
2154
|
+
commandResults,
|
|
2155
|
+
failedAt: new Date().toISOString(),
|
|
2156
|
+
};
|
|
2157
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
|
|
2158
|
+
return {
|
|
2159
|
+
ok: false,
|
|
2160
|
+
stdout: summarizeCommandResults(results).stdout,
|
|
2161
|
+
stderr: firstFailure?.stderr ||
|
|
2162
|
+
lintSyntheticFailure?.stderr ||
|
|
2163
|
+
traceError?.message ||
|
|
2164
|
+
"frontend verification failed",
|
|
2165
|
+
failureCategory,
|
|
2166
|
+
durationMs: Date.now() - started,
|
|
2167
|
+
...{ commandResults },
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
// A+B (AC-008): when lint is `require-clean` but the assessment proves
|
|
2171
|
+
// baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
|
|
2172
|
+
// whose only positive resume is `accept-baseline-debt`. Evidence stays
|
|
2173
|
+
// `baseline-debt` and is never rewritten to `passed`.
|
|
2174
|
+
if (lintAssessment?.status === "baseline-debt") {
|
|
2175
|
+
const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
|
|
2176
|
+
.filter((ref) => ref?.path && ref?.sha256)
|
|
2177
|
+
.map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
|
|
2178
|
+
if (baselineEvidenceRefs.length > 0) {
|
|
2179
|
+
const envelope = buildFrontendHumanDecisionEnvelope({
|
|
2180
|
+
operationId: input.task.id,
|
|
2181
|
+
runId: meta.runId,
|
|
2182
|
+
operationRevision: 1,
|
|
2183
|
+
targetDigest: lintAssessment.commandIdentity.sha256,
|
|
2184
|
+
staleOnTargetDigestChange: true,
|
|
2185
|
+
kind: "baseline-debt-acceptance",
|
|
2186
|
+
resumeAction: "accept-baseline-debt",
|
|
2187
|
+
payload: {
|
|
2188
|
+
baselineEvidenceRefs,
|
|
2189
|
+
candidateDigest: lintAssessment.commandIdentity.sha256,
|
|
2190
|
+
commandDigest: lintAssessment.commandIdentity.sha256,
|
|
2191
|
+
resultingPolicy: "allow-non-regression",
|
|
2192
|
+
},
|
|
2193
|
+
});
|
|
2194
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
ok: true,
|
|
2199
|
+
stdout: lintAssessment?.status === "baseline-debt"
|
|
2200
|
+
? "Frontend verification bundle: pass with lint baseline-debt"
|
|
2201
|
+
: "Frontend verification bundle: pass",
|
|
2202
|
+
stderr: "",
|
|
2203
|
+
failureCategory: "success",
|
|
2204
|
+
durationMs: Date.now() - started,
|
|
2205
|
+
...{ commandResults },
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
/* ------------------------------------------------------------------ *
|
|
2209
|
+
* M8 deterministic frontend shell nodes: design policy, writer *
|
|
2210
|
+
* admission, closeout. These replace the prewrite-gate authorization *
|
|
2211
|
+
* path and the Pi closeout node. *
|
|
2212
|
+
* ------------------------------------------------------------------ */
|
|
2213
|
+
async function readRunDirJson(runDir, relativePath) {
|
|
2214
|
+
return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
|
|
2215
|
+
}
|
|
2216
|
+
/** Committed typed review terminal kinds from the review node's typed event
|
|
2217
|
+
* store (AC-003: typed terminal tools are the only authoritative verdict). */
|
|
2218
|
+
async function collectReviewTerminalKinds(runDir, reviewNodeId) {
|
|
2219
|
+
try {
|
|
2220
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
|
|
2221
|
+
return records
|
|
2222
|
+
.filter((record) => record.phase === "committed")
|
|
2223
|
+
.map((record) => {
|
|
2224
|
+
const kind = record.fact.kind;
|
|
2225
|
+
return typeof kind === "string" ? kind : "";
|
|
2226
|
+
})
|
|
2227
|
+
.filter(Boolean);
|
|
2228
|
+
}
|
|
2229
|
+
catch {
|
|
2230
|
+
return [];
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
/** Committed typed design terminal kinds from the design review node's typed
|
|
2234
|
+
* event store (M8: the committed approve_design / request_design_changes fact
|
|
2235
|
+
* is the only authoritative verdict; the legacy first-line VERDICT text is
|
|
2236
|
+
* never read for writer admission). Fail-closed on a missing/unreadable store. */
|
|
2237
|
+
async function collectDesignTerminalKinds(runDir, designNodeId) {
|
|
2238
|
+
try {
|
|
2239
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2240
|
+
return records
|
|
2241
|
+
.filter((record) => record.phase === "committed")
|
|
2242
|
+
.map((record) => {
|
|
2243
|
+
const kind = record.fact.kind;
|
|
2244
|
+
return typeof kind === "string" ? kind : "";
|
|
2245
|
+
})
|
|
2246
|
+
.filter(Boolean);
|
|
2247
|
+
}
|
|
2248
|
+
catch {
|
|
2249
|
+
return [];
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2253
|
+
* `request_review_changes` fact. The typed issueCategory is mapped through the
|
|
2254
|
+
* frozen classifier; `unknown` (or an unreadable store) fails closed to
|
|
2255
|
+
* `undefined` so the runtime never trusts an untyped category. */
|
|
2256
|
+
async function collectReviewRestartPhase(runDir, reviewNodeId) {
|
|
2257
|
+
try {
|
|
2258
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
|
|
2259
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2260
|
+
record.fact.kind === "request_review_changes");
|
|
2261
|
+
const issueCategory = request?.fact
|
|
2262
|
+
?.issueCategory;
|
|
2263
|
+
return typeof issueCategory === "string"
|
|
2264
|
+
? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
|
|
2265
|
+
: undefined;
|
|
2266
|
+
}
|
|
2267
|
+
catch {
|
|
2268
|
+
return undefined;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2272
|
+
* `request_design_changes` fact. Mirrors the review collector. */
|
|
2273
|
+
/** Number of findings attached to the committed request_design_changes
|
|
2274
|
+
* terminal fact (0 when absent) — used for the admission advisory. */
|
|
2275
|
+
async function collectDesignReviewFindingCount(runDir, designNodeId) {
|
|
2276
|
+
try {
|
|
2277
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2278
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2279
|
+
record.fact.kind ===
|
|
2280
|
+
"request_design_changes");
|
|
2281
|
+
const findings = (request?.fact).findings;
|
|
2282
|
+
return Array.isArray(findings) ? findings.length : 0;
|
|
2283
|
+
}
|
|
2284
|
+
catch {
|
|
2285
|
+
return 0;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
async function collectDesignRestartPhase(runDir, designNodeId) {
|
|
2289
|
+
try {
|
|
2290
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2291
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2292
|
+
record.fact.kind === "request_design_changes");
|
|
2293
|
+
const issueCategory = request?.fact
|
|
2294
|
+
?.issueCategory;
|
|
2295
|
+
return typeof issueCategory === "string"
|
|
2296
|
+
? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
|
|
2297
|
+
: undefined;
|
|
2298
|
+
}
|
|
2299
|
+
catch {
|
|
2300
|
+
return undefined;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Resolve the deterministic runtime skeleton for the frontend plan patch from
|
|
2305
|
+
* the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
|
|
2306
|
+
* (the only schema-recognized persistent channel); absence is a fail-closed
|
|
2307
|
+
* generation/configuration defect, never a reason to fall back to text.
|
|
2308
|
+
*/
|
|
2309
|
+
function resolveFrontendContractSkeleton(spec, planNodeId) {
|
|
2310
|
+
const planTask = spec.tasks.find((task) => task.id === planNodeId);
|
|
2311
|
+
const skeleton = planTask?.structuredContractOutput?.skeleton;
|
|
2312
|
+
if (!skeleton || Object.keys(skeleton).length === 0) {
|
|
2313
|
+
throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
|
|
2314
|
+
}
|
|
2315
|
+
return skeleton;
|
|
2316
|
+
}
|
|
2317
|
+
async function collectFrontendSourceFreshness(input) {
|
|
2318
|
+
const entries = [];
|
|
2319
|
+
for (const source of input.sourceBinding?.sources ?? []) {
|
|
2320
|
+
entries.push({ path: source.path, sha256: source.sha256, fresh: true });
|
|
2321
|
+
}
|
|
2322
|
+
for (const evidencePath of input.contract.designEvidence.paths) {
|
|
2323
|
+
if (entries.some((entry) => entry.path === evidencePath))
|
|
2324
|
+
continue;
|
|
2325
|
+
try {
|
|
2326
|
+
const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
|
|
2327
|
+
entries.push({
|
|
2328
|
+
path: evidencePath,
|
|
2329
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
2330
|
+
fresh: true,
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
catch {
|
|
2334
|
+
entries.push({
|
|
2335
|
+
path: evidencePath,
|
|
2336
|
+
sha256: "0".repeat(64),
|
|
2337
|
+
fresh: false,
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
return entries;
|
|
2342
|
+
}
|
|
2343
|
+
/** R3/R4 openspec invariants: frozen candidates must exist and content
|
|
2344
|
+
* snapshots must not have drifted. Citation is enforced upstream by the
|
|
2345
|
+
* prewrite gate read-event evidence and ledger specReference conformance. */
|
|
2346
|
+
async function checkFrontendOpenspecFacts(input) {
|
|
2347
|
+
const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
2348
|
+
const candidateRoot = input.workspaceRoot;
|
|
2349
|
+
const missingCandidates = [];
|
|
2350
|
+
for (const candidate of frozenCandidatePaths) {
|
|
2351
|
+
try {
|
|
2352
|
+
await stat(path.join(candidateRoot, candidate));
|
|
2353
|
+
}
|
|
2354
|
+
catch {
|
|
2355
|
+
missingCandidates.push(candidate);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (missingCandidates.length > 0) {
|
|
2359
|
+
throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
|
|
2360
|
+
}
|
|
2361
|
+
const snapshots = input.config.openspecCandidateSnapshots;
|
|
2362
|
+
if (snapshots && snapshots.length > 0) {
|
|
2363
|
+
for (const snapshot of snapshots) {
|
|
2364
|
+
try {
|
|
2365
|
+
const bytes = await readFile(path.join(candidateRoot, snapshot.path));
|
|
2366
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
2367
|
+
if (actual !== snapshot.sha256) {
|
|
2368
|
+
throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
catch (error) {
|
|
2372
|
+
if (error instanceof Error &&
|
|
2373
|
+
error.message.startsWith("frontend design policy blocked")) {
|
|
2374
|
+
throw error;
|
|
2375
|
+
}
|
|
2376
|
+
throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
async function executeFrontendDesignPolicy(input, meta) {
|
|
2382
|
+
const started = Date.now();
|
|
2383
|
+
const shell = input.task.shell;
|
|
2384
|
+
const config = shell.frontendDesignPolicy;
|
|
2385
|
+
try {
|
|
2386
|
+
// 1. Materialize the canonical contract from committed typed facts. After
|
|
2387
|
+
// M9a the plan node's assistantText is a pure Markdown narrative; the
|
|
2388
|
+
// compile authority is the committed plan ledger ⊕ runtime skeleton.
|
|
2389
|
+
// Fail closed on missing committed facts — never fall back to text.
|
|
2390
|
+
const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
2391
|
+
// A+B (AC-001/003): fail-closed committed contract/scout typed facts.
|
|
2392
|
+
// Missing/uncommitted facts block here; never fall back to text parsing.
|
|
2393
|
+
const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
|
|
2394
|
+
const contractCheck = checkCommittedOriginFacts({
|
|
2395
|
+
records: committedContractFacts,
|
|
2396
|
+
origin: "contract",
|
|
2397
|
+
});
|
|
2398
|
+
if (!contractCheck.ok) {
|
|
2054
2399
|
return {
|
|
2055
2400
|
ok: false,
|
|
2056
|
-
stdout:
|
|
2057
|
-
stderr:
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2401
|
+
stdout: "",
|
|
2402
|
+
stderr: `frontend design policy blocked: ${contractCheck.reason}`,
|
|
2403
|
+
failureCategory: "invalid-output",
|
|
2404
|
+
durationMs: Date.now() - started,
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
|
|
2408
|
+
const scoutCheck = checkCommittedOriginFacts({
|
|
2409
|
+
records: committedScoutFacts,
|
|
2410
|
+
origin: "scout",
|
|
2411
|
+
});
|
|
2412
|
+
if (!scoutCheck.ok) {
|
|
2413
|
+
return {
|
|
2414
|
+
ok: false,
|
|
2415
|
+
stdout: "",
|
|
2416
|
+
stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
|
|
2417
|
+
failureCategory: "invalid-output",
|
|
2418
|
+
durationMs: Date.now() - started,
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
|
|
2422
|
+
const assembled = await buildContractFromCommittedFacts({
|
|
2423
|
+
runDir: meta.runDir,
|
|
2424
|
+
planNodeId: config.planFromNodeId,
|
|
2425
|
+
contractNodeId: "frontend-contract-pi",
|
|
2426
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2427
|
+
skeleton,
|
|
2428
|
+
});
|
|
2429
|
+
if (!assembled.ok) {
|
|
2430
|
+
return {
|
|
2431
|
+
ok: false,
|
|
2432
|
+
stdout: "",
|
|
2433
|
+
stderr: `frontend design policy blocked: ${assembled.reason}`,
|
|
2434
|
+
failureCategory: "invalid-output",
|
|
2435
|
+
durationMs: Date.now() - started,
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
const contract = assembled.canonical;
|
|
2439
|
+
// 2. Requirement id retention (R11): search the canonical contract
|
|
2440
|
+
// serialization instead of the plan node narrative.
|
|
2441
|
+
const contractText = serializeCompactDeterministicJson(contract);
|
|
2442
|
+
const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
|
|
2443
|
+
if (missingIds.length > 0) {
|
|
2444
|
+
return {
|
|
2445
|
+
ok: false,
|
|
2446
|
+
stdout: "",
|
|
2447
|
+
stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
|
|
2448
|
+
failureCategory: "invalid-output",
|
|
2449
|
+
durationMs: Date.now() - started,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
// 3. Mock strategy allowed + frozen command binding (R1).
|
|
2453
|
+
const mockStrategy = contract.mockApi.strategy;
|
|
2454
|
+
if (!config.allowedMockStrategies.includes(mockStrategy)) {
|
|
2455
|
+
return {
|
|
2456
|
+
ok: false,
|
|
2457
|
+
stdout: "",
|
|
2458
|
+
stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
|
|
2459
|
+
failureCategory: "invalid-output",
|
|
2460
|
+
durationMs: Date.now() - started,
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
|
|
2464
|
+
return {
|
|
2465
|
+
ok: false,
|
|
2466
|
+
stdout: "",
|
|
2467
|
+
stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
|
|
2468
|
+
failureCategory: "invalid-output",
|
|
2469
|
+
durationMs: Date.now() - started,
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
// 4. writeSet containment.
|
|
2473
|
+
if (config.implementationWriteSet) {
|
|
2474
|
+
const writeSet = new Set(config.implementationWriteSet);
|
|
2475
|
+
const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
|
|
2476
|
+
if (uncoveredTargets.length > 0) {
|
|
2477
|
+
return {
|
|
2478
|
+
ok: false,
|
|
2479
|
+
stdout: "",
|
|
2480
|
+
stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
|
|
2481
|
+
failureCategory: "invalid-output",
|
|
2482
|
+
durationMs: Date.now() - started,
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
const uncoveredVTs = contract.verificationTargets
|
|
2486
|
+
.filter((vt) => vt.type !== "static")
|
|
2487
|
+
.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
2488
|
+
if (uncoveredVTs.length > 0) {
|
|
2489
|
+
return {
|
|
2490
|
+
ok: false,
|
|
2491
|
+
stdout: "",
|
|
2492
|
+
stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
|
|
2493
|
+
failureCategory: "invalid-output",
|
|
2494
|
+
durationMs: Date.now() - started,
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
// 5. Source freshness (R4) + freshness entries.
|
|
2499
|
+
if (config.requireSourceFreshness) {
|
|
2500
|
+
if (!meta.spec.sourceBinding) {
|
|
2501
|
+
throw new Error("frontend design policy requires sourceBinding for freshness check");
|
|
2502
|
+
}
|
|
2503
|
+
await assertFrontendSourceBindingFresh({
|
|
2504
|
+
workspaceRoot: input.cwd,
|
|
2505
|
+
binding: meta.spec.sourceBinding,
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
// Byte freshness is necessary but not sufficient: require the same ledger
|
|
2509
|
+
// semantic checks as the shadow prewrite gate before production writer
|
|
2510
|
+
// admission can be materialized.
|
|
2511
|
+
if (meta.spec.sourceBinding) {
|
|
2512
|
+
const ledgerCheck = await checkFrontendSourceFidelityLedger({
|
|
2513
|
+
workspaceRoot: input.cwd,
|
|
2514
|
+
binding: meta.spec.sourceBinding,
|
|
2515
|
+
contractRequirements: contract.requirements.map((requirement) => ({
|
|
2516
|
+
id: requirement.id,
|
|
2517
|
+
sourceFragmentIds: requirement.sourceFragmentIds,
|
|
2518
|
+
sourceRefs: requirement.sourceRefs,
|
|
2519
|
+
})),
|
|
2520
|
+
});
|
|
2521
|
+
if (!ledgerCheck.ok) {
|
|
2522
|
+
return {
|
|
2523
|
+
ok: false,
|
|
2524
|
+
stdout: "",
|
|
2525
|
+
stderr: `frontend design policy blocked: ${ledgerCheck.failureReason ?? "source fidelity ledger validation failed"}`,
|
|
2526
|
+
failureCategory: "invalid-output",
|
|
2527
|
+
durationMs: Date.now() - started,
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const sourceFreshness = await collectFrontendSourceFreshness({
|
|
2532
|
+
workspaceRoot: input.cwd,
|
|
2533
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2534
|
+
contract,
|
|
2535
|
+
});
|
|
2536
|
+
// 6. Openspec invariants (R3): frozen candidates exist and snapshots have
|
|
2537
|
+
// not drifted. Citation is superseded by prewrite-gate read evidence.
|
|
2538
|
+
await checkFrontendOpenspecFacts({
|
|
2539
|
+
workspaceRoot: input.cwd,
|
|
2540
|
+
config,
|
|
2541
|
+
});
|
|
2542
|
+
// 7. Deterministic design policy (M8 D2: designApproval is optional here;
|
|
2543
|
+
// the design verdict is enforced by the writer-admission shell).
|
|
2544
|
+
const policyResult = evaluateFrontendDesignPolicy({
|
|
2545
|
+
contract,
|
|
2546
|
+
allowedMockStrategies: config.allowedMockStrategies,
|
|
2547
|
+
sourceFreshness,
|
|
2548
|
+
componentSpecCandidatePaths: config.openspecCandidatePaths ??
|
|
2549
|
+
config.componentSpecCandidatePaths ??
|
|
2550
|
+
[],
|
|
2551
|
+
allowedDependencies: config.allowedDependencies,
|
|
2552
|
+
writeSetPatterns: config.implementationWriteSet ?? [],
|
|
2553
|
+
});
|
|
2554
|
+
// 8. Write canonical contract + policy result.
|
|
2555
|
+
const contractArtifact = await writeFrontendImplementationContractArtifact({
|
|
2556
|
+
runDir: meta.runDir,
|
|
2557
|
+
outputDir: config.outputDir,
|
|
2558
|
+
artifactName: config.artifactName,
|
|
2559
|
+
canonical: contract,
|
|
2560
|
+
});
|
|
2561
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
|
|
2562
|
+
if (policyResult.classification === "blocked") {
|
|
2563
|
+
return {
|
|
2564
|
+
ok: false,
|
|
2565
|
+
stdout: "",
|
|
2566
|
+
stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
|
|
2567
|
+
failureCategory: "invalid-output",
|
|
2064
2568
|
durationMs: Date.now() - started,
|
|
2065
|
-
...{ commandResults },
|
|
2066
2569
|
};
|
|
2067
2570
|
}
|
|
2068
2571
|
return {
|
|
2069
2572
|
ok: true,
|
|
2070
|
-
stdout:
|
|
2071
|
-
|
|
2072
|
-
:
|
|
2573
|
+
stdout: [
|
|
2574
|
+
"Frontend design policy: approved findings=0",
|
|
2575
|
+
`Contract: ${contractArtifact.path}`,
|
|
2576
|
+
`Contract index: ${contractArtifact.reviewIndexPath}`,
|
|
2577
|
+
`Capacity diagnostic: ${contractArtifact.capacityDiagnosticPath} (non-blocking)`,
|
|
2578
|
+
].join("\n"),
|
|
2073
2579
|
stderr: "",
|
|
2074
2580
|
failureCategory: "success",
|
|
2075
2581
|
durationMs: Date.now() - started,
|
|
2076
|
-
...{ commandResults },
|
|
2077
2582
|
};
|
|
2078
2583
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
:
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
status: "FINISHED",
|
|
2088
|
-
failureCategory: firstFailure?.failureCategory ??
|
|
2089
|
-
lintSyntheticFailure?.failureCategory,
|
|
2090
|
-
// Keep classification scoped to the failed command. Aggregate
|
|
2091
|
-
// successful output may contain unrelated writeSet-like JSON.
|
|
2092
|
-
stdout: failureStdout,
|
|
2093
|
-
stderr: firstFailure?.stderr ?? lintSyntheticFailure?.stderr,
|
|
2094
|
-
commandResults,
|
|
2095
|
-
},
|
|
2096
|
-
});
|
|
2584
|
+
catch (error) {
|
|
2585
|
+
return {
|
|
2586
|
+
ok: false,
|
|
2587
|
+
stdout: "",
|
|
2588
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2589
|
+
failureCategory: "invalid-output",
|
|
2590
|
+
durationMs: Date.now() - started,
|
|
2591
|
+
};
|
|
2097
2592
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2593
|
+
}
|
|
2594
|
+
async function executeFrontendWriterAdmission(input, meta) {
|
|
2595
|
+
const started = Date.now();
|
|
2596
|
+
const shell = input.task.shell;
|
|
2597
|
+
const config = shell.frontendWriterAdmission;
|
|
2598
|
+
try {
|
|
2599
|
+
// 1. Design verdict handling (r12 policy change). The typed terminal
|
|
2600
|
+
// verdict is read from the committed typed design terminal fact, not
|
|
2601
|
+
// from the legacy first-line VERDICT text protocol (AC-003/AC-004).
|
|
2602
|
+
// A request_design_changes verdict no longer blocks writer admission:
|
|
2603
|
+
// it is downgraded to an advisory that rides along in the admission
|
|
2604
|
+
// receipt (and reaches implement via the review node's upstream
|
|
2605
|
+
// context). Blocking is owned by the deterministic gates (prewrite,
|
|
2606
|
+
// write guard) and the final code review. A missing typed terminal
|
|
2607
|
+
// verdict still fails closed. In small topology the design review node
|
|
2608
|
+
// is absent and the check is vacuous.
|
|
2609
|
+
let verdictPass = true;
|
|
2610
|
+
let designAdvisory;
|
|
2611
|
+
if (config.designReviewFromNodeId) {
|
|
2612
|
+
const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
|
|
2613
|
+
if (designKinds.length !== 1) {
|
|
2614
|
+
verdictPass = false;
|
|
2615
|
+
}
|
|
2616
|
+
else if (designKinds[0] !== "approve_design") {
|
|
2617
|
+
designAdvisory = {
|
|
2618
|
+
verdict: designKinds[0],
|
|
2619
|
+
restartPhase: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId),
|
|
2620
|
+
findingCount: await collectDesignReviewFindingCount(meta.runDir, config.designReviewFromNodeId),
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
// 2. Read the canonical contract + design-policy result.
|
|
2625
|
+
const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
|
|
2626
|
+
const contract = frontendImplementationContractSchema.parse(contractRaw);
|
|
2627
|
+
const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
|
|
2628
|
+
if (policyRaw.schemaId !== "frontend-design-policy-v1") {
|
|
2629
|
+
throw new Error("frontend writer admission: design policy result artifact invalid");
|
|
2630
|
+
}
|
|
2631
|
+
const designPolicyResult = {
|
|
2632
|
+
schemaId: "frontend-design-policy-v1",
|
|
2633
|
+
classification: policyRaw.classification === "approved" ? "approved" : "blocked",
|
|
2634
|
+
findings: policyRaw.findings ?? [],
|
|
2635
|
+
digest: "",
|
|
2636
|
+
};
|
|
2637
|
+
// 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
|
|
2638
|
+
await captureFrontendWorktreeBaseline({
|
|
2639
|
+
runDir: meta.runDir,
|
|
2640
|
+
workspaceRoot: input.cwd,
|
|
2641
|
+
});
|
|
2642
|
+
const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
|
|
2643
|
+
const baseline = (baselineRaw.files ?? [])
|
|
2644
|
+
.map((file) => ({
|
|
2645
|
+
path: file,
|
|
2646
|
+
sha256: baselineRaw.hashes?.[file] ?? null,
|
|
2647
|
+
}))
|
|
2648
|
+
.filter((entry) => entry.sha256 !== null);
|
|
2649
|
+
// 4. Writer-preceding lint baseline (R5) when lint is configured.
|
|
2650
|
+
let lintResults = [];
|
|
2651
|
+
if ((config.lintCommands?.length ?? 0) > 0) {
|
|
2652
|
+
for (const command of config.lintCommands) {
|
|
2653
|
+
const commandNumber = lintResults.length + 1;
|
|
2654
|
+
const result = await executeShellCommand({
|
|
2655
|
+
command,
|
|
2656
|
+
cwd: input.cwd,
|
|
2657
|
+
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
2658
|
+
envAllowlist: shell.envAllowlist,
|
|
2659
|
+
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2660
|
+
reportActivity: input.reportActivity,
|
|
2661
|
+
outputArtifacts: {
|
|
2662
|
+
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2663
|
+
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
2664
|
+
},
|
|
2665
|
+
});
|
|
2666
|
+
lintResults.push(result);
|
|
2667
|
+
}
|
|
2668
|
+
await materializeFrontendLintBaseline({
|
|
2669
|
+
runDir: meta.runDir,
|
|
2670
|
+
workspaceRoot: input.cwd,
|
|
2671
|
+
commands: config.lintCommands,
|
|
2672
|
+
results: lintResults,
|
|
2673
|
+
worktreeChanged: false,
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
// 5. Derive admission (writeSet + digests + findings). Glob entries in
|
|
2677
|
+
// the contract targets (e.g. `src/**` inherited from the skeleton) are
|
|
2678
|
+
// expanded against the concrete workspace files so the derived writeSet
|
|
2679
|
+
// contains only real paths the writer can touch.
|
|
2680
|
+
const workspaceFiles = await collectWorkspaceFiles(input.cwd);
|
|
2681
|
+
const admission = deriveFrontendWriterAdmissionShell({
|
|
2682
|
+
contract,
|
|
2683
|
+
designPolicyResult,
|
|
2684
|
+
baseline,
|
|
2685
|
+
frozenCommandLabels: config.frozenCommandLabels,
|
|
2686
|
+
allowedMockStrategies: config.allowedMockStrategies,
|
|
2687
|
+
workspaceFiles,
|
|
2688
|
+
});
|
|
2689
|
+
// 6. Design verdict enforcement: request-revision is a blocked admission.
|
|
2690
|
+
const failureSource = policyRaw.classification === "blocked"
|
|
2691
|
+
? "frontend-design-policy-shell"
|
|
2692
|
+
: "frontend-writer-admission-shell";
|
|
2693
|
+
const findings = verdictPass
|
|
2694
|
+
? admission.findings
|
|
2695
|
+
: [
|
|
2696
|
+
...admission.findings,
|
|
2697
|
+
{
|
|
2698
|
+
code: "design-approval-missing",
|
|
2699
|
+
message: "design review typed terminal verdict missing or ambiguous (fail closed)",
|
|
2700
|
+
},
|
|
2701
|
+
];
|
|
2702
|
+
const classification = findings.length === 0 && admission.classification === "accepted"
|
|
2703
|
+
? admission.classification
|
|
2704
|
+
: findings.length === 0 &&
|
|
2705
|
+
admission.classification === "requires-human-approval"
|
|
2706
|
+
? "requires-human-approval"
|
|
2707
|
+
: "blocked";
|
|
2708
|
+
const result = frontendWriterAdmissionResultV1Schema.parse({
|
|
2709
|
+
schemaVersion: 1,
|
|
2710
|
+
schemaId: "frontend-writer-admission-shell-v1",
|
|
2711
|
+
classification,
|
|
2712
|
+
writeSet: admission.writeSet,
|
|
2713
|
+
writeSetDigest: admission.writeSetDigest,
|
|
2714
|
+
baselineDigest: admission.baselineDigest,
|
|
2715
|
+
admissionDigest: admission.admissionDigest,
|
|
2716
|
+
...(admission.baselineDebtPolicy !== undefined
|
|
2717
|
+
? { baselineDebtPolicy: admission.baselineDebtPolicy }
|
|
2718
|
+
: {}),
|
|
2719
|
+
findings,
|
|
2720
|
+
...(designAdvisory ? { designReviewAdvisory: designAdvisory } : {}),
|
|
2721
|
+
failureSource,
|
|
2106
2722
|
});
|
|
2723
|
+
await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
|
|
2724
|
+
// A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
|
|
2725
|
+
// catalog whose candidate digest must match the admission digest before
|
|
2726
|
+
// the gate can transition from `requires-human-approval` to `accepted`.
|
|
2727
|
+
// Any digest drift resolves to `stale` (never silently accepted).
|
|
2728
|
+
if (classification === "requires-human-approval") {
|
|
2729
|
+
const receipt = evaluateHighAdmissionReceipt({
|
|
2730
|
+
candidateDigest: result.admissionDigest,
|
|
2731
|
+
admissionDigest: result.admissionDigest,
|
|
2732
|
+
});
|
|
2733
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
|
|
2734
|
+
if (receipt.status !== "accepted") {
|
|
2735
|
+
return {
|
|
2736
|
+
ok: false,
|
|
2737
|
+
stdout: "",
|
|
2738
|
+
stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
|
|
2739
|
+
failureCategory: "invalid-output",
|
|
2740
|
+
durationMs: Date.now() - started,
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
if (classification === "blocked") {
|
|
2745
|
+
return {
|
|
2746
|
+
ok: false,
|
|
2747
|
+
stdout: "",
|
|
2748
|
+
stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
|
|
2749
|
+
failureCategory: "invalid-output",
|
|
2750
|
+
durationMs: Date.now() - started,
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
return {
|
|
2754
|
+
ok: true,
|
|
2755
|
+
stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
|
|
2756
|
+
stderr: "",
|
|
2757
|
+
failureCategory: "success",
|
|
2758
|
+
durationMs: Date.now() - started,
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
catch (error) {
|
|
2762
|
+
return {
|
|
2763
|
+
ok: false,
|
|
2764
|
+
stdout: "",
|
|
2765
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2766
|
+
failureCategory: "invalid-output",
|
|
2767
|
+
durationMs: Date.now() - started,
|
|
2768
|
+
};
|
|
2107
2769
|
}
|
|
2770
|
+
}
|
|
2771
|
+
async function executeFrontendContractImport(input, meta) {
|
|
2772
|
+
const started = Date.now();
|
|
2773
|
+
const shell = input.task.shell;
|
|
2774
|
+
const config = shell.frontendContractImport;
|
|
2108
2775
|
try {
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2776
|
+
const binding = meta.spec.sourceBinding;
|
|
2777
|
+
if (!binding) {
|
|
2778
|
+
throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
|
|
2779
|
+
}
|
|
2780
|
+
if (config.requireSourceFreshness) {
|
|
2781
|
+
await assertFrontendSourceBindingFresh({
|
|
2782
|
+
workspaceRoot: input.cwd,
|
|
2783
|
+
binding,
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
const digest = createHash("sha256")
|
|
2787
|
+
.update(JSON.stringify({
|
|
2788
|
+
taskId: binding.taskId,
|
|
2789
|
+
sources: binding.sources,
|
|
2790
|
+
artifactName: config.artifactName,
|
|
2791
|
+
}))
|
|
2792
|
+
.digest("hex");
|
|
2793
|
+
const facts = {
|
|
2794
|
+
schemaVersion: 1,
|
|
2795
|
+
schemaId: "frontend-contract-import-v1",
|
|
2796
|
+
artifactName: config.artifactName,
|
|
2797
|
+
sourceBindingTaskId: binding.taskId,
|
|
2798
|
+
sourceCount: binding.sources.length,
|
|
2799
|
+
fresh: true,
|
|
2800
|
+
digest,
|
|
2801
|
+
};
|
|
2802
|
+
return {
|
|
2803
|
+
ok: true,
|
|
2804
|
+
stdout: JSON.stringify(facts, null, 2),
|
|
2805
|
+
stderr: "",
|
|
2806
|
+
failureCategory: "success",
|
|
2807
|
+
durationMs: Date.now() - started,
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
catch (error) {
|
|
2811
|
+
return {
|
|
2812
|
+
ok: false,
|
|
2813
|
+
stdout: "",
|
|
2814
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
2815
|
+
failureCategory: "invalid-output",
|
|
2816
|
+
durationMs: Date.now() - started,
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
async function executeFrontendCloseout(input, meta) {
|
|
2821
|
+
const started = Date.now();
|
|
2822
|
+
const shell = input.task.shell;
|
|
2823
|
+
const config = shell.frontendCloseout;
|
|
2824
|
+
try {
|
|
2825
|
+
const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
|
|
2826
|
+
if (context.contractRef?.path !==
|
|
2827
|
+
"contracts/frontend-implementation-contract.json") {
|
|
2828
|
+
throw new Error("frontend closeout missing canonical contract reference");
|
|
2829
|
+
}
|
|
2830
|
+
const contractRaw = await readFile(path.join(meta.runDir, context.contractRef.path), "utf8");
|
|
2831
|
+
const contractSha256 = createHash("sha256")
|
|
2832
|
+
.update(contractRaw)
|
|
2833
|
+
.digest("hex");
|
|
2834
|
+
if (contractSha256 !== context.contractRef.sha256) {
|
|
2835
|
+
throw new Error("frontend closeout canonical contract hash mismatch");
|
|
2836
|
+
}
|
|
2837
|
+
const contract = frontendImplementationContractSchema.parse(JSON.parse(contractRaw));
|
|
2838
|
+
const trace = context.verificationTrace;
|
|
2839
|
+
const verificationTargets = (trace?.targets ?? []).map((target) => ({
|
|
2840
|
+
status: target.status === "ok" ? "ok" : "failed",
|
|
2841
|
+
}));
|
|
2842
|
+
const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
|
|
2843
|
+
typeof trace?.mockCommandNodeId === "string" &&
|
|
2844
|
+
trace.mockCommandNodeId.length > 0 &&
|
|
2845
|
+
Array.isArray(trace?.mockCommandLabels) &&
|
|
2846
|
+
trace.mockCommandLabels.length > 0 &&
|
|
2847
|
+
Array.isArray(trace?.mockCommandTexts) &&
|
|
2848
|
+
trace.mockCommandTexts.length === trace.mockCommandLabels.length;
|
|
2849
|
+
const integrationFacts = deriveFrontendIntegrationFacts({
|
|
2850
|
+
mockStrategy: contract.mockApi.strategy,
|
|
2851
|
+
realApiEvidence: { present: false, passed: false },
|
|
2852
|
+
mockEvidenceBound,
|
|
2853
|
+
verificationTargets,
|
|
2112
2854
|
});
|
|
2113
|
-
|
|
2855
|
+
// Typed review terminal (AC-003) from the committed typed event store.
|
|
2856
|
+
const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
|
|
2857
|
+
const typedVerdict = typedKinds.length === 1 &&
|
|
2858
|
+
(typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
|
|
2859
|
+
? typedKinds[0]
|
|
2860
|
+
: null;
|
|
2861
|
+
// A+B (AC-009): request_review_changes issueCategory maps to the
|
|
2862
|
+
// deterministic §4.10 restart phase. Recorded as a run-owned audit
|
|
2863
|
+
// artifact so the frozen closeout schema is never widened; `unknown`
|
|
2864
|
+
// fails closed to no restart phase.
|
|
2865
|
+
const reviewRestartPhase = typedVerdict === "request_review_changes"
|
|
2866
|
+
? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
|
|
2867
|
+
: undefined;
|
|
2868
|
+
if (reviewRestartPhase !== undefined) {
|
|
2869
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
|
|
2870
|
+
schemaVersion: 1,
|
|
2871
|
+
nodeId: config.reviewFromNodeId,
|
|
2872
|
+
restartPhase: reviewRestartPhase,
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
const facts = frontendCloseoutFactsSchema.parse({
|
|
2876
|
+
schemaVersion: 1,
|
|
2877
|
+
schemaId: "frontend-closeout-facts-v1",
|
|
2878
|
+
coverageMatrix: verificationTargets.map((target, index) => ({
|
|
2879
|
+
verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
|
|
2880
|
+
status: target.status === "ok" ? "passed" : "failed",
|
|
2881
|
+
})),
|
|
2882
|
+
lintStatus: "unavailable",
|
|
2883
|
+
integrationFacts,
|
|
2884
|
+
reviewApproval: {
|
|
2885
|
+
typedVerdict,
|
|
2886
|
+
legacyVerdict: null,
|
|
2887
|
+
match: true,
|
|
2888
|
+
},
|
|
2889
|
+
cumulativeDiff: {
|
|
2890
|
+
cumulativeDigest: (typeof context.verificationTrace === "object" &&
|
|
2891
|
+
context.verificationTrace !== null &&
|
|
2892
|
+
"schemaId" in context.verificationTrace
|
|
2893
|
+
? deterministicSha256({ verificationTrace: context.verificationTrace })
|
|
2894
|
+
: deterministicSha256({})),
|
|
2895
|
+
empty: false,
|
|
2896
|
+
},
|
|
2897
|
+
notRun: { browser: "not-run", visual: "not-run" },
|
|
2898
|
+
pendingRealIntegration: integrationFacts !== "real-api",
|
|
2899
|
+
});
|
|
2900
|
+
const rendered = renderFrontendCloseout(facts);
|
|
2901
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
|
|
2902
|
+
// A+B (AC-010): materialize the deterministic frontend-test handoff.
|
|
2903
|
+
// browser/visual/runtime-a11y are always `not-run` and the handoff is
|
|
2904
|
+
// always `deferred-to-frontend-test`, so it never blocks implementation
|
|
2905
|
+
// and can never be rewritten into a passed result.
|
|
2906
|
+
const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
|
|
2907
|
+
const sourceDigest = meta.spec.sourceBinding
|
|
2908
|
+
? createHash("sha256")
|
|
2909
|
+
.update(JSON.stringify({
|
|
2910
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
2911
|
+
sources: meta.spec.sourceBinding.sources,
|
|
2912
|
+
}))
|
|
2913
|
+
.digest("hex")
|
|
2914
|
+
: deterministicSha256({});
|
|
2915
|
+
const handoff = buildFrontendTestHandoff({
|
|
2916
|
+
taskId: handoffTaskId,
|
|
2917
|
+
sourceDigest,
|
|
2918
|
+
candidateHash: facts.cumulativeDiff.cumulativeDigest,
|
|
2919
|
+
});
|
|
2920
|
+
await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
|
|
2921
|
+
await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
|
|
2114
2922
|
return {
|
|
2115
2923
|
ok: true,
|
|
2116
|
-
stdout:
|
|
2924
|
+
stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
|
|
2117
2925
|
stderr: "",
|
|
2118
2926
|
failureCategory: "success",
|
|
2119
2927
|
durationMs: Date.now() - started,
|
|
2120
|
-
...{ commandResults },
|
|
2121
2928
|
};
|
|
2122
2929
|
}
|
|
2123
2930
|
catch (error) {
|
|
@@ -2127,7 +2934,6 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
2127
2934
|
stderr: error instanceof Error ? error.message : String(error),
|
|
2128
2935
|
failureCategory: "invalid-output",
|
|
2129
2936
|
durationMs: Date.now() - started,
|
|
2130
|
-
...{ commandResults },
|
|
2131
2937
|
};
|
|
2132
2938
|
}
|
|
2133
2939
|
}
|
|
@@ -2452,68 +3258,6 @@ async function executeFrontendTestReports(input, meta) {
|
|
|
2452
3258
|
};
|
|
2453
3259
|
}
|
|
2454
3260
|
}
|
|
2455
|
-
async function executeFrontendLintBaseline(input, meta) {
|
|
2456
|
-
const started = Date.now();
|
|
2457
|
-
const shell = input.task.shell;
|
|
2458
|
-
const baseline = shell.frontendLintBaseline;
|
|
2459
|
-
const cwd = resolveShellCwd(input.cwd, shell.cwd);
|
|
2460
|
-
let beforeStatus;
|
|
2461
|
-
try {
|
|
2462
|
-
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
2463
|
-
}
|
|
2464
|
-
catch {
|
|
2465
|
-
beforeStatus = undefined;
|
|
2466
|
-
}
|
|
2467
|
-
const results = [];
|
|
2468
|
-
for (const command of baseline.lintCommands) {
|
|
2469
|
-
const commandNumber = results.length + 1;
|
|
2470
|
-
results.push(await executeShellCommand({
|
|
2471
|
-
command,
|
|
2472
|
-
cwd,
|
|
2473
|
-
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
2474
|
-
envAllowlist: shell.envAllowlist,
|
|
2475
|
-
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2476
|
-
outputArtifacts: {
|
|
2477
|
-
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2478
|
-
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
2479
|
-
},
|
|
2480
|
-
}));
|
|
2481
|
-
}
|
|
2482
|
-
let afterStatus;
|
|
2483
|
-
try {
|
|
2484
|
-
afterStatus = await readGitStatusPorcelain(input.cwd);
|
|
2485
|
-
}
|
|
2486
|
-
catch {
|
|
2487
|
-
afterStatus = undefined;
|
|
2488
|
-
}
|
|
2489
|
-
try {
|
|
2490
|
-
const materialized = await materializeFrontendLintBaseline({
|
|
2491
|
-
runDir: meta.runDir,
|
|
2492
|
-
workspaceRoot: input.cwd,
|
|
2493
|
-
commands: baseline.lintCommands,
|
|
2494
|
-
results,
|
|
2495
|
-
worktreeChanged: beforeStatus === undefined ||
|
|
2496
|
-
afterStatus === undefined ||
|
|
2497
|
-
beforeStatus !== afterStatus,
|
|
2498
|
-
});
|
|
2499
|
-
return {
|
|
2500
|
-
ok: true,
|
|
2501
|
-
stdout: `Frontend lint baseline: ${materialized.artifact.status}\nArtifact: ${materialized.ref.path}\nDiagnostics: ${materialized.artifact.diagnostics.length}`,
|
|
2502
|
-
stderr: materialized.artifact.reason ?? "",
|
|
2503
|
-
failureCategory: "success",
|
|
2504
|
-
durationMs: Date.now() - started,
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2507
|
-
catch (error) {
|
|
2508
|
-
return {
|
|
2509
|
-
ok: false,
|
|
2510
|
-
stdout: "",
|
|
2511
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2512
|
-
failureCategory: "invalid-output",
|
|
2513
|
-
durationMs: Date.now() - started,
|
|
2514
|
-
};
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
3261
|
async function writeShellPresetProcessReceipt(input) {
|
|
2518
3262
|
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
|
|
2519
3263
|
schemaVersion: 1,
|
|
@@ -2766,36 +3510,11 @@ async function executeDagShellNodeInner(input, meta) {
|
|
|
2766
3510
|
});
|
|
2767
3511
|
}
|
|
2768
3512
|
}
|
|
2769
|
-
if (shell?.
|
|
2770
|
-
|
|
2771
|
-
try {
|
|
2772
|
-
const result = await runFrontendPrewriteGate({
|
|
2773
|
-
runDir: meta.runDir,
|
|
2774
|
-
config: shell.frontendPrewriteGate,
|
|
2775
|
-
sourceBinding: meta.spec.sourceBinding,
|
|
2776
|
-
workspaceRoot: input.cwd,
|
|
2777
|
-
repoRoot: input.cwd,
|
|
2778
|
-
});
|
|
2779
|
-
return {
|
|
2780
|
-
ok: true,
|
|
2781
|
-
stdout: formatFrontendPrewriteGateStdout(result),
|
|
2782
|
-
stderr: "",
|
|
2783
|
-
failureCategory: "success",
|
|
2784
|
-
durationMs: Date.now() - started,
|
|
2785
|
-
};
|
|
2786
|
-
}
|
|
2787
|
-
catch (error) {
|
|
2788
|
-
return {
|
|
2789
|
-
ok: false,
|
|
2790
|
-
stdout: "",
|
|
2791
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2792
|
-
failureCategory: "invalid-output",
|
|
2793
|
-
durationMs: Date.now() - started,
|
|
2794
|
-
};
|
|
2795
|
-
}
|
|
3513
|
+
if (shell?.frontendDesignPolicy) {
|
|
3514
|
+
return executeFrontendDesignPolicy(input, meta);
|
|
2796
3515
|
}
|
|
2797
|
-
if (shell?.
|
|
2798
|
-
return
|
|
3516
|
+
if (shell?.frontendWriterAdmission) {
|
|
3517
|
+
return executeFrontendWriterAdmission(input, meta);
|
|
2799
3518
|
}
|
|
2800
3519
|
if (shell?.frontendVerificationBundle) {
|
|
2801
3520
|
return executeFrontendVerificationBundle(input, meta);
|
|
@@ -2826,6 +3545,12 @@ async function executeDagShellNodeInner(input, meta) {
|
|
|
2826
3545
|
};
|
|
2827
3546
|
}
|
|
2828
3547
|
}
|
|
3548
|
+
if (shell?.frontendContractImport) {
|
|
3549
|
+
return executeFrontendContractImport(input, meta);
|
|
3550
|
+
}
|
|
3551
|
+
if (shell?.frontendCloseout) {
|
|
3552
|
+
return executeFrontendCloseout(input, meta);
|
|
3553
|
+
}
|
|
2829
3554
|
if (shell?.frontendTestCaseChecklist) {
|
|
2830
3555
|
return executeFrontendTestCaseChecklist(input, meta);
|
|
2831
3556
|
}
|
|
@@ -3048,53 +3773,23 @@ async function executeDagShellNodeInner(input, meta) {
|
|
|
3048
3773
|
}
|
|
3049
3774
|
if (shell?.commands?.length === 1 &&
|
|
3050
3775
|
shell.commands[0] === "frontend-failure-assess-gate") {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
stdout: formatFrontendFailureAssessStdout(result),
|
|
3059
|
-
stderr: "",
|
|
3060
|
-
failureCategory: "success",
|
|
3061
|
-
durationMs: Date.now() - started,
|
|
3062
|
-
};
|
|
3063
|
-
}
|
|
3064
|
-
catch (error) {
|
|
3065
|
-
return {
|
|
3066
|
-
ok: false,
|
|
3067
|
-
stdout: "",
|
|
3068
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
3069
|
-
failureCategory: "invalid-output",
|
|
3070
|
-
durationMs: Date.now() - started,
|
|
3071
|
-
};
|
|
3072
|
-
}
|
|
3776
|
+
return {
|
|
3777
|
+
ok: false,
|
|
3778
|
+
stdout: "",
|
|
3779
|
+
stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
|
|
3780
|
+
failureCategory: "invalid-output",
|
|
3781
|
+
durationMs: 0,
|
|
3782
|
+
};
|
|
3073
3783
|
}
|
|
3074
3784
|
if (shell?.commands?.length === 1 &&
|
|
3075
3785
|
shell.commands[0] === "frontend-repair-contract-gate") {
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
stdout: formatFrontendRepairContractStdout(result.assessment),
|
|
3084
|
-
stderr: "",
|
|
3085
|
-
failureCategory: "success",
|
|
3086
|
-
durationMs: Date.now() - started,
|
|
3087
|
-
};
|
|
3088
|
-
}
|
|
3089
|
-
catch (error) {
|
|
3090
|
-
return {
|
|
3091
|
-
ok: false,
|
|
3092
|
-
stdout: "",
|
|
3093
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
3094
|
-
failureCategory: "invalid-output",
|
|
3095
|
-
durationMs: Date.now() - started,
|
|
3096
|
-
};
|
|
3097
|
-
}
|
|
3786
|
+
return {
|
|
3787
|
+
ok: false,
|
|
3788
|
+
stdout: "",
|
|
3789
|
+
stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
|
|
3790
|
+
failureCategory: "invalid-output",
|
|
3791
|
+
durationMs: 0,
|
|
3792
|
+
};
|
|
3098
3793
|
}
|
|
3099
3794
|
if (shell?.projectGovernanceGate) {
|
|
3100
3795
|
const started = Date.now();
|
|
@@ -3421,6 +4116,39 @@ async function executeDagShellNodeInner(input, meta) {
|
|
|
3421
4116
|
...{ commandResults },
|
|
3422
4117
|
};
|
|
3423
4118
|
}
|
|
4119
|
+
/**
|
|
4120
|
+
* Enumerate the workspace's concrete file paths for glob writeSet expansion.
|
|
4121
|
+
*
|
|
4122
|
+
* Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
|
|
4123
|
+
* gitignored) so the expanded writeSet reflects real files the writer can
|
|
4124
|
+
* touch. Falls back to [] when the workspace is not a git repository or git
|
|
4125
|
+
* fails — in that case glob entries fail closed as non-concrete paths.
|
|
4126
|
+
*/
|
|
4127
|
+
async function collectWorkspaceFiles(workspaceRoot) {
|
|
4128
|
+
try {
|
|
4129
|
+
const files = await new Promise((resolve, reject) => {
|
|
4130
|
+
const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
|
|
4131
|
+
const chunks = [];
|
|
4132
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
4133
|
+
child.on("error", reject);
|
|
4134
|
+
child.on("close", (code) => {
|
|
4135
|
+
if (code === 0) {
|
|
4136
|
+
resolve(Buffer.concat(chunks)
|
|
4137
|
+
.toString("utf8")
|
|
4138
|
+
.split("\0")
|
|
4139
|
+
.filter(Boolean));
|
|
4140
|
+
}
|
|
4141
|
+
else {
|
|
4142
|
+
reject(new Error(`git ls-files exited ${String(code)}`));
|
|
4143
|
+
}
|
|
4144
|
+
});
|
|
4145
|
+
});
|
|
4146
|
+
return files.map((file) => file.replace(/\\/g, "/")).sort();
|
|
4147
|
+
}
|
|
4148
|
+
catch {
|
|
4149
|
+
return [];
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
3424
4152
|
export async function executeDagShellNode(input, meta) {
|
|
3425
4153
|
const handler = namedShellHandler(input.task.shell);
|
|
3426
4154
|
const result = await executeDagShellNodeInner(input, meta);
|