@sunshinelife83/hearth 1.0.0
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/LICENSE +21 -0
- package/README.md +323 -0
- package/bin/hearth-agentd.js +4 -0
- package/bin/hearth.js +4 -0
- package/bin/run-entrypoint.js +20 -0
- package/dist/agent-tools.js +738 -0
- package/dist/app-version.js +16 -0
- package/dist/apply-patch.js +402 -0
- package/dist/artifact-error.js +8 -0
- package/dist/artifact-tools.js +377 -0
- package/dist/cli-workspace.js +36 -0
- package/dist/cli.js +1086 -0
- package/dist/config-migration.js +82 -0
- package/dist/config-schema.js +136 -0
- package/dist/config.js +115 -0
- package/dist/context/context-tools.js +150 -0
- package/dist/context/repo-map.js +102 -0
- package/dist/dashboard/app.js +226 -0
- package/dist/dashboard/index.html +45 -0
- package/dist/dashboard/landing.html +57 -0
- package/dist/dashboard/styles.css +23 -0
- package/dist/dashboard.js +271 -0
- package/dist/db/client.js +29 -0
- package/dist/db/migrations.js +262 -0
- package/dist/db/schema.js +85 -0
- package/dist/device-tokens.js +61 -0
- package/dist/git-worktrees.js +134 -0
- package/dist/git.js +41 -0
- package/dist/incoming-artifacts.js +378 -0
- package/dist/local-agent-acp.js +768 -0
- package/dist/local-agent-adapters.js +20 -0
- package/dist/local-agent-availability.js +92 -0
- package/dist/local-agent-catalog.js +51 -0
- package/dist/local-agent-claude.js +334 -0
- package/dist/local-agent-client.js +519 -0
- package/dist/local-agent-codex.js +532 -0
- package/dist/local-agent-config.js +34 -0
- package/dist/local-agent-daemon-lifecycle.js +195 -0
- package/dist/local-agent-daemon-main.js +86 -0
- package/dist/local-agent-daemon-protocol.js +302 -0
- package/dist/local-agent-daemon.js +407 -0
- package/dist/local-agent-errors.js +378 -0
- package/dist/local-agent-grok.js +202 -0
- package/dist/local-agent-manager.js +715 -0
- package/dist/local-agent-opencode.js +463 -0
- package/dist/local-agent-path.js +24 -0
- package/dist/local-agent-pi-sandbox.js +472 -0
- package/dist/local-agent-pi.js +298 -0
- package/dist/local-agent-presentation.js +104 -0
- package/dist/local-agent-profiles.js +127 -0
- package/dist/local-agent-runtime-pool.js +481 -0
- package/dist/local-agent-runtime.js +1 -0
- package/dist/local-agent-store.js +193 -0
- package/dist/local-agent-targets.js +97 -0
- package/dist/logger.js +97 -0
- package/dist/machine-id.js +35 -0
- package/dist/mcp-modern-server.js +65 -0
- package/dist/oauth-provider.js +331 -0
- package/dist/oauth-store.js +144 -0
- package/dist/onboarding.js +35 -0
- package/dist/orchestration/brief.js +70 -0
- package/dist/orchestration/lanes.js +145 -0
- package/dist/orchestration/result.js +46 -0
- package/dist/orchestration/scope.js +61 -0
- package/dist/pi-tools.js +58 -0
- package/dist/policy/command-policy.js +104 -0
- package/dist/policy/env-allowlist.js +96 -0
- package/dist/policy/sandbox.js +68 -0
- package/dist/policy/workspace-profiles.js +47 -0
- package/dist/process-journal.js +116 -0
- package/dist/process-platform.js +49 -0
- package/dist/process-sessions.js +388 -0
- package/dist/rate-limit.js +77 -0
- package/dist/request-meta.js +9 -0
- package/dist/review-checkpoints.js +349 -0
- package/dist/roots.js +37 -0
- package/dist/server-shutdown.js +12 -0
- package/dist/server.js +980 -0
- package/dist/skills.js +84 -0
- package/dist/snapshot-manager.js +123 -0
- package/dist/stdio-server.js +80 -0
- package/dist/task-store.js +203 -0
- package/dist/task-tools.js +455 -0
- package/dist/tool-surfaces/claude.js +220 -0
- package/dist/tool-surfaces/codex.js +255 -0
- package/dist/tool-surfaces/index.js +15 -0
- package/dist/tool-surfaces/shared.js +228 -0
- package/dist/tool-surfaces/types.js +27 -0
- package/dist/ui/.vite/manifest.json +2544 -0
- package/dist/ui/assets/abap-CLvhMVsD.js +1 -0
- package/dist/ui/assets/actionscript-3--17pq3dv.js +1 -0
- package/dist/ui/assets/ada-C5qYipkI.js +1 -0
- package/dist/ui/assets/andromeeda-vGVdxbeo.js +1 -0
- package/dist/ui/assets/angular-html-DrbO3_Xe.js +1 -0
- package/dist/ui/assets/angular-ts-CmTVzHJx.js +1 -0
- package/dist/ui/assets/apache-U0d_L8uA.js +1 -0
- package/dist/ui/assets/apex-CGTLDQj6.js +1 -0
- package/dist/ui/assets/apl-BOpCpqXf.js +1 -0
- package/dist/ui/assets/applescript-CCn79oCD.js +1 -0
- package/dist/ui/assets/ara-4CJ0cIlV.js +1 -0
- package/dist/ui/assets/asciidoc-DE70LPWp.js +1 -0
- package/dist/ui/assets/asm-Cmm7eHzH.js +1 -0
- package/dist/ui/assets/astro-CxPeBwei.js +1 -0
- package/dist/ui/assets/aurora-x-CDeNXAV0.js +1 -0
- package/dist/ui/assets/awk-BWXHIvNe.js +1 -0
- package/dist/ui/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/dist/ui/assets/ayu-light-C3h-C4tm.js +1 -0
- package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/dist/ui/assets/ballerina-B7ZEbQpA.js +1 -0
- package/dist/ui/assets/bat-Bo4NYOV-.js +1 -0
- package/dist/ui/assets/beancount-D-usSTwE.js +1 -0
- package/dist/ui/assets/berry-DKpUyyne.js +1 -0
- package/dist/ui/assets/bibtex-Ci_nEsc7.js +1 -0
- package/dist/ui/assets/bicep-CUHmPFLl.js +1 -0
- package/dist/ui/assets/bird2-C2hNVINV.js +1 -0
- package/dist/ui/assets/blade-DtsAAG4P.js +1 -0
- package/dist/ui/assets/bsl-BkkzgIyY.js +1 -0
- package/dist/ui/assets/c-D9hLjNUM.js +1 -0
- package/dist/ui/assets/c3-CnJL0r0V.js +1 -0
- package/dist/ui/assets/cadence-CQ2zXKGN.js +1 -0
- package/dist/ui/assets/cairo-DLTphjLi.js +1 -0
- package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
- package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
- package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
- package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
- package/dist/ui/assets/clarity-SemFz856.js +1 -0
- package/dist/ui/assets/clojure-DqKBuwfJ.js +1 -0
- package/dist/ui/assets/cmake-Bj61d0ZC.js +1 -0
- package/dist/ui/assets/cobol-C1SWlvZm.js +1 -0
- package/dist/ui/assets/codeowners-C8r90Shi.js +1 -0
- package/dist/ui/assets/codeql-oeQT6MSM.js +1 -0
- package/dist/ui/assets/coffee-B71R3v2U.js +1 -0
- package/dist/ui/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/dist/ui/assets/coq-BrsZFFmf.js +1 -0
- package/dist/ui/assets/cpp--5a4c3v4.js +1 -0
- package/dist/ui/assets/crystal-AtqWYtE0.js +1 -0
- package/dist/ui/assets/csharp-Ct8U2NOr.js +1 -0
- package/dist/ui/assets/css-CbqKSHI-.js +1 -0
- package/dist/ui/assets/csv-Dx-8-gkx.js +1 -0
- package/dist/ui/assets/cue-CE9AQfxI.js +1 -0
- package/dist/ui/assets/cypher-ClKdZ_lG.js +1 -0
- package/dist/ui/assets/d-qD-0Kul2.js +1 -0
- package/dist/ui/assets/dark-plus-Cs2F2srj.js +1 -0
- package/dist/ui/assets/dart-DkHntEIa.js +1 -0
- package/dist/ui/assets/dax-BkyTk9wS.js +1 -0
- package/dist/ui/assets/desktop-Dlh5hvp9.js +1 -0
- package/dist/ui/assets/diff-woXpYk--.js +1 -0
- package/dist/ui/assets/docker-IyjqRm3v.js +1 -0
- package/dist/ui/assets/dotenv-_5a1GRtc.js +1 -0
- package/dist/ui/assets/dracula-BHWKrbxM.js +1 -0
- package/dist/ui/assets/dracula-soft-5eyTD99u.js +1 -0
- package/dist/ui/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/dist/ui/assets/edge-BPwY_RrJ.js +1 -0
- package/dist/ui/assets/elixir-DVH2yLJf.js +1 -0
- package/dist/ui/assets/elm-BrleA6gS.js +1 -0
- package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +1 -0
- package/dist/ui/assets/erb-2SHiYe30.js +1 -0
- package/dist/ui/assets/erlang-Cphh6RMH.js +1 -0
- package/dist/ui/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/dist/ui/assets/everforest-light-Df2xbC6M.js +1 -0
- package/dist/ui/assets/fennel-DQxkIbk2.js +1 -0
- package/dist/ui/assets/fish-BJitypiv.js +1 -0
- package/dist/ui/assets/fluent-C03EYrpw.js +1 -0
- package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/dist/ui/assets/fsharp-D13ZGOAj.js +1 -0
- package/dist/ui/assets/gdresource-C0sCabJj.js +1 -0
- package/dist/ui/assets/gdscript-Cp2uCuqX.js +1 -0
- package/dist/ui/assets/gdshader-CBce3t8t.js +1 -0
- package/dist/ui/assets/genie-CV2tkWYe.js +1 -0
- package/dist/ui/assets/gherkin-DExj1W_8.js +1 -0
- package/dist/ui/assets/git-commit-BSykSTBG.js +1 -0
- package/dist/ui/assets/git-rebase-Bc7atMaV.js +1 -0
- package/dist/ui/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/dist/ui/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/dist/ui/assets/gleam-CSRkHgEL.js +1 -0
- package/dist/ui/assets/glimmer-js-DXeTDPtW.js +1 -0
- package/dist/ui/assets/glimmer-ts-CEOoXHU4.js +1 -0
- package/dist/ui/assets/glsl-5-43-Gsg.js +1 -0
- package/dist/ui/assets/gn-ilITqXS6.js +1 -0
- package/dist/ui/assets/gnuplot-7GGW24-e.js +1 -0
- package/dist/ui/assets/go-BJwz_mda.js +1 -0
- package/dist/ui/assets/graphql-Bk7Ah1KA.js +1 -0
- package/dist/ui/assets/groovy-CacY0gHj.js +1 -0
- package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/dist/ui/assets/hack-DUHz4rCJ.js +1 -0
- package/dist/ui/assets/haml-BPCuuTPR.js +1 -0
- package/dist/ui/assets/handlebars-CHxmUsnU.js +1 -0
- package/dist/ui/assets/haskell-D8IpX4py.js +1 -0
- package/dist/ui/assets/haxe-OTjmBuCE.js +1 -0
- package/dist/ui/assets/hcl-Dh228itO.js +1 -0
- package/dist/ui/assets/hjson-CxZEssPk.js +1 -0
- package/dist/ui/assets/hlsl-Cvrh5tZx.js +1 -0
- package/dist/ui/assets/horizon-CE9ld1lL.js +1 -0
- package/dist/ui/assets/horizon-bright-Br1oVSNq.js +1 -0
- package/dist/ui/assets/houston-CsvMBhTu.js +1 -0
- package/dist/ui/assets/html-BBJqizz0.js +1 -0
- package/dist/ui/assets/html-derivative-ShTPCcKN.js +1 -0
- package/dist/ui/assets/http-VFTyziwA.js +1 -0
- package/dist/ui/assets/hurl-B8yxTa4r.js +1 -0
- package/dist/ui/assets/hxml-B0Qn7Nwc.js +1 -0
- package/dist/ui/assets/hy-CZbG8q4J.js +1 -0
- package/dist/ui/assets/imba-DsUTQ-LC.js +1 -0
- package/dist/ui/assets/ini-B5eOa1yu.js +1 -0
- package/dist/ui/assets/java-BZ5DAgJZ.js +1 -0
- package/dist/ui/assets/javascript-C_zuIEaC.js +1 -0
- package/dist/ui/assets/jinja-DWd6xfNr.js +1 -0
- package/dist/ui/assets/jison-BbY63d1V.js +1 -0
- package/dist/ui/assets/json-C4DnajZs.js +1 -0
- package/dist/ui/assets/json5-BR5RXkoi.js +1 -0
- package/dist/ui/assets/jsonc-CYpm1nAK.js +1 -0
- package/dist/ui/assets/jsonl-CmCQp5Yx.js +1 -0
- package/dist/ui/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/dist/ui/assets/jssm-DXw9l8Rf.js +1 -0
- package/dist/ui/assets/jsx-C4AACieZ.js +1 -0
- package/dist/ui/assets/julia-sRu7EFMW.js +1 -0
- package/dist/ui/assets/just-DpZ9Sx1v.js +1 -0
- package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
- package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +1 -0
- package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +1 -0
- package/dist/ui/assets/kdl-CsD5j6eV.js +1 -0
- package/dist/ui/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/ui/assets/kusto-BUv0MjJC.js +1 -0
- package/dist/ui/assets/laserwave-C_8bwKvT.js +1 -0
- package/dist/ui/assets/latex-B6a_uum7.js +1 -0
- package/dist/ui/assets/lean-CewbzKMR.js +1 -0
- package/dist/ui/assets/less-DVTAwKKz.js +1 -0
- package/dist/ui/assets/light-plus-DVQuIRkW.js +1 -0
- package/dist/ui/assets/liquid-B2sVpjjr.js +1 -0
- package/dist/ui/assets/llvm-Cm23YOpf.js +1 -0
- package/dist/ui/assets/log-BNLmms1o.js +1 -0
- package/dist/ui/assets/logo-Cluzi2Zq.js +1 -0
- package/dist/ui/assets/lua-DxJJll61.js +1 -0
- package/dist/ui/assets/luau-CNKltnaQ.js +1 -0
- package/dist/ui/assets/make-Dixweg8N.js +1 -0
- package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/ui/assets/marko-Dx8mw-Wx.js +1 -0
- package/dist/ui/assets/material-theme-Bm3Qr25_.js +1 -0
- package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/dist/ui/assets/matlab-D7qyCx1q.js +1 -0
- package/dist/ui/assets/mdc-vNKeXEwR.js +1 -0
- package/dist/ui/assets/mdx-DQZ5AkYe.js +1 -0
- package/dist/ui/assets/mermaid-Bk4SNUv9.js +1 -0
- package/dist/ui/assets/min-dark-BSWPekZh.js +1 -0
- package/dist/ui/assets/min-light-DDpmG2fV.js +1 -0
- package/dist/ui/assets/mipsasm-BMqwQI7S.js +1 -0
- package/dist/ui/assets/mojo-BgCJLMeH.js +1 -0
- package/dist/ui/assets/monokai-CdkpiU2Y.js +1 -0
- package/dist/ui/assets/moonbit-CaWjb8XO.js +1 -0
- package/dist/ui/assets/move-B1IS1UjX.js +1 -0
- package/dist/ui/assets/narrat-_X_XdTYD.js +1 -0
- package/dist/ui/assets/nextflow-Bbiyy34d.js +1 -0
- package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +1 -0
- package/dist/ui/assets/nginx-DScQAGLT.js +1 -0
- package/dist/ui/assets/night-owl-DhmEMT88.js +1 -0
- package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/dist/ui/assets/nim-lAJch9uT.js +1 -0
- package/dist/ui/assets/nix-IvuFDN5E.js +1 -0
- package/dist/ui/assets/nord-Cb4Vim4T.js +1 -0
- package/dist/ui/assets/nushell-DcLAeLz5.js +1 -0
- package/dist/ui/assets/objective-c-D1A_Heim.js +1 -0
- package/dist/ui/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/dist/ui/assets/ocaml-O90oeIOV.js +1 -0
- package/dist/ui/assets/odin-B1RWQWA5.js +1 -0
- package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/dist/ui/assets/one-light-D7Lr4KcI.js +1 -0
- package/dist/ui/assets/openscad-BUDT5pXO.js +1 -0
- package/dist/ui/assets/pascal-4ZHwLPI5.js +1 -0
- package/dist/ui/assets/perl-C_GkVo_E.js +1 -0
- package/dist/ui/assets/php-DziV3K6J.js +1 -0
- package/dist/ui/assets/pierre-dark-CpLgRqie.js +1 -0
- package/dist/ui/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/dist/ui/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/dist/ui/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/dist/ui/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/dist/ui/assets/pierre-light-CoaEpmwp.js +1 -0
- package/dist/ui/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/dist/ui/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/dist/ui/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/dist/ui/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/dist/ui/assets/pkl-ot-7Btpt.js +1 -0
- package/dist/ui/assets/plastic-DQwYfKfQ.js +1 -0
- package/dist/ui/assets/plsql-DGHpHOYJ.js +1 -0
- package/dist/ui/assets/po-BiJDBrnU.js +1 -0
- package/dist/ui/assets/poimandres-DRFjx7u4.js +1 -0
- package/dist/ui/assets/polar-C7UOKdEL.js +1 -0
- package/dist/ui/assets/postcss-BXeXVLqQ.js +1 -0
- package/dist/ui/assets/powerquery-DNMTfnFr.js +1 -0
- package/dist/ui/assets/powershell-DshXNtvi.js +1 -0
- package/dist/ui/assets/prisma-BsRQq5mF.js +1 -0
- package/dist/ui/assets/prolog-iXnhIJG7.js +1 -0
- package/dist/ui/assets/proto-DB4EqR-F.js +1 -0
- package/dist/ui/assets/pug-z75UIWVK.js +1 -0
- package/dist/ui/assets/puppet-CDv2pdJW.js +1 -0
- package/dist/ui/assets/purescript-9MfHhQsQ.js +1 -0
- package/dist/ui/assets/python-gzcpVVnB.js +1 -0
- package/dist/ui/assets/qml-Cmm4coz1.js +1 -0
- package/dist/ui/assets/qmldir-DCQb3MpD.js +1 -0
- package/dist/ui/assets/qss-Fe1Jh2GI.js +1 -0
- package/dist/ui/assets/r-Brd0axBE.js +1 -0
- package/dist/ui/assets/racket-DcIDlBhZ.js +1 -0
- package/dist/ui/assets/raku-B3gFvitq.js +1 -0
- package/dist/ui/assets/razor-DSbwmoiu.js +1 -0
- package/dist/ui/assets/red-CJ3rzSJv.js +1 -0
- package/dist/ui/assets/reg-CRGYupPL.js +1 -0
- package/dist/ui/assets/regexp-Bs3Uk-pY.js +1 -0
- package/dist/ui/assets/rel-BtDbiS_P.js +1 -0
- package/dist/ui/assets/review-payload-CbwCBKB1.js +1580 -0
- package/dist/ui/assets/riscv-Ckw8ddFX.js +1 -0
- package/dist/ui/assets/ron-VUp2lXgN.js +1 -0
- package/dist/ui/assets/rose-pine-BthvhNj6.js +1 -0
- package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
- package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +1 -0
- package/dist/ui/assets/rosmsg-CAekHB0j.js +1 -0
- package/dist/ui/assets/rst-ChIyKpUC.js +1 -0
- package/dist/ui/assets/ruby-BdAmvOMO.js +1 -0
- package/dist/ui/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/ui/assets/sas-DpXn8hc5.js +1 -0
- package/dist/ui/assets/sass-DXrisJhu.js +1 -0
- package/dist/ui/assets/scala-DKOlJaKm.js +1 -0
- package/dist/ui/assets/scheme-DQCgrYNe.js +1 -0
- package/dist/ui/assets/scss-atX1Uuef.js +1 -0
- package/dist/ui/assets/sdbl-bTVj8UrX.js +1 -0
- package/dist/ui/assets/shaderlab-TOUzSsQk.js +1 -0
- package/dist/ui/assets/shellscript-BqgcAeMK.js +1 -0
- package/dist/ui/assets/shellsession-DfQSMRN5.js +1 -0
- package/dist/ui/assets/slack-dark-DnToyrRv.js +1 -0
- package/dist/ui/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/dist/ui/assets/smalltalk-B16xEiuN.js +1 -0
- package/dist/ui/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/dist/ui/assets/solarized-dark-DV17i1UV.js +1 -0
- package/dist/ui/assets/solarized-light-DSh2HLQt.js +1 -0
- package/dist/ui/assets/solidity-CKzVLygQ.js +1 -0
- package/dist/ui/assets/soy-DzP7zZTB.js +1 -0
- package/dist/ui/assets/sparql-D_iOobhT.js +1 -0
- package/dist/ui/assets/splunk-BC2Px7Mm.js +1 -0
- package/dist/ui/assets/sql-gmaLk66z.js +1 -0
- package/dist/ui/assets/ssh-config-BgfXC-Er.js +1 -0
- package/dist/ui/assets/stata-CJ0YTMcV.js +1 -0
- package/dist/ui/assets/stylus-B6D30XZt.js +1 -0
- package/dist/ui/assets/surrealql-LH7FciQz.js +1 -0
- package/dist/ui/assets/svelte-BME2PU6V.js +1 -0
- package/dist/ui/assets/swift-DonLKvLd.js +1 -0
- package/dist/ui/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/dist/ui/assets/system-verilog-DJ5XKQeo.js +1 -0
- package/dist/ui/assets/systemd-BxMlprV5.js +1 -0
- package/dist/ui/assets/talonscript-CohzipZa.js +1 -0
- package/dist/ui/assets/tasl-DMoTqEGO.js +1 -0
- package/dist/ui/assets/tcl-CZd0xW_V.js +1 -0
- package/dist/ui/assets/templ-DHBNIfYW.js +1 -0
- package/dist/ui/assets/terraform-DswuEJGm.js +1 -0
- package/dist/ui/assets/tex-C8-kPEzO.js +1 -0
- package/dist/ui/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
- package/dist/ui/assets/ts-tags-CiewV0vR.js +1 -0
- package/dist/ui/assets/tsv-sltzmVWM.js +1 -0
- package/dist/ui/assets/tsx-CUYPqmnq.js +1 -0
- package/dist/ui/assets/turtle-ByJddavk.js +1 -0
- package/dist/ui/assets/twig-BTkvsl0b.js +1 -0
- package/dist/ui/assets/typescript-DDQMaPwj.js +1 -0
- package/dist/ui/assets/typespec-B88KGewJ.js +1 -0
- package/dist/ui/assets/typst-DI99ib-x.js +1 -0
- package/dist/ui/assets/v-DETTlOr0.js +1 -0
- package/dist/ui/assets/vala-zf12oZj6.js +1 -0
- package/dist/ui/assets/vb-Djn5o6TS.js +1 -0
- package/dist/ui/assets/verilog-CiiDBU1e.js +1 -0
- package/dist/ui/assets/vesper-D5bVUKB1.js +1 -0
- package/dist/ui/assets/vhdl-BroJfC0k.js +1 -0
- package/dist/ui/assets/viml-DvXPmvsu.js +1 -0
- package/dist/ui/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/ui/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/ui/assets/vue-BH5SEWRH.js +1 -0
- package/dist/ui/assets/vue-html-BT7yeYez.js +1 -0
- package/dist/ui/assets/vue-vine-Cs5IN2rs.js +1 -0
- package/dist/ui/assets/vyper-CgoNMtux.js +1 -0
- package/dist/ui/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/ui/assets/wasm-ByWQv1Qj.js +1 -0
- package/dist/ui/assets/wenyan-C8pVoKbM.js +1 -0
- package/dist/ui/assets/wgsl-BsKzXJz4.js +1 -0
- package/dist/ui/assets/wikitext-ClFFjSW2.js +1 -0
- package/dist/ui/assets/wit-DdvCle-K.js +1 -0
- package/dist/ui/assets/wolfram-DLL8P-h_.js +1 -0
- package/dist/ui/assets/workspace-app-B_BnSEHO.css +1 -0
- package/dist/ui/assets/workspace-app-DSFpnrIC.js +115 -0
- package/dist/ui/assets/xml-D3_1BEcO.js +1 -0
- package/dist/ui/assets/xsl-BBOCDn4Y.js +1 -0
- package/dist/ui/assets/yaml-lThBDT45.js +1 -0
- package/dist/ui/assets/zenscript-BnlCZFoB.js +1 -0
- package/dist/ui/assets/zig-CMLA9XwU.js +1 -0
- package/dist/ui/workspace-app.html +15 -0
- package/dist/user-config.js +228 -0
- package/dist/verification.js +74 -0
- package/dist/workspace-store.js +133 -0
- package/dist/workspaces.js +433 -0
- package/docs/adr/devspace-x-implementation-adrs.md +159 -0
- package/docs/agent-profile-schema.md +203 -0
- package/docs/artifact-exchange.md +44 -0
- package/docs/assets/devspace-logo-light.png +0 -0
- package/docs/assets/devspace-screenshot.png +0 -0
- package/docs/assets/v11-review-ui-after.png +0 -0
- package/docs/assets/v11-review-ui-before.png +0 -0
- package/docs/chatgpt-coding-workflow.md +215 -0
- package/docs/configuration.md +217 -0
- package/docs/gotchas.md +294 -0
- package/docs/local-agent-daemon.md +101 -0
- package/docs/security.md +197 -0
- package/docs/setup.md +174 -0
- package/examples/agents/claude-implementer.md +30 -0
- package/examples/agents/codex-explorer.md +27 -0
- package/examples/agents/codex-qa-tester.md +29 -0
- package/examples/agents/codex-worker.md +25 -0
- package/examples/agents/copilot-reviewer.md +25 -0
- package/examples/agents/cursor-agent-worker.md +26 -0
- package/examples/agents/grok-builder.md +10 -0
- package/examples/agents/opencode-explorer.md +27 -0
- package/examples/agents/pi-reviewer.md +27 -0
- package/package.json +102 -0
- package/schema/v1/hearth.schema.json +529 -0
- package/scripts/copy-dashboard.mjs +9 -0
- package/scripts/fix-node-pty-permissions.mjs +22 -0
- package/scripts/generate-config-schema.ts +9 -0
- package/skills/subagents/SKILL.md +57 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { createManagedWorktree } from "./git-worktrees.js";
|
|
6
|
+
import { AccessDeniedError, assertAllowedPath, isPathInsideRoot, resolveAllowedPath, } from "./roots.js";
|
|
7
|
+
import { loadWorkspaceSkills, resolveSkillReadPath, } from "./skills.js";
|
|
8
|
+
import { loadLocalAgentProfiles, } from "./local-agent-profiles.js";
|
|
9
|
+
const MAX_CACHED_WORKSPACES = 32;
|
|
10
|
+
export class WorkspaceRegistry {
|
|
11
|
+
config;
|
|
12
|
+
store;
|
|
13
|
+
workspaces = new Map();
|
|
14
|
+
pendingCheckoutOpens = new Map();
|
|
15
|
+
constructor(config, store) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.store = store;
|
|
18
|
+
}
|
|
19
|
+
async openWorkspace(input, openOptions = {}) {
|
|
20
|
+
const workspaceInput = typeof input === "string" ? { path: input } : input;
|
|
21
|
+
const conversationScopeId = openOptions.conversationScopeId;
|
|
22
|
+
if (!conversationScopeId || !this.store) {
|
|
23
|
+
return this.openNewWorkspace(workspaceInput);
|
|
24
|
+
}
|
|
25
|
+
const projectKey = await this.conversationProjectKey(workspaceInput);
|
|
26
|
+
const mode = workspaceInput.mode ?? "checkout";
|
|
27
|
+
if (mode === "worktree") {
|
|
28
|
+
const context = await this.openWorktreeWorkspace(workspaceInput.path, workspaceInput.baseRef);
|
|
29
|
+
return {
|
|
30
|
+
...context,
|
|
31
|
+
// A new worktree always has its own workspace-specific context.
|
|
32
|
+
includeBootstrapContext: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const targetKey = this.conversationCheckoutTargetKey(projectKey);
|
|
36
|
+
const operationKey = JSON.stringify([conversationScopeId, targetKey]);
|
|
37
|
+
const pending = this.pendingCheckoutOpens.get(operationKey);
|
|
38
|
+
if (pending) {
|
|
39
|
+
const context = await pending;
|
|
40
|
+
return {
|
|
41
|
+
...context,
|
|
42
|
+
workspaceReused: true,
|
|
43
|
+
includeBootstrapContext: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const open = this.openConversationCheckout(workspaceInput, conversationScopeId, targetKey);
|
|
47
|
+
this.pendingCheckoutOpens.set(operationKey, open);
|
|
48
|
+
try {
|
|
49
|
+
return await open;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (this.pendingCheckoutOpens.get(operationKey) === open) {
|
|
53
|
+
this.pendingCheckoutOpens.delete(operationKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async openNewWorkspace(options) {
|
|
58
|
+
const mode = options.mode ?? "checkout";
|
|
59
|
+
if (mode === "worktree") {
|
|
60
|
+
return this.openWorktreeWorkspace(options.path, options.baseRef);
|
|
61
|
+
}
|
|
62
|
+
return this.openCheckoutWorkspace(options.path);
|
|
63
|
+
}
|
|
64
|
+
async openConversationCheckout(input, conversationScopeId, targetKey) {
|
|
65
|
+
const binding = this.store?.getConversationBinding(conversationScopeId, targetKey);
|
|
66
|
+
if (binding) {
|
|
67
|
+
const reusableWorkspace = await this.findReusableCheckoutWorkspace(binding);
|
|
68
|
+
if (reusableWorkspace) {
|
|
69
|
+
const context = await this.reusedWorkspaceContext(reusableWorkspace);
|
|
70
|
+
this.store?.touchConversationBinding(conversationScopeId, targetKey);
|
|
71
|
+
return {
|
|
72
|
+
...context,
|
|
73
|
+
includeBootstrapContext: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
this.workspaces.delete(binding.workspaceSessionId);
|
|
77
|
+
this.store?.deleteConversationBinding(conversationScopeId, targetKey);
|
|
78
|
+
}
|
|
79
|
+
const context = await this.openCheckoutWorkspace(input.path);
|
|
80
|
+
this.store?.setConversationBinding({
|
|
81
|
+
conversationScopeId,
|
|
82
|
+
targetKey,
|
|
83
|
+
workspaceSessionId: context.workspace.id,
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
...context,
|
|
87
|
+
includeBootstrapContext: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async findReusableCheckoutWorkspace(binding) {
|
|
91
|
+
const session = this.store?.getSession(binding.workspaceSessionId);
|
|
92
|
+
if (!session || session.status !== "active" || session.mode !== "checkout") {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
let root;
|
|
96
|
+
try {
|
|
97
|
+
root = this.assertWorkspaceRootAllowed(session.root, session.mode, session.sourceRoot);
|
|
98
|
+
const rootStats = await stat(root);
|
|
99
|
+
if (!rootStats.isDirectory())
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof AccessDeniedError ||
|
|
104
|
+
(isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR"))) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const workspace = this.getWorkspace(binding.workspaceSessionId);
|
|
110
|
+
if (workspace.mode !== "checkout" || workspace.root !== root)
|
|
111
|
+
return undefined;
|
|
112
|
+
return workspace;
|
|
113
|
+
}
|
|
114
|
+
async conversationProjectKey(input) {
|
|
115
|
+
const path = assertAllowedPath(input.path, this.config.allowedRoots);
|
|
116
|
+
return canonicalPath(path);
|
|
117
|
+
}
|
|
118
|
+
conversationCheckoutTargetKey(projectKey) {
|
|
119
|
+
return JSON.stringify(["checkout", projectKey, null]);
|
|
120
|
+
}
|
|
121
|
+
async reusedWorkspaceContext(workspace) {
|
|
122
|
+
workspace.agentProfiles = await loadLocalAgentProfiles(this.config, workspace.root);
|
|
123
|
+
const agentsFiles = await this.loadInitialAgentsFiles(workspace.root);
|
|
124
|
+
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace.root, agentsFiles);
|
|
125
|
+
return {
|
|
126
|
+
workspace,
|
|
127
|
+
agentsFiles,
|
|
128
|
+
availableAgentsFiles,
|
|
129
|
+
workspaceReused: true,
|
|
130
|
+
includeBootstrapContext: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Open workspaces held in memory (dashboard visibility; no ordering guarantee). */
|
|
134
|
+
listWorkspaces() {
|
|
135
|
+
return [...this.workspaces.values()].map((workspace) => ({
|
|
136
|
+
id: workspace.id,
|
|
137
|
+
root: workspace.root,
|
|
138
|
+
mode: workspace.mode,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
getWorkspace(workspaceId) {
|
|
142
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
143
|
+
if (workspace) {
|
|
144
|
+
this.workspaces.delete(workspaceId);
|
|
145
|
+
this.workspaces.set(workspaceId, workspace);
|
|
146
|
+
this.store?.touchSession(workspaceId);
|
|
147
|
+
return workspace;
|
|
148
|
+
}
|
|
149
|
+
const session = this.store?.getSession(workspaceId);
|
|
150
|
+
if (!session) {
|
|
151
|
+
throw new Error(`Unknown workspaceId: ${workspaceId}. Open the target project or worktree again and continue with the new workspaceId.`);
|
|
152
|
+
}
|
|
153
|
+
const root = this.assertWorkspaceRootAllowed(session.root, session.mode, session.sourceRoot);
|
|
154
|
+
const restoredWorkspace = {
|
|
155
|
+
id: session.id,
|
|
156
|
+
root,
|
|
157
|
+
mode: session.mode,
|
|
158
|
+
sourceRoot: session.sourceRoot,
|
|
159
|
+
worktree: session.mode === "worktree"
|
|
160
|
+
? {
|
|
161
|
+
path: root,
|
|
162
|
+
baseRef: session.baseRef ?? "HEAD",
|
|
163
|
+
baseSha: session.baseSha ?? "",
|
|
164
|
+
dirtySource: false,
|
|
165
|
+
detached: true,
|
|
166
|
+
managed: session.managed,
|
|
167
|
+
}
|
|
168
|
+
: undefined,
|
|
169
|
+
...this.loadSkillsForWorkspace(root),
|
|
170
|
+
agentProfiles: [],
|
|
171
|
+
};
|
|
172
|
+
this.store?.touchSession(workspaceId);
|
|
173
|
+
this.rememberWorkspace(restoredWorkspace);
|
|
174
|
+
return restoredWorkspace;
|
|
175
|
+
}
|
|
176
|
+
resolvePath(workspace, inputPath) {
|
|
177
|
+
const absolutePath = resolveAllowedPath(inputPath, workspace.root, [workspace.root]);
|
|
178
|
+
if (!isPathInsideRoot(absolutePath, workspace.root)) {
|
|
179
|
+
throw new Error(`Path is outside workspace root: ${inputPath}`);
|
|
180
|
+
}
|
|
181
|
+
return absolutePath;
|
|
182
|
+
}
|
|
183
|
+
resolveReadPath(workspace, inputPath) {
|
|
184
|
+
try {
|
|
185
|
+
return {
|
|
186
|
+
absolutePath: this.resolvePath(workspace, inputPath),
|
|
187
|
+
readRoots: [workspace.root],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (workspaceError) {
|
|
191
|
+
const skillRead = resolveSkillReadPath(workspace.skills, inputPath);
|
|
192
|
+
if (!skillRead)
|
|
193
|
+
throw workspaceError;
|
|
194
|
+
return {
|
|
195
|
+
absolutePath: skillRead.absolutePath,
|
|
196
|
+
readRoots: [workspace.root, skillRead.skill.baseDir],
|
|
197
|
+
skillRead,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
resolveWorkingDirectory(workspace, workingDirectory) {
|
|
202
|
+
const directory = workingDirectory ? this.resolvePath(workspace, workingDirectory) : workspace.root;
|
|
203
|
+
return assertAllowedPath(directory, [workspace.root]);
|
|
204
|
+
}
|
|
205
|
+
async openCheckoutWorkspace(path) {
|
|
206
|
+
const root = assertAllowedPath(path, this.config.allowedRoots);
|
|
207
|
+
const rootStats = await ensureCheckoutWorkspaceRoot(root);
|
|
208
|
+
if (!rootStats.isDirectory()) {
|
|
209
|
+
throw new Error(`Workspace root must be a directory: ${path}`);
|
|
210
|
+
}
|
|
211
|
+
return this.createWorkspaceContext({ root, mode: "checkout" });
|
|
212
|
+
}
|
|
213
|
+
async openWorktreeWorkspace(path, baseRef) {
|
|
214
|
+
const worktree = await createManagedWorktree({
|
|
215
|
+
sourcePath: path,
|
|
216
|
+
baseRef,
|
|
217
|
+
config: this.config,
|
|
218
|
+
});
|
|
219
|
+
return this.createWorkspaceContext({
|
|
220
|
+
root: worktree.path,
|
|
221
|
+
mode: "worktree",
|
|
222
|
+
sourceRoot: worktree.sourceRoot,
|
|
223
|
+
worktree,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async createWorkspaceContext(input) {
|
|
227
|
+
const workspace = {
|
|
228
|
+
id: `ws_${randomBytes(5).toString("hex")}`,
|
|
229
|
+
root: input.root,
|
|
230
|
+
mode: input.mode,
|
|
231
|
+
sourceRoot: input.sourceRoot,
|
|
232
|
+
worktree: input.worktree,
|
|
233
|
+
...this.loadSkillsForWorkspace(input.root),
|
|
234
|
+
agentProfiles: await loadLocalAgentProfiles(this.config, input.root),
|
|
235
|
+
};
|
|
236
|
+
this.store?.createSession({
|
|
237
|
+
id: workspace.id,
|
|
238
|
+
root: workspace.root,
|
|
239
|
+
mode: workspace.mode,
|
|
240
|
+
sourceRoot: workspace.sourceRoot,
|
|
241
|
+
baseRef: workspace.worktree?.baseRef,
|
|
242
|
+
baseSha: workspace.worktree?.baseSha,
|
|
243
|
+
managed: workspace.worktree?.managed,
|
|
244
|
+
});
|
|
245
|
+
this.rememberWorkspace(workspace);
|
|
246
|
+
const agentsFiles = await this.loadInitialAgentsFiles(workspace.root);
|
|
247
|
+
const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace.root, agentsFiles);
|
|
248
|
+
return {
|
|
249
|
+
workspace,
|
|
250
|
+
agentsFiles,
|
|
251
|
+
availableAgentsFiles,
|
|
252
|
+
workspaceReused: false,
|
|
253
|
+
includeBootstrapContext: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
rememberWorkspace(workspace) {
|
|
257
|
+
this.workspaces.delete(workspace.id);
|
|
258
|
+
this.workspaces.set(workspace.id, workspace);
|
|
259
|
+
if (!this.store)
|
|
260
|
+
return;
|
|
261
|
+
while (this.workspaces.size > MAX_CACHED_WORKSPACES) {
|
|
262
|
+
const oldestWorkspaceId = this.workspaces.keys().next().value;
|
|
263
|
+
if (!oldestWorkspaceId)
|
|
264
|
+
break;
|
|
265
|
+
this.workspaces.delete(oldestWorkspaceId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
loadSkillsForWorkspace(root) {
|
|
269
|
+
const result = loadWorkspaceSkills(this.config, root);
|
|
270
|
+
return {
|
|
271
|
+
skills: result.skills,
|
|
272
|
+
skillDiagnostics: result.diagnostics,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
assertWorkspaceRootAllowed(root, mode, sourceRoot) {
|
|
276
|
+
if (mode === "worktree") {
|
|
277
|
+
if (!sourceRoot) {
|
|
278
|
+
throw new Error(`Stored worktree workspace is missing sourceRoot: ${root}`);
|
|
279
|
+
}
|
|
280
|
+
assertAllowedPath(sourceRoot, this.config.allowedRoots);
|
|
281
|
+
return assertAllowedPath(root, [this.config.worktreeRoot]);
|
|
282
|
+
}
|
|
283
|
+
return assertAllowedPath(root, this.config.allowedRoots);
|
|
284
|
+
}
|
|
285
|
+
async loadInitialAgentsFiles(root) {
|
|
286
|
+
const agentDir = resolve(this.config.agentDir);
|
|
287
|
+
const resolvedRoot = (await tryRealpath(root)) ?? root;
|
|
288
|
+
const loadedFiles = [];
|
|
289
|
+
for (const file of loadProjectContextFiles({ cwd: root, agentDir })) {
|
|
290
|
+
const path = resolve(file.path);
|
|
291
|
+
const source = initialAgentsFileSource(path, root, agentDir);
|
|
292
|
+
if (!source)
|
|
293
|
+
continue;
|
|
294
|
+
const content = await readResolvedContextFile(path, file.content, source, resolvedRoot);
|
|
295
|
+
if (content === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
loadedFiles.push({
|
|
298
|
+
path,
|
|
299
|
+
content,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return loadedFiles;
|
|
303
|
+
}
|
|
304
|
+
async findAvailableAgentsFiles(root, loadedFiles) {
|
|
305
|
+
const loadedPaths = new Set(loadedFiles.map((file) => resolve(file.path)));
|
|
306
|
+
const loadedRealPaths = new Set();
|
|
307
|
+
for (const file of loadedFiles) {
|
|
308
|
+
const realPath = await tryRealpath(file.path);
|
|
309
|
+
if (realPath)
|
|
310
|
+
loadedRealPaths.add(realPath);
|
|
311
|
+
}
|
|
312
|
+
const discovered = [];
|
|
313
|
+
await walkWorkspace(root, async (path, entry) => {
|
|
314
|
+
if (!entry.isFile())
|
|
315
|
+
return;
|
|
316
|
+
if (!CONTEXT_FILE_NAMES.has(entry.name))
|
|
317
|
+
return;
|
|
318
|
+
if (loadedPaths.has(path))
|
|
319
|
+
return;
|
|
320
|
+
const realPath = await tryRealpath(path);
|
|
321
|
+
if (realPath && loadedRealPaths.has(realPath))
|
|
322
|
+
return;
|
|
323
|
+
discovered.push({ path });
|
|
324
|
+
});
|
|
325
|
+
return discovered.sort((a, b) => a.path.localeCompare(b.path));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function canonicalPath(path) {
|
|
329
|
+
const missingSegments = [];
|
|
330
|
+
let candidate = path;
|
|
331
|
+
while (true) {
|
|
332
|
+
try {
|
|
333
|
+
return resolve(await realpath(candidate), ...missingSegments.slice().reverse());
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
const parent = dirname(candidate);
|
|
340
|
+
if (parent === candidate)
|
|
341
|
+
return path;
|
|
342
|
+
missingSegments.push(basename(candidate));
|
|
343
|
+
candidate = parent;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
export async function ensureCheckoutWorkspaceRoot(path, ops = { stat, mkdir }) {
|
|
348
|
+
try {
|
|
349
|
+
return await ops.stat(path);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") {
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
await ops.mkdir(path, { recursive: true });
|
|
357
|
+
return await ops.stat(path);
|
|
358
|
+
}
|
|
359
|
+
const CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
|
|
360
|
+
const SKIPPED_CONTEXT_DIRS = new Set([
|
|
361
|
+
".git",
|
|
362
|
+
".hg",
|
|
363
|
+
".svn",
|
|
364
|
+
".hearth",
|
|
365
|
+
"node_modules",
|
|
366
|
+
"dist",
|
|
367
|
+
"build",
|
|
368
|
+
".next",
|
|
369
|
+
".turbo",
|
|
370
|
+
".cache",
|
|
371
|
+
]);
|
|
372
|
+
export function formatAgentsPath(path, workspaceRoot) {
|
|
373
|
+
if (!workspaceRoot)
|
|
374
|
+
return path.split(sep).join("/");
|
|
375
|
+
const relationship = relative(workspaceRoot, path);
|
|
376
|
+
if (relationship === "" ||
|
|
377
|
+
relationship.startsWith("..") ||
|
|
378
|
+
relationship === ".." ||
|
|
379
|
+
relationship.includes(`..${sep}`)) {
|
|
380
|
+
return path.split(sep).join("/");
|
|
381
|
+
}
|
|
382
|
+
return relationship.split(sep).join("/");
|
|
383
|
+
}
|
|
384
|
+
function initialAgentsFileSource(path, root, agentDir) {
|
|
385
|
+
if (isPathInsideRoot(path, agentDir))
|
|
386
|
+
return "global";
|
|
387
|
+
if (isPathInsideRoot(path, root) && dirname(path) === root)
|
|
388
|
+
return "workspace";
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
async function readResolvedContextFile(path, fallbackContent, source, root) {
|
|
392
|
+
try {
|
|
393
|
+
const resolvedPath = await realpath(path);
|
|
394
|
+
if (source === "workspace" &&
|
|
395
|
+
(!isPathInsideRoot(resolvedPath, root) || dirname(resolvedPath) !== root)) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
return await readFile(resolvedPath, "utf8");
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return fallbackContent;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function tryRealpath(path) {
|
|
405
|
+
try {
|
|
406
|
+
return await realpath(path);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function walkWorkspace(directory, visit) {
|
|
413
|
+
let entries;
|
|
414
|
+
try {
|
|
415
|
+
entries = await opendir(directory);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
for await (const entry of entries) {
|
|
421
|
+
const path = join(directory, entry.name);
|
|
422
|
+
if (entry.isDirectory()) {
|
|
423
|
+
if (!SKIPPED_CONTEXT_DIRS.has(entry.name)) {
|
|
424
|
+
await walkWorkspace(path, visit);
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
await visit(path, entry);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function isErrnoException(error) {
|
|
432
|
+
return error instanceof Error && "code" in error;
|
|
433
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Hearth — Architecture Decision Records (المرحلة الأولى من التنفيذ)
|
|
2
|
+
|
|
3
|
+
هذه ADRs موثقة لقرارات اتُّخذت فعليًا أثناء تنفيذ المرحلة الأولى على فرع `hearth-x`. كل قرار يتبع تعارضًا بين الخطة المعتمدة وواقع الكود، أو يحسم خيارًا معماريًا جديدًا.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ADR-013: بوابة سياسة الأوامر بأربع رتب + أوضاع تنفيذ
|
|
8
|
+
- **Status:** accepted (2026-09-06)
|
|
9
|
+
- **Context:** كل أمر shell كان يُنفَّذ بصلاحيات المستخدم كاملة بلا أي بوابة (SEC-01/06). لا يوجد approval channel متزامن عبر MCP stateless.
|
|
10
|
+
- **Decision:**
|
|
11
|
+
- مصنّف ثابت `classifyCommand` (regex conservative) برتب: 0 فحص، 1 عمل داخلي، 2 مؤثر خارجيًا، 3 محظور دائمًا (`src/policy/command-policy.ts`).
|
|
12
|
+
- أوضاع للتنفيذ من config: `readonly | supervised (الافتراضي) | autonomous`. tier-2 في supervised يتطلب claim صريح `approvedByUser` من النموذج (بعد موافقة بشرية في المحادثة) ويُسجَّل كـ `policy_decision` في الـ audit. tier-3 محظور في كل الأوضاع بلا استثناء — لا يوجد مسار موافقة له.
|
|
13
|
+
- البوابة واحدة `enforceShellPolicy` وتستدعى من `exec_command` و`bash` قبل أي spawn.
|
|
14
|
+
- **Consequences:** الرفض يعود للنموذج كنص إرشادي (يتصحح ذاتيًا)؛ التصنيف ثابت-speed bump وليس حدًّا أمنيًا نهائيًا — الحد الحقيقي: env allowlist (ADR-014) والجذور (ADR-015) والـ sandbox لاحقًا.
|
|
15
|
+
- **Rejected:** موافقة متزامنة عبر elicitation (لا يدعمه ChatGPT اليوم)؛ وضع trusted مع tier-3 (هجمات التدرج).
|
|
16
|
+
|
|
17
|
+
## ADR-014: فلترة بيئة العمليات للـ shell tools فقط
|
|
18
|
+
- **Status:** accepted
|
|
19
|
+
- **Context:** `processEnvironment` كانت تمرر `process.env` كاملًا لأوامر النموذج — قناة سرقة أسرار (SEC-02).
|
|
20
|
+
- **Decision:** أوامر الـ shell tools ترث allowlist (`PATH, HOME, SHELL, XDG_*, proxies, Windows essentials` + `HEARTH_WORKSPACE_*` + `HEARTH_ORIGIN`). Escape hatch: `execution.envAllowAll` أو `envAllowlist` إضافية.
|
|
21
|
+
- **قرار الحدود:** adapters الوكلاء (codex/claude/…) تُبقي env كاملًا عن قصد — فهي برامج المطور نفسه وتحتاج اعتمادياتها، ومسارها محمي بفرض الجذور (ADR-015) وبموثوقية OS.
|
|
22
|
+
- **Consequences:** أوامر تحتاج متغيرًا خارج القائمة تُمرَّر inline (`KEY=x cmd`) أو عبر config.
|
|
23
|
+
|
|
24
|
+
## ADR-015: إغلاق SEC-04 — الجذور تُفرض على كل المسارات
|
|
25
|
+
- **Status:** accepted
|
|
26
|
+
- **Context (تعارض خطة/كود):** `LocalAgentManager.authorizeWorkspace` تخطى فحص `allowedRoots` عندما يغيب `workspaceId`. الاختبار القائم `directOutside` كان *يوثّق* هذا السلوك كميزة CLI.
|
|
27
|
+
- **Decision:** الفحص يُفرض دائمًا عندما تكون `allowedRoots` مهيأة، لأي مستدعي (MCP/CLI/daemon). إضافةً: أوامر shell المسروعة من Hearth تحمل `HEARTH_ORIGIN=hearth-shell`، و`hearth agents` يرفض العمل من داخلها بلا `HEARTH_WORKSPACE_ID` — يغلق تجاوز "النموذج يشغّل CLI بنفسه".
|
|
28
|
+
- **Consequences (breaking):** CLI على مشروعات خارج `allowedRoots` يفشل الآن — العلاج: أضف المجلد إلى `workspaces.allowedRoots`. الاختبار القائم حُدِّث ليعكس الجديد. **هذا قرار مقصود موثق وليس انزلاقًا.**
|
|
29
|
+
|
|
30
|
+
## ADR-016: Snapshots على git object model، قبل tier-2 في autonomous
|
|
31
|
+
- **Status:** accepted
|
|
32
|
+
- **Decision:** `refs/hearth/snapshots/<ws>/<epochMs>` عبر temp-index (نمط review-checkpoints). Rollback = diff بين شجرة snapshot الحالية والهدف + `git apply --binary` — لا يمس HEAD/branches. أدوات MCP: `create_snapshot/list_snapshots/rollback_snapshot` (rollback يخضع لبوابة tier-2).
|
|
33
|
+
- **قرار حاسم:** الالتقاط قبل tier-2 في autonomous **best-effort** — فشل snapshot (مثل workspace بلا git) يسجل `snapshot_failed` ويستمر التنفيذ. السبب: جعل autonomous يتطلب snapshots كست حظه على مشاريع غير-git. الربط الإلزامي يأتي مع sandbox adapters.
|
|
34
|
+
- **حد معروف:** ملفات .gitignore لا تُلتقط ولا تُستعاد.
|
|
35
|
+
|
|
36
|
+
## ADR-017: دورة حياة الوكلاء الكاملة فوق daemon protocol v4
|
|
37
|
+
- **Status:** accepted
|
|
38
|
+
- **Context:** تصحيح #5 (start/status/output/send/pause/resume/stop/cancel) مقابل واقع: لا إلغاء لكل وكيل، والإخراج يُخزَّن عند اكتمال الدورة فقط (B-13).
|
|
39
|
+
- **Decision:**
|
|
40
|
+
- البروتوكول → v4 بثلاث طرق: `agent.pause/agent.resume/agent.stop` (+ `force`). `cancel` = `stop{force:true}`. `output` عبر `agent.get` (الحقل الجديد `latestOutput`).
|
|
41
|
+
- حالة جديدة `paused` + migration 7 (`latest_output`، ذيل 64KB).
|
|
42
|
+
- إلغاء لكل وكيل: `LocalAgentRuntimePool.cancelRuntimeKey(key)` — يغلق الـ runtime فورًا؛ سبب الإغلاق `agent_cancelled` يتجاوز انتظار الجولات النشطة (كان يسبب deadlock).
|
|
43
|
+
- بث الإخراج: `onOutput` callback عبر Runtime/pool إلى المخزن؛ مُفعّل في ACP (chunk stream) وCodex (عناصر agentMessage) وClaude (assistant deltas). **Pi وOpenCode يظهران الإخراج عند اكتمال الدورة فقط** — قيد مزود موثق، لا حاجب.
|
|
44
|
+
- إيقاف أثناء دورة ناجحة لا يعيد الحالة إلى idle: مسار النجاح يحترم `interruptedAgents`.
|
|
45
|
+
- **Consequences:** "pause" = إيقاف مع الحفاظ على `providerSessionId` والاستئناف بجولة جديدة (المزودون لا يدعمون إيقافًا حرفيًا داخل دورة).
|
|
46
|
+
|
|
47
|
+
## ADR-018: تحصين OAuth — CSRF مرتبط بالحقول + rate limits
|
|
48
|
+
- **Status:** accepted
|
|
49
|
+
- **Decision:**
|
|
50
|
+
- توكن CSRF = HMAC-SHA256 بمفتاح مشتق من owner token فوق الحقول الخفية — stateless وغير قابل للتزوير، مع `X-Frame-Options: DENY` و`frame-ancestors 'none'` على صفحة الموافقة (SEC-07).
|
|
51
|
+
- حد معدل sliding-window بالعملية (`src/rate-limit.ts`): /authorize 20/sa/IP، /register 30/sa/IP، /token 120/sa/IP، وفشل owner password 8/sa/IP مع reset عند النجاح (SEC-03). مفاتيح IP تتبع `trustProxy` — **بدون tunnel حقيقي لا يمكن تزوير المفاتيح**؛ مع tunnel فعّل `server.trustProxy`.
|
|
52
|
+
- Dynamic client registration يبقى مفتوحًا (متطلب MCP) لكنه مقيد بالحصة.
|
|
53
|
+
- **Known trade-off:** بدون trustProxy خلف tunnel تتشارك جميع العملاء مفتاح IP واحد (fail-closed).
|
|
54
|
+
|
|
55
|
+
## ADR-019 (تعديل اعتماد): أهداف التصحيحات العشرة على الخطة
|
|
56
|
+
- **Status:** accepted
|
|
57
|
+
- **المحتوى:** حذف Local Models/ModelRouter من النطاق (تصحيح #1) — Agent Runtime ينسق عوامل خارجية (CLI agents عبر daemon) وعملاء MCP خارجيين يقودون الحلقة؛ MCP هو الواجهة المعيارية (تصحيح #3)؛ أولوية التنفيذ: Security → MCP interop → Agent capabilities → Reliability → Performance → DX (تصحيح #10). السلوك الحالي لا يخالفها: لا يوجد أي كود نماذج محلية في المستودع، والـ P5 الأصلي (ModelRouter) أُسقط من خارطة الطريق.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## خارطة ما بقي (مقابل الخطة المعتمدة)
|
|
62
|
+
| المرحلة | الحالة |
|
|
63
|
+
|---|---|
|
|
64
|
+
| P0 stability/security fixes | **منفذة** (node range, .env.example, init --force rotation, auth atomic, drain timeout, daemon spawn listener, codex version, CI build step, CSRF+frame, rate limits) |
|
|
65
|
+
| P1 policy core (tiers + modes + env allowlist + audit) | **منفذة** |
|
|
66
|
+
| Snapshots + rollback + أدوات MCP + autonomous auto-snapshot | **منفذة** |
|
|
67
|
+
| Agent lifecycle كامل + أدوات MCP + SEC-04 | **منفذة** |
|
|
68
|
+
| P2 stdio transport + device tokens + فصل issuer | قادمة |
|
|
69
|
+
| P3 Task state machine كاملة + Verification gates | جزئي (Snapshots جاهزة كأساس) |
|
|
70
|
+
| P4 sandbox adapters (bwrap/seatbelt) + probing | قادمة |
|
|
71
|
+
| P6 Context Engine | قادمة |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## ADR-020: نقل MCP محلي عبر stdio + توكنات الأجهزة
|
|
76
|
+
- **Status:** accepted (2026-09-06، المرحلة الثانية)
|
|
77
|
+
- **Decision:** أمر `hearth mcp` يشغّل نفس سطح الأدوات عبر StdioServerTransport — بلا HTTP ولا OAuth (العميل عملية أطلقها المستخدم بنفسه). نفس النواة: policy gate، workspaces، snapshots، agent tools، task tools — بالبناء المشترك `buildLocalMcpServer`.
|
|
78
|
+
- **device tokens:** للعملاء المحليين عبر HTTP/LAN الذين لا يدعمون تدفق OAuth: `hearth token create/list/revoke` — توكن يُعرض مرة واحدة، يُخزَّن hashed (migration 8)، يمنح نفس صلاحية scope `hearth` ويُربط بمورد الخادم في verifyAccessToken.
|
|
79
|
+
- **issuerMode:** `derived` (افتراضي، سلوك اليوم) أو `local` — يثبّت هوية OAuth على العنوان المحلي بحيث لا يكسر تغيير tunnel هوية العملاء (للنشر المحلي/LAN فقط؛ العملاء البعيدون يحتاجون derived).
|
|
80
|
+
|
|
81
|
+
## ADR-021: Task Runtime — آلة حالة دائمة وبوابات تحقق غير قابلة للتحايل
|
|
82
|
+
- **Status:** accepted
|
|
83
|
+
- **Decision:**
|
|
84
|
+
- جدول `tasks` (migration 9) + آلة حالة محروسة: planning → executing → verifying → {completed | repairing} → …؛ انتقال غير شرعي = `TaskTransitionError`.
|
|
85
|
+
- **الإكمال حكم نظام لا ادعاء نموذج:** `task_complete` يشغّل بوابات التحقق المكتشفة (package.json/Cargo/go/pyproject/Makefile) قبل أي حالة `completed`؛ `verified_complete` فقط بعد نجاح كل البوابات؛ `model_complete` ممكنة صراحةً فقط عبر `acceptUnverified: true` عند غياب أي بوابات (مُدقَّقة كـ evidence kind=model_claim).
|
|
86
|
+
- **قاعدة مانعة للتحايل (اكتشفتها الاختبارات):** البوابة الفاشلة تبقى في مجموعة التحقق حتى تنجح — `task_complete` من حالة `repairing` يُرفض، و`task_verify` يعيد البوابات الفاشلة سابقًا دائمًا مع أي بوابات جديدة. لا يمكن "تبديل" بوابات فاشلة بأخرى أسهل.
|
|
87
|
+
- Crash recovery: عند الإقلاع، المهام in-flight (executing/verifying/repairing) تُعلَّم failed بسبب صريح — العميل يعيد إنشاءها أو استئنافها بوعي.
|
|
88
|
+
- Budget: سقف زمني للمهمة (30 دقيقة افتراضيًا) يفشل المهمة عند التجاوز.
|
|
89
|
+
- **Consequences:** أدوات MCP: `task_create/task_plan/task_status/task_list/task_verify/task_complete/task_cancel`. بوابات التحقق تمر عبر نفس بوابة السياسة (readonly يمنع التحقق — موثق).
|
|
90
|
+
|
|
91
|
+
## ADR-022: Sandbox adapters — best-effort بقدرات معلنة
|
|
92
|
+
- **Status:** accepted
|
|
93
|
+
- **Decision:** `probeSandboxAdapter` (مرة لكل عملية): bubblewrap على Linux، seatbelt (sandbox-exec) على macOS، none غير ذلك. الوصفة: bwrap = ro-bind للجذر + rw للمساحة + tmpfs لـ /tmp و/run (+ `--unshare-net` عند sandboxNetwork=deny)؛ seatbelt = ملف تعريف يمنع الكتابة خارج الـ workspace و/tmp.
|
|
94
|
+
- **متى يُطبَّق:** الوضع `autonomous` فقط افتراضيًا (auto)، وأوامر التحقق ضمنه؛ `execution.requireSandboxForAutonomous: true` يحوّل غياب الـ sandbox إلى رفض لـ tier-2 في autonomous.
|
|
95
|
+
- **حدود موثقة:** Windows بلا adapter (الحاوية هي المسار الموصى به لاحقًا). profile seatbelt تقريبي وليس عزلًا صارمًا. (ملاحظة الجولة الثالثة: قيد `bash` عبر Pi أُغلق في ADR-024 — كل الأسطح تمر الآن عبر `ProcessSessionManager`.)
|
|
96
|
+
|
|
97
|
+
## ADR-023: Context Engine — الشريحة الأولى (خريطة مستودع + بحث)
|
|
98
|
+
- **Status:** accepted
|
|
99
|
+
- **Decision:** `context_overview` (خريطة محلية حتمية: ملفات/لغات/manifests/أكبر مجلدات، مع استبعاد dependencies وحدود 5000 ملف) و`search` (ripgrep عند التوفر وإلا grep -E، argv array بلا shell، مهلة 10 ثوان، سقف إخراج 40KB، استبعاد node_modules/.git).
|
|
100
|
+
- **Consequences:** لا embeddings ولا شبكة — الأساس الدلالي (ONNX محليًا) يبقى في P6 اللاحقة فوق هذه الواجهة.
|
|
101
|
+
|
|
102
|
+
## ADR-024: إغلاق مسار bash الجانبي + إصلاح تغليف sandbox في pipe spawn
|
|
103
|
+
- **Status:** accepted (2026-09-07، الجولة الثالثة)
|
|
104
|
+
- **Decision:**
|
|
105
|
+
- سطح claude `bash` لم يعد يستخدم `runShellTool` (Pi SDK spawn) — ينفّذ الآن عبر `ProcessSessionManager` مثل كل أدوات shell: بوابة `enforceShellPolicy` + `sandboxDecision` + قائمة بيئة + `commandWrapper` + سجل العمليات. `runShellTool` حُذفت.
|
|
106
|
+
- اكتُشف أثناء الاختبار E2E أن `startPipe` كان يستخدم خيار `shell:` في `spawn` — Node يلحق `-c <command>` بأي executable يُعطى له، فكان `bwrap` يستقبل `-c` كخيار له (`bwrap: Unknown option -c`). الإصلاح: spawn مباشر `spawn(shell.executable, shell.args)` — `resolveShellCommand` يضمّن الأمر أصلًا — مع فاصل `--` قبل الأمر في وصفة bwrap حتى لا يبتلع bwrap وسائط shell مثل `-lc`.
|
|
107
|
+
- مسار PTY كان صحيحًا أصلًا (`nodePty.spawn(executable, args)`).
|
|
108
|
+
- **Consequences:** كل تنفيذ أوامر — MCP/agent/claude/codex/tasks — يمر بنفس الحد الأمني. لا special paths.
|
|
109
|
+
|
|
110
|
+
## ADR-025: صلاحيات مستقلة لكل workspace (profiles)
|
|
111
|
+
- **Status:** accepted (2026-09-07، الجولة الثالثة)
|
|
112
|
+
- **Decision:**
|
|
113
|
+
- `workspaces.profiles[]`: `{ path, mode?, sandbox?, sandboxNetwork?, requireSandboxForAutonomous?, commandAllow[], commandDeny[], agentsAllowed? }` — المطابقة بأطول بادئة مسار، والباقي يرث الإعدادات العامة. إعداد workspace واحد لا يمنح شيئًا لغيره.
|
|
114
|
+
- `resolveExecutionForWorkspace` هو المصدر الوحيد للحقيقة؛ `enforceShellPolicy` و`sandboxDecision` وبوابات التحقق (`runGate`) وأدوات الوكلاء وأغلفة الأوامر كلها تحلّ عبره (مع `workspaceRoot` صريح).
|
|
115
|
+
- `commandDeny` يفوز دائمًا؛ `commandAllow` غير الفارغة تقيّد؛ regex غير صالح = رفض مغلق (fail-closed). أدوات الوكلاء: قراءة (`status/output/list`) تتطلب `agentsAllowed`، والكتابة (`start/send/pause/resume/stop/cancel`) تتطلب أيضًا وضعًا غير readonly — وتُرجع أخطاء أدوات `isError` لا protocol errors.
|
|
116
|
+
- `task_create` يرث وضع الـ workspace؛ بوابات التحقق تخضع لقوائم الأوامر والسياسة الخاصة بالـ workspace.
|
|
117
|
+
- **Consequences:** مخطط JSON (`schema/v1/hearth.schema.json`) أُعيد توليده. اختبار `workspace-profiles.test.ts` يغطي العزل بين workspaces.
|
|
118
|
+
|
|
119
|
+
## ADR-026: تشديد Task Runtime — سقف إصلاح + استئناف
|
|
120
|
+
- **Status:** accepted (2026-09-07، الجولة الثالثة)
|
|
121
|
+
- **Decision:**
|
|
122
|
+
- سقف حلقات الإصلاح: بعد 5 جولات تحقق فاشلة تُعلَّم المهمة `failed` (سبب صريح) بدل `repairing` للأبد — يُحسب من evidence `verifying → repairing` بلا migration.
|
|
123
|
+
- أداة `task_resume`: تُعيد المهمة `failed` فقط إلى `planning` مع أثر تدقيق؛ `completed/cancelled` نهائية، والحية تستخدم الانتقالات العادية. هذا يعطي مسار استرداد صريح بعد `reconcileOnBoot` أو استنفاد ميزانية الإصلاح.
|
|
124
|
+
- **Consequences:** `task_verify`/`task_complete` الفاشلة تحترم السقف. الاختبارات تغطي السقف والاستئناف ورفض استئناف الحية.
|
|
125
|
+
|
|
126
|
+
## ADR-027: سجل العمليات + جامع اليتامى + ربط الوكلاء بالمهام
|
|
127
|
+
- **Status:** accepted (2026-09-07، الجولة الثالثة)
|
|
128
|
+
- **Decision:**
|
|
129
|
+
- `process-journal.ts`: كل عملية حية تُسجَّل (`{ pid, pgid, workspaceId, workspaceRoot, commandPreview }`، وضع 0600) وتُحذف عند الخروج الطبيعي. عند الإقلاع (`server.ts` + `stdio-server.ts`) يُمسح السجل: PID الميت يُسقط، وPID المُعاد استخدامه (cmdline لا يطابق) لا يُقتل أبدًا — فقط المطابق يُنهى (group ثم مفرد) مع حدث تدقيق. المنصات بلا /proc/ps تُسقط بأمان دون قتل.
|
|
130
|
+
- ربط الوكيل بالمهمة: `agent_start` يقبل `taskId?` + `correlationId?`؛ `taskId` مجهول = خطأ صريح قبل أي spawn؛ عند النجاح يُسجَّل evidence في المهمة عبر `linkAgentToTask` (مُصدَّرة ومُختبَرة وحدويًا). يتطلب `taskStore` اختياريًا في سياق أدوات الوكلاء (يُمرَّر من `createMcpServer`).
|
|
131
|
+
- **Consequences:** لا مزيد من العمليات اليتيمة الصامتة بعد crash/restart؛ ملكية الوكيل قابلة للاستعلام عبر evidence المهمة.
|
|
132
|
+
|
|
133
|
+
## ADR-028: طبقة التنسيق — briefs + lanes + watchdog + snapshot + scopes
|
|
134
|
+
- **Status:** accepted (2026-09-07، الجولة الرابعة — بمرجع delegate-skills كإلهام لا تبعية)
|
|
135
|
+
- **Decision:**
|
|
136
|
+
- `src/orchestration/brief.ts`: brief مكتمل ذاتيًا للمهمات المرتبطة (taskId) — header آلي + caller prompt + plan + scope/safety (بلا commit) + ملخص repo-map (1000 ملف) + تعليمات الريبو كمقتبس غير موثوق + بوابات التحقق المكتشفة + الإخفاقات السابقة + عقد التقرير. حتمي ومحدود الحجم. التشغيلات ad-hoc تمرر الـ prompt كما هو.
|
|
137
|
+
- `src/orchestration/result.ts`: `touchedFiles` عبر `git status --porcelain` (null خارج الريبو، [] للشجرة النظيفة) تُلحق بردود status/output، وevidence منظم `agent_result` عند الحالات المستقرة مع منع التكرار. تقرير النموذج وحده ليس دليلًا أبدًا.
|
|
138
|
+
- `src/orchestration/lanes.ts`: lanes اختيارية (global + overlay للمشروع `.hearth/fleet.json` بموافقة hash تفشل مغلقةً عند التعديل). `agent_start(lane?)`: أعلام صريحة تفوز، وتناقض target/lane خطأ صريح، ومزود معطل خطأ مبكر. أدوات `fleet_status` (قراءة) و`fleet_approve` (موافقة مدققة). لا تبديل تلقائي للمزودين أبدًا.
|
|
139
|
+
- Watchdog لكل turn (افتراضي 30 دقيقة، سقف 7 أيام، `AGENT_TIMEOUT` قابلة لإعادة المحاولة) + تنبيه stall استشاري من نبض `updatedAt` (5 دقائق) دون قتل تلقائي. الـ timeout يفوز على stop/pause المتزامن لأنه الأصدق.
|
|
140
|
+
- Snapshot تلقائي قبل التفويض الكتابي (best-effort، لا يحجب أبدًا) + حارس تداخل النطاقات (sequential by default، `scopePaths`/`allowConcurrentWrites` صريحان، مسارات هاربة مرفوضة).
|
|
141
|
+
- `requireSandboxForAutonomous` أصبح `true` افتراضيًا (fail-closed على Windows/بلا adapter) مع توثيق صراحة المنصات في `docs/security.md`.
|
|
142
|
+
- **Consequences:** أدوات جديدة `fleet_status/fleet_approve` في سطح claude/codex؛ `agent_start/send/resume` تقبل `timeoutMs`؛ `agent_start` تقبل `lane/scopePaths/allowConcurrentWrites` و`target` اختيارية مع lane. المفاهيم تبقى منفصلة: AI Client / MCP / Runtime / Orchestrator / Adapter / Session / Process / Task / Workspace / Policy / Sandbox / Verification / Transport.
|
|
143
|
+
|
|
144
|
+
## ADR-029: لوحة تحكم محلية (واجهة Hearth)
|
|
145
|
+
- **Status:** accepted (2026-09-07، واجهة محلية)
|
|
146
|
+
- **Decision:**
|
|
147
|
+
- `hearth serve` يخدم واجهة ops-console في `/dashboard` (ملفات ثابتة بلا build، تُنسخ إلى `dist/dashboard` عبر `build:dashboard`) مع APIs تحت `/dashboard/api`.
|
|
148
|
+
- المصادقة: دخول بكلمة مرور المالك يُصدر session cookie موقعة HMAC (12 ساعة، HttpOnly، SameSite=Lax) — التوكن نفسه لا يُخزن في المتصفح أبدًا. كل APIs المغيّرة تشترط JSON content-type (دفاع CSRF للـ fetch فقط)، والدخول محدود المعدل.
|
|
149
|
+
- القراءة: status/workspaces/tasks/agents/snapshots/logs/setup — نفس الحالة المرئية لسطح MCP، بلا تنفيذ مميز خاص باللوحة. السجلات حلقة محدودة (200) خالية من الأسرار بالبناء.
|
|
150
|
+
- الكتابة: `PUT /dashboard/api/config` بقائمة سماح ضيقة (execution.mode/sandbox/sandboxNetwork/requireSandbox + fleet.lanes + workspaces.profiles) مع تحقق zod، وتتطلب restart للتطبيق.
|
|
151
|
+
- **Consequences:** الواجهة مكملة لا بديلة — الوكلاء والمهام تُدار من عميل MCP، واللوحة للمراقبة والإعداد. مع tunneled host تبقى محمية بكلمة مرور المالك مثل بوابة OAuth.
|
|
152
|
+
|
|
153
|
+
## ADR-030: إعادة التسمية إلى Hearth + التعرض المباشر بلا relay
|
|
154
|
+
- **Status:** accepted (2026-09-07)
|
|
155
|
+
- **Decision:**
|
|
156
|
+
- الاسم الجديد Hearth في كل سطح هوية: ثنائيات `hearth`/`hearth-agentd`، حزمة `@waishnav/hearth`، `~/.hearth`، متغيرات `HEARTH_*`، المنفذ 7176، `hearth.sqlite`، `refs/hearth/`، نطاق OAuth `hearth`. الهدف: تعايش بلا تعارض مع أي تثبيت DevSpace موجود (ثنائي/إعداد/حزمة/منفذ مختلفة).
|
|
157
|
+
- ترحيل لمرة واحدة من `~/.devspace` (نسخ config+auth بأذونات 0600، الأصل يُحفظ) + إشعار CLI. مفتاح الاختبار `HEARTH_LEGACY_DEVSPACE_DIR`.
|
|
158
|
+
- تعرض مباشر اختياري بلا relay: قسم `tls` (certFile/keyFile/acmeDir)، إنهاء HTTPS داخل `serve`، ومسار `/.well-known/acme-challenge/` لوضع certbot webroot. هوية ثابتة لكل PC (`hearth id`, ملف `machine.json` بوضع 0600) + أمر `hearth expose` التشخيصي الصادق: عنوان عام + حالة TLS + خطوات تالية مخصصة، مع التصريح الصريح بأن URL عام يتطلب مسار inbound ونطاقًا — لا سحر هنا.
|
|
159
|
+
- **Consequences:** منح OAuth القديمة (نطاق devspace) تحتاج إعادة موافقة. روابط schema تشير إلى مستودع `Waishnav/hearth` (إعادة تسمية المستودع على GitHub خطوة يدوية لاحقة). النشر الأول على npm (`install.sh` يعمل من checkout حتى ذلك الحين).
|