@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -206
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -7
- package/dist/cli/program.js +23 -6
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +266 -3792
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/pi-sdk-executor.js +3 -0
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/console/operation-store.js +197 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +12 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +14 -7
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +80 -35
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +2 -2
- package/dist/worker/console/chat/pi-runtime.js +3 -2
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +13 -4
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +1 -1
- package/dist/worker/console/chat/turn-process.js +28 -4
- package/dist/worker/console/chat/user-questions.js +1 -1
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -6
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +107 -168
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +169 -0
- package/dist/worker/console/pi-plugins.js +60 -1
- package/dist/worker/console/routes.js +637 -8
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +6 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/workspace-context.js +28 -112
- package/dist/worker/console/workspace-registry.js +2 -2
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/static/api.js +69 -5
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1037 -126
- package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
- package/dist/worker/observe/static/views/session-timeline.js +121 -25
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +99 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +45 -3
- package/dist/workflows/dag/budget-enforcement.js +10 -0
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +740 -718
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +136 -759
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +30 -38
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +557 -11
- package/dist/workflows/dag/rerun-run.js +601 -38
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +109 -214
- package/dist/workflows/dag/runner.js +142 -430
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/skill-snapshot.js +17 -0
- package/dist/workflows/dag/types.js +195 -246
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +33 -1
- package/docs/templates/backend-test-dag.json +18 -18
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/docs/templates/product-line/task.yaml +1 -1
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +6 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/operation-store.js +0 -389
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { access, open, readFile, stat, } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
class PiReadBudgetSoftLimitError extends Error {
|
|
4
|
-
}
|
|
5
|
-
class PiReadAccessDeniedError extends Error {
|
|
6
|
-
}
|
|
7
|
-
class PiReadBudgetGuard {
|
|
8
|
-
budget;
|
|
9
|
-
files = new Set();
|
|
10
|
-
violations = [];
|
|
11
|
-
bytes = 0;
|
|
12
|
-
operations = 0;
|
|
13
|
-
startedAt = Date.now();
|
|
14
|
-
allowedReadPaths;
|
|
15
|
-
allowedReadRoots;
|
|
16
|
-
constructor(budget, readAccess) {
|
|
17
|
-
this.budget = budget;
|
|
18
|
-
this.allowedReadPaths = readAccess?.allowedReadPaths
|
|
19
|
-
? new Set(readAccess.allowedReadPaths.map((candidate) => path.resolve(candidate)))
|
|
20
|
-
: undefined;
|
|
21
|
-
this.allowedReadRoots = (readAccess?.allowedReadRoots ?? []).map((candidate) => path.resolve(candidate));
|
|
22
|
-
}
|
|
23
|
-
issues() {
|
|
24
|
-
return [...this.violations];
|
|
25
|
-
}
|
|
26
|
-
report(message) {
|
|
27
|
-
if (!this.violations.includes(message))
|
|
28
|
-
this.violations.push(message);
|
|
29
|
-
}
|
|
30
|
-
exhaust(message) {
|
|
31
|
-
if (this.budget.onExhaustion === "return-guidance") {
|
|
32
|
-
throw new PiReadBudgetSoftLimitError(message);
|
|
33
|
-
}
|
|
34
|
-
this.report(message);
|
|
35
|
-
throw new Error(message);
|
|
36
|
-
}
|
|
37
|
-
assertElapsed() {
|
|
38
|
-
if (this.budget.maxMs === undefined)
|
|
39
|
-
return;
|
|
40
|
-
const elapsedMs = Date.now() - this.startedAt;
|
|
41
|
-
if (elapsedMs > this.budget.maxMs) {
|
|
42
|
-
const message = `read budget exhausted: ${elapsedMs}ms (budget ${this.budget.maxMs}ms)`;
|
|
43
|
-
this.exhaust(message);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
beforeToolOperation() {
|
|
47
|
-
this.assertElapsed();
|
|
48
|
-
if (this.budget.maxOperations === undefined)
|
|
49
|
-
return true;
|
|
50
|
-
if (this.operations >= this.budget.maxOperations) {
|
|
51
|
-
const message = `read budget exhausted: operation ${this.operations + 1} would exceed ${this.budget.maxOperations} read-only tool calls`;
|
|
52
|
-
if (this.budget.onExhaustion === "return-guidance")
|
|
53
|
-
return false;
|
|
54
|
-
this.exhaust(message);
|
|
55
|
-
}
|
|
56
|
-
this.operations += 1;
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
operationLimitGuidance() {
|
|
60
|
-
return {
|
|
61
|
-
content: [
|
|
62
|
-
{
|
|
63
|
-
type: "text",
|
|
64
|
-
text: "[read limit reached: no repository data was read. Do not call discovery tools again; use injected facts, commit remaining record_* facts, then call finalize_plan exactly once.]",
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
readAccessGuidance() {
|
|
70
|
-
return {
|
|
71
|
-
content: [
|
|
72
|
-
{
|
|
73
|
-
type: "text",
|
|
74
|
-
text: "[read denied: this node may read only named upstream artifacts or Scout-confirmed paths. Do not explore a new repository path; use committed facts or record an evidence gap, then finish the required terminal protocol.]",
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
assertReadablePath(absolutePath) {
|
|
80
|
-
if (!this.allowedReadPaths)
|
|
81
|
-
return;
|
|
82
|
-
const resolved = path.resolve(absolutePath);
|
|
83
|
-
if (this.allowedReadPaths.has(resolved))
|
|
84
|
-
return;
|
|
85
|
-
if (this.allowedReadRoots.some((root) => resolved === root || resolved.startsWith(`${root}${path.sep}`)))
|
|
86
|
-
return;
|
|
87
|
-
throw new PiReadAccessDeniedError(`read access denied: ${resolved}`);
|
|
88
|
-
}
|
|
89
|
-
reserveFile(absolutePath) {
|
|
90
|
-
this.assertElapsed();
|
|
91
|
-
if (this.files.has(absolutePath))
|
|
92
|
-
return;
|
|
93
|
-
if (this.files.size >= this.budget.maxFiles) {
|
|
94
|
-
const message = `read budget exhausted: file ${absolutePath} would exceed ${this.budget.maxFiles} files`;
|
|
95
|
-
this.exhaust(message);
|
|
96
|
-
}
|
|
97
|
-
this.files.add(absolutePath);
|
|
98
|
-
}
|
|
99
|
-
reserveBytes(bytes) {
|
|
100
|
-
this.assertElapsed();
|
|
101
|
-
const remaining = Math.max(0, this.budget.maxBytes - this.bytes);
|
|
102
|
-
if (remaining === 0) {
|
|
103
|
-
const message = `read budget exhausted: ${this.bytes} bytes (budget ${this.budget.maxBytes})`;
|
|
104
|
-
this.exhaust(message);
|
|
105
|
-
}
|
|
106
|
-
const granted = Math.min(bytes, remaining);
|
|
107
|
-
if (granted < bytes && this.budget.onExhaustion === "return-guidance") {
|
|
108
|
-
this.exhaust(`read budget exhausted: ${bytes} requested bytes would exceed ${this.budget.maxBytes} bytes`);
|
|
109
|
-
}
|
|
110
|
-
this.bytes += granted;
|
|
111
|
-
if (granted < bytes) {
|
|
112
|
-
this.report(`read budget truncated: ${bytes} requested bytes at ${this.bytes}/${this.budget.maxBytes} bytes`);
|
|
113
|
-
}
|
|
114
|
-
return granted;
|
|
115
|
-
}
|
|
116
|
-
async readFile(absolutePath) {
|
|
117
|
-
this.assertReadablePath(absolutePath);
|
|
118
|
-
this.reserveFile(absolutePath);
|
|
119
|
-
const metadata = await stat(absolutePath);
|
|
120
|
-
const granted = this.reserveBytes(metadata.size);
|
|
121
|
-
if (granted === metadata.size)
|
|
122
|
-
return await readFile(absolutePath);
|
|
123
|
-
const handle = await open(absolutePath, "r");
|
|
124
|
-
try {
|
|
125
|
-
const buffer = Buffer.allocUnsafe(granted);
|
|
126
|
-
const { bytesRead } = await handle.read(buffer, 0, granted, 0);
|
|
127
|
-
return buffer.subarray(0, bytesRead);
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
await handle.close();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async access(absolutePath) {
|
|
134
|
-
this.assertReadablePath(absolutePath);
|
|
135
|
-
this.reserveFile(absolutePath);
|
|
136
|
-
await access(absolutePath);
|
|
137
|
-
}
|
|
138
|
-
limitToolResult(result) {
|
|
139
|
-
if (!result || typeof result !== "object" || !Array.isArray(result.content)) {
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
const content = result.content.map((item) => {
|
|
143
|
-
if (!item || item.type !== "text" || typeof item.text !== "string") {
|
|
144
|
-
this.report("read budget truncated: non-text tool result omitted");
|
|
145
|
-
return {
|
|
146
|
-
type: "text",
|
|
147
|
-
text: "[read budget reached: non-text result omitted]",
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
const granted = this.reserveBytes(Buffer.byteLength(item.text));
|
|
151
|
-
return {
|
|
152
|
-
...item,
|
|
153
|
-
text: truncateUtf8(item.text, granted),
|
|
154
|
-
};
|
|
155
|
-
});
|
|
156
|
-
return { ...result, content };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function truncateUtf8(text, maxBytes) {
|
|
160
|
-
if (Buffer.byteLength(text) <= maxBytes)
|
|
161
|
-
return text;
|
|
162
|
-
const marker = "\n[read result truncated by node budget]";
|
|
163
|
-
const markerBytes = Buffer.byteLength(marker);
|
|
164
|
-
if (maxBytes <= markerBytes)
|
|
165
|
-
return truncateUtf8Prefix(marker, maxBytes);
|
|
166
|
-
const contentBudget = maxBytes - markerBytes;
|
|
167
|
-
return `${truncateUtf8Prefix(text, contentBudget)}${marker}`;
|
|
168
|
-
}
|
|
169
|
-
function truncateUtf8Prefix(text, maxBytes) {
|
|
170
|
-
let result = "";
|
|
171
|
-
let bytes = 0;
|
|
172
|
-
for (const codePoint of text) {
|
|
173
|
-
const size = Buffer.byteLength(codePoint);
|
|
174
|
-
if (bytes + size > maxBytes)
|
|
175
|
-
break;
|
|
176
|
-
result += codePoint;
|
|
177
|
-
bytes += size;
|
|
178
|
-
}
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
function wrapTool(definition, guard, consumeResult) {
|
|
182
|
-
return {
|
|
183
|
-
...definition,
|
|
184
|
-
async execute(...args) {
|
|
185
|
-
try {
|
|
186
|
-
if (!guard.beforeToolOperation())
|
|
187
|
-
return guard.operationLimitGuidance();
|
|
188
|
-
const result = await definition.execute(...args);
|
|
189
|
-
return consumeResult ? guard.limitToolResult(result) : result;
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
if (error instanceof PiReadBudgetSoftLimitError) {
|
|
193
|
-
return guard.operationLimitGuidance();
|
|
194
|
-
}
|
|
195
|
-
if (error instanceof PiReadAccessDeniedError) {
|
|
196
|
-
return guard.readAccessGuidance();
|
|
197
|
-
}
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Override Pi's built-in read-only tools with SDK definitions that meter the
|
|
205
|
-
* content before it reaches the model. The read tool caps filesystem bytes at
|
|
206
|
-
* the operation boundary; grep/find/ls have their returned payload clipped
|
|
207
|
-
* before the agent receives it. This is SDK-only by design: the CLI backend
|
|
208
|
-
* cannot inject same-name built-in overrides safely.
|
|
209
|
-
*/
|
|
210
|
-
export async function createPiReadBudgetCustomTools(input) {
|
|
211
|
-
const sdk = (await import("@earendil-works/pi-coding-agent"));
|
|
212
|
-
const createReadToolDefinition = sdk.createReadToolDefinition;
|
|
213
|
-
const createGrepToolDefinition = sdk.createGrepToolDefinition;
|
|
214
|
-
const createFindToolDefinition = sdk.createFindToolDefinition;
|
|
215
|
-
const createLsToolDefinition = sdk.createLsToolDefinition;
|
|
216
|
-
if (typeof createReadToolDefinition !== "function" ||
|
|
217
|
-
typeof createGrepToolDefinition !== "function" ||
|
|
218
|
-
typeof createFindToolDefinition !== "function" ||
|
|
219
|
-
typeof createLsToolDefinition !== "function") {
|
|
220
|
-
throw new Error("pi read budget policy unavailable: SDK read/grep/find/ls tool factories missing");
|
|
221
|
-
}
|
|
222
|
-
const guard = new PiReadBudgetGuard(input.budget, input.readAccess);
|
|
223
|
-
const read = wrapTool(createReadToolDefinition(input.repoRoot, {
|
|
224
|
-
operations: {
|
|
225
|
-
readFile: (absolutePath) => guard.readFile(absolutePath),
|
|
226
|
-
access: (absolutePath) => guard.access(absolutePath),
|
|
227
|
-
},
|
|
228
|
-
}), guard, false);
|
|
229
|
-
const grep = wrapTool(createGrepToolDefinition(input.repoRoot), guard, true);
|
|
230
|
-
const find = wrapTool(createFindToolDefinition(input.repoRoot), guard, true);
|
|
231
|
-
const ls = wrapTool(createLsToolDefinition(input.repoRoot), guard, true);
|
|
232
|
-
if (read.name !== "read" ||
|
|
233
|
-
grep.name !== "grep" ||
|
|
234
|
-
find.name !== "find" ||
|
|
235
|
-
ls.name !== "ls") {
|
|
236
|
-
throw new Error("pi read budget policy unavailable: SDK tool name mismatch");
|
|
237
|
-
}
|
|
238
|
-
return { customTools: [read, grep, find, ls], issues: () => guard.issues() };
|
|
239
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
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";
|
|
7
|
-
import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
|
|
8
|
-
export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
|
|
9
|
-
function operationFile(appData, operationId) {
|
|
10
|
-
return path.join(appData.operations, `${operationId}.json`);
|
|
11
|
-
}
|
|
12
|
-
function requestIndexFile(appData, clientRequestId) {
|
|
13
|
-
const safe = clientRequestId.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 180);
|
|
14
|
-
return path.join(appData.operations, `req-${safe}.json`);
|
|
15
|
-
}
|
|
16
|
-
export function computeActionPayloadHash(action, actionParams) {
|
|
17
|
-
return sha256Json({ action, actionParams });
|
|
18
|
-
}
|
|
19
|
-
export function newOperationId() {
|
|
20
|
-
return `op_${randomBytes(12).toString("hex")}`;
|
|
21
|
-
}
|
|
22
|
-
const sharedWriteChains = new Map();
|
|
23
|
-
export class OperationStore {
|
|
24
|
-
appData;
|
|
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;
|
|
28
|
-
constructor(appData) {
|
|
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;
|
|
37
|
-
}
|
|
38
|
-
serialize(key, op) {
|
|
39
|
-
const prev = this.writeChains.get(key) ?? Promise.resolve();
|
|
40
|
-
const next = prev.then(op, op);
|
|
41
|
-
this.writeChains.set(key, next.then(() => undefined, () => undefined));
|
|
42
|
-
return next;
|
|
43
|
-
}
|
|
44
|
-
async create(input) {
|
|
45
|
-
const clientRequestId = input.clientRequestId.trim();
|
|
46
|
-
if (!clientRequestId) {
|
|
47
|
-
throw new Error("clientRequestId is required for mutation operations");
|
|
48
|
-
}
|
|
49
|
-
const payloadHash = computeActionPayloadHash(input.action, input.actionParams);
|
|
50
|
-
return this.serialize(`req:${clientRequestId}`, async () => {
|
|
51
|
-
const indexPath = requestIndexFile(this.appData, clientRequestId);
|
|
52
|
-
const existingIndex = await readJsonIfExists(indexPath);
|
|
53
|
-
if (existingIndex) {
|
|
54
|
-
const existing = await this.get(existingIndex.operationId);
|
|
55
|
-
if (existing) {
|
|
56
|
-
if (existing.payloadHash === payloadHash) {
|
|
57
|
-
return { kind: "idempotent", operation: existing };
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
kind: "conflict",
|
|
61
|
-
code: "REQUEST_ID_REUSE_CONFLICT",
|
|
62
|
-
message: `clientRequestId ${clientRequestId} was reused with a different payload`,
|
|
63
|
-
existing,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const operationId = newOperationId();
|
|
68
|
-
const now = new Date().toISOString();
|
|
69
|
-
const operation = {
|
|
70
|
-
schemaVersion: 1,
|
|
71
|
-
operationId,
|
|
72
|
-
clientRequestId,
|
|
73
|
-
action: input.action,
|
|
74
|
-
actionParams: input.actionParams,
|
|
75
|
-
payloadHash,
|
|
76
|
-
state: "queued",
|
|
77
|
-
createdAt: now,
|
|
78
|
-
revision: 1,
|
|
79
|
-
taskId: input.taskId,
|
|
80
|
-
cliArgs: input.cliArgs,
|
|
81
|
-
...(input.externalCommand
|
|
82
|
-
? { externalCommand: input.externalCommand }
|
|
83
|
-
: {}),
|
|
84
|
-
};
|
|
85
|
-
await writeSecureJson(operationFile(this.appData, operationId), operation);
|
|
86
|
-
await writeSecureJson(indexPath, { operationId, payloadHash });
|
|
87
|
-
return { kind: "created", operation };
|
|
88
|
-
});
|
|
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
|
-
}
|
|
127
|
-
async get(operationId) {
|
|
128
|
-
return readJsonIfExists(operationFile(this.appData, operationId));
|
|
129
|
-
}
|
|
130
|
-
async save(operation) {
|
|
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
|
-
}
|
|
145
|
-
await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
|
|
146
|
-
return operation;
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
async update(operationId, patch) {
|
|
150
|
-
return this.serialize(operationId, async () => {
|
|
151
|
-
const current = await this.get(operationId);
|
|
152
|
-
if (!current) {
|
|
153
|
-
throw new Error(`operation not found: ${operationId}`);
|
|
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");
|
|
159
|
-
const next = {
|
|
160
|
-
...current,
|
|
161
|
-
...patch,
|
|
162
|
-
operationId: current.operationId,
|
|
163
|
-
schemaVersion: 1,
|
|
164
|
-
clientRequestId: current.clientRequestId,
|
|
165
|
-
payloadHash: current.payloadHash,
|
|
166
|
-
action: current.action,
|
|
167
|
-
createdAt: current.createdAt,
|
|
168
|
-
};
|
|
169
|
-
await writeSecureJson(operationFile(this.appData, operationId), next);
|
|
170
|
-
return next;
|
|
171
|
-
});
|
|
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
|
-
}
|
|
329
|
-
async list() {
|
|
330
|
-
const files = await listJsonFiles(this.appData.operations);
|
|
331
|
-
const ops = [];
|
|
332
|
-
for (const file of files) {
|
|
333
|
-
const base = path.basename(file);
|
|
334
|
-
if (base.startsWith("req-"))
|
|
335
|
-
continue;
|
|
336
|
-
const rec = await readJsonIfExists(file);
|
|
337
|
-
if (rec?.operationId)
|
|
338
|
-
ops.push(rec);
|
|
339
|
-
}
|
|
340
|
-
ops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
341
|
-
return ops;
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* On Console boot: any non-terminal operation that cannot be proven still
|
|
345
|
-
* running becomes needs-reconcile (never invent succeeded/failed).
|
|
346
|
-
*/
|
|
347
|
-
async reconcileOnBoot(options) {
|
|
348
|
-
const isAlive = options?.isProcessAlive ??
|
|
349
|
-
((pid) => {
|
|
350
|
-
try {
|
|
351
|
-
process.kill(pid, 0);
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
const changed = [];
|
|
359
|
-
for (const op of await this.list()) {
|
|
360
|
-
if (TERMINAL_OPERATION_STATES.has(op.state))
|
|
361
|
-
continue;
|
|
362
|
-
if (op.state === "needs-reconcile")
|
|
363
|
-
continue;
|
|
364
|
-
let next;
|
|
365
|
-
if (op.state === "running" &&
|
|
366
|
-
typeof op.pid === "number" &&
|
|
367
|
-
isAlive(op.pid, op.processStartIdentity)) {
|
|
368
|
-
// still running — leave as-is
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
// queued/starting/running without proven process → needs-reconcile
|
|
372
|
-
next = await this.update(op.operationId, {
|
|
373
|
-
state: "needs-reconcile",
|
|
374
|
-
errorCode: "OPERATION_NEEDS_RECONCILE",
|
|
375
|
-
errorMessage: "Console restart or lost process evidence; operation needs reconcile (not cancelled)",
|
|
376
|
-
});
|
|
377
|
-
changed.push(next);
|
|
378
|
-
}
|
|
379
|
-
return changed;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
export function isTerminalOperationState(state) {
|
|
383
|
-
return TERMINAL_OPERATION_STATES.has(state);
|
|
384
|
-
}
|
|
385
|
-
export function clipPreview(text, max = 4096) {
|
|
386
|
-
if (text.length <= max)
|
|
387
|
-
return text;
|
|
388
|
-
return `${text.slice(0, max)}\n…[truncated ${text.length - max} chars]`;
|
|
389
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ag as o,ah as n}from"./mermaid.core-Cxg8kX8V.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|