@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3
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 +1645 -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 +1150 -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 +42 -16
- 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 +347 -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 +258 -393
- 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 +438 -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
|
@@ -8,10 +8,10 @@ import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
|
|
|
8
8
|
import { bindDagRerunFeedback } from "./rerun-feedback.js";
|
|
9
9
|
import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
10
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
11
|
-
import {
|
|
11
|
+
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, DEFAULT_FRONTEND_SPEC_ROOTS, extractTaskSourceFrontendSpecPaths, } from "../../shared/openspec-spec.js";
|
|
12
12
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
13
13
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
14
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY,
|
|
14
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
15
15
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
16
16
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
17
17
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -19,9 +19,8 @@ import { mergeDocumentIndexCompanions } from "../../governance/document-index-cl
|
|
|
19
19
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
20
20
|
import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-embedded.js";
|
|
21
21
|
import { discoverProjectGovernancePresence } from "./project-governance-context.js";
|
|
22
|
-
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
22
|
+
import { CANONICAL_TASK_ID_PATTERN, getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
23
23
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
24
|
-
import { REQUIREMENT_FACT_ROLES } from "../../task/source-prepare/artifact-meta.js";
|
|
25
24
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
26
25
|
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
27
26
|
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
@@ -33,8 +32,11 @@ import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-c
|
|
|
33
32
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
34
33
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
35
34
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
36
|
-
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
37
|
-
import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
35
|
+
import { discoverFrontendProjectCapability, resolveFrontendSpecRootAliases, } from "./frontend-project-capability.js";
|
|
36
|
+
import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
37
|
+
import { computeFrontendShapeSourceDigest, parseFrontendShapeTransitionCapsule, resolveFrontendTaskShape, } from "./frontend-shape.js";
|
|
38
|
+
import { discoverLatestCommittedFrontendShapeCapsule } from "./frontend-shape-capsule-store.js";
|
|
39
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "./frontend-verification-trace.js";
|
|
38
40
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
39
41
|
const REQUIREMENT_FILE = "需求.md";
|
|
40
42
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
@@ -216,6 +218,28 @@ async function hasDirectDependency(repoRoot, depName) {
|
|
|
216
218
|
return false;
|
|
217
219
|
}
|
|
218
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Collect the project's declared direct dependencies (dependencies +
|
|
223
|
+
* devDependencies names) from package.json. Used to freeze the frontend
|
|
224
|
+
* design-policy allowedDependencies set: the model's dependency policy must
|
|
225
|
+
* not be rejected for mentioning an already-declared dependency, and a
|
|
226
|
+
* dependency not present in the manifest is a genuine unauthorized addition.
|
|
227
|
+
* Returns [] when package.json is unreadable (no allowlist → the dependency
|
|
228
|
+
* check is skipped rather than rejecting prose tokens).
|
|
229
|
+
*/
|
|
230
|
+
async function collectDeclaredDependencies(repoRoot) {
|
|
231
|
+
try {
|
|
232
|
+
const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
|
|
233
|
+
const pkg = JSON.parse(raw);
|
|
234
|
+
return [
|
|
235
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
236
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
219
243
|
/** Check whether handler/fixture/bootstrap files exist for known mock frameworks. */
|
|
220
244
|
async function discoverMockHandlerFiles(repoRoot, serviceRoot) {
|
|
221
245
|
const exactCandidates = [
|
|
@@ -1282,11 +1306,15 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
1282
1306
|
: allowed,
|
|
1283
1307
|
};
|
|
1284
1308
|
}
|
|
1309
|
+
const FRONTEND_NO_STATIC_VERIFICATION_MARKER = `node -e "console.log('${FRONTEND_NO_VERIFICATION_MARKER_TEXT}; static/behavior verification not-run')"`;
|
|
1285
1310
|
/**
|
|
1286
1311
|
* Resolve frontend verification fallbacks from the target project's own
|
|
1287
1312
|
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1313
|
+
* repoRoot, so those fixtures retain the historical generic fallback. A real
|
|
1314
|
+
* project (repoRoot set) without a readable package.json has no scripts to
|
|
1315
|
+
* invoke; the generic fallback would only inject commands guaranteed to fail
|
|
1316
|
+
* at verify time, so such projects fall through to the tsc probe and may end
|
|
1317
|
+
* up with no fallback commands plus a generation-time advisory.
|
|
1290
1318
|
*/
|
|
1291
1319
|
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
1292
1320
|
const genericFallback = {
|
|
@@ -1303,10 +1331,8 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1303
1331
|
}
|
|
1304
1332
|
}
|
|
1305
1333
|
catch {
|
|
1306
|
-
|
|
1334
|
+
// No readable manifest: leave scripts unset and keep probing.
|
|
1307
1335
|
}
|
|
1308
|
-
if (!scripts)
|
|
1309
|
-
return genericFallback;
|
|
1310
1336
|
let packageManager = "npm";
|
|
1311
1337
|
for (const [lockfile, manager] of [
|
|
1312
1338
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -1383,19 +1409,66 @@ function toDagSourcePath(sources, absolutePath) {
|
|
|
1383
1409
|
absolutePath,
|
|
1384
1410
|
});
|
|
1385
1411
|
}
|
|
1386
|
-
|
|
1412
|
+
/**
|
|
1413
|
+
* Declaration-line shapes that assert a requirement id: list items
|
|
1414
|
+
* ("- AC-1: ..."), numbered/ordered acceptance entries ("1. AC-1 ...",
|
|
1415
|
+
* "1、AC-1"), table rows ("| AC-1 | ..."), and explicit key/value or
|
|
1416
|
+
* heading declarations ("AC-1:...", "id: AC-1"). Ids that merely appear
|
|
1417
|
+
* inside narrative prose (e.g. a fixture run id mentioned in background:
|
|
1418
|
+
* "夹具 run 2026-08-23-req-f03535aa 已存在") are incidental mentions, not
|
|
1419
|
+
* declared requirements, and must not pollute sourceBinding.requirementIds.
|
|
1420
|
+
*/
|
|
1421
|
+
const REQUIREMENT_ID_DECLARATION_LINE = /^\s*(?:[-+*]\s+|\d+[.、))]\s*|\|\s*)?(?:[-A-Z0-9]+\s*[::]\s*)?(?:\*\*)?(?:REQ|BR|AC)-/i;
|
|
1422
|
+
function isRequirementIdDeclarationLine(line) {
|
|
1423
|
+
return REQUIREMENT_ID_DECLARATION_LINE.test(line);
|
|
1424
|
+
}
|
|
1425
|
+
function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown) {
|
|
1387
1426
|
const ids = [];
|
|
1388
1427
|
const seen = new Set();
|
|
1389
|
-
|
|
1428
|
+
const collect = (markdown, declarationsOnly) => {
|
|
1390
1429
|
if (!markdown)
|
|
1391
|
-
|
|
1392
|
-
|
|
1430
|
+
return;
|
|
1431
|
+
const source = declarationsOnly
|
|
1432
|
+
? markdown
|
|
1433
|
+
.split(/\r?\n/)
|
|
1434
|
+
.filter((line) => isRequirementIdDeclarationLine(line))
|
|
1435
|
+
.join("\n")
|
|
1436
|
+
: markdown;
|
|
1437
|
+
for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1393
1438
|
const id = match[0].toUpperCase();
|
|
1394
1439
|
if (!seen.has(id)) {
|
|
1395
1440
|
seen.add(id);
|
|
1396
1441
|
ids.push(id);
|
|
1397
1442
|
}
|
|
1398
1443
|
}
|
|
1444
|
+
};
|
|
1445
|
+
// The requirement document itself is human-authored acceptance prose:
|
|
1446
|
+
// only declaration lines declare requirements there. If the document has
|
|
1447
|
+
// no declaration-shaped lines at all (minimal free-form tasks like
|
|
1448
|
+
// "covers AC-001"), fall back to its full text so bindings never go
|
|
1449
|
+
// missing for unstructured input. References and constraints are bound
|
|
1450
|
+
// documents (acceptance yaml, analysis docs) whose ids are authoritative
|
|
1451
|
+
// wherever they appear — keep full scan for them.
|
|
1452
|
+
const declarationLines = requirementMarkdown
|
|
1453
|
+
.split(/\r?\n/)
|
|
1454
|
+
.filter((line) => isRequirementIdDeclarationLine(line));
|
|
1455
|
+
collect(requirementMarkdown, declarationLines.length > 0);
|
|
1456
|
+
for (const markdown of fallbackMarkdown) {
|
|
1457
|
+
if (markdown)
|
|
1458
|
+
collect(markdown, false);
|
|
1459
|
+
}
|
|
1460
|
+
return ids;
|
|
1461
|
+
}
|
|
1462
|
+
/** Extract ids from an explicit authoritative section (full scan inside it). */
|
|
1463
|
+
function extractSectionRequirementIds(sectionMarkdown) {
|
|
1464
|
+
const ids = [];
|
|
1465
|
+
const seen = new Set();
|
|
1466
|
+
for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1467
|
+
const id = match[0].toUpperCase();
|
|
1468
|
+
if (!seen.has(id)) {
|
|
1469
|
+
seen.add(id);
|
|
1470
|
+
ids.push(id);
|
|
1471
|
+
}
|
|
1399
1472
|
}
|
|
1400
1473
|
return ids;
|
|
1401
1474
|
}
|
|
@@ -1409,7 +1482,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1409
1482
|
// Without it, preserve legacy full-scan behavior (requirement + references).
|
|
1410
1483
|
const sectionMatch = requirementMarkdown.match(/(?:^|\n)##\s*Acceptance References\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i);
|
|
1411
1484
|
if (sectionMatch?.[1]) {
|
|
1412
|
-
const fromSection =
|
|
1485
|
+
const fromSection = extractSectionRequirementIds(sectionMatch[1]);
|
|
1413
1486
|
if (fromSection.length > 0)
|
|
1414
1487
|
return fromSection;
|
|
1415
1488
|
}
|
|
@@ -1502,27 +1575,11 @@ function buildSourceContextBlock(sources) {
|
|
|
1502
1575
|
.relative(path.join(sources.taskDir, "source"), reference.path)
|
|
1503
1576
|
.replaceAll(path.sep, "/");
|
|
1504
1577
|
const referenceRef = toDagSourcePath(sources, reference.path);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const referenceExcerpt = isFactRole
|
|
1511
|
-
? {
|
|
1512
|
-
text: reference.markdown.trim(),
|
|
1513
|
-
truncated: false,
|
|
1514
|
-
originalChars: reference.markdown.trim().length,
|
|
1515
|
-
maxChars: Number.POSITIVE_INFINITY,
|
|
1516
|
-
}
|
|
1517
|
-
: excerptMarkdown(reference.markdown, {
|
|
1518
|
-
sourceRef: referenceRef,
|
|
1519
|
-
});
|
|
1520
|
-
boundReadPaths.push(`- reference ${relativePath}${isFactRole ? " (full source)" : ""}: ${referenceRef}`);
|
|
1521
|
-
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, ...(isFactRole
|
|
1522
|
-
? [
|
|
1523
|
-
`Full source injected (role: ${reference.role}) — complete and authoritative; no excerpt truncation applied.`,
|
|
1524
|
-
]
|
|
1525
|
-
: []), referenceExcerpt.text);
|
|
1578
|
+
const referenceExcerpt = excerptMarkdown(reference.markdown, {
|
|
1579
|
+
sourceRef: referenceRef,
|
|
1580
|
+
});
|
|
1581
|
+
boundReadPaths.push(`- reference ${relativePath}: ${referenceRef}`);
|
|
1582
|
+
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, referenceExcerpt.text);
|
|
1526
1583
|
}
|
|
1527
1584
|
parts.push("## Bound source read paths", ...boundReadPaths, "Use these repository-readable paths for any Pi read-tool calls. Bound files under `.harness/tasks/<taskId>/source/**` are read-only inputs: reading them is allowed even though writing `.harness/**` is forbidden.", "Never resolve task-relative citations such as `source/需求.md` or `source/references/*` against the repository root, invent `source/<taskId>/...`, search for substitutes, or fall back to `docs/**` when a bound read fails.", "## Task config summary", `- taskId: ${sources.taskConfig.taskId}`, `- flow: ${sources.taskConfig.flow}`, `- complexity: ${sources.taskConfig.complexity}`, `- contextProfile: ${sources.taskConfig.contextProfile}`, `- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none — review before execute)"}`, `- forbiddenPaths: ${sources.taskConfig.forbiddenPaths.join(", ") || "(none)"}`, '- Pi DAG nodes are read-only unless toolProfile="write" is explicitly selected for a bounded writer node.', "- Agent DAG read-only nodes must not write root artifacts/**; root artifacts/ is not a per-node scratchpad.", `- Derived execution contract and immutable references live under the Bound source read paths above (not as repo-root \`source/...\`).`);
|
|
1528
1585
|
if (sources.taskConfig.hardConstraints.length > 0) {
|
|
@@ -1530,21 +1587,6 @@ function buildSourceContextBlock(sources) {
|
|
|
1530
1587
|
}
|
|
1531
1588
|
return parts.join("\n\n");
|
|
1532
1589
|
}
|
|
1533
|
-
/**
|
|
1534
|
-
* Planner/reviewer nodes already receive the contract/scout artifacts. They
|
|
1535
|
-
* only need stable task-source pointers for an unresolved or conflicting
|
|
1536
|
-
* field, not a second inline copy of every fact-source document.
|
|
1537
|
-
*/
|
|
1538
|
-
function buildArtifactFirstSourceAccessBlock(sources) {
|
|
1539
|
-
const binding = buildDagSourceBinding(sources);
|
|
1540
|
-
return [
|
|
1541
|
-
"## Bound task-source access (read only when upstream artifacts are missing or conflict)",
|
|
1542
|
-
...binding.sources.map((source) => `- ${source.kind}: ${source.path}`),
|
|
1543
|
-
"Use these exact repository-readable paths. Do not search for substitutes or read runtime implementation code to diagnose a format retry.",
|
|
1544
|
-
`- taskId: ${binding.taskId}`,
|
|
1545
|
-
`- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none)"}`,
|
|
1546
|
-
].join("\n");
|
|
1547
|
-
}
|
|
1548
1590
|
async function loadMaterializedSourceReferences(sourceDir) {
|
|
1549
1591
|
const referenceDir = path.join(sourceDir, REFERENCE_DIRECTORY);
|
|
1550
1592
|
const referencePaths = [];
|
|
@@ -1575,49 +1617,10 @@ async function loadMaterializedSourceReferences(sourceDir) {
|
|
|
1575
1617
|
}
|
|
1576
1618
|
await collect(referenceDir);
|
|
1577
1619
|
referencePaths.sort((left, right) => left.localeCompare(right));
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
.replaceAll(path.sep, "/");
|
|
1583
|
-
const role = roleByPath.get(relative);
|
|
1584
|
-
return {
|
|
1585
|
-
path: filePath,
|
|
1586
|
-
markdown: await readFile(filePath, "utf-8"),
|
|
1587
|
-
...(role !== undefined ? { role } : {}),
|
|
1588
|
-
};
|
|
1589
|
-
}));
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* Reads `source-manifest.json` into a materializedPath → role map so the DAG
|
|
1593
|
-
* generator can tell fact-source references (requirement/acceptance) apart
|
|
1594
|
-
* from archival ones (analysis/clarification/design). A missing or malformed
|
|
1595
|
-
* manifest yields an empty map; such references keep the bounded-excerpt
|
|
1596
|
-
* treatment instead of being injected in full.
|
|
1597
|
-
*/
|
|
1598
|
-
async function loadReferenceRoles(sourceDir) {
|
|
1599
|
-
const roleByPath = new Map();
|
|
1600
|
-
const manifestPath = path.join(sourceDir, "source-manifest.json");
|
|
1601
|
-
let raw;
|
|
1602
|
-
try {
|
|
1603
|
-
raw = await readFile(manifestPath, "utf-8");
|
|
1604
|
-
}
|
|
1605
|
-
catch {
|
|
1606
|
-
return roleByPath;
|
|
1607
|
-
}
|
|
1608
|
-
try {
|
|
1609
|
-
const manifest = JSON.parse(raw);
|
|
1610
|
-
for (const document of manifest.documents ?? []) {
|
|
1611
|
-
if (typeof document.materializedPath === "string" &&
|
|
1612
|
-
typeof document.role === "string") {
|
|
1613
|
-
roleByPath.set(document.materializedPath.replaceAll(path.sep, "/"), document.role);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
catch {
|
|
1618
|
-
// A malformed manifest must never break reference injection.
|
|
1619
|
-
}
|
|
1620
|
-
return roleByPath;
|
|
1620
|
+
return Promise.all(referencePaths.map(async (filePath) => ({
|
|
1621
|
+
path: filePath,
|
|
1622
|
+
markdown: await readFile(filePath, "utf-8"),
|
|
1623
|
+
})));
|
|
1621
1624
|
}
|
|
1622
1625
|
export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
1623
1626
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
@@ -1698,6 +1701,16 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1698
1701
|
catch (error) {
|
|
1699
1702
|
throw new Error(`failed to load verification commands for task "${taskId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1700
1703
|
}
|
|
1704
|
+
const frontendShapeTransitionCapsule = taskConfig.taskKind === "frontend-implementation" && CANONICAL_TASK_ID_PATTERN.test(taskId)
|
|
1705
|
+
? await discoverLatestCommittedFrontendShapeCapsule({
|
|
1706
|
+
repoRoot,
|
|
1707
|
+
taskId,
|
|
1708
|
+
sourceDigest: computeFrontendShapeSourceDigest({
|
|
1709
|
+
requirementMarkdown,
|
|
1710
|
+
constraintMarkdown: constraintMarkdown ?? "",
|
|
1711
|
+
}),
|
|
1712
|
+
})
|
|
1713
|
+
: undefined;
|
|
1701
1714
|
const sources = {
|
|
1702
1715
|
taskId,
|
|
1703
1716
|
repoRoot,
|
|
@@ -1714,6 +1727,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1714
1727
|
verifyCommands,
|
|
1715
1728
|
sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
|
|
1716
1729
|
projectGovernancePresent: await discoverProjectGovernancePresence(repoRoot),
|
|
1730
|
+
...(frontendShapeTransitionCapsule ? { autoloadedFrontendShapeTransitionCapsule: frontendShapeTransitionCapsule } : {}),
|
|
1717
1731
|
};
|
|
1718
1732
|
return sources;
|
|
1719
1733
|
}
|
|
@@ -1731,7 +1745,9 @@ async function prepareFrontendMockSources(sources, discoveredProjectCapability)
|
|
|
1731
1745
|
}
|
|
1732
1746
|
}
|
|
1733
1747
|
const projectCapability = discoveredProjectCapability ??
|
|
1734
|
-
(await discoverFrontendProjectCapability(repoRoot
|
|
1748
|
+
(await discoverFrontendProjectCapability(repoRoot, {
|
|
1749
|
+
specRoots: sources.taskConfig.frontendOpenspec?.specRoots,
|
|
1750
|
+
}));
|
|
1735
1751
|
const frontendRisk = classifyFrontendRisk({
|
|
1736
1752
|
title: sources.taskConfig.title,
|
|
1737
1753
|
requirementMarkdown: sources.requirementMarkdown,
|
|
@@ -1790,6 +1806,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1790
1806
|
forbiddenPaths,
|
|
1791
1807
|
outputContract: "Archived final shell verification stdout/stderr with exit codes; no worktree writes.",
|
|
1792
1808
|
subtask_prompt: "Run the adapter-resolved final verification commands before read-only verification review.",
|
|
1809
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1793
1810
|
shell: {
|
|
1794
1811
|
commands: verifyShellCommands,
|
|
1795
1812
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -1910,6 +1927,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1910
1927
|
allowedPaths: implementPaths.allowedPaths,
|
|
1911
1928
|
forbiddenPaths,
|
|
1912
1929
|
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
1930
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1913
1931
|
subtask_prompt: [
|
|
1914
1932
|
"Implement the approved plan with minimal focused changes.",
|
|
1915
1933
|
"Stay within writeSet. Do not write root artifacts/** unless artifacts paths are explicitly declared in writeSet.",
|
|
@@ -1969,6 +1987,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1969
1987
|
],
|
|
1970
1988
|
};
|
|
1971
1989
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
1990
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
1972
1991
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1973
1992
|
parseDagSpec(spec);
|
|
1974
1993
|
assertValidDagSpec(spec);
|
|
@@ -2237,11 +2256,18 @@ function frontendMockStrategyMustBeNotNeeded(sources) {
|
|
|
2237
2256
|
capabilityStatus === "ambiguous" ||
|
|
2238
2257
|
!hasDeterministicMockVerification));
|
|
2239
2258
|
}
|
|
2240
|
-
function resolveFrontendOpenspecGateConfig(sources) {
|
|
2259
|
+
async function resolveFrontendOpenspecGateConfig(sources) {
|
|
2241
2260
|
const taskConfig = sources.taskConfig;
|
|
2242
2261
|
const policy = taskConfig.frontendOpenspec?.policy ?? "cited";
|
|
2243
2262
|
const declared = taskConfig.frontendOpenspec?.requiredReadPaths ?? [];
|
|
2244
|
-
const
|
|
2263
|
+
const governanceRoot = sources.frontendProjectCapability?.openspecDiscovery?.governanceRoot ??
|
|
2264
|
+
DEFAULT_OPENSPEC_GOVERNANCE_ROOT;
|
|
2265
|
+
const specRoots = taskConfig.frontendOpenspec?.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
|
|
2266
|
+
const rootAliases = sources.repoRoot
|
|
2267
|
+
? await resolveFrontendSpecRootAliases(sources.repoRoot, [...specRoots])
|
|
2268
|
+
: [];
|
|
2269
|
+
const remappedSourceMarkdown = rootAliases.reduce((markdown, { alias, logicalRoot }) => markdown.replace(new RegExp(`(^|[^A-Za-z0-9_.-])${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`, "g"), `$1${logicalRoot}/`), [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"));
|
|
2270
|
+
const taskSourceCited = extractTaskSourceFrontendSpecPaths(remappedSourceMarkdown, specRoots, governanceRoot);
|
|
2245
2271
|
const scanStrict = sources.frontendProjectCapability?.designEvidence.normativePaths ?? [];
|
|
2246
2272
|
const dedupeSorted = (paths) => [...new Set(paths)].sort();
|
|
2247
2273
|
const openspecCandidateSources = {
|
|
@@ -2249,20 +2275,30 @@ function resolveFrontendOpenspecGateConfig(sources) {
|
|
|
2249
2275
|
taskSourceCited: dedupeSorted(taskSourceCited),
|
|
2250
2276
|
scanStrict: dedupeSorted(scanStrict),
|
|
2251
2277
|
};
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2278
|
+
// Candidate discovery is frozen independently from the eventual must-read
|
|
2279
|
+
// set. The selector may mark candidates irrelevant, while explicit task
|
|
2280
|
+
// declarations/source citations are never allowed to be downgraded.
|
|
2281
|
+
const openspecMandatoryPaths = dedupeSorted([...declared, ...taskSourceCited]);
|
|
2282
|
+
const openspecCandidatePaths = dedupeSorted([
|
|
2283
|
+
...openspecCandidateSources.scanStrict,
|
|
2284
|
+
...openspecMandatoryPaths,
|
|
2285
|
+
]);
|
|
2286
|
+
const result = {
|
|
2256
2287
|
openspecPolicy: policy,
|
|
2257
2288
|
openspecCandidatePaths,
|
|
2258
2289
|
openspecCandidateSources,
|
|
2290
|
+
openspecMandatoryPaths,
|
|
2259
2291
|
};
|
|
2292
|
+
if (sources.repoRoot && openspecCandidatePaths.length > 0) {
|
|
2293
|
+
result.openspecCandidateSnapshots = await Promise.all(openspecCandidatePaths.map(async (candidate) => ({
|
|
2294
|
+
path: candidate,
|
|
2295
|
+
sha256: createHash("sha256")
|
|
2296
|
+
.update(await readFile(path.join(sources.repoRoot, candidate)))
|
|
2297
|
+
.digest("hex"),
|
|
2298
|
+
})));
|
|
2299
|
+
}
|
|
2300
|
+
return result;
|
|
2260
2301
|
}
|
|
2261
|
-
const openspecCitationInstruction = [
|
|
2262
|
-
"OpenSpec 引用块(citation block):在 fenced json 契约块之后,追加**恰好一个** ```openspec-citations 围栏代码块(三反引号 + openspec-citations)。",
|
|
2263
|
-
'该块内每行一个 JSON 对象 {"path":"<repo 相对 openspec 路径>","section":"<命中章节或空串>","line":<int 或 null>},必须逐条列出你在本计划中实际读取并应用的每个 openspec 规范文件。',
|
|
2264
|
-
"prewrite gate 会用真实 read 事件核验每条引用:引用存在但无成功 read 事件 → openspec-citation-not-read;契约冻结的必读候选未被引用 → openspec-not-cited;两者都 fail-closed。不要引用未读取的路径。",
|
|
2265
|
-
].join("\n");
|
|
2266
2302
|
const frontendComponentConformanceInstruction = [
|
|
2267
2303
|
"## Component Selection conformance (uiComponentChoices; hard rule)",
|
|
2268
2304
|
"每个 UI 用途必须在契约的 uiComponentChoices[] 中声明组件选型:{ purpose, component, decision, specReference, rationale }。",
|
|
@@ -2270,7 +2306,7 @@ const frontendComponentConformanceInstruction = [
|
|
|
2270
2306
|
"- decision=reuse-existing:复用仓库既有组件/惯例(规范未点名)→ specReference 可为 null,rationale 说明复用的现有组件与依据。",
|
|
2271
2307
|
"- decision=new:规范与既有代码均无合适组件 → specReference 必须为 null,rationale 必须说明偏差理由(design-review 审,最终 review 复核)。",
|
|
2272
2308
|
"不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
|
|
2273
|
-
"prewrite gate 确定性交叉校验:specReference.path 非法 → component-spec-reference-invalid
|
|
2309
|
+
"prewrite gate 确定性交叉校验:specReference.path 非法 → component-spec-reference-invalid;specReference.path 未在 typed decision ledger 中声明或缺少成功 read 事件 → component-spec-not-cited;候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
|
|
2274
2310
|
].join("\n");
|
|
2275
2311
|
function resolveFrontendCapabilityContextBlock(sources) {
|
|
2276
2312
|
const risk = sources.frontendRisk;
|
|
@@ -2320,17 +2356,102 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
2320
2356
|
}
|
|
2321
2357
|
return parts.join("\n");
|
|
2322
2358
|
}
|
|
2359
|
+
function pruneFrontendTasksForMicro(tasks) {
|
|
2360
|
+
// Micro topology (7 nodes): contract-import-shell → writer-admission →
|
|
2361
|
+
// implement → verify → review-context → review → closeout. Drops scout/plan/
|
|
2362
|
+
// design-policy/design-review and replaces the model contract node with a
|
|
2363
|
+
// deterministic contract-import shell (§7.3: no model, no requirement
|
|
2364
|
+
// rewrites — micro consumes a pre-validated managed Contract).
|
|
2365
|
+
const drop = new Set([
|
|
2366
|
+
"frontend-contract-pi",
|
|
2367
|
+
"frontend-scout-pi",
|
|
2368
|
+
"frontend-plan-pi",
|
|
2369
|
+
"frontend-design-policy-shell",
|
|
2370
|
+
"frontend-design-review-pi",
|
|
2371
|
+
]);
|
|
2372
|
+
const contractPi = tasks.find((task) => task.id === "frontend-contract-pi");
|
|
2373
|
+
const contractImportShell = contractPi
|
|
2374
|
+
? {
|
|
2375
|
+
id: "frontend-contract-import-shell",
|
|
2376
|
+
role: "verifier",
|
|
2377
|
+
executor: "shell",
|
|
2378
|
+
complexity: "LOW",
|
|
2379
|
+
writePolicy: "read-only",
|
|
2380
|
+
depends_on: [],
|
|
2381
|
+
allowedPaths: contractPi.allowedPaths,
|
|
2382
|
+
forbiddenPaths: contractPi.forbiddenPaths,
|
|
2383
|
+
subtask_prompt: "Deterministic managed-Contract import + source/binding/schema freshness re-validation; no model, no requirement rewrite.",
|
|
2384
|
+
shell: {
|
|
2385
|
+
commands: [],
|
|
2386
|
+
frontendContractImport: {
|
|
2387
|
+
schemaVersion: 1,
|
|
2388
|
+
artifactName: "frontend-task-contract.json",
|
|
2389
|
+
outputDir: "contracts",
|
|
2390
|
+
requireSourceFreshness: true,
|
|
2391
|
+
},
|
|
2392
|
+
cwd: ".",
|
|
2393
|
+
timeoutMs: 60000,
|
|
2394
|
+
},
|
|
2395
|
+
}
|
|
2396
|
+
: undefined;
|
|
2397
|
+
const filtered = [
|
|
2398
|
+
...(contractImportShell ? [contractImportShell] : []),
|
|
2399
|
+
...tasks.filter((task) => !drop.has(task.id)),
|
|
2400
|
+
];
|
|
2401
|
+
const byId = new Map(filtered.map((task) => [task.id, task]));
|
|
2402
|
+
const remap = (deps) => {
|
|
2403
|
+
if (!deps)
|
|
2404
|
+
return [];
|
|
2405
|
+
const next = [];
|
|
2406
|
+
for (const dep of deps) {
|
|
2407
|
+
if (drop.has(dep))
|
|
2408
|
+
continue;
|
|
2409
|
+
if (byId.has(dep))
|
|
2410
|
+
next.push(dep);
|
|
2411
|
+
}
|
|
2412
|
+
return [...new Set(next)];
|
|
2413
|
+
};
|
|
2414
|
+
return filtered.map((task) => {
|
|
2415
|
+
const depends_on = remap(task.depends_on);
|
|
2416
|
+
if (task.id === "frontend-writer-admission-shell") {
|
|
2417
|
+
const admission = task.shell?.frontendWriterAdmission;
|
|
2418
|
+
return {
|
|
2419
|
+
...task,
|
|
2420
|
+
depends_on: ["frontend-contract-import-shell"],
|
|
2421
|
+
shell: admission
|
|
2422
|
+
? {
|
|
2423
|
+
...task.shell,
|
|
2424
|
+
commands: task.shell?.commands ?? [],
|
|
2425
|
+
frontendWriterAdmission: {
|
|
2426
|
+
...admission,
|
|
2427
|
+
designReviewFromNodeId: undefined,
|
|
2428
|
+
},
|
|
2429
|
+
}
|
|
2430
|
+
: task.shell,
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
return { ...task, depends_on };
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
function pruneFrontendTasksForSplitRequired(tasks) {
|
|
2437
|
+
const writerChain = new Set([
|
|
2438
|
+
"frontend-writer-admission-shell",
|
|
2439
|
+
"frontend-implement-pi",
|
|
2440
|
+
"frontend-verify-shell",
|
|
2441
|
+
"frontend-review-context-shell",
|
|
2442
|
+
"frontend-review-pi",
|
|
2443
|
+
"frontend-closeout-shell",
|
|
2444
|
+
]);
|
|
2445
|
+
return tasks.filter((task) => !writerChain.has(task.id));
|
|
2446
|
+
}
|
|
2323
2447
|
function pruneFrontendTasksForRisk(tasks, risk) {
|
|
2324
2448
|
if (risk.forceFullGates || risk.selectedRisk !== "small") {
|
|
2325
2449
|
return tasks;
|
|
2326
2450
|
}
|
|
2327
|
-
// Small topology keeps one design review and removes only the
|
|
2328
|
-
//
|
|
2329
|
-
//
|
|
2330
|
-
const drop = new Set([
|
|
2331
|
-
"frontend-plan-revision-pi",
|
|
2332
|
-
"frontend-final-design-review-pi",
|
|
2333
|
-
]);
|
|
2451
|
+
// Small topology keeps one design review and removes only the redundant
|
|
2452
|
+
// design-review node; the deterministic design-policy and writer-admission
|
|
2453
|
+
// shells consume the surviving plan directly.
|
|
2454
|
+
const drop = new Set(["frontend-design-review-pi"]);
|
|
2334
2455
|
const filtered = tasks.filter((task) => !drop.has(task.id));
|
|
2335
2456
|
const byId = new Map(filtered.map((task) => [task.id, task]));
|
|
2336
2457
|
const remap = (deps) => {
|
|
@@ -2338,11 +2459,8 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2338
2459
|
return [];
|
|
2339
2460
|
const next = [];
|
|
2340
2461
|
for (const dep of deps) {
|
|
2341
|
-
if (dep === "frontend-
|
|
2342
|
-
if (byId.has("frontend-plan-pi"))
|
|
2343
|
-
next.push("frontend-plan-pi");
|
|
2462
|
+
if (dep === "frontend-design-review-pi")
|
|
2344
2463
|
continue;
|
|
2345
|
-
}
|
|
2346
2464
|
if (byId.has(dep) || dep === "frontend-implement-pi")
|
|
2347
2465
|
next.push(dep);
|
|
2348
2466
|
}
|
|
@@ -2350,30 +2468,27 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2350
2468
|
};
|
|
2351
2469
|
return filtered.map((task) => {
|
|
2352
2470
|
const depends_on = remap(task.depends_on);
|
|
2353
|
-
if (task.id === "frontend-
|
|
2354
|
-
|
|
2471
|
+
if (task.id === "frontend-writer-admission-shell") {
|
|
2472
|
+
// Small topology: no design review node, so the admission shell drops
|
|
2473
|
+
// the design-review dependency and the verdict requirement.
|
|
2474
|
+
const admission = task.shell?.frontendWriterAdmission;
|
|
2355
2475
|
return {
|
|
2356
2476
|
...task,
|
|
2357
|
-
depends_on: ["frontend-
|
|
2358
|
-
|
|
2359
|
-
shell: gate
|
|
2477
|
+
depends_on: ["frontend-design-policy-shell"],
|
|
2478
|
+
shell: admission
|
|
2360
2479
|
? {
|
|
2361
2480
|
...task.shell,
|
|
2362
2481
|
commands: task.shell?.commands ?? [],
|
|
2363
|
-
|
|
2364
|
-
...
|
|
2365
|
-
|
|
2366
|
-
planFallbackFromNodeIds: [],
|
|
2367
|
-
reviewFromNodeId: "frontend-design-review-pi",
|
|
2368
|
-
reviewFallbackFromNodeIds: [],
|
|
2369
|
-
revisionPatch: false,
|
|
2482
|
+
frontendWriterAdmission: {
|
|
2483
|
+
...admission,
|
|
2484
|
+
designReviewFromNodeId: undefined,
|
|
2370
2485
|
},
|
|
2371
2486
|
}
|
|
2372
2487
|
: task.shell,
|
|
2373
2488
|
};
|
|
2374
2489
|
}
|
|
2375
2490
|
if (task.id === "frontend-implement-pi") {
|
|
2376
|
-
for (const need of ["frontend-
|
|
2491
|
+
for (const need of ["frontend-writer-admission-shell"]) {
|
|
2377
2492
|
if (byId.has(need) && !depends_on.includes(need))
|
|
2378
2493
|
depends_on.push(need);
|
|
2379
2494
|
}
|
|
@@ -2398,9 +2513,23 @@ function buildFrontendWriterNodeDefaults(input) {
|
|
|
2398
2513
|
allowedPaths: input.allowedPaths,
|
|
2399
2514
|
forbiddenPaths: input.forbiddenPaths,
|
|
2400
2515
|
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2401
|
-
writerOutcomePolicy: {
|
|
2516
|
+
writerOutcomePolicy: {
|
|
2517
|
+
type: input.writerOutcomePolicyType ?? "implementation-outcome-v1",
|
|
2518
|
+
},
|
|
2402
2519
|
};
|
|
2403
2520
|
}
|
|
2521
|
+
function resolveFrontendShapeCapsuleGenerationInput(sources) {
|
|
2522
|
+
const explicit = sources.frontendShapeTransitionCapsule == null
|
|
2523
|
+
? undefined
|
|
2524
|
+
: parseFrontendShapeTransitionCapsule(sources.frontendShapeTransitionCapsule);
|
|
2525
|
+
const autoloaded = sources.autoloadedFrontendShapeTransitionCapsule == null
|
|
2526
|
+
? undefined
|
|
2527
|
+
: parseFrontendShapeTransitionCapsule(sources.autoloadedFrontendShapeTransitionCapsule);
|
|
2528
|
+
if (explicit && autoloaded && explicit.capsuleDigest !== autoloaded.capsuleDigest) {
|
|
2529
|
+
throw new Error("explicit frontend shape transition capsule conflicts with runtime autoload");
|
|
2530
|
+
}
|
|
2531
|
+
return explicit ?? autoloaded;
|
|
2532
|
+
}
|
|
2404
2533
|
async function buildFrontendHybridDagFromTask(sources) {
|
|
2405
2534
|
const { taskConfig } = sources;
|
|
2406
2535
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
@@ -2425,6 +2554,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2425
2554
|
const implementId = frontendImplementationNodeId();
|
|
2426
2555
|
const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
|
|
2427
2556
|
const capabilityContextBlock = resolveFrontendCapabilityContextBlock(frontendSources);
|
|
2557
|
+
// Freeze the design-policy dependency allowlist from the project manifest:
|
|
2558
|
+
// already-declared deps are authorized; anything else in the model's
|
|
2559
|
+
// dependency policy that looks like a package name is an unauthorized new
|
|
2560
|
+
// dependency (fail-closed at the policy shell and the plan pre-check).
|
|
2561
|
+
const declaredDependencies = await collectDeclaredDependencies(sources.repoRoot ?? process.cwd());
|
|
2428
2562
|
const frontendRisk = frontendSources.frontendRisk ??
|
|
2429
2563
|
classifyFrontendRisk({
|
|
2430
2564
|
title: taskConfig.title,
|
|
@@ -2433,6 +2567,19 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2433
2567
|
allowedPaths: taskConfig.allowedPaths,
|
|
2434
2568
|
complexity: taskConfig.complexity,
|
|
2435
2569
|
});
|
|
2570
|
+
const frontendTaskShape = resolveFrontendTaskShape({
|
|
2571
|
+
complexity: taskConfig.complexity,
|
|
2572
|
+
allowedPaths: taskConfig.allowedPaths,
|
|
2573
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
2574
|
+
constraintMarkdown: sources.constraintMarkdown ?? undefined,
|
|
2575
|
+
splitSignal: { splitRequired: false, runtimeSupportsSplit: true },
|
|
2576
|
+
taskId: sources.taskId,
|
|
2577
|
+
sourceDigest: computeFrontendShapeSourceDigest({
|
|
2578
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
2579
|
+
constraintMarkdown: sources.constraintMarkdown ?? "",
|
|
2580
|
+
}),
|
|
2581
|
+
shapeTransitionCapsule: resolveFrontendShapeCapsuleGenerationInput(sources),
|
|
2582
|
+
});
|
|
2436
2583
|
const frontendSourceBinding = buildDagSourceBinding(sources);
|
|
2437
2584
|
const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
|
|
2438
2585
|
sourceBinding: frontendSourceBinding,
|
|
@@ -2448,7 +2595,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2448
2595
|
"## Runtime contract skeleton (deterministic and protected)",
|
|
2449
2596
|
JSON.stringify(frontendContractSkeleton),
|
|
2450
2597
|
"",
|
|
2451
|
-
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then
|
|
2598
|
+
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then writes a hash-bound canonical JSON artifact; downstream review and prewrite consume that artifact path, not planner stdout.",
|
|
2452
2599
|
"",
|
|
2453
2600
|
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
2454
2601
|
"- schemaId",
|
|
@@ -2461,6 +2608,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2461
2608
|
"- Use uiStates: [] for frontend logic changes with no user-visible UI state. Do not invent UI states.",
|
|
2462
2609
|
"- For applicable=true, provide non-empty expectedBehavior plus non-empty implementationTargets and verificationTargetIds. For applicable=false, provide non-empty notApplicableReason and omit expectedBehavior instead of emitting an empty string.",
|
|
2463
2610
|
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
2611
|
+
"- dependencyPolicy must declare whether new dependencies are introduced: use a clear negative(零新增依赖/不引入新依赖)or an explicit allowlist phrase(仅使用已声明的 X、Y)。Plain prose that merely mentions tool names without declaring new deps is still enforced by the policy shell against the project package.json allowlist; an undeclared new dependency is rejected.",
|
|
2464
2612
|
"- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
|
|
2465
2613
|
"- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
|
|
2466
2614
|
"",
|
|
@@ -2521,7 +2669,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2521
2669
|
})();
|
|
2522
2670
|
const frontendContractFieldSummary = [
|
|
2523
2671
|
"## Contract field summary (authoritative JSON; no plan prose)",
|
|
2524
|
-
"The plan
|
|
2672
|
+
"The plan node emits only a fenced json contract patch; frontend-design-policy-shell applies the runtime skeleton, validates, and materializes the canonical full contract JSON. There is no Markdown plan explanation to read. Review these fields:",
|
|
2525
2673
|
"- requirements[]: id, expectedOutcome, implementationTargets, verificationTargetIds, evidenceGap",
|
|
2526
2674
|
"- uiStates[]: name, applicable, expectedBehavior, implementationTargets, verificationTargetIds, notApplicableReason",
|
|
2527
2675
|
"- interactions[]: name, trigger, expectedBehavior, implementationTargets, verificationTargetIds",
|
|
@@ -2533,34 +2681,30 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2533
2681
|
"- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
|
|
2534
2682
|
"Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
|
|
2535
2683
|
].join("\n");
|
|
2536
|
-
const frontendPlanFieldGuide = [
|
|
2537
|
-
"## Compact planner field guide",
|
|
2538
|
-
"Emit only editable fields: requirements[], implementationSteps[], targets.routes/publicApiChanges, uiStates[], interactions[], mockApi.strategy/activation/endpoints, verificationTargets[], designEvidence, evidenceGaps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap.",
|
|
2539
|
-
"Protected and forbidden: schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, schemaId, targetFiles, requirementCoverage.",
|
|
2540
|
-
"Invariants: every requirement has expectedOutcome; every interaction has trigger + expectedBehavior; applicable uiStates have expectedBehavior + implementationTargets + verificationTargetIds; non-applicable uiStates have notApplicableReason; commandLabel is frozen; mockApi production default remains off.",
|
|
2541
|
-
"Each behavior belongs to one primary row: requirements cover acceptance, uiStates cover only distinct visible states, and interactions cover only distinct user triggers. Link with IDs; do not restate the same prose in all three. Steps are action plus files, not a prose mirror. Omit optional fields when upstream contract/scout facts already settle them, and omit unchanged revision fields.",
|
|
2542
|
-
"Enums: mock strategy native|browser-intercept|request-adapter|not-needed; uiComponentChoices decision specified|reuse-existing|new. Omit optional empty strings.",
|
|
2543
|
-
].join("\n");
|
|
2544
2684
|
const sourceContext = [
|
|
2545
2685
|
buildSourceContextBlock(sources),
|
|
2546
2686
|
capabilityContextBlock,
|
|
2547
2687
|
]
|
|
2548
2688
|
.filter(Boolean)
|
|
2549
2689
|
.join("\n\n");
|
|
2550
|
-
const artifactFirstSourceContext = [
|
|
2551
|
-
buildArtifactFirstSourceAccessBlock(sources),
|
|
2552
|
-
capabilityContextBlock,
|
|
2553
|
-
]
|
|
2554
|
-
.filter(Boolean)
|
|
2555
|
-
.join("\n\n");
|
|
2556
2690
|
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
2557
2691
|
mockCapability.verifyCommands.length > 0;
|
|
2558
2692
|
const requirementIds = frontendSourceBinding.requirementIds;
|
|
2559
2693
|
const requirementCoverageInstruction = requirementIds.length > 0
|
|
2560
2694
|
? [
|
|
2561
|
-
|
|
2562
|
-
`
|
|
2563
|
-
|
|
2695
|
+
`## Requirement Coverage (per-AC echo with bad/good examples)`,
|
|
2696
|
+
`For each requirement ID below, echo the ID verbatim and confirm: expectedOutcome (user-observable or logic-observable), implementation targets (files), and verification targets (commandLabel + file).`,
|
|
2697
|
+
`Do not skip any ID. Use the bad/good patterns below as reference for each field.`,
|
|
2698
|
+
``,
|
|
2699
|
+
`Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
|
|
2700
|
+
`- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
|
|
2701
|
+
``,
|
|
2702
|
+
`Good example (concrete expectedOutcome, real files, real verification targets):`,
|
|
2703
|
+
`- AC-001: expectedOutcome="TypeScript compilation exits with code 0 and produces no errors in dist/" implementationTargets=["src/app.tsx"] verificationTargets=["vt-typecheck":"npm run typecheck","tsconfig.json"]`,
|
|
2704
|
+
``,
|
|
2705
|
+
...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
|
|
2706
|
+
``,
|
|
2707
|
+
`Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
|
|
2564
2708
|
].join("\n")
|
|
2565
2709
|
: "";
|
|
2566
2710
|
const verificationTargetFileInstruction = [
|
|
@@ -2568,16 +2712,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2568
2712
|
`verificationTargets[].file is the code file that the target verifies (the file the writer changes), NOT where the command is defined.`,
|
|
2569
2713
|
`Non-static targets (type unit/component/integration/mock) MUST set file to a concrete code file inside the implementation writeSet (task allowedPaths); the prewrite gate rejects any non-static target whose file falls outside the writeSet.`,
|
|
2570
2714
|
`Command-level checks that run project-wide (all tests, typecheck, build, governance) MUST use type "static" and must NOT be bound as non-static targets with file=package.json/tsconfig.json/vite.config.ts/scripts/*. Static targets are exempt from the writeSet containment check.`,
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
].join("\n");
|
|
2577
|
-
const plannerSourceReadInstruction = [
|
|
2578
|
-
"## Source-read policy",
|
|
2579
|
-
"Use frontend-contract-pi and frontend-scout-pi artifacts as primary evidence. Do not repeat reads that those facts already settle.",
|
|
2580
|
-
"Read a bound source only to resolve a missing or conflicting field; read every applicable OpenSpec candidate directly before citing it. Never infer omitted task fields from a preview.",
|
|
2715
|
+
``,
|
|
2716
|
+
`verificationTargets[].symbol semantics (deterministic runtime strip):`,
|
|
2717
|
+
`- Static targets MUST NOT declare a symbol at all: the runtime strips any symbol from static targets (project-wide commands verify by exit code, never by a per-file symbol).`,
|
|
2718
|
+
`- Non-static targets MUST declare the symbol they verify: an exported function/component/variable name, or a describe/it title exactly as written in that file. The trace gate resolves it against the file after verification.`,
|
|
2719
|
+
`- Never emit placeholder tokens ("null", "undefined", "n/a", "tbd", "...", "?", "-") or long prose fragments as symbols; a fabricated symbol fails plan validation and costs a retry round.`,
|
|
2581
2720
|
].join("\n");
|
|
2582
2721
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2583
2722
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
@@ -2588,15 +2727,15 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2588
2727
|
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
2589
2728
|
: []),
|
|
2590
2729
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2591
|
-
"Frontend
|
|
2592
|
-
"
|
|
2593
|
-
"
|
|
2594
|
-
"
|
|
2730
|
+
"Frontend design policy (frontend-design-policy-shell) and writer admission (frontend-writer-admission-shell) are the only write authorization; the writer runs only after both materialized the canonical contract and admitted a concrete writeSet.",
|
|
2731
|
+
"Design review verdict is consumed only as admission data input; it never drives branch selection.",
|
|
2732
|
+
"Verification failure is terminal for the run: frontend-verify-shell fails closed, routes to recovery, and never selects a same-run repair branch.",
|
|
2733
|
+
"Frontend review is decided exclusively by the committed typed terminal tools (approve_review / request_review_changes); response-text JSON verdicts and first-line VERDICT markers carry no control-flow weight.",
|
|
2595
2734
|
"Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
|
|
2596
2735
|
"Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
|
|
2597
2736
|
"Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
|
|
2598
2737
|
"frontend-implementation DAGs must complete deterministic static verification before final review. Behavior verification is also required when the task declares a behavior entrypoint or the implementation contract contains a non-static verification target; static-only contracts must map every target to the declared static entrypoint.",
|
|
2599
|
-
"
|
|
2738
|
+
"Frontend closeout renders only from committed facts; a weak status (failed / not-run / baseline-debt / mock-backed / pending) can never be rewritten into a stronger one (passed / real-integrated).",
|
|
2600
2739
|
`Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
|
|
2601
2740
|
frontendRisk.forceFullGates
|
|
2602
2741
|
? "High-risk or supervised: keep full design gates; do not weaken write boundaries."
|
|
@@ -2612,7 +2751,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2612
2751
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
|
|
2613
2752
|
}
|
|
2614
2753
|
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
2615
|
-
|
|
2754
|
+
// The verification bundle schema requires at least one static command. When
|
|
2755
|
+
// a real project exposes no usable verification command at all, run an
|
|
2756
|
+
// explicit no-op marker instead of a command that is guaranteed to fail:
|
|
2757
|
+
// the trace then records not-run honestly and the advisory asks the task
|
|
2758
|
+
// to declare verification commands and regenerate.
|
|
2759
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands.length > 0
|
|
2760
|
+
? fallbackVerifyCommands.staticCommands
|
|
2761
|
+
: [FRONTEND_NO_STATIC_VERIFICATION_MARKER];
|
|
2616
2762
|
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
2617
2763
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
2618
2764
|
repoRoot: sources.repoRoot,
|
|
@@ -2711,11 +2857,24 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2711
2857
|
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
2712
2858
|
].join("\n");
|
|
2713
2859
|
const advisories = [];
|
|
2860
|
+
if (!hasDeclaredFrontendVerification &&
|
|
2861
|
+
fallbackVerifyCommands.staticCommands.length === 0 &&
|
|
2862
|
+
fallbackVerifyCommands.behaviorCommands.length === 0) {
|
|
2863
|
+
advisories.push("未发现可用的前端验证命令:目标项目没有可读取的 package.json scripts,也未探测到本地 TypeScript,verify 节点将没有静态/行为命令可执行。请在 task.json --verify 或执行约束.md 的验证约束中显式声明命令(例如 node --check src/app.js),然后重新生成 DAG。");
|
|
2864
|
+
}
|
|
2714
2865
|
if (frontendSourceMentionsMock(frontendSources) &&
|
|
2715
2866
|
frontendMockStrategyMustBeNotNeeded(frontendSources)) {
|
|
2716
2867
|
advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
|
|
2717
2868
|
}
|
|
2718
|
-
const openspecGate = resolveFrontendOpenspecGateConfig(sources);
|
|
2869
|
+
const openspecGate = await resolveFrontendOpenspecGateConfig(sources);
|
|
2870
|
+
const requiresOpenspecClassification = openspecGate.openspecPolicy === "cited" &&
|
|
2871
|
+
openspecGate.openspecCandidatePaths.length > 0;
|
|
2872
|
+
const openspecSelectionContext = JSON.stringify({
|
|
2873
|
+
schemaVersion: 1,
|
|
2874
|
+
schemaId: "frontend-openspec-selection-v1",
|
|
2875
|
+
candidates: openspecGate.openspecCandidatePaths,
|
|
2876
|
+
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
2877
|
+
});
|
|
2719
2878
|
// Generation-frozen component/theme specification bucket (ADR 0016). Derived
|
|
2720
2879
|
// from the classified component/theme/rule.components buckets; the prewrite
|
|
2721
2880
|
// gate consumes it to enforce uiComponentChoices presence and specReference
|
|
@@ -2727,23 +2886,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2727
2886
|
...(classified?.theme ?? []),
|
|
2728
2887
|
...(classified?.rule.components ?? []),
|
|
2729
2888
|
].filter((value, index, array) => array.indexOf(value) === index).sort();
|
|
2730
|
-
const frontendPlanCitationsRequired = openspecGate.openspecCandidatePaths.length > 0 ||
|
|
2731
|
-
componentSpecCandidatePaths.length > 0;
|
|
2732
|
-
const frontendPlanCitationInstruction = frontendPlanCitationsRequired
|
|
2733
|
-
? openspecCitationInstruction
|
|
2734
|
-
: "The generation-frozen OpenSpec/component candidate set is empty. Do not read, cite, or emit an openspec-citations fence; output only the primary JSON patch. The runtime records an empty citation artifact deterministically.";
|
|
2735
|
-
const frontendPlanOutputContract = frontendPlanCitationsRequired
|
|
2736
|
-
? "JSON-only patch output: exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton, immediately followed by exactly one ```openspec-citations``` fenced citation block. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes."
|
|
2737
|
-
: "JSON-only patch output: exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. The generation-frozen OpenSpec candidate set is empty, so do NOT emit an openspec-citations fence. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.";
|
|
2738
|
-
const frontendRevisionOutputContract = frontendPlanCitationsRequired
|
|
2739
|
-
? "When the initial design review requests revision, return exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively), immediately followed by exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only; the gate applies the patch on the original contract and renders plan.md deterministically. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes."
|
|
2740
|
-
: "JSON-only output: when the initial design review requests revision, return exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). The generation-frozen OpenSpec candidate set is empty, so do NOT emit an openspec-citations fence. Do NOT emit a full contract, Markdown explanation, prose, or another fenced block. No file writes.";
|
|
2741
2889
|
if (openspecGate.openspecPolicy === "cited") {
|
|
2742
2890
|
if (openspecGate.openspecCandidatePaths.length === 0) {
|
|
2743
2891
|
advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
|
|
2744
2892
|
}
|
|
2745
2893
|
else {
|
|
2746
|
-
advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan
|
|
2894
|
+
advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan 必须在 typed decision ledger 的 uiComponentChoices.specReference 中声明并通过真实 read 事件佐证。`);
|
|
2747
2895
|
}
|
|
2748
2896
|
}
|
|
2749
2897
|
else {
|
|
@@ -2781,12 +2929,20 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2781
2929
|
allowedPaths: readOnlyPaths,
|
|
2782
2930
|
forbiddenPaths,
|
|
2783
2931
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2784
|
-
outputContract: "Markdown contract with
|
|
2932
|
+
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal, then call finalize_contract exactly once. Requirements use stable REQ/BR/AC identifiers with source spans and a disposition (explicit | repository-resolvable | assumption | blocking); each requirement registers evidence expectations across static/behavior/Mock/real-integration (required | optional | not-applicable), and UI-visible or interactive requirements register a non-blocking frontend-test handoff intent. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, also append the openspec classification and one final fenced frontend-openspec-selection-v1 JSON object. No file writes.",
|
|
2785
2933
|
subtask_prompt: [
|
|
2786
|
-
"Read task source and produce a concise frontend implementation contract.",
|
|
2787
|
-
"
|
|
2934
|
+
"Read task source and produce a concise frontend implementation contract as typed requirement facts plus narrative Markdown.",
|
|
2935
|
+
"Assign each requirement a stable REQ/BR/AC identifier and a source span (task-source section or repository file:line). Label each requirement's disposition as explicit | repository-resolvable | assumption | blocking; a blocking requirement must name its owner (human-decision or external-state) and evidence refs.",
|
|
2936
|
+
"Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
|
|
2937
|
+
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations. Do not fix target files, components, styling, or implementation methods as requirements; leave those to Scout and Plan.",
|
|
2938
|
+
"If the task is too large for one bounded writer, record a task split proposal instead of silently widening scope.",
|
|
2939
|
+
"End the contract with a single disposition: ready, ready-with-assumptions (bounded assumptions that do not change product behavior), or blocked.",
|
|
2940
|
+
...(requiresOpenspecClassification ? [
|
|
2941
|
+
"Classify every generation-frozen OpenSpec candidate while contracting: required must be read and cited by the plan; relevant may inform planning but is not forced; irrelevant is out of scope. mandatoryPaths MUST be required and cannot be downgraded.",
|
|
2942
|
+
"After the Markdown contract, append exactly one fenced json object with schemaVersion 1, schemaId frontend-openspec-selection-v1, and selections [{path,disposition,rationale}]. No other JSON fences.",
|
|
2943
|
+
openspecSelectionContext,
|
|
2944
|
+
] : []),
|
|
2788
2945
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2789
|
-
mandatorySourceReadInstruction,
|
|
2790
2946
|
sourceContext,
|
|
2791
2947
|
].join("\n\n"),
|
|
2792
2948
|
},
|
|
@@ -2800,11 +2956,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2800
2956
|
allowedPaths: readOnlyPaths,
|
|
2801
2957
|
forbiddenPaths,
|
|
2802
2958
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2803
|
-
outputContract: "Markdown scout report
|
|
2959
|
+
outputContract: "Markdown scout report covering target surface and design evidence (frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, risks). Submit through the incremental evidence tools record_target_surface / record_design_evidence. No fixed TARGET_SURFACE section title is required — target surface and design evidence are reported as committed typed facts. No file writes.",
|
|
2804
2960
|
subtask_prompt: [
|
|
2805
2961
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
2806
2962
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2807
|
-
"
|
|
2963
|
+
"Report target surface and design evidence as facts (no fixed section title required): entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts. Use repository-relative POSIX paths. implementationPaths and testPaths must name the existing files/directories that actually own the requested behavior; allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
|
|
2808
2964
|
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
2809
2965
|
"Read-only: do not modify repository files.",
|
|
2810
2966
|
sourceContext,
|
|
@@ -2817,254 +2973,196 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2817
2973
|
executor: "pi",
|
|
2818
2974
|
complexity: "MED",
|
|
2819
2975
|
writePolicy: "read-only",
|
|
2820
|
-
|
|
2821
|
-
|
|
2976
|
+
retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
|
|
2977
|
+
allowedPaths: readOnlyPaths,
|
|
2978
|
+
forbiddenPaths,
|
|
2979
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2822
2980
|
structuredContractOutput: {
|
|
2823
|
-
schemaId:
|
|
2981
|
+
schemaId: "frontend-implementation-contract-plan-patch-v1",
|
|
2824
2982
|
retryOnInvalid: true,
|
|
2825
2983
|
skeleton: frontendContractSkeleton,
|
|
2826
|
-
citationsRequired: frontendPlanCitationsRequired,
|
|
2827
2984
|
},
|
|
2828
|
-
|
|
2829
|
-
forbiddenPaths,
|
|
2830
|
-
skills: [],
|
|
2831
|
-
outputContract: frontendPlanOutputContract,
|
|
2985
|
+
outputContract: "Short Markdown narrative plus incremental typed plan record tools (record_target_surface / record_component_choice / record_state_flow / record_data_flow / record_mock_api / record_design_deviation / record_dependency) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from committed facts (requirements / verificationTargets / evidenceGaps / implementationSteps / residualRisks / realIntegrationGap). Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime compiles facts ⊕ skeleton into canonical full-contract JSON for downstream review. No file writes.",
|
|
2832
2986
|
subtask_prompt: [
|
|
2833
|
-
"Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton.
|
|
2834
|
-
"
|
|
2835
|
-
"
|
|
2836
|
-
"
|
|
2987
|
+
"Use frontend-contract-pi typed requirement facts (stable REQ/BR/AC identifiers, dispositions, evidence expectations, and frontend-test handoff intents), frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Record the plan decision ledger through the incremental record_* tools, then call finalize_plan exactly once. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2988
|
+
...(requiresOpenspecClassification ? ["Additionally consume the frontend-contract-pi openspec classification. Successfully read every required selection (including mandatory paths) and declare any used component specReference in the typed decision ledger; relevant/irrelevant selections are not forced into the ledger unless the plan actually uses them."] : []),
|
|
2989
|
+
"The incremental record_* facts become the complete implementation plan after deterministic merge with the protected skeleton. Do not treat leftover JSON in the narrative as the compile authority.",
|
|
2990
|
+
"Select the Mock / API strategy only through record_mock_api. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
|
|
2991
|
+
"Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the ledger payload. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2837
2992
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2838
|
-
"Consume the Scout
|
|
2839
|
-
|
|
2840
|
-
? "Output exactly two adjacent blocks: (1) one fenced json object containing the editable plan patch; (2) one openspec-citations citation block."
|
|
2841
|
-
: "Output exactly one fenced json object containing the editable plan patch; do not emit an openspec-citations block.",
|
|
2842
|
-
"Do NOT emit protected skeleton fields, a full contract, Markdown plan explanation, raw JSON, or any other fenced block.",
|
|
2993
|
+
"Consume the Scout target surface and design evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2994
|
+
"Output a short Markdown narrative, record the incremental facts, then call finalize_plan exactly once. Do NOT emit protected skeleton fields or a full contract as the authority.",
|
|
2843
2995
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2996
|
+
"Attribution rule (hard pre-check): every interaction.name and every applicable uiState.name must have a uiComponentChoices entry whose purpose exactly matches that name; a non-empty stylingStrategy additionally covers applicable visual UI states. Missing attribution fails the plan policy pre-check with a retryable diagnostic.",
|
|
2844
2997
|
requirementCoverageInstruction,
|
|
2845
2998
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2846
2999
|
verificationTargetFileInstruction,
|
|
2847
3000
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2848
|
-
plannerSourceReadInstruction,
|
|
2849
3001
|
fixedVerificationContext,
|
|
2850
|
-
|
|
3002
|
+
sourceContext,
|
|
2851
3003
|
mockContextBlock,
|
|
2852
|
-
|
|
3004
|
+
frontendContractSchemaBlock,
|
|
2853
3005
|
frontendComponentConformanceInstruction,
|
|
2854
|
-
frontendPlanCitationInstruction,
|
|
2855
3006
|
].join("\n\n"),
|
|
2856
3007
|
},
|
|
2857
3008
|
{
|
|
2858
|
-
id: "frontend-design-
|
|
3009
|
+
id: "frontend-design-policy-shell",
|
|
2859
3010
|
depends_on: ["frontend-plan-pi"],
|
|
2860
|
-
role: "
|
|
2861
|
-
executor: "
|
|
2862
|
-
complexity: "
|
|
3011
|
+
role: "verifier",
|
|
3012
|
+
executor: "shell",
|
|
3013
|
+
complexity: "LOW",
|
|
2863
3014
|
writePolicy: "read-only",
|
|
2864
|
-
artifactFirstUpstreamNodeIds: ["frontend-plan-pi"],
|
|
2865
3015
|
allowedPaths: readOnlyPaths,
|
|
2866
3016
|
forbiddenPaths,
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3017
|
+
outputContract: "Deterministic design policy: materialize the canonical frontend implementation contract from the plan patch, enforce requirement-id retention, Mock strategy/frozen-command binding, writeSet containment, source freshness, and openspec invariants, then evaluate the design policy (write policy result).",
|
|
3018
|
+
subtask_prompt: "Materialize the canonical contract and fail closed unless the deterministic design policy approves. The design verdict is not available at this stage; the writer admission shell enforces it.",
|
|
3019
|
+
shell: {
|
|
3020
|
+
commands: [],
|
|
3021
|
+
frontendDesignPolicy: {
|
|
3022
|
+
schemaVersion: 1,
|
|
3023
|
+
planFromNodeId: "frontend-plan-pi",
|
|
3024
|
+
requiredRequirementIds: requirementIds,
|
|
3025
|
+
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
3026
|
+
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
3027
|
+
? ["not-needed"]
|
|
3028
|
+
: taskConfig.frontendMock?.policy === "required"
|
|
3029
|
+
? [
|
|
3030
|
+
"native",
|
|
3031
|
+
"browser-intercept",
|
|
3032
|
+
"request-adapter",
|
|
3033
|
+
]
|
|
3034
|
+
: [
|
|
3035
|
+
"native",
|
|
3036
|
+
"browser-intercept",
|
|
3037
|
+
"request-adapter",
|
|
3038
|
+
"not-needed",
|
|
3039
|
+
],
|
|
3040
|
+
mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
|
|
3041
|
+
artifactName: "frontend-implementation-contract.json",
|
|
3042
|
+
outputDir: "contracts",
|
|
3043
|
+
requireSourceFreshness: true,
|
|
3044
|
+
implementationWriteSet: implementPaths.writeSet,
|
|
3045
|
+
openspecPolicy: openspecGate.openspecPolicy,
|
|
3046
|
+
openspecSpecRoots: taskConfig.frontendOpenspec?.specRoots ?? [
|
|
3047
|
+
...DEFAULT_FRONTEND_SPEC_ROOTS,
|
|
3048
|
+
],
|
|
3049
|
+
...(requiresOpenspecClassification
|
|
3050
|
+
? { openspecSelectionNodeId: "frontend-contract-pi" }
|
|
3051
|
+
: {}),
|
|
3052
|
+
openspecMandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3053
|
+
openspecCandidateSources: {
|
|
3054
|
+
declared: openspecGate.openspecCandidateSources.declared,
|
|
3055
|
+
taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
|
|
3056
|
+
scanStrict: openspecGate.openspecCandidateSources.scanStrict,
|
|
3057
|
+
},
|
|
3058
|
+
openspecCandidatePaths: openspecGate.openspecCandidatePaths,
|
|
3059
|
+
...(openspecGate.openspecCandidateSnapshots
|
|
3060
|
+
? {
|
|
3061
|
+
openspecCandidateSnapshots: openspecGate.openspecCandidateSnapshots,
|
|
3062
|
+
}
|
|
3063
|
+
: {}),
|
|
3064
|
+
componentSpecCandidatePaths,
|
|
3065
|
+
allowedDependencies: declaredDependencies,
|
|
3066
|
+
},
|
|
3067
|
+
cwd: ".",
|
|
3068
|
+
timeoutMs: 60000,
|
|
2898
3069
|
},
|
|
2899
|
-
allowedPaths: readOnlyPaths,
|
|
2900
|
-
forbiddenPaths,
|
|
2901
|
-
skills: [],
|
|
2902
|
-
outputContract: frontendRevisionOutputContract,
|
|
2903
|
-
subtask_prompt: [
|
|
2904
|
-
"Consume the hash-bound frontend-plan-pi contract artifact and frontend-design-review-pi findings. Read the artifact only for fields affected by a Required Plan Correction.",
|
|
2905
|
-
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
|
|
2906
|
-
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
|
|
2907
|
-
"Only include changed fields. For a changed requirement, preserve its expectedOutcome, implementationTargets, and verificationTargetIds; do not resend unchanged requirements.",
|
|
2908
|
-
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2909
|
-
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2910
|
-
frontendPlanCitationsRequired
|
|
2911
|
-
? "Output exactly two adjacent blocks: (1) one fenced json object containing the merge-patch delta; (2) one openspec-citations citation block."
|
|
2912
|
-
: "JSON-only: output exactly one fenced json object containing the merge-patch delta; do not emit an openspec-citations block.",
|
|
2913
|
-
"Do NOT emit a full contract, Markdown explanation, prose, raw JSON or JSON objects outside the fenced payload, secrets, or unsafe paths.",
|
|
2914
|
-
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
2915
|
-
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2916
|
-
verificationTargetFileInstruction,
|
|
2917
|
-
fixedVerificationContext,
|
|
2918
|
-
artifactFirstSourceContext,
|
|
2919
|
-
frontendPlanFieldGuide,
|
|
2920
|
-
mockContextBlock,
|
|
2921
|
-
frontendComponentConformanceInstruction,
|
|
2922
|
-
frontendPlanCitationInstruction,
|
|
2923
|
-
].join("\n\n"),
|
|
2924
3070
|
},
|
|
2925
3071
|
{
|
|
2926
|
-
id: "frontend-
|
|
2927
|
-
depends_on: [
|
|
2928
|
-
"frontend-plan-revision-pi",
|
|
2929
|
-
"frontend-plan-pi",
|
|
2930
|
-
"frontend-design-review-pi",
|
|
2931
|
-
],
|
|
2932
|
-
dependsPolicy: "all-or-condition-skip",
|
|
2933
|
-
runIf: "$.nodes['frontend-design-review-pi'].firstVerdictLine == 'VERDICT: request-revision'",
|
|
3072
|
+
id: "frontend-design-review-pi",
|
|
3073
|
+
depends_on: ["frontend-design-policy-shell"],
|
|
2934
3074
|
role: "reviewer",
|
|
2935
3075
|
executor: "pi",
|
|
2936
3076
|
complexity: "MED",
|
|
2937
3077
|
writePolicy: "read-only",
|
|
2938
|
-
artifactFirstUpstreamNodeIds: [
|
|
2939
|
-
"frontend-plan-revision-pi",
|
|
2940
|
-
"frontend-plan-pi",
|
|
2941
|
-
],
|
|
2942
3078
|
allowedPaths: readOnlyPaths,
|
|
2943
3079
|
forbiddenPaths,
|
|
2944
3080
|
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
2945
|
-
|
|
2946
|
-
outputContract: "For the effective frontend plan, return plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
|
|
3081
|
+
outputContract: "Authoritative typed design terminal via approve_design / request_design_changes tools. No JSON verdict; the committed typed design fact is the only authority. No file writes.",
|
|
2947
3082
|
subtask_prompt: [
|
|
2948
|
-
"Audit the
|
|
2949
|
-
"
|
|
2950
|
-
"
|
|
2951
|
-
"
|
|
2952
|
-
"
|
|
2953
|
-
"
|
|
2954
|
-
"
|
|
3083
|
+
"Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
|
|
3084
|
+
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
|
|
3085
|
+
"request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3086
|
+
"Your verdict is consumed only as deterministic data input by frontend-writer-admission-shell; it no longer drives any branch or gate. request_design_changes blocks writer admission (terminal).",
|
|
3087
|
+
"Request design changes when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
3088
|
+
"Also request design changes for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
3089
|
+
"Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; or when any uiComponentChoices specReference.path is missing a ledger specReference or has no successful read event.",
|
|
3090
|
+
"You must NOT make authoritative assertions about the execution result of frozen verification commands (typecheck/test/build/lint/etc.). Predicting that a command will necessarily pass or fail, or declaring an acceptance criterion unreachable on that basis, is out of your authority: command results are deterministically established by frontend-verify-shell. Any concern about verification feasibility must be recorded only as a non-blocking verification concern in findings (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical. A pure command-will-fail prediction must not be classified as contract-requirement-gap.",
|
|
2955
3091
|
"Read-only: do not modify repository files.",
|
|
2956
3092
|
fixedVerificationContext,
|
|
2957
|
-
|
|
3093
|
+
sourceContext,
|
|
2958
3094
|
frontendContractFieldSummary,
|
|
2959
3095
|
mockContextBlock,
|
|
2960
3096
|
].join("\n\n"),
|
|
2961
3097
|
},
|
|
2962
3098
|
{
|
|
2963
|
-
id: "frontend-
|
|
2964
|
-
depends_on: [
|
|
2965
|
-
"frontend-final-design-review-pi",
|
|
2966
|
-
"frontend-design-review-pi",
|
|
2967
|
-
"frontend-plan-revision-pi",
|
|
2968
|
-
"frontend-plan-pi",
|
|
2969
|
-
],
|
|
2970
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3099
|
+
id: "frontend-writer-admission-shell",
|
|
3100
|
+
depends_on: ["frontend-design-policy-shell", "frontend-design-review-pi"],
|
|
2971
3101
|
role: "verifier",
|
|
2972
3102
|
executor: "shell",
|
|
2973
3103
|
complexity: "LOW",
|
|
2974
3104
|
writePolicy: "read-only",
|
|
2975
3105
|
allowedPaths: readOnlyPaths,
|
|
2976
3106
|
forbiddenPaths,
|
|
2977
|
-
outputContract: "Deterministic
|
|
2978
|
-
subtask_prompt: "Fail closed unless the
|
|
3107
|
+
outputContract: "Deterministic writer admission: require design review verdict pass, freeze the pre-writer worktree/lint baselines, derive the concrete writeSet + admission digest, and write contracts/frontend-writer-admission-result.json (schemaId frontend-writer-admission-shell-v1).",
|
|
3108
|
+
subtask_prompt: "Fail closed unless the design review approved and the deterministic admission derived a concrete, non-empty writeSet. The admission result is the only write authorization.",
|
|
2979
3109
|
shell: {
|
|
2980
3110
|
commands: [],
|
|
2981
|
-
|
|
3111
|
+
frontendWriterAdmission: {
|
|
2982
3112
|
schemaVersion: 1,
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3113
|
+
designReviewFromNodeId: "frontend-design-review-pi",
|
|
3114
|
+
frozenCommandLabels: [
|
|
3115
|
+
...staticVerifyEvidence.commandLabels,
|
|
3116
|
+
...behaviorVerifyEvidence.commandLabels,
|
|
3117
|
+
...(mockVerifyEvidence?.commandLabels ?? []),
|
|
3118
|
+
],
|
|
2989
3119
|
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
2990
3120
|
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
2991
3121
|
? ["not-needed"]
|
|
2992
3122
|
: taskConfig.frontendMock?.policy === "required"
|
|
2993
|
-
? [
|
|
3123
|
+
? [
|
|
3124
|
+
"native",
|
|
3125
|
+
"browser-intercept",
|
|
3126
|
+
"request-adapter",
|
|
3127
|
+
]
|
|
2994
3128
|
: [
|
|
2995
3129
|
"native",
|
|
2996
3130
|
"browser-intercept",
|
|
2997
3131
|
"request-adapter",
|
|
2998
3132
|
"not-needed",
|
|
2999
3133
|
],
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
openspecPolicy: openspecGate.openspecPolicy,
|
|
3007
|
-
openspecCandidateSources: {
|
|
3008
|
-
declared: openspecGate.openspecCandidateSources.declared,
|
|
3009
|
-
taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
|
|
3010
|
-
scanStrict: openspecGate.openspecCandidateSources.scanStrict,
|
|
3011
|
-
},
|
|
3012
|
-
openspecCandidatePaths: openspecGate.openspecCandidatePaths,
|
|
3013
|
-
componentSpecCandidatePaths,
|
|
3134
|
+
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
3135
|
+
? {
|
|
3136
|
+
lintCommands: lintShellCommands,
|
|
3137
|
+
lintEvidence: lintVerifyEvidence,
|
|
3138
|
+
}
|
|
3139
|
+
: {}),
|
|
3014
3140
|
},
|
|
3015
3141
|
cwd: ".",
|
|
3016
3142
|
timeoutMs: 60000,
|
|
3017
3143
|
},
|
|
3018
3144
|
},
|
|
3019
|
-
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
3020
|
-
? [
|
|
3021
|
-
{
|
|
3022
|
-
id: "frontend-lint-baseline-shell",
|
|
3023
|
-
depends_on: ["frontend-prewrite-gate-shell"],
|
|
3024
|
-
role: "verifier",
|
|
3025
|
-
executor: "shell",
|
|
3026
|
-
complexity: "LOW",
|
|
3027
|
-
writePolicy: "read-only",
|
|
3028
|
-
allowedPaths: readOnlyPaths,
|
|
3029
|
-
forbiddenPaths,
|
|
3030
|
-
outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
|
|
3031
|
-
subtask_prompt: "Run the frozen lint commands read-only. Preserve raw output and mark the baseline unavailable on timeout, execution failure, unparseable output, or worktree mutation.",
|
|
3032
|
-
shell: {
|
|
3033
|
-
commands: lintShellCommands,
|
|
3034
|
-
frontendLintBaseline: {
|
|
3035
|
-
schemaVersion: 1,
|
|
3036
|
-
lintCommands: lintShellCommands,
|
|
3037
|
-
lintEvidence: lintVerifyEvidence,
|
|
3038
|
-
},
|
|
3039
|
-
cwd: ".",
|
|
3040
|
-
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3041
|
-
},
|
|
3042
|
-
},
|
|
3043
|
-
]
|
|
3044
|
-
: []),
|
|
3045
3145
|
{
|
|
3046
3146
|
id: implementId,
|
|
3047
|
-
depends_on: [
|
|
3048
|
-
"frontend-prewrite-gate-shell",
|
|
3049
|
-
...(lintShellCommands.length > 0
|
|
3050
|
-
? ["frontend-lint-baseline-shell"]
|
|
3051
|
-
: []),
|
|
3052
|
-
],
|
|
3147
|
+
depends_on: ["frontend-writer-admission-shell"],
|
|
3053
3148
|
...buildFrontendWriterNodeDefaults({
|
|
3054
3149
|
complexity: resolveWriterComplexity(taskConfig),
|
|
3055
3150
|
writeSet: implementPaths.writeSet,
|
|
3056
3151
|
allowedPaths: implementPaths.allowedPaths,
|
|
3057
3152
|
forbiddenPaths,
|
|
3153
|
+
writerOutcomePolicyType: "frontend-facts-v1",
|
|
3058
3154
|
}),
|
|
3059
|
-
outputContract: "
|
|
3155
|
+
outputContract: "The implementation status is derived by the executor from mechanical facts (write-tool events, run delta, write guard, requirement coverage, focused-check), not from any IMPLEMENTATION_OUTCOME first line. Deliver a Markdown summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
|
|
3060
3156
|
subtask_prompt: [
|
|
3061
|
-
"Implement against the validated run-owned Frontend Implementation Contract
|
|
3157
|
+
"Implement against the validated run-owned Frontend Implementation Contract materialized by frontend-design-policy-shell (path/schema/hash) and authorized by frontend-writer-admission-shell. Do not rebuild the contract from Markdown alone.",
|
|
3062
3158
|
"The canonical contract already contains the approved requirement, target-file, UI-state, verification, design, and Mock/API decisions. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat broad repository research. Inspect only contract target files and directly related local code needed to implement them.",
|
|
3063
3159
|
"Execute in fixed stages and report each in the delivery summary: (1) Contract confirm, (2) Tests sync, (3) Component/UI state implementation, (4) API/Mock wiring per contract.mockApi, (5) Focused checks behind frozen entrypoints only, (6) Diff cleanup.",
|
|
3064
3160
|
"Map every requirement id, expectedOutcome, interaction trigger/expectedBehavior, and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
|
|
3065
|
-
"Begin implementation after the contract and its target files are confirmed. Do not spend the turn collecting optional context. If the canonical contract lacks behavior needed to edit safely,
|
|
3161
|
+
"Begin implementation after the contract and its target files are confirmed. Do not spend the turn collecting optional context. If the canonical contract lacks behavior needed to edit safely, stop and state the blocking reason in the summary instead of reopening broad discovery.",
|
|
3162
|
+
"Your implementation status is derived by the executor from mechanical facts (persisted write-tool events, run delta, write guard, requirement coverage, focused-check failures), never from any IMPLEMENTATION_OUTCOME first line. Do not emit an IMPLEMENTATION_OUTCOME first line.",
|
|
3163
|
+
"The node runs a bounded micro-loop: after each write attempt the executor re-runs frozen focused checks and records a per-round diff checkpoint; the write guard stays active every round. Only repair local issues attributable to the current diff (syntax/type/import/format/unit-assert/obvious omission). Never change requirements, design, writeSet, or verification strictness inside the loop.",
|
|
3066
3164
|
"Implement only the approved Mock strategy carried by the validated contract. Preserve the real request path as the default, require explicit test/dev activation, and never comment out or replace the real request with inline data.",
|
|
3067
|
-
"frontend-
|
|
3165
|
+
"frontend-design-policy-shell materialized and validated the canonical contract; frontend-writer-admission-shell authorized the writeSet. Stay within writeSet and preserve unrelated files.",
|
|
3068
3166
|
"For native, browser-intercept, or request-adapter, implement contract-aligned fixtures/states and a dev/test-only activation boundary in this same writer. For not-needed, do not add Mock files or a framework and state the positive reason.",
|
|
3069
3167
|
"Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
|
|
3070
3168
|
"Edit existing files with the structured edit/write tools. NEVER rewrite Markdown (or any file with quoting/backticks/indentation-sensitive content) via bash sed/awk/echo redirection: escaping mistakes silently corrupt the file and self-repair loops burn the run.",
|
|
@@ -3079,7 +3177,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3079
3177
|
.join("\n\n"),
|
|
3080
3178
|
},
|
|
3081
3179
|
{
|
|
3082
|
-
id: "frontend-verify-
|
|
3180
|
+
id: "frontend-verify-shell",
|
|
3083
3181
|
depends_on: [implementId],
|
|
3084
3182
|
role: "verifier",
|
|
3085
3183
|
executor: "shell",
|
|
@@ -3087,8 +3185,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3087
3185
|
writePolicy: "read-only",
|
|
3088
3186
|
allowedPaths: readOnlyPaths,
|
|
3089
3187
|
forbiddenPaths,
|
|
3090
|
-
outputContract: "Run frozen Mock/static/behavior commands
|
|
3091
|
-
subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence;
|
|
3188
|
+
outputContract: "Run frozen Mock/static/behavior commands and materialize the verification trace. Any failure is terminal: no same-run repair branch, failure ownership facts are materialized for recovery.",
|
|
3189
|
+
subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence; a failure fails this node (terminal) and routes to recovery.",
|
|
3092
3190
|
shell: {
|
|
3093
3191
|
commands: [],
|
|
3094
3192
|
frontendVerificationBundle: {
|
|
@@ -3102,7 +3200,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3102
3200
|
staticEvidence: staticVerifyEvidence,
|
|
3103
3201
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
3104
3202
|
lintBaselineNodeId: lintShellCommands.length > 0
|
|
3105
|
-
? "frontend-
|
|
3203
|
+
? "frontend-writer-admission-shell"
|
|
3106
3204
|
: undefined,
|
|
3107
3205
|
writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
|
|
3108
3206
|
mode: "initial",
|
|
@@ -3111,85 +3209,17 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3111
3209
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3112
3210
|
},
|
|
3113
3211
|
},
|
|
3114
|
-
{
|
|
3115
|
-
id: "frontend-repair-pi",
|
|
3116
|
-
depends_on: ["frontend-verify-assess-shell", implementId],
|
|
3117
|
-
runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
|
|
3118
|
-
...buildFrontendWriterNodeDefaults({
|
|
3119
|
-
complexity: resolveWriterComplexity(taskConfig),
|
|
3120
|
-
writeSet: implementPaths.writeSet,
|
|
3121
|
-
allowedPaths: implementPaths.allowedPaths,
|
|
3122
|
-
forbiddenPaths,
|
|
3123
|
-
}),
|
|
3124
|
-
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
|
|
3125
|
-
subtask_prompt: [
|
|
3126
|
-
"Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
|
|
3127
|
-
"This node runs only for eligible=true. Apply the smallest fix for the classified repairable failure inside the original implement writeSet only.",
|
|
3128
|
-
"The repair assessment and canonical implementation contract are complete inputs for this phase. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat repository-wide discovery.",
|
|
3129
|
-
"Do not change lint/type/test config, do not add .skip/.only, do not comment out real requests, do not default-enable Mock, do not add dependencies.",
|
|
3130
|
-
"Do not re-plan requirements or expand allowed paths. Browser/visual remain not-run.",
|
|
3131
|
-
...(implementPaths.docIndexCompanions.length > 0
|
|
3132
|
-
? [
|
|
3133
|
-
`Doc index sync is MANDATORY: ${implementPaths.docIndexCompanions.join(", ")} are catalog index files for this writeSet. When your repair adds, renames, or removes any indexed file, update ${implementPaths.docIndexCompanions.join(" and ")} in the same run; verification runs check-doc-index and fails the run on a missing index entry.`,
|
|
3134
|
-
]
|
|
3135
|
-
: []),
|
|
3136
|
-
writerDeliveryContract(taskConfig),
|
|
3137
|
-
]
|
|
3138
|
-
.filter((value) => Boolean(value))
|
|
3139
|
-
.join("\n\n"),
|
|
3140
|
-
},
|
|
3141
|
-
{
|
|
3142
|
-
id: "frontend-reverify-shell",
|
|
3143
|
-
depends_on: ["frontend-repair-pi"],
|
|
3144
|
-
role: "verifier",
|
|
3145
|
-
executor: "shell",
|
|
3146
|
-
complexity: "LOW",
|
|
3147
|
-
writePolicy: "read-only",
|
|
3148
|
-
allowedPaths: readOnlyPaths,
|
|
3149
|
-
forbiddenPaths,
|
|
3150
|
-
outputContract: "Post-repair Mock/static/behavior re-verification plus refreshed canonical trace; any failure blocks review.",
|
|
3151
|
-
subtask_prompt: "Re-run the frozen frontend verification bundle after bounded repair and fail on any command or trace failure.",
|
|
3152
|
-
shell: {
|
|
3153
|
-
commands: [],
|
|
3154
|
-
frontendVerificationBundle: {
|
|
3155
|
-
schemaVersion: 1,
|
|
3156
|
-
mockCommands: mockShellCommands,
|
|
3157
|
-
lintCommands: lintShellCommands,
|
|
3158
|
-
staticCommands: staticShellCommands,
|
|
3159
|
-
behaviorCommands: behaviorShellCommands,
|
|
3160
|
-
mockEvidence: mockVerifyEvidence,
|
|
3161
|
-
lintEvidence: lintVerifyEvidence,
|
|
3162
|
-
staticEvidence: staticVerifyEvidence,
|
|
3163
|
-
behaviorEvidence: behaviorVerifyEvidence,
|
|
3164
|
-
lintBaselineNodeId: lintShellCommands.length > 0
|
|
3165
|
-
? "frontend-lint-baseline-shell"
|
|
3166
|
-
: undefined,
|
|
3167
|
-
writerNodeIds: lintShellCommands.length > 0
|
|
3168
|
-
? [implementId, "frontend-repair-pi"]
|
|
3169
|
-
: [],
|
|
3170
|
-
mode: "repair",
|
|
3171
|
-
},
|
|
3172
|
-
cwd: ".",
|
|
3173
|
-
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3174
|
-
},
|
|
3175
|
-
},
|
|
3176
3212
|
{
|
|
3177
3213
|
id: "frontend-review-context-shell",
|
|
3178
|
-
depends_on: [
|
|
3179
|
-
"frontend-reverify-shell",
|
|
3180
|
-
"frontend-repair-pi",
|
|
3181
|
-
"frontend-verify-assess-shell",
|
|
3182
|
-
implementId,
|
|
3183
|
-
],
|
|
3184
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3214
|
+
depends_on: ["frontend-verify-shell", implementId],
|
|
3185
3215
|
role: "verifier",
|
|
3186
3216
|
executor: "shell",
|
|
3187
3217
|
complexity: "LOW",
|
|
3188
3218
|
writePolicy: "read-only",
|
|
3189
3219
|
allowedPaths: readOnlyPaths,
|
|
3190
3220
|
forbiddenPaths,
|
|
3191
|
-
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace,
|
|
3192
|
-
subtask_prompt: "Capture the actual diff and bind it to the effective
|
|
3221
|
+
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, optional verify-failure facts, and actual worktree diff.",
|
|
3222
|
+
subtask_prompt: "Capture the actual diff and bind it to the effective verification evidence for final review.",
|
|
3193
3223
|
shell: {
|
|
3194
3224
|
commands: [],
|
|
3195
3225
|
frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
|
|
@@ -3207,76 +3237,61 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3207
3237
|
allowedPaths: readOnlyPaths,
|
|
3208
3238
|
forbiddenPaths,
|
|
3209
3239
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
3210
|
-
outputContract: '
|
|
3211
|
-
outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
|
|
3240
|
+
outputContract: 'Authoritative typed review terminal via approve_review / request_review_changes tools. No JSON verdict is required in the response text; the typed terminal fact is the only authority. No file writes.',
|
|
3212
3241
|
subtask_prompt: [
|
|
3213
3242
|
"Review the frontend implementation and verification evidence.",
|
|
3214
|
-
"
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
3243
|
+
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_review or request_review_changes. Call it once and do not call the other afterwards.",
|
|
3244
|
+
"approve_review means the implementation passes; it must not carry Critical or Important findings. request_review_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3245
|
+
"Do NOT emit an equivalent JSON verdict in the response text: the committed typed terminal fact is the only authority and no branch or gate reads response-text JSON verdicts.",
|
|
3246
|
+
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, the effective verification trace, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
3219
3247
|
"Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
|
|
3220
3248
|
"Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
|
|
3221
|
-
"The contract embedded in frontend-review-context.json is the effective
|
|
3249
|
+
"The contract embedded in frontend-review-context.json is the effective plan materialized by frontend-design-policy-shell. Do not re-open task sources, OpenSpec, AI workspace, design-review, writer summary, or verification node prose. Inspect only the canonical review context, its bound diff, and diff-referenced files when semantic review requires source code.",
|
|
3222
3250
|
"Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
|
|
3223
|
-
"Inspect the frontend-verify-
|
|
3251
|
+
"Inspect the frontend-verify-shell evidence in the review context directly, including the production/default-real-path static check, and require Mock activation to be off for that check.",
|
|
3224
3252
|
"Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
|
|
3225
3253
|
"Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
|
|
3226
3254
|
].join("\n\n"),
|
|
3227
3255
|
},
|
|
3228
3256
|
{
|
|
3229
|
-
id: "frontend-
|
|
3230
|
-
depends_on: ["frontend-review-pi"],
|
|
3231
|
-
role: "
|
|
3257
|
+
id: "frontend-closeout-shell",
|
|
3258
|
+
depends_on: ["frontend-review-context-shell", "frontend-review-pi"],
|
|
3259
|
+
role: "closeout",
|
|
3232
3260
|
executor: "shell",
|
|
3233
3261
|
complexity: "LOW",
|
|
3234
3262
|
writePolicy: "read-only",
|
|
3235
3263
|
allowedPaths: readOnlyPaths,
|
|
3236
3264
|
forbiddenPaths,
|
|
3237
|
-
outputContract:
|
|
3238
|
-
subtask_prompt:
|
|
3265
|
+
outputContract: "Deterministic closeout rendered from committed facts only: coverage matrix, lint status, integration facts, typed review verdict, cumulative diff, browser/visual not-run, risks, and follow-up. No model summaries are re-interpreted.",
|
|
3266
|
+
subtask_prompt: "Render the closeout deterministically from frontend-review-context.json, the verification trace, and the committed typed review terminal fact.",
|
|
3239
3267
|
shell: {
|
|
3240
3268
|
commands: [],
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
routingAccept: ["request-revision"],
|
|
3245
|
-
label: "frontend review",
|
|
3246
|
-
source: "json-review-verdict",
|
|
3269
|
+
frontendCloseout: {
|
|
3270
|
+
schemaVersion: 1,
|
|
3271
|
+
reviewFromNodeId: "frontend-review-pi",
|
|
3247
3272
|
},
|
|
3248
3273
|
cwd: ".",
|
|
3249
3274
|
timeoutMs: 60000,
|
|
3250
3275
|
},
|
|
3251
3276
|
},
|
|
3252
|
-
{
|
|
3253
|
-
id: "frontend-closeout-pi",
|
|
3254
|
-
depends_on: [
|
|
3255
|
-
"frontend-review-context-shell",
|
|
3256
|
-
"frontend-review-gate-shell",
|
|
3257
|
-
],
|
|
3258
|
-
role: "closeout",
|
|
3259
|
-
executor: "pi",
|
|
3260
|
-
complexity: "MED",
|
|
3261
|
-
writePolicy: "read-only",
|
|
3262
|
-
allowedPaths: taskConfig.allowedPaths.length > 0
|
|
3263
|
-
? [...taskConfig.allowedPaths, "docs/**"]
|
|
3264
|
-
: ["**", "docs/**"],
|
|
3265
|
-
forbiddenPaths,
|
|
3266
|
-
skills: FRONTEND_VERIFICATION_SKILLS,
|
|
3267
|
-
outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
|
|
3268
|
-
subtask_prompt: [
|
|
3269
|
-
"Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
|
|
3270
|
-
"Use only the canonical frontend-review-context.json plus the final frontend review gate verdict. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, design-review, writer, repair, or verification prose, and do not perform new repository research during closeout.",
|
|
3271
|
-
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
3272
|
-
`When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
|
|
3273
|
-
`When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
|
|
3274
|
-
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
3275
|
-
].join("\n\n"),
|
|
3276
|
-
},
|
|
3277
3277
|
],
|
|
3278
3278
|
};
|
|
3279
|
-
|
|
3279
|
+
if (frontendTaskShape.shape === "split-required") {
|
|
3280
|
+
spec.tasks = pruneFrontendTasksForSplitRequired(spec.tasks);
|
|
3281
|
+
}
|
|
3282
|
+
else {
|
|
3283
|
+
const reshapeRaisedTopology = frontendTaskShape.signals.includes("shape-transition-reshape");
|
|
3284
|
+
if (!reshapeRaisedTopology) {
|
|
3285
|
+
spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
|
|
3286
|
+
}
|
|
3287
|
+
if (frontendTaskShape.shape === "micro") {
|
|
3288
|
+
spec.tasks = pruneFrontendTasksForMicro(spec.tasks);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
spec.advisories = [
|
|
3292
|
+
...(spec.advisories ?? []),
|
|
3293
|
+
`frontend-task-shape: ${frontendTaskShape.shape} (${frontendTaskShape.reason})`,
|
|
3294
|
+
];
|
|
3280
3295
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
3281
3296
|
parseDagSpec(spec);
|
|
3282
3297
|
assertValidDagSpec(spec);
|
|
@@ -4871,6 +4886,13 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4871
4886
|
};
|
|
4872
4887
|
const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
4873
4888
|
const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
4889
|
+
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4890
|
+
// exists so ledger-less projects keep generating without noise.
|
|
4891
|
+
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4892
|
+
existsSync(path.join(sources.repoRoot, "testcase/frontend/rag/ui-anchors.md")));
|
|
4893
|
+
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4894
|
+
? "Also read testcase/frontend/rag/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly."
|
|
4895
|
+
: "";
|
|
4874
4896
|
const reviewMode = config.reviewMode;
|
|
4875
4897
|
const blockingReview = reviewMode === "blocking";
|
|
4876
4898
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
@@ -4911,6 +4933,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4911
4933
|
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
4912
4934
|
,
|
|
4913
4935
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4936
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4937
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4914
4938
|
"for(const c of manifest.cases){",
|
|
4915
4939
|
" const id=c&&c.caseId||'?';",
|
|
4916
4940
|
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
@@ -4929,6 +4953,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4929
4953
|
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
4930
4954
|
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
4931
4955
|
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
4956
|
+
" if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))){issues.push({ruleId:'ac-id-truncated',caseId:id,detail:ac+' is a truncated acceptance id (missing feature prefix or sequence number)'});continue;}",
|
|
4932
4957
|
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
4933
4958
|
" }",
|
|
4934
4959
|
" }",
|
|
@@ -4960,6 +4985,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4960
4985
|
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
4961
4986
|
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
4962
4987
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4988
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4989
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4963
4990
|
"for(const c of manifest.cases){",
|
|
4964
4991
|
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)) fail('case-id-shape','caseId must be FE-*, never AC-FE-*: '+String(c&&c.caseId));",
|
|
4965
4992
|
" if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
|
|
@@ -4967,7 +4994,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4967
4994
|
" seen.add(c.caseId);",
|
|
4968
4995
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
4969
4996
|
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
4970
|
-
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4997
|
+
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4971
4998
|
" c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
|
|
4972
4999
|
" c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
|
|
4973
5000
|
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
@@ -5027,15 +5054,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5027
5054
|
writeSet: ragWriteSet,
|
|
5028
5055
|
allowedPaths: [...ragWriteSet],
|
|
5029
5056
|
forbiddenPaths: forbidden,
|
|
5030
|
-
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage.",
|
|
5031
|
-
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package.",
|
|
5057
|
+
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.",
|
|
5058
|
+
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
|
|
5032
5059
|
shell: {
|
|
5033
|
-
commands: [
|
|
5034
|
-
|
|
5035
|
-
"node -e",
|
|
5036
|
-
JSON.stringify("const fs=require('fs'),path=require('path');const dest='testcase/frontend/rag/standard-scenarios.v1.json';const candidates=[path.join('docs','templates','frontend-test-standard-scenarios.v1.json')];let src=null;for(const c of candidates){if(fs.existsSync(c)){src=c;break;}}fs.mkdirSync(path.dirname(dest),{recursive:true});if(src){fs.copyFileSync(src,dest);process.stdout.write(JSON.stringify({status:'copied',from:src,to:dest}));}else{const minimal={schemaVersion:1,id:'frontend-test-standard-scenarios-v1',scenarios:[{id:'STD-FE-SMOKE-ENTRY',title:'入口可打开',category:'smoke',priority:'must',testPoints:['open'],minCases:1}]};fs.writeFileSync(dest,JSON.stringify(minimal,null,2)+'\n');process.stdout.write(JSON.stringify({status:'fallback',to:dest}));}"),
|
|
5037
|
-
].join(" "),
|
|
5038
|
-
],
|
|
5060
|
+
commands: [],
|
|
5061
|
+
frontendTestStandardScenarios: {},
|
|
5039
5062
|
cwd: ".",
|
|
5040
5063
|
timeoutMs: 60_000,
|
|
5041
5064
|
},
|
|
@@ -5071,15 +5094,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5071
5094
|
writeSet: ragWriteSet,
|
|
5072
5095
|
allowedPaths: [...ragWriteSet],
|
|
5073
5096
|
forbiddenPaths: forbidden,
|
|
5074
|
-
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (
|
|
5075
|
-
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/
|
|
5097
|
+
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
|
|
5098
|
+
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
|
|
5076
5099
|
shell: {
|
|
5077
|
-
commands: [
|
|
5078
|
-
|
|
5079
|
-
"node -e",
|
|
5080
|
-
JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl from context.md');baseUrl=baseUrl.replace(/[)\\}\\],."'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
5081
|
-
].join(" "),
|
|
5082
|
-
],
|
|
5100
|
+
commands: [],
|
|
5101
|
+
frontendTestEnvironmentProbe: {},
|
|
5083
5102
|
cwd: ".",
|
|
5084
5103
|
timeoutMs: 60000,
|
|
5085
5104
|
},
|
|
@@ -5100,6 +5119,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5100
5119
|
subtask_prompt: [
|
|
5101
5120
|
"Use skill playwright-cli-case-generator.",
|
|
5102
5121
|
"Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
|
|
5122
|
+
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
5103
5123
|
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
|
|
5104
5124
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
5105
5125
|
"HARD ID CONTRACT (do not confuse these):",
|
|
@@ -5259,7 +5279,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5259
5279
|
subtaskPromptTemplate: [
|
|
5260
5280
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
5261
5281
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
5262
|
-
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5282
|
+
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5263
5283
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
5264
5284
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
5265
5285
|
].join("\n\n"),
|
|
@@ -5282,7 +5302,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5282
5302
|
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
5283
5303
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
5284
5304
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
5285
|
-
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5305
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5286
5306
|
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
5287
5307
|
].join("");
|
|
5288
5308
|
tasks.push({
|
|
@@ -5350,6 +5370,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5350
5370
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
5351
5371
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
5352
5372
|
"Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
|
|
5373
|
+
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
5353
5374
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
5354
5375
|
].join("\n\n"),
|
|
5355
5376
|
},
|
|
@@ -6547,6 +6568,13 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
|
6547
6568
|
spec = await buildSupervisedHybridDag(standard, sources);
|
|
6548
6569
|
}
|
|
6549
6570
|
applyProjectGovernanceReview(spec, template, sources);
|
|
6571
|
+
// Backend implementation templates only (D1 outer gate): assign the two
|
|
6572
|
+
// Pi extension buckets after all template-specific nodes exist.
|
|
6573
|
+
if (template === "standard-dag" ||
|
|
6574
|
+
template === "review-gated-dag" ||
|
|
6575
|
+
template === "supervised-implementation") {
|
|
6576
|
+
applyBackendPiExtensionBuckets(spec);
|
|
6577
|
+
}
|
|
6550
6578
|
// New generate path always emits DagSpec v4 + bindings.
|
|
6551
6579
|
spec.version = 4;
|
|
6552
6580
|
if (!spec.runtimeContract) {
|
|
@@ -6555,7 +6583,12 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
|
6555
6583
|
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
6556
6584
|
spec.taskContractBinding = taskContractBinding;
|
|
6557
6585
|
assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
|
|
6558
|
-
|
|
6586
|
+
if (template === "standard-dag" ||
|
|
6587
|
+
template === "review-gated-dag" ||
|
|
6588
|
+
template === "supervised-implementation") {
|
|
6589
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6590
|
+
}
|
|
6591
|
+
spec = parseDagSpec(spec);
|
|
6559
6592
|
assertValidDagSpec(spec);
|
|
6560
6593
|
return spec;
|
|
6561
6594
|
}
|
|
@@ -6606,6 +6639,36 @@ export async function buildHybridDagFromTask(sources, options = {}) {
|
|
|
6606
6639
|
function cloneTask(task, patch = {}) {
|
|
6607
6640
|
return { ...task, ...patch };
|
|
6608
6641
|
}
|
|
6642
|
+
/**
|
|
6643
|
+
* Backend-implementation Pi extension buckets (plan 2026-08-21 D3):
|
|
6644
|
+
* - read side: every non-writer, non-closeout Pi node gets navigation
|
|
6645
|
+
* extensions (pi-codegraph + pi-lens);
|
|
6646
|
+
* - write side: bounded writers get only pi-codegraph (pi-lens registers
|
|
6647
|
+
* ast_grep_replace, which can bypass the writeSet — not loading it is
|
|
6648
|
+
* simpler and safer than filtering after load);
|
|
6649
|
+
* - everything else (closeout, shell, static, other templates) stays closed.
|
|
6650
|
+
* Idempotent: never overwrites an explicit piExtensions a task declares.
|
|
6651
|
+
*/
|
|
6652
|
+
function applyBackendPiExtensionBuckets(spec) {
|
|
6653
|
+
for (const task of spec.tasks) {
|
|
6654
|
+
if (task.piExtensions !== undefined)
|
|
6655
|
+
continue;
|
|
6656
|
+
if (task.executor !== "pi")
|
|
6657
|
+
continue;
|
|
6658
|
+
if (task.id === "closeout-pi")
|
|
6659
|
+
continue;
|
|
6660
|
+
task.piExtensions =
|
|
6661
|
+
task.toolProfile === "write" ? ["pi-codegraph"] : ["pi-codegraph", "pi-lens"];
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
function stampTargetTemplateTransientRetryProfile(spec) {
|
|
6665
|
+
for (const task of spec.tasks) {
|
|
6666
|
+
if (isTargetTemplateImplementPi(task) ||
|
|
6667
|
+
isCanonicalFinalVerifyShellRetryCandidate(task)) {
|
|
6668
|
+
task.transientRetryProfile = TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE;
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6609
6672
|
/**
|
|
6610
6673
|
* Apply default Pi retry policies to generated DAG nodes:
|
|
6611
6674
|
* - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
|
|
@@ -6626,6 +6689,10 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
|
6626
6689
|
: DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
6627
6690
|
continue;
|
|
6628
6691
|
}
|
|
6692
|
+
if (isTargetTemplateImplementPi(task)) {
|
|
6693
|
+
task.retryPolicy = TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY;
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
6629
6696
|
if (isWriterTransportRetryCandidate(task)) {
|
|
6630
6697
|
task.retryPolicy = WRITER_TRANSPORT_RETRY_POLICY;
|
|
6631
6698
|
}
|
|
@@ -6886,6 +6953,7 @@ function buildReviewGatedHybridDag(standard, sources) {
|
|
|
6886
6953
|
}));
|
|
6887
6954
|
spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
|
|
6888
6955
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
6956
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6889
6957
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
6890
6958
|
parseDagSpec(spec);
|
|
6891
6959
|
assertValidDagSpec(spec);
|
|
@@ -7217,6 +7285,9 @@ async function buildHardVerifyNode(sources) {
|
|
|
7217
7285
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
7218
7286
|
outputContract: "Archived hard verification stdout/stderr with exit codes; no worktree writes.",
|
|
7219
7287
|
subtask_prompt: "Run hard verification after repair round.",
|
|
7288
|
+
transientRetryProfile: sources.verifyCommands && commands.length > 0
|
|
7289
|
+
? TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE
|
|
7290
|
+
: undefined,
|
|
7220
7291
|
shell: {
|
|
7221
7292
|
commands,
|
|
7222
7293
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -7387,6 +7458,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
7387
7458
|
],
|
|
7388
7459
|
};
|
|
7389
7460
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
7461
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
7390
7462
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
7391
7463
|
parseDagSpec(spec);
|
|
7392
7464
|
assertValidDagSpec(spec);
|