@rudderhq/server 0.7.1 → 0.7.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/dist/agent-runtimes/codex-models.d.ts +11 -0
- package/dist/agent-runtimes/codex-models.d.ts.map +1 -1
- package/dist/agent-runtimes/codex-models.js +155 -2
- package/dist/agent-runtimes/codex-models.js.map +1 -1
- package/dist/agent-runtimes/cursor-models.d.ts.map +1 -1
- package/dist/agent-runtimes/cursor-models.js +31 -9
- package/dist/agent-runtimes/cursor-models.js.map +1 -1
- package/dist/agent-runtimes/index.d.ts +1 -1
- package/dist/agent-runtimes/index.d.ts.map +1 -1
- package/dist/agent-runtimes/index.js +1 -1
- package/dist/agent-runtimes/index.js.map +1 -1
- package/dist/agent-runtimes/registry.d.ts +11 -4
- package/dist/agent-runtimes/registry.d.ts.map +1 -1
- package/dist/agent-runtimes/registry.js +54 -3
- package/dist/agent-runtimes/registry.js.map +1 -1
- package/dist/bundled-plugins/plugin-linear/dist/worker.js +287 -18
- package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -30
- package/dist/index.js.map +1 -1
- package/dist/redaction.d.ts +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +13 -0
- package/dist/redaction.js.map +1 -1
- package/dist/routes/access.helpers.d.ts +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +20 -6
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/agents.management-routes.d.ts.map +1 -1
- package/dist/routes/agents.management-routes.js +13 -10
- package/dist/routes/agents.management-routes.js.map +1 -1
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +77 -1
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/chats.d.ts.map +1 -1
- package/dist/routes/chats.js +1 -0
- package/dist/routes/chats.js.map +1 -1
- package/dist/routes/chats.stream-routes.d.ts.map +1 -1
- package/dist/routes/chats.stream-routes.js +4 -0
- package/dist/routes/chats.stream-routes.js.map +1 -1
- package/dist/routes/goals.d.ts.map +1 -1
- package/dist/routes/goals.js +349 -23
- package/dist/routes/goals.js.map +1 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +1 -0
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/managed-mcp-connections.d.ts.map +1 -1
- package/dist/routes/managed-mcp-connections.js +46 -7
- package/dist/routes/managed-mcp-connections.js.map +1 -1
- package/dist/routes/product-analytics-collector-report.d.ts +29 -0
- package/dist/routes/product-analytics-collector-report.d.ts.map +1 -1
- package/dist/routes/product-analytics-collector-report.js +57 -1
- package/dist/routes/product-analytics-collector-report.js.map +1 -1
- package/dist/routes/product-analytics-collector-report.test.d.ts +2 -0
- package/dist/routes/product-analytics-collector-report.test.d.ts.map +1 -0
- package/dist/routes/product-analytics-collector-report.test.js +50 -0
- package/dist/routes/product-analytics-collector-report.test.js.map +1 -0
- package/dist/services/automation-chat-output.d.ts +1 -1
- package/dist/services/automation-chat-output.d.ts.map +1 -1
- package/dist/services/automation-chat-output.js +24 -14
- package/dist/services/automation-chat-output.js.map +1 -1
- package/dist/services/chat-annotation-copy-lineage.d.ts +1 -1
- package/dist/services/chat-assistant.d.ts.map +1 -1
- package/dist/services/chat-assistant.helpers.d.ts +3 -3
- package/dist/services/chat-assistant.helpers.d.ts.map +1 -1
- package/dist/services/chat-assistant.helpers.js.map +1 -1
- package/dist/services/chat-assistant.js +34 -11
- package/dist/services/chat-assistant.js.map +1 -1
- package/dist/services/chat-assistant.runtime-overrides.d.ts +2 -1
- package/dist/services/chat-assistant.runtime-overrides.d.ts.map +1 -1
- package/dist/services/chat-assistant.runtime-overrides.js +47 -18
- package/dist/services/chat-assistant.runtime-overrides.js.map +1 -1
- package/dist/services/chat-assistant.runtime-overrides.test.d.ts +2 -0
- package/dist/services/chat-assistant.runtime-overrides.test.d.ts.map +1 -0
- package/dist/services/chat-assistant.runtime-overrides.test.js +37 -0
- package/dist/services/chat-assistant.runtime-overrides.test.js.map +1 -0
- package/dist/services/chat-generation-protocol.d.ts.map +1 -1
- package/dist/services/chat-generation-protocol.js +1 -12
- package/dist/services/chat-generation-protocol.js.map +1 -1
- package/dist/services/chat-generation-protocol.test.js +14 -23
- package/dist/services/chat-generation-protocol.test.js.map +1 -1
- package/dist/services/chat-generation-provenance.d.ts.map +1 -1
- package/dist/services/chat-generation-provenance.js +4 -2
- package/dist/services/chat-generation-provenance.js.map +1 -1
- package/dist/services/chat-queued-message-materialization.d.ts.map +1 -1
- package/dist/services/chat-queued-message-materialization.js +12 -2
- package/dist/services/chat-queued-message-materialization.js.map +1 -1
- package/dist/services/chat-transcript-persistence.d.ts +49 -0
- package/dist/services/chat-transcript-persistence.d.ts.map +1 -0
- package/dist/services/chat-transcript-persistence.js +238 -0
- package/dist/services/chat-transcript-persistence.js.map +1 -0
- package/dist/services/chat-transcript-persistence.test.d.ts +2 -0
- package/dist/services/chat-transcript-persistence.test.d.ts.map +1 -0
- package/dist/services/chat-transcript-persistence.test.js +23 -0
- package/dist/services/chat-transcript-persistence.test.js.map +1 -0
- package/dist/services/chat-work-manifest.d.ts.map +1 -1
- package/dist/services/chat-work-manifest.js +36 -10
- package/dist/services/chat-work-manifest.js.map +1 -1
- package/dist/services/chats.annotation-persistence.d.ts.map +1 -1
- package/dist/services/chats.annotation-persistence.js +25 -6
- package/dist/services/chats.annotation-persistence.js.map +1 -1
- package/dist/services/chats.create.d.ts.map +1 -1
- package/dist/services/chats.create.js +49 -4
- package/dist/services/chats.create.js.map +1 -1
- package/dist/services/chats.d.ts +8 -8
- package/dist/services/chats.d.ts.map +1 -1
- package/dist/services/chats.inline-visual-persistence.d.ts +2 -1
- package/dist/services/chats.inline-visual-persistence.d.ts.map +1 -1
- package/dist/services/chats.inline-visual-persistence.js +19 -2
- package/dist/services/chats.inline-visual-persistence.js.map +1 -1
- package/dist/services/chats.js +177 -95
- package/dist/services/chats.js.map +1 -1
- package/dist/services/finance.d.ts +1 -1
- package/dist/services/goals.d.ts +2041 -126
- package/dist/services/goals.d.ts.map +1 -1
- package/dist/services/goals.js +1533 -176
- package/dist/services/goals.js.map +1 -1
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +2 -0
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +35 -16
- package/dist/services/issues.js.map +1 -1
- package/dist/services/mcp/managed-bindings.d.ts.map +1 -1
- package/dist/services/mcp/managed-bindings.js +11 -4
- package/dist/services/mcp/managed-bindings.js.map +1 -1
- package/dist/services/mcp/managed-connections.d.ts +19 -1
- package/dist/services/mcp/managed-connections.d.ts.map +1 -1
- package/dist/services/mcp/managed-connections.js +211 -37
- package/dist/services/mcp/managed-connections.js.map +1 -1
- package/dist/services/mcp/managed-runtime.js +1 -1
- package/dist/services/mcp/managed-runtime.js.map +1 -1
- package/dist/services/mcp/oauth.d.ts.map +1 -1
- package/dist/services/mcp/oauth.js +3 -0
- package/dist/services/mcp/oauth.js.map +1 -1
- package/dist/services/mcp/provider-registry.d.ts +24 -1
- package/dist/services/mcp/provider-registry.d.ts.map +1 -1
- package/dist/services/mcp/provider-registry.js +26 -0
- package/dist/services/mcp/provider-registry.js.map +1 -1
- package/dist/services/mcp/provider-registry.test.js +26 -0
- package/dist/services/mcp/provider-registry.test.js.map +1 -1
- package/dist/services/messenger-saved-views.d.ts.map +1 -1
- package/dist/services/messenger-saved-views.js +1 -0
- package/dist/services/messenger-saved-views.js.map +1 -1
- package/dist/services/messenger.d.ts.map +1 -1
- package/dist/services/messenger.js +16 -2
- package/dist/services/messenger.js.map +1 -1
- package/dist/services/plugin-host-services.d.ts.map +1 -1
- package/dist/services/plugin-host-services.js +5 -7
- package/dist/services/plugin-host-services.js.map +1 -1
- package/dist/services/product-analytics.d.ts +20 -4
- package/dist/services/product-analytics.d.ts.map +1 -1
- package/dist/services/product-analytics.js +47 -0
- package/dist/services/product-analytics.js.map +1 -1
- package/dist/services/product-analytics.test.js +63 -1
- package/dist/services/product-analytics.test.js.map +1 -1
- package/dist/services/run-events.d.ts +1 -1
- package/dist/services/runtime-kernel/heartbeat.d.ts +4 -0
- package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.execute.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.execute.js +51 -3
- package/dist/services/runtime-kernel/heartbeat.execute.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.js +187 -9
- package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.d.ts +3 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.js +2 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.js.map +1 -1
- package/dist/services/side-chats.d.ts.map +1 -1
- package/dist/services/side-chats.js +12 -0
- package/dist/services/side-chats.js.map +1 -1
- package/dist/services/workspace-runtime.services.d.ts +1 -1
- package/package.json +16 -14
- package/resources/bundled-skills/browser/SKILL.md +11 -4
- package/resources/bundled-skills/browser/agents/openai.yaml +2 -2
- package/resources/bundled-skills/browser/evals/trigger-evals.json +22 -0
- package/skills/browser/SKILL.md +11 -4
- package/skills/browser/agents/openai.yaml +2 -2
- package/skills/browser/evals/trigger-evals.json +22 -0
- package/ui-dist/assets/{LibraryLiveSurface-CZED7oKj.js → LibraryLiveSurface-cHCJmmsl.js} +1 -1
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-BoPyIoAq.js → abnfDiagram-VRR7QNED-BORJvkew.js} +1 -1
- package/ui-dist/assets/{arc-DTYs4RCH.js → arc-DPUzhx87.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BGfp2S_3.js → architectureDiagram-ZJ3FMSHR-Bz_hY8LM.js} +1 -1
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-aW0EGIUf.js → blockDiagram-677ZJIJ3-Bq1X3YkP.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV--1itKgA2.js → c4Diagram-LMCZKHZV-BawEM9Pc.js} +1 -1
- package/ui-dist/assets/channel-C08AyYJr.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-ZIqIozPQ.js → chunk-2Q5K7J3B-D0D5ZOaA.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-DiwBEZVQ.js → chunk-32BRIVSS-T90P7lug.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-CLOrTmL8.js → chunk-5VM5RSS4-jcAERTgh.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-D6xZzF47.js → chunk-EX3LRPZG-t9-JXwnR.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-B9tYvTZl.js → chunk-JWPE2WC7-hqgD1saC.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-D_qo-SzM.js → chunk-MOJQB5TN-BsSUoYzd.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-ChPHGc2P.js → chunk-RYQCIY6F-YcdDh2Zn.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-Dk6GbZYp.js → chunk-V7JOEXUC-ChFyH9Su.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-B6AREC1T.js → chunk-VR4S4FIN-BjVK8IyX.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-CUyO-JUr.js → chunk-XXDRQBXY-D-TvO9vT.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-BIXq6fec.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-BIXq6fec.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-BCxIInLZ.js → cose-bilkent-JH36ORCC-DBwhASJG.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-DqsnpnHA.js → cynefin-VYW2F7L2-SlCRmwl2.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-BD7QQNLW.js → cynefinDiagram-TSTJHNR4-DImV6N_p.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-DaE160f7.js → dagre-VKFMJZFB-FH4Wv8Zn.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-DZ0c59y8.js → diagram-FQU43EPY-CrZvQjId.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-C_413g5E.js → diagram-G47NLZAW-B7IGl_T3.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-D_o9pXB-.js → diagram-NH7WQ7WH-CB4i7H1a.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-BI2xMqiW.js → diagram-OA4YK3LP-DNyWPYY9.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-hhM8R_14.js → diagram-WEI45ONY-BAhxbhpi.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-DfFgEtmG.js → ebnfDiagram-CCIWWBDH-ClXtQ9qt.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-CCw4ysHK.js → erDiagram-Q63AITRT-DGXiMYfn.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-CzC1SMmo.js → flowDiagram-23GEKE2U-CdZ-02zc.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-CbPJIrIw.js → ganttDiagram-NO4QXBWP-TxhzNpta.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-jCxJ66_C.js → gitGraphDiagram-IHSO6WYX-D6Q_w6wR.js} +1 -1
- package/ui-dist/assets/{graph-BfNZ9Jzj.js → graph-CxNmbpl9.js} +1 -1
- package/ui-dist/assets/{index-D00XdcOq.js → index-AkhgKOxC.js} +1 -1
- package/ui-dist/assets/{index-DiMDROQY.js → index-BA9tMBhO.js} +1 -1
- package/ui-dist/assets/{index-DmjAbzHq.js → index-BQ87dGIq.js} +1 -1
- package/ui-dist/assets/index-BmXb0yC1.css +1 -0
- package/ui-dist/assets/{index-B8q4cbOz.js → index-BoTz5S-X.js} +1 -1
- package/ui-dist/assets/{index-D_K-9VDs.js → index-BujwWE3j.js} +1 -1
- package/ui-dist/assets/{index-Chop-Dpa.js → index-Bxz75ICF.js} +1 -1
- package/ui-dist/assets/{index-BKn4zRjf.js → index-C12QC1Ex.js} +1 -1
- package/ui-dist/assets/{index-BjxRqzZK.js → index-CW21qWFm.js} +1 -1
- package/ui-dist/assets/{index-ByUFNj0O.js → index-CXnqg1Oq.js} +1 -1
- package/ui-dist/assets/{index-B3h9Vdxx.js → index-C_nwI4Yj.js} +1 -1
- package/ui-dist/assets/{index-Ck-ZNQeD.js → index-Cx6oIZPm.js} +572 -561
- package/ui-dist/assets/{index-BE1vSI6a.js → index-DU2LI7f9.js} +1 -1
- package/ui-dist/assets/{index-SkHIC7Ma.js → index-Do78GymY.js} +1 -1
- package/ui-dist/assets/{index-Bwo-C8W5.js → index-Rau2cGm0.js} +1 -1
- package/ui-dist/assets/{index-BULll2e0.js → index-kbWUQ1um.js} +1 -1
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-BkDimGw3.js → infoDiagram-FWYZ7A6U-CnJd4-Jq.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-BAyUfn6x.js → ishikawaDiagram-FXEZZL3T-Bq-qc3pD.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-DlbpTZHG.js → journeyDiagram-5HDEW3XC-NSmpUGUA.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-Cd67S14K.js → kanban-definition-HUTT4EX6-D7pgWtfA.js} +1 -1
- package/ui-dist/assets/{layout-Dc4YEiCC.js → layout-BRuo6LWe.js} +1 -1
- package/ui-dist/assets/{linear-BhRsbD_5.js → linear-CkaWI9Nd.js} +1 -1
- package/ui-dist/assets/{map-CEWybj6z.js → map-CgA6tTpZ.js} +1 -1
- package/ui-dist/assets/{mermaid.core-CqqAFYNg.js → mermaid.core-DrXOt-P6.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-CCb-XmEi.js → mindmap-definition-LN4V7U3C-D9ImeWBe.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-CqUV14VZ.js → pegDiagram-2B236MQR-Cq-zZ_zz.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-Dn_M_GRM.js → pieDiagram-ENE6RG2P-FgNgy726.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-rAEhJDr2.js → quadrantDiagram-ABIIQ3AL-BxQbmGmi.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-iE2o_jkH.js → railroadDiagram-RFXS5EU6-CrpbkPH2.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-BFyKBR1h.js → requirementDiagram-TGXJPOKE-DQ-65ZW6.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DRa_2YB6.js → sankeyDiagram-HTMAVEWB-Bom0lMZm.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BG8VIjD7.js → sequenceDiagram-DBY2YBRQ-C1MSDLDj.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-CS1xlbzG.js → sizeCapture-X5ZJPWSS-Bn1vxebD.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-BSl2RUJ1.js → stateDiagram-2N3HPSRC-GRRQpmzI.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-KvSJhiLM.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-BT8Hrm9J.js → swimlanes-5IMT3BWC-BcOOCZ4q.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-Cr92NnJ4.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-CfYIqEuQ.js → timeline-definition-FHXFAJF6-gi6amswh.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-C8zBH7o9.js → vennDiagram-L72KCM5P-B9uqeyQt.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-D5HyFfI9.js → wardleyDiagram-EHGQE667-BnqWAMiE.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-CGItJmLx.js → xychartDiagram-FW5EYKEG-BJXKltIN.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/dist/services/issue-goal-fallback.d.ts +0 -15
- package/dist/services/issue-goal-fallback.d.ts.map +0 -1
- package/dist/services/issue-goal-fallback.js +0 -27
- package/dist/services/issue-goal-fallback.js.map +0 -1
- package/ui-dist/assets/channel-DLNwhPnC.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-JsIc-rlc.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-JsIc-rlc.js +0 -1
- package/ui-dist/assets/index-CxPYXL3t.css +0 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DUVNSzI8.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-DfLFj7sh.js +0 -8
package/dist/services/goals.js
CHANGED
|
@@ -1,30 +1,141 @@
|
|
|
1
|
-
import { agents, automations, costEvents, financeEvents, goals, issues, projectGoals, projects } from "@rudderhq/db";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { agentWakeupRequests, agents, approvals, automations, calendarEvents, costEvents, financeEvents, goalActivities, goalChangeProposals, goalFeedbackEntries, goalOwnerAssignments, goalPlans, goalResultProposals, goalStartRequests, goals, heartbeatRuns, issues, projectGoals, projects, } from "@rudderhq/db";
|
|
2
|
+
import { activateGoalSchema } from "@rudderhq/shared";
|
|
3
|
+
import { and, asc, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { conflict, forbidden, notFound, unprocessable } from "../errors.js";
|
|
4
6
|
const DEPENDENCY_PREVIEW_LIMIT = 5;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
const TERMINAL_RUN_STATUSES = ["succeeded", "completed", "failed", "cancelled", "timed_out"];
|
|
8
|
+
const NON_INVOKABLE_AGENT_STATUSES = new Set(["terminated", "pending_approval"]);
|
|
9
|
+
const GOAL_OUTCOME_PATTERNS = [
|
|
10
|
+
/\b(ship|publish|launch|deliver|creat(?:e|ed|ion)|build|complet(?:e|ed|ion)|finish|deploy|release|migrate|implement|fix|resolve|remove|establish|achieve|reach|increase|reduce|decrease|grow|choose|select|decide|approve|pass|verify|maintain|keep|sustain|recommend|submit)\b/i,
|
|
11
|
+
/(?:发布|上线|交付|创建|构建|完成|部署|发版|迁移|实现|修复|解决|删除|建立|达到|提升|增加|降低|减少|增长|选择|决定|批准|通过|验证|保持|维持|产出|提交)/,
|
|
12
|
+
/(?:\d+(?:\.\d+)?\s*(?:%|percent|x|倍|个|项|天|周|月|年))|(?:[$€£¥¥]\s*\d)/i,
|
|
13
|
+
];
|
|
14
|
+
const GOAL_MODE_PATTERNS = {
|
|
15
|
+
decide: /\b(choose|select|decide|recommend|approve)\b|(?:选择|决定|推荐|批准)/i,
|
|
16
|
+
maintain: /\b(maintain|keep|sustain|preserve|prevent)\b|(?:保持|维持|防止|确保持续)/i,
|
|
17
|
+
maximize: /\b(increase|reduce|decrease|grow|improve by|maximi[sz]e|minimi[sz]e)\b|(?:提升|增加|降低|减少|增长|最大化|最小化)/i,
|
|
18
|
+
};
|
|
19
|
+
const BROAD_CAPABILITY_PATTERNS = [
|
|
20
|
+
/\b(general[- ]purpose|end[- ]to[- ]end)\b/i,
|
|
21
|
+
/\bplan\w*\b[\s\S]*\bexecut\w*\b|\bexecut\w*\b[\s\S]*\bplan\w*\b/i,
|
|
22
|
+
/(?:通用|端到端|规划[\s\S]*执行|执行[\s\S]*规划)/,
|
|
23
|
+
];
|
|
24
|
+
const CAPABILITY_STOP_WORDS = new Set([
|
|
25
|
+
"about", "after", "agent", "and", "are", "available", "before", "bounded", "can", "context",
|
|
26
|
+
"external", "first", "for", "from", "goal", "have", "into", "must", "not", "produce", "result",
|
|
27
|
+
"should", "that", "the", "their", "this", "through", "user", "will", "with", "work",
|
|
28
|
+
]);
|
|
29
|
+
function hasConcreteGoalOutcome(input) {
|
|
30
|
+
const combined = `${input.title.trim()} ${input.context?.trim() ?? ""}`;
|
|
31
|
+
return input.title.trim().length >= 8 && GOAL_OUTCOME_PATTERNS.some((pattern) => pattern.test(combined));
|
|
32
|
+
}
|
|
33
|
+
function goalMode(input) {
|
|
34
|
+
const title = input.title.trim();
|
|
35
|
+
if (GOAL_MODE_PATTERNS.decide.test(title))
|
|
36
|
+
return "decide";
|
|
37
|
+
if (GOAL_MODE_PATTERNS.maintain.test(title))
|
|
38
|
+
return "maintain";
|
|
39
|
+
if (GOAL_MODE_PATTERNS.maximize.test(title))
|
|
40
|
+
return "maximize";
|
|
41
|
+
if (GOAL_OUTCOME_PATTERNS.some((pattern) => pattern.test(title)))
|
|
42
|
+
return "target";
|
|
43
|
+
const context = input.context?.trim() ?? "";
|
|
44
|
+
if (GOAL_MODE_PATTERNS.decide.test(context))
|
|
45
|
+
return "decide";
|
|
46
|
+
if (GOAL_MODE_PATTERNS.maintain.test(context))
|
|
47
|
+
return "maintain";
|
|
48
|
+
if (GOAL_MODE_PATTERNS.maximize.test(context))
|
|
49
|
+
return "maximize";
|
|
50
|
+
return "target";
|
|
51
|
+
}
|
|
52
|
+
function meaningfulTokens(value) {
|
|
53
|
+
return new Set((value.toLowerCase().match(/[a-z0-9]+|[\p{Script=Han}]{2,}/gu) ?? [])
|
|
54
|
+
.map((token) => token.replace(/(?:ing|ed|es|s)$/i, ""))
|
|
55
|
+
.filter((token) => token.length >= 3 && !CAPABILITY_STOP_WORDS.has(token)));
|
|
56
|
+
}
|
|
57
|
+
function ownerCanAdvanceGoal(owner, input) {
|
|
58
|
+
const capabilities = owner.capabilities?.trim() ?? "";
|
|
59
|
+
if (!capabilities)
|
|
60
|
+
return false;
|
|
61
|
+
const descriptor = [owner.role, owner.title, capabilities].filter(Boolean).join(" ");
|
|
62
|
+
if (BROAD_CAPABILITY_PATTERNS.some((pattern) => pattern.test(descriptor)))
|
|
63
|
+
return true;
|
|
64
|
+
const goalTokens = meaningfulTokens(`${input.title} ${input.context ?? ""}`);
|
|
65
|
+
const capabilityTokens = meaningfulTokens(descriptor);
|
|
66
|
+
let overlappingTokenCount = 0;
|
|
67
|
+
for (const token of goalTokens) {
|
|
68
|
+
if (capabilityTokens.has(token))
|
|
69
|
+
overlappingTokenCount += 1;
|
|
70
|
+
}
|
|
71
|
+
if (overlappingTokenCount >= 2)
|
|
72
|
+
return true;
|
|
73
|
+
const domainPairs = [
|
|
74
|
+
[/\b(api|app|build|code|deploy|engineer|implement|release|software|system|test|ui|workspace)\b|(?:代码|工程|开发|部署|测试|系统|界面)/i, /\b(engineer|software|code|develop|build|deploy|test|technical|product)\b|(?:工程|开发|代码|部署|测试|技术|产品)/i],
|
|
75
|
+
[/\b(analy[sz]e|research|study|report|investigate)\b|(?:分析|研究|调研|报告)/i, /\b(analyst|analysis|research|data|strategy)\b|(?:分析|研究|数据|策略)/i],
|
|
76
|
+
[/\b(article|content|copy|marketing|publish|write)\b|(?:文章|内容|文案|营销|发布|写作)/i, /\b(content|copy|marketing|publish|writer|writing)\b|(?:内容|文案|营销|发布|写作)/i],
|
|
77
|
+
[/\b(design|ux|ui|prototype)\b|(?:设计|原型|界面)/i, /\b(design|designer|ux|ui|product)\b|(?:设计|产品|界面)/i],
|
|
78
|
+
[/\b(budget|finance|invoice|pricing|revenue)\b|(?:预算|财务|发票|定价|收入)/i, /\b(finance|financial|invoice|pricing|revenue|accounting)\b|(?:财务|发票|定价|收入|会计)/i],
|
|
79
|
+
];
|
|
80
|
+
const goalText = `${input.title} ${input.context ?? ""}`;
|
|
81
|
+
return domainPairs.some(([goalPattern, capabilityPattern]) => goalPattern.test(goalText) && capabilityPattern.test(descriptor));
|
|
82
|
+
}
|
|
83
|
+
function canonicalizeJson(value) {
|
|
84
|
+
if (value instanceof Date)
|
|
85
|
+
return value.toISOString();
|
|
86
|
+
if (Array.isArray(value))
|
|
87
|
+
return value.map(canonicalizeJson);
|
|
88
|
+
if (!value || typeof value !== "object")
|
|
89
|
+
return value;
|
|
90
|
+
return Object.fromEntries(Object.entries(value)
|
|
91
|
+
.filter(([, entry]) => entry !== undefined)
|
|
92
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
93
|
+
.map(([key, entry]) => [key, canonicalizeJson(entry)]));
|
|
94
|
+
}
|
|
95
|
+
export function stableGoalHash(value) {
|
|
96
|
+
return createHash("sha256").update(JSON.stringify(canonicalizeJson(value))).digest("hex");
|
|
97
|
+
}
|
|
98
|
+
function stringEvidenceRefs(value) {
|
|
99
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
100
|
+
}
|
|
101
|
+
function resultSummary(run) {
|
|
102
|
+
const candidates = [run.resultSummaryJson, run.resultJson];
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
for (const key of ["summary", "result", "message", "userMessage", "body", "error"]) {
|
|
105
|
+
const value = candidate?.[key];
|
|
106
|
+
if (typeof value === "string" && value.trim())
|
|
107
|
+
return value.trim();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return run.error?.trim() || `Agent run is ${run.status}.`;
|
|
111
|
+
}
|
|
112
|
+
function sameHashPayload(left, right) {
|
|
113
|
+
return stableGoalHash(left) === stableGoalHash(right);
|
|
114
|
+
}
|
|
115
|
+
function toIsoString(value) {
|
|
116
|
+
if (!value)
|
|
117
|
+
return null;
|
|
118
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
119
|
+
}
|
|
120
|
+
function goalContractSnapshot(goal) {
|
|
121
|
+
return {
|
|
122
|
+
contractRevision: goal.contractRevision,
|
|
123
|
+
outcomeStatement: goal.outcomeStatement ?? "",
|
|
124
|
+
objectiveMode: goal.objectiveMode,
|
|
125
|
+
criteria: goal.criteria,
|
|
126
|
+
autonomyEnvelope: goal.autonomyEnvelope,
|
|
127
|
+
humanAuthorities: goal.humanAuthorities,
|
|
128
|
+
evaluationPolicy: goal.evaluationPolicy,
|
|
129
|
+
actionDeadline: toIsoString(goal.actionDeadline),
|
|
130
|
+
evaluationDeadline: toIsoString(goal.evaluationDeadline),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function normalizeContractPatch(patch) {
|
|
134
|
+
return {
|
|
135
|
+
...patch,
|
|
136
|
+
...(patch.actionDeadline !== undefined ? { actionDeadline: toIsoString(patch.actionDeadline) } : {}),
|
|
137
|
+
...(patch.evaluationDeadline !== undefined ? { evaluationDeadline: toIsoString(patch.evaluationDeadline) } : {}),
|
|
138
|
+
};
|
|
28
139
|
}
|
|
29
140
|
function countRows(rows) {
|
|
30
141
|
return Number(rows[0]?.count ?? 0);
|
|
@@ -32,205 +143,1451 @@ function countRows(rows) {
|
|
|
32
143
|
function previewRows(rows, map) {
|
|
33
144
|
return rows.slice(0, DEPENDENCY_PREVIEW_LIMIT).map(map);
|
|
34
145
|
}
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
146
|
+
function assertCanonicalActiveGoal(goal) {
|
|
147
|
+
if (goal.lifecycle !== "active")
|
|
148
|
+
throw conflict("Only active Goals can receive this command");
|
|
149
|
+
if (!goal.ownerAgentId || !goal.outcomeStatement?.trim() || !Array.isArray(goal.criteria) || goal.criteria.length === 0
|
|
150
|
+
|| goal.planRevision < 1 || !goal.continuationKind || !goal.continuationSummary?.trim()) {
|
|
151
|
+
throw conflict("Goal requires Contract activation before this command");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function assertGoalOwner(goal, actorAgentId) {
|
|
155
|
+
if (actorAgentId && actorAgentId !== goal.ownerAgentId) {
|
|
156
|
+
throw forbidden("Only the Goal Owner can perform this Agent command");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function requireInvokableOwner(db, orgId, ownerAgentId) {
|
|
38
160
|
const owner = await db
|
|
39
|
-
.select({
|
|
161
|
+
.select({
|
|
162
|
+
id: agents.id,
|
|
163
|
+
orgId: agents.orgId,
|
|
164
|
+
name: agents.name,
|
|
165
|
+
status: agents.status,
|
|
166
|
+
role: agents.role,
|
|
167
|
+
title: agents.title,
|
|
168
|
+
capabilities: agents.capabilities,
|
|
169
|
+
})
|
|
40
170
|
.from(agents)
|
|
41
171
|
.where(and(eq(agents.id, ownerAgentId), eq(agents.orgId, orgId)))
|
|
42
172
|
.then((rows) => rows[0] ?? null);
|
|
43
|
-
if (!owner)
|
|
173
|
+
if (!owner)
|
|
44
174
|
throw unprocessable("Goal owner must belong to the same organization");
|
|
175
|
+
if (NON_INVOKABLE_AGENT_STATUSES.has(owner.status)) {
|
|
176
|
+
throw unprocessable("Goal owner must be invokable", { status: owner.status });
|
|
45
177
|
}
|
|
178
|
+
return owner;
|
|
46
179
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const parent = allGoals.find((goal) => goal.id === parentId);
|
|
58
|
-
if (!parent) {
|
|
59
|
-
throw unprocessable("Goal parent must belong to the same organization");
|
|
60
|
-
}
|
|
61
|
-
if (!goalId)
|
|
62
|
-
return;
|
|
63
|
-
const byId = new Map(allGoals.map((goal) => [goal.id, goal]));
|
|
64
|
-
let cursor = parentId;
|
|
65
|
-
const seen = new Set();
|
|
66
|
-
while (cursor) {
|
|
67
|
-
if (cursor === goalId) {
|
|
68
|
-
throw unprocessable("Goal parent cannot create a cycle");
|
|
69
|
-
}
|
|
70
|
-
if (seen.has(cursor))
|
|
71
|
-
break;
|
|
72
|
-
seen.add(cursor);
|
|
73
|
-
cursor = byId.get(cursor)?.parentId ?? null;
|
|
180
|
+
export function compileGoalStartPreview(input, owner) {
|
|
181
|
+
const title = input.title.trim();
|
|
182
|
+
if (!hasConcreteGoalOutcome(input)) {
|
|
183
|
+
return {
|
|
184
|
+
valid: false,
|
|
185
|
+
packetHash: null,
|
|
186
|
+
packet: null,
|
|
187
|
+
review: null,
|
|
188
|
+
alignmentQuestion: "What observable result or decision should this Goal produce, and how will we know it worked?",
|
|
189
|
+
};
|
|
74
190
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
191
|
+
if (!owner || owner.id !== input.ownerAgentId || NON_INVOKABLE_AGENT_STATUSES.has(owner.status)) {
|
|
192
|
+
return {
|
|
193
|
+
valid: false,
|
|
194
|
+
packetHash: null,
|
|
195
|
+
packet: null,
|
|
196
|
+
review: null,
|
|
197
|
+
alignmentQuestion: "Which available same-organization Agent should own and advance this Goal?",
|
|
198
|
+
};
|
|
79
199
|
}
|
|
80
|
-
if (
|
|
81
|
-
|
|
200
|
+
if (!ownerCanAdvanceGoal(owner, input)) {
|
|
201
|
+
return {
|
|
202
|
+
valid: false,
|
|
203
|
+
packetHash: null,
|
|
204
|
+
packet: null,
|
|
205
|
+
review: null,
|
|
206
|
+
alignmentQuestion: "The assigned Agent does not appear to cover this Goal. Which better-matched Agent should own it?",
|
|
207
|
+
};
|
|
82
208
|
}
|
|
209
|
+
const mode = goalMode(input);
|
|
210
|
+
const context = input.context?.trim() || null;
|
|
211
|
+
const success = context
|
|
212
|
+
? `Evidence demonstrates that ${title} while satisfying: ${context}`
|
|
213
|
+
: mode === "decide"
|
|
214
|
+
? `A documented decision resolves: ${title}`
|
|
215
|
+
: mode === "maximize"
|
|
216
|
+
? `Measured evidence demonstrates progress toward: ${title}`
|
|
217
|
+
: mode === "maintain"
|
|
218
|
+
? `Evidence confirms the condition remained true: ${title}`
|
|
219
|
+
: `An inspectable artifact demonstrates that ${title}`;
|
|
220
|
+
const firstAction = mode === "decide"
|
|
221
|
+
? "Gather the decision inputs and return with a clear recommendation and tradeoffs."
|
|
222
|
+
: mode === "maximize"
|
|
223
|
+
? "Establish the current baseline and take the first measurable action."
|
|
224
|
+
: mode === "maintain"
|
|
225
|
+
? "Confirm the current baseline and monitor the first material risk."
|
|
226
|
+
: "Inspect the available context and produce the first bounded, reviewable artifact.";
|
|
227
|
+
const evaluator = mode === "decide" ? "human" : mode === "maximize" ? "metric" : mode === "maintain" ? "policy" : "artifact";
|
|
228
|
+
const boundary = "The Owner may perform bounded, reversible work; consequential or irreversible actions require human approval.";
|
|
229
|
+
const activation = activateGoalSchema.parse({
|
|
230
|
+
confirmed: true,
|
|
231
|
+
ownerAgentId: owner.id,
|
|
232
|
+
outcomeStatement: title,
|
|
233
|
+
objectiveMode: mode,
|
|
234
|
+
criteria: [{ id: "primary-outcome", label: success, evaluator }],
|
|
235
|
+
autonomyEnvelope: {
|
|
236
|
+
allowed: ["bounded_reversible_work"],
|
|
237
|
+
requiresHumanApproval: ["external_or_irreversible_action", "authority_expansion"],
|
|
238
|
+
},
|
|
239
|
+
humanAuthorities: {
|
|
240
|
+
acceptance: "board_human",
|
|
241
|
+
consequentialChanges: "board_human",
|
|
242
|
+
},
|
|
243
|
+
evaluationPolicy: {
|
|
244
|
+
terminalEvidenceRequired: true,
|
|
245
|
+
humanAcceptanceRequired: true,
|
|
246
|
+
},
|
|
247
|
+
actionDeadline: null,
|
|
248
|
+
evaluationDeadline: input.targetTime,
|
|
249
|
+
initialPlan: { summary: firstAction },
|
|
250
|
+
initialContinuation: {
|
|
251
|
+
kind: "commitment",
|
|
252
|
+
summary: "Advance the first bounded action and report evidence or a named blocker.",
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
const packet = {
|
|
256
|
+
version: 1,
|
|
257
|
+
title,
|
|
258
|
+
description: context,
|
|
259
|
+
ownerAgentId: owner.id,
|
|
260
|
+
activation,
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
valid: true,
|
|
264
|
+
packetHash: stableGoalHash(packet),
|
|
265
|
+
packet,
|
|
266
|
+
review: {
|
|
267
|
+
outcome: title,
|
|
268
|
+
success,
|
|
269
|
+
boundary,
|
|
270
|
+
firstAction,
|
|
271
|
+
ownerAgentId: owner.id,
|
|
272
|
+
targetTime: input.targetTime,
|
|
273
|
+
},
|
|
274
|
+
alignmentQuestion: null,
|
|
275
|
+
};
|
|
83
276
|
}
|
|
84
277
|
export async function getGoalDependencies(db, goal) {
|
|
85
|
-
const [childGoalRows, projectJoinRows, legacyProjectRows, issueRows, automationRows, costEventRows, financeEventRows
|
|
86
|
-
db
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
.where(and(eq(goals.orgId, goal.orgId), eq(goals.parentId, goal.id)))
|
|
90
|
-
.orderBy(asc(goals.createdAt)),
|
|
91
|
-
db
|
|
92
|
-
.select({ id: projects.id, name: projects.name, status: projects.status })
|
|
93
|
-
.from(projectGoals)
|
|
278
|
+
const [childGoalRows, projectJoinRows, legacyProjectRows, issueRows, automationRows, calendarEventRows, costEventRows, financeEventRows] = await Promise.all([
|
|
279
|
+
db.select({ id: goals.id, title: goals.title, status: goals.status }).from(goals)
|
|
280
|
+
.where(and(eq(goals.orgId, goal.orgId), eq(goals.parentId, goal.id))).orderBy(asc(goals.createdAt)),
|
|
281
|
+
db.select({ id: projects.id, name: projects.name, status: projects.status }).from(projectGoals)
|
|
94
282
|
.innerJoin(projects, eq(projectGoals.projectId, projects.id))
|
|
95
|
-
.where(and(eq(projectGoals.orgId, goal.orgId), eq(projectGoals.goalId, goal.id)))
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
.orderBy(asc(
|
|
102
|
-
db
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
.orderBy(asc(issues.createdAt)),
|
|
107
|
-
db
|
|
108
|
-
.select({ id: automations.id, title: automations.title, status: automations.status })
|
|
109
|
-
.from(automations)
|
|
110
|
-
.where(and(eq(automations.orgId, goal.orgId), eq(automations.goalId, goal.id)))
|
|
111
|
-
.orderBy(asc(automations.createdAt)),
|
|
112
|
-
db
|
|
113
|
-
.select({ count: sql `count(*)::int` })
|
|
114
|
-
.from(costEvents)
|
|
283
|
+
.where(and(eq(projectGoals.orgId, goal.orgId), eq(projectGoals.goalId, goal.id))).orderBy(asc(projects.createdAt)),
|
|
284
|
+
db.select({ id: projects.id, name: projects.name, status: projects.status }).from(projects)
|
|
285
|
+
.where(and(eq(projects.orgId, goal.orgId), eq(projects.goalId, goal.id))).orderBy(asc(projects.createdAt)),
|
|
286
|
+
db.select({ id: issues.id, identifier: issues.identifier, title: issues.title, status: issues.status }).from(issues)
|
|
287
|
+
.where(and(eq(issues.orgId, goal.orgId), eq(issues.goalId, goal.id))).orderBy(asc(issues.createdAt)),
|
|
288
|
+
db.select({ id: automations.id, title: automations.title, status: automations.status }).from(automations)
|
|
289
|
+
.where(and(eq(automations.orgId, goal.orgId), eq(automations.goalId, goal.id))).orderBy(asc(automations.createdAt)),
|
|
290
|
+
db.select({ id: calendarEvents.id, title: calendarEvents.title, status: calendarEvents.eventStatus }).from(calendarEvents)
|
|
291
|
+
.where(and(eq(calendarEvents.orgId, goal.orgId), eq(calendarEvents.goalId, goal.id), isNull(calendarEvents.deletedAt)))
|
|
292
|
+
.orderBy(asc(calendarEvents.createdAt)),
|
|
293
|
+
db.select({ count: sql `count(*)::int` }).from(costEvents)
|
|
115
294
|
.where(and(eq(costEvents.orgId, goal.orgId), eq(costEvents.goalId, goal.id))),
|
|
116
|
-
db
|
|
117
|
-
.select({ count: sql `count(*)::int` })
|
|
118
|
-
.from(financeEvents)
|
|
295
|
+
db.select({ count: sql `count(*)::int` }).from(financeEvents)
|
|
119
296
|
.where(and(eq(financeEvents.orgId, goal.orgId), eq(financeEvents.goalId, goal.id))),
|
|
120
|
-
goal.level === "organization" && goal.parentId === null
|
|
121
|
-
? db
|
|
122
|
-
.select({ count: sql `count(*)::int` })
|
|
123
|
-
.from(goals)
|
|
124
|
-
.where(and(eq(goals.orgId, goal.orgId), eq(goals.level, "organization"), isNull(goals.parentId)))
|
|
125
|
-
: Promise.resolve([{ count: 0 }]),
|
|
126
297
|
]);
|
|
127
298
|
const linkedProjectsById = new Map();
|
|
128
|
-
for (const project of [...projectJoinRows, ...legacyProjectRows])
|
|
299
|
+
for (const project of [...projectJoinRows, ...legacyProjectRows])
|
|
129
300
|
linkedProjectsById.set(project.id, project);
|
|
130
|
-
}
|
|
131
301
|
const linkedProjects = [...linkedProjectsById.values()];
|
|
132
302
|
const counts = {
|
|
133
303
|
childGoals: childGoalRows.length,
|
|
134
304
|
linkedProjects: linkedProjects.length,
|
|
135
305
|
linkedIssues: issueRows.length,
|
|
136
306
|
automations: automationRows.length,
|
|
307
|
+
calendarEvents: calendarEventRows.length,
|
|
137
308
|
costEvents: countRows(costEventRows),
|
|
138
309
|
financeEvents: countRows(financeEventRows),
|
|
139
310
|
};
|
|
140
|
-
const isLastRootOrganizationGoal = goal.level === "organization" &&
|
|
141
|
-
goal.parentId === null &&
|
|
142
|
-
countRows(rootGoalRows) <= 1;
|
|
143
311
|
const blockers = [
|
|
144
|
-
...(isLastRootOrganizationGoal ? ["last_root_organization_goal"] : []),
|
|
145
312
|
...(counts.childGoals > 0 ? ["child_goals"] : []),
|
|
146
313
|
...(counts.linkedProjects > 0 ? ["linked_projects"] : []),
|
|
147
314
|
...(counts.linkedIssues > 0 ? ["linked_issues"] : []),
|
|
148
315
|
...(counts.automations > 0 ? ["automations"] : []),
|
|
316
|
+
...(counts.calendarEvents > 0 ? ["calendar_events"] : []),
|
|
149
317
|
...(counts.costEvents > 0 ? ["cost_events"] : []),
|
|
150
318
|
...(counts.financeEvents > 0 ? ["finance_events"] : []),
|
|
151
319
|
];
|
|
152
320
|
return {
|
|
153
321
|
goalId: goal.id,
|
|
154
322
|
orgId: goal.orgId,
|
|
155
|
-
canDelete: blockers.length === 0,
|
|
156
|
-
blockers,
|
|
157
|
-
isLastRootOrganizationGoal,
|
|
323
|
+
canDelete: blockers.length === 0 && goal.lifecycle === "draft",
|
|
324
|
+
blockers: goal.lifecycle === "draft" ? blockers : [...blockers, "goal_not_draft"],
|
|
325
|
+
isLastRootOrganizationGoal: false,
|
|
158
326
|
counts,
|
|
159
327
|
previews: {
|
|
160
|
-
childGoals: previewRows(childGoalRows, (row) => ({
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
})),
|
|
165
|
-
linkedProjects: previewRows(linkedProjects, (row) => ({
|
|
166
|
-
id: row.id,
|
|
167
|
-
title: row.name,
|
|
168
|
-
subtitle: row.status,
|
|
169
|
-
})),
|
|
170
|
-
linkedIssues: previewRows(issueRows, (row) => ({
|
|
171
|
-
id: row.id,
|
|
172
|
-
title: row.title,
|
|
173
|
-
subtitle: row.identifier ?? row.status,
|
|
174
|
-
})),
|
|
175
|
-
automations: previewRows(automationRows, (row) => ({
|
|
176
|
-
id: row.id,
|
|
177
|
-
title: row.title,
|
|
178
|
-
subtitle: row.status,
|
|
179
|
-
})),
|
|
328
|
+
childGoals: previewRows(childGoalRows, (row) => ({ id: row.id, title: row.title, subtitle: row.status })),
|
|
329
|
+
linkedProjects: previewRows(linkedProjects, (row) => ({ id: row.id, title: row.name, subtitle: row.status })),
|
|
330
|
+
linkedIssues: previewRows(issueRows, (row) => ({ id: row.id, title: row.title, subtitle: row.identifier ?? row.status })),
|
|
331
|
+
automations: previewRows(automationRows, (row) => ({ id: row.id, title: row.title, subtitle: row.status })),
|
|
332
|
+
calendarEvents: previewRows(calendarEventRows, (row) => ({ id: row.id, title: row.title, subtitle: row.status })),
|
|
180
333
|
},
|
|
181
334
|
};
|
|
182
335
|
}
|
|
336
|
+
async function requireGoal(db, id) {
|
|
337
|
+
const goal = await db.select().from(goals).where(eq(goals.id, id)).then((rows) => rows[0] ?? null);
|
|
338
|
+
if (!goal)
|
|
339
|
+
throw notFound("Goal not found");
|
|
340
|
+
return goal;
|
|
341
|
+
}
|
|
342
|
+
function positiveOutcome(mode, outcome) {
|
|
343
|
+
return (mode === "target" && outcome === "achieved")
|
|
344
|
+
|| (mode === "maximize" && outcome === "completed_with_result")
|
|
345
|
+
|| (mode === "maintain" && outcome === "maintained")
|
|
346
|
+
|| (mode === "decide" && outcome === "decided");
|
|
347
|
+
}
|
|
348
|
+
export function reduceGoalEvaluation(goal, input) {
|
|
349
|
+
const statuses = new Map(input.criteria.map((criterion) => [criterion.id, criterion.status]));
|
|
350
|
+
const evidenceRefs = input.evidenceRefs;
|
|
351
|
+
const hasReference = (reference) => evidenceRefs.some((evidence) => evidence === reference);
|
|
352
|
+
const criterionStatuses = goal.criteria.map((criterion) => {
|
|
353
|
+
const requiredEvidence = criterion.evidenceRequirements ?? [];
|
|
354
|
+
const missingEvidence = requiredEvidence.filter((reference) => !hasReference(reference));
|
|
355
|
+
const evaluatorEvidenceSatisfied = criterion.evaluator === "metric"
|
|
356
|
+
? input.resultValue !== undefined
|
|
357
|
+
: criterion.evaluator === "human"
|
|
358
|
+
? Boolean(input.decision?.trim() || input.resultPayload.humanApproval === true)
|
|
359
|
+
: evidenceRefs.length > 0;
|
|
360
|
+
const evidenceSatisfied = missingEvidence.length === 0 && evaluatorEvidenceSatisfied;
|
|
361
|
+
const submittedStatus = statuses.get(criterion.id) ?? "unknown";
|
|
362
|
+
return {
|
|
363
|
+
id: criterion.id,
|
|
364
|
+
evaluator: criterion.evaluator ?? null,
|
|
365
|
+
status: !evidenceSatisfied ? "unknown" : submittedStatus,
|
|
366
|
+
evidenceSatisfied,
|
|
367
|
+
missingEvidence,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const has = (status) => criterionStatuses.some((criterion) => criterion.status === status);
|
|
371
|
+
const allMet = criterionStatuses.length > 0 && criterionStatuses.every((criterion) => criterion.status === "met");
|
|
372
|
+
const hasFailure = has("unmet") || has("breached");
|
|
373
|
+
let outcome;
|
|
374
|
+
switch (goal.objectiveMode) {
|
|
375
|
+
case "target":
|
|
376
|
+
outcome = hasFailure ? "not_achieved" : allMet ? "achieved" : "inconclusive";
|
|
377
|
+
break;
|
|
378
|
+
case "maximize":
|
|
379
|
+
outcome = !hasFailure && allMet && input.resultValue !== undefined ? "completed_with_result" : "inconclusive";
|
|
380
|
+
break;
|
|
381
|
+
case "maintain":
|
|
382
|
+
outcome = has("breached") ? "breached" : allMet ? "maintained" : "inconclusive";
|
|
383
|
+
break;
|
|
384
|
+
case "decide":
|
|
385
|
+
outcome = !hasFailure && allMet && input.decision?.trim() ? "decided" : "inconclusive";
|
|
386
|
+
break;
|
|
387
|
+
default:
|
|
388
|
+
outcome = "inconclusive";
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
mode: goal.objectiveMode,
|
|
392
|
+
outcome,
|
|
393
|
+
criteria: criterionStatuses,
|
|
394
|
+
evidenceRefs: input.evidenceRefs,
|
|
395
|
+
resultValue: input.resultValue,
|
|
396
|
+
decision: input.decision ?? null,
|
|
397
|
+
evaluatedAt: new Date().toISOString(),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function evaluationCandidate(input) {
|
|
401
|
+
return {
|
|
402
|
+
evidenceRefs: input.evidenceRefs,
|
|
403
|
+
criteria: input.criteria,
|
|
404
|
+
...(input.resultValue !== undefined ? { resultValue: input.resultValue } : {}),
|
|
405
|
+
...(input.decision !== undefined ? { decision: input.decision } : {}),
|
|
406
|
+
resultPayload: input.resultPayload,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function isTerminalEvaluation(outcome) {
|
|
410
|
+
return outcome !== "inconclusive";
|
|
411
|
+
}
|
|
412
|
+
function inconclusiveResultSummary(preflight, riskSummary) {
|
|
413
|
+
const gaps = preflight.criteria.flatMap((criterion) => {
|
|
414
|
+
if (criterion.missingEvidence.length > 0) {
|
|
415
|
+
return criterion.missingEvidence.map((requirement) => `${criterion.id}: missing ${requirement}`);
|
|
416
|
+
}
|
|
417
|
+
return criterion.status === "unknown" ? [`${criterion.id}: unknown`] : [];
|
|
418
|
+
});
|
|
419
|
+
const details = [
|
|
420
|
+
preflight.resultValue !== undefined ? `Result value: ${String(preflight.resultValue)}.` : null,
|
|
421
|
+
preflight.decision ? `Decision: ${preflight.decision}.` : null,
|
|
422
|
+
gaps.length > 0 ? `Gaps: ${gaps.join("; ")}.` : null,
|
|
423
|
+
].filter((value) => Boolean(value));
|
|
424
|
+
return [`Result attempt remains inconclusive: ${riskSummary}`, ...details].join(" ");
|
|
425
|
+
}
|
|
426
|
+
function proposalCreationPayload(input, normalizedPatch) {
|
|
427
|
+
return {
|
|
428
|
+
expectedContractRevision: input.expectedContractRevision,
|
|
429
|
+
afterContract: normalizedPatch,
|
|
430
|
+
rationale: input.rationale,
|
|
431
|
+
evidenceRefs: input.evidenceRefs,
|
|
432
|
+
approvalId: input.approvalId ?? null,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function assertSameChangeProposalPayload(existing, input, normalizedPatch) {
|
|
436
|
+
if (input.approvalId && existing.approvalId !== input.approvalId) {
|
|
437
|
+
throw conflict("Goal change proposal idempotency key was reused with a different approval");
|
|
438
|
+
}
|
|
439
|
+
const stored = {
|
|
440
|
+
expectedContractRevision: existing.expectedContractRevision,
|
|
441
|
+
afterContract: existing.afterContract,
|
|
442
|
+
rationale: existing.rationale,
|
|
443
|
+
evidenceRefs: existing.evidenceRefs,
|
|
444
|
+
approvalId: input.approvalId ?? null,
|
|
445
|
+
};
|
|
446
|
+
if (!sameHashPayload(stored, proposalCreationPayload(input, normalizedPatch))) {
|
|
447
|
+
throw conflict("Goal change proposal idempotency key was reused with a different payload");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function assertSameResultProposalPayload(existing, input, candidateHash) {
|
|
451
|
+
if (existing.contractRevision !== input.contractRevision
|
|
452
|
+
|| existing.candidateHash !== candidateHash
|
|
453
|
+
|| existing.riskSummary !== input.riskSummary) {
|
|
454
|
+
throw conflict("Goal result proposal idempotency key was reused with a different payload");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
183
457
|
export function goalService(db) {
|
|
458
|
+
function facetFor(goal, pendingChange, readyResult) {
|
|
459
|
+
if (goal.lifecycle === "closed")
|
|
460
|
+
return "closed";
|
|
461
|
+
if (goal.lifecycle === "draft")
|
|
462
|
+
return "needs_attention";
|
|
463
|
+
if (readyResult)
|
|
464
|
+
return "ready_for_acceptance";
|
|
465
|
+
if (pendingChange)
|
|
466
|
+
return "needs_attention";
|
|
467
|
+
if (goal.continuationKind === "wait")
|
|
468
|
+
return "waiting_external";
|
|
469
|
+
return "agent_advancing";
|
|
470
|
+
}
|
|
471
|
+
async function latestExternalCheckpoint(goal) {
|
|
472
|
+
const [issueFacts, linkedProjectFacts] = await Promise.all([
|
|
473
|
+
db.select({
|
|
474
|
+
id: issues.id,
|
|
475
|
+
identifier: issues.identifier,
|
|
476
|
+
title: issues.title,
|
|
477
|
+
status: issues.status,
|
|
478
|
+
updatedAt: issues.updatedAt,
|
|
479
|
+
checkoutRunId: issues.checkoutRunId,
|
|
480
|
+
executionRunId: issues.executionRunId,
|
|
481
|
+
}).from(issues).where(and(eq(issues.orgId, goal.orgId), eq(issues.goalId, goal.id))),
|
|
482
|
+
db.select({
|
|
483
|
+
id: projects.id,
|
|
484
|
+
name: projects.name,
|
|
485
|
+
status: projects.status,
|
|
486
|
+
updatedAt: projects.updatedAt,
|
|
487
|
+
}).from(projects).leftJoin(projectGoals, and(eq(projectGoals.projectId, projects.id), eq(projectGoals.orgId, goal.orgId))).where(and(eq(projects.orgId, goal.orgId), or(eq(projects.goalId, goal.id), eq(projectGoals.goalId, goal.id)))),
|
|
488
|
+
]);
|
|
489
|
+
const runIds = [...new Set(issueFacts.flatMap((issue) => [issue.checkoutRunId, issue.executionRunId]).filter((id) => Boolean(id)))];
|
|
490
|
+
const runFacts = runIds.length > 0
|
|
491
|
+
? await db.select({
|
|
492
|
+
id: heartbeatRuns.id,
|
|
493
|
+
status: heartbeatRuns.status,
|
|
494
|
+
updatedAt: heartbeatRuns.updatedAt,
|
|
495
|
+
resultJson: heartbeatRuns.resultJson,
|
|
496
|
+
resultSummaryJson: heartbeatRuns.resultSummaryJson,
|
|
497
|
+
error: heartbeatRuns.error,
|
|
498
|
+
}).from(heartbeatRuns).where(and(eq(heartbeatRuns.orgId, goal.orgId), inArray(heartbeatRuns.id, runIds)))
|
|
499
|
+
: [];
|
|
500
|
+
const facts = [
|
|
501
|
+
...issueFacts.map((issue) => ({
|
|
502
|
+
id: `checkpoint:issue:${issue.id}`,
|
|
503
|
+
kind: "checkpoint",
|
|
504
|
+
summary: `Issue ${issue.identifier ?? issue.title} is ${issue.status}.`,
|
|
505
|
+
occurredAt: issue.updatedAt,
|
|
506
|
+
sourceId: issue.id,
|
|
507
|
+
sourceRunId: issue.executionRunId ?? issue.checkoutRunId,
|
|
508
|
+
evidenceRefs: [`issue://${issue.id}`],
|
|
509
|
+
})),
|
|
510
|
+
...linkedProjectFacts.map((project) => ({
|
|
511
|
+
id: `checkpoint:project:${project.id}`,
|
|
512
|
+
kind: "checkpoint",
|
|
513
|
+
summary: `Project ${project.name} is ${project.status}.`,
|
|
514
|
+
occurredAt: project.updatedAt,
|
|
515
|
+
sourceId: project.id,
|
|
516
|
+
sourceRunId: null,
|
|
517
|
+
evidenceRefs: [`project://${project.id}`],
|
|
518
|
+
})),
|
|
519
|
+
...runFacts.map((run) => ({
|
|
520
|
+
id: `checkpoint:run:${run.id}`,
|
|
521
|
+
kind: "checkpoint",
|
|
522
|
+
summary: `Agent run ${run.id.slice(0, 8)} is ${run.status}: ${resultSummary(run)}`,
|
|
523
|
+
occurredAt: run.updatedAt,
|
|
524
|
+
sourceId: run.id,
|
|
525
|
+
sourceRunId: run.id,
|
|
526
|
+
evidenceRefs: [`run://${run.id}`],
|
|
527
|
+
})),
|
|
528
|
+
];
|
|
529
|
+
return facts.sort((left, right) => right.occurredAt.getTime() - left.occurredAt.getTime())[0] ?? null;
|
|
530
|
+
}
|
|
531
|
+
async function recordFeedback(database, goal, input, actorUserId) {
|
|
532
|
+
assertCanonicalActiveGoal(goal);
|
|
533
|
+
const contentHash = stableGoalHash({
|
|
534
|
+
actorType: "user",
|
|
535
|
+
actorId: actorUserId,
|
|
536
|
+
body: input.body,
|
|
537
|
+
attachments: input.attachments,
|
|
538
|
+
feedbackKind: input.feedbackKind,
|
|
539
|
+
});
|
|
540
|
+
const existing = await database.select().from(goalFeedbackEntries).where(and(eq(goalFeedbackEntries.goalId, goal.id), eq(goalFeedbackEntries.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
541
|
+
if (existing) {
|
|
542
|
+
if (existing.contentHash !== contentHash) {
|
|
543
|
+
throw conflict("Goal feedback idempotency key was reused with a different payload");
|
|
544
|
+
}
|
|
545
|
+
return existing;
|
|
546
|
+
}
|
|
547
|
+
const [inserted] = await database.insert(goalFeedbackEntries).values({
|
|
548
|
+
orgId: goal.orgId,
|
|
549
|
+
goalId: goal.id,
|
|
550
|
+
actorType: "user",
|
|
551
|
+
actorId: actorUserId,
|
|
552
|
+
body: input.body,
|
|
553
|
+
attachments: input.attachments,
|
|
554
|
+
contentHash,
|
|
555
|
+
feedbackKind: input.feedbackKind,
|
|
556
|
+
idempotencyKey: input.idempotencyKey,
|
|
557
|
+
}).onConflictDoNothing().returning();
|
|
558
|
+
const feedback = inserted ?? await database.select().from(goalFeedbackEntries).where(and(eq(goalFeedbackEntries.goalId, goal.id), eq(goalFeedbackEntries.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
559
|
+
if (!feedback)
|
|
560
|
+
throw conflict("Goal feedback could not be recorded");
|
|
561
|
+
if (feedback.contentHash !== contentHash) {
|
|
562
|
+
throw conflict("Goal feedback idempotency key was reused with a different payload");
|
|
563
|
+
}
|
|
564
|
+
if (feedback.routedWakeupRequestId)
|
|
565
|
+
return feedback;
|
|
566
|
+
const wakeupKey = `goal-feedback:${feedback.id}`;
|
|
567
|
+
const [insertedWakeup] = await database.insert(agentWakeupRequests).values({
|
|
568
|
+
orgId: goal.orgId,
|
|
569
|
+
agentId: goal.ownerAgentId,
|
|
570
|
+
source: "goal_feedback",
|
|
571
|
+
triggerDetail: "goal_feedback",
|
|
572
|
+
reason: "Goal feedback requires Owner review",
|
|
573
|
+
payload: { goalId: goal.id, feedbackId: feedback.id },
|
|
574
|
+
status: "queued",
|
|
575
|
+
requestedByActorType: "user",
|
|
576
|
+
requestedByActorId: actorUserId,
|
|
577
|
+
idempotencyKey: wakeupKey,
|
|
578
|
+
}).onConflictDoNothing().returning();
|
|
579
|
+
const wakeup = insertedWakeup ?? await database.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.orgId, goal.orgId), eq(agentWakeupRequests.agentId, goal.ownerAgentId), eq(agentWakeupRequests.idempotencyKey, wakeupKey))).then((rows) => rows[0] ?? null);
|
|
580
|
+
if (!wakeup)
|
|
581
|
+
throw conflict("Goal feedback Owner wakeup could not be queued");
|
|
582
|
+
return database.update(goalFeedbackEntries).set({
|
|
583
|
+
routedWakeupRequestId: wakeup.id,
|
|
584
|
+
updatedAt: new Date(),
|
|
585
|
+
}).where(and(eq(goalFeedbackEntries.id, feedback.id), eq(goalFeedbackEntries.orgId, goal.orgId))).returning().then((rows) => rows[0] ?? feedback);
|
|
586
|
+
}
|
|
587
|
+
async function evaluateInTransaction(database, current, input, actorAgentId) {
|
|
588
|
+
assertCanonicalActiveGoal(current);
|
|
589
|
+
assertGoalOwner(current, actorAgentId);
|
|
590
|
+
const evaluation = reduceGoalEvaluation(current, input);
|
|
591
|
+
const terminal = isTerminalEvaluation(evaluation.outcome);
|
|
592
|
+
let acceptedProposal = null;
|
|
593
|
+
if (terminal) {
|
|
594
|
+
if (!input.resultProposalId) {
|
|
595
|
+
throw conflict("Terminal Goal evaluation requires an accepted Result Proposal");
|
|
596
|
+
}
|
|
597
|
+
acceptedProposal = await database.select().from(goalResultProposals).where(and(eq(goalResultProposals.id, input.resultProposalId), eq(goalResultProposals.goalId, current.id), eq(goalResultProposals.orgId, current.orgId))).then((rows) => rows[0] ?? null);
|
|
598
|
+
const candidate = evaluationCandidate(input);
|
|
599
|
+
if (!acceptedProposal
|
|
600
|
+
|| acceptedProposal.status !== "accepted"
|
|
601
|
+
|| acceptedProposal.acceptedByActorType !== "user"
|
|
602
|
+
|| !acceptedProposal.acceptedByActorId
|
|
603
|
+
|| !acceptedProposal.acceptedAt
|
|
604
|
+
|| acceptedProposal.consumedAt
|
|
605
|
+
|| acceptedProposal.contractRevision !== current.contractRevision
|
|
606
|
+
|| acceptedProposal.candidateHash !== stableGoalHash(candidate)
|
|
607
|
+
|| !sameHashPayload(acceptedProposal.candidate, candidate)) {
|
|
608
|
+
throw conflict("Accepted Result Proposal does not match the current Goal and evaluation candidate");
|
|
609
|
+
}
|
|
610
|
+
const consumed = await database.update(goalResultProposals).set({
|
|
611
|
+
consumedAt: new Date(),
|
|
612
|
+
updatedAt: new Date(),
|
|
613
|
+
}).where(and(eq(goalResultProposals.id, acceptedProposal.id), isNull(goalResultProposals.consumedAt))).returning().then((rows) => rows[0] ?? null);
|
|
614
|
+
if (!consumed)
|
|
615
|
+
throw conflict("Result Proposal was already consumed");
|
|
616
|
+
}
|
|
617
|
+
const lifecycle = terminal ? "closed" : "active";
|
|
618
|
+
const status = terminal
|
|
619
|
+
? positiveOutcome(current.objectiveMode, evaluation.outcome) ? "achieved" : "cancelled"
|
|
620
|
+
: "active";
|
|
621
|
+
const [goal] = await database.update(goals).set({
|
|
622
|
+
lifecycle,
|
|
623
|
+
status,
|
|
624
|
+
...(terminal ? { focus: false, closeReason: "evaluated" } : {}),
|
|
625
|
+
evaluationResult: evaluation,
|
|
626
|
+
resultPayload: input.resultPayload,
|
|
627
|
+
updatedAt: new Date(),
|
|
628
|
+
}).where(and(eq(goals.id, current.id), eq(goals.orgId, current.orgId), eq(goals.lifecycle, "active"), eq(goals.contractRevision, current.contractRevision))).returning();
|
|
629
|
+
if (!goal)
|
|
630
|
+
throw conflict("Goal changed before evaluation; reload and retry");
|
|
631
|
+
await database.insert(goalActivities).values({
|
|
632
|
+
orgId: current.orgId,
|
|
633
|
+
goalId: current.id,
|
|
634
|
+
contractRevision: current.contractRevision,
|
|
635
|
+
submittedByAgentId: actorAgentId,
|
|
636
|
+
agentOwnerRefAtTime: current.ownerAgentId,
|
|
637
|
+
activityKind: "evidence",
|
|
638
|
+
summary: `Goal evaluated as ${evaluation.outcome}`,
|
|
639
|
+
evidenceRefs: input.evidenceRefs,
|
|
640
|
+
idempotencyKey: acceptedProposal ? `goal-result-evaluation:${acceptedProposal.id}` : null,
|
|
641
|
+
}).onConflictDoNothing();
|
|
642
|
+
return goal;
|
|
643
|
+
}
|
|
644
|
+
async function workspaceFor(id) {
|
|
645
|
+
const goal = await requireGoal(db, id);
|
|
646
|
+
const [ownerAssignment, plan, activities, feedback, changes, results] = await Promise.all([
|
|
647
|
+
db.select().from(goalOwnerAssignments).where(and(eq(goalOwnerAssignments.goalId, id), isNull(goalOwnerAssignments.endsAt))).then((rows) => rows[0] ?? null),
|
|
648
|
+
goal.planRevision > 0
|
|
649
|
+
? db.select().from(goalPlans).where(and(eq(goalPlans.goalId, id), eq(goalPlans.revision, goal.planRevision))).then((rows) => rows[0] ?? null)
|
|
650
|
+
: Promise.resolve(null),
|
|
651
|
+
db.select().from(goalActivities).where(eq(goalActivities.goalId, id))
|
|
652
|
+
.orderBy(desc(goalActivities.occurredAt), desc(goalActivities.createdAt)).limit(100),
|
|
653
|
+
db.select().from(goalFeedbackEntries).where(eq(goalFeedbackEntries.goalId, id))
|
|
654
|
+
.orderBy(desc(goalFeedbackEntries.createdAt)).limit(100),
|
|
655
|
+
db.select().from(goalChangeProposals).where(eq(goalChangeProposals.goalId, id))
|
|
656
|
+
.orderBy(desc(goalChangeProposals.createdAt)).limit(100),
|
|
657
|
+
db.select().from(goalResultProposals).where(eq(goalResultProposals.goalId, id))
|
|
658
|
+
.orderBy(desc(goalResultProposals.createdAt)).limit(100),
|
|
659
|
+
]);
|
|
660
|
+
const externalCheckpoint = await latestExternalCheckpoint(goal);
|
|
661
|
+
const evidenceActivity = activities.find((activity) => stringEvidenceRefs(activity.evidenceRefs).length > 0) ?? null;
|
|
662
|
+
const progressSource = [
|
|
663
|
+
evidenceActivity
|
|
664
|
+
? {
|
|
665
|
+
summary: evidenceActivity.summary,
|
|
666
|
+
sourceActivityId: evidenceActivity.id,
|
|
667
|
+
evidenceRefs: stringEvidenceRefs(evidenceActivity.evidenceRefs),
|
|
668
|
+
occurredAt: evidenceActivity.occurredAt,
|
|
669
|
+
}
|
|
670
|
+
: null,
|
|
671
|
+
externalCheckpoint
|
|
672
|
+
? {
|
|
673
|
+
summary: externalCheckpoint.summary,
|
|
674
|
+
sourceActivityId: null,
|
|
675
|
+
evidenceRefs: externalCheckpoint.evidenceRefs,
|
|
676
|
+
occurredAt: externalCheckpoint.occurredAt,
|
|
677
|
+
}
|
|
678
|
+
: null,
|
|
679
|
+
].filter((value) => Boolean(value))
|
|
680
|
+
.sort((left, right) => right.occurredAt.getTime() - left.occurredAt.getTime())[0] ?? null;
|
|
681
|
+
const pendingChange = changes.find((proposal) => proposal.status === "pending") ?? null;
|
|
682
|
+
const readyResult = results.find((proposal) => proposal.status === "ready") ?? null;
|
|
683
|
+
const facet = facetFor(goal, pendingChange, readyResult);
|
|
684
|
+
const attention = readyResult
|
|
685
|
+
? {
|
|
686
|
+
kind: "result_proposal",
|
|
687
|
+
reason: `Review the proposed Goal result: ${readyResult.preflight.outcome}`,
|
|
688
|
+
sourceId: readyResult.id,
|
|
689
|
+
}
|
|
690
|
+
: pendingChange
|
|
691
|
+
? {
|
|
692
|
+
kind: "change_proposal",
|
|
693
|
+
reason: pendingChange.rationale,
|
|
694
|
+
sourceId: pendingChange.id,
|
|
695
|
+
}
|
|
696
|
+
: goal.lifecycle === "draft" && goal.alignmentQuestion
|
|
697
|
+
? { kind: "alignment_question", reason: goal.alignmentQuestion, sourceId: goal.id }
|
|
698
|
+
: null;
|
|
699
|
+
const timeline = [
|
|
700
|
+
...activities.map((activity) => ({
|
|
701
|
+
id: activity.id,
|
|
702
|
+
kind: "activity",
|
|
703
|
+
summary: activity.summary,
|
|
704
|
+
occurredAt: activity.occurredAt,
|
|
705
|
+
sourceId: activity.id,
|
|
706
|
+
sourceRunId: activity.runRef,
|
|
707
|
+
evidenceRefs: stringEvidenceRefs(activity.evidenceRefs),
|
|
708
|
+
})),
|
|
709
|
+
...feedback.map((entry) => ({
|
|
710
|
+
id: entry.id,
|
|
711
|
+
kind: "feedback",
|
|
712
|
+
summary: entry.body,
|
|
713
|
+
occurredAt: entry.createdAt,
|
|
714
|
+
sourceId: entry.id,
|
|
715
|
+
actorType: entry.actorType,
|
|
716
|
+
actorId: entry.actorId,
|
|
717
|
+
attachments: entry.attachments,
|
|
718
|
+
feedbackKind: entry.feedbackKind,
|
|
719
|
+
})),
|
|
720
|
+
...changes.map((proposal) => ({
|
|
721
|
+
id: proposal.id,
|
|
722
|
+
kind: "change_proposal",
|
|
723
|
+
summary: proposal.rationale,
|
|
724
|
+
occurredAt: proposal.createdAt,
|
|
725
|
+
sourceId: proposal.id,
|
|
726
|
+
approvalId: proposal.approvalId,
|
|
727
|
+
status: proposal.status,
|
|
728
|
+
evidenceRefs: proposal.evidenceRefs,
|
|
729
|
+
})),
|
|
730
|
+
...results.map((proposal) => ({
|
|
731
|
+
id: proposal.id,
|
|
732
|
+
kind: "result_proposal",
|
|
733
|
+
summary: `Result proposed as ${proposal.preflight.outcome}: ${proposal.riskSummary}`,
|
|
734
|
+
occurredAt: proposal.createdAt,
|
|
735
|
+
sourceId: proposal.id,
|
|
736
|
+
status: proposal.status,
|
|
737
|
+
evidenceRefs: proposal.candidate.evidenceRefs,
|
|
738
|
+
})),
|
|
739
|
+
...(externalCheckpoint ? [externalCheckpoint] : []),
|
|
740
|
+
].sort((left, right) => right.occurredAt.getTime() - left.occurredAt.getTime());
|
|
741
|
+
return {
|
|
742
|
+
goal: { ...goal, ownerAssignment, plan, activities },
|
|
743
|
+
facet,
|
|
744
|
+
currentGoal: { summary: goal.outcomeStatement ?? goal.title, revision: goal.contractRevision },
|
|
745
|
+
currentProgress: progressSource
|
|
746
|
+
? {
|
|
747
|
+
summary: progressSource.summary,
|
|
748
|
+
sourceActivityId: progressSource.sourceActivityId,
|
|
749
|
+
evidenceRefs: progressSource.evidenceRefs,
|
|
750
|
+
}
|
|
751
|
+
: {
|
|
752
|
+
summary: "No evidence-backed progress has been recorded yet.",
|
|
753
|
+
sourceActivityId: null,
|
|
754
|
+
evidenceRefs: [],
|
|
755
|
+
},
|
|
756
|
+
agentAction: plan ? { summary: plan.summary, sourcePlanId: plan.id, sourceIds: [plan.id] } : null,
|
|
757
|
+
nextStep: goal.continuationKind && goal.continuationSummary
|
|
758
|
+
? {
|
|
759
|
+
kind: goal.continuationKind,
|
|
760
|
+
summary: goal.continuationSummary,
|
|
761
|
+
wakeCondition: goal.wakeCondition,
|
|
762
|
+
}
|
|
763
|
+
: null,
|
|
764
|
+
attention,
|
|
765
|
+
timeline,
|
|
766
|
+
changeProposals: changes,
|
|
767
|
+
resultProposals: results,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
184
770
|
return {
|
|
185
|
-
list: (orgId) => db.select().from(goals).where(eq(goals.orgId, orgId)),
|
|
186
|
-
getById: (id) => db
|
|
187
|
-
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
.then((rows) => rows[0] ?? null),
|
|
191
|
-
getDefaultCompanyGoal: (orgId) => getDefaultCompanyGoal(db, orgId),
|
|
771
|
+
list: (orgId) => db.select().from(goals).where(eq(goals.orgId, orgId)).orderBy(asc(goals.createdAt)),
|
|
772
|
+
getById: (id) => db.select().from(goals).where(eq(goals.id, id)).then((rows) => rows[0] ?? null),
|
|
773
|
+
getChangeProposalById: (id) => db.select().from(goalChangeProposals)
|
|
774
|
+
.where(eq(goalChangeProposals.id, id)).then((rows) => rows[0] ?? null),
|
|
775
|
+
getResultProposalById: (id) => db.select().from(goalResultProposals)
|
|
776
|
+
.where(eq(goalResultProposals.id, id)).then((rows) => rows[0] ?? null),
|
|
192
777
|
dependencies: (goal) => getGoalDependencies(db, goal),
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
778
|
+
previewStart: async (orgId, input) => {
|
|
779
|
+
if (!hasConcreteGoalOutcome(input))
|
|
780
|
+
return compileGoalStartPreview(input, null);
|
|
781
|
+
const owner = input.ownerAgentId
|
|
782
|
+
? await db.select({
|
|
783
|
+
id: agents.id,
|
|
784
|
+
orgId: agents.orgId,
|
|
785
|
+
name: agents.name,
|
|
786
|
+
status: agents.status,
|
|
787
|
+
role: agents.role,
|
|
788
|
+
title: agents.title,
|
|
789
|
+
capabilities: agents.capabilities,
|
|
790
|
+
})
|
|
791
|
+
.from(agents).where(and(eq(agents.id, input.ownerAgentId), eq(agents.orgId, orgId)))
|
|
792
|
+
.then((rows) => rows[0] ?? null)
|
|
793
|
+
: null;
|
|
794
|
+
return compileGoalStartPreview(input, owner);
|
|
795
|
+
},
|
|
796
|
+
start: async (orgId, input) => {
|
|
797
|
+
const computedHash = stableGoalHash(input.packet);
|
|
798
|
+
if (computedHash !== input.packetHash) {
|
|
799
|
+
throw conflict("Goal Start packet hash does not match the submitted packet");
|
|
800
|
+
}
|
|
801
|
+
const replay = await db.select().from(goalStartRequests).where(and(eq(goalStartRequests.orgId, orgId), eq(goalStartRequests.requestKey, input.requestKey))).then((rows) => rows[0] ?? null);
|
|
802
|
+
if (replay) {
|
|
803
|
+
if (replay.packetHash !== input.packetHash) {
|
|
804
|
+
throw conflict("Goal Start request key was reused with a different packet hash");
|
|
805
|
+
}
|
|
806
|
+
if (!sameHashPayload(replay.packet, input.packet)) {
|
|
807
|
+
throw conflict("Goal Start request packet does not match the original request");
|
|
808
|
+
}
|
|
809
|
+
if (replay.status !== "completed" || !replay.goalId) {
|
|
810
|
+
throw conflict("Goal Start request has not completed");
|
|
811
|
+
}
|
|
812
|
+
const goal = await db.select().from(goals).where(and(eq(goals.id, replay.goalId), eq(goals.orgId, orgId))).then((rows) => rows[0] ?? null);
|
|
813
|
+
if (!goal)
|
|
814
|
+
throw conflict("Completed Goal Start request has no Goal");
|
|
815
|
+
return { goal, replayed: true };
|
|
816
|
+
}
|
|
817
|
+
return db.transaction(async (tx) => {
|
|
818
|
+
const database = tx;
|
|
819
|
+
const owner = await requireInvokableOwner(database, orgId, input.packet.ownerAgentId);
|
|
820
|
+
if (!ownerCanAdvanceGoal(owner, {
|
|
821
|
+
title: input.packet.title,
|
|
822
|
+
context: input.packet.description,
|
|
823
|
+
ownerAgentId: input.packet.ownerAgentId,
|
|
824
|
+
targetTime: input.packet.activation.evaluationDeadline ?? null,
|
|
825
|
+
})) {
|
|
826
|
+
throw unprocessable("Goal owner capabilities no longer match the Goal");
|
|
827
|
+
}
|
|
828
|
+
const [insertedRequest] = await database.insert(goalStartRequests).values({
|
|
829
|
+
orgId,
|
|
830
|
+
requestKey: input.requestKey,
|
|
831
|
+
packetHash: input.packetHash,
|
|
832
|
+
packet: input.packet,
|
|
833
|
+
draftGoalId: input.draftGoalId ?? null,
|
|
834
|
+
status: "pending",
|
|
835
|
+
}).onConflictDoNothing().returning();
|
|
836
|
+
if (!insertedRequest) {
|
|
837
|
+
const existing = await database.select().from(goalStartRequests).where(and(eq(goalStartRequests.orgId, orgId), eq(goalStartRequests.requestKey, input.requestKey))).then((rows) => rows[0] ?? null);
|
|
838
|
+
if (!existing || existing.packetHash !== input.packetHash || !sameHashPayload(existing.packet, input.packet)) {
|
|
839
|
+
throw conflict("Goal Start request key was reused with a different packet hash");
|
|
840
|
+
}
|
|
841
|
+
if (existing.status !== "completed" || !existing.goalId) {
|
|
842
|
+
throw conflict("Goal Start request has not completed");
|
|
843
|
+
}
|
|
844
|
+
const existingGoal = await database.select().from(goals).where(and(eq(goals.id, existing.goalId), eq(goals.orgId, orgId))).then((rows) => rows[0] ?? null);
|
|
845
|
+
if (!existingGoal)
|
|
846
|
+
throw conflict("Completed Goal Start request has no Goal");
|
|
847
|
+
return { goal: existingGoal, replayed: true };
|
|
848
|
+
}
|
|
849
|
+
let draft;
|
|
850
|
+
if (input.draftGoalId) {
|
|
851
|
+
draft = await database.select().from(goals).where(and(eq(goals.id, input.draftGoalId), eq(goals.orgId, orgId), eq(goals.lifecycle, "draft"))).then((rows) => rows[0] ?? null);
|
|
852
|
+
if (!draft)
|
|
853
|
+
throw conflict("Reusable Goal Draft was not found in this organization");
|
|
854
|
+
const updatedDraft = await database.update(goals).set({
|
|
855
|
+
title: input.packet.title,
|
|
856
|
+
description: input.packet.description,
|
|
857
|
+
alignmentQuestion: null,
|
|
858
|
+
updatedAt: new Date(),
|
|
859
|
+
}).where(and(eq(goals.id, input.draftGoalId), eq(goals.orgId, orgId), eq(goals.lifecycle, "draft"))).returning().then((rows) => rows[0] ?? null);
|
|
860
|
+
if (!updatedDraft)
|
|
861
|
+
throw conflict("Goal Draft changed before start; reload and retry");
|
|
862
|
+
draft = updatedDraft;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
draft = await database.insert(goals).values({
|
|
866
|
+
orgId,
|
|
867
|
+
title: input.packet.title,
|
|
868
|
+
description: input.packet.description,
|
|
869
|
+
alignmentQuestion: null,
|
|
870
|
+
level: "task",
|
|
871
|
+
status: "planned",
|
|
872
|
+
lifecycle: "draft",
|
|
873
|
+
parentId: null,
|
|
874
|
+
ownerAgentId: null,
|
|
875
|
+
}).returning().then((rows) => rows[0] ?? null);
|
|
876
|
+
}
|
|
877
|
+
if (!draft)
|
|
878
|
+
throw conflict("Goal Draft could not be created");
|
|
879
|
+
const activation = input.packet.activation;
|
|
880
|
+
const [goal] = await database.update(goals).set({
|
|
881
|
+
outcomeStatement: activation.outcomeStatement,
|
|
882
|
+
objectiveMode: activation.objectiveMode,
|
|
883
|
+
lifecycle: "active",
|
|
884
|
+
status: "active",
|
|
885
|
+
contractRevision: 1,
|
|
886
|
+
criteria: activation.criteria,
|
|
887
|
+
autonomyEnvelope: activation.autonomyEnvelope,
|
|
888
|
+
humanAuthorities: activation.humanAuthorities,
|
|
889
|
+
evaluationPolicy: activation.evaluationPolicy,
|
|
890
|
+
actionDeadline: activation.actionDeadline ?? null,
|
|
891
|
+
evaluationDeadline: activation.evaluationDeadline ?? null,
|
|
892
|
+
ownerAgentId: input.packet.ownerAgentId,
|
|
893
|
+
planRevision: 1,
|
|
894
|
+
continuationKind: activation.initialContinuation.kind,
|
|
895
|
+
continuationSummary: activation.initialContinuation.summary,
|
|
896
|
+
wakeCondition: activation.initialContinuation.wakeCondition ?? null,
|
|
897
|
+
alignmentQuestion: null,
|
|
898
|
+
updatedAt: new Date(),
|
|
899
|
+
}).where(and(eq(goals.id, draft.id), eq(goals.orgId, orgId), eq(goals.lifecycle, "draft"))).returning();
|
|
900
|
+
if (!goal)
|
|
901
|
+
throw conflict("Goal changed before start activation; reload and retry");
|
|
902
|
+
await database.insert(goalOwnerAssignments).values({
|
|
903
|
+
orgId,
|
|
904
|
+
goalId: goal.id,
|
|
905
|
+
agentId: input.packet.ownerAgentId,
|
|
906
|
+
assignedByAuthorityRef: `goal_start:${insertedRequest.id}`,
|
|
907
|
+
assignmentRevision: 1,
|
|
908
|
+
});
|
|
909
|
+
await database.insert(goalPlans).values({
|
|
910
|
+
orgId,
|
|
911
|
+
goalId: goal.id,
|
|
912
|
+
revision: 1,
|
|
913
|
+
...activation.initialPlan,
|
|
914
|
+
createdByAgentId: null,
|
|
915
|
+
});
|
|
916
|
+
await database.insert(goalActivities).values({
|
|
917
|
+
orgId,
|
|
918
|
+
goalId: goal.id,
|
|
919
|
+
contractRevision: 1,
|
|
920
|
+
submittedByAgentId: null,
|
|
921
|
+
agentOwnerRefAtTime: input.packet.ownerAgentId,
|
|
922
|
+
activityKind: "progress",
|
|
923
|
+
summary: `Goal activated with initial ${activation.initialContinuation.kind}: ${activation.initialContinuation.summary}`,
|
|
924
|
+
evidenceRefs: [],
|
|
925
|
+
idempotencyKey: `goal-start:${insertedRequest.id}`,
|
|
926
|
+
});
|
|
927
|
+
const completedAt = new Date();
|
|
928
|
+
const completedRequest = await database.update(goalStartRequests).set({
|
|
929
|
+
goalId: goal.id,
|
|
930
|
+
status: "completed",
|
|
931
|
+
completedAt,
|
|
932
|
+
updatedAt: completedAt,
|
|
933
|
+
}).where(and(eq(goalStartRequests.id, insertedRequest.id), eq(goalStartRequests.status, "pending"))).returning().then((rows) => rows[0] ?? null);
|
|
934
|
+
if (!completedRequest)
|
|
935
|
+
throw conflict("Goal Start request could not be completed");
|
|
936
|
+
return { goal, replayed: false };
|
|
937
|
+
});
|
|
938
|
+
},
|
|
939
|
+
workspace: workspaceFor,
|
|
940
|
+
workspaceCards: async (orgId) => {
|
|
941
|
+
const organizationGoals = await db.select().from(goals).where(eq(goals.orgId, orgId))
|
|
942
|
+
.orderBy(asc(goals.createdAt));
|
|
943
|
+
return Promise.all(organizationGoals.map(async (goal) => {
|
|
944
|
+
const [progress, externalCheckpoint, pendingChange, readyResult, owner] = await Promise.all([
|
|
945
|
+
db.select().from(goalActivities).where(and(eq(goalActivities.goalId, goal.id), sql `jsonb_array_length(${goalActivities.evidenceRefs}) > 0`)).orderBy(desc(goalActivities.occurredAt), desc(goalActivities.createdAt)).limit(1)
|
|
946
|
+
.then((rows) => rows[0] ?? null),
|
|
947
|
+
latestExternalCheckpoint(goal),
|
|
948
|
+
db.select().from(goalChangeProposals).where(and(eq(goalChangeProposals.goalId, goal.id), eq(goalChangeProposals.status, "pending"))).orderBy(asc(goalChangeProposals.createdAt)).limit(1).then((rows) => rows[0] ?? null),
|
|
949
|
+
db.select().from(goalResultProposals).where(and(eq(goalResultProposals.goalId, goal.id), eq(goalResultProposals.status, "ready"))).orderBy(asc(goalResultProposals.createdAt)).limit(1).then((rows) => rows[0] ?? null),
|
|
950
|
+
goal.ownerAgentId
|
|
951
|
+
? db.select({ name: agents.name }).from(agents).where(and(eq(agents.id, goal.ownerAgentId), eq(agents.orgId, orgId))).then((rows) => rows[0] ?? null)
|
|
952
|
+
: Promise.resolve(null),
|
|
953
|
+
]);
|
|
954
|
+
const currentProgress = [
|
|
955
|
+
progress ? { summary: progress.summary, occurredAt: progress.occurredAt } : null,
|
|
956
|
+
externalCheckpoint ? { summary: externalCheckpoint.summary, occurredAt: externalCheckpoint.occurredAt } : null,
|
|
957
|
+
].filter((value) => Boolean(value))
|
|
958
|
+
.sort((left, right) => right.occurredAt.getTime() - left.occurredAt.getTime())[0]?.summary
|
|
959
|
+
?? "No evidence-backed progress has been recorded yet.";
|
|
960
|
+
const attentionReason = readyResult
|
|
961
|
+
? `Review the proposed Goal result: ${readyResult.preflight.outcome}`
|
|
962
|
+
: pendingChange?.rationale ?? (goal.lifecycle === "draft" ? goal.alignmentQuestion : null);
|
|
963
|
+
return {
|
|
964
|
+
id: goal.id,
|
|
965
|
+
orgId: goal.orgId,
|
|
966
|
+
title: goal.title,
|
|
967
|
+
lifecycle: goal.lifecycle,
|
|
968
|
+
status: goal.status,
|
|
969
|
+
facet: facetFor(goal, pendingChange, readyResult),
|
|
970
|
+
ownerAgentId: goal.ownerAgentId,
|
|
971
|
+
ownerName: owner?.name ?? null,
|
|
972
|
+
currentProgress,
|
|
973
|
+
progressSummary: currentProgress,
|
|
974
|
+
nextAction: goal.continuationSummary,
|
|
975
|
+
nextStepSummary: goal.continuationSummary ?? "No next step has been recorded.",
|
|
976
|
+
targetTime: goal.evaluationDeadline ?? goal.actionDeadline,
|
|
977
|
+
attentionReason,
|
|
978
|
+
focus: goal.focus,
|
|
979
|
+
updatedAt: goal.updatedAt,
|
|
980
|
+
};
|
|
981
|
+
}));
|
|
982
|
+
},
|
|
983
|
+
detail: async (id) => {
|
|
984
|
+
const goal = await requireGoal(db, id);
|
|
985
|
+
const [ownerAssignment, plan, activities] = await Promise.all([
|
|
986
|
+
db.select().from(goalOwnerAssignments).where(and(eq(goalOwnerAssignments.goalId, id), isNull(goalOwnerAssignments.endsAt)))
|
|
987
|
+
.then((rows) => rows[0] ?? null),
|
|
988
|
+
goal.planRevision > 0
|
|
989
|
+
? db.select().from(goalPlans).where(and(eq(goalPlans.goalId, id), eq(goalPlans.revision, goal.planRevision))).then((rows) => rows[0] ?? null)
|
|
990
|
+
: Promise.resolve(null),
|
|
991
|
+
db.select().from(goalActivities).where(eq(goalActivities.goalId, id)).orderBy(desc(goalActivities.occurredAt), desc(goalActivities.createdAt)).limit(100),
|
|
992
|
+
]);
|
|
993
|
+
return { ...goal, ownerAssignment, plan, activities };
|
|
994
|
+
},
|
|
995
|
+
create: async (orgId, data) => db.insert(goals).values({
|
|
996
|
+
orgId,
|
|
997
|
+
title: data.title,
|
|
998
|
+
description: data.description ?? null,
|
|
999
|
+
alignmentQuestion: data.alignmentQuestion ?? null,
|
|
1000
|
+
level: "task",
|
|
1001
|
+
status: "planned",
|
|
1002
|
+
lifecycle: "draft",
|
|
1003
|
+
parentId: null,
|
|
1004
|
+
ownerAgentId: null,
|
|
1005
|
+
}).returning().then((rows) => rows[0]),
|
|
1006
|
+
update: async (id, data, actorAgentId = null) => {
|
|
1007
|
+
const current = await requireGoal(db, id);
|
|
1008
|
+
assertGoalOwner(current, actorAgentId);
|
|
1009
|
+
const { title, description, alignmentQuestion } = data;
|
|
1010
|
+
return db.update(goals).set({ title, description, alignmentQuestion, updatedAt: new Date() })
|
|
1011
|
+
.where(eq(goals.id, id)).returning().then((rows) => rows[0] ?? null);
|
|
1012
|
+
},
|
|
1013
|
+
activate: async (id, input, actorAgentId = null) => {
|
|
1014
|
+
const current = await requireGoal(db, id);
|
|
1015
|
+
if (current.lifecycle !== "draft")
|
|
1016
|
+
throw conflict("Only draft Goals can be activated");
|
|
1017
|
+
if (actorAgentId && actorAgentId !== input.ownerAgentId) {
|
|
1018
|
+
throw forbidden("An Agent can only activate a Goal for itself as Owner");
|
|
1019
|
+
}
|
|
1020
|
+
await requireInvokableOwner(db, current.orgId, input.ownerAgentId);
|
|
1021
|
+
const result = await db.transaction(async (tx) => {
|
|
1022
|
+
const [goal] = await tx.update(goals).set({
|
|
1023
|
+
outcomeStatement: input.outcomeStatement,
|
|
1024
|
+
objectiveMode: input.objectiveMode,
|
|
1025
|
+
lifecycle: "active",
|
|
1026
|
+
status: "active",
|
|
1027
|
+
contractRevision: 1,
|
|
1028
|
+
criteria: input.criteria,
|
|
1029
|
+
autonomyEnvelope: input.autonomyEnvelope,
|
|
1030
|
+
humanAuthorities: input.humanAuthorities,
|
|
1031
|
+
evaluationPolicy: input.evaluationPolicy,
|
|
1032
|
+
actionDeadline: input.actionDeadline ?? null,
|
|
1033
|
+
evaluationDeadline: input.evaluationDeadline ?? null,
|
|
1034
|
+
ownerAgentId: input.ownerAgentId,
|
|
1035
|
+
planRevision: 1,
|
|
1036
|
+
continuationKind: input.initialContinuation.kind,
|
|
1037
|
+
continuationSummary: input.initialContinuation.summary,
|
|
1038
|
+
wakeCondition: input.initialContinuation.wakeCondition ?? null,
|
|
1039
|
+
alignmentQuestion: null,
|
|
1040
|
+
updatedAt: new Date(),
|
|
1041
|
+
}).where(and(eq(goals.id, id), eq(goals.lifecycle, "draft"))).returning();
|
|
1042
|
+
if (!goal)
|
|
1043
|
+
throw conflict("Goal changed before activation; reload and retry");
|
|
1044
|
+
await tx.insert(goalOwnerAssignments).values({
|
|
1045
|
+
orgId: current.orgId,
|
|
1046
|
+
goalId: id,
|
|
1047
|
+
agentId: input.ownerAgentId,
|
|
1048
|
+
assignedByAuthorityRef: "activation",
|
|
1049
|
+
assignmentRevision: 1,
|
|
1050
|
+
});
|
|
1051
|
+
await tx.insert(goalPlans).values({
|
|
1052
|
+
orgId: current.orgId,
|
|
1053
|
+
goalId: id,
|
|
1054
|
+
revision: 1,
|
|
1055
|
+
...input.initialPlan,
|
|
1056
|
+
createdByAgentId: actorAgentId,
|
|
1057
|
+
});
|
|
1058
|
+
await tx.insert(goalActivities).values({
|
|
1059
|
+
orgId: current.orgId,
|
|
1060
|
+
goalId: id,
|
|
1061
|
+
contractRevision: 1,
|
|
1062
|
+
submittedByAgentId: actorAgentId,
|
|
1063
|
+
agentOwnerRefAtTime: input.ownerAgentId,
|
|
1064
|
+
activityKind: "progress",
|
|
1065
|
+
summary: `Goal activated with initial ${input.initialContinuation.kind}: ${input.initialContinuation.summary}`,
|
|
1066
|
+
evidenceRefs: [],
|
|
1067
|
+
});
|
|
1068
|
+
return goal;
|
|
1069
|
+
});
|
|
1070
|
+
return result;
|
|
1071
|
+
},
|
|
1072
|
+
updatePlan: async (id, input, actorAgentId = null) => {
|
|
1073
|
+
const current = await requireGoal(db, id);
|
|
1074
|
+
assertCanonicalActiveGoal(current);
|
|
1075
|
+
assertGoalOwner(current, actorAgentId);
|
|
1076
|
+
const revision = current.planRevision + 1;
|
|
1077
|
+
return db.transaction(async (tx) => {
|
|
1078
|
+
const [plan] = await tx.insert(goalPlans).values({
|
|
1079
|
+
orgId: current.orgId,
|
|
1080
|
+
goalId: id,
|
|
1081
|
+
revision,
|
|
1082
|
+
...input,
|
|
1083
|
+
createdByAgentId: actorAgentId,
|
|
1084
|
+
}).returning();
|
|
1085
|
+
await tx.update(goals).set({ planRevision: revision, updatedAt: new Date() }).where(eq(goals.id, id));
|
|
1086
|
+
return plan;
|
|
1087
|
+
});
|
|
1088
|
+
},
|
|
1089
|
+
listActivities: (id) => db.select().from(goalActivities).where(eq(goalActivities.goalId, id))
|
|
1090
|
+
.orderBy(desc(goalActivities.occurredAt), desc(goalActivities.createdAt)).limit(100),
|
|
1091
|
+
createActivity: async (id, input, actorAgentId = null) => {
|
|
1092
|
+
const current = await requireGoal(db, id);
|
|
1093
|
+
assertCanonicalActiveGoal(current);
|
|
1094
|
+
assertGoalOwner(current, actorAgentId);
|
|
1095
|
+
if (input.activityKind === "closeout" && !input.runRef) {
|
|
1096
|
+
throw unprocessable("A closeout Activity requires a Run reference");
|
|
1097
|
+
}
|
|
1098
|
+
if (input.runRef) {
|
|
1099
|
+
const run = await db.select({ id: heartbeatRuns.id, orgId: heartbeatRuns.orgId, status: heartbeatRuns.status })
|
|
1100
|
+
.from(heartbeatRuns).where(eq(heartbeatRuns.id, input.runRef)).then((rows) => rows[0] ?? null);
|
|
1101
|
+
if (!run || run.orgId !== current.orgId)
|
|
1102
|
+
throw unprocessable("Goal Activity run must belong to the same organization");
|
|
1103
|
+
if (input.activityKind === "closeout" && !TERMINAL_RUN_STATUSES.includes(run.status)) {
|
|
1104
|
+
throw conflict("A closeout Activity requires a terminal Run");
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
const [activity] = await db.insert(goalActivities).values({
|
|
1108
|
+
orgId: current.orgId,
|
|
1109
|
+
goalId: id,
|
|
1110
|
+
contractRevision: current.contractRevision,
|
|
1111
|
+
submittedByAgentId: actorAgentId,
|
|
1112
|
+
agentOwnerRefAtTime: current.ownerAgentId,
|
|
1113
|
+
activityKind: input.activityKind ?? "progress",
|
|
1114
|
+
commitmentRef: input.commitmentRef ?? null,
|
|
1115
|
+
runRef: input.runRef ?? null,
|
|
1116
|
+
summary: input.summary,
|
|
1117
|
+
evidenceRefs: input.evidenceRefs,
|
|
1118
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
1119
|
+
occurredAt: input.occurredAt ?? new Date(),
|
|
1120
|
+
}).onConflictDoNothing().returning();
|
|
1121
|
+
if (activity)
|
|
1122
|
+
return activity;
|
|
1123
|
+
if (input.idempotencyKey) {
|
|
1124
|
+
const existingIdempotent = await db.select().from(goalActivities).where(and(eq(goalActivities.goalId, id), eq(goalActivities.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
1125
|
+
if (existingIdempotent)
|
|
1126
|
+
return existingIdempotent;
|
|
1127
|
+
}
|
|
1128
|
+
if (input.activityKind === "closeout" && input.runRef) {
|
|
1129
|
+
const existingCloseout = await db.select().from(goalActivities).where(and(eq(goalActivities.goalId, id), eq(goalActivities.runRef, input.runRef), eq(goalActivities.activityKind, "closeout"))).then((rows) => rows[0] ?? null);
|
|
1130
|
+
if (existingCloseout)
|
|
1131
|
+
throw conflict("A closeout Activity already exists for this Run");
|
|
1132
|
+
}
|
|
1133
|
+
throw conflict("Goal Activity was already recorded");
|
|
1134
|
+
},
|
|
1135
|
+
feedback: async (id, input, actorUserId) => {
|
|
1136
|
+
return db.transaction(async (tx) => {
|
|
1137
|
+
const database = tx;
|
|
1138
|
+
const current = await requireGoal(database, id);
|
|
1139
|
+
return recordFeedback(database, current, input, actorUserId);
|
|
1140
|
+
});
|
|
1141
|
+
},
|
|
1142
|
+
createChangeProposal: async (id, input, actorAgentId) => {
|
|
1143
|
+
if (!actorAgentId)
|
|
1144
|
+
throw forbidden("Only the Goal Owner Agent can propose a consequential change");
|
|
1145
|
+
const normalizedPatch = normalizeContractPatch(input.afterContract);
|
|
1146
|
+
return db.transaction(async (tx) => {
|
|
1147
|
+
const database = tx;
|
|
1148
|
+
const current = await requireGoal(database, id);
|
|
1149
|
+
assertCanonicalActiveGoal(current);
|
|
1150
|
+
assertGoalOwner(current, actorAgentId);
|
|
1151
|
+
const existing = await database.select().from(goalChangeProposals).where(and(eq(goalChangeProposals.goalId, id), eq(goalChangeProposals.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
1152
|
+
if (existing) {
|
|
1153
|
+
assertSameChangeProposalPayload(existing, input, normalizedPatch);
|
|
1154
|
+
return existing;
|
|
1155
|
+
}
|
|
1156
|
+
if (current.contractRevision !== input.expectedContractRevision) {
|
|
1157
|
+
throw conflict("Goal Contract revision changed before proposal creation", {
|
|
1158
|
+
expectedContractRevision: input.expectedContractRevision,
|
|
1159
|
+
currentContractRevision: current.contractRevision,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
const beforeContract = goalContractSnapshot(current);
|
|
1163
|
+
const mergedContract = { ...beforeContract, ...normalizedPatch };
|
|
1164
|
+
activateGoalSchema.parse({
|
|
1165
|
+
confirmed: true,
|
|
1166
|
+
ownerAgentId: current.ownerAgentId,
|
|
1167
|
+
outcomeStatement: mergedContract.outcomeStatement,
|
|
1168
|
+
objectiveMode: mergedContract.objectiveMode,
|
|
1169
|
+
criteria: mergedContract.criteria,
|
|
1170
|
+
autonomyEnvelope: mergedContract.autonomyEnvelope,
|
|
1171
|
+
humanAuthorities: mergedContract.humanAuthorities,
|
|
1172
|
+
evaluationPolicy: mergedContract.evaluationPolicy,
|
|
1173
|
+
actionDeadline: mergedContract.actionDeadline,
|
|
1174
|
+
evaluationDeadline: mergedContract.evaluationDeadline,
|
|
1175
|
+
initialPlan: { summary: "Validate the proposed Contract revision" },
|
|
1176
|
+
initialContinuation: {
|
|
1177
|
+
kind: current.continuationKind,
|
|
1178
|
+
summary: current.continuationSummary,
|
|
1179
|
+
wakeCondition: current.wakeCondition,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
const proposalId = randomUUID();
|
|
1183
|
+
const approvalId = input.approvalId ?? randomUUID();
|
|
1184
|
+
if (input.approvalId) {
|
|
1185
|
+
const linkedApproval = await database.select().from(approvals).where(eq(approvals.id, approvalId))
|
|
1186
|
+
.then((rows) => rows[0] ?? null);
|
|
1187
|
+
if (!linkedApproval
|
|
1188
|
+
|| linkedApproval.orgId !== current.orgId
|
|
1189
|
+
|| linkedApproval.type !== "goal_change"
|
|
1190
|
+
|| linkedApproval.status !== "pending") {
|
|
1191
|
+
throw unprocessable("Goal change approval must be pending and belong to the same organization");
|
|
1192
|
+
}
|
|
1193
|
+
await database.update(approvals).set({
|
|
1194
|
+
payload: {
|
|
1195
|
+
...linkedApproval.payload,
|
|
1196
|
+
goalId: id,
|
|
1197
|
+
proposalId,
|
|
1198
|
+
expectedContractRevision: current.contractRevision,
|
|
1199
|
+
},
|
|
1200
|
+
updatedAt: new Date(),
|
|
1201
|
+
}).where(eq(approvals.id, approvalId));
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
await database.insert(approvals).values({
|
|
1205
|
+
id: approvalId,
|
|
1206
|
+
orgId: current.orgId,
|
|
1207
|
+
type: "goal_change",
|
|
1208
|
+
requestedByAgentId: actorAgentId,
|
|
1209
|
+
requestedByUserId: null,
|
|
1210
|
+
status: "pending",
|
|
1211
|
+
payload: {
|
|
1212
|
+
goalId: id,
|
|
1213
|
+
proposalId,
|
|
1214
|
+
expectedContractRevision: current.contractRevision,
|
|
1215
|
+
beforeContract,
|
|
1216
|
+
afterContract: normalizedPatch,
|
|
1217
|
+
rationale: input.rationale,
|
|
1218
|
+
evidenceRefs: input.evidenceRefs,
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
const [proposal] = await database.insert(goalChangeProposals).values({
|
|
1223
|
+
id: proposalId,
|
|
1224
|
+
orgId: current.orgId,
|
|
1225
|
+
goalId: id,
|
|
1226
|
+
expectedContractRevision: current.contractRevision,
|
|
1227
|
+
beforeContract,
|
|
1228
|
+
afterContract: normalizedPatch,
|
|
1229
|
+
rationale: input.rationale,
|
|
1230
|
+
evidenceRefs: input.evidenceRefs,
|
|
1231
|
+
approvalId,
|
|
1232
|
+
status: "pending",
|
|
1233
|
+
idempotencyKey: input.idempotencyKey,
|
|
1234
|
+
proposedByAgentId: actorAgentId,
|
|
1235
|
+
}).onConflictDoNothing().returning();
|
|
1236
|
+
if (proposal)
|
|
1237
|
+
return proposal;
|
|
1238
|
+
const raced = await database.select().from(goalChangeProposals).where(and(eq(goalChangeProposals.goalId, id), eq(goalChangeProposals.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
1239
|
+
if (!raced)
|
|
1240
|
+
throw conflict("Goal change proposal could not be recorded");
|
|
1241
|
+
assertSameChangeProposalPayload(raced, input, normalizedPatch);
|
|
1242
|
+
return raced;
|
|
1243
|
+
});
|
|
1244
|
+
},
|
|
1245
|
+
decideChangeProposal: async (proposalId, input, actorUserId) => {
|
|
1246
|
+
const result = await db.transaction(async (tx) => {
|
|
1247
|
+
const database = tx;
|
|
1248
|
+
const proposal = await database.select().from(goalChangeProposals)
|
|
1249
|
+
.where(eq(goalChangeProposals.id, proposalId)).then((rows) => rows[0] ?? null);
|
|
1250
|
+
if (!proposal)
|
|
1251
|
+
throw notFound("Goal change proposal not found");
|
|
1252
|
+
if (input.decision === "approve" && proposal.status === "applied")
|
|
1253
|
+
return { proposal, stale: false };
|
|
1254
|
+
if (input.decision === "reject" && proposal.status === "rejected")
|
|
1255
|
+
return { proposal, stale: false };
|
|
1256
|
+
if (proposal.status !== "pending") {
|
|
1257
|
+
throw conflict(`Goal change proposal is already ${proposal.status}`);
|
|
1258
|
+
}
|
|
1259
|
+
const current = await requireGoal(database, proposal.goalId);
|
|
1260
|
+
assertCanonicalActiveGoal(current);
|
|
1261
|
+
if (current.orgId !== proposal.orgId)
|
|
1262
|
+
throw unprocessable("Goal change proposal organization mismatch");
|
|
1263
|
+
const approval = await database.select().from(approvals).where(and(eq(approvals.id, proposal.approvalId), eq(approvals.orgId, proposal.orgId))).then((rows) => rows[0] ?? null);
|
|
1264
|
+
if (!approval || approval.type !== "goal_change") {
|
|
1265
|
+
throw unprocessable("Goal change proposal approval is invalid");
|
|
1266
|
+
}
|
|
1267
|
+
if (input.decision === "reject") {
|
|
1268
|
+
const now = new Date();
|
|
1269
|
+
await database.update(approvals).set({
|
|
1270
|
+
status: "rejected",
|
|
1271
|
+
decisionNote: input.note ?? null,
|
|
1272
|
+
decidedByUserId: actorUserId,
|
|
1273
|
+
decidedAt: now,
|
|
1274
|
+
updatedAt: now,
|
|
1275
|
+
}).where(and(eq(approvals.id, approval.id), eq(approvals.status, "pending")));
|
|
1276
|
+
const rejected = await database.update(goalChangeProposals).set({
|
|
1277
|
+
status: "rejected",
|
|
1278
|
+
updatedAt: now,
|
|
1279
|
+
}).where(and(eq(goalChangeProposals.id, proposal.id), eq(goalChangeProposals.status, "pending"))).returning().then((rows) => rows[0] ?? null);
|
|
1280
|
+
if (!rejected)
|
|
1281
|
+
throw conflict("Goal change proposal changed before rejection");
|
|
1282
|
+
await database.insert(goalActivities).values({
|
|
1283
|
+
orgId: current.orgId,
|
|
1284
|
+
goalId: current.id,
|
|
1285
|
+
contractRevision: current.contractRevision,
|
|
1286
|
+
submittedByAgentId: null,
|
|
1287
|
+
agentOwnerRefAtTime: current.ownerAgentId,
|
|
1288
|
+
activityKind: "decision_requested",
|
|
1289
|
+
summary: `Goal change proposal rejected${input.note ? `: ${input.note}` : ""}`,
|
|
1290
|
+
evidenceRefs: proposal.evidenceRefs,
|
|
1291
|
+
idempotencyKey: `goal-change:${proposal.id}:rejected`,
|
|
1292
|
+
}).onConflictDoNothing();
|
|
1293
|
+
return { proposal: rejected, stale: false };
|
|
1294
|
+
}
|
|
1295
|
+
if (current.contractRevision !== proposal.expectedContractRevision) {
|
|
1296
|
+
const now = new Date();
|
|
1297
|
+
await database.update(approvals).set({
|
|
1298
|
+
status: "approved",
|
|
1299
|
+
decisionNote: input.note ?? null,
|
|
1300
|
+
decidedByUserId: actorUserId,
|
|
1301
|
+
decidedAt: now,
|
|
1302
|
+
updatedAt: now,
|
|
1303
|
+
}).where(and(eq(approvals.id, approval.id), eq(approvals.status, "pending")));
|
|
1304
|
+
const superseded = await database.update(goalChangeProposals).set({
|
|
1305
|
+
status: "superseded",
|
|
1306
|
+
updatedAt: now,
|
|
1307
|
+
}).where(and(eq(goalChangeProposals.id, proposal.id), eq(goalChangeProposals.status, "pending"))).returning().then((rows) => rows[0] ?? proposal);
|
|
1308
|
+
return { proposal: superseded, stale: true, currentRevision: current.contractRevision };
|
|
1309
|
+
}
|
|
1310
|
+
const merged = { ...proposal.beforeContract, ...proposal.afterContract };
|
|
1311
|
+
const validated = activateGoalSchema.parse({
|
|
1312
|
+
confirmed: true,
|
|
1313
|
+
ownerAgentId: current.ownerAgentId,
|
|
1314
|
+
outcomeStatement: merged.outcomeStatement,
|
|
1315
|
+
objectiveMode: merged.objectiveMode,
|
|
1316
|
+
criteria: merged.criteria,
|
|
1317
|
+
autonomyEnvelope: merged.autonomyEnvelope,
|
|
1318
|
+
humanAuthorities: merged.humanAuthorities,
|
|
1319
|
+
evaluationPolicy: merged.evaluationPolicy,
|
|
1320
|
+
actionDeadline: merged.actionDeadline,
|
|
1321
|
+
evaluationDeadline: merged.evaluationDeadline,
|
|
1322
|
+
initialPlan: { summary: "Apply the approved Contract revision" },
|
|
1323
|
+
initialContinuation: {
|
|
1324
|
+
kind: current.continuationKind,
|
|
1325
|
+
summary: current.continuationSummary,
|
|
1326
|
+
wakeCondition: current.wakeCondition,
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
const now = new Date();
|
|
1330
|
+
const approvalUpdate = await database.update(approvals).set({
|
|
1331
|
+
status: "approved",
|
|
1332
|
+
decisionNote: input.note ?? null,
|
|
1333
|
+
decidedByUserId: actorUserId,
|
|
1334
|
+
decidedAt: now,
|
|
1335
|
+
updatedAt: now,
|
|
1336
|
+
}).where(and(eq(approvals.id, approval.id), eq(approvals.status, "pending")))
|
|
1337
|
+
.returning().then((rows) => rows[0] ?? null);
|
|
1338
|
+
if (!approvalUpdate)
|
|
1339
|
+
throw conflict("Goal change approval changed before decision");
|
|
1340
|
+
const nextRevision = current.contractRevision + 1;
|
|
1341
|
+
const changedGoal = await database.update(goals).set({
|
|
1342
|
+
outcomeStatement: validated.outcomeStatement,
|
|
1343
|
+
objectiveMode: validated.objectiveMode,
|
|
1344
|
+
criteria: validated.criteria,
|
|
1345
|
+
autonomyEnvelope: validated.autonomyEnvelope,
|
|
1346
|
+
humanAuthorities: validated.humanAuthorities,
|
|
1347
|
+
evaluationPolicy: validated.evaluationPolicy,
|
|
1348
|
+
actionDeadline: validated.actionDeadline ?? null,
|
|
1349
|
+
evaluationDeadline: validated.evaluationDeadline ?? null,
|
|
1350
|
+
contractRevision: nextRevision,
|
|
1351
|
+
updatedAt: now,
|
|
1352
|
+
}).where(and(eq(goals.id, current.id), eq(goals.orgId, proposal.orgId), eq(goals.lifecycle, "active"), eq(goals.contractRevision, proposal.expectedContractRevision))).returning().then((rows) => rows[0] ?? null);
|
|
1353
|
+
if (!changedGoal)
|
|
1354
|
+
throw conflict("Goal Contract revision changed before proposal application");
|
|
1355
|
+
await database.insert(goalActivities).values({
|
|
1356
|
+
orgId: current.orgId,
|
|
1357
|
+
goalId: current.id,
|
|
1358
|
+
contractRevision: nextRevision,
|
|
1359
|
+
submittedByAgentId: proposal.proposedByAgentId,
|
|
1360
|
+
agentOwnerRefAtTime: current.ownerAgentId,
|
|
1361
|
+
activityKind: "decision_requested",
|
|
1362
|
+
summary: `Applied Goal Contract revision ${nextRevision}: ${proposal.rationale}`,
|
|
1363
|
+
evidenceRefs: proposal.evidenceRefs,
|
|
1364
|
+
idempotencyKey: `goal-change:${proposal.id}:applied`,
|
|
1365
|
+
}).onConflictDoNothing();
|
|
1366
|
+
const applied = await database.update(goalChangeProposals).set({
|
|
1367
|
+
status: "applied",
|
|
1368
|
+
appliedRevision: nextRevision,
|
|
1369
|
+
appliedAt: now,
|
|
1370
|
+
updatedAt: now,
|
|
1371
|
+
}).where(and(eq(goalChangeProposals.id, proposal.id), eq(goalChangeProposals.status, "pending"))).returning().then((rows) => rows[0] ?? null);
|
|
1372
|
+
if (!applied)
|
|
1373
|
+
throw conflict("Goal change proposal changed before application");
|
|
1374
|
+
return { proposal: applied, stale: false };
|
|
1375
|
+
});
|
|
1376
|
+
if (result.stale) {
|
|
1377
|
+
throw conflict("Goal Contract revision changed before proposal application", {
|
|
1378
|
+
expectedContractRevision: result.proposal.expectedContractRevision,
|
|
1379
|
+
currentContractRevision: result.currentRevision,
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
return result.proposal;
|
|
1383
|
+
},
|
|
1384
|
+
createResultProposal: async (id, input, actorAgentId) => {
|
|
1385
|
+
if (!actorAgentId)
|
|
1386
|
+
throw forbidden("Only the Goal Owner Agent can submit a Result Proposal");
|
|
1387
|
+
const candidate = evaluationCandidate(input);
|
|
1388
|
+
const candidateHash = stableGoalHash(candidate);
|
|
1389
|
+
return db.transaction(async (tx) => {
|
|
1390
|
+
const database = tx;
|
|
1391
|
+
const current = await requireGoal(database, id);
|
|
1392
|
+
const existing = await database.select().from(goalResultProposals).where(and(eq(goalResultProposals.goalId, id), eq(goalResultProposals.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
1393
|
+
if (existing) {
|
|
1394
|
+
assertSameResultProposalPayload(existing, input, candidateHash);
|
|
1395
|
+
return existing;
|
|
1396
|
+
}
|
|
1397
|
+
assertCanonicalActiveGoal(current);
|
|
1398
|
+
assertGoalOwner(current, actorAgentId);
|
|
1399
|
+
if (input.contractRevision !== current.contractRevision) {
|
|
1400
|
+
throw conflict("Goal Contract revision changed before Result Proposal creation", {
|
|
1401
|
+
expectedContractRevision: input.contractRevision,
|
|
1402
|
+
currentContractRevision: current.contractRevision,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
const preflight = reduceGoalEvaluation(current, candidate);
|
|
1406
|
+
const status = isTerminalEvaluation(preflight.outcome) ? "ready" : "inconclusive";
|
|
1407
|
+
const [proposal] = await database.insert(goalResultProposals).values({
|
|
1408
|
+
orgId: current.orgId,
|
|
1409
|
+
goalId: current.id,
|
|
1410
|
+
contractRevision: current.contractRevision,
|
|
1411
|
+
candidate,
|
|
1412
|
+
candidateHash,
|
|
1413
|
+
preflight,
|
|
1414
|
+
riskSummary: input.riskSummary,
|
|
1415
|
+
status,
|
|
1416
|
+
idempotencyKey: input.idempotencyKey,
|
|
1417
|
+
proposedByAgentId: actorAgentId,
|
|
1418
|
+
}).onConflictDoNothing().returning();
|
|
1419
|
+
const persisted = proposal ?? await database.select().from(goalResultProposals).where(and(eq(goalResultProposals.goalId, id), eq(goalResultProposals.idempotencyKey, input.idempotencyKey))).then((rows) => rows[0] ?? null);
|
|
1420
|
+
if (!persisted)
|
|
1421
|
+
throw conflict("Goal Result Proposal could not be recorded");
|
|
1422
|
+
assertSameResultProposalPayload(persisted, input, candidateHash);
|
|
1423
|
+
if (status === "inconclusive") {
|
|
1424
|
+
await database.insert(goalActivities).values({
|
|
1425
|
+
orgId: current.orgId,
|
|
1426
|
+
goalId: current.id,
|
|
1427
|
+
contractRevision: current.contractRevision,
|
|
1428
|
+
submittedByAgentId: actorAgentId,
|
|
1429
|
+
agentOwnerRefAtTime: current.ownerAgentId,
|
|
1430
|
+
activityKind: "bottleneck",
|
|
1431
|
+
summary: inconclusiveResultSummary(preflight, input.riskSummary),
|
|
1432
|
+
evidenceRefs: candidate.evidenceRefs,
|
|
1433
|
+
idempotencyKey: `goal-result-inconclusive:${input.idempotencyKey}`,
|
|
1434
|
+
}).onConflictDoNothing();
|
|
1435
|
+
return persisted;
|
|
1436
|
+
}
|
|
1437
|
+
return persisted;
|
|
1438
|
+
});
|
|
1439
|
+
},
|
|
1440
|
+
acceptResultProposal: async (proposalId, input, actorUserId) => {
|
|
1441
|
+
return db.transaction(async (tx) => {
|
|
1442
|
+
const database = tx;
|
|
1443
|
+
const proposal = await database.select().from(goalResultProposals)
|
|
1444
|
+
.where(eq(goalResultProposals.id, proposalId)).then((rows) => rows[0] ?? null);
|
|
1445
|
+
if (!proposal)
|
|
1446
|
+
throw notFound("Goal Result Proposal not found");
|
|
1447
|
+
const current = await requireGoal(database, proposal.goalId);
|
|
1448
|
+
if (current.orgId !== proposal.orgId)
|
|
1449
|
+
throw unprocessable("Goal Result Proposal organization mismatch");
|
|
1450
|
+
if (proposal.consumedAt) {
|
|
1451
|
+
if (proposal.acceptanceIdempotencyKey !== input.idempotencyKey) {
|
|
1452
|
+
throw conflict("Goal Result Proposal acceptance was replayed with a different idempotency key");
|
|
1453
|
+
}
|
|
1454
|
+
return current;
|
|
1455
|
+
}
|
|
1456
|
+
if (proposal.status !== "ready") {
|
|
1457
|
+
throw conflict(`Goal Result Proposal is already ${proposal.status}`);
|
|
1458
|
+
}
|
|
1459
|
+
assertCanonicalActiveGoal(current);
|
|
1460
|
+
if (proposal.contractRevision !== current.contractRevision) {
|
|
1461
|
+
throw conflict("Goal Contract revision changed before Result Proposal acceptance", {
|
|
1462
|
+
expectedContractRevision: proposal.contractRevision,
|
|
1463
|
+
currentContractRevision: current.contractRevision,
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
if (proposal.candidateHash !== stableGoalHash(proposal.candidate)) {
|
|
1467
|
+
throw conflict("Goal Result Proposal candidate no longer matches its immutable hash");
|
|
1468
|
+
}
|
|
1469
|
+
const freshPreflight = reduceGoalEvaluation(current, proposal.candidate);
|
|
1470
|
+
if (!isTerminalEvaluation(freshPreflight.outcome)
|
|
1471
|
+
|| !sameHashPayload({ ...proposal.preflight, evaluatedAt: null }, { ...freshPreflight, evaluatedAt: null })) {
|
|
1472
|
+
throw conflict("Goal Result Proposal no longer matches canonical evaluation");
|
|
1473
|
+
}
|
|
1474
|
+
const now = new Date();
|
|
1475
|
+
const accepted = await database.update(goalResultProposals).set({
|
|
1476
|
+
status: "accepted",
|
|
1477
|
+
acceptedByActorType: "user",
|
|
1478
|
+
acceptedByActorId: actorUserId,
|
|
1479
|
+
acceptanceIdempotencyKey: input.idempotencyKey,
|
|
1480
|
+
acceptedAt: now,
|
|
1481
|
+
updatedAt: now,
|
|
1482
|
+
}).where(and(eq(goalResultProposals.id, proposal.id), eq(goalResultProposals.status, "ready"), isNull(goalResultProposals.consumedAt))).returning().then((rows) => rows[0] ?? null);
|
|
1483
|
+
if (!accepted) {
|
|
1484
|
+
const latestProposal = await database.select().from(goalResultProposals)
|
|
1485
|
+
.where(eq(goalResultProposals.id, proposal.id)).then((rows) => rows[0] ?? null);
|
|
1486
|
+
if (latestProposal?.consumedAt && latestProposal.acceptanceIdempotencyKey === input.idempotencyKey) {
|
|
1487
|
+
return requireGoal(database, proposal.goalId);
|
|
1488
|
+
}
|
|
1489
|
+
throw conflict("Goal Result Proposal changed before acceptance");
|
|
1490
|
+
}
|
|
1491
|
+
return evaluateInTransaction(database, current, {
|
|
1492
|
+
...proposal.candidate,
|
|
1493
|
+
resultProposalId: proposal.id,
|
|
1494
|
+
}, null);
|
|
1495
|
+
});
|
|
1496
|
+
},
|
|
1497
|
+
rejectResultProposal: async (proposalId, input, actorUserId) => {
|
|
1498
|
+
return db.transaction(async (tx) => {
|
|
1499
|
+
const database = tx;
|
|
1500
|
+
const proposal = await database.select().from(goalResultProposals)
|
|
1501
|
+
.where(eq(goalResultProposals.id, proposalId)).then((rows) => rows[0] ?? null);
|
|
1502
|
+
if (!proposal)
|
|
1503
|
+
throw notFound("Goal Result Proposal not found");
|
|
1504
|
+
if (proposal.status === "rejected") {
|
|
1505
|
+
if (proposal.rejectionFeedback !== input.feedback) {
|
|
1506
|
+
throw conflict("Goal Result Proposal rejection was replayed with different feedback");
|
|
1507
|
+
}
|
|
1508
|
+
return proposal;
|
|
1509
|
+
}
|
|
1510
|
+
if (proposal.status !== "ready") {
|
|
1511
|
+
throw conflict(`Goal Result Proposal is already ${proposal.status}`);
|
|
1512
|
+
}
|
|
1513
|
+
const current = await requireGoal(database, proposal.goalId);
|
|
1514
|
+
assertCanonicalActiveGoal(current);
|
|
1515
|
+
if (current.orgId !== proposal.orgId)
|
|
1516
|
+
throw unprocessable("Goal Result Proposal organization mismatch");
|
|
1517
|
+
if (current.contractRevision !== proposal.contractRevision) {
|
|
1518
|
+
throw conflict("Goal Contract revision changed before Result Proposal rejection");
|
|
1519
|
+
}
|
|
1520
|
+
await recordFeedback(database, current, {
|
|
1521
|
+
body: input.feedback,
|
|
1522
|
+
attachments: [],
|
|
1523
|
+
feedbackKind: "ordinary",
|
|
1524
|
+
idempotencyKey: input.idempotencyKey,
|
|
1525
|
+
}, actorUserId);
|
|
1526
|
+
const now = new Date();
|
|
1527
|
+
const rejected = await database.update(goalResultProposals).set({
|
|
1528
|
+
status: "rejected",
|
|
1529
|
+
rejectedByActorType: "user",
|
|
1530
|
+
rejectedByActorId: actorUserId,
|
|
1531
|
+
rejectedAt: now,
|
|
1532
|
+
rejectionFeedback: input.feedback,
|
|
1533
|
+
updatedAt: now,
|
|
1534
|
+
}).where(and(eq(goalResultProposals.id, proposal.id), eq(goalResultProposals.status, "ready"))).returning().then((rows) => rows[0] ?? null);
|
|
1535
|
+
if (!rejected)
|
|
1536
|
+
throw conflict("Goal Result Proposal changed before rejection");
|
|
1537
|
+
return rejected;
|
|
1538
|
+
});
|
|
1539
|
+
},
|
|
1540
|
+
assignOwner: async (id, input, actorAgentId = null) => {
|
|
1541
|
+
const current = await requireGoal(db, id);
|
|
1542
|
+
assertCanonicalActiveGoal(current);
|
|
1543
|
+
assertGoalOwner(current, actorAgentId);
|
|
1544
|
+
await requireInvokableOwner(db, current.orgId, input.agentId);
|
|
1545
|
+
return db.transaction(async (tx) => {
|
|
1546
|
+
const previous = await tx.select().from(goalOwnerAssignments)
|
|
1547
|
+
.where(and(eq(goalOwnerAssignments.goalId, id), isNull(goalOwnerAssignments.endsAt)))
|
|
1548
|
+
.then((rows) => rows[0] ?? null);
|
|
1549
|
+
if (previous?.agentId === input.agentId)
|
|
1550
|
+
return previous;
|
|
1551
|
+
const now = new Date();
|
|
1552
|
+
if (previous)
|
|
1553
|
+
await tx.update(goalOwnerAssignments).set({ endsAt: now }).where(eq(goalOwnerAssignments.id, previous.id));
|
|
1554
|
+
const [assignment] = await tx.insert(goalOwnerAssignments).values({
|
|
1555
|
+
orgId: current.orgId,
|
|
1556
|
+
goalId: id,
|
|
1557
|
+
agentId: input.agentId,
|
|
1558
|
+
assignmentRevision: (previous?.assignmentRevision ?? 0) + 1,
|
|
1559
|
+
assignedByAuthorityRef: input.authorityRef ?? null,
|
|
1560
|
+
startsAt: now,
|
|
1561
|
+
}).returning();
|
|
1562
|
+
await tx.update(goals).set({ ownerAgentId: input.agentId, updatedAt: now }).where(eq(goals.id, id));
|
|
1563
|
+
return assignment;
|
|
1564
|
+
});
|
|
1565
|
+
},
|
|
1566
|
+
setFocus: async (id, focus, actorAgentId = null) => {
|
|
1567
|
+
const current = await requireGoal(db, id);
|
|
1568
|
+
if (focus)
|
|
1569
|
+
assertCanonicalActiveGoal(current);
|
|
1570
|
+
assertGoalOwner(current, actorAgentId);
|
|
1571
|
+
return db.transaction(async (tx) => {
|
|
1572
|
+
if (focus)
|
|
1573
|
+
await tx.update(goals).set({ focus: false }).where(eq(goals.orgId, current.orgId));
|
|
1574
|
+
const [goal] = await tx.update(goals).set({ focus, updatedAt: new Date() }).where(eq(goals.id, id)).returning();
|
|
1575
|
+
return goal;
|
|
1576
|
+
});
|
|
1577
|
+
},
|
|
1578
|
+
evaluate: async (id, input, actorAgentId = null) => {
|
|
1579
|
+
return db.transaction(async (tx) => {
|
|
1580
|
+
const database = tx;
|
|
1581
|
+
const current = await requireGoal(database, id);
|
|
1582
|
+
return evaluateInTransaction(database, current, input, actorAgentId);
|
|
1583
|
+
});
|
|
216
1584
|
},
|
|
217
1585
|
remove: async (id) => {
|
|
218
|
-
const existing = await db
|
|
219
|
-
.select()
|
|
220
|
-
.from(goals)
|
|
221
|
-
.where(eq(goals.id, id))
|
|
222
|
-
.then((rows) => rows[0] ?? null);
|
|
223
|
-
if (!existing)
|
|
224
|
-
return null;
|
|
1586
|
+
const existing = await requireGoal(db, id);
|
|
225
1587
|
const dependencies = await getGoalDependencies(db, existing);
|
|
226
|
-
if (!dependencies.canDelete)
|
|
227
|
-
throw conflict("
|
|
228
|
-
|
|
229
|
-
return db
|
|
230
|
-
.delete(goals)
|
|
231
|
-
.where(eq(goals.id, id))
|
|
232
|
-
.returning()
|
|
233
|
-
.then((rows) => rows[0] ?? null);
|
|
1588
|
+
if (!dependencies.canDelete)
|
|
1589
|
+
throw conflict("Only an unlinked draft Goal can be deleted", dependencies);
|
|
1590
|
+
return db.delete(goals).where(eq(goals.id, id)).returning().then((rows) => rows[0] ?? null);
|
|
234
1591
|
},
|
|
235
1592
|
};
|
|
236
1593
|
}
|