From 6ac195c2e6cc0438d4517978e2b072b13d775dc2 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sun, 29 Mar 2026 20:15:58 +0000 Subject: [PATCH] fix(agentic): align workspace flow with AX design Co-Authored-By: Virgil --- cmd/core-agent/main.go | 5 +- cmd/core-agent/mcp_service.go | 33 ++ go.mod | 14 +- go.sum | 506 +++++++++++++++++++++++++++ pkg/agentic/actions.go | 10 +- pkg/agentic/actions_example_test.go | 7 +- pkg/agentic/commands_forge.go | 83 ++++- pkg/agentic/commands_workspace.go | 9 +- pkg/agentic/dispatch.go | 34 +- pkg/agentic/ingest.go | 11 +- pkg/agentic/ingest_test.go | 99 +++--- pkg/agentic/paths.go | 33 ++ pkg/agentic/pr.go | 27 +- pkg/agentic/prep.go | 29 +- pkg/agentic/proc_test.go | 3 +- pkg/agentic/process_register.go | 88 ++++- pkg/agentic/register_test.go | 2 +- pkg/agentic/resume.go | 4 +- pkg/agentic/status.go | 47 ++- pkg/agentic/status_test.go | 15 +- pkg/agentic/verify.go | 18 +- pkg/agentic/watch.go | 7 +- pkg/agentic/watch_test.go | 51 ++- pkg/brain/brain.go | 2 +- pkg/brain/bridge_test.go | 7 +- pkg/brain/direct.go | 2 +- pkg/brain/provider.go | 4 +- pkg/brain/tools.go | 2 +- pkg/monitor/harvest.go | 14 +- pkg/monitor/harvest_test.go | 67 ++-- pkg/monitor/monitor.go | 42 +-- pkg/monitor/monitor_test.go | 82 ++--- pkg/monitor/monitor_testmain_test.go | 4 +- pkg/runner/queue.go | 25 +- pkg/runner/queue_test.go | 12 +- pkg/runner/runner.go | 68 ++-- pkg/runner/runner_test.go | 25 ++ 37 files changed, 1156 insertions(+), 335 deletions(-) create mode 100644 cmd/core-agent/mcp_service.go create mode 100644 go.sum diff --git a/cmd/core-agent/main.go b/cmd/core-agent/main.go index 951204a..6eca564 100644 --- a/cmd/core-agent/main.go +++ b/cmd/core-agent/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: EUPL-1.2 + package main import ( @@ -7,7 +9,6 @@ import ( "dappco.re/go/agent/pkg/brain" "dappco.re/go/agent/pkg/monitor" "dappco.re/go/agent/pkg/runner" - "dappco.re/go/mcp/pkg/mcp" ) func main() { @@ -22,7 +23,7 @@ func main() { core.WithService(runner.Register), core.WithService(monitor.Register), core.WithService(brain.Register), - core.WithService(mcp.Register), + core.WithName("mcp", registerMCPService), ) c.App().Version = appVersion() diff --git a/cmd/core-agent/mcp_service.go b/cmd/core-agent/mcp_service.go new file mode 100644 index 0000000..34d24f4 --- /dev/null +++ b/cmd/core-agent/mcp_service.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package main + +import ( + "dappco.re/go/agent/pkg/agentic" + "dappco.re/go/agent/pkg/brain" + "dappco.re/go/agent/pkg/monitor" + core "dappco.re/go/core" + "forge.lthn.ai/core/mcp/pkg/mcp" +) + +func registerMCPService(c *core.Core) core.Result { + var subsystems []mcp.Subsystem + + if prep, ok := core.ServiceFor[*agentic.PrepSubsystem](c, "agentic"); ok { + subsystems = append(subsystems, prep) + } + if mon, ok := core.ServiceFor[*monitor.Subsystem](c, "monitor"); ok { + subsystems = append(subsystems, mon) + } + if brn, ok := core.ServiceFor[*brain.DirectSubsystem](c, "brain"); ok { + subsystems = append(subsystems, brn) + } + + svc, err := mcp.New(mcp.Options{ + Subsystems: subsystems, + }) + if err != nil { + return core.Result{Value: core.E("main.registerMCPService", "create mcp service", err), OK: false} + } + return core.Result{Value: svc, OK: true} +} diff --git a/go.mod b/go.mod index 3a27ad8..af882a1 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( dappco.re/go/core/api v0.2.0 dappco.re/go/core/process v0.3.0 dappco.re/go/core/ws v0.3.0 + forge.lthn.ai/core/mcp v0.4.8 github.com/gin-gonic/gin v1.12.0 github.com/gorilla/websocket v1.5.3 github.com/modelcontextprotocol/go-sdk v1.4.1 @@ -14,11 +15,22 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require dappco.re/go/core/forge v0.2.0 // indirect +require ( + dappco.re/go/core/forge v0.2.0 + forge.lthn.ai/core/go-ws v0.2.5 +) require ( dappco.re/go/core/io v0.2.0 // indirect dappco.re/go/core/log v0.1.0 // indirect + forge.lthn.ai/core/api v0.1.5 // indirect + forge.lthn.ai/core/go v0.3.3 // indirect + forge.lthn.ai/core/go-ai v0.1.12 // indirect + forge.lthn.ai/core/go-io v0.1.7 // indirect + forge.lthn.ai/core/go-log v0.0.4 // indirect + forge.lthn.ai/core/go-process v0.2.9 // indirect + forge.lthn.ai/core/go-rag v0.1.11 // indirect + forge.lthn.ai/core/go-webview v0.1.6 // indirect github.com/99designs/gqlgen v0.17.88 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b24ddf2 --- /dev/null +++ b/go.sum @@ -0,0 +1,506 @@ +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core/api v0.2.0 h1:5OcN9nawpp18Jp6dB1OwI2CBfs0Tacb0y0zqxFB6TJ0= +dappco.re/go/core/api v0.2.0/go.mod h1:AtgNAx8lDY+qhVObFdNQOjSUQrHX1BeiDdMuA6RIfzo= +dappco.re/go/core/forge v0.2.0 h1:EBCHaUdzEAbYpDwRTXMmJoSfSrK30IJTOVBPRxxkJTg= +dappco.re/go/core/forge v0.2.0/go.mod h1:XMz9ZNVl9xane9Rg3AEBuVV5UNNBGWbPY9rSKbqYgnM= +dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= +dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= +dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= +dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= +dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM= +dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as= +dappco.re/go/core/ws v0.3.0 h1:ZxR8y5pfrWvnCHVN7qExXz7fdP5a063uNqyqE0Ab8pQ= +dappco.re/go/core/ws v0.3.0/go.mod h1:aLyXrJnbCOGL0SW9rC1EHAAIS83w3djO374gHIz4Nic= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= +forge.lthn.ai/core/api v0.1.5 h1:NwZrcOyBjaiz5/cn0n0tnlMUodi8Or6FHMx59C7Kv2o= +forge.lthn.ai/core/api v0.1.5/go.mod h1:PBnaWyOVXSOGy+0x2XAPUFMYJxQ2CNhppia/D06ZPII= +forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs= +forge.lthn.ai/core/go v0.3.2/go.mod h1:f7/zb3Labn4ARfwTq5Bi2AFHY+uxyPHozO+hLb54eFo= +forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0= +forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ= +forge.lthn.ai/core/go-ai v0.1.12 h1:OHt0bUABlyhvgxZxyMwueRoh8rS3YKWGFY6++zCAwC8= +forge.lthn.ai/core/go-ai v0.1.12/go.mod h1:5Pc9lszxgkO7Aj2Z3dtq4L9Xk9l/VNN+Baj1t///OCM= +forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo= +forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8= +forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= +forge.lthn.ai/core/go-io v0.1.5/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI= +forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk= +forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4= +forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= +forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= +forge.lthn.ai/core/go-ml v0.1.0/go.mod h1:FPV9JhIUOZdLeJpX1ggC15BpmM740NPg6rycnOc5vss= +forge.lthn.ai/core/go-mlx v0.1.0/go.mod h1:b4BJX67nx9QZiyREl2lmYIPJ+Yp5amZug3y7vXaRy/Y= +forge.lthn.ai/core/go-process v0.2.9 h1:Wql+5TUF+lfU2oJ9I+S764MkTqJhBsuyMM0v1zsfZC4= +forge.lthn.ai/core/go-process v0.2.9/go.mod h1:NIzZOF5IVYYCjHkcNIGcg1mZH+bzGoie4SlZUDYOKIM= +forge.lthn.ai/core/go-rag v0.1.11 h1:KXTOtnOdrx8YKmvnj0EOi2EI/+cKjE8w2PpJCQIrSd8= +forge.lthn.ai/core/go-rag v0.1.11/go.mod h1:vIlOKVD1SdqqjkJ2XQyXPuKPtiajz/STPLCaDpqOzk8= +forge.lthn.ai/core/go-scm v0.2.0/go.mod h1:Q/PV2FbqDlWnAOsXAd1pgSiHOlRCPW4HcPmOt8Z9H+E= +forge.lthn.ai/core/go-webview v0.1.6 h1:szXQxRJf2bOZJKh3v1P01B1Vf9mgXaBCXzh0EZu9aoc= +forge.lthn.ai/core/go-webview v0.1.6/go.mod h1:5n1tECD1wBV/uFZRY9ZjfPFO5TYZrlaR3mQFwvO2nek= +forge.lthn.ai/core/go-ws v0.2.5 h1:ZIV7Yrv01R/xpJUogA5vrfP9yB9li1w7EV3eZFMt8h0= +forge.lthn.ai/core/go-ws v0.2.5/go.mod h1:C3riJyLLcV6QhLvYlq3P/XkGTsN598qQeGBoLdoHBU4= +forge.lthn.ai/core/mcp v0.4.8 h1:nd1x3AL8AkUfl0kziltoJUX96Nx1BeFWEbgHmfrkKz8= +forge.lthn.ai/core/mcp v0.4.8/go.mod h1:eU35WT/8Mc0oJDVWdKaXEtNp27+Hc8KvnTKPf4DAqXE= +github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc= +github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a/go.mod h1:Sdr/tmSOLEnncCuXS5TwZRxuk7deH1WXVY8cve3eVBM= +github.com/apache/arrow-go/v18 v18.5.2/go.mod h1:yNoizNTT4peTciJ7V01d2EgOkE1d0fQ1vZcFOsVtFsw= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/boj/redistore v1.4.1/go.mod h1:c0Tvw6aMjslog4jHIAcNv6EtJM849YoOAhMY7JBbWpI= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20240916143655-c0e34fd2f304/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= +github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= +github.com/chewxy/math32 v1.11.0/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1/go.mod h1:uw2gLcxEuYUlAd/EXyjc/v55nd3+47YAgWbSXVxPrNI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/authz v1.0.6 h1:qAO4sSSzOPCwYRZI6YtubC+h2tZVwhwSJeyEZn2W+5k= +github.com/gin-contrib/authz v1.0.6/go.mod h1:A2B5Im1M/HIoHPjLc31j3RlENSE6j8euJY9NFdzZeYo= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/expvar v1.0.3 h1:nIbUaokxZfUEC/35h+RyWCP1SMF/suV/ARbXL3H3jrw= +github.com/gin-contrib/expvar v1.0.3/go.mod h1:bwqqmhty1Zl2JYVLzBIL6CSHDWDbQoQoicalAnBvUnY= +github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= +github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= +github.com/gin-contrib/httpsign v1.0.3 h1:NpeDQjmUV0qFjGCm/rkXSp3HH0hU7r84q1v+VtTiI5I= +github.com/gin-contrib/httpsign v1.0.3/go.mod h1:n4GC7StmHNBhIzWzuW2njKbZMeEWh4tDbmn3bD1ab+k= +github.com/gin-contrib/location/v2 v2.0.0 h1:iLx5RatHQHSxgC0tm2AG0sIuQKecI7FhREessVd6RWY= +github.com/gin-contrib/location/v2 v2.0.0/go.mod h1:276TDNr25NENBA/NQZUuEIlwxy/I5CYVFIr/d2TgOdU= +github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM= +github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY= +github.com/gin-contrib/secure v1.1.2 h1:6G8/NCOTSywWY7TeaH/0Yfaa6bfkE5ukkqtIm7lK11U= +github.com/gin-contrib/secure v1.1.2/go.mod h1:xI3jI5/BpOYMCBtjgmIVrMA3kI7y9LwCFxs+eLf5S3w= +github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= +github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= +github.com/gin-contrib/slog v1.2.0 h1:vAxZfr7knD1ZYK5+pMJLP52sZXIkJXkcRPa/0dx9hSk= +github.com/gin-contrib/slog v1.2.0/go.mod h1:vYK6YltmpsEFkO0zfRMLTKHrWS3DwUSn0TMpT+kMagI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4= +github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM= +github.com/gin-contrib/timeout v1.1.0 h1:WAmWseo5gfBUbMrMJu5hJxDclehfSJUmK2wGwCC/EFw= +github.com/gin-contrib/timeout v1.1.0/go.mod h1:NpRo4gd1Ad8ZQ4T6bQLVFDqiplCmPRs2nvfckxS2Fw4= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/laziness-coders/mongostore v0.0.14/go.mod h1:Rh+yJax2Vxc2QY62clIM/kRnLk+TxivgSLHOXENXPtk= +github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/matryer/moq v0.6.0/go.mod h1:iEVhY/XBwFG/nbRyEf0oV+SqnTHZJ5wectzx7yT+y98= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= +github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= +github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nlpodyssey/gopickle v0.3.0/go.mod h1:f070HJ/yR+eLi5WmM1OXJEGaTpuJEUiib19olXgYha0= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/ollama/ollama v0.18.2 h1:RsOY8oZ6TufRiPgsSlKJp4/V/X+oBREscUlEHZfd554= +github.com/ollama/ollama v0.18.2/go.mod h1:tCX4IMV8DHjl3zY0THxuEkpWDZSOchJpzTuLACpMwFw= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs= +github.com/parquet-go/jsonlite v1.4.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= +github.com/parquet-go/parquet-go v0.29.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg= +github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c/go.mod h1:PSojXDXF7TbgQiD6kkd98IHOS0QqTyUEaWRiS8+BLu8= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g= +github.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8= +github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/typescriptify-golang-structs v0.2.0/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= +github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU= +github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= +github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/wader/gormstore/v2 v2.0.3/go.mod h1:sr3N3a8F1+PBc3fHoKaphFqDXLRJ9Oe6Yow0HxKFbbg= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0 h1:E7DmskpIO7ZR6QI6zKSEKIDNUYoKw9oHXP23gzbCdU0= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0/go.mod h1:WB2cS9y+AwqqKhoo9gw6/ZxlSjFBUQGZ8BQOaD3FVXM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA= +gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/agentic/actions.go b/pkg/agentic/actions.go index 44b331d..f969721 100644 --- a/pkg/agentic/actions.go +++ b/pkg/agentic/actions.go @@ -111,7 +111,13 @@ func (s *PrepSubsystem) handleScan(ctx context.Context, opts core.Options) core. // core.Option{Key: "workspace", Value: "core/go-io/task-5"}, // )) func (s *PrepSubsystem) handleWatch(ctx context.Context, opts core.Options) core.Result { - input := WatchInput{} + input := WatchInput{ + PollInterval: opts.Int("poll_interval"), + Timeout: opts.Int("timeout"), + } + if workspace := opts.String("workspace"); workspace != "" { + input.Workspaces = []string{workspace} + } _, out, err := s.watch(ctx, nil, input) if err != nil { return core.Result{Value: err, OK: false} @@ -151,7 +157,7 @@ func (s *PrepSubsystem) handleQA(ctx context.Context, opts core.Options) core.Re repo = st.Repo } s.Core().ACTION(messages.QAResult{ - Workspace: core.PathBase(wsDir), + Workspace: WorkspaceName(wsDir), Repo: repo, Passed: passed, }) diff --git a/pkg/agentic/actions_example_test.go b/pkg/agentic/actions_example_test.go index 2c4ce3a..9bdc486 100644 --- a/pkg/agentic/actions_example_test.go +++ b/pkg/agentic/actions_example_test.go @@ -6,14 +6,13 @@ import ( "context" core "dappco.re/go/core" - "dappco.re/go/core/process" "dappco.re/go/agent/pkg/agentic" ) func ExampleRegister() { c := core.New( - core.WithService(process.Register), + core.WithService(agentic.ProcessRegister), core.WithService(agentic.Register), ) c.ServiceStartup(context.Background(), nil) @@ -30,7 +29,7 @@ func ExampleRegister() { func ExampleRegister_actions() { c := core.New( - core.WithService(process.Register), + core.WithService(agentic.ProcessRegister), core.WithService(agentic.Register), ) c.ServiceStartup(context.Background(), nil) @@ -42,7 +41,7 @@ func ExampleRegister_actions() { func ExampleRegister_task() { c := core.New( - core.WithService(process.Register), + core.WithService(agentic.ProcessRegister), core.WithService(agentic.Register), ) c.ServiceStartup(context.Background(), nil) diff --git a/pkg/agentic/commands_forge.go b/pkg/agentic/commands_forge.go index c751727..dabf255 100644 --- a/pkg/agentic/commands_forge.go +++ b/pkg/agentic/commands_forge.go @@ -11,6 +11,66 @@ import ( forge_types "dappco.re/go/core/forge/types" ) +type issueView struct { + Index int64 `json:"index"` + Number int64 `json:"number"` + Title string `json:"title"` + State string `json:"state"` + HTMLURL string `json:"html_url"` + Body string `json:"body"` +} + +type prBranchView struct { + Ref string `json:"ref"` +} + +type prUserView struct { + Login string `json:"login"` + UserName string `json:"username"` +} + +type prLabelView struct { + Name string `json:"name"` +} + +type pullRequestView struct { + Index int64 `json:"index"` + Number int64 `json:"number"` + Title string `json:"title"` + State string `json:"state"` + Mergeable bool `json:"mergeable"` + HTMLURL string `json:"html_url"` + Body string `json:"body"` + Head prBranchView `json:"head"` + Base prBranchView `json:"base"` + User *prUserView `json:"user"` + Labels []prLabelView `json:"labels"` +} + +func issueNumber(issue issueView) int64 { + if issue.Index != 0 { + return issue.Index + } + return issue.Number +} + +func pullRequestNumber(pr pullRequestView) int64 { + if pr.Index != 0 { + return pr.Index + } + return pr.Number +} + +func pullRequestAuthor(pr pullRequestView) string { + if pr.User == nil { + return "" + } + if pr.User.UserName != "" { + return pr.User.UserName + } + return pr.User.Login +} + // parseForgeArgs extracts org and repo from opts. func parseForgeArgs(opts core.Options) (org, repo string, num int64) { org = opts.String("org") @@ -47,12 +107,13 @@ func (s *PrepSubsystem) cmdIssueGet(opts core.Options) core.Result { core.Print(nil, "usage: core-agent issue get --number=N [--org=core]") return core.Result{OK: false} } - issue, err := s.forge.Issues.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": fmtIndex(num)}) + var issue issueView + err := s.forge.Client().Get(ctx, core.Sprintf("/api/v1/repos/%s/%s/issues/%d", org, repo, num), &issue) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} } - core.Print(nil, "#%d %s", issue.Index, issue.Title) + core.Print(nil, "#%d %s", issueNumber(issue), issue.Title) core.Print(nil, " state: %s", issue.State) core.Print(nil, " url: %s", issue.HTMLURL) if issue.Body != "" { @@ -69,13 +130,14 @@ func (s *PrepSubsystem) cmdIssueList(opts core.Options) core.Result { core.Print(nil, "usage: core-agent issue list [--org=core]") return core.Result{OK: false} } - issues, err := s.forge.Issues.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + var issues []issueView + err := s.forge.Client().Get(ctx, core.Sprintf("/api/v1/repos/%s/%s/issues?limit=50&page=1", org, repo), &issues) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} } for _, issue := range issues { - core.Print(nil, " #%-4d %-6s %s", issue.Index, issue.State, issue.Title) + core.Print(nil, " #%-4d %-6s %s", issueNumber(issue), issue.State, issue.Title) } if len(issues) == 0 { core.Print(nil, " no issues") @@ -117,7 +179,8 @@ func (s *PrepSubsystem) cmdIssueCreate(opts core.Options) core.Result { createOpts := &forge_types.CreateIssueOption{Title: title, Body: body, Ref: ref} if milestone != "" { - milestones, err := s.forge.Milestones.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + var milestones []forge_types.Milestone + err := s.forge.Client().Get(ctx, core.Sprintf("/api/v1/repos/%s/%s/milestones", org, repo), &milestones) if err == nil { for _, m := range milestones { if m.Title == milestone { @@ -163,12 +226,13 @@ func (s *PrepSubsystem) cmdPRGet(opts core.Options) core.Result { core.Print(nil, "usage: core-agent pr get --number=N [--org=core]") return core.Result{OK: false} } - pr, err := s.forge.Pulls.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": fmtIndex(num)}) + var pr pullRequestView + err := s.forge.Client().Get(ctx, core.Sprintf("/api/v1/repos/%s/%s/pulls/%d", org, repo, num), &pr) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} } - core.Print(nil, "#%d %s", pr.Index, pr.Title) + core.Print(nil, "#%d %s", pullRequestNumber(pr), pr.Title) core.Print(nil, " state: %s", pr.State) core.Print(nil, " head: %s", pr.Head.Ref) core.Print(nil, " base: %s", pr.Base.Ref) @@ -188,13 +252,14 @@ func (s *PrepSubsystem) cmdPRList(opts core.Options) core.Result { core.Print(nil, "usage: core-agent pr list [--org=core]") return core.Result{OK: false} } - prs, err := s.forge.Pulls.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + var prs []pullRequestView + err := s.forge.Client().Get(ctx, core.Sprintf("/api/v1/repos/%s/%s/pulls?limit=50&page=1", org, repo), &prs) if err != nil { core.Print(nil, "error: %v", err) return core.Result{Value: err, OK: false} } for _, pr := range prs { - core.Print(nil, " #%-4d %-6s %s → %s %s", pr.Index, pr.State, pr.Head.Ref, pr.Base.Ref, pr.Title) + core.Print(nil, " #%-4d %-6s %s → %s %s", pullRequestNumber(pr), pr.State, pr.Head.Ref, pr.Base.Ref, pr.Title) } if len(prs) == 0 { core.Print(nil, " no PRs") diff --git a/pkg/agentic/commands_workspace.go b/pkg/agentic/commands_workspace.go index 9a1301e..c9d88c9 100644 --- a/pkg/agentic/commands_workspace.go +++ b/pkg/agentic/commands_workspace.go @@ -19,13 +19,12 @@ func (s *PrepSubsystem) registerWorkspaceCommands() { } func (s *PrepSubsystem) cmdWorkspaceList(opts core.Options) core.Result { - wsRoot := WorkspaceRoot() fsys := s.Core().Fs() - statusFiles := core.PathGlob(core.JoinPath(wsRoot, "*", "status.json")) + statusFiles := WorkspaceStatusPaths() count := 0 for _, sf := range statusFiles { - wsName := core.PathBase(core.PathDir(sf)) + wsName := WorkspaceName(core.PathDir(sf)) if sr := fsys.Read(sf); sr.OK { content := sr.Value.(string) status := extractField(content, "status") @@ -49,11 +48,11 @@ func (s *PrepSubsystem) cmdWorkspaceClean(opts core.Options) core.Result { filter = "all" } - statusFiles := core.PathGlob(core.JoinPath(wsRoot, "*", "status.json")) + statusFiles := WorkspaceStatusPaths() var toRemove []string for _, sf := range statusFiles { - wsName := core.PathBase(core.PathDir(sf)) + wsName := WorkspaceName(core.PathDir(sf)) sr := fsys.Read(sf) if !sr.OK { continue diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index 754f65a..044c82c 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -288,6 +288,7 @@ func (s *PrepSubsystem) stopIssueTracking(wsDir string) { // broadcastStart emits IPC + audit events for agent start. func (s *PrepSubsystem) broadcastStart(agent, wsDir string) { + wsName := WorkspaceName(wsDir) st, _ := ReadStatus(wsDir) repo := "" if st != nil { @@ -295,15 +296,16 @@ func (s *PrepSubsystem) broadcastStart(agent, wsDir string) { } if s.ServiceRuntime != nil { s.Core().ACTION(messages.AgentStarted{ - Agent: agent, Repo: repo, Workspace: core.PathBase(wsDir), + Agent: agent, Repo: repo, Workspace: wsName, }) } - emitStartEvent(agent, core.PathBase(wsDir)) + emitStartEvent(agent, wsName) } // broadcastComplete emits IPC + audit events for agent completion. func (s *PrepSubsystem) broadcastComplete(agent, wsDir, finalStatus string) { - emitCompletionEvent(agent, core.PathBase(wsDir), finalStatus) + wsName := WorkspaceName(wsDir) + emitCompletionEvent(agent, wsName, finalStatus) if s.ServiceRuntime != nil { st, _ := ReadStatus(wsDir) repo := "" @@ -312,7 +314,7 @@ func (s *PrepSubsystem) broadcastComplete(agent, wsDir, finalStatus string) { } s.Core().ACTION(messages.AgentCompleted{ Agent: agent, Repo: repo, - Workspace: core.PathBase(wsDir), Status: finalStatus, + Workspace: wsName, Status: finalStatus, }) } } @@ -383,16 +385,15 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er if !ok { return 0, "", core.E("dispatch.spawnAgent", "process service not registered", nil) } - sr := procSvc.StartWithOptions(context.Background(), process.RunOptions{ + proc, err := procSvc.StartWithOptions(context.Background(), process.RunOptions{ Command: command, Args: args, Dir: repoDir, Detach: true, }) - if !sr.OK { - return 0, "", core.E("dispatch.spawnAgent", core.Concat("failed to spawn ", agent), nil) + if err != nil { + return 0, "", core.E("dispatch.spawnAgent", core.Concat("failed to spawn ", agent), err) } - proc := sr.Value.(*process.Process) proc.CloseStdin() pid := proc.Info().PID @@ -402,7 +403,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir string) (int, string, er // Register a one-shot Action that monitors this agent, then run it via PerformAsync. // PerformAsync tracks it in Core's WaitGroup — ServiceShutdown waits for it. - monitorAction := core.Concat("agentic.monitor.", core.PathBase(wsDir)) + monitorAction := core.Concat("agentic.monitor.", core.Replace(WorkspaceName(wsDir), "/", ".")) s.Core().Action(monitorAction, func(_ context.Context, _ core.Options) core.Result { <-proc.Done() s.onAgentComplete(agent, wsDir, outputFile, @@ -468,21 +469,6 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, input.Template = "coding" } - // Concurrency check — ask the runner - r := s.Core().Action("runner.dispatch").Run(ctx, core.NewOptions( - core.Option{Key: "agent", Value: input.Agent}, - core.Option{Key: "repo", Value: input.Repo}, - )) - if !r.OK { - reason, _ := r.Value.(string) - out := DispatchOutput{ - Repo: input.Repo, - Success: true, - OutputFile: core.Concat("queued — ", reason), - } - return nil, out, nil - } - // Step 1: Prep workspace — clone + build prompt prepInput := PrepInput{ Repo: input.Repo, diff --git a/pkg/agentic/ingest.go b/pkg/agentic/ingest.go index 10fe554..513fd86 100644 --- a/pkg/agentic/ingest.go +++ b/pkg/agentic/ingest.go @@ -8,6 +8,13 @@ import ( core "dappco.re/go/core" ) +func agentHomeDir() string { + if home := core.Env("HOME"); home != "" { + return home + } + return core.Env("DIR_HOME") +} + // ingestFindings reads the agent output log and creates issues via the API // for scan/audit results. Only runs for conventions and security templates. func (s *PrepSubsystem) ingestFindings(wsDir string) { @@ -17,7 +24,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { } // Read the log file - logFiles := core.PathGlob(core.JoinPath(wsDir, "agent-*.log")) + logFiles := workspaceLogFiles(wsDir) if len(logFiles) == 0 { return } @@ -88,7 +95,7 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p } // Read the agent API key from file - r := fs.Read(core.JoinPath(core.Env("DIR_HOME"), ".claude", "agent-api.key")) + r := fs.Read(core.JoinPath(agentHomeDir(), ".claude", "agent-api.key")) if !r.OK { return } diff --git a/pkg/agentic/ingest_test.go b/pkg/agentic/ingest_test.go index 632cb19..592b11a 100644 --- a/pkg/agentic/ingest_test.go +++ b/pkg/agentic/ingest_test.go @@ -45,20 +45,21 @@ func TestIngest_IngestFindings_Good_WithFindings(t *testing.T) { "- `pkg/core/service.go:100` has a missing error check\n" + "- `pkg/core/config.go:25` needs documentation\n" + "This is padding to get past the 100 char minimum length requirement for the log file content parsing." - require.True(t, fs.Write(core.JoinPath(wsDir, "agent-codex.log"), logContent).OK) + require.True(t, fs.EnsureDir(core.JoinPath(wsDir, ".meta")).OK) + require.True(t, fs.Write(core.JoinPath(wsDir, ".meta", "agent-codex.log"), logContent).OK) // Set up HOME for the agent-api.key read home := t.TempDir() - t.Setenv("DIR_HOME", home) + t.Setenv("HOME", home) require.True(t, fs.EnsureDir(core.JoinPath(home, ".claude")).OK) require.True(t, fs.Write(core.JoinPath(home, ".claude", "agent-api.key"), "test-api-key").OK) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - brainURL: srv.URL, - brainKey: "test-brain-key", - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + brainURL: srv.URL, + brainKey: "test-brain-key", + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } s.ingestFindings(wsDir) @@ -74,8 +75,8 @@ func TestIngest_IngestFindings_Bad_NotCompleted(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } // Should return early — status is not "completed" @@ -93,8 +94,8 @@ func TestIngest_IngestFindings_Bad_NoLogFile(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } // Should return early — no log files @@ -112,12 +113,13 @@ func TestIngest_IngestFindings_Bad_TooFewFindings(t *testing.T) { // Only 1 finding (need >= 2 to ingest) logContent := "Found: `main.go:1` has an issue. This padding makes the content long enough to pass the 100 char minimum check." - require.True(t, fs.Write(core.JoinPath(wsDir, "agent-codex.log"), logContent).OK) + require.True(t, fs.EnsureDir(core.JoinPath(wsDir, ".meta")).OK) + require.True(t, fs.Write(core.JoinPath(wsDir, ".meta", "agent-codex.log"), logContent).OK) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } assert.NotPanics(t, func() { @@ -134,12 +136,13 @@ func TestIngest_IngestFindings_Bad_QuotaExhausted(t *testing.T) { // Log contains quota error — should skip logContent := "QUOTA_EXHAUSTED: Rate limit exceeded. `main.go:1` `other.go:2` padding to ensure we pass length check and get past the threshold." - require.True(t, fs.Write(core.JoinPath(wsDir, "agent-codex.log"), logContent).OK) + require.True(t, fs.EnsureDir(core.JoinPath(wsDir, ".meta")).OK) + require.True(t, fs.Write(core.JoinPath(wsDir, ".meta", "agent-codex.log"), logContent).OK) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } assert.NotPanics(t, func() { @@ -152,8 +155,8 @@ func TestIngest_IngestFindings_Bad_NoStatusFile(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } assert.NotPanics(t, func() { @@ -169,12 +172,13 @@ func TestIngest_IngestFindings_Bad_ShortLogFile(t *testing.T) { })) // Log content is less than 100 bytes — should skip - require.True(t, fs.Write(core.JoinPath(wsDir, "agent-codex.log"), "short").OK) + require.True(t, fs.EnsureDir(core.JoinPath(wsDir, ".meta")).OK) + require.True(t, fs.Write(core.JoinPath(wsDir, ".meta", "agent-codex.log"), "short").OK) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } assert.NotPanics(t, func() { @@ -203,12 +207,17 @@ func TestIngest_CreateIssueViaAPI_Good_Success(t *testing.T) { })) t.Cleanup(srv.Close) + home := t.TempDir() + t.Setenv("HOME", home) + require.True(t, fs.EnsureDir(core.JoinPath(home, ".claude")).OK) + require.True(t, fs.Write(core.JoinPath(home, ".claude", "agent-api.key"), "test-key").OK) + s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - brainURL: srv.URL, - brainKey: "test-brain-key", - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + brainURL: srv.URL, + brainKey: "test-brain-key", + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } s.createIssueViaAPI("go-io", "Test Issue", "Description", "bug", "high", "scan") @@ -218,9 +227,9 @@ func TestIngest_CreateIssueViaAPI_Good_Success(t *testing.T) { func TestIngest_CreateIssueViaAPI_Bad_NoBrainKey(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - brainKey: "", - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + brainKey: "", + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } // Should return early without panic @@ -231,15 +240,15 @@ func TestIngest_CreateIssueViaAPI_Bad_NoBrainKey(t *testing.T) { func TestIngest_CreateIssueViaAPI_Bad_NoAPIKey(t *testing.T) { home := t.TempDir() - t.Setenv("DIR_HOME", home) + t.Setenv("HOME", home) // No agent-api.key file s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - brainURL: "https://example.com", - brainKey: "test-brain-key", - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + brainURL: "https://example.com", + brainKey: "test-brain-key", + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } // Should return early — no API key file @@ -255,16 +264,16 @@ func TestIngest_CreateIssueViaAPI_Bad_ServerError(t *testing.T) { t.Cleanup(srv.Close) home := t.TempDir() - t.Setenv("DIR_HOME", home) + t.Setenv("HOME", home) require.True(t, fs.EnsureDir(core.JoinPath(home, ".claude")).OK) require.True(t, fs.Write(core.JoinPath(home, ".claude", "agent-api.key"), "test-key").OK) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - brainURL: srv.URL, - brainKey: "test-brain-key", - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + brainURL: srv.URL, + brainKey: "test-brain-key", + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } // Should not panic even on server error @@ -296,8 +305,8 @@ func TestIngest_IngestFindings_Ugly(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } // Should return early without panic — no log files @@ -323,16 +332,16 @@ func TestIngest_CreateIssueViaAPI_Ugly(t *testing.T) { t.Cleanup(srv.Close) home := t.TempDir() - t.Setenv("DIR_HOME", home) + t.Setenv("HOME", home) require.True(t, fs.EnsureDir(core.JoinPath(home, ".claude")).OK) require.True(t, fs.Write(core.JoinPath(home, ".claude", "agent-api.key"), "test-key").OK) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - brainURL: srv.URL, - brainKey: "test-brain-key", - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + brainURL: srv.URL, + brainKey: "test-brain-key", + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } s.createIssueViaAPI("go-io", "XSS Test", "bold&", "bug", "high", "scan") diff --git a/pkg/agentic/paths.go b/pkg/agentic/paths.go index 61b41a2..87077eb 100644 --- a/pkg/agentic/paths.go +++ b/pkg/agentic/paths.go @@ -29,6 +29,13 @@ func WorkspaceRoot() string { return core.JoinPath(CoreRoot(), "workspace") } +// WorkspaceStatusPaths returns all workspace status files across supported layouts. +// +// paths := agentic.WorkspaceStatusPaths() +func WorkspaceStatusPaths() []string { + return workspaceStatusPaths(WorkspaceRoot()) +} + // WorkspaceName extracts the unique workspace name from a full path. // Given /Users/snider/Code/.core/workspace/core/go-io/dev → core/go-io/dev // @@ -54,6 +61,32 @@ func CoreRoot() string { return core.JoinPath(core.Env("DIR_HOME"), "Code", ".core") } +func workspaceStatusPaths(wsRoot string) []string { + old := core.PathGlob(core.JoinPath(wsRoot, "*", "status.json")) + deep := core.PathGlob(core.JoinPath(wsRoot, "*", "*", "*", "status.json")) + return append(old, deep...) +} + +func workspaceRepoDir(wsDir string) string { + return core.JoinPath(wsDir, "repo") +} + +func workspaceMetaDir(wsDir string) string { + return core.JoinPath(wsDir, ".meta") +} + +func workspaceBlockedPath(wsDir string) string { + return core.JoinPath(workspaceRepoDir(wsDir), "BLOCKED.md") +} + +func workspaceAnswerPath(wsDir string) string { + return core.JoinPath(workspaceRepoDir(wsDir), "ANSWER.md") +} + +func workspaceLogFiles(wsDir string) []string { + return core.PathGlob(core.JoinPath(workspaceMetaDir(wsDir), "agent-*.log")) +} + // PlansRoot returns the root directory for agent plans. // // plansDir := agentic.PlansRoot() diff --git a/pkg/agentic/pr.go b/pkg/agentic/pr.go index bded2ff..ceb8668 100644 --- a/pkg/agentic/pr.go +++ b/pkg/agentic/pr.go @@ -6,7 +6,6 @@ import ( "context" core "dappco.re/go/core" - "dappco.re/go/core/forge" forge_types "dappco.re/go/core/forge/types" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -157,16 +156,17 @@ func (s *PrepSubsystem) buildPRBody(st *WorkspaceStatus) string { } func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base, title, body string) (string, int, error) { - pr, err := s.forge.Pulls.Create(ctx, forge.Params{"owner": org, "repo": repo}, &forge_types.CreatePullRequestOption{ + var pr pullRequestView + err := s.forge.Client().Post(ctx, core.Sprintf("/api/v1/repos/%s/%s/pulls", org, repo), &forge_types.CreatePullRequestOption{ Title: title, Body: body, Head: head, Base: base, - }) + }, &pr) if err != nil { return "", 0, core.E("forgeCreatePR", "create PR failed", err) } - return pr.HTMLURL, int(pr.Index), nil + return pr.HTMLURL, int(pullRequestNumber(pr)), nil } func (s *PrepSubsystem) commentOnIssue(ctx context.Context, org, repo string, issue int, comment string) { @@ -269,30 +269,31 @@ func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, inp } func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string) ([]PRInfo, error) { - prs, err := s.forge.Pulls.ListAll(ctx, forge.Params{"owner": org, "repo": repo}) + var prs []pullRequestView + err := s.forge.Client().Get(ctx, core.Sprintf("/api/v1/repos/%s/%s/pulls?limit=50&page=1", org, repo), &prs) if err != nil { return nil, core.E("listRepoPRs", core.Concat("failed to list PRs for ", repo), err) } var result []PRInfo for _, pr := range prs { - if state != "" && state != "all" && string(pr.State) != state { + prState := pr.State + if prState == "" { + prState = "open" + } + if state != "" && state != "all" && prState != state { continue } var labels []string for _, l := range pr.Labels { labels = append(labels, l.Name) } - author := "" - if pr.User != nil { - author = pr.User.UserName - } result = append(result, PRInfo{ Repo: repo, - Number: int(pr.Index), + Number: int(pullRequestNumber(pr)), Title: pr.Title, - State: string(pr.State), - Author: author, + State: prState, + Author: pullRequestAuthor(pr), Branch: pr.Head.Ref, Base: pr.Base.Ref, Labels: labels, diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 5b9ef3c..10d0ba2 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -13,7 +13,7 @@ import ( "dappco.re/go/agent/pkg/lib" core "dappco.re/go/core" "dappco.re/go/core/forge" - coremcp "dappco.re/go/mcp/pkg/mcp" + coremcp "forge.lthn.ai/core/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" "gopkg.in/yaml.v3" ) @@ -37,8 +37,8 @@ type PrepSubsystem struct { drainMu sync.Mutex pokeCh chan struct{} frozen bool - backoff map[string]time.Time // pool → paused until - failCount map[string]int // pool → consecutive fast failures + backoff map[string]time.Time // pool → paused until + failCount map[string]int // pool → consecutive fast failures workspaces *core.Registry[*WorkspaceStatus] // in-memory workspace state } @@ -410,8 +410,8 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques return nil, PrepOutput{}, err } - repoDir := core.JoinPath(wsDir, "repo") - metaDir := core.JoinPath(wsDir, ".meta") + repoDir := workspaceRepoDir(wsDir) + metaDir := workspaceMetaDir(wsDir) out := PrepOutput{WorkspaceDir: wsDir, RepoDir: repoDir} // Source repo path — sanitise to prevent path traversal @@ -431,9 +431,22 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques out.Resumed = resumed if resumed { - // Pull latest from origin so the workspace has current code - s.gitCmd(ctx, repoDir, "checkout", "main") - s.gitCmd(ctx, repoDir, "pull", "origin", "main") + // Preserve the current branch on resume. Pull it only if it exists on + // origin; otherwise refresh the default branch refs without abandoning the + // workspace branch. + currentBranch := s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD") + defaultBranch := s.DefaultBranch(repoDir) + if currentBranch == "" || currentBranch == "HEAD" { + currentBranch = defaultBranch + } + if currentBranch != "" { + s.gitCmd(ctx, repoDir, "checkout", currentBranch) + if s.gitCmdOK(ctx, repoDir, "ls-remote", "--exit-code", "--heads", "origin", currentBranch) { + s.gitCmd(ctx, repoDir, "pull", "--ff-only", "origin", currentBranch) + } else if defaultBranch != "" { + s.gitCmd(ctx, repoDir, "fetch", "origin", defaultBranch) + } + } } // Extract default workspace template (go.work etc.) diff --git a/pkg/agentic/proc_test.go b/pkg/agentic/proc_test.go index 186fc22..c1a10fb 100644 --- a/pkg/agentic/proc_test.go +++ b/pkg/agentic/proc_test.go @@ -10,7 +10,6 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/core/process" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,7 +23,7 @@ var testCore *core.Core // TestMain sets up a PrepSubsystem with go-process registered for all tests in the package. func TestMain(m *testing.M) { testCore = core.New( - core.WithService(process.Register), + core.WithService(ProcessRegister), ) testCore.ServiceStartup(context.Background(), nil) diff --git a/pkg/agentic/process_register.go b/pkg/agentic/process_register.go index 9a97f90..eb5c382 100644 --- a/pkg/agentic/process_register.go +++ b/pkg/agentic/process_register.go @@ -3,17 +3,99 @@ package agentic import ( + "context" + core "dappco.re/go/core" "dappco.re/go/core/process" ) // ProcessRegister is the service factory for go-process. -// Delegates to process.Register — named Actions (process.run, process.start, -// process.kill, process.list, process.get) are registered during OnStartup. +// Registers the process service under the canonical "process" name and exposes +// the Core `process.*` Actions expected by `c.Process()`. // // core.New( // core.WithService(agentic.ProcessRegister), // ) func ProcessRegister(c *core.Core) core.Result { - return process.Register(c) + if c == nil { + return core.Result{Value: core.E("agentic.ProcessRegister", "core is required", nil), OK: false} + } + if _, ok := core.ServiceFor[*process.Service](c, "process"); ok { + return core.Result{OK: true} + } + + factory := process.NewService(process.Options{}) + instance, err := factory(c) + if err != nil { + return core.Result{Value: core.E("agentic.ProcessRegister", "create process service", err), OK: false} + } + svc, ok := instance.(*process.Service) + if !ok { + return core.Result{Value: core.E("agentic.ProcessRegister", "unexpected process service type", nil), OK: false} + } + if r := c.RegisterService("process", svc); !r.OK { + return r + } + + c.Action("process.run", func(ctx context.Context, opts core.Options) core.Result { + output, err := svc.RunWithOptions(ctx, process.RunOptions{ + Command: opts.String("command"), + Args: optionStrings(opts, "args"), + Dir: opts.String("dir"), + Env: optionStrings(opts, "env"), + }) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} + }) + + c.Action("process.start", func(ctx context.Context, opts core.Options) core.Result { + proc, err := svc.StartWithOptions(ctx, process.RunOptions{ + Command: opts.String("command"), + Args: optionStrings(opts, "args"), + Dir: opts.String("dir"), + Env: optionStrings(opts, "env"), + Detach: opts.Bool("detach"), + }) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: proc, OK: true} + }) + + c.Action("process.kill", func(_ context.Context, opts core.Options) core.Result { + id := opts.String("id") + if id == "" { + return core.Result{Value: core.E("agentic.ProcessRegister", "process id is required", nil), OK: false} + } + if err := svc.Kill(id); err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{OK: true} + }) + + return core.Result{OK: true} +} + +func optionStrings(opts core.Options, key string) []string { + r := opts.Get(key) + if !r.OK { + return nil + } + switch values := r.Value.(type) { + case []string: + return values + case []any: + out := make([]string, 0, len(values)) + for _, value := range values { + out = append(out, core.Sprint(value)) + } + return out + default: + if text := core.Sprint(values); text != "" { + return []string{text} + } + } + return nil } diff --git a/pkg/agentic/register_test.go b/pkg/agentic/register_test.go index cbfff2a..2172e36 100644 --- a/pkg/agentic/register_test.go +++ b/pkg/agentic/register_test.go @@ -63,7 +63,7 @@ func TestRegister_ProcessRegister_Good(t *testing.T) { c := core.New() result := ProcessRegister(c) assert.True(t, result.OK, "ProcessRegister should succeed with a real Core instance") - assert.NotNil(t, result.Value) + assert.True(t, c.Process().Exists(), "ProcessRegister should register the process service") } func TestRegister_ProcessRegister_Bad(t *testing.T) { diff --git a/pkg/agentic/resume.go b/pkg/agentic/resume.go index 5b9715f..edaeada 100644 --- a/pkg/agentic/resume.go +++ b/pkg/agentic/resume.go @@ -69,7 +69,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu // Write ANSWER.md if answer provided if input.Answer != "" { - answerPath := core.JoinPath(repoDir, "ANSWER.md") + answerPath := workspaceAnswerPath(wsDir) content := core.Sprintf("# Answer\n\n%s\n", input.Answer) if r := fs.Write(answerPath, content); !r.OK { err, _ := r.Value.(error) @@ -111,6 +111,6 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu Workspace: input.Workspace, Agent: agent, PID: pid, - OutputFile: core.JoinPath(wsDir, core.Sprintf("agent-%s.log", agent)), + OutputFile: agentOutputFile(wsDir, agent), }, nil } diff --git a/pkg/agentic/status.go b/pkg/agentic/status.go index 43ed160..f3dbe31 100644 --- a/pkg/agentic/status.go +++ b/pkg/agentic/status.go @@ -14,8 +14,9 @@ import ( // Workspace status file convention: // // {workspace}/status.json — current state of the workspace -// {workspace}/BLOCKED.md — question the agent needs answered (written by agent) -// {workspace}/ANSWER.md — response from human (written by reviewer) +// {workspace}/repo/BLOCKED.md — question the agent needs answered (written by agent) +// {workspace}/repo/ANSWER.md — response from human (written by reviewer) +// {workspace}/.meta/agent-*.log — captured agent output // // Status lifecycle: // running → completed (normal finish) @@ -29,20 +30,20 @@ import ( // st, err := ReadStatus(wsDir) // if err == nil && st.Status == "completed" { autoCreatePR(wsDir) } type WorkspaceStatus struct { - Status string `json:"status"` // running, completed, blocked, failed - Agent string `json:"agent"` // gemini, claude, codex - Repo string `json:"repo"` // target repo - Org string `json:"org,omitempty"` // forge org (e.g. "core") - Task string `json:"task"` // task description - Branch string `json:"branch,omitempty"` // git branch name + Status string `json:"status"` // running, completed, blocked, failed + Agent string `json:"agent"` // gemini, claude, codex + Repo string `json:"repo"` // target repo + Org string `json:"org,omitempty"` // forge org (e.g. "core") + Task string `json:"task"` // task description + Branch string `json:"branch,omitempty"` // git branch name Issue int `json:"issue,omitempty"` // forge issue number PID int `json:"pid,omitempty"` // OS process ID (if running) ProcessID string `json:"process_id,omitempty"` // go-process ID for managed lookup StartedAt time.Time `json:"started_at"` // when dispatch started - UpdatedAt time.Time `json:"updated_at"` // last status change - Question string `json:"question,omitempty"` // from BLOCKED.md - Runs int `json:"runs"` // how many times dispatched/resumed - PRURL string `json:"pr_url,omitempty"` // pull request URL (after PR created) + UpdatedAt time.Time `json:"updated_at"` // last status change + Question string `json:"question,omitempty"` // from BLOCKED.md + Runs int `json:"runs"` // how many times dispatched/resumed + PRURL string `json:"pr_url,omitempty"` // pull request URL (after PR created) } // WorkspaceQuery is the QUERY type for workspace state lookups. @@ -98,12 +99,12 @@ type StatusInput struct { // // out := agentic.StatusOutput{Total: 42, Running: 3, Queued: 10, Completed: 25} type StatusOutput struct { - Total int `json:"total"` - Running int `json:"running"` - Queued int `json:"queued"` - Completed int `json:"completed"` - Failed int `json:"failed"` - Blocked []BlockedInfo `json:"blocked,omitempty"` + Total int `json:"total"` + Running int `json:"running"` + Queued int `json:"queued"` + Completed int `json:"completed"` + Failed int `json:"failed"` + Blocked []BlockedInfo `json:"blocked,omitempty"` } // BlockedInfo shows a workspace that needs human input. @@ -126,10 +127,7 @@ func (s *PrepSubsystem) registerStatusTool(server *mcp.Server) { func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, input StatusInput) (*mcp.CallToolResult, StatusOutput, error) { wsRoot := WorkspaceRoot() - // Scan both old (*/status.json) and new (*/*/*/status.json) layouts - old := core.PathGlob(core.JoinPath(wsRoot, "*", "status.json")) - deep := core.PathGlob(core.JoinPath(wsRoot, "*", "*", "*", "status.json")) - statusFiles := append(old, deep...) + statusFiles := WorkspaceStatusPaths() var out StatusOutput @@ -147,13 +145,12 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu // If status is "running", check if PID is still alive if st.Status == "running" && st.PID > 0 { if err := syscall.Kill(st.PID, 0); err != nil { - blockedPath := core.JoinPath(wsDir, "repo", "BLOCKED.md") + blockedPath := workspaceBlockedPath(wsDir) if r := fs.Read(blockedPath); r.OK { st.Status = "blocked" st.Question = core.Trim(r.Value.(string)) } else { - logFile := core.JoinPath(wsDir, core.Sprintf("agent-%s.log", st.Agent)) - if r := fs.Read(logFile); !r.OK { + if len(workspaceLogFiles(wsDir)) == 0 { st.Status = "failed" st.Question = "Agent process died (no output log)" } else { diff --git a/pkg/agentic/status_test.go b/pkg/agentic/status_test.go index 67ced19..a6ec969 100644 --- a/pkg/agentic/status_test.go +++ b/pkg/agentic/status_test.go @@ -199,13 +199,14 @@ func TestStatus_Status_Ugly(t *testing.T) { // Case 2: running + dead PID + agent log → completed ws2 := core.JoinPath(wsRoot, "dead-completed") require.True(t, fs.EnsureDir(core.JoinPath(ws2, "repo")).OK) + require.True(t, fs.EnsureDir(core.JoinPath(ws2, ".meta")).OK) require.NoError(t, writeStatus(ws2, &WorkspaceStatus{ Status: "running", Repo: "go-log", Agent: "claude", PID: 999999, })) - require.True(t, fs.Write(core.JoinPath(ws2, "agent-claude.log"), "agent finished ok").OK) + require.True(t, fs.Write(core.JoinPath(ws2, ".meta", "agent-claude.log"), "agent finished ok").OK) // Case 3: running + dead PID + no log + no BLOCKED.md → failed ws3 := core.JoinPath(wsRoot, "dead-failed") @@ -219,8 +220,8 @@ func TestStatus_Status_Ugly(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } _, out, err := s.status(nil, nil, StatusInput{}) @@ -336,8 +337,8 @@ func TestStatus_Status_Good_PopulatedWorkspaces(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } _, out, err := s.status(context.Background(), nil, StatusInput{}) @@ -356,8 +357,8 @@ func TestStatus_Status_Bad_EmptyWorkspaceRoot(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } _, out, err := s.status(context.Background(), nil, StatusInput{}) diff --git a/pkg/agentic/verify.go b/pkg/agentic/verify.go index 3b76ed7..ac63c9c 100644 --- a/pkg/agentic/verify.go +++ b/pkg/agentic/verify.go @@ -191,6 +191,16 @@ type verifyResult struct { testCmd string } +func resultText(r core.Result) string { + if text, ok := r.Value.(string); ok { + return text + } + if r.Value != nil { + return core.Sprint(r.Value) + } + return "" +} + // runVerification detects the project type and runs the appropriate test suite. func (s *PrepSubsystem) runVerification(repoDir string) verifyResult { if fileExists(core.JoinPath(repoDir, "go.mod")) { @@ -208,7 +218,7 @@ func (s *PrepSubsystem) runVerification(repoDir string) verifyResult { func (s *PrepSubsystem) runGoTests(repoDir string) verifyResult { ctx := context.Background() r := s.runCmdEnv(ctx, repoDir, []string{"GOWORK=off"}, "go", "test", "./...", "-count=1", "-timeout", "120s") - out := r.Value.(string) + out := resultText(r) exitCode := 0 if !r.OK { exitCode = 1 @@ -225,9 +235,9 @@ func (s *PrepSubsystem) runPHPTests(repoDir string) verifyResult { if !r2.OK { return verifyResult{passed: false, testCmd: "none", output: "No PHP test runner found (composer test and vendor/bin/pest both unavailable)", exitCode: 1} } - return verifyResult{passed: true, output: r2.Value.(string), exitCode: 0, testCmd: "vendor/bin/pest"} + return verifyResult{passed: true, output: resultText(r2), exitCode: 0, testCmd: "vendor/bin/pest"} } - return verifyResult{passed: true, output: r.Value.(string), exitCode: 0, testCmd: "composer test"} + return verifyResult{passed: true, output: resultText(r), exitCode: 0, testCmd: "composer test"} } func (s *PrepSubsystem) runNodeTests(repoDir string) verifyResult { @@ -245,7 +255,7 @@ func (s *PrepSubsystem) runNodeTests(repoDir string) verifyResult { ctx := context.Background() r = s.runCmd(ctx, repoDir, "npm", "test") - out := r.Value.(string) + out := resultText(r) exitCode := 0 if !r.OK { exitCode = 1 diff --git a/pkg/agentic/watch.go b/pkg/agentic/watch.go index ba82b9a..eb3492a 100644 --- a/pkg/agentic/watch.go +++ b/pkg/agentic/watch.go @@ -190,18 +190,15 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp // findActiveWorkspaces returns workspace names that are running or queued. func (s *PrepSubsystem) findActiveWorkspaces() []string { - wsRoot := WorkspaceRoot() - entries := core.PathGlob(core.JoinPath(wsRoot, "*/status.json")) - var active []string - for _, entry := range entries { + for _, entry := range WorkspaceStatusPaths() { wsDir := core.PathDir(entry) st, err := ReadStatus(wsDir) if err != nil { continue } if st.Status == "running" || st.Status == "queued" { - active = append(active, core.PathBase(wsDir)) + active = append(active, WorkspaceName(wsDir)) } } return active diff --git a/pkg/agentic/watch_test.go b/pkg/agentic/watch_test.go index 80180b6..aeb7eb7 100644 --- a/pkg/agentic/watch_test.go +++ b/pkg/agentic/watch_test.go @@ -15,8 +15,8 @@ import ( func TestWatch_ResolveWorkspaceDir_Good_RelativeName(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } dir := s.resolveWorkspaceDir("go-io-abc123") assert.Contains(t, dir, "go-io-abc123") @@ -26,8 +26,8 @@ func TestWatch_ResolveWorkspaceDir_Good_RelativeName(t *testing.T) { func TestWatch_ResolveWorkspaceDir_Good_AbsolutePath(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } abs := "/some/absolute/path" assert.Equal(t, abs, s.resolveWorkspaceDir(abs)) @@ -58,8 +58,8 @@ func TestWatch_FindActiveWorkspaces_Good_WithActive(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } active := s.findActiveWorkspaces() assert.Contains(t, active, "ws-running") @@ -67,6 +67,25 @@ func TestWatch_FindActiveWorkspaces_Good_WithActive(t *testing.T) { assert.NotContains(t, active, "ws-done") } +func TestWatch_FindActiveWorkspaces_Good_DeepLayout(t *testing.T) { + root := t.TempDir() + t.Setenv("CORE_WORKSPACE", root) + + ws := core.JoinPath(root, "workspace", "core", "go-io", "task-15") + fs.EnsureDir(ws) + fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{ + Status: "running", Repo: "go-io", Agent: "codex", + })) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + active := s.findActiveWorkspaces() + assert.Contains(t, active, "core/go-io/task-15") +} + func TestWatch_FindActiveWorkspaces_Good_Empty(t *testing.T) { root := t.TempDir() t.Setenv("CORE_WORKSPACE", root) @@ -76,8 +95,8 @@ func TestWatch_FindActiveWorkspaces_Good_Empty(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } active := s.findActiveWorkspaces() assert.Empty(t, active) @@ -92,8 +111,8 @@ func TestWatch_FindActiveWorkspaces_Bad(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } assert.NotPanics(t, func() { active := s.findActiveWorkspaces() @@ -119,8 +138,8 @@ func TestWatch_FindActiveWorkspaces_Ugly(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } active := s.findActiveWorkspaces() @@ -135,8 +154,8 @@ func TestWatch_ResolveWorkspaceDir_Bad(t *testing.T) { // Empty name s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } dir := s.resolveWorkspaceDir("") assert.NotEmpty(t, dir, "empty name should still resolve to workspace root") @@ -147,8 +166,8 @@ func TestWatch_ResolveWorkspaceDir_Ugly(t *testing.T) { // Name with path traversal "../.." s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), - backoff: make(map[string]time.Time), - failCount: make(map[string]int), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), } assert.NotPanics(t, func() { dir := s.resolveWorkspaceDir("../..") diff --git a/pkg/brain/brain.go b/pkg/brain/brain.go index ae44d7b..5e04998 100644 --- a/pkg/brain/brain.go +++ b/pkg/brain/brain.go @@ -9,7 +9,7 @@ import ( "dappco.re/go/agent/pkg/agentic" core "dappco.re/go/core" - "dappco.re/go/mcp/pkg/mcp/ide" + "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/brain/bridge_test.go b/pkg/brain/bridge_test.go index 436fe4b..9ab2455 100644 --- a/pkg/brain/bridge_test.go +++ b/pkg/brain/bridge_test.go @@ -11,9 +11,8 @@ import ( "time" core "dappco.re/go/core" - providerws "dappco.re/go/core/ws" - bridgews "forge.lthn.ai/core/go-ws" - "dappco.re/go/mcp/pkg/mcp/ide" + providerws "forge.lthn.ai/core/go-ws" + "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/gorilla/websocket" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" @@ -46,7 +45,7 @@ func testBridge(t *testing.T) *ide.Bridge { srv := testWSServer(t) wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") - hub := bridgews.NewHub() + hub := providerws.NewHub() bridge := ide.NewBridge(hub, ide.Config{ LaravelWSURL: wsURL, ReconnectInterval: 100 * time.Millisecond, diff --git a/pkg/brain/direct.go b/pkg/brain/direct.go index 373624d..c7475c4 100644 --- a/pkg/brain/direct.go +++ b/pkg/brain/direct.go @@ -8,7 +8,7 @@ import ( "dappco.re/go/agent/pkg/agentic" core "dappco.re/go/core" - coremcp "dappco.re/go/mcp/pkg/mcp" + coremcp "forge.lthn.ai/core/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/brain/provider.go b/pkg/brain/provider.go index 92a29c7..1d4069a 100644 --- a/pkg/brain/provider.go +++ b/pkg/brain/provider.go @@ -8,8 +8,8 @@ import ( "dappco.re/go/core/api" "dappco.re/go/core/api/pkg/provider" - "dappco.re/go/core/ws" - "dappco.re/go/mcp/pkg/mcp/ide" + "forge.lthn.ai/core/go-ws" + "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/gin-gonic/gin" ) diff --git a/pkg/brain/tools.go b/pkg/brain/tools.go index bab44f8..c693b4d 100644 --- a/pkg/brain/tools.go +++ b/pkg/brain/tools.go @@ -7,7 +7,7 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/mcp/pkg/mcp/ide" + "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/monitor/harvest.go b/pkg/monitor/harvest.go index a92d4a9..159726f 100644 --- a/pkg/monitor/harvest.go +++ b/pkg/monitor/harvest.go @@ -88,35 +88,35 @@ func (m *Subsystem) harvestWorkspace(wsDir string) *harvestResult { return nil } - srcDir := core.Concat(wsDir, "/src") - if !fs.IsDir(srcDir) { + repoDir := core.JoinPath(wsDir, "repo") + if !fs.IsDir(repoDir) { return nil } // Check if there are commits to push branch := st.Branch if branch == "" { - branch = m.detectBranch(srcDir) + branch = m.detectBranch(repoDir) } - base := m.defaultBranch(srcDir) + base := m.defaultBranch(repoDir) if branch == "" || branch == base { return nil } // Check for unpushed commits - unpushed := m.countUnpushed(srcDir, branch) + unpushed := m.countUnpushed(repoDir, branch) if unpushed == 0 { return nil // already pushed or no commits } // Safety checks before pushing - if reason := m.checkSafety(srcDir); reason != "" { + if reason := m.checkSafety(repoDir); reason != "" { updateStatus(wsDir, "rejected", reason) return &harvestResult{repo: st.Repo, branch: branch, rejected: reason} } // Count changed files - files := m.countChangedFiles(srcDir) + files := m.countChangedFiles(repoDir) // Mark ready for review — do NOT auto-push. // Pushing is a high-impact mutation that should happen during diff --git a/pkg/monitor/harvest_test.go b/pkg/monitor/harvest_test.go index 237d222..ac701a1 100644 --- a/pkg/monitor/harvest_test.go +++ b/pkg/monitor/harvest_test.go @@ -6,9 +6,9 @@ import ( "context" "testing" + "dappco.re/go/agent/pkg/agentic" "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" - "dappco.re/go/core/process" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,17 +26,17 @@ func initTestRepo(t *testing.T) (sourceDir, wsDir string) { run(t, sourceDir, "git", "add", ".") run(t, sourceDir, "git", "commit", "-m", "init") - // Create workspace dir with src/ clone + // Create workspace dir with repo/ clone wsDir = core.JoinPath(t.TempDir(), "workspace") - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") fs.EnsureDir(wsDir) - run(t, wsDir, "git", "clone", sourceDir, "src") + run(t, wsDir, "git", "clone", sourceDir, "repo") // Create agent branch with a commit - run(t, srcDir, "git", "checkout", "-b", "agent/test-task") - fs.Write(core.JoinPath(srcDir, "new.go"), "package main\n") - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "agent work") + run(t, repoDir, "git", "checkout", "-b", "agent/test-task") + fs.Write(core.JoinPath(repoDir, "new.go"), "package main\n") + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "agent work") return sourceDir, wsDir } @@ -62,9 +62,9 @@ func writeStatus(t *testing.T, wsDir, status, repo, branch string) { func TestHarvest_DetectBranch_Good(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") - branch := testMon.detectBranch(srcDir) + branch := testMon.detectBranch(repoDir) assert.Equal(t, "agent/test-task", branch) } @@ -75,53 +75,53 @@ func TestHarvest_DetectBranch_Bad_NoRepo(t *testing.T) { func TestHarvest_CountUnpushed_Good(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") - count := testMon.countUnpushed(srcDir, "agent/test-task") + count := testMon.countUnpushed(repoDir, "agent/test-task") assert.Equal(t, 1, count) } func TestHarvest_CountChangedFiles_Good(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") - count := testMon.countChangedFiles(srcDir) + count := testMon.countChangedFiles(repoDir) assert.Equal(t, 1, count) } func TestHarvest_CheckSafety_Good_CleanWorkspace(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") - reason := testMon.checkSafety(srcDir) + reason := testMon.checkSafety(repoDir) assert.Equal(t, "", reason) } func TestHarvest_CheckSafety_Bad_BinaryFile(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") // Add a binary file - fs.Write(core.JoinPath(srcDir, "app.exe"), "binary") - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "add binary") + fs.Write(core.JoinPath(repoDir, "app.exe"), "binary") + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "add binary") - reason := testMon.checkSafety(srcDir) + reason := testMon.checkSafety(repoDir) assert.Contains(t, reason, "binary file added") assert.Contains(t, reason, "app.exe") } func TestHarvest_CheckSafety_Bad_LargeFile(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") // Add a file > 1MB bigData := make([]byte, 1024*1024+1) - fs.Write(core.JoinPath(srcDir, "huge.txt"), string(bigData)) - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "add large file") + fs.Write(core.JoinPath(repoDir, "huge.txt"), string(bigData)) + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "add large file") - reason := testMon.checkSafety(srcDir) + reason := testMon.checkSafety(repoDir) assert.Contains(t, reason, "large file") assert.Contains(t, reason, "huge.txt") } @@ -160,8 +160,8 @@ func TestHarvest_HarvestWorkspace_Bad_MainBranch(t *testing.T) { _, wsDir := initTestRepo(t) // Switch back to main - srcDir := core.JoinPath(wsDir, "src") - run(t, srcDir, "git", "checkout", "main") + repoDir := core.JoinPath(wsDir, "repo") + run(t, repoDir, "git", "checkout", "main") writeStatus(t, wsDir, "completed", "test-repo", "main") @@ -173,12 +173,12 @@ func TestHarvest_HarvestWorkspace_Bad_MainBranch(t *testing.T) { func TestHarvest_HarvestWorkspace_Bad_BinaryRejected(t *testing.T) { _, wsDir := initTestRepo(t) - srcDir := core.JoinPath(wsDir, "src") + repoDir := core.JoinPath(wsDir, "repo") // Add binary - fs.Write(core.JoinPath(srcDir, "build.so"), "elf") - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "add binary") + fs.Write(core.JoinPath(repoDir, "build.so"), "elf") + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "add binary") writeStatus(t, wsDir, "completed", "test-repo", "agent/test-task") @@ -201,7 +201,7 @@ func TestHarvest_HarvestCompleted_Good_ChannelEvents(t *testing.T) { // Create a Core with process + IPC handler to capture HarvestComplete messages var captured []messages.HarvestComplete - c := core.New(core.WithService(process.Register)) + c := core.New(core.WithService(agentic.ProcessRegister)) c.ServiceStartup(context.Background(), nil) c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if ev, ok := msg.(messages.HarvestComplete); ok { @@ -251,4 +251,3 @@ func TestHarvest_UpdateStatus_Good_WithQuestion(t *testing.T) { assert.Equal(t, "rejected", st["status"]) assert.Equal(t, "binary file: app.exe", st["question"]) } - diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index 44516c8..8700b70 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -18,7 +18,7 @@ import ( "dappco.re/go/agent/pkg/agentic" "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" - coremcp "dappco.re/go/mcp/pkg/mcp" + coremcp "forge.lthn.ai/core/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -28,6 +28,10 @@ import ( // if text, ok := resultString(r); ok { json.Unmarshal([]byte(text), &st) } var fs = agentic.LocalFs() +type channelSender interface { + ChannelSend(ctx context.Context, channel string, data any) +} + // workspaceStatusPaths returns all status.json files across both old and new workspace layouts. // Old: workspace/{name}/status.json (1 level) // New: workspace/{org}/{repo}/{identifier}/status.json (3 levels) @@ -54,6 +58,9 @@ func monitorHomeDir() string { if d := core.Env("CORE_HOME"); d != "" { return d } + if d := core.Env("HOME"); d != "" { + return d + } return core.Env("DIR_HOME") } @@ -200,7 +207,7 @@ func (m *Subsystem) Start(ctx context.Context) { monCtx, cancel := context.WithCancel(ctx) m.cancel = cancel - core.Info( "monitor: started (interval=%s)", m.interval) + core.Info("monitor: started (interval=%s)", m.interval) m.wg.Add(1) go func() { @@ -240,7 +247,6 @@ func (m *Subsystem) Poke() { } } - // checkIdleAfterDelay waits briefly then checks if the fleet is genuinely idle. // Only emits queue.drained when there are truly zero running or queued agents, // verified by checking PIDs are alive, not just trusting status files. @@ -447,26 +453,13 @@ func (m *Subsystem) checkCompletions() string { // checkInbox checks for unread messages. func (m *Subsystem) checkInbox() string { - apiKeyStr := core.Env("CORE_BRAIN_KEY") + apiKeyStr := monitorBrainKey() if apiKeyStr == "" { - home := core.Env("DIR_HOME") - keyFile := brainKeyPath(home) - r := fs.Read(keyFile) - if !r.OK { - return "" - } - value, ok := resultString(r) - if !ok { - return "" - } - apiKeyStr = value + return "" } // Call the API to check inbox - apiURL := core.Env("CORE_API_URL") - if apiURL == "" { - apiURL = "https://api.lthn.sh" - } + apiURL := monitorAPIURL() inboxURL := core.Concat(apiURL, "/v1/messages/inbox?agent=", core.Replace(agentic.AgentName(), " ", "%20")) hr := agentic.HTTPGet(context.Background(), inboxURL, core.Trim(apiKeyStr), "Bearer") if !hr.OK { @@ -539,15 +532,14 @@ func (m *Subsystem) checkInbox() string { // Push each message as a channel event so it lands in the session if m.ServiceRuntime != nil { - for _, msg := range newMessages { - m.Core().ACTION(coremcp.ChannelPush{ - Channel: "inbox.message", - Data: map[string]any{ + if notifier, ok := core.ServiceFor[channelSender](m.Core(), "mcp"); ok { + for _, msg := range newMessages { + notifier.ChannelSend(context.Background(), "inbox.message", map[string]any{ "from": msg.From, "subject": msg.Subject, "content": msg.Content, - }, - }) + }) + } } } diff --git a/pkg/monitor/monitor_test.go b/pkg/monitor/monitor_test.go index 54b9962..ad29c4d 100644 --- a/pkg/monitor/monitor_test.go +++ b/pkg/monitor/monitor_test.go @@ -10,15 +10,27 @@ import ( "testing" "time" + "dappco.re/go/agent/pkg/agentic" "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" - coremcp "dappco.re/go/mcp/pkg/mcp" - "dappco.re/go/core/process" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type capturedChannelEvent struct { + Channel string + Data any +} + +type captureNotifier struct { + events []capturedChannelEvent +} + +func (n *captureNotifier) ChannelSend(_ context.Context, channel string, data any) { + n.events = append(n.events, capturedChannelEvent{Channel: channel, Data: data}) +} + // setupBrainKey creates a ~/.claude/brain.key file for API auth tests. func setupBrainKey(t *testing.T, key string) { t.Helper() @@ -260,15 +272,10 @@ func TestMonitor_CheckInbox_Good_UnreadMessages(t *testing.T) { t.Setenv("CORE_API_URL", srv.URL) t.Setenv("AGENT_NAME", "test-agent") - // Create Core with IPC handler to capture ChannelPush - var captured []coremcp.ChannelPush + // Create Core with an MCP notifier to capture channel events. c := core.New() - c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { - if ev, ok := msg.(coremcp.ChannelPush); ok { - captured = append(captured, ev) - } - return core.Result{OK: true} - }) + notifier := &captureNotifier{} + require.True(t, c.RegisterService("mcp", notifier).OK) mon := New() mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{}) @@ -277,10 +284,10 @@ func TestMonitor_CheckInbox_Good_UnreadMessages(t *testing.T) { msg := mon.checkInbox() assert.Contains(t, msg, "2 unread message(s) in inbox") - // 3 new messages (ids 1,2,3 all > prevMaxID=0), but only 2 unread - // Monitor sends one ChannelPush per NEW message (higher ID than last seen) - require.Len(t, captured, 3) - data := captured[0].Data.(map[string]any) + // 3 new messages (ids 1,2,3 all > prevMaxID=0), but only 2 unread. + // Monitor sends one channel notification per new message. + require.Len(t, notifier.events, 3) + data := notifier.events[0].Data.(map[string]any) assert.Equal(t, "clotho", data["from"]) } @@ -376,15 +383,10 @@ func TestMonitor_CheckInbox_Good_MultipleSameSender(t *testing.T) { setupAPIEnv(t, srv.URL) - // Create Core with IPC handler to capture ChannelPush - var captured []coremcp.ChannelPush + // Create Core with an MCP notifier to capture channel events. c := core.New() - c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { - if ev, ok := msg.(coremcp.ChannelPush); ok { - captured = append(captured, ev) - } - return core.Result{OK: true} - }) + notifier := &captureNotifier{} + require.True(t, c.RegisterService("mcp", notifier).OK) mon := New() mon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{}) @@ -393,7 +395,7 @@ func TestMonitor_CheckInbox_Good_MultipleSameSender(t *testing.T) { msg := mon.checkInbox() assert.Contains(t, msg, "3 unread message(s)") - require.True(t, len(captured) > 0, "should capture at least one ChannelPush") + require.True(t, len(notifier.events) > 0, "should capture at least one channel event") } // --- check (integration of sub-checks) --- @@ -808,19 +810,19 @@ func TestMonitor_HarvestCompleted_Good_MultipleWorkspaces(t *testing.T) { run(t, sourceDir, "git", "commit", "-m", "init") fs.EnsureDir(wsDir) - run(t, wsDir, "git", "clone", sourceDir, "src") - srcDir := core.JoinPath(wsDir, "src") - run(t, srcDir, "git", "checkout", "-b", "agent/test-task") - fs.Write(core.JoinPath(srcDir, "new.go"), "package main\n") - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "agent work") + run(t, wsDir, "git", "clone", sourceDir, "repo") + repoDir := core.JoinPath(wsDir, "repo") + run(t, repoDir, "git", "checkout", "-b", "agent/test-task") + fs.Write(core.JoinPath(repoDir, "new.go"), "package main\n") + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "agent work") writeStatus(t, wsDir, "completed", fmt.Sprintf("repo-%d", i), "agent/test-task") } // Create Core with IPC handler to capture HarvestComplete messages var harvests []messages.HarvestComplete - c := core.New(core.WithService(process.Register)) + c := core.New(core.WithService(agentic.ProcessRegister)) c.ServiceStartup(context.Background(), nil) c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if ev, ok := msg.(messages.HarvestComplete); ok { @@ -865,23 +867,23 @@ func TestMonitor_HarvestCompleted_Good_RejectedWorkspace(t *testing.T) { wsDir := core.JoinPath(wsRoot, "workspace", "ws-rej") fs.EnsureDir(wsDir) - run(t, wsDir, "git", "clone", sourceDir, "src") - srcDir := core.JoinPath(wsDir, "src") - run(t, srcDir, "git", "checkout", "-b", "agent/test-task") - fs.Write(core.JoinPath(srcDir, "new.go"), "package main\n") - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "agent work") + run(t, wsDir, "git", "clone", sourceDir, "repo") + repoDir := core.JoinPath(wsDir, "repo") + run(t, repoDir, "git", "checkout", "-b", "agent/test-task") + fs.Write(core.JoinPath(repoDir, "new.go"), "package main\n") + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "agent work") // Add binary to trigger rejection - fs.Write(core.JoinPath(srcDir, "app.exe"), "binary") - run(t, srcDir, "git", "add", ".") - run(t, srcDir, "git", "commit", "-m", "add binary") + fs.Write(core.JoinPath(repoDir, "app.exe"), "binary") + run(t, repoDir, "git", "add", ".") + run(t, repoDir, "git", "commit", "-m", "add binary") writeStatus(t, wsDir, "completed", "rej-repo", "agent/test-task") // Create Core with IPC handler to capture HarvestRejected messages var rejections []messages.HarvestRejected - c := core.New(core.WithService(process.Register)) + c := core.New(core.WithService(agentic.ProcessRegister)) c.ServiceStartup(context.Background(), nil) c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if ev, ok := msg.(messages.HarvestRejected); ok { diff --git a/pkg/monitor/monitor_testmain_test.go b/pkg/monitor/monitor_testmain_test.go index 45ad1a6..602c6de 100644 --- a/pkg/monitor/monitor_testmain_test.go +++ b/pkg/monitor/monitor_testmain_test.go @@ -7,14 +7,14 @@ import ( "os" "testing" + "dappco.re/go/agent/pkg/agentic" core "dappco.re/go/core" - "dappco.re/go/core/process" ) var testMon *Subsystem func TestMain(m *testing.M) { - c := core.New(core.WithService(process.Register)) + c := core.New(core.WithService(agentic.ProcessRegister)) c.ServiceStartup(context.Background(), nil) testMon = New() testMon.ServiceRuntime = core.NewServiceRuntime(c, MonitorOptions{}) diff --git a/pkg/runner/queue.go b/pkg/runner/queue.go index ed16f3f..56db845 100644 --- a/pkg/runner/queue.go +++ b/pkg/runner/queue.go @@ -4,6 +4,7 @@ package runner import ( "strconv" + "syscall" "time" core "dappco.re/go/core" @@ -65,10 +66,10 @@ func (c *ConcurrencyLimit) UnmarshalYAML(value *yaml.Node) error { // AgentsConfig is the root of agents.yaml. type AgentsConfig struct { - Version int `yaml:"version"` - Dispatch DispatchConfig `yaml:"dispatch"` - Concurrency map[string]ConcurrencyLimit `yaml:"concurrency"` - Rates map[string]RateConfig `yaml:"rates"` + Version int `yaml:"version"` + Dispatch DispatchConfig `yaml:"dispatch"` + Concurrency map[string]ConcurrencyLimit `yaml:"concurrency"` + Rates map[string]RateConfig `yaml:"rates"` } // loadAgentsConfig reads agents.yaml from known paths. @@ -147,7 +148,13 @@ func (s *Service) canDispatchAgent(agent string) (bool, string) { func (s *Service) countRunningByAgent(agent string) int { count := 0 s.workspaces.Each(func(_ string, st *WorkspaceStatus) { - if st.Status == "running" && baseAgent(st.Agent) == agent { + if st.Status != "running" || baseAgent(st.Agent) != agent { + return + } + switch { + case st.PID < 0: + count++ + case st.PID > 0 && syscall.Kill(st.PID, 0) == nil: count++ } }) @@ -158,7 +165,13 @@ func (s *Service) countRunningByAgent(agent string) int { func (s *Service) countRunningByModel(agent string) int { count := 0 s.workspaces.Each(func(_ string, st *WorkspaceStatus) { - if st.Status == "running" && st.Agent == agent { + if st.Status != "running" || st.Agent != agent { + return + } + switch { + case st.PID < 0: + count++ + case st.PID > 0 && syscall.Kill(st.PID, 0) == nil: count++ } }) diff --git a/pkg/runner/queue_test.go b/pkg/runner/queue_test.go index c7f7f6c..e5ed74d 100644 --- a/pkg/runner/queue_test.go +++ b/pkg/runner/queue_test.go @@ -97,12 +97,14 @@ func TestQueue_ModelVariant_Ugly_Empty(t *testing.T) { func TestQueue_CanDispatchAgent_Good_NoConfig(t *testing.T) { svc := New() - assert.True(t, svc.canDispatchAgent("codex"), "no config → unlimited") + can, _ := svc.canDispatchAgent("codex") + assert.True(t, can, "no config → unlimited") } func TestQueue_CanDispatchAgent_Good_UnknownAgent(t *testing.T) { svc := New() - assert.True(t, svc.canDispatchAgent("unknown-agent")) + can, _ := svc.canDispatchAgent("unknown-agent") + assert.True(t, can) } func TestQueue_CanDispatchAgent_Bad_AtLimit(t *testing.T) { @@ -114,13 +116,15 @@ func TestQueue_CanDispatchAgent_Bad_AtLimit(t *testing.T) { }) } // Since PIDs aren't alive, count should be 0 - assert.True(t, svc.canDispatchAgent("codex"), "dead PIDs don't count") + can, _ := svc.canDispatchAgent("codex") + assert.True(t, can, "dead PIDs don't count") } func TestQueue_CanDispatchAgent_Ugly_ZeroLimit(t *testing.T) { svc := New() // Zero total means unlimited - assert.True(t, svc.canDispatchAgent("codex")) + can, _ := svc.canDispatchAgent("codex") + assert.True(t, can) } // --- countRunningByAgent --- diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 2e19687..97ce45f 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -15,7 +15,6 @@ import ( "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" - coremcp "dappco.re/go/mcp/pkg/mcp" ) // Options configures the runner service. @@ -38,6 +37,10 @@ type Service struct { workspaces *core.Registry[*WorkspaceStatus] } +type channelSender interface { + ChannelSend(ctx context.Context, channel string, data any) +} + // New creates a runner service. // // svc := runner.New() @@ -140,26 +143,35 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { } } } - c.ACTION(coremcp.ChannelPush{ - Channel: "agent.status", - Data: &AgentNotification{ - Status: "started", - Repo: ev.Repo, - Agent: ev.Agent, - Workspace: ev.Workspace, - Running: running, - Limit: limit, - }, - }) + notification := &AgentNotification{ + Status: "started", + Repo: ev.Repo, + Agent: ev.Agent, + Workspace: ev.Workspace, + Running: running, + Limit: limit, + } + if notifier, ok := core.ServiceFor[channelSender](c, "mcp"); ok { + notifier.ChannelSend(context.Background(), "agent.status", notification) + } case messages.AgentCompleted: // Update workspace status in Registry so concurrency count drops - s.workspaces.Each(func(name string, st *WorkspaceStatus) { - if st.Repo == ev.Repo && st.Status == "running" { - st.Status = ev.Status - st.PID = 0 + if ev.Workspace != "" { + if r := s.workspaces.Get(ev.Workspace); r.OK { + if st, ok := r.Value.(*WorkspaceStatus); ok && st.Status == "running" { + st.Status = ev.Status + st.PID = 0 + } } - }) + } else { + s.workspaces.Each(func(_ string, st *WorkspaceStatus) { + if st.Repo == ev.Repo && st.Status == "running" { + st.Status = ev.Status + st.PID = 0 + } + }) + } cBase := baseAgent(ev.Agent) cRunning := s.countRunningByAgent(cBase) var cLimit int @@ -171,17 +183,17 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { } } } - c.ACTION(coremcp.ChannelPush{ - Channel: "agent.status", - Data: &AgentNotification{ - Status: ev.Status, - Repo: ev.Repo, - Agent: ev.Agent, - Workspace: ev.Workspace, - Running: cRunning, - Limit: cLimit, - }, - }) + notification := &AgentNotification{ + Status: ev.Status, + Repo: ev.Repo, + Agent: ev.Agent, + Workspace: ev.Workspace, + Running: cRunning, + Limit: cLimit, + } + if notifier, ok := core.ServiceFor[channelSender](c, "mcp"); ok { + notifier.ChannelSend(context.Background(), "agent.status", notification) + } s.Poke() case messages.PokeQueue: diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 5691cc1..cd8e748 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -6,6 +6,7 @@ import ( "context" "testing" + "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -304,6 +305,30 @@ func TestRunner_HandleIPCEvents_Good_UnknownMessage(t *testing.T) { assert.True(t, r.OK) } +func TestRunner_HandleIPCEvents_Good_UpdatesMatchingWorkspaceOnly(t *testing.T) { + c := core.New(core.WithOption("name", "test")) + svc := New() + svc.ServiceRuntime = core.NewServiceRuntime(c, Options{}) + svc.TrackWorkspace("core/go-io/task-1", &WorkspaceStatus{ + Status: "running", Agent: "codex", Repo: "go-io", PID: 111, + }) + svc.TrackWorkspace("core/go-io/task-2", &WorkspaceStatus{ + Status: "running", Agent: "codex", Repo: "go-io", PID: 222, + }) + + r := svc.HandleIPCEvents(c, messages.AgentCompleted{ + Agent: "codex", Repo: "go-io", Workspace: "core/go-io/task-1", Status: "completed", + }) + assert.True(t, r.OK) + + first := svc.workspaces.Get("core/go-io/task-1").Value.(*WorkspaceStatus) + second := svc.workspaces.Get("core/go-io/task-2").Value.(*WorkspaceStatus) + assert.Equal(t, "completed", first.Status) + assert.Equal(t, 0, first.PID) + assert.Equal(t, "running", second.Status) + assert.Equal(t, 222, second.PID) +} + // --- WriteStatus / ReadStatus --- func TestRunner_WriteReadStatus_Good(t *testing.T) {