docs: remove implemented plan/spec files
Some checks failed
Security Scan / security (push) Successful in 9s
Test / test (push) Failing after 56s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-14 12:45:54 +00:00
parent b9500bf866
commit d828c6356a
10 changed files with 0 additions and 4529 deletions

View file

@ -1,788 +0,0 @@
# core/api Polyglot Merge — Implementation Plan
**Date:** 2026-03-14
**Design:** [2026-03-14-api-polyglot-merge-design.md](../specs/2026-03-14-api-polyglot-merge-design.md)
**Pattern:** Follows core/mcp merge (2026-03-09)
**Co-author:** Co-Authored-By: Virgil <virgil@lethean.io>
## Source Inventory
### Go side (`core/go-api`)
Module: `forge.lthn.ai/core/go-api` (Go 1.26)
Root-level `.go` files (50 files):
- Core: `api.go`, `group.go`, `options.go`, `response.go`, `middleware.go`
- Features: `authentik.go`, `bridge.go`, `brotli.go`, `cache.go`, `codegen.go`, `export.go`, `graphql.go`, `i18n.go`, `openapi.go`, `sse.go`, `swagger.go`, `tracing.go`, `websocket.go`
- Tests: `*_test.go` (26 files including `race_test.go`, `norace_test.go`)
- Subdirs: `cmd/api/` (CLI commands), `docs/` (4 markdown files), `.core/` (build.yaml, release.yaml)
Key dependencies: `forge.lthn.ai/core/cli`, `github.com/gin-gonic/gin`, OpenTelemetry, Casbin, OIDC
### PHP side (`core/php-api`)
Package: `lthn/php-api` (PHP 8.2+, requires `lthn/php`)
Three namespace roots:
- `Core\Api\` (`src/Api/`) — Boot, Controllers, Middleware, Models, Services, Documentation, RateLimit, Tests
- `Core\Front\Api\` (`src/Front/Api/`) — API versioning frontage, auto-discovered provider
- `Core\Website\Api\` (`src/Website/Api/`) — Documentation UI, Blade views, web routes
### Consumer repos (Go)
| Repo | Dependency | Go source imports | Notes |
|------|-----------|-------------------|-------|
| `core/go-ml` | direct | `api/routes.go`, `api/routes_test.go` | Aliased as `goapi` |
| `core/mcp` | direct | `pkg/mcp/bridge.go`, `pkg/mcp/bridge_test.go` | Aliased as `api` |
| `core/go-ai` | direct (go.mod only) | None | go.mod + go.sum + docs reference |
| `core/ide` | indirect | None | Transitive via go-ai or mcp |
| `core/agent` | indirect (v0.0.3) | None | Transitive |
### Consumer repos (PHP)
| Repo / App | Dependency | Notes |
|------------|-----------|-------|
| `host.uk.com` (Laravel app) | `core/php-api: *` | composer.json + repositories block |
| `core/php-template` | `lthn/php-api: dev-main` | Starter template |
---
## Task 1 — Clone core/api and copy Go files from go-api
Clone the empty target repo and copy all Go source files at root level (Option B from the design spec — no `pkg/api/` nesting).
```bash
cd ~/Code/core
git clone ssh://git@forge.lthn.ai:2223/core/api.git
cd api
# Copy all Go source files (root level)
cp ~/Code/core/go-api/*.go .
# Copy cmd/ directory
cp -r ~/Code/core/go-api/cmd .
# Copy docs/
cp -r ~/Code/core/go-api/docs .
# Copy .core/ build config
mkdir -p .core
cp ~/Code/core/go-api/.core/build.yaml .core/build.yaml
cp ~/Code/core/go-api/.core/release.yaml .core/release.yaml
# Copy go.sum as starting point
cp ~/Code/core/go-api/go.sum .
```
Update `.core/build.yaml`:
```yaml
version: 1
project:
name: core-api
description: REST API framework (Go + PHP)
binary: ""
build:
cgo: false
flags:
- -trimpath
ldflags:
- -s
- -w
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: darwin
arch: arm64
- os: windows
arch: amd64
```
Update `.core/release.yaml`:
```yaml
version: 1
project:
name: core-api
repository: core/api
publishers: []
changelog:
include:
- feat
- fix
- perf
- refactor
exclude:
- chore
- docs
- style
- test
- ci
```
### Verification
```bash
ls *.go | wc -l # Should be 50 files
ls cmd/api/ # Should have cmd.go, cmd_sdk.go, cmd_spec.go, cmd_test.go
ls docs/ # architecture.md, development.md, history.md, index.md
```
---
## Task 2 — Copy PHP files from php-api into src/php/
```bash
cd ~/Code/core/api
# Create PHP directory structure
mkdir -p src/php/src
mkdir -p src/php/tests
# Copy PHP source namespaces
cp -r ~/Code/core/php-api/src/Api src/php/src/Api
cp -r ~/Code/core/php-api/src/Front src/php/src/Front
cp -r ~/Code/core/php-api/src/Website src/php/src/Website
# Copy PHP test infrastructure
cp -r ~/Code/core/php-api/tests/Feature src/php/tests/Feature 2>/dev/null || true
cp -r ~/Code/core/php-api/tests/Unit src/php/tests/Unit 2>/dev/null || true
cp ~/Code/core/php-api/tests/TestCase.php src/php/tests/TestCase.php 2>/dev/null || true
# Copy phpunit.xml (adjust paths for new location)
cp ~/Code/core/php-api/phpunit.xml src/php/phpunit.xml
```
Edit `src/php/phpunit.xml` — update the `<directory>` paths:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>
```
### Verification
```bash
ls src/php/src/ # Api, Front, Website
ls src/php/src/Api/ # Boot.php, Controllers/, Middleware/, Models/, etc.
ls src/php/src/Front/Api/ # Boot.php, ApiVersionService.php, Middleware/, etc.
ls src/php/src/Website/Api/ # Boot.php, Controllers/, Views/, etc.
```
---
## Task 3 — Create go.mod, composer.json, and .gitattributes
### go.mod
Create `go.mod` with the new module path. Start from the existing go.mod and change the module line:
```bash
cd ~/Code/core/api
sed 's|module forge.lthn.ai/core/go-api|module forge.lthn.ai/core/api|' \
~/Code/core/go-api/go.mod > go.mod
```
The first line should now read:
```
module forge.lthn.ai/core/api
```
Then tidy:
```bash
go mod tidy
```
### composer.json
Create `composer.json` following the core/mcp pattern:
```json
{
"name": "lthn/api",
"description": "REST API module — Laravel API layer + standalone Go binary",
"keywords": ["api", "rest", "laravel", "openapi"],
"license": "EUPL-1.2",
"require": {
"php": "^8.2",
"lthn/php": "*",
"symfony/yaml": "^7.0"
},
"autoload": {
"psr-4": {
"Core\\Api\\": "src/php/src/Api/",
"Core\\Front\\Api\\": "src/php/src/Front/Api/",
"Core\\Website\\Api\\": "src/php/src/Website/Api/"
}
},
"autoload-dev": {
"psr-4": {
"Core\\Api\\Tests\\": "src/php/tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Core\\Front\\Api\\Boot"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"replace": {
"core/php-api": "self.version",
"lthn/php-api": "self.version"
}
}
```
### .gitattributes
Create `.gitattributes` matching the core/mcp pattern — exclude Go files from Composer installs:
```
*.go export-ignore
go.mod export-ignore
go.sum export-ignore
cmd/ export-ignore
pkg/ export-ignore
.core/ export-ignore
src/php/tests/ export-ignore
```
### .gitignore
```
# Binaries
core-api
*.exe
# IDE
.idea/
.vscode/
# Go
vendor/
# PHP
/vendor/
node_modules/
```
### Verification
```bash
head -1 go.mod # module forge.lthn.ai/core/api
cat composer.json | python3 -m json.tool # Valid JSON
cat .gitattributes # 7 export-ignore lines
```
---
## Task 4 — Create CLAUDE.md for the polyglot repo
Create `CLAUDE.md` at the repo root:
```markdown
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Core API is the REST framework for the Lethean ecosystem, providing both a **Go HTTP engine** (Gin-based, with OpenAPI generation, WebSocket/SSE, ToolBridge) and a **PHP Laravel package** (rate limiting, webhooks, API key management, OpenAPI documentation). Both halves serve the same purpose in their respective stacks.
Module: `forge.lthn.ai/core/api` | Package: `lthn/api` | Licence: EUPL-1.2
## Build and Test Commands
### Go
```bash
core build # Build binary (if cmd/ has main)
go build ./... # Build library
core go test # Run all Go tests
core go test --run TestName # Run a single test
core go cov # Coverage report
core go cov --open # Open HTML coverage in browser
core go qa # Format + vet + lint + test
core go qa full # Also race detector, vuln scan, security audit
core go fmt # gofmt
core go lint # golangci-lint
core go vet # go vet
```
### PHP (from repo root)
```bash
composer test # Run all PHP tests (Pest)
composer test -- --filter=ApiKey # Single test
composer lint # Laravel Pint (PSR-12)
./vendor/bin/pint --dirty # Format changed files
```
Tests live in `src/php/src/Api/Tests/Feature/` (in-source) and `src/php/tests/` (standalone).
## Architecture
### Go Engine (root-level .go files)
`Engine` is the central type, configured via functional `Option` functions passed to `New()`:
```go
engine, _ := api.New(api.WithAddr(":8080"), api.WithCORS("*"), api.WithSwagger(...))
engine.Register(myRouteGroup)
engine.Serve(ctx)
```
**Extension interfaces** (`group.go`):
- `RouteGroup` — minimum: `Name()`, `BasePath()`, `RegisterRoutes(*gin.RouterGroup)`
- `StreamGroup` — optional: `Channels() []string` for WebSocket
- `DescribableGroup` — extends RouteGroup with `Describe() []RouteDescription` for OpenAPI
**ToolBridge** (`bridge.go`): Converts `ToolDescriptor` structs into `POST /{tool_name}` REST endpoints with auto-generated OpenAPI paths.
**Authentication** (`authentik.go`): Authentik OIDC integration + static bearer token. Permissive middleware with `RequireAuth()` / `RequireGroup()` guards.
**OpenAPI** (`openapi.go`, `export.go`, `codegen.go`): `SpecBuilder.Build()` generates OpenAPI 3.1 JSON. `SDKGenerator` wraps openapi-generator-cli for 11 languages.
**CLI** (`cmd/api/`): Registers `core api spec` and `core api sdk` commands.
### PHP Package (`src/php/`)
Three namespace roots:
| Namespace | Path | Role |
|-----------|------|------|
| `Core\Front\Api` | `src/php/src/Front/Api/` | API frontage — middleware, versioning, auto-discovered provider |
| `Core\Api` | `src/php/src/Api/` | Backend — auth, scopes, models, webhooks, OpenAPI docs |
| `Core\Website\Api` | `src/php/src/Website/Api/` | Documentation UI — controllers, Blade views, web routes |
Boot chain: `Front\Api\Boot` (auto-discovered) fires `ApiRoutesRegistering``Api\Boot` registers middleware and routes.
Key services: `WebhookService`, `RateLimitService`, `IpRestrictionService`, `OpenApiBuilder`, `ApiKeyService`.
## Conventions
- **UK English** in all user-facing strings and docs (colour, organisation, unauthorised)
- **SPDX headers** in Go files: `// SPDX-License-Identifier: EUPL-1.2`
- **`declare(strict_types=1);`** in every PHP file
- **Full type hints** on all PHP parameters and return types
- **Pest syntax** for PHP tests (not PHPUnit)
- **Flux Pro** components in Livewire views; **Font Awesome** icons
- **Conventional commits**: `type(scope): description`
- **Co-Author**: `Co-Authored-By: Virgil <virgil@lethean.io>`
- Go test names use `_Good` / `_Bad` / `_Ugly` suffixes
## Key Dependencies
| Go module | Role |
|-----------|------|
| `forge.lthn.ai/core/cli` | CLI command registration |
| `github.com/gin-gonic/gin` | HTTP router |
| `github.com/casbin/casbin/v2` | Authorisation policies |
| `github.com/coreos/go-oidc/v3` | OIDC / Authentik |
| `go.opentelemetry.io/otel` | OpenTelemetry tracing |
PHP: `lthn/php` (Core framework), Laravel 12, `symfony/yaml`.
Go workspace: this module is part of `~/Code/go.work`. Requires Go 1.26+, PHP 8.2+.
```
### Verification
```bash
wc -l CLAUDE.md # Should be ~100 lines
```
---
## Task 5 — Update consumer repos (Go import paths)
Change `forge.lthn.ai/core/go-api` to `forge.lthn.ai/core/api` in all consumers.
### 5a — core/go-ml (2 source files + go.mod)
```bash
cd ~/Code/core/go-ml
# Update Go source imports
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' \
api/routes.go api/routes_test.go
# Update go.mod
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod
go mod tidy
```
Verify the import alias in `api/routes.go` reads:
```go
goapi "forge.lthn.ai/core/api"
```
### 5b — core/mcp (2 source files + go.mod)
```bash
cd ~/Code/core/mcp
# Update Go source imports
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' \
pkg/mcp/bridge.go pkg/mcp/bridge_test.go
# Update go.mod
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod
go mod tidy
```
Verify the import alias in `pkg/mcp/bridge.go` reads:
```go
api "forge.lthn.ai/core/api"
```
### 5c — core/go-ai (go.mod only — no Go source imports)
```bash
cd ~/Code/core/go-ai
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod
go mod tidy
```
Also update the docs reference:
```bash
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' docs/index.md
```
### 5d — core/ide (indirect only — go.mod)
```bash
cd ~/Code/core/ide
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod
go mod tidy
```
### 5e — core/agent (indirect only — go.mod)
```bash
cd ~/Code/core/agent
sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod
go mod tidy
```
### 5f — Update CLAUDE.md references in core/mcp
In `~/Code/core/mcp/CLAUDE.md`, update the dependency table:
```
| `forge.lthn.ai/core/go-api` | REST framework + `ToolBridge` |
```
becomes:
```
| `forge.lthn.ai/core/api` | REST framework + `ToolBridge` |
```
### 5g — PHP consumers
**host.uk.com Laravel app** (`~/Code/lab/host.uk.com/composer.json`):
Replace the `core/php-api` requirement and repository entry:
- Change `"core/php-api": "*"` to `"lthn/api": "dev-main"`
- Change repository URL from `ssh://git@forge.lthn.ai:2223/core/php-api.git` to `ssh://git@forge.lthn.ai:2223/core/api.git`
The `replace` block in the new `composer.json` (`"core/php-api": "self.version"`, `"lthn/php-api": "self.version"`) ensures backward compatibility.
**core/php-template** (`~/Code/core/php-template/composer.json`):
```bash
cd ~/Code/core/php-template
sed -i '' 's|"lthn/php-api": "dev-main"|"lthn/api": "dev-main"|g' composer.json
```
### Verification
```bash
# Confirm no stale references remain
grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/go-ml/ --include='*.go' --include='go.mod'
grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/mcp/ --include='*.go' --include='go.mod'
grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/go-ai/ --include='*.go' --include='go.mod'
grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/ide/ --include='*.go' --include='go.mod'
grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/agent/ --include='*.go' --include='go.mod'
# All should return nothing
```
---
## Task 6 — Update go.work
Edit `~/Code/go.work`:
```bash
cd ~/Code
# Remove go-api entry, add api entry
sed -i '' 's|./core/go-api|./core/api|' go.work
# Sync workspace
go work sync
```
### Verification
```bash
grep 'core/api' ~/Code/go.work # Should show ./core/api
grep 'core/go-api' ~/Code/go.work # Should return nothing
```
---
## Task 7 — Build verification across all affected repos
Run in sequence — each depends on the workspace being consistent:
```bash
cd ~/Code
# 1. Verify core/api itself builds and tests pass
cd ~/Code/core/api
go build ./...
go test ./...
go vet ./...
# 2. Verify core/go-ml
cd ~/Code/core/go-ml
go build ./...
go test ./...
# 3. Verify core/mcp
cd ~/Code/core/mcp
go build ./...
go test ./...
# 4. Verify core/go-ai
cd ~/Code/core/go-ai
go build ./...
# 5. Verify core/ide
cd ~/Code/core/ide
go build ./...
# 6. Verify core/agent
cd ~/Code/core/agent
go build ./...
```
If any fail, fix import paths and re-run `go mod tidy` in the affected repo.
### Verification
All six builds should succeed with zero errors. Test failures unrelated to the import path change can be ignored (pre-existing).
---
## Task 8 — Archive go-api and php-api on Forge
Archive both source repos on Forge. This marks them read-only but keeps them accessible for existing consumers that haven't updated.
```bash
# Archive via Forgejo API (requires GITEA_TOKEN or gh-equivalent)
# Option A: Forgejo web UI
# 1. Navigate to https://forge.lthn.ai/core/go-api/settings
# 2. Click "Archive this repository"
# 3. Repeat for https://forge.lthn.ai/core/php-api/settings
# Option B: Forgejo API
curl -X PATCH \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"archived": true}' \
"https://forge.lthn.ai/api/v1/repos/core/go-api"
curl -X PATCH \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"archived": true}' \
"https://forge.lthn.ai/api/v1/repos/core/php-api"
```
Update repo descriptions to point to the merged repo:
```bash
curl -X PATCH \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"description": "ARCHIVED — merged into core/api"}' \
"https://forge.lthn.ai/api/v1/repos/core/go-api"
curl -X PATCH \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"description": "ARCHIVED — merged into core/api"}' \
"https://forge.lthn.ai/api/v1/repos/core/php-api"
```
### Local cleanup
```bash
# Optionally remove the old local clones (or keep for reference)
# rm -rf ~/Code/core/go-api ~/Code/core/php-api
```
---
## Commit Strategy
Initial commit in core/api:
```bash
cd ~/Code/core/api
git add -A
git commit -m "$(cat <<'EOF'
feat(api): merge go-api + php-api into polyglot repo
Go source at root level (Option B), PHP under src/php/.
Module path: forge.lthn.ai/core/api
Package name: lthn/api
Co-Authored-By: Virgil <virgil@lethean.io>
EOF
)"
git push origin main
```
Consumer updates (one commit per repo):
```bash
# core/go-ml
cd ~/Code/core/go-ml
git add -A
git commit -m "$(cat <<'EOF'
refactor(api): update import path from go-api to core/api
Part of the polyglot merge — forge.lthn.ai/core/go-api is now
forge.lthn.ai/core/api.
Co-Authored-By: Virgil <virgil@lethean.io>
EOF
)"
# core/mcp
cd ~/Code/core/mcp
git add -A
git commit -m "$(cat <<'EOF'
refactor(api): update import path from go-api to core/api
Part of the polyglot merge — forge.lthn.ai/core/go-api is now
forge.lthn.ai/core/api.
Co-Authored-By: Virgil <virgil@lethean.io>
EOF
)"
# core/go-ai
cd ~/Code/core/go-ai
git add -A
git commit -m "$(cat <<'EOF'
refactor(api): update import path from go-api to core/api
Part of the polyglot merge — forge.lthn.ai/core/go-api is now
forge.lthn.ai/core/api.
Co-Authored-By: Virgil <virgil@lethean.io>
EOF
)"
# core/ide
cd ~/Code/core/ide
git add -A
git commit -m "$(cat <<'EOF'
refactor(api): update import path from go-api to core/api
Part of the polyglot merge — forge.lthn.ai/core/go-api is now
forge.lthn.ai/core/api.
Co-Authored-By: Virgil <virgil@lethean.io>
EOF
)"
# core/agent
cd ~/Code/core/agent
git add -A
git commit -m "$(cat <<'EOF'
refactor(api): update import path from go-api to core/api
Part of the polyglot merge — forge.lthn.ai/core/go-api is now
forge.lthn.ai/core/api.
Co-Authored-By: Virgil <virgil@lethean.io>
EOF
)"
```
---
## Summary
| # | Task | Files touched | Risk |
|---|------|--------------|------|
| 1 | Clone core/api, copy Go files | 50 .go + cmd/ + docs/ + .core/ | Low |
| 2 | Copy PHP files into src/php/ | ~80 PHP files | Low |
| 3 | Create go.mod, composer.json, .gitattributes | 4 new files | Low |
| 4 | Create CLAUDE.md | 1 new file | Low |
| 5 | Update 5 Go consumers + 2 PHP consumers | ~12 files across 7 repos | Medium |
| 6 | Update go.work | 1 file | Low |
| 7 | Build verification | 0 files (validation only) | N/A |
| 8 | Archive go-api + php-api | 0 local files (Forge API) | Low |
Estimated effort: 3045 minutes of execution time.

View file

@ -1,452 +0,0 @@
# core/ide Modernisation — Implementation Plan
**Date:** 2026-03-14
**Spec:** [2026-03-14-ide-modernisation-design.md](../specs/2026-03-14-ide-modernisation-design.md)
---
## Task 1 — Delete the 7 obsolete files
Remove all hand-rolled services that are now provided by ecosystem packages.
```bash
cd /Users/snider/Code/core/ide
rm mcp_bridge.go # Replaced by core/mcp Service + transports
rm webview_svc.go # Replaced by core/gui/pkg/display + webview
rm brain_mcp.go # Replaced by core/mcp/pkg/mcp/brain
rm claude_bridge.go # Replaced by core/mcp/pkg/mcp/ide Bridge
rm headless_mcp.go # Not needed — core/mcp runs in-process
rm headless.go # config gui.enabled flag; jobrunner belongs in core/agent
rm greetservice.go # Scaffold placeholder
```
**Verification:** `ls *.go` should show only `main.go`.
---
## Task 2 — Rewrite main.go
Replace the current main.go with a thin Wails shell that wires `core.Core` with
ecosystem services. The full file contents:
```go
package main
import (
"context"
"embed"
"io/fs"
"log"
"os"
"os/signal"
"runtime"
"syscall"
"forge.lthn.ai/core/config"
"forge.lthn.ai/core/go-ws"
"forge.lthn.ai/core/go/pkg/core"
guiMCP "forge.lthn.ai/core/gui/pkg/mcp"
"forge.lthn.ai/core/gui/pkg/display"
"forge.lthn.ai/core/ide/icons"
"forge.lthn.ai/core/mcp/pkg/mcp"
"forge.lthn.ai/core/mcp/pkg/mcp/brain"
"forge.lthn.ai/core/mcp/pkg/mcp/ide"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed all:frontend/dist/wails-angular-template/browser
var assets embed.FS
func main() {
// ── Flags ──────────────────────────────────────────────────
mcpOnly := false
for _, arg := range os.Args[1:] {
if arg == "--mcp" {
mcpOnly = true
}
}
// ── Configuration ──────────────────────────────────────────
cfg, _ := config.New()
cwd, err := os.Getwd()
if err != nil {
log.Fatalf("failed to get working directory: %v", err)
}
// ── Shared resources (built before Core) ───────────────────
hub := ws.NewHub()
bridgeCfg := ide.DefaultConfig()
bridgeCfg.WorkspaceRoot = cwd
if url := os.Getenv("CORE_API_URL"); url != "" {
bridgeCfg.LaravelWSURL = url
}
if token := os.Getenv("CORE_API_TOKEN"); token != "" {
bridgeCfg.Token = token
}
bridge := ide.NewBridge(hub, bridgeCfg)
// ── Core framework ─────────────────────────────────────────
c, err := core.New(
core.WithName("ws", func(c *core.Core) (any, error) {
return hub, nil
}),
core.WithService(display.Register(nil)), // nil platform until Wails starts
core.WithName("mcp", func(c *core.Core) (any, error) {
return mcp.New(
mcp.WithWorkspaceRoot(cwd),
mcp.WithWSHub(hub),
mcp.WithSubsystem(brain.New(bridge)),
mcp.WithSubsystem(guiMCP.New(c)),
)
}),
)
if err != nil {
log.Fatalf("failed to create core: %v", err)
}
// Retrieve the MCP service for transport control
mcpSvc, err := core.ServiceFor[*mcp.Service](c, "mcp")
if err != nil {
log.Fatalf("failed to get MCP service: %v", err)
}
// ── Mode selection ─────────────────────────────────────────
if mcpOnly {
// stdio mode — Claude Code connects via --mcp flag
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
// Start Core lifecycle manually
if err := c.ServiceStartup(ctx, nil); err != nil {
log.Fatalf("core startup failed: %v", err)
}
bridge.Start(ctx)
if err := mcpSvc.ServeStdio(ctx); err != nil {
log.Printf("MCP stdio error: %v", err)
}
_ = mcpSvc.Shutdown(ctx)
_ = c.ServiceShutdown(ctx)
return
}
if !guiEnabled(cfg) {
// No GUI — run Core with MCP transport in background
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
if err := c.ServiceStartup(ctx, nil); err != nil {
log.Fatalf("core startup failed: %v", err)
}
bridge.Start(ctx)
go func() {
if err := mcpSvc.Run(ctx); err != nil {
log.Printf("MCP error: %v", err)
}
}()
<-ctx.Done()
shutdownCtx := context.Background()
_ = mcpSvc.Shutdown(shutdownCtx)
_ = c.ServiceShutdown(shutdownCtx)
return
}
// ── GUI mode ───────────────────────────────────────────────
staticAssets, err := fs.Sub(assets, "frontend/dist/wails-angular-template/browser")
if err != nil {
log.Fatal(err)
}
app := application.New(application.Options{
Name: "Core IDE",
Description: "Host UK Core IDE - Development Environment",
Services: []application.Service{
application.NewService(c),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(staticAssets),
},
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
},
OnShutdown: func() {
ctx := context.Background()
_ = mcpSvc.Shutdown(ctx)
bridge.Shutdown()
},
})
// System tray
systray := app.SystemTray.New()
systray.SetTooltip("Core IDE")
if runtime.GOOS == "darwin" {
systray.SetTemplateIcon(icons.AppTray)
} else {
systray.SetDarkModeIcon(icons.AppTray)
systray.SetIcon(icons.AppTray)
}
// Tray panel window
trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "tray-panel",
Title: "Core IDE",
Width: 380,
Height: 480,
URL: "/tray",
Hidden: true,
Frameless: true,
BackgroundColour: application.NewRGB(26, 27, 38),
})
systray.AttachWindow(trayWindow).WindowOffset(5)
// Tray menu
trayMenu := app.Menu.New()
trayMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systray.SetMenu(trayMenu)
// Start MCP transport alongside Wails
go func() {
ctx := context.Background()
bridge.Start(ctx)
if err := mcpSvc.Run(ctx); err != nil {
log.Printf("MCP error: %v", err)
}
}()
log.Println("Starting Core IDE...")
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
// guiEnabled checks whether the GUI should start.
// Returns false if config says gui.enabled: false, or if no display is available.
func guiEnabled(cfg *config.Config) bool {
if cfg != nil {
var guiCfg struct {
Enabled *bool `mapstructure:"enabled"`
}
if err := cfg.Get("gui", &guiCfg); err == nil && guiCfg.Enabled != nil {
return *guiCfg.Enabled
}
}
// Fall back to display detection
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
return true
}
return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
}
```
### Key changes from the old main.go
1. **Three modes**: `--mcp` (stdio for Claude Code), no-GUI (headless with MCP_ADDR), GUI (Wails systray).
2. **`core.Core` is the Wails service** — its `ServiceStartup`/`ServiceShutdown` drive all sub-service lifecycles.
3. **Ecosystem wiring**: `display.Register(nil)` for GUI tools, `brain.New(bridge)` for OpenBrain, `guiMCP.New(c)` for 74 GUI MCP tools, `mcp.WithWSHub(hub)` for WS streaming.
4. **No hand-rolled HTTP server**`mcp.Service` owns the MCP protocol (stdio/TCP/Unix). The WS hub is injected via option.
5. **IDE bridge** uses `ide.DefaultConfig()` from `core/mcp/pkg/mcp/ide` with env var overrides.
---
## Task 3 — Update go.mod
### 3a. Replace go.mod with updated dependencies
```
module forge.lthn.ai/core/ide
go 1.26.0
require (
forge.lthn.ai/core/go v0.2.2
forge.lthn.ai/core/config v0.1.2
forge.lthn.ai/core/go-ws v0.1.3
forge.lthn.ai/core/gui v0.1.0
forge.lthn.ai/core/mcp v0.1.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.74
)
```
**Removed** (no longer directly imported):
- `forge.lthn.ai/core/agent` — brain tools now via `core/mcp/pkg/mcp/brain`
- `forge.lthn.ai/core/go-process` — daemon/PID logic was in headless.go
- `forge.lthn.ai/core/go-scm` — Forgejo client was in headless.go
- `github.com/gorilla/websocket` — replaced by `core/mcp/pkg/mcp/ide` (uses it internally)
**Added**:
- `forge.lthn.ai/core/gui` — display service + 74 MCP tools
- `forge.lthn.ai/core/mcp` — MCP server, brain subsystem, IDE bridge
### 3b. Sync workspace and tidy
```bash
cd /Users/snider/Code/core/ide
go mod tidy
cd /Users/snider/Code
go work sync
```
The `go mod tidy` will pull indirect dependencies and populate the `require`
block. The `go work sync` aligns versions across the workspace.
---
## Task 4 — Update CLAUDE.md
Replace the existing CLAUDE.md with content reflecting the new architecture:
```markdown
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build & Development Commands
\`\`\`bash
# Development (hot-reload GUI + Go rebuild)
wails3 dev
# Production build (preferred)
core build
# Frontend-only development
cd frontend && npm install && npm run dev
# Go tests
core go test # All tests
core go test --run TestName # Single test
core go cov # Coverage report
core go cov --open # Coverage in browser
# Quality assurance
core go qa # Format + vet + lint + test
core go qa full # + race detector, vuln scan, security audit
core go fmt # Format only
core go lint # Lint only
# Frontend tests
cd frontend && npm run test
\`\`\`
## Architecture
**Thin Wails shell** wiring ecosystem packages via `core.Core` dependency injection. Three operating modes:
### GUI Mode (default)
`main()` → Wails 3 application with embedded Angular frontend, system tray (macOS: accessory app, no Dock icon). Core framework manages all services:
- **display** (`core/gui`) — window management, webview automation, 74 MCP tools across 14 categories
- **MCP** (`core/mcp`) — Model Context Protocol server with file ops, brain subsystem, GUI subsystem
- **IDE bridge** (`core/mcp/pkg/mcp/ide`) — WebSocket bridge to Laravel core-agentic backend
- **WS hub** (`core/go-ws`) — WebSocket hub for Angular frontend communication
### MCP Mode (`--mcp`)
`core-ide --mcp` → stdio MCP server for Claude Code integration. No GUI, no HTTP. Configure in `.claude/.mcp.json`:
\`\`\`json
{
"mcpServers": {
"core-ide": {
"type": "stdio",
"command": "core-ide",
"args": ["--mcp"]
}
}
}
\`\`\`
### Headless Mode (no display or `gui.enabled: false`)
Core framework runs all services without Wails. MCP transport determined by `MCP_ADDR` env var (TCP if set, stdio otherwise).
### Frontend
Angular 20+ app embedded via `//go:embed`. Two routes: `/tray` (system tray panel, 380x480 frameless) and `/ide` (full IDE layout).
## Configuration
\`\`\`yaml
# .core/config.yaml
gui:
enabled: true # false = no Wails, Core still runs
mcp:
transport: stdio # stdio | tcp | unix
tcp:
port: 9877
brain:
api_url: http://localhost:8000
api_token: "" # or CORE_API_TOKEN env var
\`\`\`
## Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `CORE_API_URL` | `http://localhost:8000` | Laravel backend WebSocket URL |
| `CORE_API_TOKEN` | (empty) | Bearer token for Laravel backend auth |
| `MCP_ADDR` | (empty) | TCP address for MCP server (headless mode) |
## Workspace Dependencies
This module uses a Go workspace (`~/Code/go.work`) with `replace` directives for sibling modules:
- `../go``forge.lthn.ai/core/go`
- `../gui``forge.lthn.ai/core/gui`
- `../mcp``forge.lthn.ai/core/mcp`
- `../config``forge.lthn.ai/core/config`
- `../go-ws``forge.lthn.ai/core/go-ws`
## Conventions
- **UK English** in documentation and user-facing strings (colour, organisation, centre).
- **Conventional commits**: `type(scope): description` with co-author line `Co-Authored-By: Virgil <virgil@lethean.io>`.
- **Licence**: EUPL-1.2.
- All Go code is in `package main` (single-package application).
- Services are registered via `core.WithService` or `core.WithName` factory functions.
- MCP subsystems implement `mcp.Subsystem` interface from `core/mcp`.
```
---
## Task 5 — Build verification
```bash
cd /Users/snider/Code/core/ide
go build ./...
```
If the build fails, the likely causes are:
1. **Missing workspace entries** — ensure `go.work` includes `./core/mcp` and `./core/gui`:
```bash
cd /Users/snider/Code
go work use ./core/mcp ./core/gui
go work sync
```
2. **Stale go.sum** — clear and regenerate:
```bash
cd /Users/snider/Code/core/ide
rm go.sum
go mod tidy
```
3. **Version mismatches** — check that indirect dependency versions align:
```bash
go mod graph | grep -i conflict
```
Once `go build ./...` succeeds, run the quality check:
```bash
core go qa
```
This confirms formatting, vetting, linting, and tests all pass.

View file

@ -1,105 +0,0 @@
# Runtime Provider Loading — Implementation Plan
**Date:** 2026-03-14
**Spec:** ../specs/2026-03-14-runtime-provider-loading-design.md
**Status:** Complete
## Task 1: ProxyProvider in core/api (`pkg/provider/proxy.go`)
**File:** `/Users/snider/Code/core/api/pkg/provider/proxy.go`
Replace the Phase 3 stub with a working ProxyProvider that:
- Takes a `ProxyConfig` struct: Name, BasePath, Upstream URL, ElementSpec, SpecFile path
- Implements `Provider` (Name, BasePath, RegisterRoutes)
- Implements `Renderable` (Element)
- `RegisterRoutes` creates a catch-all `/*path` handler using `net/http/httputil.ReverseProxy`
- Strips the base path before proxying so the upstream sees clean paths
- Upstream is always `127.0.0.1` (local process)
**Test file:** `pkg/provider/proxy_test.go`
- Proxy routes requests to a test upstream server
- Health check passthrough
- Element() returns configured ElementSpec
- Name/BasePath return configured values
## Task 2: Manifest Extensions in go-scm (`manifest/manifest.go`)
**File:** `/Users/snider/Code/core/go-scm/manifest/manifest.go`
Extend the Manifest struct with provider-specific fields:
- `Namespace` — API route prefix (e.g. `/api/v1/cool-widget`)
- `Port` — listen port (0 = auto-assign)
- `Binary` — path to binary relative to provider dir
- `Args` — additional CLI args
- `Element` — UI element spec (tag + source)
- `Spec` — path to OpenAPI spec file
These fields are optional — existing manifests without them remain valid.
**Test:** Add parse test with provider fields.
## Task 3: Provider Discovery in go-scm (`marketplace/discovery.go`)
**File:** `/Users/snider/Code/core/go-scm/marketplace/discovery.go`
- `DiscoverProviders(dir string)` — scans `dir/*/manifest.yaml` (using `os` directly, not Medium, since this is filesystem discovery)
- Returns `[]DiscoveredProvider` with Dir + Manifest
- Skips directories without manifests (logs warning, continues)
**File:** `/Users/snider/Code/core/go-scm/marketplace/registry_file.go`
- `ProviderRegistry` — read/write `registry.yaml` tracking installed providers
- `LoadRegistry(path)`, `SaveRegistry(path)`, `Add()`, `Remove()`, `Get()`, `List()`
**Test file:** `marketplace/discovery_test.go`
- Discover finds providers in temp dirs with manifests
- Discover skips dirs without manifests
- Registry CRUD operations
## Task 4: RuntimeManager in core/ide (`runtime.go`)
**File:** `/Users/snider/Code/core/ide/runtime.go`
- `RuntimeManager` struct holding discovered providers, engine reference, process registry
- `NewRuntimeManager(engine, processRegistry)` constructor
- `StartAll(ctx)` — discover providers in `~/.core/providers/`, start each binary via `os/exec.Cmd`, wait for health, register ProxyProvider
- `StopAll()` — SIGTERM each provider process, clean up
- `List()` — return running providers
- Free port allocation via `net.Listen(":0")`
- Health check polling with timeout
## Task 5: Wire into core/ide main.go
**File:** `/Users/snider/Code/core/ide/main.go`
- After creating the api.Engine (`engine, _ := api.New(...)`), create a RuntimeManager
- In each mode (mcpOnly, headless, GUI), call `rm.StartAll(ctx)` before serving
- In shutdown paths, call `rm.StopAll()`
- Import `forge.lthn.ai/core/go-scm/marketplace` (add dependency)
## Task 6: Build Verification
- `cd core/api && go build ./...`
- `cd go-scm && go build ./...`
- `cd ide && go build ./...` (may need go.mod updates)
## Task 7: Tests
- `cd core/api && go test ./pkg/provider/...`
- `cd go-scm && go test ./marketplace/... ./manifest/...`
## Repos Affected
| Repo | Changes |
|------|---------|
| core/api | `pkg/provider/proxy.go` (implement), `pkg/provider/proxy_test.go` (new) |
| go-scm | `manifest/manifest.go` (extend), `manifest/manifest_test.go` (extend), `marketplace/discovery.go` (new), `marketplace/registry_file.go` (new), `marketplace/discovery_test.go` (new) |
| core/ide | `runtime.go` (new), `main.go` (wire), `go.mod` (add go-scm dep) |
## Commit Strategy
1. core/api — `feat(provider): implement ProxyProvider reverse proxy`
2. go-scm — `feat(marketplace): add provider discovery and registry`
3. core/ide — `feat(runtime): add RuntimeManager for provider loading`

File diff suppressed because it is too large Load diff

View file

@ -1,154 +0,0 @@
# core/api Polyglot Merge — Go + PHP API in One Repo
**Date:** 2026-03-14
**Status:** Approved
**Pattern:** Same as core/mcp merge (2026-03-09)
## Problem
The API layer is split across two repos:
- `core/go-api` — Gin REST framework, OpenAPI, ToolBridge, SDK codegen, WS/SSE
- `core/php-api` — Laravel REST module, rate limiting, webhooks, OAuth, API docs
These serve the same purpose in their respective stacks. The provider framework
(previous spec) needs a single `core/api` that both Go and PHP packages register
into. Same problem core/mcp solved by merging.
## Solution
Merge both into `core/api` (`ssh://git@forge.lthn.ai:2223/core/api.git`),
following the exact pattern established by core/mcp.
## Target Structure
```
core/api/
├── go.mod # module forge.lthn.ai/core/api
├── composer.json # name: lthn/api
├── .gitattributes # export-ignore Go files for Composer
├── CLAUDE.md
├── pkg/ # Go packages
│ ├── api/ # Engine, RouteGroup, middleware (from go-api root)
│ ├── provider/ # Provider framework (new, from previous spec)
│ └── openapi/ # SpecBuilder, codegen (from go-api root)
├── cmd/
│ └── core-api/ # Standalone binary (if needed)
├── src/
│ └── php/
│ └── src/
│ ├── Api/ # Boot, Controllers, Middleware, etc. (from php-api/src/Api/)
│ ├── Front/Api/ # Frontend API routes (from php-api/src/Front/)
│ └── Website/Api/ # Website API routes (from php-api/src/Website/)
└── docs/
```
## Go Module
```go
module forge.lthn.ai/core/api
```
All current consumers importing `forge.lthn.ai/core/go-api` will need to update
to `forge.lthn.ai/core/api/pkg/api` (or `forge.lthn.ai/core/api` if we keep
packages at root level — see decision below).
### Package Layout Decision
**Option A**: Packages under `pkg/api/` — clean but changes all import paths.
**Option B**: Packages at root (like go-api currently) — no import path change
if module path stays `forge.lthn.ai/core/api`.
**Recommendation: B** — keep Go files at repo root. The module path changes from
`forge.lthn.ai/core/go-api` to `forge.lthn.ai/core/api`. This is a one-line
change in each consumer's import. The PHP code lives under `src/php/` and
doesn't conflict.
```
core/api/
├── api.go, group.go, openapi.go, ... # Go source at root (same as go-api today)
├── pkg/provider/ # New provider framework
├── cmd/core-api/ # Binary
├── src/php/ # PHP source
├── composer.json
├── go.mod
└── .gitattributes
```
## Composer Package
```json
{
"name": "lthn/api",
"autoload": {
"psr-4": {
"Core\\Api\\": "src/php/src/Api/",
"Core\\Website\\Api\\": "src/php/src/Website/Api/"
}
},
"replace": {
"lthn/php-api": "self.version",
"core/php-api": "self.version"
}
}
```
Note: No `Core\Front\Api\` namespace — php-api has no `src/Front/` directory.
Add it later if a frontend API boot provider is needed.
## .gitattributes
Same pattern as core/mcp — exclude Go files from Composer installs:
```
*.go export-ignore
go.mod export-ignore
go.sum export-ignore
cmd/ export-ignore
pkg/ export-ignore
.core/ export-ignore
src/php/tests/ export-ignore
```
## Migration Steps
1. **Clone core/api** (empty repo, just created)
2. **Copy Go code** from go-api → core/api root (all .go files, cmd/, docs/)
3. **Copy PHP code** from php-api/src/ → core/api/src/php/src/
4. **Copy PHP config** — composer.json, phpunit.xml, tests
5. **Update go.mod** — change module path to `forge.lthn.ai/core/api`
6. **Create .gitattributes** — export-ignore Go files
7. **Create composer.json** — lthn/api with PSR-4 autoload
8. **Update consumers** — sed import paths in all repos that import go-api
9. **Add provider framework**`pkg/provider/` from the previous spec
10. **Archive go-api and php-api** on forge (keep for backward compat period)
## Consumer Updates
Repos that import `forge.lthn.ai/core/go-api`:
- core/go-ai (ToolBridge, API routes)
- core/go-ml (API route group)
- core/mcp (bridge.go)
- core/agent (indirect, v0.0.3)
- core/ide (via provider framework)
Each needs: `s/forge.lthn.ai\/core\/go-api/forge.lthn.ai\/core\/api/g` in
imports and go.mod.
PHP consumers via Composer — update `lthn/php-api``lthn/api` in
composer.json. Namespace stays `Core\Api\` (unchanged).
## Go Workspace
Add `core/api` to `~/Code/go.work`, remove `core/go-api` entry.
## Testing
- `go test ./...` in core/api
- `composer test` in core/api (runs PHP tests via phpunit)
- Build verification across consumer repos after import path update
## Not In Scope
- Provider framework implementation (separate spec, separate plan)
- New API features — this is a structural merge only
- TypeScript API layer (future Phase 3)

View file

@ -1,226 +0,0 @@
# GUI App Shell — Framework-Level Application Frame
**Date:** 2026-03-14
**Status:** Approved
**Depends on:** Service Provider Framework, Runtime Provider Loading
**Source:** Port from core-gui/cmd/lthn-desktop/frontend
## Problem
core/ide has a bare Angular frontend with placeholder routes. The real app
shell exists in the archived `core-gui/cmd/lthn-desktop/frontend` with
HLCRF layout, Web Awesome components, sidebar navigation, feature flags,
i18n, and custom element support. It needs to live in `core/gui/ui/` as a
framework component that any Wails app can import.
## Solution
Port the application frame from lthn-desktop into `core/gui/ui/` as a
reusable Angular library. Add a `ProviderDiscoveryService` that dynamically
populates navigation and loads custom elements from registered providers.
## Architecture
```
core/gui/ui/ <- framework (npm package)
src/
frame/
application-frame.ts <- HLCRF shell (header, sidebar, content, footer)
application-frame.html <- wa-page template with slots
system-tray-frame.ts <- tray panel (380x480 frameless)
services/
provider-discovery.ts <- fetch providers, load custom elements
websocket.ts <- WS connection with reconnect
api-config.ts <- API base URL configuration
i18n.ts <- translation service
components/
provider-host.ts <- wrapper that hosts a custom element
provider-nav.ts <- dynamic sidebar from providers
status-bar.ts <- footer with provider status
index.ts <- public API exports
package.json
tsconfig.json
core/ide/frontend/ <- application (imports framework)
src/
app/
app.routes.ts <- routes using framework components
app.config.ts <- provider registrations
main.ts
angular.json
```
## Provider Discovery Service
The core service that makes everything dynamic:
```typescript
@Injectable({ providedIn: 'root' })
export class ProviderDiscoveryService {
private providers = signal<ProviderInfo[]>([]);
readonly providers$ = this.providers.asReadonly();
constructor(private apiConfig: ApiConfigService) {}
async discover(): Promise<void> {
const res = await fetch(`${this.apiConfig.baseUrl}/api/v1/providers`);
const data = await res.json();
this.providers.set(data.providers);
// Load custom elements for Renderable providers
for (const p of data.providers) {
if (p.element?.tag && p.element?.source) {
await this.loadElement(p.element.tag, p.element.source);
}
}
}
private async loadElement(tag: string, source: string): Promise<void> {
if (customElements.get(tag)) return;
const script = document.createElement('script');
script.type = 'module';
script.src = source;
document.head.appendChild(script);
await customElements.whenDefined(tag);
}
}
interface ProviderInfo {
name: string;
basePath: string;
status?: string;
element?: { tag: string; source: string };
channels?: string[];
}
```
## Application Frame
Ported from lthn-desktop with these changes:
1. **Navigation is dynamic** — populated from ProviderDiscoveryService
2. **Content area hosts custom elements** — ProviderHostComponent wraps any custom element
3. **Feature flags from config** — reads from core/config API
4. **Web Awesome** — keeps wa-page, wa-button design system
5. **Font Awesome Pro** — keeps icon system
6. **i18n** — keeps translation service
7. **HLCRF slots** — header, navigation, main, footer map to wa-page slots
### Dynamic Navigation
```typescript
// In application-frame.ts
async ngOnInit() {
await this.providerService.discover();
this.navigation = this.providerService.providers$()
.filter(p => p.element)
.map(p => ({
name: p.name,
href: p.name.toLowerCase(),
icon: 'fa-regular fa-puzzle-piece',
element: p.element
}));
}
```
### Provider Host Component
Renders any custom element by tag name using Angular's Renderer2 for safe DOM manipulation:
```typescript
@Component({
selector: 'provider-host',
template: '<div #container></div>',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ProviderHostComponent implements OnChanges {
@Input() tag!: string;
@Input() apiUrl = '';
@ViewChild('container') container!: ElementRef;
constructor(private renderer: Renderer2) {}
ngOnChanges() {
const native = this.container.nativeElement;
// Clear previous element safely
while (native.firstChild) {
this.renderer.removeChild(native, native.firstChild);
}
// Create and append custom element
const el = this.renderer.createElement(this.tag);
if (this.apiUrl) this.renderer.setAttribute(el, 'api-url', this.apiUrl);
this.renderer.appendChild(native, el);
}
}
```
### Routes
```typescript
export const routes: Routes = [
{ path: 'tray', component: SystemTrayFrame },
{
path: '',
component: ApplicationFrame,
children: [
{ path: ':provider', component: ProviderHostComponent },
{ path: '', redirectTo: 'process', pathMatch: 'full' }
]
}
];
```
## System Tray Frame
380x480 frameless panel showing:
- Provider status cards (from discovery service)
- Quick stats from Streamable providers (via WS)
- Brain connection status
- MCP server status
## What to Port from lthn-desktop
| Source | Target | Changes |
|--------|--------|---------|
| frame/application.frame.ts | core/gui/ui/src/frame/ | Dynamic nav from providers |
| frame/application.frame.html | core/gui/ui/src/frame/ | Keep wa-page template |
| frame/system-tray.frame.ts | core/gui/ui/src/frame/ | Add provider status cards |
| services/translation.service.ts | core/gui/ui/src/services/ | Keep as-is |
| services/i18n.service.ts | core/gui/ui/src/services/ | Keep as-is |
## What NOT to Port
- blockchain/ — that is a provider, not framework
- mining/ — that is a provider (already has its own elements)
- developer/ — that is a provider
- system/setup* — future setup wizard provider
- Wails bindings (@lthn/core/*) — replaced by REST API calls
## Dependencies
### core/gui/ui (npm)
- @angular/core, @angular/router, @angular/common
- @awesome.me/webawesome (design system)
- @fortawesome/fontawesome-pro (icons)
### core/ide/frontend (npm)
- core/gui/ui (local dependency)
- Angular 20+
## Build
```bash
# Build framework
cd core/gui/ui && npm run build
# Build IDE app (imports framework)
cd core/ide/frontend && npm run build
```
## Not In Scope
- Setup wizard (future provider)
- Monaco editor integration (future provider)
- Blockchain dashboard (future provider)
- Theming system (future — Web Awesome handles dark mode)

View file

@ -1,235 +0,0 @@
# core/ide Modernisation — Lego Rewire
**Date:** 2026-03-14
**Status:** Approved
## Problem
core/ide was built before the ecosystem packages existed. It hand-rolls an MCP bridge, webview service, brain tools, and WebSocket relay that are now properly implemented in dedicated packages. The codebase is ~1,200 lines of duplicated logic.
## Solution
Replace all hand-rolled services with imports from the existing ecosystem. The IDE becomes a thin Wails shell that wires up `core.Core` with registered services.
## Architecture
```
┌─────────────────────────────────────────────┐
│ Wails 3 │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ Systray │ │ Angular Frontend │ │
│ │ (lifecycle)│ │ /tray (control pane) │ │
│ │ │ │ /ide (full IDE) │ │
│ └────┬─────┘ └──────────┬───────────────┘ │
│ │ │ WS │
└───────┼───────────────────┼─────────────────┘
│ │
┌───────┼───────────────────┼─────────────────┐
│ │ core.Core │ │
│ ┌────┴─────┐ ┌─────────┴───────┐ │
│ │ display │ │ go-ws Hub │ │
│ │ Service │ │ (Angular comms) │ │
│ │(core/gui)│ └─────────────────┘ │
│ └──────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ core/mcp Service │ │
│ │ ├─ brain subsystem (OpenBrain) │ │
│ │ ├─ gui subsystem (74 tools) │ │
│ │ ├─ file ops (go-io sandboxed) │ │
│ │ └─ transports: stdio, TCP, Unix │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ config │ │go-process│ │ go-log │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
│ stdio (--mcp flag)
┌───────┴─────────────────────────────────────┐
│ Claude Code / other MCP clients │
└─────────────────────────────────────────────┘
```
## Application Lifecycle
- **Systray** is the app lifecycle. Quit from tray menu = shutdown Core.
- **Tray panel** is a 380x480 frameless Angular window attached to the tray icon. Renders a control pane (MCP status, connected agents, brain stats).
- **Windows** are disposable views — closing a window does not terminate the app.
- **macOS**: Accessory app (no Dock icon). Template tray icon.
- **config** reads `.core/config.yaml`. If `gui.enabled: false` (or no display), Wails is skipped entirely. Core still runs all services — MCP server, brain, etc.
## Service Wiring
Services are registered as factory functions via `core.WithService`. Each factory
receives the `*core.Core` instance and returns the service. Construction order
matters — the WS hub and IDE bridge must be built before the MCP service that
depends on them.
```go
func main() {
cfg, _ := config.New()
// Shared resources built before Core
hub := ws.NewHub()
bridgeCfg := ide.ConfigFromEnv()
bridge := ide.NewBridge(hub, bridgeCfg)
// Core framework — services run regardless of GUI
c, _ := core.New(
core.WithService(func(c *core.Core) (any, error) {
return hub, nil // go-ws hub for Angular + bridge
}),
core.WithService(display.Register(nil)), // core/gui — nil platform until Wails starts
core.WithService(func(c *core.Core) (any, error) {
// MCP service with subsystems
return mcp.New(
mcp.WithWorkspaceRoot(cwd),
mcp.WithSubsystem(brain.New(bridge)),
mcp.WithSubsystem(guiMCP.New(c)),
)
}),
)
if guiEnabled(cfg) {
startWailsApp(c, hub, bridge)
} else {
// No GUI — start Core lifecycle manually
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
startMCP(ctx, c) // goroutine: mcpSvc.ServeStdio or ServeTCP
bridge.Start(ctx)
<-ctx.Done()
}
}
```
### MCP Lifecycle
`mcp.Service` does not implement `core.Startable`/`core.Stoppable`. The MCP
transport must be started explicitly:
- **GUI mode**: `startWailsApp` starts `mcpSvc.ServeStdio(ctx)` or
`mcpSvc.ServeTCP(ctx, addr)` in a goroutine alongside the Wails event loop.
Wails quit triggers `mcpSvc.Shutdown(ctx)`.
- **No-GUI mode**: `startMCP(ctx, c)` starts the transport in a goroutine.
Context cancellation (SIGINT/SIGTERM) triggers shutdown.
### Version Alignment
After updating `go.mod` to import `core/mcp`, `core/gui`, etc., run
`go work sync` to align indirect dependency versions across the workspace.
## MCP Server
The `core/mcp` package provides the standard MCP protocol server with three transports:
- **stdio** — for Claude Code integration via `.claude/.mcp.json`
- **TCP** — for network MCP clients (port from config, default 9877)
- **Unix socket** — for local IPC
Claude Code connects via stdio:
```json
{
"mcpServers": {
"core-ide": {
"type": "stdio",
"command": "core-ide",
"args": ["--mcp"]
}
}
}
```
## Brain Subsystem
Already implemented in `core/mcp/pkg/mcp/brain`. Proxies to Laravel OpenBrain API via the IDE bridge (`core/mcp/pkg/mcp/ide`). Tools:
- `brain_remember` — store a memory (content, type, project, agent_id, tags)
- `brain_recall` — semantic search (query, top_k, project, type, agent_id)
- `brain_forget` — remove a memory by ID
- `brain_ensure_collection` — ensure Qdrant collection exists
Environment: `CORE_API_URL` (default `http://localhost:8000`), `CORE_API_TOKEN`.
## GUI Subsystem
The `core/gui/pkg/mcp.Subsystem` provides 74 MCP tools across 14 categories (webview, window, layout, screen, clipboard, dialog, notification, tray, environment, browser, contextmenu, keybinding, dock, lifecycle). These are only active when a display platform is registered.
The `core/gui/pkg/display.Service` bridges Core IPC to the Wails platform — window management, DOM interaction, event forwarding, WS event bridging for Angular.
## Files
### Delete (7 files)
| File | Replaced by |
|------|-------------|
| `mcp_bridge.go` | `core/mcp` Service + transports |
| `webview_svc.go` | `core/gui/pkg/webview` + `core/gui/pkg/display` |
| `brain_mcp.go` | `core/mcp/pkg/mcp/brain` |
| `claude_bridge.go` | `core/mcp/pkg/mcp/ide` Bridge |
| `headless_mcp.go` | Not needed — core/mcp runs in-process |
| `headless.go` | config `gui.enabled` flag. Jobrunner/poller extracted to core/agent (not in scope). |
| `greetservice.go` | Scaffold placeholder, not needed |
### Rewrite (1 file)
| File | Changes |
|------|---------|
| `main.go` | Thin shell: config, core.New with services, Wails systray, `--mcp` flag for stdio |
### Keep (unchanged)
| File/Dir | Reason |
|----------|--------|
| `frontend/` | Angular app — tray panel + IDE routes |
| `icons/` | Tray icons |
| `.core/build.yaml` | Build configuration |
| `CLAUDE.md` | Update after implementation |
| `go.mod` | Update dependencies |
## Dependencies
### Add
- `forge.lthn.ai/core/go` — DI framework, core.Core
- `forge.lthn.ai/core/mcp` — MCP server, brain subsystem, IDE bridge
- `forge.lthn.ai/core/gui` — display service, 74 MCP tools
- `forge.lthn.ai/core/go-io` — sandboxed filesystem
- `forge.lthn.ai/core/go-log` — structured logging + errors
- `forge.lthn.ai/core/config` — configuration
### Remove (indirect cleanup)
- `forge.lthn.ai/core/agent` — no longer imported directly (brain tools via core/mcp)
- Direct `gorilla/websocket` — replaced by go-ws
## Configuration
`.core/config.yaml`:
```yaml
gui:
enabled: true # false = no Wails, Core still runs
mcp:
transport: stdio # stdio | tcp | unix
tcp:
port: 9877
brain:
api_url: http://localhost:8000
api_token: "" # or CORE_API_TOKEN env var
```
## Testing
- Core framework tests live in their respective packages (core/mcp, core/gui)
- IDE-specific tests: Wails service startup, config loading, systray lifecycle
- Integration: `core-ide --mcp` stdio round-trip with a test MCP client
## Not In Scope
- Angular frontend changes (existing routes work as-is)
- New MCP tool categories beyond what core/mcp and core/gui already provide
- Jobrunner/Forgejo poller (was in headless.go — separate concern, belongs in core/agent)
- CoreDeno/TypeScript runtime integration (future phase)

View file

@ -1,336 +0,0 @@
# Runtime Provider Loading — Plugin Ecosystem
**Date:** 2026-03-14
**Status:** Approved
**Depends on:** Service Provider Framework, SCM Provider
## Problem
All providers are currently compiled into the binary. Users cannot install,
remove, or update providers without rebuilding core-ide. There's no plugin
ecosystem — every provider is a Go import in main.go.
## Solution
Runtime provider discovery using the Mining namespace pattern. Installed
providers run as managed processes with `--namespace` flags. The IDE's Gin
router proxies to them. JS bundles load dynamically in Angular. No recompile.
## Architecture
```
~/.core/providers/
├── cool-widget/
│ ├── manifest.yaml # Name, namespace, element, permissions
│ ├── cool-widget # Binary (or path to system binary)
│ ├── openapi.json # OpenAPI spec
│ └── assets/
│ └── core-cool-widget.js # Custom element bundle
├── data-viz/
│ ├── manifest.yaml
│ ├── data-viz
│ └── assets/
│ └── core-data-viz.js
└── registry.yaml # Installed providers list
```
### Runtime Flow
```
core-ide (main process)
┌─────────────────────────────────┐
│ Gin Router │
│ /api/v1/scm/* → compiled in │
│ /api/v1/brain/* → compiled in │
│ /api/v1/cool-widget/* → proxy ──┼──→ :9901 (cool-widget binary)
│ /api/v1/data-viz/* → proxy ──┼──→ :9902 (data-viz binary)
│ │
│ Angular Shell │
<core-scm-panel> → compiled │
<core-cool-widget> → dynamic │
<core-data-viz> → dynamic │
└─────────────────────────────────┘
```
## Manifest Format
`.core/manifest.yaml` (same as go-scm's manifest loader):
```yaml
code: cool-widget
name: Cool Widget Dashboard
version: 1.0.0
author: someone
licence: EUPL-1.2
# Provider configuration
namespace: /api/v1/cool-widget
port: 0 # 0 = auto-assign
binary: ./cool-widget # Relative to provider dir
args: [] # Additional CLI args
# UI
element:
tag: core-cool-widget
source: ./assets/core-cool-widget.js
# Layout
layout: HCF
slots:
H: toolbar
C: dashboard
F: status
# OpenAPI spec
spec: ./openapi.json
# Permissions (for TIM sandbox — future)
permissions:
network: ["api.example.com"]
filesystem: ["~/.core/providers/cool-widget/data/"]
# Signature
sign: <ed25519 signature>
```
## Provider Lifecycle
### Discovery
On startup, the IDE scans `~/.core/providers/*/manifest.yaml`:
```go
func DiscoverProviders(dir string) ([]RuntimeProvider, error) {
entries, _ := os.ReadDir(dir)
var providers []RuntimeProvider
for _, e := range entries {
if !e.IsDir() { continue }
m, err := manifest.Load(filepath.Join(dir, e.Name()))
if err != nil { continue }
providers = append(providers, RuntimeProvider{
Dir: filepath.Join(dir, e.Name()),
Manifest: m,
})
}
return providers, nil
}
```
### Start
For each discovered provider:
1. Assign a free port (if `port: 0` in manifest)
2. Start the binary via go-process: `./cool-widget --namespace /api/v1/cool-widget --port 9901`
3. Wait for health check: `GET http://localhost:9901/health`
4. Register a `ProxyProvider` in the API engine that reverse-proxies to that port
5. Serve the JS bundle as a static asset at `/assets/{code}.js`
```go
type RuntimeProvider struct {
Dir string
Manifest *manifest.Manifest
Process *process.Daemon
Port int
}
func (rp *RuntimeProvider) Start(engine *api.Engine, hub *ws.Hub) error {
// Start binary
rp.Port = findFreePort()
rp.Process = process.NewDaemon(process.DaemonOptions{
Command: filepath.Join(rp.Dir, rp.Manifest.Binary),
Args: append(rp.Manifest.Args, "--namespace", rp.Manifest.Namespace, "--port", strconv.Itoa(rp.Port)),
PIDFile: filepath.Join(rp.Dir, "provider.pid"),
})
rp.Process.Start()
// Wait for health
waitForHealth(rp.Port)
// Register proxy provider
proxy := provider.NewProxy(provider.ProxyConfig{
Name: rp.Manifest.Code,
BasePath: rp.Manifest.Namespace,
Upstream: fmt.Sprintf("http://127.0.0.1:%d", rp.Port),
Element: rp.Manifest.Element,
SpecFile: filepath.Join(rp.Dir, rp.Manifest.Spec),
})
engine.Register(proxy)
// Serve JS assets
engine.Router().Static("/assets/"+rp.Manifest.Code, filepath.Join(rp.Dir, "assets"))
return nil
}
```
### Stop
On IDE quit or provider removal:
1. Send SIGTERM to provider process
2. Remove proxy routes from Gin
3. Unload JS bundle from Angular
### Hot Reload (Development)
During development (`core dev` in a provider dir):
1. Watch for binary changes → restart process
2. Watch for JS changes → reload in Angular
3. Watch for manifest changes → re-register proxy
## Install / Remove
### Install
```bash
core install forge.lthn.ai/someone/cool-widget
```
1. Clone or download the provider repo
2. Verify Ed25519 signature in manifest
3. If Go source: `go build -o cool-widget .` in the provider dir
4. Copy to `~/.core/providers/cool-widget/`
5. Update `~/.core/providers/registry.yaml`
6. If IDE is running: hot-load the provider (no restart needed)
### Remove
```bash
core remove cool-widget
```
1. Stop the provider process
2. Remove from `~/.core/providers/cool-widget/`
3. Update `~/.core/providers/registry.yaml`
4. If IDE is running: unload the proxy + UI
### Update
```bash
core update cool-widget
```
1. Pull latest from git
2. Verify new signature
3. Rebuild if source-based
4. Stop old process, start new
5. Reload JS bundle
## Registry
`~/.core/providers/registry.yaml`:
```yaml
version: 1
providers:
cool-widget:
installed: "2026-03-14T12:00:00Z"
version: 1.0.0
source: forge.lthn.ai/someone/cool-widget
auto_start: true
data-viz:
installed: "2026-03-14T13:00:00Z"
version: 0.2.0
source: github.com/user/data-viz
auto_start: true
```
## Custom Binary Build
```bash
core build --brand "My Product" --include cool-widget,data-viz
```
Instead of runtime proxy, this compiles the selected providers directly
into the binary:
1. Read each provider's Go source
2. Import as compiled providers (not proxied)
3. Embed JS bundles via `//go:embed`
4. Set binary name, icon, and metadata from brand config
5. Output: single binary with everything compiled in
Same providers, two modes: proxied (plugin) or compiled (product).
## Provider Binary Contract
A provider binary must:
1. Accept `--namespace` flag (API route prefix)
2. Accept `--port` flag (HTTP listen port)
3. Serve `GET /health``{"status": "ok"}`
4. Serve its API under the namespace path
5. Optionally accept `--ws-url` flag to connect to IDE's WS hub for events
The element-template already scaffolds this pattern.
## Swagger Aggregation
The IDE's `SpecBuilder` aggregates OpenAPI specs from:
1. Compiled providers (via `DescribableGroup.Describe()`)
2. Runtime providers (via their `openapi.json` files)
Merged into one spec at `/swagger/doc.json`. The Swagger UI shows all
providers' endpoints in one place.
## Angular Dynamic Loading
Custom elements load at runtime without Angular knowing about them at
build time:
```typescript
// In the IDE's Angular shell
async function loadProviderElement(tag: string, scriptUrl: string) {
if (customElements.get(tag)) return; // Already loaded
const script = document.createElement('script');
script.type = 'module';
script.src = scriptUrl;
document.head.appendChild(script);
// Wait for registration
await customElements.whenDefined(tag);
}
```
The tray panel and IDE layout call this for each Renderable provider
discovered at startup. Angular wraps the custom element in a host component
for the HLCRF slot assignment.
## Security
### Signature Verification
All manifests must be signed (Ed25519). Unsigned providers are rejected
unless `--allow-unsigned` is passed (development only).
### Process Isolation
Provider processes run as the current user with no special privileges.
Future: TIM containers for full sandbox (filesystem + network isolation
per the manifest's permissions declaration).
### Network
Providers listen on `127.0.0.1` only. No external network exposure.
The IDE's Gin router is the only entry point.
## Implementation Location
| Component | Package | New/Existing |
|-----------|---------|-------------|
| Provider discovery | go-scm/marketplace | Extend existing |
| Process management | go-process | Existing daemon API |
| Proxy provider | core/api/pkg/provider | New: proxy.go |
| Install/remove CLI | core/cli cmd/ | New commands |
| Runtime loader | core/ide | New: runtime.go |
| JS dynamic loading | core/ide frontend/ | New: provider-loader service |
| Registry file | go-scm/marketplace | Extend existing |
## Not In Scope
- TIM container sandbox (future — Phase 4 from provider framework spec)
- Provider marketplace server (git-based discovery is sufficient)
- Revenue sharing / paid providers (future — SMSG licensing)
- Angular module federation (future — current pattern is custom elements)
- Multi-language provider SDKs (future — element-template is Go-first)

View file

@ -1,258 +0,0 @@
# go-scm Service Provider + Custom Elements
**Date:** 2026-03-14
**Status:** Approved
**Depends on:** Service Provider Framework, core/api
## Problem
go-scm has marketplace, manifest, and registry functionality but no REST API
or UI. The IDE can't browse providers, install apps, or inspect manifests
without CLI commands.
## Solution
Add a service provider to go-scm with REST endpoints and a Lit custom element
bundle containing multiple composable elements.
## Provider (Go)
### ScmProvider
Lives in `go-scm/pkg/api/provider.go`. Implements `Provider` + `Streamable` +
`Describable` + `Renderable`.
```go
Name() → "scm"
BasePath() → "/api/v1/scm"
Element() → ElementSpec{Tag: "core-scm-panel", Source: "/assets/core-scm.js"}
Channels() → []string{"scm.marketplace.*", "scm.manifest.*", "scm.registry.*"}
```
### REST Endpoints
#### Marketplace
| Method | Path | Description |
|--------|------|-------------|
| GET | /marketplace | List available providers from git registry |
| GET | /marketplace/:code | Get provider details |
| POST | /marketplace/:code/install | Install provider |
| DELETE | /marketplace/:code | Remove installed provider |
| POST | /marketplace/refresh | Pull latest marketplace index (requires new FetchIndex function) |
#### Manifest
| Method | Path | Description |
|--------|------|-------------|
| GET | /manifest | Read .core/manifest.yaml from current directory |
| POST | /manifest/verify | Verify Ed25519 signature (body: {public_key: hex}) |
| POST | /manifest/sign | Sign manifest with private key (body: {private_key: hex}) |
| GET | /manifest/permissions | List declared permissions |
#### Installed
| Method | Path | Description |
|--------|------|-------------|
| GET | /installed | List installed providers |
| POST | /installed/:code/update | Apply update (pulls latest from git) |
Note: Single-item `GET /installed/:code` and update-check require new methods
on `Installer` (`Get(code)` and `CheckUpdate(code)`). Deferred — the list
endpoint is sufficient for Phase 1. Update applies `Installer.Update()`.
#### Registry
| Method | Path | Description |
|--------|------|-------------|
| GET | /registry | List repos from repos.yaml |
| GET | /registry/:name/status | Git status for a repo |
Note: Registry endpoints are read-only. Pull/push actions are handled by
`core dev` CLI commands, not the REST API. The `<core-scm-registry>` element
shows status only — no write buttons.
### WS Events
| Event | Data | Trigger |
|-------|------|---------|
| scm.marketplace.refreshed | {count} | After marketplace pull |
| scm.marketplace.installed | {code, name, version, installed_at} | After install |
| scm.marketplace.removed | {code} | After remove |
| scm.manifest.verified | {code, valid, signer} | After signature check |
| scm.registry.changed | {name, status} | Repo status change |
## Custom Elements (Lit)
### Bundle Structure
```
go-scm/ui/
├── src/
│ ├── index.ts # Bundle entry — exports all elements
│ ├── scm-panel.ts # <core-scm-panel> — HLCRF layout
│ ├── scm-marketplace.ts # <core-scm-marketplace> — browse/install
│ ├── scm-manifest.ts # <core-scm-manifest> — view/verify
│ ├── scm-installed.ts # <core-scm-installed> — manage installed
│ ├── scm-registry.ts # <core-scm-registry> — repo status
│ └── shared/
│ ├── api.ts # Fetch wrapper for /api/v1/scm/*
│ └── events.ts # WS event listener helpers
├── package.json
├── tsconfig.json
└── index.html # Demo page
```
### Elements
#### `<core-scm-panel>`
Top-level element. Arranges child elements in HLCRF layout `H[LC]CF`:
- H: Title bar with refresh button
- H-L: Navigation tabs (Marketplace / Installed / Registry)
- H-C: Search input
- C: Active tab content (one of the child elements)
- F: Status bar (connection state, last refresh)
#### `<core-scm-marketplace>`
Browse the git-based provider marketplace.
| Attribute | Type | Description |
|-----------|------|-------------|
| api-url | string | API base URL (default: current origin) |
| category | string | Filter by category |
Displays:
- Provider cards with name, description, version, author
- Install/Remove button per card
- Category filter tabs
- Search (filters client-side)
#### `<core-scm-manifest>`
View and verify a .core/manifest.yaml file.
| Attribute | Type | Description |
|-----------|------|-------------|
| api-url | string | API base URL |
| path | string | Directory to read manifest from |
Displays:
- Manifest fields (code, name, version, layout variant)
- HLCRF slot assignments
- Permission declarations
- Signature status badge (verified/unsigned/invalid)
- Sign button (calls POST /manifest/sign)
#### `<core-scm-installed>`
Manage installed providers.
| Attribute | Type | Description |
|-----------|------|-------------|
| api-url | string | API base URL |
Displays:
- Installed provider list with version, status
- Update available indicator
- Update/Remove buttons
- Provider detail panel on click
#### `<core-scm-registry>`
Show repos.yaml registry status.
| Attribute | Type | Description |
|-----------|------|-------------|
| api-url | string | API base URL |
Displays:
- Repo list from registry
- Git status per repo (clean/dirty/ahead/behind)
### Shared
#### `api.ts`
```typescript
export class ScmApi {
constructor(private baseUrl: string = '') {}
private get base() {
return `${this.baseUrl}/api/v1/scm`;
}
marketplace() { return fetch(`${this.base}/marketplace`).then(r => r.json()); }
install(code: string){ return fetch(`${this.base}/marketplace/${code}/install`, {method:'POST'}).then(r => r.json()); }
remove(code: string) { return fetch(`${this.base}/marketplace/${code}`, {method:'DELETE'}).then(r => r.json()); }
installed() { return fetch(`${this.base}/installed`).then(r => r.json()); }
manifest() { return fetch(`${this.base}/manifest`).then(r => r.json()); }
verify(publicKey: string) { return fetch(`${this.base}/manifest/verify`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({public_key: publicKey})}).then(r => r.json()); }
sign(privateKey: string) { return fetch(`${this.base}/manifest/sign`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({private_key: privateKey})}).then(r => r.json()); }
registry() { return fetch(`${this.base}/registry`).then(r => r.json()); }
}
```
#### `events.ts`
```typescript
export function connectScmEvents(wsUrl: string, handler: (event: any) => void) {
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (event.type?.startsWith('scm.')) handler(event);
};
return ws;
}
```
## Build
```bash
cd go-scm/ui
npm install
npm run build # → dist/core-scm.js (single bundle, all elements)
```
The built JS is embedded in go-scm's Go binary via `//go:embed` and served
as a static asset by the provider.
## Dependencies
### Go (go-scm)
- core/api (provider interfaces, Gin)
- go-ws (WS hub for events)
- Existing: manifest, marketplace, repos packages (already in go-scm)
### TypeScript (ui/)
- lit (Web Components)
- No other runtime deps
## Files
### Create in go-scm
| File | Purpose |
|------|---------|
| `pkg/api/provider.go` | ScmProvider with all REST endpoints |
| `pkg/api/provider_test.go` | Endpoint tests |
| `pkg/api/embed.go` | `//go:embed` for UI assets |
| `ui/src/index.ts` | Bundle entry |
| `ui/src/scm-panel.ts` | Top-level HLCRF panel |
| `ui/src/scm-marketplace.ts` | Marketplace browser |
| `ui/src/scm-manifest.ts` | Manifest viewer |
| `ui/src/scm-installed.ts` | Installed provider manager |
| `ui/src/scm-registry.ts` | Registry status |
| `ui/src/shared/api.ts` | API client |
| `ui/src/shared/events.ts` | WS event helpers |
| `ui/package.json` | Lit + TypeScript deps |
| `ui/tsconfig.json` | TypeScript config |
| `ui/index.html` | Demo page |
## Not In Scope
- Actual STIM packaging/distribution (future — uses Borg)
- Provider sandbox enforcement (future — uses TIM/CoreDeno)
- Marketplace git server (uses existing forge)
- Angular wrappers (IDE dynamically loads custom elements)

View file

@ -1,290 +0,0 @@
# Service Provider Framework — Polyglot API + UI
**Date:** 2026-03-14
**Status:** Approved
**Depends on:** IDE Modernisation (2026-03-14)
## Problem
Each package in the ecosystem (Go, PHP, TypeScript) builds its own API endpoints,
WebSocket events, and UI components independently. There's no standard way for a
package to say "I provide these capabilities" and have them automatically
assembled into an API router, MCP server, or GUI.
The Mining repo proves the pattern works — Gin routes + WS events + Angular
custom element = full process management UI. But it's hand-wired for one use case.
## Vision
Any package in any language can register as a service provider. The contract is
OpenAPI. Go packages implement a Go interface directly. PHP and TypeScript
packages publish an OpenAPI spec and run their own HTTP handler — the API layer
reverse-proxies or aggregates. The result:
- Every provider automatically gets a REST API
- Every provider with a custom element automatically gets a GUI panel
- Every provider with tool descriptions automatically gets MCP tools
- The language the provider is written in is irrelevant
## Architecture
```
.core/config.yaml
├─→ core/go-api (Service Registry)
│ ├─ Go providers: implement Provider interface directly
│ ├─ PHP providers: OpenAPI spec + reverse proxy to FrankenPHP
│ ├─ TS providers: OpenAPI spec + reverse proxy to CoreDeno
│ ├─ Assembled Gin router (all routes merged)
│ └─ WS hub (all events merged)
├─→ core/gui (Display Layer)
│ ├─ Discovers Renderable providers
│ ├─ Loads custom elements into Angular shell
│ └─ HLCRF layout from .core/ config
├─→ core/mcp (Tool Layer)
│ ├─ Discovers Describable providers
│ ├─ Registers as MCP tools
│ └─ stdio/TCP/Unix transports
└─→ core/ide (Application Shell)
├─ Wails systray + Angular frontend
├─ Hosts go-api router
└─ Hosts core/mcp server
```
## The Provider Interface (Go)
Lives in `core/go-api/pkg/provider/`. Built on top of the existing `RouteGroup`
and `DescribableGroup` interfaces — providers ARE route groups, not a parallel
system.
```go
// Provider extends RouteGroup with a provider identity.
// Every Provider is a RouteGroup and registers through api.Engine.Register().
type Provider interface {
api.RouteGroup // Name(), BasePath(), RegisterRoutes(*gin.RouterGroup)
}
// Streamable providers emit real-time events via WebSocket.
// The hub is injected at construction time. Channels() declares the
// event prefixes this provider will emit (e.g. "brain.*").
type Streamable interface {
Provider
Channels() []string // Event prefixes emitted by this provider
}
// Describable providers expose structured route descriptions for OpenAPI.
// This extends the existing DescribableGroup interface.
type Describable interface {
Provider
api.DescribableGroup // Describe() []RouteDescription
}
// Renderable providers declare a custom element for GUI display.
type Renderable interface {
Provider
Element() ElementSpec
}
type ElementSpec struct {
Tag string // e.g. "core-brain-panel"
Source string // URL or embedded path to the JS bundle
}
```
Note: `Manageable` (Start/Stop/Status) is deferred to Phase 2. In Phase 1,
provider lifecycle is handled by `core.Core`'s existing `Startable`/`Stoppable`
interfaces — providers that need lifecycle management implement those directly
when registered as Core services.
### Registration
Providers register through the existing `api.Engine`, not a parallel router.
This gives them middleware, CORS, Swagger, health checks, and OpenAPI for free.
```go
engine, _ := api.New(
api.WithCORS(),
api.WithSwagger(),
api.WithWSHub(hub),
)
// Register providers as route groups — they get middleware, OpenAPI, etc.
engine.Register(brain.NewProvider(bridge, hub))
engine.Register(daemon.NewProvider(registry))
engine.Register(build.NewProvider())
// Providers that are Streamable have the hub injected at construction.
// They call hub.SendToChannel("brain.recall.complete", event) internally.
```
The `Registry` type is a convenience wrapper that collects providers and
calls `engine.Register()` for each:
```go
reg := provider.NewRegistry()
reg.Add(brain.NewProvider(bridge, hub))
reg.Add(build.NewProvider())
reg.MountAll(engine) // calls engine.Register() for each
```
## Polyglot Providers (PHP, TypeScript)
Non-Go providers don't implement the Go interface. They:
1. Publish an OpenAPI spec in `.core/providers/{name}.yaml`
2. Run their own HTTP server (FrankenPHP, CoreDeno, or any process)
3. The Go API layer discovers the spec and creates a reverse proxy route group
```yaml
# .core/providers/studio.yaml
name: studio
language: php
spec: openapi-3.json # Path to OpenAPI spec
endpoint: http://localhost:8000 # Where the PHP handler listens
element:
tag: core-studio-panel
source: /assets/studio-panel.js
events:
- studio.render.started
- studio.render.complete
```
The Go registry wraps this as a `ProxyProvider` — it implements `Provider` by
reverse-proxying to the endpoint, `Describable` by reading the spec file,
and `Renderable` by reading the element config.
For real-time events, the upstream process connects to the Go WS hub as a
client (using `ws.ReconnectingClient`) or pushes events via the go-ws Redis
pub/sub backend. The `ProxyProvider` declares the expected channels from the
YAML config. The mechanism choice depends on deployment: Redis for multi-host,
direct WS for single-binary.
### OpenAPI as Contract
The OpenAPI spec is the single source of truth for:
- **go-api**: Route mounting and request validation
- **core/mcp**: Automatic MCP tool generation from endpoints
- **core/gui**: Form generation for Manageable providers
- **SDK codegen**: TypeScript/Python/PHP client generation (already in go-api)
A PHP package that publishes a valid OpenAPI spec gets all four for free.
## Discovery
Provider discovery follows the `.core/` convention:
1. **Static config**`.core/config.yaml` lists enabled providers
2. **Directory scan**`.core/providers/*.yaml` for polyglot provider specs
3. **Go registration**`core.WithService(provider.Register(registry))` in main.go
```yaml
# .core/config.yaml
providers:
brain:
enabled: true
studio:
enabled: true
endpoint: http://localhost:8000
gallery:
enabled: false
```
## GUI Integration
`core/gui`'s display service queries the registry for `Renderable` providers.
For each one, it:
1. Loads the custom element JS bundle (from `ElementSpec.Source`)
2. Creates an Angular wrapper component that hosts the custom element
3. Registers it in the available panels list
4. Layout is configured via `.core/config.yaml` or defaults to auto-arrangement
The Angular shell doesn't know about providers at build time. Custom elements
are loaded dynamically at runtime. This is the same pattern as Mining's
`<mbe-mining-dashboard>` — a self-contained web component that talks to the
Gin API via fetch/WS.
### Tray Panel
The systray control pane shows:
- List of registered providers with status indicators
- Start/Stop controls for Manageable providers
- Quick stats for Streamable providers
- Click to open full panel in a new window
## WS Event Protocol
All providers share a single WS hub. Events are namespaced by provider:
```json
{
"type": "brain.recall.complete",
"timestamp": "2026-03-14T10:30:00Z",
"data": { "query": "...", "results": 5 }
}
```
Angular services filter by prefix (`brain.*`, `studio.*`, etc.).
This is identical to Mining's `WebSocketService` pattern but generalised.
## Implementation Phases
### Phase 1: Go Provider Framework (this spec)
- `Provider` interface (extends `RouteGroup`) + `Registry` in `core/go-api/pkg/provider/`
- Providers register through existing `api.Engine` — get middleware, OpenAPI, Swagger for free
- Streamable providers receive WS hub at construction, declare channel prefixes
- **go-process as first provider** — daemon registry, PID files, health checks → `<core-process-panel>`
- Brain as second provider
- core/ide consumes the registry
- Element template: [core-element-template](https://github.com/Snider/core-element-template) — Go CLI + Lit custom element scaffold for new providers
### Phase 2: GUI Consumer
- core/gui discovers Renderable providers
- Dynamic custom element loading in Angular shell
- Tray panel with provider status
- HLCRF layout configuration
### Phase 3: Polyglot Providers
- `ProxyProvider` for PHP/TS providers
- `.core/providers/*.yaml` discovery
- OpenAPI spec → MCP tool auto-generation
- PHP packages (core/php-*) expose providers via FrankenPHP
- TS packages (core/ts) expose providers via CoreDeno
### Phase 4: SDK + Marketplace
- Auto-generate client SDKs from assembled OpenAPI spec
- Provider marketplace (git-based, same pattern as dAppServer)
- Signed provider manifests (ed25519, from `.core/view.yml` spec)
## Files (Phase 1)
### Create in core/go-api
| File | Purpose |
|------|---------|
| `pkg/provider/provider.go` | Provider (extends RouteGroup), Streamable, Describable, Renderable interfaces |
| `pkg/provider/registry.go` | Registry: Add, MountAll(engine), List |
| `pkg/provider/proxy.go` | ProxyProvider for polyglot (Phase 3, stub for now) |
| `pkg/provider/registry_test.go` | Unit tests |
### Update in core/ide
| File | Change |
|------|--------|
| `main.go` | Create registry, register providers, mount router |
## Dependencies
- `core/go-api` — Gin, route groups, OpenAPI (already there)
- `core/go-ws` — WS hub (already there)
- No new external dependencies
## Not In Scope
- Angular component library (Phase 2)
- PHP/TS provider runtime (Phase 3)
- Provider marketplace (Phase 4)
- Authentication/authorisation per provider (future — Authentik integration)