@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.11
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 +349 -97
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +23 -5
- package/dist/cli/program.js +21 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/cli.js +2 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- 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 +455 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +2370 -64
- package/dist/executors/pi-executor.js +11 -2
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/pi-sdk-executor.js +225 -33
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +1258 -224
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-failure-category.js +150 -0
- package/dist/shared/dag-prompt-override.js +27 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +39 -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 +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +50 -5
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +18 -1
- package/dist/task/contract/schema.js +4 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +199 -18
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +298 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/cli.js +18 -2
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +395 -59
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -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/inspect-split.js +18 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +227 -7
- package/dist/worker/console/operator-actions.js +388 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +355 -3
- package/dist/worker/console/security.js +67 -13
- package/dist/worker/console/server.js +609 -164
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-CXj_GnSb.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BZp6JAp7.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-DfpcEuYU.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-_Gf0xadb.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Ct2QPCmv.js} +1 -1
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BfklkzKl.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-Dy25vZJV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-BFlCRZep.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-op3oVxIE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-C0R7rzF2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-Bt44TCGy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-_uEE_XFx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-C3TOYGZ9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-Cz60oBPV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-8Bik0qis.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C2QIOA4a.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CSH_yUUd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BDLw2XFM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-CcD9ZtF4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DqwtkBsI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BWVEcqsC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-M-WAbtQi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DKW7SixP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-PHHPPtrj.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-BQD-B4RX.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-BFvULb51.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-Bw-aXTCb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BGKw04Qx.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-RqKaPR-I.js} +1 -1
- package/dist/worker/console/static/assets/index-B_D8rbWc.js +407 -0
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-YO5dnzrJ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-Bszls1DD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-PhzeaZ09.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-BsjbDoXi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-0B7NnWKk.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BJr4Fj-q.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-moC4fpGB.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BEw37-2c.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-Cq6LyasU.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-DUCMcK0D.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-C3upTZm7.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-BI_gMsCW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-YFOIRzfN.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-dOnB7UDD.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BXUniaIh.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-2tA4wTNu.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-DO0HkJXC.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DvIOzixv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DXDZ0cTj.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-B32Ark0D.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
- package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +183 -48
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +344 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- 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/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +90 -4
- package/dist/worker/observe/static/app.js +18 -70
- 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/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +208 -0
- package/dist/worker/observe/static/format.js +60 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -96
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +184 -1
- package/dist/worker/observe/static/styles.css +395 -0
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +14 -5
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +2 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
- 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 +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +288 -14
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +272 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +1072 -660
- package/dist/workflows/dag/interrupt-request.js +10 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +552 -58
- package/dist/workflows/dag/prompt.js +44 -19
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +124 -1
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +81 -6
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +318 -8
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +338 -31
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +92 -32
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +9 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +63 -1
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- 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/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +7 -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-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +3 -4
- package/skills/frontend-implementation/references/design-spec.md +15 -10
- package/skills/frontend-implementation/references/node-contracts.md +23 -13
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/command-reference.md +12 -4
- package/skills/loop-agent/references/hybrid-dag.md +8 -1
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- 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-DTZOKgAn.js +0 -404
- 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
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { readPackageVersion } from "../../shared/package-metadata.js";
|
|
3
|
+
import { createNpmUpdateClient } from "../../shared/update/npm-client.js";
|
|
4
|
+
import { acquireUpdateLock, updateLockPath } from "../../shared/update/state.js";
|
|
5
|
+
import { readInitRuntimeActivity } from "../observability/init-runtime-activity.js";
|
|
6
|
+
import { performConsoleUpdateAndInit, } from "./console-update-and-init.js";
|
|
7
|
+
import { TERMINAL_OPERATION_STATES } from "./operation-store.js";
|
|
8
|
+
export function createConsoleUpdateExecutor(input) {
|
|
9
|
+
return async (channel) => {
|
|
10
|
+
const currentVersion = readPackageVersion(input.packageRoot);
|
|
11
|
+
if (!currentVersion) {
|
|
12
|
+
return {
|
|
13
|
+
state: "failed",
|
|
14
|
+
errorCode: "PACKAGE_IDENTITY_UNAVAILABLE",
|
|
15
|
+
errorMessage: "Console package version cannot be proven",
|
|
16
|
+
maintenance: {
|
|
17
|
+
package: { status: "failed", channel },
|
|
18
|
+
initialization: { status: "not-run", pendingPaths: [] },
|
|
19
|
+
handoff: { status: "not-needed" },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const npm = createNpmUpdateClient({
|
|
24
|
+
currentVersion,
|
|
25
|
+
packageRoot: input.packageRoot,
|
|
26
|
+
cwd: input.repoRoot,
|
|
27
|
+
});
|
|
28
|
+
return performConsoleUpdateAndInit({
|
|
29
|
+
currentVersion,
|
|
30
|
+
preflight: async () => {
|
|
31
|
+
const [activity, operations] = await Promise.all([
|
|
32
|
+
readInitRuntimeActivity(input.repoRoot),
|
|
33
|
+
input.operations.list(),
|
|
34
|
+
]);
|
|
35
|
+
if (activity.workerProjectionError ||
|
|
36
|
+
activity.activeDagRunIds.length > 0 ||
|
|
37
|
+
activity.activeWorkerTasks > 0 ||
|
|
38
|
+
activity.activeWorkerBatches > 0) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
code: "ACTIVE_RUNTIME",
|
|
42
|
+
message: "active DAG/Worker runtime exists or cannot be proven inactive",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (input.activeChatTurnCount() > 0) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: "CHAT_TURN_ACTIVE",
|
|
49
|
+
message: "an Operator Chat turn is active",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const liveOtherOperation = operations.some((operation) => operation.action !== "consoleUpdateAndInit" &&
|
|
53
|
+
!TERMINAL_OPERATION_STATES.has(operation.state));
|
|
54
|
+
return liveOtherOperation
|
|
55
|
+
? {
|
|
56
|
+
ok: false,
|
|
57
|
+
code: "OPERATION_ACTIVE",
|
|
58
|
+
message: "another Console operation is active",
|
|
59
|
+
}
|
|
60
|
+
: { ok: true };
|
|
61
|
+
},
|
|
62
|
+
acquireLock: () => acquireUpdateLock(updateLockPath(os.homedir())),
|
|
63
|
+
resolveVersion: (selectedChannel) => npm.distTagVersionCheck(selectedChannel),
|
|
64
|
+
canAutoInstall: () => npm.canAutoInstall(),
|
|
65
|
+
installExact: (version, selectedChannel) => npm.installExact(version, selectedChannel),
|
|
66
|
+
verifyInstalled: (version) => npm.verifyInstalled(version),
|
|
67
|
+
applySafeInit: async () => {
|
|
68
|
+
const result = await input.client.runExternal("loop-agent", ["init", "update", "--apply-safe", "--repo-root", input.repoRoot], {
|
|
69
|
+
cwd: input.repoRoot,
|
|
70
|
+
artifactName: "console-update-safe-init",
|
|
71
|
+
timeoutMs: 120_000,
|
|
72
|
+
});
|
|
73
|
+
return result.ok
|
|
74
|
+
? { ok: true, pendingPaths: [] }
|
|
75
|
+
: {
|
|
76
|
+
ok: false,
|
|
77
|
+
message: result.stderr || result.stdout || "safe initialization failed",
|
|
78
|
+
pendingPaths: [],
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
requestHandoff: async ({ targetVersion }) => input.requestHandoff(targetVersion),
|
|
82
|
+
}, channel);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -35,6 +35,32 @@ import { hashDagBytes, } from "./dag-confirmation.js";
|
|
|
35
35
|
* framework invocations that don't embed the word "verify".
|
|
36
36
|
*/
|
|
37
37
|
export const STRUCTURED_VERIFICATION_COMMAND_RE = /\b(vitest|jest|playwright|cypress|pytest|py-test|go\s+test|cargo\s+test|npm\s+run\s+(lint|typecheck|test|verify)|npm\s+(test|run\s+test)|check-repo\.sh|loop-agent-standard-verify)\b/i;
|
|
38
|
+
/**
|
|
39
|
+
* Deterministic shell preset keys whose executors are themselves structured
|
|
40
|
+
* verification/gate materializers: their shell.commands[] is empty and the
|
|
41
|
+
* preset alone carries the verification authority (hash-bound artifact
|
|
42
|
+
* gates, evidence validators, report finalize). Mirrors the named-shell
|
|
43
|
+
* handler registry in shell-executor.ts so preset-only workflows
|
|
44
|
+
* (frontend-test finalize/reports, backend-test pipeline, verify-evidence
|
|
45
|
+
* gates) pass the G2 autonomous execution gate without depending on node-id
|
|
46
|
+
* naming conventions.
|
|
47
|
+
*/
|
|
48
|
+
export const STRUCTURED_VERIFICATION_SHELL_PRESETS = new Set([
|
|
49
|
+
"frontendTestCaseChecklist",
|
|
50
|
+
"frontendTestCaseManifest",
|
|
51
|
+
"frontendTestEvidenceValidation",
|
|
52
|
+
"frontendTestResultFinalize",
|
|
53
|
+
"frontendTestL5Report",
|
|
54
|
+
"frontendTestHtmlReport",
|
|
55
|
+
"frontendTestReports",
|
|
56
|
+
"backendTestPipeline",
|
|
57
|
+
"verifyEvidence",
|
|
58
|
+
"repairArtifactGate",
|
|
59
|
+
"jacocoCoverage",
|
|
60
|
+
"requirementCoverageGate",
|
|
61
|
+
"jsonArtifactGate",
|
|
62
|
+
"verdictGate",
|
|
63
|
+
]);
|
|
38
64
|
export const DEFAULT_EXECUTION_RECEIPT_TTL_MS = 30 * 60 * 1000;
|
|
39
65
|
function normalizePathEntry(entry) {
|
|
40
66
|
return entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -157,6 +183,13 @@ export function deriveEligibilityFactsFromDagSpec(input) {
|
|
|
157
183
|
return false;
|
|
158
184
|
if (/verify|verification/i.test(task.id ?? ""))
|
|
159
185
|
return true;
|
|
186
|
+
// Preset-only verification shells (empty commands[]): the preset key is
|
|
187
|
+
// the verification authority (frontend-test finalize, backend-test
|
|
188
|
+
// pipeline, evidence/artifact gates).
|
|
189
|
+
const shellRecord = task.shell;
|
|
190
|
+
const hasVerificationPreset = Object.keys(shellRecord).some((key) => STRUCTURED_VERIFICATION_SHELL_PRESETS.has(key));
|
|
191
|
+
if (hasVerificationPreset)
|
|
192
|
+
return true;
|
|
160
193
|
const commands = stringList(task.shell.commands);
|
|
161
194
|
return commands.some((command) => STRUCTURED_VERIFICATION_COMMAND_RE.test(command));
|
|
162
195
|
});
|
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
|
|
3
|
+
/**
|
|
4
|
+
* 从任意上游载荷安全投影 ledger 摘要(未知形状丢弃,绝不抛错、绝不信任字段外的数据)。
|
|
5
|
+
* 供 DraftStore / prd-intake-bridge 把 prepare/advance 结果中的 ledger 摘要带入草稿。
|
|
6
|
+
*/
|
|
7
|
+
export function summarizeLedgerPayload(value) {
|
|
8
|
+
if (!value || typeof value !== "object")
|
|
9
|
+
return undefined;
|
|
10
|
+
const record = value;
|
|
11
|
+
if (record.present !== true)
|
|
12
|
+
return undefined;
|
|
13
|
+
const num = (key) => {
|
|
14
|
+
const raw = record[key];
|
|
15
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
16
|
+
};
|
|
17
|
+
const gateRaw = record.gateStatus;
|
|
18
|
+
const gateStatus = gateRaw === "pass" || gateRaw === "fail" || gateRaw === "unknown"
|
|
19
|
+
? gateRaw
|
|
20
|
+
: "unknown";
|
|
21
|
+
const failureCode = typeof record.failureCode === "string" && record.failureCode
|
|
22
|
+
? record.failureCode
|
|
23
|
+
: null;
|
|
24
|
+
const rawExclusions = Array.isArray(record.exclusions) ? record.exclusions : [];
|
|
25
|
+
const exclusions = [];
|
|
26
|
+
for (const item of rawExclusions) {
|
|
27
|
+
if (!item || typeof item !== "object")
|
|
28
|
+
continue;
|
|
29
|
+
const entry = item;
|
|
30
|
+
const fragmentId = typeof entry.fragmentId === "string" ? entry.fragmentId : "";
|
|
31
|
+
const reason = typeof entry.reason === "string" ? entry.reason : "";
|
|
32
|
+
if (!fragmentId)
|
|
33
|
+
continue;
|
|
34
|
+
exclusions.push({ fragmentId, reason });
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
present: true,
|
|
38
|
+
fragmentCount: num("fragmentCount"),
|
|
39
|
+
canonicalRequirementCount: num("canonicalRequirementCount"),
|
|
40
|
+
contextOnlyCount: num("contextOnlyCount"),
|
|
41
|
+
duplicateCount: num("duplicateCount"),
|
|
42
|
+
unresolvedCount: num("unresolvedCount"),
|
|
43
|
+
conflictCount: num("conflictCount"),
|
|
44
|
+
gateStatus,
|
|
45
|
+
failureCode,
|
|
46
|
+
exclusions,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
3
49
|
function draftPath(appData, taskId) {
|
|
4
50
|
const safe = taskId.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
5
51
|
return path.join(appData.drafts, `${safe}.json`);
|
|
@@ -86,6 +132,9 @@ export function toCanonicalDraftForCli(draft) {
|
|
|
86
132
|
if (draft.interviewAssessmentRef) {
|
|
87
133
|
canonical.interviewAssessmentRef = draft.interviewAssessmentRef;
|
|
88
134
|
}
|
|
135
|
+
// AC-BLOCK-002:canonical draft JSON 绝不携带 ledgerSummary 字段(strict
|
|
136
|
+
// taskContractDraftSchema 拒绝未知键)。摘要仅留在 prd-intake-bridge 响应与
|
|
137
|
+
// console-types 的 LedgerSummaryView(UI 视图模型),不进入持久化 draft。
|
|
89
138
|
return canonical;
|
|
90
139
|
}
|
|
91
140
|
export class DraftStore {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parseFrontendHumanDecisionProjection } from "../../workflows/dag/frontend-human-decision.js";
|
|
2
|
+
/** Worker/Console persistence seam. Canonical validation and freshness remain in the workflow service. */
|
|
3
|
+
export function createFrontendHumanDecisionAdapter(store) {
|
|
4
|
+
return {
|
|
5
|
+
async load(operationId) {
|
|
6
|
+
const record = await store.get(operationId);
|
|
7
|
+
if (!record || record.revision === undefined || !record.frontendHumanDecision)
|
|
8
|
+
return undefined;
|
|
9
|
+
return { revision: record.revision, projection: parseFrontendHumanDecisionProjection(record.frontendHumanDecision) };
|
|
10
|
+
},
|
|
11
|
+
materialize(input) {
|
|
12
|
+
return store.materializeFrontendHumanDecision(input);
|
|
13
|
+
},
|
|
14
|
+
consume(input) {
|
|
15
|
+
return store.consumeFrontendHumanDecision(input);
|
|
16
|
+
},
|
|
17
|
+
initializeSplit: store.initializeFrontendHumanApprovedSplit.bind(store),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FrontendSplitOrchestrator, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
|
|
2
|
+
/** Worker/Console boundary adapter. All orchestration policy remains canonical. */
|
|
3
|
+
export function createFrontendSplitOperationAdapter(dependencies) {
|
|
4
|
+
const ports = {
|
|
5
|
+
load: async (operationId) => {
|
|
6
|
+
const record = await dependencies.store.get(operationId);
|
|
7
|
+
return record?.frontendSplit === undefined ? undefined : frontendSplitProjectionSchema.parse(record.frontendSplit);
|
|
8
|
+
},
|
|
9
|
+
compareAndSwap: (input) => dependencies.store.compareAndSwapFrontendSplit(input),
|
|
10
|
+
materialize: async (request) => {
|
|
11
|
+
const result = await dependencies.materializer.materialize(request);
|
|
12
|
+
return { taskId: result.taskId, operationId: result.operationId };
|
|
13
|
+
},
|
|
14
|
+
schedule: dependencies.schedule,
|
|
15
|
+
auditStale: dependencies.auditStale,
|
|
16
|
+
loadChildCloseoutFacts: (input) => dependencies.store.loadChildCloseoutFacts(input),
|
|
17
|
+
settleAggregate: (input) => dependencies.store.settleFrontendSplitAggregate({ ...input, prepareAggregate: dependencies.prepareAggregate }),
|
|
18
|
+
};
|
|
19
|
+
return new FrontendSplitOrchestrator(ports);
|
|
20
|
+
}
|
|
@@ -11,6 +11,9 @@ export { createClosedResourceLoader, createInterviewResourceLoader, tryActivateT
|
|
|
11
11
|
export { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
12
12
|
export { openConsoleAppData, defaultConsoleAppDataRoot } from "./app-data.js";
|
|
13
13
|
export { OperationStore } from "./operation-store.js";
|
|
14
|
+
export { createFrontendSplitOperationAdapter } from "./frontend-split-operation-adapter.js";
|
|
15
|
+
export { createFrontendHumanDecisionAdapter } from "./frontend-human-decision-adapter.js";
|
|
16
|
+
export { FrontendSplitTaskMaterializer } from "../materialize/frontend-split-task-materializer.js";
|
|
14
17
|
export { createOperationEventStore } from "./operation-sse.js";
|
|
15
18
|
export { DagConfirmationStore } from "./dag-confirmation.js";
|
|
16
19
|
export { DraftStore } from "./draft-store.js";
|
|
@@ -80,3 +80,21 @@ export function pickInspectNode(nodes, preferredNodeId) {
|
|
|
80
80
|
});
|
|
81
81
|
return running ?? nodes[0];
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Terminal DagRun effective/status values (mirrors observe read-model
|
|
85
|
+
* TERMINAL_RUN_STATUSES). A run that reached one of these states no longer
|
|
86
|
+
* needs live polling in the console split panel.
|
|
87
|
+
*/
|
|
88
|
+
export const TERMINAL_DAG_RUN_STATUSES = new Set([
|
|
89
|
+
"finished",
|
|
90
|
+
"failed",
|
|
91
|
+
"partial_failed",
|
|
92
|
+
"superseded",
|
|
93
|
+
"abandoned",
|
|
94
|
+
]);
|
|
95
|
+
export function isTerminalDagRun(dag) {
|
|
96
|
+
if (!dag)
|
|
97
|
+
return false;
|
|
98
|
+
const status = String(dag.effectiveStatus ?? dag.status ?? "").toLowerCase();
|
|
99
|
+
return TERMINAL_DAG_RUN_STATUSES.has(status);
|
|
100
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH's native directory-picker adapter.
|
|
3
|
+
*
|
|
4
|
+
* The published DSH host package uses the modern Win32 IFileOpenDialog in a
|
|
5
|
+
* dedicated child process (plus osascript/Zenity/KDialog on other platforms),
|
|
6
|
+
* so the Console does not pay PowerShell startup cost or lose the chooser
|
|
7
|
+
* behind the browser window.
|
|
8
|
+
*/
|
|
9
|
+
import { pickNativeDirectory, } from "@deepseek-ai/dsh-host-directory-picker-native";
|
|
10
|
+
/** Open the host OS single-folder chooser. Cancellation is a null result. */
|
|
11
|
+
export async function pickNativeWorkspaceDirectory(signal, internals = {}) {
|
|
12
|
+
return pickNativeDirectory(signal, internals);
|
|
13
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - A run summary is a projection only: it can never change the terminal
|
|
14
14
|
* operation state decided by the command envelope/exit facts.
|
|
15
15
|
*/
|
|
16
|
+
import { normalizeDagFailureCategory } from "../../shared/dag-failure-category.js";
|
|
16
17
|
import { scrubSecrets } from "./chat/explore-tools.js";
|
|
17
18
|
const RUN_SUMMARY_MAX_NODES = 20;
|
|
18
19
|
const RUN_SUMMARY_MAX_TEXT_CHARS = 600;
|
|
@@ -72,7 +73,13 @@ function safeNode(value) {
|
|
|
72
73
|
const status = boundedRedactedText(rec.status, 100);
|
|
73
74
|
if (!id || !status)
|
|
74
75
|
return undefined;
|
|
75
|
-
|
|
76
|
+
const rawCategory = boundedRedactedText(rec.failureCategory, 200);
|
|
77
|
+
if (!rawCategory)
|
|
78
|
+
return { id, status };
|
|
79
|
+
const category = normalizeDagFailureCategory(rawCategory, status);
|
|
80
|
+
if (category === "success" || category === "skipped")
|
|
81
|
+
return { id, status };
|
|
82
|
+
return { id, status, failureCategory: category };
|
|
76
83
|
}
|
|
77
84
|
/** Independent allowlist projection of a structured run summary. */
|
|
78
85
|
export function safeRunSummaryForProjection(value) {
|
|
@@ -117,13 +124,22 @@ export function safeRunSummaryForProjection(value) {
|
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
const verdict = boundedRedactedText(rec.verdict);
|
|
120
|
-
const
|
|
127
|
+
const rawFailureCategory = boundedRedactedText(rec.failureCategory, 200);
|
|
128
|
+
// Normalize the run-level category too (2026-08-23): raw codes like
|
|
129
|
+
// missing-api-key / nonzero-exit never matched the browser advice table.
|
|
130
|
+
const failureCategory = rawFailureCategory !== undefined
|
|
131
|
+
? normalizeDagFailureCategory(rawFailureCategory, status)
|
|
132
|
+
: undefined;
|
|
121
133
|
return {
|
|
122
134
|
runId,
|
|
123
135
|
status,
|
|
124
136
|
nodes,
|
|
125
137
|
...(verdict !== undefined ? { verdict } : {}),
|
|
126
|
-
...(failureCategory !== undefined
|
|
138
|
+
...(failureCategory !== undefined &&
|
|
139
|
+
failureCategory !== "success" &&
|
|
140
|
+
failureCategory !== "skipped"
|
|
141
|
+
? { failureCategory }
|
|
142
|
+
: {}),
|
|
127
143
|
...(outOfBounds.length > 0 ? { outOfBounds } : {}),
|
|
128
144
|
...(next ? { next } : {}),
|
|
129
145
|
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { assertFrontendSplitProjectionSuccessor, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
|
|
4
|
+
import { buildFrontendHumanDecisionProjection, consumeFrontendHumanDecision, materializeFrontendHumanDecision, parseFrontendHumanDecisionProjection, parseFrontendHumanDecisionReceipt, } from "../../workflows/dag/frontend-human-decision.js";
|
|
5
|
+
import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendSplitChildCloseoutFactsSchema, initializeFrontendSplitParent, } from "../../workflows/dag/frontend-split-orchestrator.js";
|
|
6
|
+
import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
|
|
3
7
|
import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
|
|
4
|
-
export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out"]);
|
|
8
|
+
export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
|
|
5
9
|
function operationFile(appData, operationId) {
|
|
6
10
|
return path.join(appData.operations, `${operationId}.json`);
|
|
7
11
|
}
|
|
@@ -15,16 +19,21 @@ export function computeActionPayloadHash(action, actionParams) {
|
|
|
15
19
|
export function newOperationId() {
|
|
16
20
|
return `op_${randomBytes(12).toString("hex")}`;
|
|
17
21
|
}
|
|
22
|
+
const sharedWriteChains = new Map();
|
|
18
23
|
export class OperationStore {
|
|
19
24
|
appData;
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
* (Windows EPERM) or last-write-wins lost patches.
|
|
24
|
-
*/
|
|
25
|
-
writeChains = new Map();
|
|
25
|
+
/** Shared per-store-root, per-key write lock. Multiple OperationStore instances
|
|
26
|
+
* in one worker process must observe the same CAS critical section. */
|
|
27
|
+
writeChains;
|
|
26
28
|
constructor(appData) {
|
|
27
29
|
this.appData = appData;
|
|
30
|
+
const root = path.resolve(appData.operations);
|
|
31
|
+
let chains = sharedWriteChains.get(root);
|
|
32
|
+
if (!chains) {
|
|
33
|
+
chains = new Map();
|
|
34
|
+
sharedWriteChains.set(root, chains);
|
|
35
|
+
}
|
|
36
|
+
this.writeChains = chains;
|
|
28
37
|
}
|
|
29
38
|
serialize(key, op) {
|
|
30
39
|
const prev = this.writeChains.get(key) ?? Promise.resolve();
|
|
@@ -66,6 +75,7 @@ export class OperationStore {
|
|
|
66
75
|
payloadHash,
|
|
67
76
|
state: "queued",
|
|
68
77
|
createdAt: now,
|
|
78
|
+
revision: 1,
|
|
69
79
|
taskId: input.taskId,
|
|
70
80
|
cliArgs: input.cliArgs,
|
|
71
81
|
...(input.externalCommand
|
|
@@ -77,11 +87,61 @@ export class OperationStore {
|
|
|
77
87
|
return { kind: "created", operation };
|
|
78
88
|
});
|
|
79
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Idempotently materialize a split child/integration operation with a
|
|
92
|
+
* caller-specified deterministic operationId (from the split manifest).
|
|
93
|
+
* Unlike create() (random op_<hex> id) and save/update (no create), this is
|
|
94
|
+
* the durable cross-restart idempotency boundary for the production
|
|
95
|
+
* FrontendSplitTaskMaterializer backend: an existing operation file with the
|
|
96
|
+
* same taskId + payloadHash is returned as {created:false}; any drift fails
|
|
97
|
+
* closed instead of overwriting or silently diverging.
|
|
98
|
+
*/
|
|
99
|
+
async ensureSplitMaterializedOperation(input) {
|
|
100
|
+
return this.serialize(input.operationId, async () => {
|
|
101
|
+
const existing = await this.get(input.operationId);
|
|
102
|
+
if (existing) {
|
|
103
|
+
if (existing.taskId !== input.taskId ||
|
|
104
|
+
existing.payloadHash !== input.payloadHash) {
|
|
105
|
+
throw new Error("frontend split operation materialization payload drift");
|
|
106
|
+
}
|
|
107
|
+
return { operation: existing, created: false };
|
|
108
|
+
}
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
const operation = {
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
operationId: input.operationId,
|
|
113
|
+
clientRequestId: `split:${input.operationId}`,
|
|
114
|
+
action: input.action,
|
|
115
|
+
actionParams: input.actionParams,
|
|
116
|
+
payloadHash: input.payloadHash,
|
|
117
|
+
state: "queued",
|
|
118
|
+
createdAt: now,
|
|
119
|
+
revision: 1,
|
|
120
|
+
taskId: input.taskId,
|
|
121
|
+
...(input.cliArgs ? { cliArgs: input.cliArgs } : {}),
|
|
122
|
+
};
|
|
123
|
+
await writeSecureJson(operationFile(this.appData, input.operationId), operation);
|
|
124
|
+
return { operation, created: true };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
80
127
|
async get(operationId) {
|
|
81
128
|
return readJsonIfExists(operationFile(this.appData, operationId));
|
|
82
129
|
}
|
|
83
130
|
async save(operation) {
|
|
84
131
|
return this.serialize(operation.operationId, async () => {
|
|
132
|
+
const current = await this.get(operation.operationId);
|
|
133
|
+
if (current?.revision !== undefined && operation.revision !== current.revision)
|
|
134
|
+
throw new Error("operation revision may not be changed by save");
|
|
135
|
+
if (operation.frontendHumanDecision !== undefined || current?.frontendHumanDecision !== undefined) {
|
|
136
|
+
if (!current || JSON.stringify(operation) !== JSON.stringify(current))
|
|
137
|
+
throw new Error("human-decision projection may only be mutated by canonical CAS");
|
|
138
|
+
return current;
|
|
139
|
+
}
|
|
140
|
+
if (operation.frontendSplit !== undefined || current?.frontendSplit !== undefined) {
|
|
141
|
+
if (!current || JSON.stringify(operation) !== JSON.stringify(current))
|
|
142
|
+
throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
|
|
143
|
+
return current;
|
|
144
|
+
}
|
|
85
145
|
await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
|
|
86
146
|
return operation;
|
|
87
147
|
});
|
|
@@ -92,6 +152,10 @@ export class OperationStore {
|
|
|
92
152
|
if (!current) {
|
|
93
153
|
throw new Error(`operation not found: ${operationId}`);
|
|
94
154
|
}
|
|
155
|
+
if ("frontendHumanDecision" in patch || (current.frontendHumanDecision !== undefined && ("state" in patch || "frontendExecution" in patch)))
|
|
156
|
+
throw new Error("human-decision projection may only be mutated by canonical CAS");
|
|
157
|
+
if ("frontendSplit" in patch || "revision" in patch || (current.frontendSplit !== undefined && ("state" in patch || "frontendExecution" in patch)))
|
|
158
|
+
throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
|
|
95
159
|
const next = {
|
|
96
160
|
...current,
|
|
97
161
|
...patch,
|
|
@@ -106,6 +170,162 @@ export class OperationStore {
|
|
|
106
170
|
return next;
|
|
107
171
|
});
|
|
108
172
|
}
|
|
173
|
+
async materializeFrontendHumanDecision(input) {
|
|
174
|
+
return this.serialize(input.operationId, async () => {
|
|
175
|
+
const current = await this.get(input.operationId);
|
|
176
|
+
if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision) {
|
|
177
|
+
const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.envelope.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
178
|
+
return { outcome: "rejected", reason: "record revision conflict or legacy record", projection: fallback };
|
|
179
|
+
}
|
|
180
|
+
const projection = current.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: current.operationId, runId: input.envelope.runId, revision: current.revision });
|
|
181
|
+
const result = materializeFrontendHumanDecision({ projection, envelope: input.envelope, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest });
|
|
182
|
+
if (result.outcome !== "applied")
|
|
183
|
+
return result;
|
|
184
|
+
const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection, state: "running" };
|
|
185
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async consumeFrontendHumanDecision(input) {
|
|
190
|
+
return this.serialize(input.operationId, async () => {
|
|
191
|
+
const current = await this.get(input.operationId);
|
|
192
|
+
if (!current || current.revision === undefined || !current.frontendHumanDecision) {
|
|
193
|
+
const fallback = buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
194
|
+
return { outcome: "rejected", reason: "record has no mutable human-decision projection", projection: fallback };
|
|
195
|
+
}
|
|
196
|
+
const projection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
|
|
197
|
+
if (current.revision !== input.expectedRecordRevision)
|
|
198
|
+
return { outcome: "rejected", reason: "record revision conflict", projection };
|
|
199
|
+
const result = consumeFrontendHumanDecision({ projection, receipt: input.receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
|
|
200
|
+
if (result.outcome !== "applied")
|
|
201
|
+
return result;
|
|
202
|
+
const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection };
|
|
203
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async initializeFrontendHumanApprovedSplit(input) {
|
|
208
|
+
return this.serialize(input.operationId, async () => {
|
|
209
|
+
const current = await this.get(input.operationId);
|
|
210
|
+
const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
211
|
+
if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision || !current.frontendHumanDecision || !current.frontendExecution || current.frontendSplit)
|
|
212
|
+
return { outcome: "rejected", reason: "record is not an authoritative pending single-task split", projection: fallback };
|
|
213
|
+
const humanProjection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
|
|
214
|
+
if (humanProjection.status !== "blocked-human" || humanProjection.envelope.kind !== "split-materialization-approval" || humanProjection.digest !== input.expectedProjectionDigest)
|
|
215
|
+
return { outcome: "rejected", reason: "pending split envelope conflict", projection: humanProjection };
|
|
216
|
+
const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
|
|
217
|
+
if (receipt.kind !== "split-materialization-approval")
|
|
218
|
+
return { outcome: "rejected", reason: "receipt is not a split approval", projection: humanProjection };
|
|
219
|
+
if (JSON.stringify(current.frontendExecution) !== JSON.stringify(input.execution) || input.execution.kind !== "single-task")
|
|
220
|
+
return { outcome: "rejected", reason: "single-task execution conflict", projection: humanProjection };
|
|
221
|
+
const consumed = consumeFrontendHumanDecision({ projection: humanProjection, receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
|
|
222
|
+
if (consumed.outcome !== "applied")
|
|
223
|
+
return consumed;
|
|
224
|
+
const initialized = initializeFrontendSplitParent({
|
|
225
|
+
execution: input.execution,
|
|
226
|
+
manifest: input.manifest,
|
|
227
|
+
expectedOperationRevision: input.execution.operationRevision,
|
|
228
|
+
successorOperationRevision: consumed.projection.revision,
|
|
229
|
+
envelopeTargetDigest: humanProjection.envelope.targetDigest,
|
|
230
|
+
activeWriterOperationId: input.activeWriterOperationId,
|
|
231
|
+
activeProviderOperationId: input.activeProviderOperationId,
|
|
232
|
+
authorization: { mode: "human", envelope: humanProjection.envelope, receipt },
|
|
233
|
+
});
|
|
234
|
+
if (initialized.outcome !== "applied")
|
|
235
|
+
return { outcome: "rejected", reason: initialized.reason, projection: humanProjection };
|
|
236
|
+
const splitProjection = frontendSplitProjectionSchema.parse({ revision: consumed.projection.revision, execution: initialized.execution, manifest: input.manifest, parent: initialized.parent });
|
|
237
|
+
const { frontendExecution: _singleTask, ...withoutSingleTask } = current;
|
|
238
|
+
const record = { ...withoutSingleTask, revision: consumed.projection.revision, frontendHumanDecision: consumed.projection, frontendSplit: splitProjection, state: "running" };
|
|
239
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
240
|
+
return { outcome: "applied", projection: splitProjection, humanDecision: consumed.projection };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async compareAndSwapFrontendSplit(input) {
|
|
244
|
+
return this.serialize(input.operationId, async () => {
|
|
245
|
+
const current = await this.get(input.operationId);
|
|
246
|
+
if (!current || current.revision === undefined || current.revision !== input.expectedRevision)
|
|
247
|
+
return { outcome: "conflict" };
|
|
248
|
+
const currentProjection = current.frontendSplit;
|
|
249
|
+
if (currentProjection) {
|
|
250
|
+
if (currentProjection.execution.digest !== input.expectedExecutionDigest || currentProjection.parent.digest !== input.expectedParentDigest)
|
|
251
|
+
return { outcome: "conflict" };
|
|
252
|
+
}
|
|
253
|
+
else if (input.expectedExecutionDigest || input.expectedParentDigest)
|
|
254
|
+
return { outcome: "conflict" };
|
|
255
|
+
const next = currentProjection
|
|
256
|
+
? assertFrontendSplitProjectionSuccessor(currentProjection, input.next)
|
|
257
|
+
: frontendSplitProjectionSchema.parse(input.next);
|
|
258
|
+
if (next.revision !== current.revision + 1 || next.parent.operationId !== input.operationId)
|
|
259
|
+
throw new Error("split CAS next revision or operation binding mismatch");
|
|
260
|
+
const state = next.parent.operationState === "success" ? "succeeded" : next.parent.operationTerminal !== undefined ? "failed" : current.state;
|
|
261
|
+
const record = { ...current, revision: next.revision, frontendSplit: next, state };
|
|
262
|
+
await writeSecureJson(operationFile(this.appData, input.operationId), record);
|
|
263
|
+
return { outcome: "applied", projection: next };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async loadChildCloseoutFacts(input) {
|
|
267
|
+
const factsDir = input.closeoutRef.ref;
|
|
268
|
+
const facts = await readJsonIfExists(path.join(factsDir, "frontend-closeout-facts.json"));
|
|
269
|
+
if (facts === undefined)
|
|
270
|
+
return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: [], frontendTestHandoffRef: undefined });
|
|
271
|
+
const parsedFacts = frontendCloseoutFactsSchema.parse(facts);
|
|
272
|
+
if (computeFrontendSplitDigest(parsedFacts) !== input.closeoutRef.digest)
|
|
273
|
+
throw new Error(`child closeout facts digest mismatch: ${input.childId}`);
|
|
274
|
+
const handoff = await readJsonIfExists(path.join(factsDir, "frontend-test-handoff.json"));
|
|
275
|
+
const frontendTestHandoffRef = handoff === undefined ? undefined : computeFrontendSplitDigest(frontendTestHandoffSchema.parse(handoff));
|
|
276
|
+
return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: parsedFacts.residualRisks ?? [], frontendTestHandoffRef });
|
|
277
|
+
}
|
|
278
|
+
async settleFrontendSplitAggregate(input) {
|
|
279
|
+
return this.serialize(input.operationId, async () => {
|
|
280
|
+
const record = await this.get(input.operationId);
|
|
281
|
+
if (!record || record.revision === undefined || record.revision !== input.expectedRevision || !record.frontendSplit)
|
|
282
|
+
return { outcome: "conflict" };
|
|
283
|
+
const authoritative = frontendSplitProjectionSchema.parse(record.frontendSplit);
|
|
284
|
+
if (authoritative.parent.operationId !== input.operationId
|
|
285
|
+
|| authoritative.revision !== input.expectedRevision
|
|
286
|
+
|| authoritative.execution.digest !== input.expectedExecutionDigest
|
|
287
|
+
|| authoritative.parent.digest !== input.expectedParentDigest
|
|
288
|
+
|| authoritative.manifest.digest !== input.expectedManifestDigest
|
|
289
|
+
|| authoritative.candidateBinding?.digest !== input.expectedCandidateBindingDigest
|
|
290
|
+
|| authoritative.integrationBinding?.digest !== input.expectedIntegrationBindingDigest)
|
|
291
|
+
return { outcome: "conflict" };
|
|
292
|
+
if (!authoritative.candidateBinding)
|
|
293
|
+
return { outcome: "conflict" };
|
|
294
|
+
const expectedAggregate = buildFrontendSplitAggregateCloseout(authoritative.parent, authoritative.manifest, authoritative.candidateBinding, authoritative.integrationBinding, input.childCloseoutFacts);
|
|
295
|
+
if (JSON.stringify(expectedAggregate) !== JSON.stringify(input.aggregate))
|
|
296
|
+
return { outcome: "conflict" };
|
|
297
|
+
const transaction = await input.prepareAggregate({
|
|
298
|
+
operationId: authoritative.parent.operationId,
|
|
299
|
+
aggregate: expectedAggregate,
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
const committedRef = frontendCommittedFactRefSchema.parse(transaction.committedRef);
|
|
303
|
+
const finalized = finalizeFrontendSplitAggregate(authoritative.parent, expectedAggregate, committedRef);
|
|
304
|
+
const next = assertFrontendSplitProjectionSuccessor(authoritative, { ...authoritative, revision: authoritative.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: { value: expectedAggregate, committedRef } });
|
|
305
|
+
const nextRecord = { ...record, revision: next.revision, frontendSplit: next, state: "succeeded" };
|
|
306
|
+
await transaction.commit({
|
|
307
|
+
parentRecordPath: operationFile(this.appData, record.operationId),
|
|
308
|
+
parentRecord: nextRecord,
|
|
309
|
+
});
|
|
310
|
+
return { outcome: "applied", projection: next };
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
await transaction.rollback();
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async initializeFrontendSplit(expectedRevision, projectionValue) {
|
|
319
|
+
const projection = frontendSplitProjectionSchema.parse(projectionValue);
|
|
320
|
+
return this.serialize(projection.parent.operationId, async () => {
|
|
321
|
+
const current = await this.get(projection.parent.operationId);
|
|
322
|
+
if (!current || current.revision === undefined || current.revision !== expectedRevision || current.frontendSplit !== undefined || projection.revision !== expectedRevision + 1)
|
|
323
|
+
return { outcome: "conflict" };
|
|
324
|
+
const record = { ...current, revision: projection.revision, frontendSplit: projection };
|
|
325
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
326
|
+
return { outcome: "applied", projection };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
109
329
|
async list() {
|
|
110
330
|
const files = await listJsonFiles(this.appData.operations);
|
|
111
331
|
const ops = [];
|