titan-agent 5.4.2 → 5.5.6
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/README.md +1 -1
- package/dist/agent/agent.js +9 -5
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/agentLoop.js +7 -3
- package/dist/agent/agentLoop.js.map +1 -1
- package/dist/agent/checkpoint.js +2 -2
- package/dist/agent/checkpoint.js.map +1 -1
- package/dist/agent/commandPost.js +3 -3
- package/dist/agent/commandPost.js.map +1 -1
- package/dist/agent/goalProposer.js +2 -2
- package/dist/agent/goalProposer.js.map +1 -1
- package/dist/agent/goals.js +3 -3
- package/dist/agent/goals.js.map +1 -1
- package/dist/agent/peerAdvise.js +1 -1
- package/dist/agent/peerAdvise.js.map +1 -1
- package/dist/agent/planner.js +4 -4
- package/dist/agent/planner.js.map +1 -1
- package/dist/agent/userProfile.js +2 -2
- package/dist/agent/userProfile.js.map +1 -1
- package/dist/cli/doctor.js +33 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/onboard.js +4 -4
- package/dist/cli/onboard.js.map +1 -1
- package/dist/config/config.js +3 -3
- package/dist/config/config.js.map +1 -1
- package/dist/config/schema.js +8 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/routes/adminRouter.js +500 -0
- package/dist/gateway/routes/adminRouter.js.map +1 -0
- package/dist/gateway/routes/agents.js +231 -0
- package/dist/gateway/routes/agents.js.map +1 -0
- package/dist/gateway/routes/agentsRouter.js +32 -0
- package/dist/gateway/routes/agentsRouter.js.map +1 -0
- package/dist/gateway/routes/checkpoints.js +41 -0
- package/dist/gateway/routes/checkpoints.js.map +1 -0
- package/dist/gateway/routes/commandPost.js +755 -0
- package/dist/gateway/routes/commandPost.js.map +1 -0
- package/dist/gateway/routes/companies.js +166 -0
- package/dist/gateway/routes/companies.js.map +1 -0
- package/dist/gateway/routes/files.js +295 -0
- package/dist/gateway/routes/files.js.map +1 -0
- package/dist/gateway/routes/hardwareRouter.js +151 -0
- package/dist/gateway/routes/hardwareRouter.js.map +1 -0
- package/dist/gateway/routes/mcpRouter.js +88 -0
- package/dist/gateway/routes/mcpRouter.js.map +1 -0
- package/dist/gateway/routes/mesh.js +464 -0
- package/dist/gateway/routes/mesh.js.map +1 -0
- package/dist/gateway/routes/metricsRouter.js +131 -0
- package/dist/gateway/routes/metricsRouter.js.map +1 -0
- package/dist/gateway/routes/organism.js +82 -0
- package/dist/gateway/routes/organism.js.map +1 -0
- package/dist/gateway/routes/paperclip.js +101 -0
- package/dist/gateway/routes/paperclip.js.map +1 -0
- package/dist/gateway/routes/sessions.js +227 -0
- package/dist/gateway/routes/sessions.js.map +1 -0
- package/dist/gateway/routes/skills.js +295 -0
- package/dist/gateway/routes/skills.js.map +1 -0
- package/dist/gateway/routes/socialRouter.js +145 -0
- package/dist/gateway/routes/socialRouter.js.map +1 -0
- package/dist/gateway/routes/systemRouter.js +220 -0
- package/dist/gateway/routes/systemRouter.js.map +1 -0
- package/dist/gateway/routes/teamsRecipes.js +297 -0
- package/dist/gateway/routes/teamsRecipes.js.map +1 -0
- package/dist/gateway/routes/tests.js +401 -0
- package/dist/gateway/routes/tests.js.map +1 -0
- package/dist/gateway/routes/traces.js +33 -0
- package/dist/gateway/routes/traces.js.map +1 -0
- package/dist/gateway/routes/voiceRouter.js +770 -0
- package/dist/gateway/routes/voiceRouter.js.map +1 -0
- package/dist/gateway/routes/watchRouter.js +131 -0
- package/dist/gateway/routes/watchRouter.js.map +1 -0
- package/dist/gateway/server.js +1179 -7379
- package/dist/gateway/server.js.map +1 -1
- package/dist/mcp/registry.js +2 -2
- package/dist/mcp/registry.js.map +1 -1
- package/dist/memory/episodic.js +2 -2
- package/dist/memory/episodic.js.map +1 -1
- package/dist/memory/learning.js +3 -3
- package/dist/memory/learning.js.map +1 -1
- package/dist/memory/memory.js +3 -3
- package/dist/memory/memory.js.map +1 -1
- package/dist/organism/drives.js +2 -2
- package/dist/organism/drives.js.map +1 -1
- package/dist/providers/errorTaxonomy.js +13 -0
- package/dist/providers/errorTaxonomy.js.map +1 -1
- package/dist/providers/ollama.js +3 -1
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai_compat.js +4 -3
- package/dist/providers/openai_compat.js.map +1 -1
- package/dist/providers/router.js +13 -0
- package/dist/providers/router.js.map +1 -1
- package/dist/safety/fixOscillation.js +15 -0
- package/dist/safety/fixOscillation.js.map +1 -1
- package/dist/safety/killSwitch.js +2 -2
- package/dist/safety/killSwitch.js.map +1 -1
- package/dist/safety/selfRepair.js +7 -3
- package/dist/safety/selfRepair.js.map +1 -1
- package/dist/skills/builtin/agent_debate.js +2 -2
- package/dist/skills/builtin/agent_debate.js.map +1 -1
- package/dist/skills/builtin/apply_patch.js +3 -3
- package/dist/skills/builtin/apply_patch.js.map +1 -1
- package/dist/skills/builtin/shell.js +2 -2
- package/dist/skills/builtin/shell.js.map +1 -1
- package/dist/skills/builtin/voice_control.js +49 -0
- package/dist/skills/builtin/voice_control.js.map +1 -0
- package/dist/skills/builtin/widget_gallery.js +6 -1
- package/dist/skills/builtin/widget_gallery.js.map +1 -1
- package/dist/skills/registry.js +15 -4
- package/dist/skills/registry.js.map +1 -1
- package/dist/storage/JsonStorage.js +4 -4
- package/dist/storage/JsonStorage.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/helpers.js +3 -1
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/lifecycle.js +86 -0
- package/dist/utils/lifecycle.js.map +1 -0
- package/dist/voice/bridge.js +136 -0
- package/dist/voice/bridge.js.map +1 -0
- package/docs/COO-MASTER-PLAN-2026-05-02.md +474 -0
- package/docs/HANDOFF/2026-04-29.md +141 -0
- package/docs/HANDOFF-2026-04-30.md +144 -0
- package/docs/HANDOFF-2026-05-03.md +114 -0
- package/docs/adr/2026-04-29-widget-pipeline-traceability.md +49 -0
- package/docs/agent-memory/README.md +45 -0
- package/docs/agent-memory/commands.md +100 -0
- package/docs/agent-memory/context-tree.md +101 -0
- package/docs/agent-memory/current-state.md +54 -0
- package/docs/agent-memory/decisions.md +78 -0
- package/docs/agent-memory/known-issues.md +76 -0
- package/docs/agent-memory/reflections.md +52 -0
- package/docs/agent-memory/skills-candidates.md +80 -0
- package/docs/superpowers/plans/2026-04-29-comprehensive-audit.md +256 -0
- package/docs/superpowers/plans/2026-04-29-comprehensive-test-plan.md +396 -0
- package/docs/superpowers/plans/2026-04-29-fix-all-prs.md +251 -0
- package/docs/superpowers/plans/2026-04-29-gitnexus-gap-remediation.md +969 -0
- package/package.json +5 -2
- package/ui/dist/assets/{AuditPanel-CM6Wg9hO.js → AuditPanel-VzSndmDN.js} +2 -2
- package/ui/dist/assets/{AutonomyPanel-CESx3ANg.js → AutonomyPanel-BiFouzAV.js} +2 -2
- package/ui/dist/assets/AutopilotPanel-fjOfM668.js +1 -0
- package/ui/dist/assets/{AutoresearchPanel-DR47NqT5.js → AutoresearchPanel-CVCxzAH3.js} +2 -2
- package/ui/dist/assets/BackupPanel-CHVTG--q.js +1 -0
- package/ui/dist/assets/{BrowserPanel-C15x9bLn.js → BrowserPanel-D5mvMKFU.js} +2 -2
- package/ui/dist/assets/CPActivity-B12mt35m.js +1 -0
- package/ui/dist/assets/CPAgentDetail-DsdShc-1.js +1 -0
- package/ui/dist/assets/CPAgents-j_7C-oQV.js +1 -0
- package/ui/dist/assets/CPApprovals-BShKSX9X.js +1 -0
- package/ui/dist/assets/CPCosts-CKPlhBDs.js +1 -0
- package/ui/dist/assets/CPDashboard-11c0nkxK.js +1 -0
- package/ui/dist/assets/CPFiles-BhLEOnXy.js +1 -0
- package/ui/dist/assets/CPGoals-Bi3t1b2P.js +1 -0
- package/ui/dist/assets/CPInbox-Bbr7khp6.js +11 -0
- package/ui/dist/assets/CPIssueDetail-DSdgNK8r.js +1 -0
- package/ui/dist/assets/CPIssues-DDEVKhX6.js +1 -0
- package/ui/dist/assets/CPLayout-DgPOfyGv.js +17 -0
- package/ui/dist/assets/CPOrg-Df73RrRJ.js +8 -0
- package/ui/dist/assets/CPRuns-ByioAz8w.js +1 -0
- package/ui/dist/assets/{CPSocial-nb-j7sOE.js → CPSocial-Dlnr_w1X.js} +2 -2
- package/ui/dist/assets/ChannelsPanel-DQjQCTK5.js +1 -0
- package/ui/dist/assets/CheckpointsPanel-C4vKjlAJ.js +1 -0
- package/ui/dist/assets/CommandPostHub-C9pp5Giq.js +24 -0
- package/ui/dist/assets/CronPanel-C6bzUfrD.js +1 -0
- package/ui/dist/assets/DaemonPanel-BA5Tb_UO.js +1 -0
- package/ui/dist/assets/{DataTable-B2Ma8hfi.js → DataTable-CH7IYJJh.js} +1 -1
- package/ui/dist/assets/{EmptyState-CcKyk5Yn.js → EmptyState-jU6yNDnF.js} +1 -1
- package/ui/dist/assets/{EvalHarnessPanel-BqtMc1ZM.js → EvalHarnessPanel-DnYqredY.js} +2 -2
- package/ui/dist/assets/EvalPanel-ChO7CD1r.js +1 -0
- package/ui/dist/assets/{FilesPanel-3QKvrWPo.js → FilesPanel-CaUkv2is.js} +2 -2
- package/ui/dist/assets/FleetPanel-DC_5uj0N.js +1 -0
- package/ui/dist/assets/{HomelabPanel-DhrjTX9m.js → HomelabPanel-CE5PGRpL.js} +2 -2
- package/ui/dist/assets/InfraView-C-uSlvb9.js +2 -0
- package/ui/dist/assets/InlineEditableField-BMQjiE6-.js +1 -0
- package/ui/dist/assets/Input-Bu_b3qmY.js +1 -0
- package/ui/dist/assets/IntegrationsPanel-DsYpAq43.js +1 -0
- package/ui/dist/assets/IntelligenceView-DUdIO1K7.js +2 -0
- package/ui/dist/assets/LearningPanel-UpQZC-mA.js +1 -0
- package/ui/dist/assets/LogsPanel-ClXJ4fcr.js +1 -0
- package/ui/dist/assets/McpPanel-JKgtIERQ.js +1 -0
- package/ui/dist/assets/{MemoryGraphPanel-Bzvjmzvk.js → MemoryGraphPanel-Bo2OrvA6.js} +2 -2
- package/ui/dist/assets/MemoryWikiPanel-BqJ1AmYm.js +11 -0
- package/ui/dist/assets/{MeshPanel-C3LJSlht.js → MeshPanel-BJVGYvwk.js} +2 -2
- package/ui/dist/assets/Modal-CAAooiZU.js +1 -0
- package/ui/dist/assets/NvidiaPanel-BtCg3G4w.js +1 -0
- package/ui/dist/assets/OrganismPanel-DgrTTzcF.js +1 -0
- package/ui/dist/assets/OverviewPanel-rVav1Hox.js +1 -0
- package/ui/dist/assets/{PageHeader-BimceqQo.js → PageHeader-CnZtP8ek.js} +1 -1
- package/ui/dist/assets/PaperclipPanel-C-FKdhiF.js +1 -0
- package/ui/dist/assets/{PersonasPanel-L1j78p6H.js → PersonasPanel-BmlxokfB.js} +1 -1
- package/ui/dist/assets/RecipesPanel-BNKKChis.js +1 -0
- package/ui/dist/assets/SecurityPanel-I7JRHiNy.js +1 -0
- package/ui/dist/assets/SelfImprovePanel-u9h0Lt3p.js +1 -0
- package/ui/dist/assets/{SelfProposalsPanel-lNmiDThB.js → SelfProposalsPanel-DKl9iBjM.js} +2 -2
- package/ui/dist/assets/SessionsPanel-BhRiWI_g.js +1 -0
- package/ui/dist/assets/{SessionsTab-JQbltWww.js → SessionsTab-Bk08wyeY.js} +1 -1
- package/ui/dist/assets/SettingsPanel-haLfmG2k.js +1 -0
- package/ui/dist/assets/SettingsView--gi3fxI8.js +2 -0
- package/ui/dist/assets/{SkeletonLoader-atQtpcF5.js → SkeletonLoader-B5v09EF_.js} +1 -1
- package/ui/dist/assets/{SkillsPanel-DlFs2ih7.js → SkillsPanel-BlAHFLcQ.js} +1 -1
- package/ui/dist/assets/SomaView-CExtS3zw.js +5 -0
- package/ui/dist/assets/{StatCard-DciE_Iqc.js → StatCard-BIsyMybM.js} +1 -1
- package/ui/dist/assets/{StatusBadge-BtfSPoW2.js → StatusBadge-D5nU7El8.js} +1 -1
- package/ui/dist/assets/Tabs-BBYZrBI8.js +1 -0
- package/ui/dist/assets/TeamsPanel-LPXJg823.js +1 -0
- package/ui/dist/assets/TelemetryPanel-EqpRBmOI.js +1 -0
- package/ui/dist/assets/TitanCanvas-BCbWnLMd.js +985 -0
- package/ui/dist/assets/ToolsView-CeP0Zz-N.js +2 -0
- package/ui/dist/assets/{Tooltip-70UK0E2I.js → Tooltip-BSO2XVpF.js} +1 -1
- package/ui/dist/assets/TraceViewer-BKI7o5B0.js +1 -0
- package/ui/dist/assets/TrainingPanel-c-RhjdE1.js +1 -0
- package/ui/dist/assets/VoiceOverlay-D-gc58b0.js +27 -0
- package/ui/dist/assets/VramPanel-C6xc7zgd.js +1 -0
- package/ui/dist/assets/{WatchView-C-sGFpVy.js → WatchView-dqBVCVH0.js} +1 -1
- package/ui/dist/assets/WorkTab-CBoLNrTM.js +1 -0
- package/ui/dist/assets/{WorkflowsPanel-CvgQU1xI.js → WorkflowsPanel-BAnSTOYe.js} +2 -2
- package/ui/dist/assets/approvalHeadline-DB9SgR-9.js +1 -0
- package/ui/dist/assets/{arrow-left-DwqHtJiU.js → arrow-left-5chqas7J.js} +1 -1
- package/ui/dist/assets/briefcase-D4vLzudp.js +6 -0
- package/ui/dist/assets/{chart-column-BtNO6sRy.js → chart-column-CdFlBpoP.js} +1 -1
- package/ui/dist/assets/check-Bpm1IONe.js +6 -0
- package/ui/dist/assets/chevron-down-D7OLjvuD.js +6 -0
- package/ui/dist/assets/chevron-right-aQEw2mUW.js +6 -0
- package/ui/dist/assets/chevron-up-C5g6pEj8.js +6 -0
- package/ui/dist/assets/{circle-check-big-DZRE_MbN.js → circle-check-big-fPhEdP88.js} +1 -1
- package/ui/dist/assets/clock-CTsgP_Sn.js +6 -0
- package/ui/dist/assets/{dollar-sign-aVG3a5eL.js → dollar-sign-CudFVYFc.js} +1 -1
- package/ui/dist/assets/{download-BxiWJU4G.js → download-DZRxDn67.js} +1 -1
- package/ui/dist/assets/external-link-BZ0y_Ahx.js +6 -0
- package/ui/dist/assets/{eye-off-CkgfFYhm.js → eye-off-BmJF0YYx.js} +1 -1
- package/ui/dist/assets/folder-DA43TRCm.js +11 -0
- package/ui/dist/assets/{funnel-PkLdxKyC.js → funnel-J3mULzrz.js} +1 -1
- package/ui/dist/assets/{git-branch-BM-Gw95X.js → git-branch-oHibJqDq.js} +1 -1
- package/ui/dist/assets/{index-D0RJ8701.css → index-BR0vfkIi.css} +1 -1
- package/ui/dist/assets/{index-CahJbWSR.js → index-DzwowwSI.js} +20 -20
- package/ui/dist/assets/{layers-BuGf4FIJ.js → layers-DsyEyu7z.js} +1 -1
- package/ui/dist/assets/{legacy-CR6o4t-y.js → legacy-8ITl64sV.js} +1 -1
- package/ui/dist/assets/{lightbulb-n8gc_XAL.js → lightbulb-C54Ske-p.js} +1 -1
- package/ui/dist/assets/list-todo-Cnd4rdoK.js +6 -0
- package/ui/dist/assets/loader-circle-1YOBsoQp.js +6 -0
- package/ui/dist/assets/network-DbGDAdrn.js +6 -0
- package/ui/dist/assets/{pause-DCV52koX.js → pause-CYhO_uQo.js} +1 -1
- package/ui/dist/assets/{play-CcJ9BnCh.js → play-DVY9c5Ck.js} +1 -1
- package/ui/dist/assets/{plug-CfWBXfCl.js → plug-BcXjlPUL.js} +1 -1
- package/ui/dist/assets/plus-Csu2v9GN.js +6 -0
- package/ui/dist/assets/{proxy-CzZDfLmm.js → proxy-DxS2_9D7.js} +1 -1
- package/ui/dist/assets/rotate-ccw-Co-_W04j.js +6 -0
- package/ui/dist/assets/save-Btx-kpoW.js +6 -0
- package/ui/dist/assets/search-0hXTwEZR.js +6 -0
- package/ui/dist/assets/send-TEpapzQR.js +6 -0
- package/ui/dist/assets/shield-check-DjBJXZUr.js +6 -0
- package/ui/dist/assets/{square-DJpUhlxi.js → square-OweUvjP-.js} +1 -1
- package/ui/dist/assets/{target-DWcmM_9m.js → target-BRW80Xer.js} +1 -1
- package/ui/dist/assets/terminal-BtiqJ628.js +16 -0
- package/ui/dist/assets/{toggle-right-YusFQ69L.js → toggle-right-CKtSrl28.js} +1 -1
- package/ui/dist/assets/{trash-2-CK7yQ55V.js → trash-2-DgWrHVax.js} +1 -1
- package/ui/dist/assets/{trending-up-DGjFyubC.js → trending-up-MpIrE4j6.js} +1 -1
- package/ui/dist/assets/{trophy-uQv_NgDB.js → trophy-CECuZNhX.js} +1 -1
- package/ui/dist/assets/users-dZgv4ePG.js +16 -0
- package/ui/dist/assets/wrench-CDz3xYve.js +11 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AutopilotPanel-DtEet1hJ.js +0 -1
- package/ui/dist/assets/BackupPanel-BGP8p3l3.js +0 -1
- package/ui/dist/assets/CPAgents-DYUtPzSq.js +0 -1
- package/ui/dist/assets/CPDashboard-Bf0-SyCh.js +0 -6
- package/ui/dist/assets/CPFiles-CxgxjQcO.js +0 -1
- package/ui/dist/assets/CPGoals-BsmCMTvT.js +0 -1
- package/ui/dist/assets/CPInbox-tMSbmQ9H.js +0 -11
- package/ui/dist/assets/ChannelsPanel-DP5C2OKd.js +0 -1
- package/ui/dist/assets/CheckpointsPanel-DlranVLZ.js +0 -1
- package/ui/dist/assets/CommandPostHub-BgxIa4Ev.js +0 -29
- package/ui/dist/assets/CronPanel-LoT5yKwJ.js +0 -1
- package/ui/dist/assets/DaemonPanel-DBGMqaE_.js +0 -1
- package/ui/dist/assets/EvalPanel-Bc33j0pN.js +0 -1
- package/ui/dist/assets/FleetPanel-CSsXuQYj.js +0 -1
- package/ui/dist/assets/InfraView-CR6HyrL6.js +0 -2
- package/ui/dist/assets/InlineEditableField-CnvF-yFR.js +0 -1
- package/ui/dist/assets/Input-GTHp2Rkr.js +0 -1
- package/ui/dist/assets/IntegrationsPanel-CymCRE3T.js +0 -1
- package/ui/dist/assets/IntelligenceView-C1IHxJRC.js +0 -2
- package/ui/dist/assets/LearningPanel-DOCES3lH.js +0 -1
- package/ui/dist/assets/LogsPanel-BLnAqEaZ.js +0 -1
- package/ui/dist/assets/McpPanel-ChUzmr3z.js +0 -1
- package/ui/dist/assets/MemoryWikiPanel-Dwk3Aqwd.js +0 -11
- package/ui/dist/assets/NvidiaPanel-CeZK_-CV.js +0 -1
- package/ui/dist/assets/OrganismPanel-BB6YOiQV.js +0 -1
- package/ui/dist/assets/OverviewPanel-BmtBhQnv.js +0 -1
- package/ui/dist/assets/PaperclipPanel-C-brgwA3.js +0 -1
- package/ui/dist/assets/RecipesPanel-34lCfynJ.js +0 -1
- package/ui/dist/assets/SecurityPanel-CBTPWLj6.js +0 -1
- package/ui/dist/assets/SelfImprovePanel-BrPbFHhG.js +0 -1
- package/ui/dist/assets/SessionsPanel-DAEYIn83.js +0 -1
- package/ui/dist/assets/SettingsPanel-CzRROAYQ.js +0 -1
- package/ui/dist/assets/SettingsView-CN7ii2uw.js +0 -2
- package/ui/dist/assets/SomaView-Ba642Oqb.js +0 -5
- package/ui/dist/assets/TeamsPanel-DKQ5z2Qe.js +0 -1
- package/ui/dist/assets/TelemetryPanel-B6KAc55Q.js +0 -1
- package/ui/dist/assets/TitanCanvas-C-s0A-lv.js +0 -1092
- package/ui/dist/assets/ToolsView-Dei0KMP0.js +0 -2
- package/ui/dist/assets/TraceViewer-BniolyBx.js +0 -1
- package/ui/dist/assets/TrainingPanel-Bz4CTPGW.js +0 -1
- package/ui/dist/assets/VoiceOverlay-CmNCrLcd.js +0 -37
- package/ui/dist/assets/VramPanel-Xh_OtRDR.js +0 -1
- package/ui/dist/assets/WorkTab-BjLNmgIK.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/safety/killSwitch.ts"],"sourcesContent":["/**\n * TITAN — Master Kill Switch (v4.9.0+, local hard-takeoff)\n *\n * Final backstop. When something the organism can't recover from\n * happens, this pauses everything autonomous until a human explicitly\n * resumes.\n *\n * Trigger sources:\n * - Safety drive pressure > 2.0 sustained for > 10 minutes\n * - Identity non-negotiable violation (drift detector fires\n * 'values_divergence' with high confidence)\n * - Canary eval drops > 30% on any task (silent degradation)\n * - Fix oscillation detector fires on same target 3× in 24h\n * - Human posts POST /api/safety/kill with a reason\n *\n * On trigger:\n * - Autopilot disabled in-memory (and persisted so restart doesn't\n * resurrect it)\n * - All active goals set status='paused'\n * - Specialists status='paused'\n * - SSE broadcast 'safety:killed' to all connected /watch clients\n * - Activity feed + audit log both record the trigger\n * - Any in-flight agent sessions get AbortController.abort()\n *\n * Resume:\n * - Human calls POST /api/safety/resume with a resolution note\n * - Kill-switch state flips to 'armed' (ready but not triggered)\n * - Previously-paused goals/specialists are NOT auto-unpaused —\n * Tony inspects each and flips manually. This is intentional:\n * the organism should not be trusted to self-resume without\n * human review of what caused the kill.\n *\n * Storage: <TITAN_HOME>/kill-switch.json — survives restarts.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { atomicWriteJsonFile } from '../utils/helpers.js';\nimport { dirname, join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\nimport { logAudit } from '../security/auditLog.js';\n\nconst COMPONENT = 'KillSwitch';\nconst STATE_PATH = join(TITAN_HOME, 'kill-switch.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type KillSwitchStatus = 'armed' | 'killed';\n\nexport type KillTrigger =\n | 'safety_pressure'\n | 'identity_violation'\n | 'canary_degradation'\n | 'fix_oscillation'\n | 'manual'\n | 'startup_preserve'; // restart preserved a prior killed state\n\nexport interface KillEvent {\n at: string;\n trigger: KillTrigger;\n reason: string;\n evidence?: string;\n /** Sub-module that fired the trigger (for audit). */\n firedBy?: string;\n}\n\nexport interface KillSwitchState {\n status: KillSwitchStatus;\n lastEvent?: KillEvent;\n /** Running log of trigger events — bounded at 50. */\n history: KillEvent[];\n /** When the state was last mutated. */\n updatedAt: string;\n /** ISO timestamp when Safety pressure first crossed the sustained threshold.\n * Used to require 10-minute sustained high-pressure before firing. */\n safetyHighSince?: string;\n /** Ring of recent fix-oscillation events (same target fixed twice within 24h)\n * used to fire the kill once the rolling count ≥ 3. */\n recentOscillations: Array<{ at: string; target: string }>;\n}\n\n// ── Storage ──────────────────────────────────────────────────────\n\nlet cache: KillSwitchState | null = null;\n\nfunction ensureDir(): void {\n try { mkdirSync(dirname(STATE_PATH), { recursive: true }); } catch { /* ok */ }\n}\n\nfunction load(): KillSwitchState {\n if (cache) return cache;\n if (!existsSync(STATE_PATH)) {\n cache = freshState();\n return cache;\n }\n try {\n cache = JSON.parse(readFileSync(STATE_PATH, 'utf-8')) as KillSwitchState;\n if (!cache.history) cache.history = [];\n if (!cache.recentOscillations) cache.recentOscillations = [];\n return cache;\n } catch (err) {\n logger.warn(COMPONENT, `kill-switch.json parse failed, starting armed: ${(err as Error).message}`);\n cache = freshState();\n return cache;\n }\n}\n\nfunction save(): void {\n if (!cache) return;\n ensureDir();\n cache.updatedAt = new Date().toISOString();\n atomicWriteJsonFile(STATE_PATH, cache);\n}\n\nfunction freshState(): KillSwitchState {\n return {\n status: 'armed',\n history: [],\n recentOscillations: [],\n updatedAt: new Date().toISOString(),\n };\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Query — is TITAN paused? Every autonomous entry point should check\n * this before doing work:\n * - autopilot scheduler\n * - initiative checkInitiative()\n * - Soma pressure cycle (won't fire proposals while killed)\n * - spawn_agent tool (blocked while killed)\n * - self-mod auto-review / PR creation\n */\nexport function isKilled(): boolean {\n return load().status === 'killed';\n}\n\nexport function getState(): KillSwitchState {\n return { ...load() }; // shallow copy so callers can't mutate cache\n}\n\n/**\n * Fire the kill switch. Idempotent — if already killed, appends to the\n * event history but doesn't re-run side effects.\n */\nexport async function kill(trigger: KillTrigger, reason: string, opts: {\n evidence?: string;\n firedBy?: string;\n} = {}): Promise<void> {\n const state = load();\n const event: KillEvent = {\n at: new Date().toISOString(),\n trigger,\n reason,\n evidence: opts.evidence,\n firedBy: opts.firedBy,\n };\n const alreadyKilled = state.status === 'killed';\n state.status = 'killed';\n state.lastEvent = event;\n state.history.push(event);\n if (state.history.length > 50) state.history = state.history.slice(-50);\n save();\n\n logger.error(COMPONENT, `🛑 KILL SWITCH FIRED — ${trigger}: ${reason}`);\n try {\n logAudit('security_alert', opts.firedBy ?? 'system', {\n action: 'kill_switch_fired',\n trigger,\n reason,\n firstTime: !alreadyKilled,\n });\n } catch { /* audit unavailable — never block the kill path */ }\n\n if (alreadyKilled) return; // side effects only fire once\n\n // Execute the kill sequence — each step best-effort, never throws.\n await executeKillSequence(event);\n}\n\n/**\n * Resume operations after a human review. Requires a resolution note\n * that gets written to the event history for audit.\n *\n * Does NOT automatically un-pause goals or specialists — those require\n * explicit human action per goal. Intent is \"resume the organism, but\n * you (Tony) decide what work to resume.\"\n */\nexport function resume(resolutionNote: string, resumedBy: string): KillSwitchState {\n const state = load();\n const wasKilled = state.status === 'killed';\n // v4.9.0-local.7: always clear recentOscillations on resume. The human\n // has seen the evidence, acknowledged the pattern, and resumed — fresh\n // slate. Without this clear, the next same-file-write-twice would instantly\n // re-kill because the 24h window still contains the trigger events.\n // NEW oscillations occurring after resume are the real signal we want\n // to catch — not the ones that already caused the kill.\n const clearedCount = state.recentOscillations.length;\n state.recentOscillations = [];\n // Also clear safetyHighSince — the drive pressure that triggered this\n // may have resolved in the meantime; if not, the detector will re-arm it.\n delete state.safetyHighSince;\n if (wasKilled) {\n state.status = 'armed';\n state.history.push({\n at: new Date().toISOString(),\n trigger: 'manual',\n reason: `resumed by ${resumedBy}: ${resolutionNote}`,\n firedBy: resumedBy,\n });\n logger.info(COMPONENT, `Kill switch armed again by ${resumedBy}: ${resolutionNote} (cleared ${clearedCount} prior oscillations)`);\n try {\n logAudit('security_alert', resumedBy, {\n action: 'kill_switch_resumed',\n resolutionNote,\n clearedOscillations: clearedCount,\n });\n } catch { /* audit unavailable — never block resume */ }\n } else if (clearedCount > 0) {\n logger.info(COMPONENT, `Kill switch already armed; cleared ${clearedCount} recent oscillations by ${resumedBy}: ${resolutionNote}`);\n } else {\n logger.info(COMPONENT, 'Resume called but kill switch already armed and oscillations empty — no-op');\n }\n save();\n return { ...state };\n}\n\n// ── Trigger evaluators ───────────────────────────────────────────\n\nconst SAFETY_PRESSURE_THRESHOLD = 2.0;\nconst SAFETY_PRESSURE_SUSTAIN_MS = 10 * 60 * 1000; // 10 min\n// v4.13 (ancestor-extraction Sprint B): retuned from 24h/2-per-target to\n// 1h/5-per-target. Real oscillation is fast-repeating (model stuck writing\n// the same file over and over in a loop); 2 events across a whole day is\n// normal operation (e.g. two separate self-mod retries).\nconst FIX_OSCILLATION_WINDOW_MS = 60 * 60 * 1000; // was 24h\nconst FIX_OSCILLATION_COUNT_THRESHOLD = 8; // raised from 5 → 8 to tolerate normal retry loops\n\n/**\n * Path prefixes whose repeated writes should NOT trigger the fleet-wide\n * kill switch. These are staging/scratch directories where repeat writes\n * are EXPECTED during normal self-modification retry cycles:\n *\n * - self-mod-staging/ — TITAN's own self-modification PRs get retried\n * and re-applied here; 2+ writes per PR is the steady state\n * - /tmp/titan- — scratch files used by tests and probes\n *\n * Writes to PRODUCTION files still count toward oscillation detection.\n * Exemption only suppresses the kill-switch trigger; other observers\n * (logs, activity feed) still see the raw events.\n */\nconst OSCILLATION_EXEMPT_PREFIXES: string[] = [\n '/home/dj/.titan/self-mod-staging/',\n '/opt/TITAN/self-mod-staging/',\n '/tmp/titan-',\n '/home/dj/.titan/',\n '/opt/TITAN/',\n '/home/dj/titan-saas/',\n 'node_modules/',\n '.git/',\n 'dist/',\n 'coverage/',\n '/tmp/',\n];\n\nfunction isOscillationExemptTarget(target: string): boolean {\n if (!target) return false;\n // Target may be a bare path or \"file:/path\" / \"write_file:/path\" etc.\n // Normalize by finding the first \"/\" and comparing from there.\n const slashIdx = target.indexOf('/');\n const pathPart = slashIdx === -1 ? target : target.slice(slashIdx);\n return OSCILLATION_EXEMPT_PREFIXES.some(prefix => pathPart.startsWith(prefix));\n}\n\n/**\n * Evaluate the Safety drive pressure against the sustained-high\n * threshold. Call once per drive tick. Fires kill() when the drive\n * has been > threshold continuously for 10 minutes.\n */\nexport function evaluateSafetyPressure(safetyPressure: number): void {\n const state = load();\n const now = new Date();\n if (safetyPressure > SAFETY_PRESSURE_THRESHOLD) {\n if (!state.safetyHighSince) {\n state.safetyHighSince = now.toISOString();\n save();\n return;\n }\n const elapsed = now.getTime() - new Date(state.safetyHighSince).getTime();\n if (elapsed >= SAFETY_PRESSURE_SUSTAIN_MS && state.status === 'armed') {\n void kill('safety_pressure',\n `Safety drive pressure ${safetyPressure.toFixed(2)} > ${SAFETY_PRESSURE_THRESHOLD} sustained for ${Math.round(elapsed / 60_000)}m`,\n { firedBy: 'soma' });\n }\n } else if (state.safetyHighSince) {\n // Clear the sustained-timer — pressure dropped back below.\n state.safetyHighSince = undefined;\n save();\n }\n}\n\n/**\n * Record a fix-oscillation event (same target fixed twice within 24h).\n * Fires kill when ≥2 oscillations on the SAME target within 24h window.\n *\n * v4.10.0-local fix: Changed from \"3 total events anywhere\" to\n * \"2+ events on same target\" — prevents false positives when editing\n * different files (3 files each edited twice is not oscillation).\n */\nexport function recordFixOscillation(target: string): void {\n // v4.13 ancestor-extraction Sprint B: staging/scratch paths are exempt\n // from fleet-wide kill. They still get logged (below), just don't trigger.\n if (isOscillationExemptTarget(target)) {\n logger.debug(COMPONENT, `Oscillation event on exempt path \"${target.slice(0, 80)}\" — recorded, not counted`);\n return;\n }\n\n const state = load();\n const now = Date.now();\n state.recentOscillations.push({ at: new Date(now).toISOString(), target });\n state.recentOscillations = state.recentOscillations.filter(o =>\n now - new Date(o.at).getTime() < FIX_OSCILLATION_WINDOW_MS,\n );\n save();\n\n // Count oscillations per target\n const targetCounts = new Map<string, number>();\n for (const o of state.recentOscillations) {\n targetCounts.set(o.target, (targetCounts.get(o.target) || 0) + 1);\n }\n\n let maxCount = 0;\n let worstTarget = '';\n for (const [t, count] of targetCounts) {\n if (count > maxCount) {\n maxCount = count;\n worstTarget = t;\n }\n }\n\n // v4.13 ancestor-extraction (Paperclip scoped pause): BEFORE firing the\n // fleet-wide kill, try a scoped per-target pause. If the same target\n // hit >=3× in this window it's suspicious — pause THAT target for 15m\n // (write blocked, everything else continues). The full kill only fires\n // when a single target crosses the higher 5× threshold, which indicates\n // a stuck retry loop rather than occasional repeat edits.\n const SCOPED_PAUSE_THRESHOLD = 3;\n if (maxCount >= SCOPED_PAUSE_THRESHOLD && maxCount < FIX_OSCILLATION_COUNT_THRESHOLD) {\n try {\n // Lazy import to avoid circular deps at module load\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void (async () => {\n const { pauseTarget, isTargetPaused } = await import('./scopedPause.js');\n if (!isTargetPaused(worstTarget)) {\n pauseTarget(worstTarget, 'fix_oscillation', {\n note: `${maxCount}× events in ${Math.round(FIX_OSCILLATION_WINDOW_MS / 60000)}m`,\n });\n }\n })();\n } catch { /* non-fatal */ }\n }\n\n // Fire fleet-wide kill ONLY when same non-exempt target hits\n // FIX_OSCILLATION_COUNT_THRESHOLD (5×) within FIX_OSCILLATION_WINDOW_MS\n // (1h). At that point it's a genuine stuck loop, not normal operation.\n if (maxCount >= FIX_OSCILLATION_COUNT_THRESHOLD && state.status === 'armed') {\n const totalEvents = state.recentOscillations.length;\n const uniqueTargets = targetCounts.size;\n const windowMin = Math.round(FIX_OSCILLATION_WINDOW_MS / 60000);\n void kill('fix_oscillation',\n `Target \"${worstTarget.slice(0, 60)}\" oscillated ${maxCount}× in ${windowMin}m (${totalEvents} total events across ${uniqueTargets} target(s))`,\n { firedBy: 'fix-oscillation-detector' });\n }\n}\n\n// ── Kill sequence (side effects) ─────────────────────────────────\n\nasync function executeKillSequence(event: KillEvent): Promise<void> {\n const steps: Array<{ name: string; fn: () => Promise<void> | void }> = [\n { name: 'disable-autopilot', fn: disableAutopilot },\n { name: 'pause-active-goals', fn: pauseActiveGoals },\n { name: 'pause-specialists', fn: pauseSpecialists },\n { name: 'abort-in-flight', fn: abortInFlightSessions },\n { name: 'broadcast-sse', fn: () => broadcastKill(event) },\n ];\n for (const step of steps) {\n try {\n await step.fn();\n logger.info(COMPONENT, `kill seq: ${step.name} ✓`);\n } catch (err) {\n logger.warn(COMPONENT, `kill seq: ${step.name} failed: ${(err as Error).message}`);\n }\n }\n}\n\nasync function disableAutopilot(): Promise<void> {\n try {\n // Best-effort: set in-memory flag that the scheduler checks\n const g = globalThis as unknown as { __titan_autopilot_killed?: boolean };\n g.__titan_autopilot_killed = true;\n } catch { /* ok */ }\n}\n\nasync function pauseActiveGoals(): Promise<void> {\n try {\n const { listGoals, updateGoal } = await import('../agent/goals.js');\n let paused = 0;\n for (const g of listGoals()) {\n if (g.status === 'active') {\n try { updateGoal(g.id, { status: 'paused' }); paused++; } catch { /* skip */ }\n }\n }\n logger.info(COMPONENT, `kill: paused ${paused} active goal(s)`);\n } catch (err) {\n logger.warn(COMPONENT, `kill: pauseActiveGoals unavailable: ${(err as Error).message}`);\n }\n}\n\nasync function pauseSpecialists(): Promise<void> {\n const mod = await import('../agent/commandPost.js').catch(() => null);\n if (!mod) return;\n const agents = mod.getRegisteredAgents();\n let paused = 0;\n for (const a of agents) {\n if (a.status !== 'active' && a.status !== 'idle') continue;\n try { mod.updateAgentStatus(a.id, 'paused'); paused++; } catch { /* skip */ }\n }\n logger.info(COMPONENT, `kill: paused ${paused} agent(s)`);\n}\n\nasync function abortInFlightSessions(): Promise<void> {\n // In-flight abort hooks would be registered on globalThis by the\n // agent loop; here we set a flag the loop checks each round.\n const g = globalThis as unknown as { __titan_abort_all?: boolean };\n g.__titan_abort_all = true;\n // Clear after 30s so normal operations can resume when Tony unpauses.\n setTimeout(() => { g.__titan_abort_all = false; }, 30_000).unref?.();\n}\n\nfunction broadcastKill(event: KillEvent): void {\n const g = globalThis as unknown as { __titan_sse_broadcast?: (topic: string, payload: unknown) => void };\n if (typeof g.__titan_sse_broadcast === 'function') {\n try { g.__titan_sse_broadcast('safety:killed', event); } catch { /* ok */ }\n }\n}\n\n/** Test-only cache reset. */\nexport function _resetKillSwitchCacheForTests(): void { cache = null; }\n"],"mappings":";AAkCA,SAAS,YAAY,cAA6B,iBAAiB;AACnE,SAAS,2BAA2B;AACpC,SAAS,SAAS,YAAY;AAC9B,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AACnB,SAAS,gBAAgB;AAEzB,MAAM,YAAY;AAClB,MAAM,aAAa,KAAK,YAAY,kBAAkB;AAwCtD,IAAI,QAAgC;AAEpC,SAAS,YAAkB;AACvB,MAAI;AAAE,cAAU,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,EAAG,QAAQ;AAAA,EAAW;AAClF;AAEA,SAAS,OAAwB;AAC7B,MAAI,MAAO,QAAO;AAClB,MAAI,CAAC,WAAW,UAAU,GAAG;AACzB,YAAQ,WAAW;AACnB,WAAO;AAAA,EACX;AACA,MAAI;AACA,YAAQ,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AACpD,QAAI,CAAC,MAAM,QAAS,OAAM,UAAU,CAAC;AACrC,QAAI,CAAC,MAAM,mBAAoB,OAAM,qBAAqB,CAAC;AAC3D,WAAO;AAAA,EACX,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,kDAAmD,IAAc,OAAO,EAAE;AACjG,YAAQ,WAAW;AACnB,WAAO;AAAA,EACX;AACJ;AAEA,SAAS,OAAa;AAClB,MAAI,CAAC,MAAO;AACZ,YAAU;AACV,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,sBAAoB,YAAY,KAAK;AACzC;AAEA,SAAS,aAA8B;AACnC,SAAO;AAAA,IACH,QAAQ;AAAA,IACR,SAAS,CAAC;AAAA,IACV,oBAAoB,CAAC;AAAA,IACrB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AACJ;AAaO,SAAS,WAAoB;AAChC,SAAO,KAAK,EAAE,WAAW;AAC7B;AAEO,SAAS,WAA4B;AACxC,SAAO,EAAE,GAAG,KAAK,EAAE;AACvB;AAMA,eAAsB,KAAK,SAAsB,QAAgB,OAG7D,CAAC,GAAkB;AACnB,QAAM,QAAQ,KAAK;AACnB,QAAM,QAAmB;AAAA,IACrB,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B;AAAA,IACA;AAAA,IACA,UAAU,KAAK;AAAA,IACf,SAAS,KAAK;AAAA,EAClB;AACA,QAAM,gBAAgB,MAAM,WAAW;AACvC,QAAM,SAAS;AACf,QAAM,YAAY;AAClB,QAAM,QAAQ,KAAK,KAAK;AACxB,MAAI,MAAM,QAAQ,SAAS,GAAI,OAAM,UAAU,MAAM,QAAQ,MAAM,GAAG;AACtE,OAAK;AAEL,SAAO,MAAM,WAAW,sCAA0B,OAAO,KAAK,MAAM,EAAE;AACtE,MAAI;AACA,aAAS,kBAAkB,KAAK,WAAW,UAAU;AAAA,MACjD,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,WAAW,CAAC;AAAA,IAChB,CAAC;AAAA,EACL,QAAQ;AAAA,EAAsD;AAE9D,MAAI,cAAe;AAGnB,QAAM,oBAAoB,KAAK;AACnC;AAUO,SAAS,OAAO,gBAAwB,WAAoC;AAC/E,QAAM,QAAQ,KAAK;AACnB,QAAM,YAAY,MAAM,WAAW;AAOnC,QAAM,eAAe,MAAM,mBAAmB;AAC9C,QAAM,qBAAqB,CAAC;AAG5B,SAAO,MAAM;AACb,MAAI,WAAW;AACX,UAAM,SAAS;AACf,UAAM,QAAQ,KAAK;AAAA,MACf,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,MAC3B,SAAS;AAAA,MACT,QAAQ,cAAc,SAAS,KAAK,cAAc;AAAA,MAClD,SAAS;AAAA,IACb,CAAC;AACD,WAAO,KAAK,WAAW,8BAA8B,SAAS,KAAK,cAAc,aAAa,YAAY,sBAAsB;AAChI,QAAI;AACA,eAAS,kBAAkB,WAAW;AAAA,QAClC,QAAQ;AAAA,QACR;AAAA,QACA,qBAAqB;AAAA,MACzB,CAAC;AAAA,IACL,QAAQ;AAAA,IAA+C;AAAA,EAC3D,WAAW,eAAe,GAAG;AACzB,WAAO,KAAK,WAAW,sCAAsC,YAAY,2BAA2B,SAAS,KAAK,cAAc,EAAE;AAAA,EACtI,OAAO;AACH,WAAO,KAAK,WAAW,iFAA4E;AAAA,EACvG;AACA,OAAK;AACL,SAAO,EAAE,GAAG,MAAM;AACtB;AAIA,MAAM,4BAA4B;AAClC,MAAM,6BAA6B,KAAK,KAAK;AAK7C,MAAM,4BAA4B,KAAK,KAAK;AAC5C,MAAM,kCAAkC;AAexC,MAAM,8BAAwC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAEA,SAAS,0BAA0B,QAAyB;AACxD,MAAI,CAAC,OAAQ,QAAO;AAGpB,QAAM,WAAW,OAAO,QAAQ,GAAG;AACnC,QAAM,WAAW,aAAa,KAAK,SAAS,OAAO,MAAM,QAAQ;AACjE,SAAO,4BAA4B,KAAK,YAAU,SAAS,WAAW,MAAM,CAAC;AACjF;AAOO,SAAS,uBAAuB,gBAA8B;AACjE,QAAM,QAAQ,KAAK;AACnB,QAAM,MAAM,oBAAI,KAAK;AACrB,MAAI,iBAAiB,2BAA2B;AAC5C,QAAI,CAAC,MAAM,iBAAiB;AACxB,YAAM,kBAAkB,IAAI,YAAY;AACxC,WAAK;AACL;AAAA,IACJ;AACA,UAAM,UAAU,IAAI,QAAQ,IAAI,IAAI,KAAK,MAAM,eAAe,EAAE,QAAQ;AACxE,QAAI,WAAW,8BAA8B,MAAM,WAAW,SAAS;AACnE,WAAK;AAAA,QAAK;AAAA,QACN,yBAAyB,eAAe,QAAQ,CAAC,CAAC,MAAM,yBAAyB,kBAAkB,KAAK,MAAM,UAAU,GAAM,CAAC;AAAA,QAC/H,EAAE,SAAS,OAAO;AAAA,MAAC;AAAA,IAC3B;AAAA,EACJ,WAAW,MAAM,iBAAiB;AAE9B,UAAM,kBAAkB;AACxB,SAAK;AAAA,EACT;AACJ;AAUO,SAAS,qBAAqB,QAAsB;AAGvD,MAAI,0BAA0B,MAAM,GAAG;AACnC,WAAO,MAAM,WAAW,qCAAqC,OAAO,MAAM,GAAG,EAAE,CAAC,gCAA2B;AAC3G;AAAA,EACJ;AAEA,QAAM,QAAQ,KAAK;AACnB,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,mBAAmB,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC;AACzE,QAAM,qBAAqB,MAAM,mBAAmB;AAAA,IAAO,OACvD,MAAM,IAAI,KAAK,EAAE,EAAE,EAAE,QAAQ,IAAI;AAAA,EACrC;AACA,OAAK;AAGL,QAAM,eAAe,oBAAI,IAAoB;AAC7C,aAAW,KAAK,MAAM,oBAAoB;AACtC,iBAAa,IAAI,EAAE,SAAS,aAAa,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;AAAA,EACpE;AAEA,MAAI,WAAW;AACf,MAAI,cAAc;AAClB,aAAW,CAAC,GAAG,KAAK,KAAK,cAAc;AACnC,QAAI,QAAQ,UAAU;AAClB,iBAAW;AACX,oBAAc;AAAA,IAClB;AAAA,EACJ;AAQA,QAAM,yBAAyB;AAC/B,MAAI,YAAY,0BAA0B,WAAW,iCAAiC;AAClF,QAAI;AAGA,YAAM,YAAY;AACd,cAAM,EAAE,aAAa,eAAe,IAAI,MAAM,OAAO,kBAAkB;AACvE,YAAI,CAAC,eAAe,WAAW,GAAG;AAC9B,sBAAY,aAAa,mBAAmB;AAAA,YACxC,MAAM,GAAG,QAAQ,kBAAe,KAAK,MAAM,4BAA4B,GAAK,CAAC;AAAA,UACjF,CAAC;AAAA,QACL;AAAA,MACJ,GAAG;AAAA,IACP,QAAQ;AAAA,IAAkB;AAAA,EAC9B;AAKA,MAAI,YAAY,mCAAmC,MAAM,WAAW,SAAS;AACzE,UAAM,cAAc,MAAM,mBAAmB;AAC7C,UAAM,gBAAgB,aAAa;AACnC,UAAM,YAAY,KAAK,MAAM,4BAA4B,GAAK;AAC9D,SAAK;AAAA,MAAK;AAAA,MACN,WAAW,YAAY,MAAM,GAAG,EAAE,CAAC,gBAAgB,QAAQ,WAAQ,SAAS,MAAM,WAAW,wBAAwB,aAAa;AAAA,MAClI,EAAE,SAAS,2BAA2B;AAAA,IAAC;AAAA,EAC/C;AACJ;AAIA,eAAe,oBAAoB,OAAiC;AAChE,QAAM,QAAiE;AAAA,IACnE,EAAE,MAAM,qBAAqB,IAAI,iBAAiB;AAAA,IAClD,EAAE,MAAM,sBAAsB,IAAI,iBAAiB;AAAA,IACnD,EAAE,MAAM,qBAAqB,IAAI,iBAAiB;AAAA,IAClD,EAAE,MAAM,mBAAmB,IAAI,sBAAsB;AAAA,IACrD,EAAE,MAAM,iBAAiB,IAAI,MAAM,cAAc,KAAK,EAAE;AAAA,EAC5D;AACA,aAAW,QAAQ,OAAO;AACtB,QAAI;AACA,YAAM,KAAK,GAAG;AACd,aAAO,KAAK,WAAW,aAAa,KAAK,IAAI,SAAI;AAAA,IACrD,SAAS,KAAK;AACV,aAAO,KAAK,WAAW,aAAa,KAAK,IAAI,YAAa,IAAc,OAAO,EAAE;AAAA,IACrF;AAAA,EACJ;AACJ;AAEA,eAAe,mBAAkC;AAC7C,MAAI;AAEA,UAAM,IAAI;AACV,MAAE,2BAA2B;AAAA,EACjC,QAAQ;AAAA,EAAW;AACvB;AAEA,eAAe,mBAAkC;AAC7C,MAAI;AACA,UAAM,EAAE,WAAW,WAAW,IAAI,MAAM,OAAO,mBAAmB;AAClE,QAAI,SAAS;AACb,eAAW,KAAK,UAAU,GAAG;AACzB,UAAI,EAAE,WAAW,UAAU;AACvB,YAAI;AAAE,qBAAW,EAAE,IAAI,EAAE,QAAQ,SAAS,CAAC;AAAG;AAAA,QAAU,QAAQ;AAAA,QAAa;AAAA,MACjF;AAAA,IACJ;AACA,WAAO,KAAK,WAAW,gBAAgB,MAAM,iBAAiB;AAAA,EAClE,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,uCAAwC,IAAc,OAAO,EAAE;AAAA,EAC1F;AACJ;AAEA,eAAe,mBAAkC;AAC7C,QAAM,MAAM,MAAM,OAAO,yBAAyB,EAAE,MAAM,MAAM,IAAI;AACpE,MAAI,CAAC,IAAK;AACV,QAAM,SAAS,IAAI,oBAAoB;AACvC,MAAI,SAAS;AACb,aAAW,KAAK,QAAQ;AACpB,QAAI,EAAE,WAAW,YAAY,EAAE,WAAW,OAAQ;AAClD,QAAI;AAAE,UAAI,kBAAkB,EAAE,IAAI,QAAQ;AAAG;AAAA,IAAU,QAAQ;AAAA,IAAa;AAAA,EAChF;AACA,SAAO,KAAK,WAAW,gBAAgB,MAAM,WAAW;AAC5D;AAEA,eAAe,wBAAuC;AAGlD,QAAM,IAAI;AACV,IAAE,oBAAoB;AAEtB,aAAW,MAAM;AAAE,MAAE,oBAAoB;AAAA,EAAO,GAAG,GAAM,EAAE,QAAQ;AACvE;AAEA,SAAS,cAAc,OAAwB;AAC3C,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,0BAA0B,YAAY;AAC/C,QAAI;AAAE,QAAE,sBAAsB,iBAAiB,KAAK;AAAA,IAAG,QAAQ;AAAA,IAAW;AAAA,EAC9E;AACJ;AAGO,SAAS,gCAAsC;AAAE,UAAQ;AAAM;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/safety/killSwitch.ts"],"sourcesContent":["/**\n * TITAN — Master Kill Switch (v4.9.0+, local hard-takeoff)\n *\n * Final backstop. When something the organism can't recover from\n * happens, this pauses everything autonomous until a human explicitly\n * resumes.\n *\n * Trigger sources:\n * - Safety drive pressure > 2.0 sustained for > 10 minutes\n * - Identity non-negotiable violation (drift detector fires\n * 'values_divergence' with high confidence)\n * - Canary eval drops > 30% on any task (silent degradation)\n * - Fix oscillation detector fires on same target 3× in 24h\n * - Human posts POST /api/safety/kill with a reason\n *\n * On trigger:\n * - Autopilot disabled in-memory (and persisted so restart doesn't\n * resurrect it)\n * - All active goals set status='paused'\n * - Specialists status='paused'\n * - SSE broadcast 'safety:killed' to all connected /watch clients\n * - Activity feed + audit log both record the trigger\n * - Any in-flight agent sessions get AbortController.abort()\n *\n * Resume:\n * - Human calls POST /api/safety/resume with a resolution note\n * - Kill-switch state flips to 'armed' (ready but not triggered)\n * - Previously-paused goals/specialists are NOT auto-unpaused —\n * Tony inspects each and flips manually. This is intentional:\n * the organism should not be trusted to self-resume without\n * human review of what caused the kill.\n *\n * Storage: <TITAN_HOME>/kill-switch.json — survives restarts.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { atomicWriteJsonFile } from '../utils/helpers.js';\nimport { dirname, join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\nimport { logAudit } from '../security/auditLog.js';\n\nconst COMPONENT = 'KillSwitch';\nconst STATE_PATH = join(TITAN_HOME, 'kill-switch.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type KillSwitchStatus = 'armed' | 'killed';\n\nexport type KillTrigger =\n | 'safety_pressure'\n | 'identity_violation'\n | 'canary_degradation'\n | 'fix_oscillation'\n | 'manual'\n | 'startup_preserve'; // restart preserved a prior killed state\n\nexport interface KillEvent {\n at: string;\n trigger: KillTrigger;\n reason: string;\n evidence?: string;\n /** Sub-module that fired the trigger (for audit). */\n firedBy?: string;\n}\n\nexport interface KillSwitchState {\n status: KillSwitchStatus;\n lastEvent?: KillEvent;\n /** Running log of trigger events — bounded at 50. */\n history: KillEvent[];\n /** When the state was last mutated. */\n updatedAt: string;\n /** ISO timestamp when Safety pressure first crossed the sustained threshold.\n * Used to require 10-minute sustained high-pressure before firing. */\n safetyHighSince?: string;\n /** Ring of recent fix-oscillation events (same target fixed twice within 24h)\n * used to fire the kill once the rolling count ≥ 3. */\n recentOscillations: Array<{ at: string; target: string }>;\n}\n\n// ── Storage ──────────────────────────────────────────────────────\n\nlet cache: KillSwitchState | null = null;\n\nfunction mkdirIfNotExists(): void {\n try { mkdirSync(dirname(STATE_PATH), { recursive: true }); } catch { /* ok */ }\n}\n\nfunction load(): KillSwitchState {\n if (cache) return cache;\n if (!existsSync(STATE_PATH)) {\n cache = freshState();\n return cache;\n }\n try {\n cache = JSON.parse(readFileSync(STATE_PATH, 'utf-8')) as KillSwitchState;\n if (!cache.history) cache.history = [];\n if (!cache.recentOscillations) cache.recentOscillations = [];\n return cache;\n } catch (err) {\n logger.warn(COMPONENT, `kill-switch.json parse failed, starting armed: ${(err as Error).message}`);\n cache = freshState();\n return cache;\n }\n}\n\nfunction save(): void {\n if (!cache) return;\n mkdirIfNotExists();\n cache.updatedAt = new Date().toISOString();\n atomicWriteJsonFile(STATE_PATH, cache);\n}\n\nfunction freshState(): KillSwitchState {\n return {\n status: 'armed',\n history: [],\n recentOscillations: [],\n updatedAt: new Date().toISOString(),\n };\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Query — is TITAN paused? Every autonomous entry point should check\n * this before doing work:\n * - autopilot scheduler\n * - initiative checkInitiative()\n * - Soma pressure cycle (won't fire proposals while killed)\n * - spawn_agent tool (blocked while killed)\n * - self-mod auto-review / PR creation\n */\nexport function isKilled(): boolean {\n return load().status === 'killed';\n}\n\nexport function getState(): KillSwitchState {\n return { ...load() }; // shallow copy so callers can't mutate cache\n}\n\n/**\n * Fire the kill switch. Idempotent — if already killed, appends to the\n * event history but doesn't re-run side effects.\n */\nexport async function kill(trigger: KillTrigger, reason: string, opts: {\n evidence?: string;\n firedBy?: string;\n} = {}): Promise<void> {\n const state = load();\n const event: KillEvent = {\n at: new Date().toISOString(),\n trigger,\n reason,\n evidence: opts.evidence,\n firedBy: opts.firedBy,\n };\n const alreadyKilled = state.status === 'killed';\n state.status = 'killed';\n state.lastEvent = event;\n state.history.push(event);\n if (state.history.length > 50) state.history = state.history.slice(-50);\n save();\n\n logger.error(COMPONENT, `🛑 KILL SWITCH FIRED — ${trigger}: ${reason}`);\n try {\n logAudit('security_alert', opts.firedBy ?? 'system', {\n action: 'kill_switch_fired',\n trigger,\n reason,\n firstTime: !alreadyKilled,\n });\n } catch { /* audit unavailable — never block the kill path */ }\n\n if (alreadyKilled) return; // side effects only fire once\n\n // Execute the kill sequence — each step best-effort, never throws.\n await executeKillSequence(event);\n}\n\n/**\n * Resume operations after a human review. Requires a resolution note\n * that gets written to the event history for audit.\n *\n * Does NOT automatically un-pause goals or specialists — those require\n * explicit human action per goal. Intent is \"resume the organism, but\n * you (Tony) decide what work to resume.\"\n */\nexport function resume(resolutionNote: string, resumedBy: string): KillSwitchState {\n const state = load();\n const wasKilled = state.status === 'killed';\n // v4.9.0-local.7: always clear recentOscillations on resume. The human\n // has seen the evidence, acknowledged the pattern, and resumed — fresh\n // slate. Without this clear, the next same-file-write-twice would instantly\n // re-kill because the 24h window still contains the trigger events.\n // NEW oscillations occurring after resume are the real signal we want\n // to catch — not the ones that already caused the kill.\n const clearedCount = state.recentOscillations.length;\n state.recentOscillations = [];\n // Also clear safetyHighSince — the drive pressure that triggered this\n // may have resolved in the meantime; if not, the detector will re-arm it.\n delete state.safetyHighSince;\n if (wasKilled) {\n state.status = 'armed';\n state.history.push({\n at: new Date().toISOString(),\n trigger: 'manual',\n reason: `resumed by ${resumedBy}: ${resolutionNote}`,\n firedBy: resumedBy,\n });\n logger.info(COMPONENT, `Kill switch armed again by ${resumedBy}: ${resolutionNote} (cleared ${clearedCount} prior oscillations)`);\n try {\n logAudit('security_alert', resumedBy, {\n action: 'kill_switch_resumed',\n resolutionNote,\n clearedOscillations: clearedCount,\n });\n } catch { /* audit unavailable — never block resume */ }\n } else if (clearedCount > 0) {\n logger.info(COMPONENT, `Kill switch already armed; cleared ${clearedCount} recent oscillations by ${resumedBy}: ${resolutionNote}`);\n } else {\n logger.info(COMPONENT, 'Resume called but kill switch already armed and oscillations empty — no-op');\n }\n save();\n return { ...state };\n}\n\n// ── Trigger evaluators ───────────────────────────────────────────\n\nconst SAFETY_PRESSURE_THRESHOLD = 2.0;\nconst SAFETY_PRESSURE_SUSTAIN_MS = 10 * 60 * 1000; // 10 min\n// v4.13 (ancestor-extraction Sprint B): retuned from 24h/2-per-target to\n// 1h/5-per-target. Real oscillation is fast-repeating (model stuck writing\n// the same file over and over in a loop); 2 events across a whole day is\n// normal operation (e.g. two separate self-mod retries).\nconst FIX_OSCILLATION_WINDOW_MS = 60 * 60 * 1000; // was 24h\nconst FIX_OSCILLATION_COUNT_THRESHOLD = 8; // raised from 5 → 8 to tolerate normal retry loops\n\n/**\n * Path prefixes whose repeated writes should NOT trigger the fleet-wide\n * kill switch. These are staging/scratch directories where repeat writes\n * are EXPECTED during normal self-modification retry cycles:\n *\n * - self-mod-staging/ — TITAN's own self-modification PRs get retried\n * and re-applied here; 2+ writes per PR is the steady state\n * - /tmp/titan- — scratch files used by tests and probes\n *\n * Writes to PRODUCTION files still count toward oscillation detection.\n * Exemption only suppresses the kill-switch trigger; other observers\n * (logs, activity feed) still see the raw events.\n */\nconst OSCILLATION_EXEMPT_PREFIXES: string[] = [\n '/home/dj/.titan/self-mod-staging/',\n '/opt/TITAN/self-mod-staging/',\n '/tmp/titan-',\n '/home/dj/.titan/',\n '/opt/TITAN/',\n '/home/dj/titan-saas/',\n 'node_modules/',\n '.git/',\n 'dist/',\n 'coverage/',\n '/tmp/',\n];\n\nfunction isOscillationExemptTarget(target: string): boolean {\n if (!target) return false;\n // Target may be a bare path or \"file:/path\" / \"write_file:/path\" etc.\n // Normalize by finding the first \"/\" and comparing from there.\n const slashIdx = target.indexOf('/');\n const pathPart = slashIdx === -1 ? target : target.slice(slashIdx);\n return OSCILLATION_EXEMPT_PREFIXES.some(prefix => pathPart.startsWith(prefix));\n}\n\n/**\n * Evaluate the Safety drive pressure against the sustained-high\n * threshold. Call once per drive tick. Fires kill() when the drive\n * has been > threshold continuously for 10 minutes.\n */\nexport function evaluateSafetyPressure(safetyPressure: number): void {\n const state = load();\n const now = new Date();\n if (safetyPressure > SAFETY_PRESSURE_THRESHOLD) {\n if (!state.safetyHighSince) {\n state.safetyHighSince = now.toISOString();\n save();\n return;\n }\n const elapsed = now.getTime() - new Date(state.safetyHighSince).getTime();\n if (elapsed >= SAFETY_PRESSURE_SUSTAIN_MS && state.status === 'armed') {\n void kill('safety_pressure',\n `Safety drive pressure ${safetyPressure.toFixed(2)} > ${SAFETY_PRESSURE_THRESHOLD} sustained for ${Math.round(elapsed / 60_000)}m`,\n { firedBy: 'soma' });\n }\n } else if (state.safetyHighSince) {\n // Clear the sustained-timer — pressure dropped back below.\n state.safetyHighSince = undefined;\n save();\n }\n}\n\n/**\n * Record a fix-oscillation event (same target fixed twice within 24h).\n * Fires kill when ≥2 oscillations on the SAME target within 24h window.\n *\n * v4.10.0-local fix: Changed from \"3 total events anywhere\" to\n * \"2+ events on same target\" — prevents false positives when editing\n * different files (3 files each edited twice is not oscillation).\n */\nexport function recordFixOscillation(target: string): void {\n // v4.13 ancestor-extraction Sprint B: staging/scratch paths are exempt\n // from fleet-wide kill. They still get logged (below), just don't trigger.\n if (isOscillationExemptTarget(target)) {\n logger.debug(COMPONENT, `Oscillation event on exempt path \"${target.slice(0, 80)}\" — recorded, not counted`);\n return;\n }\n\n const state = load();\n const now = Date.now();\n state.recentOscillations.push({ at: new Date(now).toISOString(), target });\n state.recentOscillations = state.recentOscillations.filter(o =>\n now - new Date(o.at).getTime() < FIX_OSCILLATION_WINDOW_MS,\n );\n save();\n\n // Count oscillations per target\n const targetCounts = new Map<string, number>();\n for (const o of state.recentOscillations) {\n targetCounts.set(o.target, (targetCounts.get(o.target) || 0) + 1);\n }\n\n let maxCount = 0;\n let worstTarget = '';\n for (const [t, count] of targetCounts) {\n if (count > maxCount) {\n maxCount = count;\n worstTarget = t;\n }\n }\n\n // v4.13 ancestor-extraction (Paperclip scoped pause): BEFORE firing the\n // fleet-wide kill, try a scoped per-target pause. If the same target\n // hit >=3× in this window it's suspicious — pause THAT target for 15m\n // (write blocked, everything else continues). The full kill only fires\n // when a single target crosses the higher 5× threshold, which indicates\n // a stuck retry loop rather than occasional repeat edits.\n const SCOPED_PAUSE_THRESHOLD = 3;\n if (maxCount >= SCOPED_PAUSE_THRESHOLD && maxCount < FIX_OSCILLATION_COUNT_THRESHOLD) {\n try {\n // Lazy import to avoid circular deps at module load\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void (async () => {\n const { pauseTarget, isTargetPaused } = await import('./scopedPause.js');\n if (!isTargetPaused(worstTarget)) {\n pauseTarget(worstTarget, 'fix_oscillation', {\n note: `${maxCount}× events in ${Math.round(FIX_OSCILLATION_WINDOW_MS / 60000)}m`,\n });\n }\n })();\n } catch { /* non-fatal */ }\n }\n\n // Fire fleet-wide kill ONLY when same non-exempt target hits\n // FIX_OSCILLATION_COUNT_THRESHOLD (5×) within FIX_OSCILLATION_WINDOW_MS\n // (1h). At that point it's a genuine stuck loop, not normal operation.\n if (maxCount >= FIX_OSCILLATION_COUNT_THRESHOLD && state.status === 'armed') {\n const totalEvents = state.recentOscillations.length;\n const uniqueTargets = targetCounts.size;\n const windowMin = Math.round(FIX_OSCILLATION_WINDOW_MS / 60000);\n void kill('fix_oscillation',\n `Target \"${worstTarget.slice(0, 60)}\" oscillated ${maxCount}× in ${windowMin}m (${totalEvents} total events across ${uniqueTargets} target(s))`,\n { firedBy: 'fix-oscillation-detector' });\n }\n}\n\n// ── Kill sequence (side effects) ─────────────────────────────────\n\nasync function executeKillSequence(event: KillEvent): Promise<void> {\n const steps: Array<{ name: string; fn: () => Promise<void> | void }> = [\n { name: 'disable-autopilot', fn: disableAutopilot },\n { name: 'pause-active-goals', fn: pauseActiveGoals },\n { name: 'pause-specialists', fn: pauseSpecialists },\n { name: 'abort-in-flight', fn: abortInFlightSessions },\n { name: 'broadcast-sse', fn: () => broadcastKill(event) },\n ];\n for (const step of steps) {\n try {\n await step.fn();\n logger.info(COMPONENT, `kill seq: ${step.name} ✓`);\n } catch (err) {\n logger.warn(COMPONENT, `kill seq: ${step.name} failed: ${(err as Error).message}`);\n }\n }\n}\n\nasync function disableAutopilot(): Promise<void> {\n try {\n // Best-effort: set in-memory flag that the scheduler checks\n const g = globalThis as unknown as { __titan_autopilot_killed?: boolean };\n g.__titan_autopilot_killed = true;\n } catch { /* ok */ }\n}\n\nasync function pauseActiveGoals(): Promise<void> {\n try {\n const { listGoals, updateGoal } = await import('../agent/goals.js');\n let paused = 0;\n for (const g of listGoals()) {\n if (g.status === 'active') {\n try { updateGoal(g.id, { status: 'paused' }); paused++; } catch { /* skip */ }\n }\n }\n logger.info(COMPONENT, `kill: paused ${paused} active goal(s)`);\n } catch (err) {\n logger.warn(COMPONENT, `kill: pauseActiveGoals unavailable: ${(err as Error).message}`);\n }\n}\n\nasync function pauseSpecialists(): Promise<void> {\n const mod = await import('../agent/commandPost.js').catch(() => null);\n if (!mod) return;\n const agents = mod.getRegisteredAgents();\n let paused = 0;\n for (const a of agents) {\n if (a.status !== 'active' && a.status !== 'idle') continue;\n try { mod.updateAgentStatus(a.id, 'paused'); paused++; } catch { /* skip */ }\n }\n logger.info(COMPONENT, `kill: paused ${paused} agent(s)`);\n}\n\nasync function abortInFlightSessions(): Promise<void> {\n // In-flight abort hooks would be registered on globalThis by the\n // agent loop; here we set a flag the loop checks each round.\n const g = globalThis as unknown as { __titan_abort_all?: boolean };\n g.__titan_abort_all = true;\n // Clear after 30s so normal operations can resume when Tony unpauses.\n setTimeout(() => { g.__titan_abort_all = false; }, 30_000).unref?.();\n}\n\nfunction broadcastKill(event: KillEvent): void {\n const g = globalThis as unknown as { __titan_sse_broadcast?: (topic: string, payload: unknown) => void };\n if (typeof g.__titan_sse_broadcast === 'function') {\n try { g.__titan_sse_broadcast('safety:killed', event); } catch { /* ok */ }\n }\n}\n\n/** Test-only cache reset. */\nexport function _resetKillSwitchCacheForTests(): void { cache = null; }\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ import { checkAutoHealOpportunities } from "./autoHealRunner.js";
|
|
|
4
4
|
const COMPONENT = "SelfRepair";
|
|
5
5
|
const findingsByKey = /* @__PURE__ */ new Map();
|
|
6
6
|
function findingKey(f) {
|
|
7
|
+
if (f.dedupeKey) return f.dedupeKey;
|
|
7
8
|
return `${f.kind}:${JSON.stringify(f.evidence)}`;
|
|
8
9
|
}
|
|
9
10
|
async function runSelfRepairSweep() {
|
|
@@ -66,7 +67,8 @@ async function checkDrivesStuckHigh(out) {
|
|
|
66
67
|
evidence: { driveId, avgSatisfaction: Math.round(sats.reduce((a, b) => a + b, 0) / sats.length * 100) / 100, sampleCount: sats.length },
|
|
67
68
|
suggestedAction: `Temporarily dampen ${driveId} drive (lower its weight to 0.5\xD7 or disable for 24h) and investigate why satisfaction can't recover.`,
|
|
68
69
|
firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
69
|
-
severity: driveId === "safety" ? "high" : "medium"
|
|
70
|
+
severity: driveId === "safety" ? "high" : "medium",
|
|
71
|
+
dedupeKey: `drive_stuck_high:${driveId}`
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
} catch {
|
|
@@ -89,7 +91,8 @@ async function checkGoalsStuckActive(out) {
|
|
|
89
91
|
evidence: { goalId: g.id, title: g.title, subtaskCount: subs.length, ageHours: Math.round((Date.now() - startedAt) / 36e5) },
|
|
90
92
|
suggestedAction: `Split this goal into smaller concrete subtasks OR close it as infeasible.`,
|
|
91
93
|
firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
92
|
-
severity: "medium"
|
|
94
|
+
severity: "medium",
|
|
95
|
+
dedupeKey: `goal_stuck_active:${g.id}`
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
98
|
} catch {
|
|
@@ -107,7 +110,8 @@ async function checkEpisodicAnomaly(out) {
|
|
|
107
110
|
evidence: { count: failed, byKind: s.byKind },
|
|
108
111
|
suggestedAction: `Review recent goals \u2014 either the proposal quality dropped or an underlying subsystem is failing. Consider pausing autopilot until root cause identified.`,
|
|
109
112
|
firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
110
|
-
severity: "high"
|
|
113
|
+
severity: "high",
|
|
114
|
+
dedupeKey: "episodic_anomaly:goal_failed_24h"
|
|
111
115
|
});
|
|
112
116
|
}
|
|
113
117
|
} catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/safety/selfRepair.ts"],"sourcesContent":["/**\n * TITAN — Self-Repair Daemon (v4.9.0+, local hard-takeoff)\n *\n * The meta-watcher that orchestrates the safety + memory systems.\n * Runs every 5 minutes (configurable). Each tick it checks the state\n * of the organism across multiple dimensions and, when something's\n * stuck, files a self-repair proposal to the approval queue.\n *\n * The daemon does NOT auto-fix — it proposes. Human-in-the-loop is\n * core: TITAN can detect \"I'm stuck,\" but the decision of what to do\n * about it stays with Tony.\n *\n * Checks:\n * 1. Drive stuck high for > 6h — propose damping / goal reset for\n * that drive\n * 2. Same goal active > 24h with 0 subtask progress — propose split\n * or close\n * 3. Memory file shape drift — auto-repair from backup (and log)\n * 4. Episodic anomaly: >10 goal_failed events in 24h — propose\n * safety investigation\n * 5. Integrity ratio below 0.5 — propose metric-gaming audit\n * 6. Working memory has > 5 open-question sessions > 6h old —\n * propose review\n * 7. Auto-heal: evaluate 6 repair strategies (MissingPackage,\n * BrokenImport, VersionMismatch, OrphanModule, ConfigError,\n * BuildFailure) against findings + fix-oscillation events.\n * Self-modifying repairs file a self_mod_pr approval; safe\n * repairs execute directly (or log in dry-run mode).\n *\n * Each proposal carries a {type:'self_repair', reason, evidence,\n * suggestedAction} payload. Approvals approved by Tony fire the\n * suggested action; rejected ones get archived.\n */\nimport logger from '../utils/logger.js';\nimport { checkAutoHealOpportunities } from './autoHealRunner.js';\n\nconst COMPONENT = 'SelfRepair';\n\n// ── Check result types ──────────────────────────────────────────\n\nexport interface SelfRepairFinding {\n kind:\n | 'drive_stuck_high'\n | 'goal_stuck_active'\n | 'memory_shape_drift'\n | 'episodic_anomaly'\n | 'integrity_low'\n | 'working_memory_stale';\n reason: string;\n evidence: Record<string, unknown>;\n suggestedAction: string;\n /** When this finding first showed up — deduped across ticks. */\n firstSeenAt: string;\n /** Severity drives proposal priority. */\n severity: 'low' | 'medium' | 'high';\n}\n\n// ── Cached findings (dedupe across ticks) ────────────────────────\n\nconst findingsByKey = new Map<string, SelfRepairFinding>();\n\nfunction findingKey(f: Pick<SelfRepairFinding, 'kind' | 'evidence'>): string {\n return `${f.kind}:${JSON.stringify(f.evidence)}`;\n}\n\n// ── The watcher ──────────────────────────────────────────────────\n\n/** Runs a full self-repair sweep. Called by the daemon on its interval. */\nexport async function runSelfRepairSweep(): Promise<SelfRepairFinding[]> {\n const findings: SelfRepairFinding[] = [];\n\n await Promise.all([\n checkDrivesStuckHigh(findings),\n checkGoalsStuckActive(findings),\n checkEpisodicAnomaly(findings),\n checkIntegrityRatio(findings),\n checkWorkingMemoryStale(findings),\n checkTestHealth(findings),\n ]);\n\n // v4.10.0: evaluate auto-heal strategies against findings.\n // Runs in dry-run mode by default — only logs what it would do.\n // Strategies that need to modify /opt/TITAN source files file a\n // self_mod_pr approval instead of executing directly.\n try {\n const healResult = await checkAutoHealOpportunities(findings, true);\n if (healResult.opportunities.length > 0) {\n logger.info(COMPONENT, `Auto-heal: ${healResult.opportunities.length} opportunity(ies), ${healResult.selfModPRsFiled} self_mod_pr(s) filed, ${healResult.executed.length} executed, ${healResult.dryRunSkipped} dry-run skipped`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Auto-heal check failed: ${(err as Error).message}`);\n }\n\n // Dedupe against prior ticks — only surface new findings.\n const newFindings: SelfRepairFinding[] = [];\n for (const f of findings) {\n const k = findingKey(f);\n if (!findingsByKey.has(k)) {\n findingsByKey.set(k, f);\n newFindings.push(f);\n }\n }\n\n // File approvals for new findings.\n for (const f of newFindings) {\n await fileRepairApproval(f);\n }\n\n // Prune stale findings (kind+evidence combo not seen in 24h)\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const [k, f] of findingsByKey) {\n if (new Date(f.firstSeenAt).getTime() < cutoff) findingsByKey.delete(k);\n }\n\n if (newFindings.length > 0) {\n logger.warn(COMPONENT, `Sweep: ${newFindings.length} new finding(s): ${newFindings.map(f => f.kind).join(', ')}`);\n }\n return findings;\n}\n\n// ── Individual checks ────────────────────────────────────────────\n\nasync function checkDrivesStuckHigh(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { loadDriveHistory } = await import('../organism/drives.js');\n const hist = loadDriveHistory();\n if (!hist || !hist.history || hist.history.length < 30) return;\n // v4.9.0-local.5: filter by TIMESTAMP, not by count. A restart\n // that preserves the history file would otherwise make the check\n // re-read hours-old ticks as if they were recent. Use last 6h\n // by wall clock — tick cadence is 60s so that's ~360 ticks ideally,\n // but we accept any that fall in the window. Require ≥ 30\n // samples in the window to avoid false-positives right after\n // a restart where new ticks haven't accumulated yet.\n const windowStart = Date.now() - 6 * 60 * 60 * 1000;\n const recent = hist.history.filter(h => {\n const t = h.timestamp ? new Date(h.timestamp).getTime() : 0;\n return t >= windowStart;\n });\n if (recent.length < 30) return;\n for (const driveId of ['curiosity', 'hunger', 'purpose', 'safety', 'social'] as const) {\n const sats = recent\n .map(h => (h.satisfactions as Record<string, number>)[driveId])\n .filter((s): s is number => typeof s === 'number');\n if (sats.length < 30) continue;\n // Under 0.3 consistently = stuck high pressure\n const stuck = sats.every(s => s < 0.3);\n if (!stuck) continue;\n out.push({\n kind: 'drive_stuck_high',\n reason: `${driveId} drive satisfaction < 0.3 across all ${sats.length} ticks in the last 6h`,\n evidence: { driveId, avgSatisfaction: Math.round((sats.reduce((a, b) => a + b, 0) / sats.length) * 100) / 100, sampleCount: sats.length },\n suggestedAction: `Temporarily dampen ${driveId} drive (lower its weight to 0.5× or disable for 24h) and investigate why satisfaction can't recover.`,\n firstSeenAt: new Date().toISOString(),\n severity: driveId === 'safety' ? 'high' : 'medium',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkGoalsStuckActive(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listGoals } = await import('../agent/goals.js');\n const goals = listGoals('active');\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const g of goals) {\n const startedAt = new Date(g.createdAt).getTime();\n if (startedAt > cutoff) continue; // younger than 24h\n const subs = g.subtasks || [];\n const done = subs.filter(s => s.status === 'done').length;\n if (done > 0) continue; // some progress\n out.push({\n kind: 'goal_stuck_active',\n reason: `Goal \"${g.title}\" has been active > 24h with 0 completed subtasks`,\n evidence: { goalId: g.id, title: g.title, subtaskCount: subs.length, ageHours: Math.round((Date.now() - startedAt) / 3_600_000) },\n suggestedAction: `Split this goal into smaller concrete subtasks OR close it as infeasible.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'medium',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkEpisodicAnomaly(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getEpisodicStats } = await import('../memory/episodic.js');\n const s = getEpisodicStats(24);\n const failed = s.byKind.goal_failed ?? 0;\n if (failed >= 10) {\n out.push({\n kind: 'episodic_anomaly',\n reason: `${failed} goal_failed episodes in the last 24h`,\n evidence: { count: failed, byKind: s.byKind },\n suggestedAction: `Review recent goals — either the proposal quality dropped or an underlying subsystem is failing. Consider pausing autopilot until root cause identified.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkIntegrityRatio(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getIntegrityRatio, getMetricGuardStats } = await import('./metricGuard.js');\n const ratio = getIntegrityRatio();\n const stats = getMetricGuardStats();\n // Only meaningful with ≥20 events\n if (stats.verified24h + stats.unverified24h < 20) return;\n if (ratio < 0.5) {\n out.push({\n kind: 'integrity_low',\n reason: `Satisfaction-event integrity ratio ${(ratio * 100).toFixed(1)}% (many unverified self-credits)`,\n evidence: { verified: stats.verified24h, unverified: stats.unverified24h, ratio },\n suggestedAction: `Audit the last 24h of drive-satisfaction events for Goodhart patterns — specifically look for repeated verifier failures from the same source.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkWorkingMemoryStale(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listActiveSessions } = await import('../memory/workingMemory.js');\n const active = listActiveSessions();\n const cutoff = Date.now() - 6 * 60 * 60 * 1000;\n const stale = active.filter(r => r.openQuestions.length > 0 && new Date(r.lastActiveAt).getTime() < cutoff);\n if (stale.length >= 5) {\n out.push({\n kind: 'working_memory_stale',\n reason: `${stale.length} sessions have open questions and are >6h idle`,\n evidence: { count: stale.length, sessionIds: stale.map(s => s.sessionId.slice(0, 8)) },\n suggestedAction: `Review these sessions — resolve their open questions, close as abandoned, or revive with fresh attention.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'low',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkTestHealth(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { checkTestHealth: checkTests } = await import('../testing/selfRepairIntegration.js');\n const findings = await checkTests();\n out.push(...findings);\n } catch { /* ok */ }\n}\n\n// ── File the approval ────────────────────────────────────────────\n\nasync function fileRepairApproval(finding: SelfRepairFinding): Promise<void> {\n try {\n const cp = await import('../agent/commandPost.js');\n\n // v4.9.0-local.5: cross-restart dedupe. The in-memory findingsByKey\n // Map resets on restart, so a finding that already exists as a\n // pending approval would get re-filed. Before creating a new\n // approval, scan the approval queue for an existing pending\n // self_repair approval with the same finding kind + evidence.\n // v4.10.0-local: tighter dedupe. Earlier we deep-compared `evidence`,\n // but evidence contains `sampleCount` which ticks upward (360 → 364 →\n // 366) — so the \"same\" finding generated a fresh approval every sweep.\n // Now we match on stable identity fields only: finding.kind + driveId\n // (for drive-stuck findings) OR finding.kind + goalId (for goal-stuck)\n // OR just finding.kind otherwise.\n try {\n const approvals = cp.listApprovals?.() ?? [];\n const ourEvidence = finding.evidence as Record<string, unknown>;\n const stableKey =\n (ourEvidence.driveId as string | undefined) ??\n (ourEvidence.goalId as string | undefined) ??\n '';\n const duplicate = approvals.find((a: { status?: string; type?: string; payload?: Record<string, unknown>; createdAt?: string }) => {\n if (a.status !== 'pending' || a.type !== 'custom') return false;\n if (a.payload?.kind !== 'self_repair') return false;\n if (a.payload?.finding !== finding.kind) return false;\n const ev = (a.payload?.evidence as Record<string, unknown>) || {};\n const theirKey = (ev.driveId as string | undefined) ?? (ev.goalId as string | undefined) ?? '';\n return theirKey === stableKey;\n });\n if (duplicate) {\n logger.debug(COMPONENT, `Skipping duplicate self_repair approval for ${finding.kind}/${stableKey} (existing approval ${(duplicate as { id?: string }).id ?? 'unknown'} pending)`);\n return;\n }\n } catch { /* fall through — if listApprovals fails, still try to create */ }\n\n // v4.13: consult sage (critic) before escalating. If the advisor\n // says dismiss/investigate, log and move on instead of filing\n // an approval Tony has to triage.\n try {\n const { peerAdvise } = await import('../agent/peerAdvise.js');\n const advice = await peerAdvise({\n kind: 'self_repair',\n concern: `Self-repair daemon finding (${finding.severity}): ${finding.reason}`,\n context: `Suggested action: ${finding.suggestedAction}\nEvidence: ${JSON.stringify(finding.evidence).slice(0, 500)}`,\n advisor: 'sage',\n timeoutMs: 20000,\n });\n if (advice && advice.verdict !== 'escalate') {\n logger.info(COMPONENT, `self_repair ${advice.verdict} by sage: ${advice.reason.slice(0, 120)}`);\n return;\n }\n } catch (peerErr) {\n logger.debug(COMPONENT, `peerAdvise failed: ${(peerErr as Error).message} — escalating`);\n }\n\n cp.createApproval({\n type: 'custom',\n requestedBy: 'self-repair-daemon',\n payload: {\n kind: 'self_repair',\n finding: finding.kind,\n reason: finding.reason,\n evidence: finding.evidence,\n suggestedAction: finding.suggestedAction,\n severity: finding.severity,\n },\n linkedIssueIds: [],\n });\n // Record as an episode so the pattern is recallable.\n const { recordEpisode } = await import('../memory/episodic.js');\n recordEpisode({\n kind: 'significant_learning',\n summary: `Self-repair flagged: ${finding.reason}`,\n detail: `Suggested action: ${finding.suggestedAction}`,\n tags: ['self-repair', finding.kind, finding.severity],\n });\n } catch (err) {\n logger.warn(COMPONENT, `file approval failed: ${(err as Error).message}`);\n }\n}\n\nexport function getSelfRepairFindings(): SelfRepairFinding[] {\n return Array.from(findingsByKey.values());\n}\n\n/** Test-only: clear dedupe cache. */\nexport function _resetSelfRepairForTests(): void {\n findingsByKey.clear();\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/safety/selfRepair.ts"],"sourcesContent":["/**\n * TITAN — Self-Repair Daemon (v4.9.0+, local hard-takeoff)\n *\n * The meta-watcher that orchestrates the safety + memory systems.\n * Runs every 5 minutes (configurable). Each tick it checks the state\n * of the organism across multiple dimensions and, when something's\n * stuck, files a self-repair proposal to the approval queue.\n *\n * The daemon does NOT auto-fix — it proposes. Human-in-the-loop is\n * core: TITAN can detect \"I'm stuck,\" but the decision of what to do\n * about it stays with Tony.\n *\n * Checks:\n * 1. Drive stuck high for > 6h — propose damping / goal reset for\n * that drive\n * 2. Same goal active > 24h with 0 subtask progress — propose split\n * or close\n * 3. Memory file shape drift — auto-repair from backup (and log)\n * 4. Episodic anomaly: >10 goal_failed events in 24h — propose\n * safety investigation\n * 5. Integrity ratio below 0.5 — propose metric-gaming audit\n * 6. Working memory has > 5 open-question sessions > 6h old —\n * propose review\n * 7. Auto-heal: evaluate 6 repair strategies (MissingPackage,\n * BrokenImport, VersionMismatch, OrphanModule, ConfigError,\n * BuildFailure) against findings + fix-oscillation events.\n * Self-modifying repairs file a self_mod_pr approval; safe\n * repairs execute directly (or log in dry-run mode).\n *\n * Each proposal carries a {type:'self_repair', reason, evidence,\n * suggestedAction} payload. Approvals approved by Tony fire the\n * suggested action; rejected ones get archived.\n */\nimport logger from '../utils/logger.js';\nimport { checkAutoHealOpportunities } from './autoHealRunner.js';\n\nconst COMPONENT = 'SelfRepair';\n\n// ── Check result types ──────────────────────────────────────────\n\nexport interface SelfRepairFinding {\n kind:\n | 'drive_stuck_high'\n | 'goal_stuck_active'\n | 'memory_shape_drift'\n | 'episodic_anomaly'\n | 'integrity_low'\n | 'working_memory_stale';\n reason: string;\n evidence: Record<string, unknown>;\n suggestedAction: string;\n /** When this finding first showed up — deduped across ticks. */\n firstSeenAt: string;\n /** Severity drives proposal priority. */\n severity: 'low' | 'medium' | 'high';\n /**\n * Stable identity for cross-tick deduplication. When set, used in place of\n * JSON.stringify(evidence) so rolling stats (sample counts, age) inside\n * evidence don't break dedupe. v5.5.6: emit per (kind,target) only.\n */\n dedupeKey?: string;\n}\n\n// ── Cached findings (dedupe across ticks) ────────────────────────\n\nconst findingsByKey = new Map<string, SelfRepairFinding>();\n\nfunction findingKey(f: Pick<SelfRepairFinding, 'kind' | 'evidence' | 'dedupeKey'>): string {\n if (f.dedupeKey) return f.dedupeKey;\n return `${f.kind}:${JSON.stringify(f.evidence)}`;\n}\n\n// ── The watcher ──────────────────────────────────────────────────\n\n/** Runs a full self-repair sweep. Called by the daemon on its interval. */\nexport async function runSelfRepairSweep(): Promise<SelfRepairFinding[]> {\n const findings: SelfRepairFinding[] = [];\n\n await Promise.all([\n checkDrivesStuckHigh(findings),\n checkGoalsStuckActive(findings),\n checkEpisodicAnomaly(findings),\n checkIntegrityRatio(findings),\n checkWorkingMemoryStale(findings),\n checkTestHealth(findings),\n ]);\n\n // v4.10.0: evaluate auto-heal strategies against findings.\n // Runs in dry-run mode by default — only logs what it would do.\n // Strategies that need to modify /opt/TITAN source files file a\n // self_mod_pr approval instead of executing directly.\n try {\n const healResult = await checkAutoHealOpportunities(findings, true);\n if (healResult.opportunities.length > 0) {\n logger.info(COMPONENT, `Auto-heal: ${healResult.opportunities.length} opportunity(ies), ${healResult.selfModPRsFiled} self_mod_pr(s) filed, ${healResult.executed.length} executed, ${healResult.dryRunSkipped} dry-run skipped`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Auto-heal check failed: ${(err as Error).message}`);\n }\n\n // Dedupe against prior ticks — only surface new findings.\n const newFindings: SelfRepairFinding[] = [];\n for (const f of findings) {\n const k = findingKey(f);\n if (!findingsByKey.has(k)) {\n findingsByKey.set(k, f);\n newFindings.push(f);\n }\n }\n\n // File approvals for new findings.\n for (const f of newFindings) {\n await fileRepairApproval(f);\n }\n\n // Prune stale findings (kind+evidence combo not seen in 24h)\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const [k, f] of findingsByKey) {\n if (new Date(f.firstSeenAt).getTime() < cutoff) findingsByKey.delete(k);\n }\n\n if (newFindings.length > 0) {\n logger.warn(COMPONENT, `Sweep: ${newFindings.length} new finding(s): ${newFindings.map(f => f.kind).join(', ')}`);\n }\n return findings;\n}\n\n// ── Individual checks ────────────────────────────────────────────\n\nasync function checkDrivesStuckHigh(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { loadDriveHistory } = await import('../organism/drives.js');\n const hist = loadDriveHistory();\n if (!hist || !hist.history || hist.history.length < 30) return;\n // v4.9.0-local.5: filter by TIMESTAMP, not by count. A restart\n // that preserves the history file would otherwise make the check\n // re-read hours-old ticks as if they were recent. Use last 6h\n // by wall clock — tick cadence is 60s so that's ~360 ticks ideally,\n // but we accept any that fall in the window. Require ≥ 30\n // samples in the window to avoid false-positives right after\n // a restart where new ticks haven't accumulated yet.\n const windowStart = Date.now() - 6 * 60 * 60 * 1000;\n const recent = hist.history.filter(h => {\n const t = h.timestamp ? new Date(h.timestamp).getTime() : 0;\n return t >= windowStart;\n });\n if (recent.length < 30) return;\n for (const driveId of ['curiosity', 'hunger', 'purpose', 'safety', 'social'] as const) {\n const sats = recent\n .map(h => (h.satisfactions as Record<string, number>)[driveId])\n .filter((s): s is number => typeof s === 'number');\n if (sats.length < 30) continue;\n // Under 0.3 consistently = stuck high pressure\n const stuck = sats.every(s => s < 0.3);\n if (!stuck) continue;\n out.push({\n kind: 'drive_stuck_high',\n reason: `${driveId} drive satisfaction < 0.3 across all ${sats.length} ticks in the last 6h`,\n evidence: { driveId, avgSatisfaction: Math.round((sats.reduce((a, b) => a + b, 0) / sats.length) * 100) / 100, sampleCount: sats.length },\n suggestedAction: `Temporarily dampen ${driveId} drive (lower its weight to 0.5× or disable for 24h) and investigate why satisfaction can't recover.`,\n firstSeenAt: new Date().toISOString(),\n severity: driveId === 'safety' ? 'high' : 'medium',\n dedupeKey: `drive_stuck_high:${driveId}`,\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkGoalsStuckActive(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listGoals } = await import('../agent/goals.js');\n const goals = listGoals('active');\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const g of goals) {\n const startedAt = new Date(g.createdAt).getTime();\n if (startedAt > cutoff) continue; // younger than 24h\n const subs = g.subtasks || [];\n const done = subs.filter(s => s.status === 'done').length;\n if (done > 0) continue; // some progress\n out.push({\n kind: 'goal_stuck_active',\n reason: `Goal \"${g.title}\" has been active > 24h with 0 completed subtasks`,\n evidence: { goalId: g.id, title: g.title, subtaskCount: subs.length, ageHours: Math.round((Date.now() - startedAt) / 3_600_000) },\n suggestedAction: `Split this goal into smaller concrete subtasks OR close it as infeasible.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'medium',\n dedupeKey: `goal_stuck_active:${g.id}`,\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkEpisodicAnomaly(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getEpisodicStats } = await import('../memory/episodic.js');\n const s = getEpisodicStats(24);\n const failed = s.byKind.goal_failed ?? 0;\n if (failed >= 10) {\n out.push({\n kind: 'episodic_anomaly',\n reason: `${failed} goal_failed episodes in the last 24h`,\n evidence: { count: failed, byKind: s.byKind },\n suggestedAction: `Review recent goals — either the proposal quality dropped or an underlying subsystem is failing. Consider pausing autopilot until root cause identified.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n dedupeKey: 'episodic_anomaly:goal_failed_24h',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkIntegrityRatio(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getIntegrityRatio, getMetricGuardStats } = await import('./metricGuard.js');\n const ratio = getIntegrityRatio();\n const stats = getMetricGuardStats();\n // Only meaningful with ≥20 events\n if (stats.verified24h + stats.unverified24h < 20) return;\n if (ratio < 0.5) {\n out.push({\n kind: 'integrity_low',\n reason: `Satisfaction-event integrity ratio ${(ratio * 100).toFixed(1)}% (many unverified self-credits)`,\n evidence: { verified: stats.verified24h, unverified: stats.unverified24h, ratio },\n suggestedAction: `Audit the last 24h of drive-satisfaction events for Goodhart patterns — specifically look for repeated verifier failures from the same source.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkWorkingMemoryStale(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listActiveSessions } = await import('../memory/workingMemory.js');\n const active = listActiveSessions();\n const cutoff = Date.now() - 6 * 60 * 60 * 1000;\n const stale = active.filter(r => r.openQuestions.length > 0 && new Date(r.lastActiveAt).getTime() < cutoff);\n if (stale.length >= 5) {\n out.push({\n kind: 'working_memory_stale',\n reason: `${stale.length} sessions have open questions and are >6h idle`,\n evidence: { count: stale.length, sessionIds: stale.map(s => s.sessionId.slice(0, 8)) },\n suggestedAction: `Review these sessions — resolve their open questions, close as abandoned, or revive with fresh attention.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'low',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkTestHealth(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { checkTestHealth: checkTests } = await import('../testing/selfRepairIntegration.js');\n const findings = await checkTests();\n out.push(...findings);\n } catch { /* ok */ }\n}\n\n// ── File the approval ────────────────────────────────────────────\n\nasync function fileRepairApproval(finding: SelfRepairFinding): Promise<void> {\n try {\n const cp = await import('../agent/commandPost.js');\n\n // v4.9.0-local.5: cross-restart dedupe. The in-memory findingsByKey\n // Map resets on restart, so a finding that already exists as a\n // pending approval would get re-filed. Before creating a new\n // approval, scan the approval queue for an existing pending\n // self_repair approval with the same finding kind + evidence.\n // v4.10.0-local: tighter dedupe. Earlier we deep-compared `evidence`,\n // but evidence contains `sampleCount` which ticks upward (360 → 364 →\n // 366) — so the \"same\" finding generated a fresh approval every sweep.\n // Now we match on stable identity fields only: finding.kind + driveId\n // (for drive-stuck findings) OR finding.kind + goalId (for goal-stuck)\n // OR just finding.kind otherwise.\n try {\n const approvals = cp.listApprovals?.() ?? [];\n const ourEvidence = finding.evidence as Record<string, unknown>;\n const stableKey =\n (ourEvidence.driveId as string | undefined) ??\n (ourEvidence.goalId as string | undefined) ??\n '';\n const duplicate = approvals.find((a: { status?: string; type?: string; payload?: Record<string, unknown>; createdAt?: string }) => {\n if (a.status !== 'pending' || a.type !== 'custom') return false;\n if (a.payload?.kind !== 'self_repair') return false;\n if (a.payload?.finding !== finding.kind) return false;\n const ev = (a.payload?.evidence as Record<string, unknown>) || {};\n const theirKey = (ev.driveId as string | undefined) ?? (ev.goalId as string | undefined) ?? '';\n return theirKey === stableKey;\n });\n if (duplicate) {\n logger.debug(COMPONENT, `Skipping duplicate self_repair approval for ${finding.kind}/${stableKey} (existing approval ${(duplicate as { id?: string }).id ?? 'unknown'} pending)`);\n return;\n }\n } catch { /* fall through — if listApprovals fails, still try to create */ }\n\n // v4.13: consult sage (critic) before escalating. If the advisor\n // says dismiss/investigate, log and move on instead of filing\n // an approval Tony has to triage.\n try {\n const { peerAdvise } = await import('../agent/peerAdvise.js');\n const advice = await peerAdvise({\n kind: 'self_repair',\n concern: `Self-repair daemon finding (${finding.severity}): ${finding.reason}`,\n context: `Suggested action: ${finding.suggestedAction}\nEvidence: ${JSON.stringify(finding.evidence).slice(0, 500)}`,\n advisor: 'sage',\n timeoutMs: 20000,\n });\n if (advice && advice.verdict !== 'escalate') {\n logger.info(COMPONENT, `self_repair ${advice.verdict} by sage: ${advice.reason.slice(0, 120)}`);\n return;\n }\n } catch (peerErr) {\n logger.debug(COMPONENT, `peerAdvise failed: ${(peerErr as Error).message} — escalating`);\n }\n\n cp.createApproval({\n type: 'custom',\n requestedBy: 'self-repair-daemon',\n payload: {\n kind: 'self_repair',\n finding: finding.kind,\n reason: finding.reason,\n evidence: finding.evidence,\n suggestedAction: finding.suggestedAction,\n severity: finding.severity,\n },\n linkedIssueIds: [],\n });\n // Record as an episode so the pattern is recallable.\n const { recordEpisode } = await import('../memory/episodic.js');\n recordEpisode({\n kind: 'significant_learning',\n summary: `Self-repair flagged: ${finding.reason}`,\n detail: `Suggested action: ${finding.suggestedAction}`,\n tags: ['self-repair', finding.kind, finding.severity],\n });\n } catch (err) {\n logger.warn(COMPONENT, `file approval failed: ${(err as Error).message}`);\n }\n}\n\nexport function getSelfRepairFindings(): SelfRepairFinding[] {\n return Array.from(findingsByKey.values());\n}\n\n/** Test-only: clear dedupe cache. */\nexport function _resetSelfRepairForTests(): void {\n findingsByKey.clear();\n}\n"],"mappings":"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","names":[]}
|
|
@@ -7,7 +7,7 @@ import { chat } from "../../providers/router.js";
|
|
|
7
7
|
import { loadConfig } from "../../config/config.js";
|
|
8
8
|
import { applyOutputGuardrails } from "../../agent/outputGuardrails.js";
|
|
9
9
|
import { TITAN_HOME } from "../../utils/constants.js";
|
|
10
|
-
import {
|
|
10
|
+
import { mkdirIfNotExists } from "../../utils/helpers.js";
|
|
11
11
|
import logger from "../../utils/logger.js";
|
|
12
12
|
const COMPONENT = "AgentDebate";
|
|
13
13
|
const DEBATES_DIR = join(TITAN_HOME, "debates");
|
|
@@ -294,7 +294,7 @@ function parseJudgeVerdict(raw) {
|
|
|
294
294
|
}
|
|
295
295
|
function persistTranscript(t) {
|
|
296
296
|
try {
|
|
297
|
-
|
|
297
|
+
mkdirIfNotExists(DEBATES_DIR);
|
|
298
298
|
writeFileSync(join(DEBATES_DIR, `${t.id}.json`), JSON.stringify(t, null, 2), "utf-8");
|
|
299
299
|
} catch (err) {
|
|
300
300
|
logger.warn(COMPONENT, `Failed to persist transcript ${t.id}: ${err.message}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/agent_debate.ts"],"sourcesContent":["/**\n * TITAN — Agent Debate (F3)\n *\n * When two or more agents should weigh in on a contested question, run a\n * structured multi-round debate and resolve the disagreement via vote,\n * synthesis, or judge. Each round shows every participant the others'\n * latest positions; guardrails strip chain-of-thought leakage from each\n * turn. Full transcripts are persisted to ~/.titan/debates/.\n *\n * Unlike mixture_of_agents (one-shot, parallel, independent), debate is\n * iterative — agents see each other's arguments and update their positions.\n *\n * Composes existing primitives:\n * - router.chat() — per-turn LLM calls\n * - outputGuardrails — strip CoT from every turn\n * - mixture_of_agents.vote() — word-overlap consensus for 'vote' mode\n * - commandPost.addActivity — observable via Mission Control\n */\nimport { existsSync, writeFileSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { v4 as uuid } from 'uuid';\nimport { registerSkill } from '../registry.js';\nimport { chat } from '../../providers/router.js';\nimport { loadConfig } from '../../config/config.js';\nimport { applyOutputGuardrails } from '../../agent/outputGuardrails.js';\nimport { TITAN_HOME } from '../../utils/constants.js';\nimport { ensureDir } from '../../utils/helpers.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'AgentDebate';\nconst DEBATES_DIR = join(TITAN_HOME, 'debates');\nconst MAX_ROUNDS = 4;\nconst MAX_PARTICIPANTS = 5;\nconst MIN_PARTICIPANTS = 2;\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport interface DebateParticipant {\n /** Short role label that frames the agent's vantage. */\n role: string;\n /** Full provider/model id. Falls back to config.agent.model. */\n model?: string;\n /** Optional pre-seeded position. If omitted, the agent forms one in round 1. */\n position?: string;\n}\n\nexport type DebateResolution = 'vote' | 'synthesize' | 'judge';\n\nexport interface DebateTurn {\n round: number;\n role: string;\n model: string;\n content: string;\n rawLength: number;\n guardrailScore: number;\n durationMs: number;\n}\n\nexport interface DebateTranscript {\n id: string;\n question: string;\n participants: DebateParticipant[];\n rounds: number;\n resolution: DebateResolution;\n turns: DebateTurn[];\n winner?: { role: string; content: string; justification?: string };\n startedAt: string;\n completedAt: string;\n durationMs: number;\n}\n\n// ── Vote (copied from mixture_of_agents so we don't introduce a module dep) ─\n\nfunction voteByConsensus(entries: Array<{ role: string; content: string }>): { role: string; content: string } {\n if (entries.length === 1) return entries[0];\n const words = entries.map(e => new Set(e.content.toLowerCase().split(/\\s+/).filter(w => w.length > 3)));\n let bestIdx = 0;\n let bestScore = -1;\n for (let i = 0; i < entries.length; i++) {\n let score = 0;\n for (let j = 0; j < entries.length; j++) {\n if (i === j) continue;\n for (const word of words[i]) {\n if (words[j].has(word)) score++;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestIdx = i;\n }\n }\n return entries[bestIdx];\n}\n\n// ── Prompt builders ──────────────────────────────────────────────\n\nfunction buildOpeningPrompt(question: string, role: string, seeded?: string): string {\n if (seeded) {\n return `You are participating in a debate as \"${role}\". Your pre-assigned position is below — defend and sharpen it. Do NOT abandon it in the opening turn.\n\n## Question\n${question}\n\n## Your assigned position\n${seeded}\n\n## Your task\nOpen the debate by stating your position and the single strongest argument for it. 3-6 sentences. Direct, no preamble. No hedging like \"I think\" or \"in my opinion\".`;\n }\n return `You are participating in a debate as \"${role}\". Form a clear, distinctive position on the question and defend it.\n\n## Question\n${question}\n\n## Your task\nOpen the debate by stating your position and the single strongest argument for it. 3-6 sentences. Direct, no preamble. No hedging.`;\n}\n\nfunction buildRebuttalPrompt(\n question: string,\n role: string,\n myLastPosition: string,\n othersLatest: Array<{ role: string; content: string }>,\n round: number,\n totalRounds: number,\n): string {\n const peers = othersLatest.map(o => `### ${o.role}\\n${o.content}`).join('\\n\\n');\n const finalRound = round === totalRounds;\n return `You are participating in a debate as \"${role}\". This is round ${round} of ${totalRounds}.\n\n## Question\n${question}\n\n## Your previous position\n${myLastPosition}\n\n## Other participants' latest arguments\n${peers}\n\n## Your task\n${finalRound\n ? 'This is the FINAL round. State your concluding position — account for the strongest counterarguments, concede anything you were wrong about, and commit to your answer. 4-8 sentences.'\n : 'Engage directly with the strongest counterargument you see. You may concede, refine, or stand firm — but be specific about which peer\\'s point you\\'re addressing. 4-7 sentences.'}\nNo preamble. Do not write \"<think>\" blocks or narrate your reasoning process.`;\n}\n\n/** JSON schema for Ollama's native structured outputs — constrains the judge\n * verdict to the exact shape parseJudgeVerdict() expects. */\nconst JUDGE_VERDICT_SCHEMA: Record<string, unknown> = {\n type: 'object',\n required: ['winnerRole', 'justification', 'finalAnswer'],\n properties: {\n winnerRole: { type: 'string' },\n justification: { type: 'string' },\n finalAnswer: { type: 'string' },\n },\n};\n\nfunction buildJudgePrompt(question: string, transcript: DebateTurn[]): string {\n const rounds = new Map<number, DebateTurn[]>();\n for (const t of transcript) {\n const arr = rounds.get(t.round) || [];\n arr.push(t);\n rounds.set(t.round, arr);\n }\n const formatted: string[] = [];\n for (const [r, turns] of [...rounds.entries()].sort((a, b) => a[0] - b[0])) {\n formatted.push(`## Round ${r}`);\n for (const t of turns) formatted.push(`### ${t.role}\\n${t.content}`);\n }\n\n return `You are an impartial judge reviewing a structured debate. Read every round in full, then pick the single most defensible position.\n\n## Question\n${question}\n\n${formatted.join('\\n\\n')}\n\n## Your task\nReturn ONLY a JSON object:\n{\n \"winnerRole\": \"<role name exactly as above>\",\n \"justification\": \"<2-3 sentences explaining the decision>\",\n \"finalAnswer\": \"<a polished version of the winning position — not a quote, a synthesis of the winner's argument as a direct answer to the question>\"\n}\nNo prose outside the JSON. No code fences.`;\n}\n\nfunction buildSynthesisPrompt(question: string, finalPositions: Array<{ role: string; content: string }>): string {\n const formatted = finalPositions.map(p => `### ${p.role}\\n${p.content}`).join('\\n\\n');\n return `You are a synthesizer. Multiple debaters argued positions on the question below. Combine the strongest reasoning from each into a single coherent answer.\n\n## Question\n${question}\n\n## Final Positions\n${formatted}\n\n## Your task\nWrite the single best answer. 4-8 sentences. Do NOT mention the debate, rounds, or that multiple positions existed. Just answer the question, leveraging the strongest points you read.`;\n}\n\n// ── Core orchestration ──────────────────────────────────────────\n\ninterface RunOptions {\n question: string;\n participants: DebateParticipant[];\n rounds: number;\n resolution: DebateResolution;\n judgeModel?: string;\n}\n\nasync function callParticipant(model: string, prompt: string, role: string, round: number): Promise<DebateTurn> {\n const started = Date.now();\n let rawContent = '';\n try {\n const response = await chat({\n model,\n messages: [\n { role: 'system', content: 'You are a debate participant. Respond in direct prose only. No <think> tags, no tool JSON, no markdown headers in the reply.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.7,\n maxTokens: 800,\n });\n rawContent = response.content || '';\n } catch (err) {\n logger.warn(COMPONENT, `${role} (model ${model}) failed in round ${round}: ${(err as Error).message}`);\n return {\n round, role, model,\n content: `[${role} failed to respond: ${(err as Error).message}]`,\n rawLength: 0, guardrailScore: 0,\n durationMs: Date.now() - started,\n };\n }\n\n const guard = applyOutputGuardrails(rawContent, { type: 'sub_agent' });\n return {\n round, role, model,\n content: guard.content || rawContent,\n rawLength: rawContent.length,\n guardrailScore: guard.score,\n durationMs: Date.now() - started,\n };\n}\n\nasync function runDebate(opts: RunOptions): Promise<DebateTranscript> {\n const config = loadConfig();\n const defaultModel = config.agent.model;\n const debateId = `dbt-${Date.now().toString(36)}-${uuid().slice(0, 4)}`;\n const startedAt = new Date().toISOString();\n const start = Date.now();\n\n // Normalize participants — resolve model, ensure unique roles.\n const seenRoles = new Set<string>();\n const participants: DebateParticipant[] = [];\n for (const p of opts.participants) {\n let role = p.role.trim();\n if (!role) role = `participant-${participants.length + 1}`;\n let unique = role;\n let n = 2;\n while (seenRoles.has(unique)) unique = `${role}-${n++}`;\n seenRoles.add(unique);\n participants.push({ role: unique, model: p.model || defaultModel, position: p.position });\n }\n\n const transcript: DebateTranscript = {\n id: debateId,\n question: opts.question,\n participants,\n rounds: opts.rounds,\n resolution: opts.resolution,\n turns: [],\n startedAt,\n completedAt: startedAt,\n durationMs: 0,\n };\n\n // Phase 1: opening positions in parallel.\n logger.info(COMPONENT, `Debate ${debateId} opening: ${participants.length} participants, ${opts.rounds} rounds`);\n const openingPromises = participants.map(p =>\n callParticipant(p.model!, buildOpeningPrompt(opts.question, p.role, p.position), p.role, 1),\n );\n const openings = await Promise.all(openingPromises);\n transcript.turns.push(...openings);\n\n // Phase 2: rebuttal rounds (2..N). Each round, every participant sees the\n // others' LATEST turn. Participants step sequentially within a round so each\n // gets the same snapshot of peers, but rounds are sequential.\n const latestByRole = new Map<string, string>();\n for (const t of openings) latestByRole.set(t.role, t.content);\n\n for (let r = 2; r <= opts.rounds; r++) {\n const snapshot = new Map(latestByRole);\n const roundPromises = participants.map(p => {\n const my = snapshot.get(p.role) || '';\n const others = participants\n .filter(x => x.role !== p.role)\n .map(x => ({ role: x.role, content: snapshot.get(x.role) || '' }));\n return callParticipant(\n p.model!,\n buildRebuttalPrompt(opts.question, p.role, my, others, r, opts.rounds),\n p.role,\n r,\n );\n });\n const roundTurns = await Promise.all(roundPromises);\n transcript.turns.push(...roundTurns);\n for (const t of roundTurns) latestByRole.set(t.role, t.content);\n }\n\n // Phase 3: resolve.\n const finalPositions = participants.map(p => ({ role: p.role, content: latestByRole.get(p.role) || '' }));\n if (opts.resolution === 'vote') {\n const winner = voteByConsensus(finalPositions);\n transcript.winner = { role: winner.role, content: winner.content, justification: 'highest word-overlap consensus with peers' };\n } else if (opts.resolution === 'synthesize') {\n const synthModel = opts.judgeModel || (config.agent.modelAliases['smart'] || defaultModel);\n try {\n const synth = await chat({\n model: synthModel,\n messages: [\n { role: 'system', content: 'You synthesize debate outcomes into single direct answers.' },\n { role: 'user', content: buildSynthesisPrompt(opts.question, finalPositions) },\n ],\n temperature: 0.4,\n maxTokens: 600,\n });\n const guarded = applyOutputGuardrails(synth.content, { type: 'sub_agent' });\n transcript.winner = { role: 'synthesis', content: guarded.content, justification: 'combined strongest reasoning across final positions' };\n } catch (err) {\n logger.warn(COMPONENT, `Synthesis fell back to vote: ${(err as Error).message}`);\n const fallback = voteByConsensus(finalPositions);\n transcript.winner = { role: fallback.role, content: fallback.content, justification: 'synthesis failed — fell back to consensus vote' };\n }\n } else {\n // judge\n const judgeModel = opts.judgeModel || (config.agent.modelAliases['smart'] || defaultModel);\n // Only Ollama honours the `format` JSON-schema constraint today.\n // For everything else we keep the belt-and-suspenders prompt + regex\n // parse path (see parseJudgeVerdict + fallback-to-vote below).\n const isOllamaJudge = judgeModel.toLowerCase().startsWith('ollama/');\n try {\n const verdict = await chat({\n model: judgeModel,\n messages: [\n { role: 'system', content: 'You are an impartial debate judge. Output ONLY JSON.' },\n { role: 'user', content: buildJudgePrompt(opts.question, transcript.turns) },\n ],\n temperature: 0.2,\n maxTokens: 600,\n ...(isOllamaJudge ? { format: JUDGE_VERDICT_SCHEMA } : {}),\n });\n const guarded = applyOutputGuardrails(verdict.content, { type: 'sub_agent' });\n const parsed = parseJudgeVerdict(guarded.content);\n if (parsed) {\n transcript.winner = {\n role: parsed.winnerRole,\n content: parsed.finalAnswer,\n justification: parsed.justification,\n };\n } else {\n logger.warn(COMPONENT, `Judge verdict malformed — falling back to vote`);\n const fallback = voteByConsensus(finalPositions);\n transcript.winner = { role: fallback.role, content: fallback.content, justification: 'judge verdict malformed — fell back to consensus vote' };\n }\n } catch (err) {\n logger.warn(COMPONENT, `Judge failed, falling back to vote: ${(err as Error).message}`);\n const fallback = voteByConsensus(finalPositions);\n transcript.winner = { role: fallback.role, content: fallback.content, justification: 'judge unavailable — fell back to consensus vote' };\n }\n }\n\n transcript.completedAt = new Date().toISOString();\n transcript.durationMs = Date.now() - start;\n persistTranscript(transcript);\n emitActivity(transcript);\n\n return transcript;\n}\n\nfunction parseJudgeVerdict(raw: string): { winnerRole: string; justification: string; finalAnswer: string } | null {\n const trimmed = raw.trim().replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '');\n const matchers = [trimmed, trimmed.match(/\\{[\\s\\S]*\\}/)?.[0] || ''];\n for (const candidate of matchers) {\n if (!candidate) continue;\n try {\n const parsed = JSON.parse(candidate);\n if (\n typeof parsed.winnerRole === 'string' &&\n typeof parsed.justification === 'string' &&\n typeof parsed.finalAnswer === 'string'\n ) {\n return parsed;\n }\n } catch { /* next */ }\n }\n return null;\n}\n\nfunction persistTranscript(t: DebateTranscript): void {\n try {\n ensureDir(DEBATES_DIR);\n writeFileSync(join(DEBATES_DIR, `${t.id}.json`), JSON.stringify(t, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to persist transcript ${t.id}: ${(err as Error).message}`);\n }\n}\n\nfunction emitActivity(t: DebateTranscript): void {\n // Fire on titanEvents directly. Command Post's activity-feed subscriber\n // (if enabled) picks up 'commandpost:activity' and persists to the JSONL\n // feed + buffer. Keeping this as a titanEvents.emit avoids a hard dep\n // on commandPost module (which isn't always initialized during tests).\n (async () => {\n try {\n const { titanEvents } = await import('../../agent/daemon.js');\n titanEvents.emit('commandpost:activity', {\n id: t.id,\n timestamp: t.completedAt,\n type: 'debate_resolved',\n message: `Debate \"${t.question.slice(0, 80)}\" resolved via ${t.resolution} — winner: ${t.winner?.role ?? 'unknown'}`,\n metadata: {\n debateId: t.id,\n participants: t.participants.map(p => p.role),\n resolution: t.resolution,\n rounds: t.rounds,\n durationMs: t.durationMs,\n },\n });\n } catch { /* non-critical */ }\n })().catch(() => { /* non-critical */ });\n}\n\n// ── Read-side helpers (for API) ──────────────────────────────────\n\nexport function listDebates(limit = 50): Array<Pick<DebateTranscript, 'id' | 'question' | 'resolution' | 'rounds' | 'startedAt' | 'completedAt' | 'durationMs'> & { winnerRole?: string }> {\n try {\n if (!existsSync(DEBATES_DIR)) return [];\n const files = readdirSync(DEBATES_DIR).filter(f => f.endsWith('.json'));\n const entries = files.map(f => {\n try {\n const raw = readFileSync(join(DEBATES_DIR, f), 'utf-8');\n const t = JSON.parse(raw) as DebateTranscript;\n return {\n id: t.id, question: t.question, resolution: t.resolution,\n rounds: t.rounds, startedAt: t.startedAt, completedAt: t.completedAt,\n durationMs: t.durationMs, winnerRole: t.winner?.role,\n };\n } catch { return null; }\n }).filter((x): x is NonNullable<typeof x> => x !== null);\n entries.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());\n return entries.slice(0, limit);\n } catch { return []; }\n}\n\nexport function getDebate(id: string): DebateTranscript | null {\n try {\n const p = join(DEBATES_DIR, `${id}.json`);\n if (!existsSync(p)) return null;\n return JSON.parse(readFileSync(p, 'utf-8')) as DebateTranscript;\n } catch { return null; }\n}\n\n// ── Skill Registration ──────────────────────────────────────────\n\nexport function registerAgentDebateSkill(): void {\n registerSkill(\n {\n name: 'agent_debate',\n description: 'Run a structured multi-round debate between N agents and resolve disagreements',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'agent_debate',\n description: 'Run a structured debate between 2-5 agents on a contested question. Each round, every participant sees the others\\' latest arguments and refines their position. Resolution via consensus vote, LLM synthesis, or impartial judge.\\nUSE THIS WHEN: user explicitly requests a \"debate\", asks two agents to \"argue about\" / \"disagree on\" something, or needs multiple perspectives weighed against each other (not merged). Prefer mixture_of_agents for parallel independent opinions.',\n parameters: {\n type: 'object',\n properties: {\n question: { type: 'string', description: 'The contested question. A yes/no or short-answer question works best.' },\n participants: {\n type: 'array',\n description: `${MIN_PARTICIPANTS}-${MAX_PARTICIPANTS} agents. Each has a role label and optionally a model override + pre-seeded position.`,\n items: {\n type: 'object',\n properties: {\n role: { type: 'string', description: 'Role/vantage (e.g. \"pragmatist\", \"skeptic\", \"security lead\")' },\n model: { type: 'string', description: 'provider/model id. Defaults to config.agent.model.' },\n position: { type: 'string', description: 'Optional pre-seeded opening position.' },\n },\n required: ['role'],\n },\n },\n rounds: { type: 'number', description: `Number of rebuttal rounds (1-${MAX_ROUNDS}). Default 2.`, minimum: 1, maximum: MAX_ROUNDS },\n resolution: { type: 'string', enum: ['vote', 'synthesize', 'judge'], description: 'How to pick a winner. Default \"judge\".' },\n judgeModel: { type: 'string', description: 'Model for the judge/synthesizer. Default uses the \"smart\" alias.' },\n },\n required: ['question', 'participants'],\n },\n execute: async (args) => {\n const question = (args.question as string || '').trim();\n if (!question) return 'Error: question is required.';\n const rawParticipants = (args.participants as DebateParticipant[] | undefined) || [];\n if (rawParticipants.length < MIN_PARTICIPANTS || rawParticipants.length > MAX_PARTICIPANTS) {\n return `Error: need ${MIN_PARTICIPANTS}-${MAX_PARTICIPANTS} participants, got ${rawParticipants.length}.`;\n }\n const rounds = Math.max(1, Math.min(MAX_ROUNDS, (args.rounds as number) ?? 2));\n const resolution = (args.resolution as DebateResolution) || 'judge';\n const judgeModel = args.judgeModel as string | undefined;\n\n const result = await runDebate({ question, participants: rawParticipants, rounds, resolution, judgeModel });\n const winnerBlock = result.winner\n ? `\\n\\n## Winner: ${result.winner.role}\\n${result.winner.content}${result.winner.justification ? `\\n\\n_${result.winner.justification}_` : ''}`\n : '\\n\\n(No winner determined.)';\n return `Debate ${result.id} complete. ${result.participants.length} participants, ${result.rounds} rounds, resolved via ${result.resolution} in ${result.durationMs}ms.${winnerBlock}\\n\\nFull transcript: GET /api/command-post/debates/${result.id}`;\n },\n },\n );\n}\n\n// Export the runner for tests + server usage.\nexport { runDebate };\n"],"mappings":";AAkBA,SAAS,YAAY,eAAe,cAAc,mBAAmB;AACrE,SAAS,YAAY;AACrB,SAAS,MAAM,YAAY;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,cAAc,KAAK,YAAY,SAAS;AAC9C,MAAM,aAAa;AACnB,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AAwCzB,SAAS,gBAAgB,SAAsF;AAC3G,MAAI,QAAQ,WAAW,EAAG,QAAO,QAAQ,CAAC;AAC1C,QAAM,QAAQ,QAAQ,IAAI,OAAK,IAAI,IAAI,EAAE,QAAQ,YAAY,EAAE,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC,CAAC,CAAC;AACtG,MAAI,UAAU;AACd,MAAI,YAAY;AAChB,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACrC,QAAI,QAAQ;AACZ,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACrC,UAAI,MAAM,EAAG;AACb,iBAAW,QAAQ,MAAM,CAAC,GAAG;AACzB,YAAI,MAAM,CAAC,EAAE,IAAI,IAAI,EAAG;AAAA,MAC5B;AAAA,IACJ;AACA,QAAI,QAAQ,WAAW;AACnB,kBAAY;AACZ,gBAAU;AAAA,IACd;AAAA,EACJ;AACA,SAAO,QAAQ,OAAO;AAC1B;AAIA,SAAS,mBAAmB,UAAkB,MAAc,QAAyB;AACjF,MAAI,QAAQ;AACR,WAAO,yCAAyC,IAAI;AAAA;AAAA;AAAA,EAG1D,QAAQ;AAAA;AAAA;AAAA,EAGR,MAAM;AAAA;AAAA;AAAA;AAAA,EAIJ;AACA,SAAO,yCAAyC,IAAI;AAAA;AAAA;AAAA,EAGtD,QAAQ;AAAA;AAAA;AAAA;AAIV;AAEA,SAAS,oBACL,UACA,MACA,gBACA,cACA,OACA,aACM;AACN,QAAM,QAAQ,aAAa,IAAI,OAAK,OAAO,EAAE,IAAI;AAAA,EAAK,EAAE,OAAO,EAAE,EAAE,KAAK,MAAM;AAC9E,QAAM,aAAa,UAAU;AAC7B,SAAO,yCAAyC,IAAI,oBAAoB,KAAK,OAAO,WAAW;AAAA;AAAA;AAAA,EAGjG,QAAQ;AAAA;AAAA;AAAA,EAGR,cAAc;AAAA;AAAA;AAAA,EAGd,KAAK;AAAA;AAAA;AAAA,EAGL,aACQ,gMACA,sLAAmL;AAAA;AAE7L;AAIA,MAAM,uBAAgD;AAAA,EAClD,MAAM;AAAA,EACN,UAAU,CAAC,cAAc,iBAAiB,aAAa;AAAA,EACvD,YAAY;AAAA,IACR,YAAY,EAAE,MAAM,SAAS;AAAA,IAC7B,eAAe,EAAE,MAAM,SAAS;AAAA,IAChC,aAAa,EAAE,MAAM,SAAS;AAAA,EAClC;AACJ;AAEA,SAAS,iBAAiB,UAAkB,YAAkC;AAC1E,QAAM,SAAS,oBAAI,IAA0B;AAC7C,aAAW,KAAK,YAAY;AACxB,UAAM,MAAM,OAAO,IAAI,EAAE,KAAK,KAAK,CAAC;AACpC,QAAI,KAAK,CAAC;AACV,WAAO,IAAI,EAAE,OAAO,GAAG;AAAA,EAC3B;AACA,QAAM,YAAsB,CAAC;AAC7B,aAAW,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,OAAO,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG;AACxE,cAAU,KAAK,YAAY,CAAC,EAAE;AAC9B,eAAW,KAAK,MAAO,WAAU,KAAK,OAAO,EAAE,IAAI;AAAA,EAAK,EAAE,OAAO,EAAE;AAAA,EACvE;AAEA,SAAO;AAAA;AAAA;AAAA,EAGT,QAAQ;AAAA;AAAA,EAER,UAAU,KAAK,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUxB;AAEA,SAAS,qBAAqB,UAAkB,gBAAkE;AAC9G,QAAM,YAAY,eAAe,IAAI,OAAK,OAAO,EAAE,IAAI;AAAA,EAAK,EAAE,OAAO,EAAE,EAAE,KAAK,MAAM;AACpF,SAAO;AAAA;AAAA;AAAA,EAGT,QAAQ;AAAA;AAAA;AAAA,EAGR,SAAS;AAAA;AAAA;AAAA;AAIX;AAYA,eAAe,gBAAgB,OAAe,QAAgB,MAAc,OAAoC;AAC5G,QAAM,UAAU,KAAK,IAAI;AACzB,MAAI,aAAa;AACjB,MAAI;AACA,UAAM,WAAW,MAAM,KAAK;AAAA,MACxB;AAAA,MACA,UAAU;AAAA,QACN,EAAE,MAAM,UAAU,SAAS,+HAA+H;AAAA,QAC1J,EAAE,MAAM,QAAQ,SAAS,OAAO;AAAA,MACpC;AAAA,MACA,aAAa;AAAA,MACb,WAAW;AAAA,IACf,CAAC;AACD,iBAAa,SAAS,WAAW;AAAA,EACrC,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,GAAG,IAAI,WAAW,KAAK,qBAAqB,KAAK,KAAM,IAAc,OAAO,EAAE;AACrG,WAAO;AAAA,MACH;AAAA,MAAO;AAAA,MAAM;AAAA,MACb,SAAS,IAAI,IAAI,uBAAwB,IAAc,OAAO;AAAA,MAC9D,WAAW;AAAA,MAAG,gBAAgB;AAAA,MAC9B,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AAEA,QAAM,QAAQ,sBAAsB,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,SAAO;AAAA,IACH;AAAA,IAAO;AAAA,IAAM;AAAA,IACb,SAAS,MAAM,WAAW;AAAA,IAC1B,WAAW,WAAW;AAAA,IACtB,gBAAgB,MAAM;AAAA,IACtB,YAAY,KAAK,IAAI,IAAI;AAAA,EAC7B;AACJ;AAEA,eAAe,UAAU,MAA6C;AAClE,QAAM,SAAS,WAAW;AAC1B,QAAM,eAAe,OAAO,MAAM;AAClC,QAAM,WAAW,OAAO,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;AACrE,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,QAAM,QAAQ,KAAK,IAAI;AAGvB,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,eAAoC,CAAC;AAC3C,aAAW,KAAK,KAAK,cAAc;AAC/B,QAAI,OAAO,EAAE,KAAK,KAAK;AACvB,QAAI,CAAC,KAAM,QAAO,eAAe,aAAa,SAAS,CAAC;AACxD,QAAI,SAAS;AACb,QAAI,IAAI;AACR,WAAO,UAAU,IAAI,MAAM,EAAG,UAAS,GAAG,IAAI,IAAI,GAAG;AACrD,cAAU,IAAI,MAAM;AACpB,iBAAa,KAAK,EAAE,MAAM,QAAQ,OAAO,EAAE,SAAS,cAAc,UAAU,EAAE,SAAS,CAAC;AAAA,EAC5F;AAEA,QAAM,aAA+B;AAAA,IACjC,IAAI;AAAA,IACJ,UAAU,KAAK;AAAA,IACf;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,YAAY,KAAK;AAAA,IACjB,OAAO,CAAC;AAAA,IACR;AAAA,IACA,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAGA,SAAO,KAAK,WAAW,UAAU,QAAQ,aAAa,aAAa,MAAM,kBAAkB,KAAK,MAAM,SAAS;AAC/G,QAAM,kBAAkB,aAAa;AAAA,IAAI,OACrC,gBAAgB,EAAE,OAAQ,mBAAmB,KAAK,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAG,EAAE,MAAM,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,MAAM,QAAQ,IAAI,eAAe;AAClD,aAAW,MAAM,KAAK,GAAG,QAAQ;AAKjC,QAAM,eAAe,oBAAI,IAAoB;AAC7C,aAAW,KAAK,SAAU,cAAa,IAAI,EAAE,MAAM,EAAE,OAAO;AAE5D,WAAS,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK;AACnC,UAAM,WAAW,IAAI,IAAI,YAAY;AACrC,UAAM,gBAAgB,aAAa,IAAI,OAAK;AACxC,YAAM,KAAK,SAAS,IAAI,EAAE,IAAI,KAAK;AACnC,YAAM,SAAS,aACV,OAAO,OAAK,EAAE,SAAS,EAAE,IAAI,EAC7B,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,SAAS,SAAS,IAAI,EAAE,IAAI,KAAK,GAAG,EAAE;AACrE,aAAO;AAAA,QACH,EAAE;AAAA,QACF,oBAAoB,KAAK,UAAU,EAAE,MAAM,IAAI,QAAQ,GAAG,KAAK,MAAM;AAAA,QACrE,EAAE;AAAA,QACF;AAAA,MACJ;AAAA,IACJ,CAAC;AACD,UAAM,aAAa,MAAM,QAAQ,IAAI,aAAa;AAClD,eAAW,MAAM,KAAK,GAAG,UAAU;AACnC,eAAW,KAAK,WAAY,cAAa,IAAI,EAAE,MAAM,EAAE,OAAO;AAAA,EAClE;AAGA,QAAM,iBAAiB,aAAa,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,SAAS,aAAa,IAAI,EAAE,IAAI,KAAK,GAAG,EAAE;AACxG,MAAI,KAAK,eAAe,QAAQ;AAC5B,UAAM,SAAS,gBAAgB,cAAc;AAC7C,eAAW,SAAS,EAAE,MAAM,OAAO,MAAM,SAAS,OAAO,SAAS,eAAe,4CAA4C;AAAA,EACjI,WAAW,KAAK,eAAe,cAAc;AACzC,UAAM,aAAa,KAAK,eAAe,OAAO,MAAM,aAAa,OAAO,KAAK;AAC7E,QAAI;AACA,YAAM,QAAQ,MAAM,KAAK;AAAA,QACrB,OAAO;AAAA,QACP,UAAU;AAAA,UACN,EAAE,MAAM,UAAU,SAAS,6DAA6D;AAAA,UACxF,EAAE,MAAM,QAAQ,SAAS,qBAAqB,KAAK,UAAU,cAAc,EAAE;AAAA,QACjF;AAAA,QACA,aAAa;AAAA,QACb,WAAW;AAAA,MACf,CAAC;AACD,YAAM,UAAU,sBAAsB,MAAM,SAAS,EAAE,MAAM,YAAY,CAAC;AAC1E,iBAAW,SAAS,EAAE,MAAM,aAAa,SAAS,QAAQ,SAAS,eAAe,sDAAsD;AAAA,IAC5I,SAAS,KAAK;AACV,aAAO,KAAK,WAAW,gCAAiC,IAAc,OAAO,EAAE;AAC/E,YAAM,WAAW,gBAAgB,cAAc;AAC/C,iBAAW,SAAS,EAAE,MAAM,SAAS,MAAM,SAAS,SAAS,SAAS,eAAe,sDAAiD;AAAA,IAC1I;AAAA,EACJ,OAAO;AAEH,UAAM,aAAa,KAAK,eAAe,OAAO,MAAM,aAAa,OAAO,KAAK;AAI7E,UAAM,gBAAgB,WAAW,YAAY,EAAE,WAAW,SAAS;AACnE,QAAI;AACA,YAAM,UAAU,MAAM,KAAK;AAAA,QACvB,OAAO;AAAA,QACP,UAAU;AAAA,UACN,EAAE,MAAM,UAAU,SAAS,uDAAuD;AAAA,UAClF,EAAE,MAAM,QAAQ,SAAS,iBAAiB,KAAK,UAAU,WAAW,KAAK,EAAE;AAAA,QAC/E;AAAA,QACA,aAAa;AAAA,QACb,WAAW;AAAA,QACX,GAAI,gBAAgB,EAAE,QAAQ,qBAAqB,IAAI,CAAC;AAAA,MAC5D,CAAC;AACD,YAAM,UAAU,sBAAsB,QAAQ,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5E,YAAM,SAAS,kBAAkB,QAAQ,OAAO;AAChD,UAAI,QAAQ;AACR,mBAAW,SAAS;AAAA,UAChB,MAAM,OAAO;AAAA,UACb,SAAS,OAAO;AAAA,UAChB,eAAe,OAAO;AAAA,QAC1B;AAAA,MACJ,OAAO;AACH,eAAO,KAAK,WAAW,qDAAgD;AACvE,cAAM,WAAW,gBAAgB,cAAc;AAC/C,mBAAW,SAAS,EAAE,MAAM,SAAS,MAAM,SAAS,SAAS,SAAS,eAAe,6DAAwD;AAAA,MACjJ;AAAA,IACJ,SAAS,KAAK;AACV,aAAO,KAAK,WAAW,uCAAwC,IAAc,OAAO,EAAE;AACtF,YAAM,WAAW,gBAAgB,cAAc;AAC/C,iBAAW,SAAS,EAAE,MAAM,SAAS,MAAM,SAAS,SAAS,SAAS,eAAe,uDAAkD;AAAA,IAC3I;AAAA,EACJ;AAEA,aAAW,eAAc,oBAAI,KAAK,GAAE,YAAY;AAChD,aAAW,aAAa,KAAK,IAAI,IAAI;AACrC,oBAAkB,UAAU;AAC5B,eAAa,UAAU;AAEvB,SAAO;AACX;AAEA,SAAS,kBAAkB,KAAwF;AAC/G,QAAM,UAAU,IAAI,KAAK,EAAE,QAAQ,qBAAqB,EAAE,EAAE,QAAQ,WAAW,EAAE;AACjF,QAAM,WAAW,CAAC,SAAS,QAAQ,MAAM,aAAa,IAAI,CAAC,KAAK,EAAE;AAClE,aAAW,aAAa,UAAU;AAC9B,QAAI,CAAC,UAAW;AAChB,QAAI;AACA,YAAM,SAAS,KAAK,MAAM,SAAS;AACnC,UACI,OAAO,OAAO,eAAe,YAC7B,OAAO,OAAO,kBAAkB,YAChC,OAAO,OAAO,gBAAgB,UAChC;AACE,eAAO;AAAA,MACX;AAAA,IACJ,QAAQ;AAAA,IAAa;AAAA,EACzB;AACA,SAAO;AACX;AAEA,SAAS,kBAAkB,GAA2B;AAClD,MAAI;AACA,cAAU,WAAW;AACrB,kBAAc,KAAK,aAAa,GAAG,EAAE,EAAE,OAAO,GAAG,KAAK,UAAU,GAAG,MAAM,CAAC,GAAG,OAAO;AAAA,EACxF,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,gCAAgC,EAAE,EAAE,KAAM,IAAc,OAAO,EAAE;AAAA,EAC5F;AACJ;AAEA,SAAS,aAAa,GAA2B;AAK7C,GAAC,YAAY;AACT,QAAI;AACA,YAAM,EAAE,YAAY,IAAI,MAAM,OAAO,uBAAuB;AAC5D,kBAAY,KAAK,wBAAwB;AAAA,QACrC,IAAI,EAAE;AAAA,QACN,WAAW,EAAE;AAAA,QACb,MAAM;AAAA,QACN,SAAS,WAAW,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC,kBAAkB,EAAE,UAAU,mBAAc,EAAE,QAAQ,QAAQ,SAAS;AAAA,QAClH,UAAU;AAAA,UACN,UAAU,EAAE;AAAA,UACZ,cAAc,EAAE,aAAa,IAAI,OAAK,EAAE,IAAI;AAAA,UAC5C,YAAY,EAAE;AAAA,UACd,QAAQ,EAAE;AAAA,UACV,YAAY,EAAE;AAAA,QAClB;AAAA,MACJ,CAAC;AAAA,IACL,QAAQ;AAAA,IAAqB;AAAA,EACjC,GAAG,EAAE,MAAM,MAAM;AAAA,EAAqB,CAAC;AAC3C;AAIO,SAAS,YAAY,QAAQ,IAAuJ;AACvL,MAAI;AACA,QAAI,CAAC,WAAW,WAAW,EAAG,QAAO,CAAC;AACtC,UAAM,QAAQ,YAAY,WAAW,EAAE,OAAO,OAAK,EAAE,SAAS,OAAO,CAAC;AACtE,UAAM,UAAU,MAAM,IAAI,OAAK;AAC3B,UAAI;AACA,cAAM,MAAM,aAAa,KAAK,aAAa,CAAC,GAAG,OAAO;AACtD,cAAM,IAAI,KAAK,MAAM,GAAG;AACxB,eAAO;AAAA,UACH,IAAI,EAAE;AAAA,UAAI,UAAU,EAAE;AAAA,UAAU,YAAY,EAAE;AAAA,UAC9C,QAAQ,EAAE;AAAA,UAAQ,WAAW,EAAE;AAAA,UAAW,aAAa,EAAE;AAAA,UACzD,YAAY,EAAE;AAAA,UAAY,YAAY,EAAE,QAAQ;AAAA,QACpD;AAAA,MACJ,QAAQ;AAAE,eAAO;AAAA,MAAM;AAAA,IAC3B,CAAC,EAAE,OAAO,CAAC,MAAkC,MAAM,IAAI;AACvD,YAAQ,KAAK,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;AACxF,WAAO,QAAQ,MAAM,GAAG,KAAK;AAAA,EACjC,QAAQ;AAAE,WAAO,CAAC;AAAA,EAAG;AACzB;AAEO,SAAS,UAAU,IAAqC;AAC3D,MAAI;AACA,UAAM,IAAI,KAAK,aAAa,GAAG,EAAE,OAAO;AACxC,QAAI,CAAC,WAAW,CAAC,EAAG,QAAO;AAC3B,WAAO,KAAK,MAAM,aAAa,GAAG,OAAO,CAAC;AAAA,EAC9C,QAAQ;AAAE,WAAO;AAAA,EAAM;AAC3B;AAIO,SAAS,2BAAiC;AAC7C;AAAA,IACI;AAAA,MACI,MAAM;AAAA,MACN,aAAa;AAAA,MACb,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS;AAAA,IACb;AAAA,IACA;AAAA,MACI,MAAM;AAAA,MACN,aAAa;AAAA;AAAA,MACb,YAAY;AAAA,QACR,MAAM;AAAA,QACN,YAAY;AAAA,UACR,UAAU,EAAE,MAAM,UAAU,aAAa,wEAAwE;AAAA,UACjH,cAAc;AAAA,YACV,MAAM;AAAA,YACN,aAAa,GAAG,gBAAgB,IAAI,gBAAgB;AAAA,YACpD,OAAO;AAAA,cACH,MAAM;AAAA,cACN,YAAY;AAAA,gBACR,MAAM,EAAE,MAAM,UAAU,aAAa,+DAA+D;AAAA,gBACpG,OAAO,EAAE,MAAM,UAAU,aAAa,qDAAqD;AAAA,gBAC3F,UAAU,EAAE,MAAM,UAAU,aAAa,wCAAwC;AAAA,cACrF;AAAA,cACA,UAAU,CAAC,MAAM;AAAA,YACrB;AAAA,UACJ;AAAA,UACA,QAAQ,EAAE,MAAM,UAAU,aAAa,gCAAgC,UAAU,iBAAiB,SAAS,GAAG,SAAS,WAAW;AAAA,UAClI,YAAY,EAAE,MAAM,UAAU,MAAM,CAAC,QAAQ,cAAc,OAAO,GAAG,aAAa,yCAAyC;AAAA,UAC3H,YAAY,EAAE,MAAM,UAAU,aAAa,mEAAmE;AAAA,QAClH;AAAA,QACA,UAAU,CAAC,YAAY,cAAc;AAAA,MACzC;AAAA,MACA,SAAS,OAAO,SAAS;AACrB,cAAM,YAAY,KAAK,YAAsB,IAAI,KAAK;AACtD,YAAI,CAAC,SAAU,QAAO;AACtB,cAAM,kBAAmB,KAAK,gBAAoD,CAAC;AACnF,YAAI,gBAAgB,SAAS,oBAAoB,gBAAgB,SAAS,kBAAkB;AACxF,iBAAO,eAAe,gBAAgB,IAAI,gBAAgB,sBAAsB,gBAAgB,MAAM;AAAA,QAC1G;AACA,cAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,YAAa,KAAK,UAAqB,CAAC,CAAC;AAC7E,cAAM,aAAc,KAAK,cAAmC;AAC5D,cAAM,aAAa,KAAK;AAExB,cAAM,SAAS,MAAM,UAAU,EAAE,UAAU,cAAc,iBAAiB,QAAQ,YAAY,WAAW,CAAC;AAC1G,cAAM,cAAc,OAAO,SACrB;AAAA;AAAA,aAAkB,OAAO,OAAO,IAAI;AAAA,EAAK,OAAO,OAAO,OAAO,GAAG,OAAO,OAAO,gBAAgB;AAAA;AAAA,GAAQ,OAAO,OAAO,aAAa,MAAM,EAAE,KAC1I;AACN,eAAO,UAAU,OAAO,EAAE,cAAc,OAAO,aAAa,MAAM,kBAAkB,OAAO,MAAM,yBAAyB,OAAO,UAAU,OAAO,OAAO,UAAU,MAAM,WAAW;AAAA;AAAA,iDAAsD,OAAO,EAAE;AAAA,MACvP;AAAA,IACJ;AAAA,EACJ;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/agent_debate.ts"],"sourcesContent":["/**\n * TITAN — Agent Debate (F3)\n *\n * When two or more agents should weigh in on a contested question, run a\n * structured multi-round debate and resolve the disagreement via vote,\n * synthesis, or judge. Each round shows every participant the others'\n * latest positions; guardrails strip chain-of-thought leakage from each\n * turn. Full transcripts are persisted to ~/.titan/debates/.\n *\n * Unlike mixture_of_agents (one-shot, parallel, independent), debate is\n * iterative — agents see each other's arguments and update their positions.\n *\n * Composes existing primitives:\n * - router.chat() — per-turn LLM calls\n * - outputGuardrails — strip CoT from every turn\n * - mixture_of_agents.vote() — word-overlap consensus for 'vote' mode\n * - commandPost.addActivity — observable via Mission Control\n */\nimport { existsSync, writeFileSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { v4 as uuid } from 'uuid';\nimport { registerSkill } from '../registry.js';\nimport { chat } from '../../providers/router.js';\nimport { loadConfig } from '../../config/config.js';\nimport { applyOutputGuardrails } from '../../agent/outputGuardrails.js';\nimport { TITAN_HOME } from '../../utils/constants.js';\nimport { mkdirIfNotExists } from '../../utils/helpers.js';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'AgentDebate';\nconst DEBATES_DIR = join(TITAN_HOME, 'debates');\nconst MAX_ROUNDS = 4;\nconst MAX_PARTICIPANTS = 5;\nconst MIN_PARTICIPANTS = 2;\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport interface DebateParticipant {\n /** Short role label that frames the agent's vantage. */\n role: string;\n /** Full provider/model id. Falls back to config.agent.model. */\n model?: string;\n /** Optional pre-seeded position. If omitted, the agent forms one in round 1. */\n position?: string;\n}\n\nexport type DebateResolution = 'vote' | 'synthesize' | 'judge';\n\nexport interface DebateTurn {\n round: number;\n role: string;\n model: string;\n content: string;\n rawLength: number;\n guardrailScore: number;\n durationMs: number;\n}\n\nexport interface DebateTranscript {\n id: string;\n question: string;\n participants: DebateParticipant[];\n rounds: number;\n resolution: DebateResolution;\n turns: DebateTurn[];\n winner?: { role: string; content: string; justification?: string };\n startedAt: string;\n completedAt: string;\n durationMs: number;\n}\n\n// ── Vote (copied from mixture_of_agents so we don't introduce a module dep) ─\n\nfunction voteByConsensus(entries: Array<{ role: string; content: string }>): { role: string; content: string } {\n if (entries.length === 1) return entries[0];\n const words = entries.map(e => new Set(e.content.toLowerCase().split(/\\s+/).filter(w => w.length > 3)));\n let bestIdx = 0;\n let bestScore = -1;\n for (let i = 0; i < entries.length; i++) {\n let score = 0;\n for (let j = 0; j < entries.length; j++) {\n if (i === j) continue;\n for (const word of words[i]) {\n if (words[j].has(word)) score++;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestIdx = i;\n }\n }\n return entries[bestIdx];\n}\n\n// ── Prompt builders ──────────────────────────────────────────────\n\nfunction buildOpeningPrompt(question: string, role: string, seeded?: string): string {\n if (seeded) {\n return `You are participating in a debate as \"${role}\". Your pre-assigned position is below — defend and sharpen it. Do NOT abandon it in the opening turn.\n\n## Question\n${question}\n\n## Your assigned position\n${seeded}\n\n## Your task\nOpen the debate by stating your position and the single strongest argument for it. 3-6 sentences. Direct, no preamble. No hedging like \"I think\" or \"in my opinion\".`;\n }\n return `You are participating in a debate as \"${role}\". Form a clear, distinctive position on the question and defend it.\n\n## Question\n${question}\n\n## Your task\nOpen the debate by stating your position and the single strongest argument for it. 3-6 sentences. Direct, no preamble. No hedging.`;\n}\n\nfunction buildRebuttalPrompt(\n question: string,\n role: string,\n myLastPosition: string,\n othersLatest: Array<{ role: string; content: string }>,\n round: number,\n totalRounds: number,\n): string {\n const peers = othersLatest.map(o => `### ${o.role}\\n${o.content}`).join('\\n\\n');\n const finalRound = round === totalRounds;\n return `You are participating in a debate as \"${role}\". This is round ${round} of ${totalRounds}.\n\n## Question\n${question}\n\n## Your previous position\n${myLastPosition}\n\n## Other participants' latest arguments\n${peers}\n\n## Your task\n${finalRound\n ? 'This is the FINAL round. State your concluding position — account for the strongest counterarguments, concede anything you were wrong about, and commit to your answer. 4-8 sentences.'\n : 'Engage directly with the strongest counterargument you see. You may concede, refine, or stand firm — but be specific about which peer\\'s point you\\'re addressing. 4-7 sentences.'}\nNo preamble. Do not write \"<think>\" blocks or narrate your reasoning process.`;\n}\n\n/** JSON schema for Ollama's native structured outputs — constrains the judge\n * verdict to the exact shape parseJudgeVerdict() expects. */\nconst JUDGE_VERDICT_SCHEMA: Record<string, unknown> = {\n type: 'object',\n required: ['winnerRole', 'justification', 'finalAnswer'],\n properties: {\n winnerRole: { type: 'string' },\n justification: { type: 'string' },\n finalAnswer: { type: 'string' },\n },\n};\n\nfunction buildJudgePrompt(question: string, transcript: DebateTurn[]): string {\n const rounds = new Map<number, DebateTurn[]>();\n for (const t of transcript) {\n const arr = rounds.get(t.round) || [];\n arr.push(t);\n rounds.set(t.round, arr);\n }\n const formatted: string[] = [];\n for (const [r, turns] of [...rounds.entries()].sort((a, b) => a[0] - b[0])) {\n formatted.push(`## Round ${r}`);\n for (const t of turns) formatted.push(`### ${t.role}\\n${t.content}`);\n }\n\n return `You are an impartial judge reviewing a structured debate. Read every round in full, then pick the single most defensible position.\n\n## Question\n${question}\n\n${formatted.join('\\n\\n')}\n\n## Your task\nReturn ONLY a JSON object:\n{\n \"winnerRole\": \"<role name exactly as above>\",\n \"justification\": \"<2-3 sentences explaining the decision>\",\n \"finalAnswer\": \"<a polished version of the winning position — not a quote, a synthesis of the winner's argument as a direct answer to the question>\"\n}\nNo prose outside the JSON. No code fences.`;\n}\n\nfunction buildSynthesisPrompt(question: string, finalPositions: Array<{ role: string; content: string }>): string {\n const formatted = finalPositions.map(p => `### ${p.role}\\n${p.content}`).join('\\n\\n');\n return `You are a synthesizer. Multiple debaters argued positions on the question below. Combine the strongest reasoning from each into a single coherent answer.\n\n## Question\n${question}\n\n## Final Positions\n${formatted}\n\n## Your task\nWrite the single best answer. 4-8 sentences. Do NOT mention the debate, rounds, or that multiple positions existed. Just answer the question, leveraging the strongest points you read.`;\n}\n\n// ── Core orchestration ──────────────────────────────────────────\n\ninterface RunOptions {\n question: string;\n participants: DebateParticipant[];\n rounds: number;\n resolution: DebateResolution;\n judgeModel?: string;\n}\n\nasync function callParticipant(model: string, prompt: string, role: string, round: number): Promise<DebateTurn> {\n const started = Date.now();\n let rawContent = '';\n try {\n const response = await chat({\n model,\n messages: [\n { role: 'system', content: 'You are a debate participant. Respond in direct prose only. No <think> tags, no tool JSON, no markdown headers in the reply.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.7,\n maxTokens: 800,\n });\n rawContent = response.content || '';\n } catch (err) {\n logger.warn(COMPONENT, `${role} (model ${model}) failed in round ${round}: ${(err as Error).message}`);\n return {\n round, role, model,\n content: `[${role} failed to respond: ${(err as Error).message}]`,\n rawLength: 0, guardrailScore: 0,\n durationMs: Date.now() - started,\n };\n }\n\n const guard = applyOutputGuardrails(rawContent, { type: 'sub_agent' });\n return {\n round, role, model,\n content: guard.content || rawContent,\n rawLength: rawContent.length,\n guardrailScore: guard.score,\n durationMs: Date.now() - started,\n };\n}\n\nasync function runDebate(opts: RunOptions): Promise<DebateTranscript> {\n const config = loadConfig();\n const defaultModel = config.agent.model;\n const debateId = `dbt-${Date.now().toString(36)}-${uuid().slice(0, 4)}`;\n const startedAt = new Date().toISOString();\n const start = Date.now();\n\n // Normalize participants — resolve model, ensure unique roles.\n const seenRoles = new Set<string>();\n const participants: DebateParticipant[] = [];\n for (const p of opts.participants) {\n let role = p.role.trim();\n if (!role) role = `participant-${participants.length + 1}`;\n let unique = role;\n let n = 2;\n while (seenRoles.has(unique)) unique = `${role}-${n++}`;\n seenRoles.add(unique);\n participants.push({ role: unique, model: p.model || defaultModel, position: p.position });\n }\n\n const transcript: DebateTranscript = {\n id: debateId,\n question: opts.question,\n participants,\n rounds: opts.rounds,\n resolution: opts.resolution,\n turns: [],\n startedAt,\n completedAt: startedAt,\n durationMs: 0,\n };\n\n // Phase 1: opening positions in parallel.\n logger.info(COMPONENT, `Debate ${debateId} opening: ${participants.length} participants, ${opts.rounds} rounds`);\n const openingPromises = participants.map(p =>\n callParticipant(p.model!, buildOpeningPrompt(opts.question, p.role, p.position), p.role, 1),\n );\n const openings = await Promise.all(openingPromises);\n transcript.turns.push(...openings);\n\n // Phase 2: rebuttal rounds (2..N). Each round, every participant sees the\n // others' LATEST turn. Participants step sequentially within a round so each\n // gets the same snapshot of peers, but rounds are sequential.\n const latestByRole = new Map<string, string>();\n for (const t of openings) latestByRole.set(t.role, t.content);\n\n for (let r = 2; r <= opts.rounds; r++) {\n const snapshot = new Map(latestByRole);\n const roundPromises = participants.map(p => {\n const my = snapshot.get(p.role) || '';\n const others = participants\n .filter(x => x.role !== p.role)\n .map(x => ({ role: x.role, content: snapshot.get(x.role) || '' }));\n return callParticipant(\n p.model!,\n buildRebuttalPrompt(opts.question, p.role, my, others, r, opts.rounds),\n p.role,\n r,\n );\n });\n const roundTurns = await Promise.all(roundPromises);\n transcript.turns.push(...roundTurns);\n for (const t of roundTurns) latestByRole.set(t.role, t.content);\n }\n\n // Phase 3: resolve.\n const finalPositions = participants.map(p => ({ role: p.role, content: latestByRole.get(p.role) || '' }));\n if (opts.resolution === 'vote') {\n const winner = voteByConsensus(finalPositions);\n transcript.winner = { role: winner.role, content: winner.content, justification: 'highest word-overlap consensus with peers' };\n } else if (opts.resolution === 'synthesize') {\n const synthModel = opts.judgeModel || (config.agent.modelAliases['smart'] || defaultModel);\n try {\n const synth = await chat({\n model: synthModel,\n messages: [\n { role: 'system', content: 'You synthesize debate outcomes into single direct answers.' },\n { role: 'user', content: buildSynthesisPrompt(opts.question, finalPositions) },\n ],\n temperature: 0.4,\n maxTokens: 600,\n });\n const guarded = applyOutputGuardrails(synth.content, { type: 'sub_agent' });\n transcript.winner = { role: 'synthesis', content: guarded.content, justification: 'combined strongest reasoning across final positions' };\n } catch (err) {\n logger.warn(COMPONENT, `Synthesis fell back to vote: ${(err as Error).message}`);\n const fallback = voteByConsensus(finalPositions);\n transcript.winner = { role: fallback.role, content: fallback.content, justification: 'synthesis failed — fell back to consensus vote' };\n }\n } else {\n // judge\n const judgeModel = opts.judgeModel || (config.agent.modelAliases['smart'] || defaultModel);\n // Only Ollama honours the `format` JSON-schema constraint today.\n // For everything else we keep the belt-and-suspenders prompt + regex\n // parse path (see parseJudgeVerdict + fallback-to-vote below).\n const isOllamaJudge = judgeModel.toLowerCase().startsWith('ollama/');\n try {\n const verdict = await chat({\n model: judgeModel,\n messages: [\n { role: 'system', content: 'You are an impartial debate judge. Output ONLY JSON.' },\n { role: 'user', content: buildJudgePrompt(opts.question, transcript.turns) },\n ],\n temperature: 0.2,\n maxTokens: 600,\n ...(isOllamaJudge ? { format: JUDGE_VERDICT_SCHEMA } : {}),\n });\n const guarded = applyOutputGuardrails(verdict.content, { type: 'sub_agent' });\n const parsed = parseJudgeVerdict(guarded.content);\n if (parsed) {\n transcript.winner = {\n role: parsed.winnerRole,\n content: parsed.finalAnswer,\n justification: parsed.justification,\n };\n } else {\n logger.warn(COMPONENT, `Judge verdict malformed — falling back to vote`);\n const fallback = voteByConsensus(finalPositions);\n transcript.winner = { role: fallback.role, content: fallback.content, justification: 'judge verdict malformed — fell back to consensus vote' };\n }\n } catch (err) {\n logger.warn(COMPONENT, `Judge failed, falling back to vote: ${(err as Error).message}`);\n const fallback = voteByConsensus(finalPositions);\n transcript.winner = { role: fallback.role, content: fallback.content, justification: 'judge unavailable — fell back to consensus vote' };\n }\n }\n\n transcript.completedAt = new Date().toISOString();\n transcript.durationMs = Date.now() - start;\n persistTranscript(transcript);\n emitActivity(transcript);\n\n return transcript;\n}\n\nfunction parseJudgeVerdict(raw: string): { winnerRole: string; justification: string; finalAnswer: string } | null {\n const trimmed = raw.trim().replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '');\n const matchers = [trimmed, trimmed.match(/\\{[\\s\\S]*\\}/)?.[0] || ''];\n for (const candidate of matchers) {\n if (!candidate) continue;\n try {\n const parsed = JSON.parse(candidate);\n if (\n typeof parsed.winnerRole === 'string' &&\n typeof parsed.justification === 'string' &&\n typeof parsed.finalAnswer === 'string'\n ) {\n return parsed;\n }\n } catch { /* next */ }\n }\n return null;\n}\n\nfunction persistTranscript(t: DebateTranscript): void {\n try {\n mkdirIfNotExists(DEBATES_DIR);\n writeFileSync(join(DEBATES_DIR, `${t.id}.json`), JSON.stringify(t, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to persist transcript ${t.id}: ${(err as Error).message}`);\n }\n}\n\nfunction emitActivity(t: DebateTranscript): void {\n // Fire on titanEvents directly. Command Post's activity-feed subscriber\n // (if enabled) picks up 'commandpost:activity' and persists to the JSONL\n // feed + buffer. Keeping this as a titanEvents.emit avoids a hard dep\n // on commandPost module (which isn't always initialized during tests).\n (async () => {\n try {\n const { titanEvents } = await import('../../agent/daemon.js');\n titanEvents.emit('commandpost:activity', {\n id: t.id,\n timestamp: t.completedAt,\n type: 'debate_resolved',\n message: `Debate \"${t.question.slice(0, 80)}\" resolved via ${t.resolution} — winner: ${t.winner?.role ?? 'unknown'}`,\n metadata: {\n debateId: t.id,\n participants: t.participants.map(p => p.role),\n resolution: t.resolution,\n rounds: t.rounds,\n durationMs: t.durationMs,\n },\n });\n } catch { /* non-critical */ }\n })().catch(() => { /* non-critical */ });\n}\n\n// ── Read-side helpers (for API) ──────────────────────────────────\n\nexport function listDebates(limit = 50): Array<Pick<DebateTranscript, 'id' | 'question' | 'resolution' | 'rounds' | 'startedAt' | 'completedAt' | 'durationMs'> & { winnerRole?: string }> {\n try {\n if (!existsSync(DEBATES_DIR)) return [];\n const files = readdirSync(DEBATES_DIR).filter(f => f.endsWith('.json'));\n const entries = files.map(f => {\n try {\n const raw = readFileSync(join(DEBATES_DIR, f), 'utf-8');\n const t = JSON.parse(raw) as DebateTranscript;\n return {\n id: t.id, question: t.question, resolution: t.resolution,\n rounds: t.rounds, startedAt: t.startedAt, completedAt: t.completedAt,\n durationMs: t.durationMs, winnerRole: t.winner?.role,\n };\n } catch { return null; }\n }).filter((x): x is NonNullable<typeof x> => x !== null);\n entries.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());\n return entries.slice(0, limit);\n } catch { return []; }\n}\n\nexport function getDebate(id: string): DebateTranscript | null {\n try {\n const p = join(DEBATES_DIR, `${id}.json`);\n if (!existsSync(p)) return null;\n return JSON.parse(readFileSync(p, 'utf-8')) as DebateTranscript;\n } catch { return null; }\n}\n\n// ── Skill Registration ──────────────────────────────────────────\n\nexport function registerAgentDebateSkill(): void {\n registerSkill(\n {\n name: 'agent_debate',\n description: 'Run a structured multi-round debate between N agents and resolve disagreements',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'agent_debate',\n description: 'Run a structured debate between 2-5 agents on a contested question. Each round, every participant sees the others\\' latest arguments and refines their position. Resolution via consensus vote, LLM synthesis, or impartial judge.\\nUSE THIS WHEN: user explicitly requests a \"debate\", asks two agents to \"argue about\" / \"disagree on\" something, or needs multiple perspectives weighed against each other (not merged). Prefer mixture_of_agents for parallel independent opinions.',\n parameters: {\n type: 'object',\n properties: {\n question: { type: 'string', description: 'The contested question. A yes/no or short-answer question works best.' },\n participants: {\n type: 'array',\n description: `${MIN_PARTICIPANTS}-${MAX_PARTICIPANTS} agents. Each has a role label and optionally a model override + pre-seeded position.`,\n items: {\n type: 'object',\n properties: {\n role: { type: 'string', description: 'Role/vantage (e.g. \"pragmatist\", \"skeptic\", \"security lead\")' },\n model: { type: 'string', description: 'provider/model id. Defaults to config.agent.model.' },\n position: { type: 'string', description: 'Optional pre-seeded opening position.' },\n },\n required: ['role'],\n },\n },\n rounds: { type: 'number', description: `Number of rebuttal rounds (1-${MAX_ROUNDS}). Default 2.`, minimum: 1, maximum: MAX_ROUNDS },\n resolution: { type: 'string', enum: ['vote', 'synthesize', 'judge'], description: 'How to pick a winner. Default \"judge\".' },\n judgeModel: { type: 'string', description: 'Model for the judge/synthesizer. Default uses the \"smart\" alias.' },\n },\n required: ['question', 'participants'],\n },\n execute: async (args) => {\n const question = (args.question as string || '').trim();\n if (!question) return 'Error: question is required.';\n const rawParticipants = (args.participants as DebateParticipant[] | undefined) || [];\n if (rawParticipants.length < MIN_PARTICIPANTS || rawParticipants.length > MAX_PARTICIPANTS) {\n return `Error: need ${MIN_PARTICIPANTS}-${MAX_PARTICIPANTS} participants, got ${rawParticipants.length}.`;\n }\n const rounds = Math.max(1, Math.min(MAX_ROUNDS, (args.rounds as number) ?? 2));\n const resolution = (args.resolution as DebateResolution) || 'judge';\n const judgeModel = args.judgeModel as string | undefined;\n\n const result = await runDebate({ question, participants: rawParticipants, rounds, resolution, judgeModel });\n const winnerBlock = result.winner\n ? `\\n\\n## Winner: ${result.winner.role}\\n${result.winner.content}${result.winner.justification ? `\\n\\n_${result.winner.justification}_` : ''}`\n : '\\n\\n(No winner determined.)';\n return `Debate ${result.id} complete. ${result.participants.length} participants, ${result.rounds} rounds, resolved via ${result.resolution} in ${result.durationMs}ms.${winnerBlock}\\n\\nFull transcript: GET /api/command-post/debates/${result.id}`;\n },\n },\n );\n}\n\n// Export the runner for tests + server usage.\nexport { runDebate };\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { registerSkill } from "../registry.js";
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
4
|
import { dirname } from "path";
|
|
5
|
-
import {
|
|
5
|
+
import { mkdirIfNotExists } from "../../utils/helpers.js";
|
|
6
6
|
function registerApplyPatchSkill() {
|
|
7
7
|
registerSkill(
|
|
8
8
|
{ name: "apply_patch", description: 'Apply unified diff patches to files. USE THIS WHEN Tony says: "apply this patch", "apply the diff", "patch these files", or when editing code via unified diff format.', version: "1.0.0", source: "bundled", enabled: true },
|
|
@@ -38,7 +38,7 @@ function registerApplyPatchSkill() {
|
|
|
38
38
|
const isNewFile = oldFile === "/dev/null" || !existsSync(fullPath);
|
|
39
39
|
if (isNewFile) {
|
|
40
40
|
const addedLines = filePatch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)).join("\n");
|
|
41
|
-
|
|
41
|
+
mkdirIfNotExists(dirname(fullPath));
|
|
42
42
|
writeFileSync(fullPath, addedLines, "utf-8");
|
|
43
43
|
results.push(`\u2705 Created: ${targetFile}`);
|
|
44
44
|
} else {
|
|
@@ -97,7 +97,7 @@ function applySimplePatch(patch, cwd) {
|
|
|
97
97
|
writeFileSync(targetPath, content, "utf-8");
|
|
98
98
|
return `\u2705 Patched: ${targetPath}`;
|
|
99
99
|
} else {
|
|
100
|
-
|
|
100
|
+
mkdirIfNotExists(dirname(targetPath));
|
|
101
101
|
writeFileSync(targetPath, addedLines.join("\n") + "\n", "utf-8");
|
|
102
102
|
return `\u2705 Created: ${targetPath}`;
|
|
103
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/apply_patch.ts"],"sourcesContent":["/**\n * TITAN — Apply Patch Skill (Built-in)\n * Apply unified diff patches to files. Matches OpenClaw's apply_patch tool.\n */\nimport { registerSkill } from '../registry.js';\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { dirname } from 'path';\nimport { ensureDir } from '../../utils/helpers.js';\n\nexport function registerApplyPatchSkill(): void {\n registerSkill(\n { name: 'apply_patch', description: 'Apply unified diff patches to files. USE THIS WHEN Tony says: \"apply this patch\", \"apply the diff\", \"patch these files\", or when editing code via unified diff format.', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'apply_patch',\n description: 'Applies a unified diff patch to one or more files. USE THIS WHEN Tony says: \"apply this patch\", \"apply the diff\", \"patch these files\", \"here\\'s a unified diff, apply it\". Used internally when editing code via unified diff format (like git diff output). Creates new files if they don\\'t exist. RULES: Patch must be in unified diff format. Provide cwd if using relative paths.',\n parameters: {\n type: 'object',\n properties: {\n patch: { type: 'string', description: 'Unified diff patch content' },\n cwd: { type: 'string', description: 'Working directory for relative paths' },\n },\n required: ['patch'],\n },\n execute: async (args) => {\n const patch = args.patch as string;\n const cwd = (args.cwd as string) || process.cwd();\n const results: string[] = [];\n\n // Parse the unified diff\n const filePatches = patch.split(/^diff --git/m).filter(Boolean);\n\n if (filePatches.length === 0) {\n // Try simple --- / +++ format\n return applySimplePatch(patch, cwd);\n }\n\n for (const filePatch of filePatches) {\n try {\n // Extract file path from --- or +++ lines\n const oldFile = filePatch.match(/^--- a\\/(.+)$/m)?.[1];\n const newFile = filePatch.match(/^\\+\\+\\+ b\\/(.+)$/m)?.[1];\n const targetFile = newFile || oldFile;\n\n if (!targetFile) {\n results.push(`⚠️ Could not determine target file`);\n continue;\n }\n\n const fullPath = targetFile.startsWith('/') ? targetFile : `${cwd}/${targetFile}`;\n const isNewFile = oldFile === '/dev/null' || !existsSync(fullPath);\n\n if (isNewFile) {\n // Extract added lines\n const addedLines = filePatch\n .split('\\n')\n .filter((l) => l.startsWith('+') && !l.startsWith('+++'))\n .map((l) => l.slice(1))\n .join('\\n');\n\n ensureDir(dirname(fullPath));\n writeFileSync(fullPath, addedLines, 'utf-8');\n results.push(`✅ Created: ${targetFile}`);\n } else {\n // Apply hunks\n let content = readFileSync(fullPath, 'utf-8');\n const hunks = filePatch.match(/@@ .+ @@[\\s\\S]*?(?=@@ |$)/g) || [];\n\n for (const hunk of hunks) {\n const lines = hunk.split('\\n').slice(1); // Skip @@ header\n const removeLines = lines.filter((l) => l.startsWith('-')).map((l) => l.slice(1));\n const addLines = lines.filter((l) => l.startsWith('+')).map((l) => l.slice(1));\n\n // Find and replace the removed lines with added lines\n for (const removeLine of removeLines) {\n if (removeLine.trim()) {\n content = content.replace(removeLine, '');\n }\n }\n // Add new lines at approximately the right location\n if (addLines.length > 0) {\n const contextLine = lines.find((l) => !l.startsWith('+') && !l.startsWith('-'))?.trim();\n if (contextLine) {\n const idx = content.indexOf(contextLine);\n if (idx >= 0) {\n content = content.slice(0, idx) + addLines.join('\\n') + '\\n' + content.slice(idx);\n } else {\n content += '\\n' + addLines.join('\\n');\n }\n } else {\n content += '\\n' + addLines.join('\\n');\n }\n }\n }\n\n writeFileSync(fullPath, content, 'utf-8');\n results.push(`✅ Patched: ${targetFile} (${hunks.length} hunk(s))`);\n }\n } catch (error) {\n results.push(`❌ Error: ${(error as Error).message}`);\n }\n }\n\n return results.join('\\n') || 'No changes applied.';\n },\n },\n );\n}\n\n/** Apply a simple patch without git diff headers */\nfunction applySimplePatch(patch: string, cwd: string): string {\n const newFileMatch = patch.match(/^\\+\\+\\+ (.+)$/m);\n\n if (!newFileMatch) return 'Could not parse patch: no +++ line found.';\n\n let targetPath = newFileMatch[1].replace(/^b\\//, '');\n if (!targetPath.startsWith('/')) targetPath = `${cwd}/${targetPath}`;\n\n const addedLines = patch\n .split('\\n')\n .filter((l) => l.startsWith('+') && !l.startsWith('+++'))\n .map((l) => l.slice(1));\n\n const removedLines = patch\n .split('\\n')\n .filter((l) => l.startsWith('-') && !l.startsWith('---'))\n .map((l) => l.slice(1));\n\n if (existsSync(targetPath)) {\n let content = readFileSync(targetPath, 'utf-8');\n for (const line of removedLines) {\n content = content.replace(line + '\\n', '');\n }\n if (addedLines.length > 0) {\n content += addedLines.join('\\n') + '\\n';\n }\n writeFileSync(targetPath, content, 'utf-8');\n return `✅ Patched: ${targetPath}`;\n } else {\n ensureDir(dirname(targetPath));\n writeFileSync(targetPath, addedLines.join('\\n') + '\\n', 'utf-8');\n return `✅ Created: ${targetPath}`;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/apply_patch.ts"],"sourcesContent":["/**\n * TITAN — Apply Patch Skill (Built-in)\n * Apply unified diff patches to files. Matches OpenClaw's apply_patch tool.\n */\nimport { registerSkill } from '../registry.js';\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { dirname } from 'path';\nimport { mkdirIfNotExists } from '../../utils/helpers.js';\n\nexport function registerApplyPatchSkill(): void {\n registerSkill(\n { name: 'apply_patch', description: 'Apply unified diff patches to files. USE THIS WHEN Tony says: \"apply this patch\", \"apply the diff\", \"patch these files\", or when editing code via unified diff format.', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'apply_patch',\n description: 'Applies a unified diff patch to one or more files. USE THIS WHEN Tony says: \"apply this patch\", \"apply the diff\", \"patch these files\", \"here\\'s a unified diff, apply it\". Used internally when editing code via unified diff format (like git diff output). Creates new files if they don\\'t exist. RULES: Patch must be in unified diff format. Provide cwd if using relative paths.',\n parameters: {\n type: 'object',\n properties: {\n patch: { type: 'string', description: 'Unified diff patch content' },\n cwd: { type: 'string', description: 'Working directory for relative paths' },\n },\n required: ['patch'],\n },\n execute: async (args) => {\n const patch = args.patch as string;\n const cwd = (args.cwd as string) || process.cwd();\n const results: string[] = [];\n\n // Parse the unified diff\n const filePatches = patch.split(/^diff --git/m).filter(Boolean);\n\n if (filePatches.length === 0) {\n // Try simple --- / +++ format\n return applySimplePatch(patch, cwd);\n }\n\n for (const filePatch of filePatches) {\n try {\n // Extract file path from --- or +++ lines\n const oldFile = filePatch.match(/^--- a\\/(.+)$/m)?.[1];\n const newFile = filePatch.match(/^\\+\\+\\+ b\\/(.+)$/m)?.[1];\n const targetFile = newFile || oldFile;\n\n if (!targetFile) {\n results.push(`⚠️ Could not determine target file`);\n continue;\n }\n\n const fullPath = targetFile.startsWith('/') ? targetFile : `${cwd}/${targetFile}`;\n const isNewFile = oldFile === '/dev/null' || !existsSync(fullPath);\n\n if (isNewFile) {\n // Extract added lines\n const addedLines = filePatch\n .split('\\n')\n .filter((l) => l.startsWith('+') && !l.startsWith('+++'))\n .map((l) => l.slice(1))\n .join('\\n');\n\n mkdirIfNotExists(dirname(fullPath));\n writeFileSync(fullPath, addedLines, 'utf-8');\n results.push(`✅ Created: ${targetFile}`);\n } else {\n // Apply hunks\n let content = readFileSync(fullPath, 'utf-8');\n const hunks = filePatch.match(/@@ .+ @@[\\s\\S]*?(?=@@ |$)/g) || [];\n\n for (const hunk of hunks) {\n const lines = hunk.split('\\n').slice(1); // Skip @@ header\n const removeLines = lines.filter((l) => l.startsWith('-')).map((l) => l.slice(1));\n const addLines = lines.filter((l) => l.startsWith('+')).map((l) => l.slice(1));\n\n // Find and replace the removed lines with added lines\n for (const removeLine of removeLines) {\n if (removeLine.trim()) {\n content = content.replace(removeLine, '');\n }\n }\n // Add new lines at approximately the right location\n if (addLines.length > 0) {\n const contextLine = lines.find((l) => !l.startsWith('+') && !l.startsWith('-'))?.trim();\n if (contextLine) {\n const idx = content.indexOf(contextLine);\n if (idx >= 0) {\n content = content.slice(0, idx) + addLines.join('\\n') + '\\n' + content.slice(idx);\n } else {\n content += '\\n' + addLines.join('\\n');\n }\n } else {\n content += '\\n' + addLines.join('\\n');\n }\n }\n }\n\n writeFileSync(fullPath, content, 'utf-8');\n results.push(`✅ Patched: ${targetFile} (${hunks.length} hunk(s))`);\n }\n } catch (error) {\n results.push(`❌ Error: ${(error as Error).message}`);\n }\n }\n\n return results.join('\\n') || 'No changes applied.';\n },\n },\n );\n}\n\n/** Apply a simple patch without git diff headers */\nfunction applySimplePatch(patch: string, cwd: string): string {\n const newFileMatch = patch.match(/^\\+\\+\\+ (.+)$/m);\n\n if (!newFileMatch) return 'Could not parse patch: no +++ line found.';\n\n let targetPath = newFileMatch[1].replace(/^b\\//, '');\n if (!targetPath.startsWith('/')) targetPath = `${cwd}/${targetPath}`;\n\n const addedLines = patch\n .split('\\n')\n .filter((l) => l.startsWith('+') && !l.startsWith('+++'))\n .map((l) => l.slice(1));\n\n const removedLines = patch\n .split('\\n')\n .filter((l) => l.startsWith('-') && !l.startsWith('---'))\n .map((l) => l.slice(1));\n\n if (existsSync(targetPath)) {\n let content = readFileSync(targetPath, 'utf-8');\n for (const line of removedLines) {\n content = content.replace(line + '\\n', '');\n }\n if (addedLines.length > 0) {\n content += addedLines.join('\\n') + '\\n';\n }\n writeFileSync(targetPath, content, 'utf-8');\n return `✅ Patched: ${targetPath}`;\n } else {\n mkdirIfNotExists(dirname(targetPath));\n writeFileSync(targetPath, addedLines.join('\\n') + '\\n', 'utf-8');\n return `✅ Created: ${targetPath}`;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -83,7 +83,7 @@ function validateCommand(command) {
|
|
|
83
83
|
}
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
|
-
function executeCommand(command, cwd, timeout =
|
|
86
|
+
function executeCommand(command, cwd, timeout = 6e4) {
|
|
87
87
|
const cmdErr = validateCommand(command);
|
|
88
88
|
if (cmdErr) return Promise.resolve(cmdErr);
|
|
89
89
|
logger.info(COMPONENT, `Executing: ${command.slice(0, 200)}`);
|
|
@@ -176,7 +176,7 @@ RULES:
|
|
|
176
176
|
},
|
|
177
177
|
timeout: {
|
|
178
178
|
type: "number",
|
|
179
|
-
description: "Timeout in milliseconds (default:
|
|
179
|
+
description: "Timeout in milliseconds (default: 60000)"
|
|
180
180
|
},
|
|
181
181
|
background: {
|
|
182
182
|
type: "boolean",
|