feat(lib): embed Core documentation in workspace template

18 doc files from core/go/docs — getting-started, primitives,
services, commands, configuration, errors, lifecycle, messaging,
subsystems, testing. Agents can read full Core documentation
without network access.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-22 07:19:20 +00:00
parent 5417f547ca
commit e8ca0d856f
18 changed files with 4782 additions and 0 deletions

View file

@ -0,0 +1,177 @@
---
title: Commands
description: Path-based command registration and CLI execution.
---
# Commands
Commands are one of the most AX-native parts of CoreGO. The path is the identity.
## Register a Command
```go
c.Command("deploy/to/homelab", core.Command{
Action: func(opts core.Options) core.Result {
target := opts.String("target")
return core.Result{Value: "deploying to " + target, OK: true}
},
})
```
## Command Paths
Paths must be clean:
- no empty path
- no leading slash
- no trailing slash
- no double slash
These paths are valid:
```text
deploy
deploy/to/homelab
workspace/create
```
These are rejected:
```text
/deploy
deploy/
deploy//to
```
## Parent Commands Are Auto-Created
When you register `deploy/to/homelab`, CoreGO also creates placeholder parents if they do not already exist:
- `deploy`
- `deploy/to`
This makes the path tree navigable without extra setup.
## Read a Command Back
```go
r := c.Command("deploy/to/homelab")
if r.OK {
cmd := r.Value.(*core.Command)
_ = cmd
}
```
## Run a Command Directly
```go
cmd := c.Command("deploy/to/homelab").Value.(*core.Command)
r := cmd.Run(core.Options{
{Key: "target", Value: "uk-prod"},
})
```
If `Action` is nil, `Run` returns `Result{OK:false}` with a structured error.
## Run Through the CLI Surface
```go
r := c.Cli().Run("deploy", "to", "homelab", "--target=uk-prod", "--debug")
```
`Cli.Run` resolves the longest matching command path from the arguments, then converts the remaining args into `core.Options`.
## Flag Parsing Rules
### Double Dash
```text
--target=uk-prod -> key "target", value "uk-prod"
--debug -> key "debug", value true
```
### Single Dash
```text
-v -> key "v", value true
-n=4 -> key "n", value "4"
```
### Positional Arguments
Non-flag arguments after the command path are stored as repeated `_arg` options.
```go
r := c.Cli().Run("workspace", "open", "alpha")
```
That produces an option like:
```go
core.Option{Key: "_arg", Value: "alpha"}
```
### Important Details
- flag values stay as strings
- `opts.Int("port")` only works if some code stored an actual `int`
- invalid flags such as `-verbose` and `--v` are ignored
## Help Output
`Cli.PrintHelp()` prints executable commands:
```go
c.Cli().PrintHelp()
```
It skips:
- hidden commands
- placeholder parents with no `Action` and no `Lifecycle`
Descriptions are resolved through `cmd.I18nKey()`.
## I18n Description Keys
If `Description` is empty, CoreGO derives a key from the path.
```text
deploy -> cmd.deploy.description
deploy/to/homelab -> cmd.deploy.to.homelab.description
workspace/create -> cmd.workspace.create.description
```
If `Description` is already set, CoreGO uses it as-is.
## Lifecycle Commands
Commands can also delegate to a lifecycle implementation.
```go
type daemonCommand struct{}
func (d *daemonCommand) Start(opts core.Options) core.Result { return core.Result{OK: true} }
func (d *daemonCommand) Stop() core.Result { return core.Result{OK: true} }
func (d *daemonCommand) Restart() core.Result { return core.Result{OK: true} }
func (d *daemonCommand) Reload() core.Result { return core.Result{OK: true} }
func (d *daemonCommand) Signal(sig string) core.Result { return core.Result{Value: sig, OK: true} }
c.Command("agent/serve", core.Command{
Lifecycle: &daemonCommand{},
})
```
Important behavior:
- `Start` falls back to `Run` when `Lifecycle` is nil
- `Stop`, `Restart`, `Reload`, and `Signal` return an empty `Result` when `Lifecycle` is nil
## List Command Paths
```go
paths := c.Commands()
```
Like the service registry, the command registry is map-backed, so iteration order is not guaranteed.

View file

@ -0,0 +1,96 @@
---
title: Configuration
description: Constructor options, runtime settings, and feature flags.
---
# Configuration
CoreGO uses two different configuration layers:
- constructor-time `core.Options`
- runtime `c.Config()`
## Constructor-Time Options
```go
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
```
### Current Behavior
- `New` accepts `opts ...Options`
- the current implementation copies only the first `Options` slice
- the `name` key is applied to `c.App().Name`
If you need more constructor data, put it in the first `core.Options` slice.
## Runtime Settings with `Config`
Use `c.Config()` for mutable process settings.
```go
c.Config().Set("workspace.root", "/srv/workspaces")
c.Config().Set("max_agents", 8)
c.Config().Set("debug", true)
```
Read them back with:
```go
root := c.Config().String("workspace.root")
maxAgents := c.Config().Int("max_agents")
debug := c.Config().Bool("debug")
raw := c.Config().Get("workspace.root")
```
### Important Details
- missing keys return zero values
- typed accessors do not coerce strings into ints or bools
- `Get` returns `core.Result`
## Feature Flags
`Config` also tracks named feature flags.
```go
c.Config().Enable("workspace.templates")
c.Config().Enable("agent.review")
c.Config().Disable("agent.review")
```
Read them with:
```go
enabled := c.Config().Enabled("workspace.templates")
features := c.Config().EnabledFeatures()
```
Feature names are case-sensitive.
## `ConfigVar[T]`
Use `ConfigVar[T]` when you need a typed value that can also represent “set versus unset”.
```go
theme := core.NewConfigVar("amber")
if theme.IsSet() {
fmt.Println(theme.Get())
}
theme.Unset()
```
This is useful for package-local state where zero values are not enough to describe configuration presence.
## Recommended Pattern
Use the two layers for different jobs:
- put startup identity such as `name` into `core.Options`
- put mutable runtime values and feature switches into `c.Config()`
That keeps constructor intent separate from live process state.

View file

@ -0,0 +1,120 @@
---
title: Errors
description: Structured errors, logging helpers, and panic recovery.
---
# Errors
CoreGO treats failures as structured operational data.
Repository convention: use `E()` instead of `fmt.Errorf` for framework and service errors.
## `Err`
The structured error type is:
```go
type Err struct {
Operation string
Message string
Cause error
Code string
}
```
## Create Errors
### `E`
```go
err := core.E("workspace.Load", "failed to read workspace manifest", cause)
```
### `Wrap`
```go
err := core.Wrap(cause, "workspace.Load", "manifest parse failed")
```
### `WrapCode`
```go
err := core.WrapCode(cause, "WORKSPACE_INVALID", "workspace.Load", "manifest parse failed")
```
### `NewCode`
```go
err := core.NewCode("NOT_FOUND", "workspace not found")
```
## Inspect Errors
```go
op := core.Operation(err)
code := core.ErrorCode(err)
msg := core.ErrorMessage(err)
root := core.Root(err)
stack := core.StackTrace(err)
pretty := core.FormatStackTrace(err)
```
These helpers keep the operational chain visible without extra type assertions.
## Join and Standard Wrappers
```go
combined := core.ErrorJoin(err1, err2)
same := core.Is(combined, err1)
```
`core.As` and `core.NewError` mirror the standard library for convenience.
## Log-and-Return Helpers
`Core` exposes two convenience wrappers:
```go
r1 := c.LogError(err, "workspace.Load", "workspace load failed")
r2 := c.LogWarn(err, "workspace.Load", "workspace load degraded")
```
These log through the default logger and return `core.Result`.
You can also use the underlying `ErrorLog` directly:
```go
r := c.Log().Error(err, "workspace.Load", "workspace load failed")
```
`Must` logs and then panics when the error is non-nil:
```go
c.Must(err, "workspace.Load", "workspace load failed")
```
## Panic Recovery
`ErrorPanic` handles process-safe panic capture.
```go
defer c.Error().Recover()
```
Run background work with recovery:
```go
c.Error().SafeGo(func() {
panic("captured")
})
```
If `ErrorPanic` has a configured crash file path, it appends JSON crash reports and `Reports(n)` reads them back.
That crash file path is currently internal state on `ErrorPanic`, not a public constructor option on `Core.New()`.
## Logging and Error Context
The logging subsystem automatically extracts `op` and logical stack information from structured errors when those values are present in the key-value list.
That makes errors created with `E`, `Wrap`, or `WrapCode` much easier to follow in logs.

View file

@ -0,0 +1,208 @@
---
title: Getting Started
description: Build a first CoreGO application with the current API.
---
# Getting Started
This page shows the shortest path to a useful CoreGO application using the API that exists in this repository today.
## Install
```bash
go get dappco.re/go/core
```
## Create a Core
`New` takes zero or more `core.Options` slices, but the current implementation only reads the first one. In practice, treat the constructor as `core.New(core.Options{...})`.
```go
package main
import "dappco.re/go/core"
func main() {
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
_ = c
}
```
The `name` option is copied into `c.App().Name`.
## Register a Service
Services are registered explicitly with a name and a `core.Service` DTO.
```go
c.Service("audit", core.Service{
OnStart: func() core.Result {
core.Info("audit service started", "app", c.App().Name)
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("audit service stopped", "app", c.App().Name)
return core.Result{OK: true}
},
})
```
This registry stores `core.Service` values. It is a lifecycle registry, not a typed object container.
## Register a Query, Task, and Command
```go
type workspaceCountQuery struct{}
type createWorkspaceTask struct {
Name string
}
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
switch q.(type) {
case workspaceCountQuery:
return core.Result{Value: 1, OK: true}
}
return core.Result{}
})
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case createWorkspaceTask:
path := "/tmp/agent-workbench/" + task.Name
return core.Result{Value: path, OK: true}
}
return core.Result{}
})
c.Command("workspace/create", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(createWorkspaceTask{
Name: opts.String("name"),
})
},
})
```
## Start the Runtime
```go
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
```
`ServiceStartup` returns `core.Result`, not `error`.
## Run Through the CLI Surface
```go
r := c.Cli().Run("workspace", "create", "--name=alpha")
if r.OK {
fmt.Println("created:", r.Value)
}
```
For flags with values, the CLI stores the value as a string. `--name=alpha` becomes `opts.String("name") == "alpha"`.
## Query the System
```go
count := c.QUERY(workspaceCountQuery{})
if count.OK {
fmt.Println("workspace count:", count.Value)
}
```
## Shut Down Cleanly
```go
_ = c.ServiceShutdown(context.Background())
```
Shutdown cancels `c.Context()`, broadcasts `ActionServiceShutdown{}`, waits for background tasks to finish, and then runs service stop hooks.
## Full Example
```go
package main
import (
"context"
"fmt"
"dappco.re/go/core"
)
type workspaceCountQuery struct{}
type createWorkspaceTask struct {
Name string
}
func main() {
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
c.Config().Set("workspace.root", "/tmp/agent-workbench")
c.Config().Enable("workspace.templates")
c.Service("audit", core.Service{
OnStart: func() core.Result {
core.Info("service started", "service", "audit")
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("service stopped", "service", "audit")
return core.Result{OK: true}
},
})
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
switch q.(type) {
case workspaceCountQuery:
return core.Result{Value: 1, OK: true}
}
return core.Result{}
})
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case createWorkspaceTask:
path := c.Config().String("workspace.root") + "/" + task.Name
return core.Result{Value: path, OK: true}
}
return core.Result{}
})
c.Command("workspace/create", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(createWorkspaceTask{
Name: opts.String("name"),
})
},
})
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
created := c.Cli().Run("workspace", "create", "--name=alpha")
fmt.Println("created:", created.Value)
count := c.QUERY(workspaceCountQuery{})
fmt.Println("workspace count:", count.Value)
_ = c.ServiceShutdown(context.Background())
}
```
## Next Steps
- Read [primitives.md](primitives.md) next so the repeated shapes are clear.
- Read [commands.md](commands.md) if you are building a CLI-first system.
- Read [messaging.md](messaging.md) if services need to collaborate without direct imports.

View file

@ -0,0 +1,112 @@
---
title: CoreGO
description: AX-first documentation for the CoreGO framework.
---
# CoreGO
CoreGO is the foundation layer for the Core ecosystem. It gives you one container, one command tree, one message bus, and a small set of shared primitives that repeat across the whole framework.
The current module path is `dappco.re/go/core`.
## AX View
CoreGO already follows the main AX ideas from RFC-025:
- predictable names such as `Core`, `Service`, `Command`, `Options`, `Result`, `Message`
- path-shaped command registration such as `deploy/to/homelab`
- one repeated input shape (`Options`) and one repeated return shape (`Result`)
- comments and examples that show real usage instead of restating the type signature
## What CoreGO Owns
| Surface | Purpose |
|---------|---------|
| `Core` | Central container and access point |
| `Service` | Managed lifecycle component |
| `Command` | Path-based command tree node |
| `ACTION`, `QUERY`, `PERFORM` | Decoupled communication between components |
| `Data`, `Drive`, `Fs`, `Config`, `I18n`, `Cli` | Built-in subsystems for common runtime work |
| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery |
## Quick Example
```go
package main
import (
"context"
"fmt"
"dappco.re/go/core"
)
type flushCacheTask struct {
Name string
}
func main() {
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result {
core.Info("cache ready", "app", c.App().Name)
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("cache stopped", "app", c.App().Name)
return core.Result{OK: true}
},
})
c.RegisterTask(func(_ *core.Core, task core.Task) core.Result {
switch task.(type) {
case flushCacheTask:
return core.Result{Value: "cache flushed", OK: true}
}
return core.Result{}
})
c.Command("cache/flush", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(flushCacheTask{Name: opts.String("name")})
},
})
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
r := c.Cli().Run("cache", "flush", "--name=session-store")
fmt.Println(r.Value)
_ = c.ServiceShutdown(context.Background())
}
```
## Documentation Paths
| Path | Covers |
|------|--------|
| [getting-started.md](getting-started.md) | First runnable CoreGO app |
| [primitives.md](primitives.md) | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` |
| [services.md](services.md) | Service registry, service locks, runtime helpers |
| [commands.md](commands.md) | Path-based commands and CLI execution |
| [messaging.md](messaging.md) | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` |
| [lifecycle.md](lifecycle.md) | Startup, shutdown, context, background task draining |
| [configuration.md](configuration.md) | Constructor options, config state, feature flags |
| [subsystems.md](subsystems.md) | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` |
| [errors.md](errors.md) | Structured errors, logging helpers, panic recovery |
| [testing.md](testing.md) | Test naming and framework-level testing patterns |
| [pkg/core.md](pkg/core.md) | Package-level reference summary |
| [pkg/log.md](pkg/log.md) | Logging reference for the root package |
| [pkg/PACKAGE_STANDARDS.md](pkg/PACKAGE_STANDARDS.md) | AX package-authoring guidance |
## Good Reading Order
1. Start with [getting-started.md](getting-started.md).
2. Learn the repeated shapes in [primitives.md](primitives.md).
3. Pick the integration path you need next: [services.md](services.md), [commands.md](commands.md), or [messaging.md](messaging.md).
4. Use [subsystems.md](subsystems.md), [errors.md](errors.md), and [testing.md](testing.md) as reference pages while building.

View file

@ -0,0 +1,111 @@
---
title: Lifecycle
description: Startup, shutdown, context ownership, and background task draining.
---
# Lifecycle
CoreGO manages lifecycle through `core.Service` callbacks, not through reflection or implicit interfaces.
## Service Hooks
```go
c.Service("cache", core.Service{
OnStart: func() core.Result {
return core.Result{OK: true}
},
OnStop: func() core.Result {
return core.Result{OK: true}
},
})
```
Only services with `OnStart` appear in `Startables()`. Only services with `OnStop` appear in `Stoppables()`.
## `ServiceStartup`
```go
r := c.ServiceStartup(context.Background(), nil)
```
### What It Does
1. clears the shutdown flag
2. stores a new cancellable context on `c.Context()`
3. runs each `OnStart`
4. broadcasts `ActionServiceStartup{}`
### Failure Behavior
- if the input context is already cancelled, startup returns that error
- if any `OnStart` returns `OK:false`, startup stops immediately and returns that result
## `ServiceShutdown`
```go
r := c.ServiceShutdown(context.Background())
```
### What It Does
1. sets the shutdown flag
2. cancels `c.Context()`
3. broadcasts `ActionServiceShutdown{}`
4. waits for background tasks created by `PerformAsync`
5. runs each `OnStop`
### Failure Behavior
- if draining background tasks hits the shutdown context deadline, shutdown returns that context error
- when service stop hooks fail, CoreGO returns the first error it sees
## Ordering
The current implementation builds `Startables()` and `Stoppables()` by iterating over a map-backed registry.
That means lifecycle order is not guaranteed today.
If your application needs strict startup or shutdown ordering, orchestrate it explicitly inside a smaller number of service callbacks instead of relying on registry order.
## `c.Context()`
`ServiceStartup` creates the context returned by `c.Context()`.
Use it for background work that should stop when the application shuts down:
```go
c.Service("watcher", core.Service{
OnStart: func() core.Result {
go func(ctx context.Context) {
<-ctx.Done()
}(c.Context())
return core.Result{OK: true}
},
})
```
## Built-In Lifecycle Actions
You can listen for lifecycle state changes through the action bus.
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
switch msg.(type) {
case core.ActionServiceStartup:
core.Info("core startup completed")
case core.ActionServiceShutdown:
core.Info("core shutdown started")
}
return core.Result{OK: true}
})
```
## Background Task Draining
`ServiceShutdown` waits for the internal task waitgroup to finish before calling stop hooks.
This is what makes `PerformAsync` safe for long-running work that should complete before teardown.
## `OnReload`
`Service` includes an `OnReload` callback field, but CoreGO does not currently expose a top-level lifecycle runner for reload operations.

View file

@ -0,0 +1,171 @@
---
title: Messaging
description: ACTION, QUERY, QUERYALL, PERFORM, and async task flow.
---
# Messaging
CoreGO uses one message bus for broadcasts, lookups, and work dispatch.
## Message Types
```go
type Message any
type Query any
type Task any
```
Your own structs define the protocol.
```go
type repositoryIndexed struct {
Name string
}
type repositoryCountQuery struct{}
type syncRepositoryTask struct {
Name string
}
```
## `ACTION`
`ACTION` is a broadcast.
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
switch m := msg.(type) {
case repositoryIndexed:
core.Info("repository indexed", "name", m.Name)
return core.Result{OK: true}
}
return core.Result{OK: true}
})
r := c.ACTION(repositoryIndexed{Name: "core-go"})
```
### Behavior
- all registered action handlers are called in their current registration order
- if a handler returns `OK:false`, dispatch stops and that `Result` is returned
- if no handler fails, `ACTION` returns `Result{OK:true}`
## `QUERY`
`QUERY` is first-match request-response.
```go
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
switch q.(type) {
case repositoryCountQuery:
return core.Result{Value: 42, OK: true}
}
return core.Result{}
})
r := c.QUERY(repositoryCountQuery{})
```
### Behavior
- handlers run until one returns `OK:true`
- the first successful result wins
- if nothing handles the query, CoreGO returns an empty `Result`
## `QUERYALL`
`QUERYALL` collects every successful non-nil response.
```go
r := c.QUERYALL(repositoryCountQuery{})
results := r.Value.([]any)
```
### Behavior
- every query handler is called
- only `OK:true` results with non-nil `Value` are collected
- the call itself returns `OK:true` even when the result list is empty
## `PERFORM`
`PERFORM` dispatches a task to the first handler that accepts it.
```go
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case syncRepositoryTask:
return core.Result{Value: "synced " + task.Name, OK: true}
}
return core.Result{}
})
r := c.PERFORM(syncRepositoryTask{Name: "core-go"})
```
### Behavior
- handlers run until one returns `OK:true`
- the first successful result wins
- if nothing handles the task, CoreGO returns an empty `Result`
## `PerformAsync`
`PerformAsync` runs a task in a background goroutine and returns a generated task identifier.
```go
r := c.PerformAsync(syncRepositoryTask{Name: "core-go"})
taskID := r.Value.(string)
```
### Generated Events
Async execution emits three action messages:
| Message | When |
|---------|------|
| `ActionTaskStarted` | just before background execution begins |
| `ActionTaskProgress` | whenever `Progress` is called |
| `ActionTaskCompleted` | after the task finishes or panics |
Example listener:
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
switch m := msg.(type) {
case core.ActionTaskCompleted:
core.Info("task completed", "task", m.TaskIdentifier, "err", m.Error)
}
return core.Result{OK: true}
})
```
## Progress Updates
```go
c.Progress(taskID, 0.5, "indexing commits", syncRepositoryTask{Name: "core-go"})
```
That broadcasts `ActionTaskProgress`.
## `TaskWithIdentifier`
Tasks that implement `TaskWithIdentifier` receive the generated ID before dispatch.
```go
type trackedTask struct {
ID string
Name string
}
func (t *trackedTask) SetTaskIdentifier(id string) { t.ID = id }
func (t *trackedTask) GetTaskIdentifier() string { return t.ID }
```
## Shutdown Interaction
When shutdown has started, `PerformAsync` returns an empty `Result` instead of scheduling more work.
This is why `ServiceShutdown` can safely drain the outstanding background tasks before stopping services.

View file

@ -0,0 +1,138 @@
# AX Package Standards
This page describes how to build packages on top of CoreGO in the style described by RFC-025.
## 1. Prefer Predictable Names
Use names that tell an agent what the thing is without translation.
Good:
- `RepositoryService`
- `RepositoryServiceOptions`
- `WorkspaceCountQuery`
- `SyncRepositoryTask`
Avoid shortening names unless the abbreviation is already universal.
## 2. Put Real Usage in Comments
Write comments that show a real call with realistic values.
Good:
```go
// Sync a repository into the local workspace cache.
// svc.SyncRepository("core-go", "/srv/repos/core-go")
```
Avoid comments that only repeat the signature.
## 3. Keep Paths Semantic
If a command or template lives at a path, let the path explain the intent.
Good:
```text
deploy/to/homelab
workspace/create
template/workspace/go
```
That keeps the CLI, tests, docs, and message vocabulary aligned.
## 4. Reuse CoreGO Primitives
At Core boundaries, prefer the shared shapes:
- `core.Options` for lightweight input
- `core.Result` for output
- `core.Service` for lifecycle registration
- `core.Message`, `core.Query`, `core.Task` for bus protocols
Inside your package, typed structs are still good. Use `ServiceRuntime[T]` when you want typed package options plus a `Core` reference.
```go
type repositoryServiceOptions struct {
BaseDirectory string
}
type repositoryService struct {
*core.ServiceRuntime[repositoryServiceOptions]
}
```
## 5. Prefer Explicit Registration
Register services and commands with names and paths that stay readable in grep results.
```go
c.Service("repository", core.Service{...})
c.Command("repository/sync", core.Command{...})
```
## 6. Use the Bus for Decoupling
When one package needs another packages behavior, prefer queries and tasks over tight package coupling.
```go
type repositoryCountQuery struct{}
type syncRepositoryTask struct {
Name string
}
```
That keeps the protocol visible in code and easy for agents to follow.
## 7. Use Structured Errors
Use `core.E`, `core.Wrap`, and `core.WrapCode`.
```go
return core.Result{
Value: core.E("repository.Sync", "git fetch failed", err),
OK: false,
}
```
Do not introduce free-form `fmt.Errorf` chains in framework code.
## 8. Keep Testing Names Predictable
Follow the repository pattern:
- `_Good`
- `_Bad`
- `_Ugly`
Example:
```go
func TestRepositorySync_Good(t *testing.T) {}
func TestRepositorySync_Bad(t *testing.T) {}
func TestRepositorySync_Ugly(t *testing.T) {}
```
## 9. Prefer Stable Shapes Over Clever APIs
For package APIs, avoid patterns that force an agent to infer too much hidden control flow.
Prefer:
- clear structs
- explicit names
- path-based commands
- visible message types
Avoid:
- implicit global state unless it is truly a default service
- panic-hiding constructors
- dense option chains when a small explicit struct would do
## 10. Document the Current Reality
If the implementation is in transition, document what the code does now, not the API shape you plan to have later.
That keeps agents correct on first pass, which is the real AX metric.

View file

@ -0,0 +1,81 @@
# Package Reference: `core`
Import path:
```go
import "dappco.re/go/core"
```
This repository exposes one root package. The main areas are:
## Constructors and Accessors
| Name | Purpose |
|------|---------|
| `New` | Create a `*Core` |
| `NewRuntime` | Create an empty runtime wrapper |
| `NewWithFactories` | Create a runtime wrapper from named service factories |
| `Options`, `App`, `Data`, `Drive`, `Fs`, `Config`, `Error`, `Log`, `Cli`, `IPC`, `I18n`, `Context` | Access the built-in subsystems |
## Core Primitives
| Name | Purpose |
|------|---------|
| `Option`, `Options` | Input configuration and metadata |
| `Result` | Shared output shape |
| `Service` | Lifecycle DTO |
| `Command` | Command tree node |
| `Message`, `Query`, `Task` | Message bus payload types |
## Service and Runtime APIs
| Name | Purpose |
|------|---------|
| `Service` | Register or read a named service |
| `Services` | List registered service names |
| `Startables`, `Stoppables` | Snapshot lifecycle-capable services |
| `LockEnable`, `LockApply` | Activate the service registry lock |
| `ServiceRuntime[T]` | Helper for package authors |
## Command and CLI APIs
| Name | Purpose |
|------|---------|
| `Command` | Register or read a command by path |
| `Commands` | List command paths |
| `Cli().Run` | Resolve arguments to a command and execute it |
| `Cli().PrintHelp` | Show executable commands |
## Messaging APIs
| Name | Purpose |
|------|---------|
| `ACTION`, `Action` | Broadcast a message |
| `QUERY`, `Query` | Return the first successful query result |
| `QUERYALL`, `QueryAll` | Collect all successful query results |
| `PERFORM`, `Perform` | Run the first task handler that accepts the task |
| `PerformAsync` | Run a task in the background |
| `Progress` | Broadcast async task progress |
| `RegisterAction`, `RegisterActions`, `RegisterQuery`, `RegisterTask` | Register bus handlers |
## Subsystems
| Name | Purpose |
|------|---------|
| `Config` | Runtime settings and feature flags |
| `Data` | Embedded filesystem mounts |
| `Drive` | Named transport handles |
| `Fs` | Local filesystem operations |
| `I18n` | Locale collection and translation delegation |
| `App`, `Find` | Application identity and executable lookup |
## Errors and Logging
| Name | Purpose |
|------|---------|
| `E`, `Wrap`, `WrapCode`, `NewCode` | Structured error creation |
| `Operation`, `ErrorCode`, `ErrorMessage`, `Root`, `StackTrace`, `FormatStackTrace` | Error inspection |
| `NewLog`, `Default`, `SetDefault`, `SetLevel`, `SetRedactKeys` | Logger creation and defaults |
| `LogErr`, `LogPanic`, `ErrorLog`, `ErrorPanic` | Error-aware logging and panic recovery |
Use the top-level docs in `docs/` for task-oriented guidance, then use this page as a compact reference.

View file

@ -0,0 +1,83 @@
# Logging Reference
Logging lives in the root `core` package in this repository. There is no separate `pkg/log` import path here.
## Create a Logger
```go
logger := core.NewLog(core.LogOptions{
Level: core.LevelInfo,
})
```
## Levels
| Level | Meaning |
|-------|---------|
| `LevelQuiet` | no output |
| `LevelError` | errors and security events |
| `LevelWarn` | warnings, errors, security events |
| `LevelInfo` | informational, warnings, errors, security events |
| `LevelDebug` | everything |
## Log Methods
```go
logger.Debug("workspace discovered", "path", "/srv/workspaces")
logger.Info("service started", "service", "audit")
logger.Warn("retrying fetch", "attempt", 2)
logger.Error("fetch failed", "err", err)
logger.Security("sandbox escape detected", "path", attemptedPath)
```
## Default Logger
The package owns a default logger.
```go
core.SetLevel(core.LevelDebug)
core.SetRedactKeys("token", "password")
core.Info("service started", "service", "audit")
```
## Redaction
Values for keys listed in `RedactKeys` are replaced with `[REDACTED]`.
```go
logger.SetRedactKeys("token")
logger.Info("login", "user", "cladius", "token", "secret-value")
```
## Output and Rotation
```go
logger := core.NewLog(core.LogOptions{
Level: core.LevelInfo,
Output: os.Stderr,
})
```
If you provide `Rotation` and set `RotationWriterFactory`, the logger writes to the rotating writer instead of the plain output stream.
## Error-Aware Logging
`LogErr` extracts structured error context before logging:
```go
le := core.NewLogErr(logger)
le.Log(err)
```
`ErrorLog` is the log-and-return wrapper exposed through `c.Log()`.
## Panic-Aware Logging
`LogPanic` is the lightweight panic logger:
```go
defer core.NewLogPanic(logger).Recover()
```
It logs the recovered panic but does not manage crash files. For crash reports, use `c.Error().Recover()`.

View file

@ -0,0 +1,261 @@
# Lint Pattern Catalog & Polish Skill Design
> **Partial implementation (14 Mar 2026):** Layer 1 (`core/lint` -- catalog, matcher, scanner, CLI) is fully implemented and documented at `docs/tools/lint/index.md`. Layer 2 (MCP subsystem in `go-ai`) and Layer 3 (Claude Code polish skill in `core/agent`) are NOT implemented. This plan is retained for those remaining layers.
**Goal:** A structured pattern catalog (`core/lint`) that captures recurring code quality findings as regex rules, exposes them via MCP tools in `go-ai`, and orchestrates multi-AI code review via a Claude Code skill in `core/agent`.
**Architecture:** Three layers — a standalone catalog+matcher library (`core/lint`), an MCP subsystem in `go-ai` that exposes lint tools to agents, and a Claude Code plugin in `core/agent` that orchestrates the "polish" workflow (deterministic checks + AI reviewers + feedback loop into the catalog).
**Tech Stack:** Go (catalog, matcher, CLI, MCP subsystem), YAML (rule definitions), JSONL (findings output, compatible with `~/.core/ai/metrics/`), Claude Code plugin format (hooks.json, commands/*.md, plugin.json).
---
## Context
During a code review sweep of 18 Go repos (March 2026), AI reviewers (Gemini, Claude) found ~20 recurring patterns: SQL injection, path traversal, XSS, missing constant-time comparison, goroutine leaks, Go 1.26 modernisation opportunities, and more. Many of these patterns repeat across repos.
Currently these findings exist only as commit messages. This design captures them as a reusable, machine-readable catalog that:
1. Deterministic tools can run immediately (regex matching)
2. MCP-connected agents can query and apply
3. LEM models can train on for "does this comply with CoreGo standards?" judgements
4. Grows automatically as AI reviewers find new patterns
## Layer 1: `core/lint` — Pattern Catalog & Matcher
### Repository Structure
```
core/lint/
├── go.mod # forge.lthn.ai/core/lint
├── catalog/
│ ├── go-security.yaml # SQL injection, path traversal, XSS, constant-time
│ ├── go-modernise.yaml # Go 1.26: slices.Clone, wg.Go, maps.Keys, range-over-int
│ ├── go-correctness.yaml # Deadlocks, goroutine leaks, nil guards, error handling
│ ├── php-security.yaml # XSS, CSRF, mass assignment, SQL injection
│ ├── ts-security.yaml # DOM XSS, prototype pollution
│ └── cpp-safety.yaml # Buffer overflow, use-after-free
├── pkg/lint/
│ ├── catalog.go # Load + parse YAML catalog files
│ ├── rule.go # Rule struct definition
│ ├── matcher.go # Regex matcher against file contents
│ ├── report.go # Structured findings output (JSON/JSONL/text)
│ ├── catalog_test.go
│ ├── matcher_test.go
│ └── report_test.go
├── cmd/core-lint/
│ └── main.go # `core-lint check ./...` CLI
└── .core/
└── build.yaml # Produces core-lint binary
```
### Rule Schema (YAML)
```yaml
- id: go-sec-001
title: "SQL wildcard injection in LIKE clauses"
severity: high # critical, high, medium, low, info
languages: [go]
tags: [security, injection, owasp-a03]
pattern: 'LIKE\s+\?\s*,\s*["\x60]%\s*\+'
exclude_pattern: 'EscapeLike' # suppress if this also matches
fix: "Use parameterised LIKE with explicit escaping of % and _ characters"
found_in: [go-store] # repos where first discovered
example_bad: |
db.Where("name LIKE ?", "%"+input+"%")
example_good: |
db.Where("name LIKE ?", EscapeLike(input))
first_seen: "2026-03-09"
detection: regex # future: ast, semantic
auto_fixable: false # future: true when we add codemods
```
### Rule Struct (Go)
```go
type Rule struct {
ID string `yaml:"id"`
Title string `yaml:"title"`
Severity string `yaml:"severity"`
Languages []string `yaml:"languages"`
Tags []string `yaml:"tags"`
Pattern string `yaml:"pattern"`
ExcludePattern string `yaml:"exclude_pattern,omitempty"`
Fix string `yaml:"fix"`
FoundIn []string `yaml:"found_in,omitempty"`
ExampleBad string `yaml:"example_bad,omitempty"`
ExampleGood string `yaml:"example_good,omitempty"`
FirstSeen string `yaml:"first_seen"`
Detection string `yaml:"detection"` // regex | ast | semantic
AutoFixable bool `yaml:"auto_fixable"`
}
```
### Finding Struct (Go)
Designed to align with go-ai's `ScanAlert` shape and `~/.core/ai/metrics/` JSONL format:
```go
type Finding struct {
RuleID string `json:"rule_id"`
Title string `json:"title"`
Severity string `json:"severity"`
File string `json:"file"`
Line int `json:"line"`
Match string `json:"match"` // matched text
Fix string `json:"fix"`
Repo string `json:"repo,omitempty"`
}
```
### CLI Interface
```bash
# Check current directory against all catalogs for detected languages
core-lint check ./...
# Check specific languages/catalogs
core-lint check --lang go --catalog go-security ./pkg/...
# Output as JSON (for piping to other tools)
core-lint check --format json ./...
# List available rules
core-lint catalog list
core-lint catalog list --lang go --severity high
# Show a specific rule with examples
core-lint catalog show go-sec-001
```
## Layer 2: `go-ai` Lint MCP Subsystem
New subsystem registered alongside files/rag/ml/brain:
```go
type LintSubsystem struct {
catalog *lint.Catalog
root string // workspace root for scanning
}
func (s *LintSubsystem) Name() string { return "lint" }
func (s *LintSubsystem) RegisterTools(server *mcp.Server) {
// lint_check - run rules against workspace files
// lint_catalog - list/search available rules
// lint_report - get findings summary for a path
}
```
### MCP Tools
| Tool | Input | Output | Group |
|------|-------|--------|-------|
| `lint_check` | `{path: string, lang?: string, severity?: string}` | `{findings: []Finding}` | lint |
| `lint_catalog` | `{lang?: string, tags?: []string, severity?: string}` | `{rules: []Rule}` | lint |
| `lint_report` | `{path: string, format?: "summary" or "detailed"}` | `{summary: ReportSummary}` | lint |
This means any MCP-connected agent (Claude, LEM, Codex) can call `lint_check` to scan code against the catalog.
## Layer 3: `core/agent` Polish Skill
Claude Code plugin at `core/agent/claude/polish/`:
```
core/agent/claude/polish/
├── plugin.json
├── hooks.json # optional: PostToolUse after git commit
├── commands/
│ └── polish.md # /polish slash command
└── scripts/
└── run-lint.sh # shells out to core-lint
```
### `/polish` Command Flow
1. Run `core-lint check ./...` for fast deterministic findings
2. Report findings to user
3. Optionally run AI reviewers (Gemini CLI, Codex) for deeper analysis
4. Deduplicate AI findings against catalog (already-known patterns)
5. Propose new patterns as catalog additions (PR to core/lint)
### Subagent Configuration (`.core/agents/`)
Repos can configure polish behaviour:
```yaml
# any-repo/.core/agents/polish.yaml
languages: [go]
catalogs: [go-security, go-modernise, go-correctness]
reviewers: [gemini] # which AI tools to invoke
exclude: [vendor/, testdata/, *_test.go]
severity_threshold: medium # only report medium+ findings
```
## Findings to LEM Pipeline
```
core-lint check -> findings.json
|
v
~/.core/ai/metrics/YYYY-MM-DD.jsonl (audit trail)
|
v
LEM training data:
- Rule examples (bad/good pairs) -> supervised training signal
- Finding frequency -> pattern importance weighting
- Rule descriptions -> natural language understanding of "why"
|
v
LEM tool: "does this code comply with CoreGo standards?"
-> queries lint_catalog via MCP
-> applies learned pattern recognition
-> reports violations with rule IDs and fixes
```
## Initial Catalog Seed
From the March 2026 ecosystem sweep:
| ID | Title | Severity | Language | Found In |
|----|-------|----------|----------|----------|
| go-sec-001 | SQL wildcard injection | high | go | go-store |
| go-sec-002 | Path traversal in cache keys | high | go | go-cache |
| go-sec-003 | XSS in HTML output | high | go | go-html |
| go-sec-004 | Non-constant-time auth comparison | high | go | go-crypt |
| go-sec-005 | Log injection via unescaped input | medium | go | go-log |
| go-sec-006 | Key material in log output | high | go | go-log |
| go-cor-001 | Goroutine leak (no WaitGroup) | high | go | core/go |
| go-cor-002 | Shutdown deadlock (wg.Wait no timeout) | high | go | core/go |
| go-cor-003 | Silent error swallowing | medium | go | go-process, go-ratelimit |
| go-cor-004 | Panic in library code | medium | go | go-i18n |
| go-cor-005 | Delete without path validation | high | go | go-io |
| go-mod-001 | Manual slice clone (append nil pattern) | low | go | core/go |
| go-mod-002 | Manual sort instead of slices.Sorted | low | go | core/go |
| go-mod-003 | Manual reverse loop instead of slices.Backward | low | go | core/go |
| go-mod-004 | sync.WaitGroup Add+Done instead of Go() | low | go | core/go |
| go-mod-005 | Manual map key collection instead of maps.Keys | low | go | core/go |
| go-cor-006 | Missing error return from API calls | medium | go | go-forge, go-git |
| go-cor-007 | Signal handler uses wrong type | medium | go | go-process |
## Dependencies
```
core/lint (standalone, zero core deps)
^
|
go-ai/mcp/lint/ (imports core/lint for catalog + matcher)
^
|
core/agent/claude/polish/ (shells out to core-lint CLI)
```
`core/lint` has no dependency on `core/go` or any other framework module. It is a standalone library + CLI, like `core/go-io`.
## Future Extensions (Not Built Now)
- **AST-based detection** (layer 2): Parse Go/PHP AST, match structural patterns
- **Semantic detection** (layer 3): LEM judges code against rule descriptions
- **Auto-fix codemods**: `core-lint fix` applies known fixes automatically
- **CI integration**: GitHub Actions workflow runs `core-lint check` on PRs
- **CodeRabbit integration**: Import CodeRabbit findings as catalog entries
- **Cross-repo dashboard**: Aggregate findings across all repos in workspace

View file

@ -0,0 +1,160 @@
# AltumCode Update Checker — Design
> **Note:** Layer 1 (version detection via PHP artisan) is implemented and documented at `docs/docs/php/packages/uptelligence.md`. Layer 2 (browser-automated downloads via Claude Code skill) is NOT yet implemented.
## Problem
Host UK runs 4 AltumCode SaaS products and 13 plugins across two marketplaces (CodeCanyon + LemonSqueezy). Checking for updates and downloading them is a manual process: ~50 clicks across two marketplace UIs, moving 16+ zip files, extracting to the right directories. This eats a morning of momentum every update cycle.
## Solution
Two-layer system: lightweight version detection (PHP artisan command) + browser-automated download (Claude Code skill).
## Architecture
```
Layer 1: Detection (core/php-uptelligence)
artisan uptelligence:check-updates
5 HTTP GETs, no auth, schedulable
Compares remote vs deployed versions
Layer 2: Download (Claude Code skill)
Playwright → LemonSqueezy (16 items)
Claude in Chrome → CodeCanyon (2 items)
Downloads zips to staging folder
Extracts to saas/services/{product}/package/
Layer 3: Deploy (existing — manual)
docker build → scp → deploy_saas.yml
Human in the loop
```
## Layer 1: Version Detection
### Public Endpoints (no auth required)
| Endpoint | Returns |
|----------|---------|
| `GET https://66analytics.com/info.php` | `{"latest_release_version": "66.0.0", "latest_release_version_code": 6600}` |
| `GET https://66biolinks.com/info.php` | Same format |
| `GET https://66pusher.com/info.php` | Same format |
| `GET https://66socialproof.com/info.php` | Same format |
| `GET https://dev.altumcode.com/plugins-versions` | `{"affiliate": {"version": "2.0.1"}, "ultimate-blocks": {"version": "9.1.0"}, ...}` |
### Deployed Version Sources
- **Product version**: `PRODUCT_CODE` constant in deployed source `config.php`
- **Plugin versions**: `version` field in each plugin's `config.php` or `config.json`
### Artisan Command
`php artisan uptelligence:check-updates`
Output:
```
Product Deployed Latest Status
──────────────────────────────────────────────
66analytics 65.0.0 66.0.0 UPDATE AVAILABLE
66biolinks 65.0.0 66.0.0 UPDATE AVAILABLE
66pusher 65.0.0 65.0.0 ✓ current
66socialproof 65.0.0 66.0.0 UPDATE AVAILABLE
Plugin Deployed Latest Status
──────────────────────────────────────────────
affiliate 2.0.0 2.0.1 UPDATE AVAILABLE
ultimate-blocks 9.1.0 9.1.0 ✓ current
...
```
Lives in `core/php-uptelligence` as a scheduled check or on-demand command.
## Layer 2: Browser-Automated Download
### Claude Code Skill: `/update-altum`
Workflow:
1. Run version check (Layer 1) — show what needs updating
2. Ask for confirmation before downloading
3. Download from both marketplaces
4. Extract to staging directories
5. Report what changed
### Marketplace Access
**LemonSqueezy (Playwright)**
- Auth: Magic link email to `snider@lt.hn` — user taps on phone
- Flow per item: Navigate to order detail → click "Download" button
- 16 items across 2 pages of orders
- Session persists for the skill invocation
**CodeCanyon (Claude in Chrome)**
- Auth: Saved browser session cookies (user `snidered`)
- Flow per item: Click "Download" dropdown → "All files & documentation"
- 2 items on downloads page
### Product-to-Marketplace Mapping
| Product | CodeCanyon | LemonSqueezy |
|---------|-----------|--------------|
| 66biolinks | Regular licence | Extended licence (66biolinks custom, $359.28) |
| 66socialproof | Regular licence | — |
| 66analytics | — | Regular licence |
| 66pusher | — | Regular licence |
### Plugin Inventory (all LemonSqueezy)
| Plugin | Price | Applies To |
|--------|-------|------------|
| Pro Notifications | $58.80 | 66socialproof |
| Teams Plugin | $58.80 | All products |
| Push Notifications Plugin | $46.80 | All products |
| Ultimate Blocks | $32.40 | 66biolinks |
| Pro Blocks | $32.40 | 66biolinks |
| Payment Blocks | $32.40 | 66biolinks |
| Affiliate Plugin | $32.40 | All products |
| PWA Plugin | $25.20 | All products |
| Image Optimizer Plugin | $19.20 | All products |
| Email Shield Plugin | FREE | All products |
| Dynamic OG images plugin | FREE | 66biolinks |
| Offload & CDN Plugin | FREE | All products (gift from Altum) |
### Staging & Extraction
- Download to: `~/Code/lthn/saas/updates/YYYY-MM-DD/`
- Products extract to: `~/Code/lthn/saas/services/{product}/package/product/`
- Plugins extract to: `~/Code/lthn/saas/services/{product}/package/product/plugins/{plugin_id}/`
## LemonSqueezy Order UUIDs
Stable order URLs for direct navigation:
| Product | Order URL |
|---------|-----------|
| 66analytics | `/my-orders/2972471f-abac-4165-b78d-541b176de180` |
(Remaining UUIDs to be captured on first full run of the skill.)
## Out of Scope
- No auto-deploy to production (human runs `deploy_saas.yml`)
- No licence key handling or financial transactions
- No AltumCode Club membership management
- No Blesta updates (different vendor)
- No update SQL migration execution (handled by AltumCode's own update scripts)
## Key Technical Details
- AltumCode products use Unirest HTTP client for API calls
- Product `info.php` endpoints are public, no rate limiting observed
- Plugin versions endpoint (`dev.altumcode.com`) is also public
- Production Docker images have `/install/` and `/update/` directories stripped
- Updates require full Docker image rebuild and redeployment via Ansible
- CodeCanyon download URLs contain stable purchase UUIDs
- LemonSqueezy uses magic link auth (no password, email-based)
- Playwright can access LemonSqueezy; Claude in Chrome cannot (payment platform safety block)
## Workflow Summary
**Before**: Get email from AltumCode → log into 2 marketplaces → click through 18 products/plugins → download 16+ zips → extract to right directories → rebuild Docker images → deploy. Half a morning.
**After**: Run `artisan uptelligence:check-updates` → see what's behind → invoke `/update-altum` → tap magic link on phone → go make coffee → come back to staged files → `deploy_saas.yml`. 10 minutes of human time.

View file

@ -0,0 +1,799 @@
# AltumCode Update Checker Implementation Plan
> **Note:** Layer 1 (Tasks 1-2, 4: version checking + seeder + sync command) is implemented and documented at `docs/docs/php/packages/uptelligence.md`. Task 3 (Claude Code browser skill for Layer 2 downloads) is NOT yet implemented.
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add AltumCode product + plugin version checking to uptelligence, and create a Claude Code skill for browser-automated downloads from LemonSqueezy and CodeCanyon.
**Architecture:** Extend the existing `VendorUpdateCheckerService` to handle `PLATFORM_ALTUM` vendors via 5 public HTTP endpoints. Seed the vendors table with all 4 products and 13 plugins. Create a Claude Code plugin skill that uses Playwright (LemonSqueezy) and Chrome (CodeCanyon) to download updates.
**Tech Stack:** PHP 8.4, Laravel, Pest, Claude Code plugins (Playwright MCP + Chrome MCP)
---
### Task 1: Add AltumCode check to VendorUpdateCheckerService
**Files:**
- Modify: `/Users/snider/Code/core/php-uptelligence/Services/VendorUpdateCheckerService.php`
- Test: `/Users/snider/Code/core/php-uptelligence/tests/Unit/AltumCodeCheckerTest.php`
**Step 1: Write the failing test**
Create `/Users/snider/Code/core/php-uptelligence/tests/Unit/AltumCodeCheckerTest.php`:
```php
<?php
declare(strict_types=1);
use Core\Mod\Uptelligence\Models\Vendor;
use Core\Mod\Uptelligence\Services\VendorUpdateCheckerService;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
$this->service = app(VendorUpdateCheckerService::class);
});
it('checks altum product version via info.php', function () {
Http::fake([
'https://66analytics.com/info.php' => Http::response([
'latest_release_version' => '66.0.0',
'latest_release_version_code' => 6600,
]),
]);
$vendor = Vendor::factory()->create([
'slug' => '66analytics',
'name' => '66analytics',
'source_type' => Vendor::SOURCE_LICENSED,
'plugin_platform' => Vendor::PLATFORM_ALTUM,
'current_version' => '65.0.0',
'is_active' => true,
]);
$result = $this->service->checkVendor($vendor);
expect($result['status'])->toBe('success')
->and($result['current'])->toBe('65.0.0')
->and($result['latest'])->toBe('66.0.0')
->and($result['has_update'])->toBeTrue();
});
it('reports no update when altum product is current', function () {
Http::fake([
'https://66analytics.com/info.php' => Http::response([
'latest_release_version' => '65.0.0',
'latest_release_version_code' => 6500,
]),
]);
$vendor = Vendor::factory()->create([
'slug' => '66analytics',
'name' => '66analytics',
'source_type' => Vendor::SOURCE_LICENSED,
'plugin_platform' => Vendor::PLATFORM_ALTUM,
'current_version' => '65.0.0',
'is_active' => true,
]);
$result = $this->service->checkVendor($vendor);
expect($result['has_update'])->toBeFalse();
});
it('checks altum plugin versions via plugins-versions endpoint', function () {
Http::fake([
'https://dev.altumcode.com/plugins-versions' => Http::response([
'affiliate' => ['version' => '2.0.1'],
'teams' => ['version' => '3.0.0'],
]),
]);
$vendor = Vendor::factory()->create([
'slug' => 'altum-plugin-affiliate',
'name' => 'Affiliate Plugin',
'source_type' => Vendor::SOURCE_PLUGIN,
'plugin_platform' => Vendor::PLATFORM_ALTUM,
'current_version' => '2.0.0',
'is_active' => true,
]);
$result = $this->service->checkVendor($vendor);
expect($result['status'])->toBe('success')
->and($result['latest'])->toBe('2.0.1')
->and($result['has_update'])->toBeTrue();
});
it('handles altum info.php timeout gracefully', function () {
Http::fake([
'https://66analytics.com/info.php' => Http::response('', 500),
]);
$vendor = Vendor::factory()->create([
'slug' => '66analytics',
'name' => '66analytics',
'source_type' => Vendor::SOURCE_LICENSED,
'plugin_platform' => Vendor::PLATFORM_ALTUM,
'current_version' => '65.0.0',
'is_active' => true,
]);
$result = $this->service->checkVendor($vendor);
expect($result['status'])->toBe('error')
->and($result['has_update'])->toBeFalse();
});
```
**Step 2: Run test to verify it fails**
Run: `cd /Users/snider/Code/core/php-uptelligence && composer test -- --filter=AltumCodeChecker`
Expected: FAIL — altum vendors still hit `skipCheck()`
**Step 3: Write minimal implementation**
In `/Users/snider/Code/core/php-uptelligence/Services/VendorUpdateCheckerService.php`, modify `checkVendor()` to route altum vendors:
```php
public function checkVendor(Vendor $vendor): array
{
$result = match (true) {
$this->isAltumPlatform($vendor) && $vendor->isLicensed() => $this->checkAltumProduct($vendor),
$this->isAltumPlatform($vendor) && $vendor->isPlugin() => $this->checkAltumPlugin($vendor),
$vendor->isOss() && $this->isGitHubUrl($vendor->git_repo_url) => $this->checkGitHub($vendor),
$vendor->isOss() && $this->isGiteaUrl($vendor->git_repo_url) => $this->checkGitea($vendor),
default => $this->skipCheck($vendor),
};
// ... rest unchanged
}
```
Add the three new methods:
```php
/**
* Check if vendor is on the AltumCode platform.
*/
protected function isAltumPlatform(Vendor $vendor): bool
{
return $vendor->plugin_platform === Vendor::PLATFORM_ALTUM;
}
/**
* AltumCode product info endpoint mapping.
*/
protected function getAltumProductInfoUrl(Vendor $vendor): ?string
{
$urls = [
'66analytics' => 'https://66analytics.com/info.php',
'66biolinks' => 'https://66biolinks.com/info.php',
'66pusher' => 'https://66pusher.com/info.php',
'66socialproof' => 'https://66socialproof.com/info.php',
];
return $urls[$vendor->slug] ?? null;
}
/**
* Check an AltumCode product for updates via its info.php endpoint.
*/
protected function checkAltumProduct(Vendor $vendor): array
{
$url = $this->getAltumProductInfoUrl($vendor);
if (! $url) {
return $this->errorResult("No info.php URL mapped for {$vendor->slug}");
}
try {
$response = Http::timeout(5)->get($url);
if (! $response->successful()) {
return $this->errorResult("AltumCode info.php returned {$response->status()}");
}
$data = $response->json();
$latestVersion = $data['latest_release_version'] ?? null;
if (! $latestVersion) {
return $this->errorResult('No version in info.php response');
}
return $this->buildResult(
vendor: $vendor,
latestVersion: $this->normaliseVersion($latestVersion),
releaseInfo: [
'version_code' => $data['latest_release_version_code'] ?? null,
'source' => $url,
]
);
} catch (\Exception $e) {
return $this->errorResult("AltumCode check failed: {$e->getMessage()}");
}
}
/**
* Check an AltumCode plugin for updates via the central plugins-versions endpoint.
*/
protected function checkAltumPlugin(Vendor $vendor): array
{
try {
$allPlugins = $this->getAltumPluginVersions();
if ($allPlugins === null) {
return $this->errorResult('Failed to fetch AltumCode plugin versions');
}
// Extract the plugin_id from the vendor slug (strip 'altum-plugin-' prefix)
$pluginId = str_replace('altum-plugin-', '', $vendor->slug);
if (! isset($allPlugins[$pluginId])) {
return $this->errorResult("Plugin '{$pluginId}' not found in AltumCode registry");
}
$latestVersion = $allPlugins[$pluginId]['version'] ?? null;
return $this->buildResult(
vendor: $vendor,
latestVersion: $this->normaliseVersion($latestVersion),
releaseInfo: ['source' => 'dev.altumcode.com/plugins-versions']
);
} catch (\Exception $e) {
return $this->errorResult("AltumCode plugin check failed: {$e->getMessage()}");
}
}
/**
* Fetch all AltumCode plugin versions (cached for 1 hour within a check run).
*/
protected ?array $altumPluginVersionsCache = null;
protected function getAltumPluginVersions(): ?array
{
if ($this->altumPluginVersionsCache !== null) {
return $this->altumPluginVersionsCache;
}
$response = Http::timeout(5)->get('https://dev.altumcode.com/plugins-versions');
if (! $response->successful()) {
return null;
}
$this->altumPluginVersionsCache = $response->json();
return $this->altumPluginVersionsCache;
}
```
**Step 4: Run test to verify it passes**
Run: `cd /Users/snider/Code/core/php-uptelligence && composer test -- --filter=AltumCodeChecker`
Expected: PASS (4 tests)
**Step 5: Commit**
```bash
cd /Users/snider/Code/core/php-uptelligence
git add Services/VendorUpdateCheckerService.php tests/Unit/AltumCodeCheckerTest.php
git commit -m "feat: add AltumCode product + plugin version checking
Extends VendorUpdateCheckerService to check AltumCode products via
their info.php endpoints and plugins via dev.altumcode.com/plugins-versions.
No auth required — all endpoints are public.
Co-Authored-By: Virgil <virgil@lethean.io>"
```
---
### Task 2: Seed AltumCode vendors
**Files:**
- Create: `/Users/snider/Code/core/php-uptelligence/database/seeders/AltumCodeVendorSeeder.php`
- Test: `/Users/snider/Code/core/php-uptelligence/tests/Unit/AltumCodeVendorSeederTest.php`
**Step 1: Write the failing test**
Create `/Users/snider/Code/core/php-uptelligence/tests/Unit/AltumCodeVendorSeederTest.php`:
```php
<?php
declare(strict_types=1);
use Core\Mod\Uptelligence\Models\Vendor;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('seeds 4 altum products', function () {
$this->artisan('db:seed', ['--class' => 'Core\\Mod\\Uptelligence\\Database\\Seeders\\AltumCodeVendorSeeder']);
expect(Vendor::where('source_type', Vendor::SOURCE_LICENSED)
->where('plugin_platform', Vendor::PLATFORM_ALTUM)
->count()
)->toBe(4);
});
it('seeds 13 altum plugins', function () {
$this->artisan('db:seed', ['--class' => 'Core\\Mod\\Uptelligence\\Database\\Seeders\\AltumCodeVendorSeeder']);
expect(Vendor::where('source_type', Vendor::SOURCE_PLUGIN)
->where('plugin_platform', Vendor::PLATFORM_ALTUM)
->count()
)->toBe(13);
});
it('is idempotent', function () {
$this->artisan('db:seed', ['--class' => 'Core\\Mod\\Uptelligence\\Database\\Seeders\\AltumCodeVendorSeeder']);
$this->artisan('db:seed', ['--class' => 'Core\\Mod\\Uptelligence\\Database\\Seeders\\AltumCodeVendorSeeder']);
expect(Vendor::where('plugin_platform', Vendor::PLATFORM_ALTUM)->count())->toBe(17);
});
```
**Step 2: Run test to verify it fails**
Run: `cd /Users/snider/Code/core/php-uptelligence && composer test -- --filter=AltumCodeVendorSeeder`
Expected: FAIL — seeder class not found
**Step 3: Write minimal implementation**
Create `/Users/snider/Code/core/php-uptelligence/database/seeders/AltumCodeVendorSeeder.php`:
```php
<?php
declare(strict_types=1);
namespace Core\Mod\Uptelligence\Database\Seeders;
use Core\Mod\Uptelligence\Models\Vendor;
use Illuminate\Database\Seeder;
class AltumCodeVendorSeeder extends Seeder
{
public function run(): void
{
$products = [
['slug' => '66analytics', 'name' => '66analytics', 'vendor_name' => 'AltumCode', 'current_version' => '65.0.0'],
['slug' => '66biolinks', 'name' => '66biolinks', 'vendor_name' => 'AltumCode', 'current_version' => '65.0.0'],
['slug' => '66pusher', 'name' => '66pusher', 'vendor_name' => 'AltumCode', 'current_version' => '65.0.0'],
['slug' => '66socialproof', 'name' => '66socialproof', 'vendor_name' => 'AltumCode', 'current_version' => '65.0.0'],
];
foreach ($products as $product) {
Vendor::updateOrCreate(
['slug' => $product['slug']],
[
...$product,
'source_type' => Vendor::SOURCE_LICENSED,
'plugin_platform' => Vendor::PLATFORM_ALTUM,
'is_active' => true,
]
);
}
$plugins = [
['slug' => 'altum-plugin-affiliate', 'name' => 'Affiliate Plugin', 'current_version' => '2.0.0'],
['slug' => 'altum-plugin-push-notifications', 'name' => 'Push Notifications Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-teams', 'name' => 'Teams Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-pwa', 'name' => 'PWA Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-image-optimizer', 'name' => 'Image Optimizer Plugin', 'current_version' => '3.1.0'],
['slug' => 'altum-plugin-email-shield', 'name' => 'Email Shield Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-dynamic-og-images', 'name' => 'Dynamic OG Images Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-offload', 'name' => 'Offload & CDN Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-payment-blocks', 'name' => 'Payment Blocks Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-ultimate-blocks', 'name' => 'Ultimate Blocks Plugin', 'current_version' => '9.1.0'],
['slug' => 'altum-plugin-pro-blocks', 'name' => 'Pro Blocks Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-pro-notifications', 'name' => 'Pro Notifications Plugin', 'current_version' => '1.0.0'],
['slug' => 'altum-plugin-aix', 'name' => 'AIX Plugin', 'current_version' => '1.0.0'],
];
foreach ($plugins as $plugin) {
Vendor::updateOrCreate(
['slug' => $plugin['slug']],
[
...$plugin,
'vendor_name' => 'AltumCode',
'source_type' => Vendor::SOURCE_PLUGIN,
'plugin_platform' => Vendor::PLATFORM_ALTUM,
'is_active' => true,
]
);
}
}
}
```
**Step 4: Run test to verify it passes**
Run: `cd /Users/snider/Code/core/php-uptelligence && composer test -- --filter=AltumCodeVendorSeeder`
Expected: PASS (3 tests)
**Step 5: Commit**
```bash
cd /Users/snider/Code/core/php-uptelligence
git add database/seeders/AltumCodeVendorSeeder.php tests/Unit/AltumCodeVendorSeederTest.php
git commit -m "feat: seed AltumCode vendors — 4 products + 13 plugins
Idempotent seeder using updateOrCreate. Products are SOURCE_LICENSED,
plugins are SOURCE_PLUGIN, all PLATFORM_ALTUM. Version numbers will
need updating to match actual deployed versions.
Co-Authored-By: Virgil <virgil@lethean.io>"
```
---
### Task 3: Create Claude Code plugin skill for downloads
**Files:**
- Create: `/Users/snider/.claude/plugins/altum-updater/plugin.json`
- Create: `/Users/snider/.claude/plugins/altum-updater/skills/update-altum.md`
**Step 1: Create plugin manifest**
Create `/Users/snider/.claude/plugins/altum-updater/plugin.json`:
```json
{
"name": "altum-updater",
"description": "Download AltumCode product and plugin updates from LemonSqueezy and CodeCanyon",
"version": "0.1.0",
"skills": [
{
"name": "update-altum",
"path": "skills/update-altum.md",
"description": "Download AltumCode product and plugin updates from marketplaces. Use when the user mentions updating AltumCode products, downloading from LemonSqueezy or CodeCanyon, or running the update checker."
}
]
}
```
**Step 2: Create skill file**
Create `/Users/snider/.claude/plugins/altum-updater/skills/update-altum.md`:
```markdown
---
name: update-altum
description: Download AltumCode product and plugin updates from LemonSqueezy and CodeCanyon
---
# AltumCode Update Downloader
## Overview
Downloads updated AltumCode products and plugins from two marketplaces:
- **LemonSqueezy** (Playwright): 66analytics, 66pusher, 66biolinks (extended), 13 plugins
- **CodeCanyon** (Claude in Chrome): 66biolinks (regular), 66socialproof
## Pre-flight
1. Run `php artisan uptelligence:check-updates --vendor=66analytics` (or check all) to see what needs updating
2. Show the user the version comparison table
3. Ask which products/plugins to download
## LemonSqueezy Download Flow (Playwright)
LemonSqueezy uses magic link auth. The user will need to tap the link on their phone.
1. Navigate to `https://app.lemonsqueezy.com/my-orders`
2. If on login page, fill email `snider@lt.hn` and click Sign In
3. Tell user: "Magic link sent — tap the link on your phone"
4. Wait for redirect to orders page
5. For each product/plugin that needs updating:
a. Click the product link on the orders page (paginated — 10 per page, 2 pages)
b. In the order detail, find the "Download" button under "Files"
c. Click Download — file saves to default downloads folder
6. Move downloaded zips to staging: `~/Code/lthn/saas/updates/YYYY-MM-DD/`
### LemonSqueezy Product Names (as shown on orders page)
| Our Name | LemonSqueezy Order Name |
|----------|------------------------|
| 66analytics | "66analytics - Regular License" |
| 66pusher | "66pusher - Regular License" |
| 66biolinks (extended) | "66biolinks custom" |
| Affiliate Plugin | "Affiliate Plugin" |
| Push Notifications Plugin | "Push Notifications Plugin" |
| Teams Plugin | "Teams Plugin" |
| PWA Plugin | "PWA Plugin" |
| Image Optimizer Plugin | "Image Optimizer Plugin" |
| Email Shield Plugin | "Email Shield Plugin" |
| Dynamic OG Images | "Dynamic OG images plugin" |
| Offload & CDN | "Offload & CDN Plugin" |
| Payment Blocks | "Payment Blocks - 66biolinks plugin" |
| Ultimate Blocks | "Ultimate Blocks - 66biolinks plugin" |
| Pro Blocks | "Pro Blocks - 66biolinks plugin" |
| Pro Notifications | "Pro Notifications - 66socialproof plugin" |
| AltumCode Club | "The AltumCode Club" |
## CodeCanyon Download Flow (Claude in Chrome)
CodeCanyon uses saved browser session cookies (user: snidered).
1. Navigate to `https://codecanyon.net/downloads`
2. Dismiss cookie banner if present (click "Reject all")
3. For 66socialproof:
a. Find "66socialproof" Download button
b. Click the dropdown arrow
c. Click "All files & documentation"
4. For 66biolinks:
a. Find "66biolinks" Download button (scroll down)
b. Click the dropdown arrow
c. Click "All files & documentation"
5. Move downloaded zips to staging
### CodeCanyon Download URLs (stable)
- 66socialproof: `/user/snidered/download_purchase/8d8ef4c1-5add-4eba-9a89-4261a9c87e0b`
- 66biolinks: `/user/snidered/download_purchase/38d79f4e-19cd-480a-b068-4332629b5206`
## Post-Download
1. List all zips in staging folder
2. For each product zip:
- Extract to `~/Code/lthn/saas/services/{product}/package/product/`
3. For each plugin zip:
- Extract to the correct product's `plugins/{plugin_id}/` directory
- Note: Some plugins apply to multiple products (affiliate, teams, etc.)
4. Show summary of what was updated
5. Remind user: "Files staged. Run `deploy_saas.yml` when ready to deploy."
## Important Notes
- Never make purchases or enter financial information
- LemonSqueezy session expires — if Playwright gets a login page mid-flow, re-trigger magic link
- CodeCanyon session depends on Chrome cookies — if logged out, tell user to log in manually
- The AltumCode Club subscription is NOT a downloadable product — skip it
- Plugin `aix` may not appear on LemonSqueezy (bundled with products) — skip if not found
```
**Step 3: Verify plugin loads**
Run: `claude` in a new terminal, then type `/update-altum` to verify the skill is discovered.
**Step 4: Commit**
```bash
cd /Users/snider/.claude/plugins/altum-updater
git init
git add plugin.json skills/update-altum.md
git commit -m "feat: altum-updater Claude Code plugin — marketplace download skill
Playwright for LemonSqueezy, Chrome for CodeCanyon. Includes full
product/plugin mapping and download flow documentation.
Co-Authored-By: Virgil <virgil@lethean.io>"
```
---
### Task 4: Sync deployed plugin versions from source
**Files:**
- Create: `/Users/snider/Code/core/php-uptelligence/Console/SyncAltumVersionsCommand.php`
- Modify: `/Users/snider/Code/core/php-uptelligence/Boot.php` (register command)
- Test: `/Users/snider/Code/core/php-uptelligence/tests/Unit/SyncAltumVersionsCommandTest.php`
**Step 1: Write the failing test**
```php
<?php
declare(strict_types=1);
it('reads product version from saas service config', function () {
$this->artisan('uptelligence:sync-altum-versions', ['--dry-run' => true])
->assertExitCode(0);
});
```
**Step 2: Run test to verify it fails**
Run: `cd /Users/snider/Code/core/php-uptelligence && composer test -- --filter=SyncAltumVersions`
Expected: FAIL — command not found
**Step 3: Write minimal implementation**
Create `/Users/snider/Code/core/php-uptelligence/Console/SyncAltumVersionsCommand.php`:
```php
<?php
declare(strict_types=1);
namespace Core\Mod\Uptelligence\Console;
use Core\Mod\Uptelligence\Models\Vendor;
use Illuminate\Console\Command;
/**
* Sync deployed AltumCode product/plugin versions from local source files.
*
* Reads PRODUCT_CODE from each product's source and plugin versions
* from config.php files, then updates the vendors table.
*/
class SyncAltumVersionsCommand extends Command
{
protected $signature = 'uptelligence:sync-altum-versions
{--dry-run : Show what would be updated without writing}
{--path= : Base path to saas services (default: ~/Code/lthn/saas/services)}';
protected $description = 'Sync deployed AltumCode product and plugin versions from source files';
protected array $productPaths = [
'66analytics' => '66analytics/package/product',
'66biolinks' => '66biolinks/package/product',
'66pusher' => '66pusher/package/product',
'66socialproof' => '66socialproof/package/product',
];
public function handle(): int
{
$basePath = $this->option('path')
?? env('SAAS_SERVICES_PATH', base_path('../lthn/saas/services'));
$dryRun = $this->option('dry-run');
$this->info('Syncing AltumCode versions from source...');
$this->newLine();
$updates = [];
// Sync product versions
foreach ($this->productPaths as $slug => $relativePath) {
$productPath = rtrim($basePath, '/') . '/' . $relativePath;
$version = $this->readProductVersion($productPath);
if ($version) {
$updates[] = $this->syncVendorVersion($slug, $version, $dryRun);
} else {
$this->warn(" Could not read version for {$slug} at {$productPath}");
}
}
// Sync plugin versions — read from biolinks as canonical source
$biolinkPluginsPath = rtrim($basePath, '/') . '/66biolinks/package/product/plugins';
if (is_dir($biolinkPluginsPath)) {
foreach (glob($biolinkPluginsPath . '/*/config.php') as $configFile) {
$pluginId = basename(dirname($configFile));
$version = $this->readPluginVersion($configFile);
if ($version) {
$slug = "altum-plugin-{$pluginId}";
$updates[] = $this->syncVendorVersion($slug, $version, $dryRun);
}
}
}
// Output table
$this->table(
['Vendor', 'Old Version', 'New Version', 'Status'],
array_filter($updates)
);
if ($dryRun) {
$this->warn('Dry run — no changes written.');
}
return self::SUCCESS;
}
protected function readProductVersion(string $productPath): ?string
{
// Read version from app/init.php or similar — look for PRODUCT_VERSION define
$initFile = $productPath . '/app/init.php';
if (! file_exists($initFile)) {
return null;
}
$content = file_get_contents($initFile);
if (preg_match("/define\('PRODUCT_VERSION',\s*'([^']+)'\)/", $content, $matches)) {
return $matches[1];
}
return null;
}
protected function readPluginVersion(string $configFile): ?string
{
if (! file_exists($configFile)) {
return null;
}
$content = file_get_contents($configFile);
// PHP config format: 'version' => '2.0.0'
if (preg_match("/'version'\s*=>\s*'([^']+)'/", $content, $matches)) {
return $matches[1];
}
return null;
}
protected function syncVendorVersion(string $slug, string $version, bool $dryRun): ?array
{
$vendor = Vendor::where('slug', $slug)->first();
if (! $vendor) {
return [$slug, '(not in DB)', $version, 'SKIPPED'];
}
$oldVersion = $vendor->current_version;
if ($oldVersion === $version) {
return [$slug, $oldVersion, $version, 'current'];
}
if (! $dryRun) {
$vendor->update(['current_version' => $version]);
}
return [$slug, $oldVersion ?? '(none)', $version, $dryRun ? 'WOULD UPDATE' : 'UPDATED'];
}
}
```
Register in Boot.php — add to `onConsole()`:
```php
$event->command(Console\SyncAltumVersionsCommand::class);
```
**Step 4: Run test to verify it passes**
Run: `cd /Users/snider/Code/core/php-uptelligence && composer test -- --filter=SyncAltumVersions`
Expected: PASS
**Step 5: Commit**
```bash
cd /Users/snider/Code/core/php-uptelligence
git add Console/SyncAltumVersionsCommand.php Boot.php tests/Unit/SyncAltumVersionsCommandTest.php
git commit -m "feat: sync deployed AltumCode versions from source files
Reads PRODUCT_VERSION from product init.php and plugin versions from
config.php files. Updates uptelligence_vendors table so check-updates
knows what's actually deployed.
Co-Authored-By: Virgil <virgil@lethean.io>"
```
---
### Task 5: End-to-end verification
**Step 1: Seed vendors on local dev**
```bash
cd /Users/snider/Code/lab/host.uk.com
php artisan db:seed --class="Core\Mod\Uptelligence\Database\Seeders\AltumCodeVendorSeeder"
```
**Step 2: Sync actual deployed versions**
```bash
php artisan uptelligence:sync-altum-versions --path=/Users/snider/Code/lthn/saas/services
```
**Step 3: Run the update check**
```bash
php artisan uptelligence:check-updates
```
Expected: Table showing current vs latest versions for all 17 AltumCode vendors.
**Step 4: Test the skill**
Open a new Claude Code session and run `/update-altum` to verify the skill loads and shows the workflow.
**Step 5: Commit any fixes**
```bash
git add -A && git commit -m "fix: adjustments from end-to-end testing"
```

View file

@ -0,0 +1,169 @@
---
title: Core Primitives
description: The repeated shapes that make CoreGO easy to navigate.
---
# Core Primitives
CoreGO is easiest to use when you read it as a small vocabulary repeated everywhere. Most of the framework is built from the same handful of types.
## Primitive Map
| Type | Used For |
|------|----------|
| `Options` | Input values and lightweight metadata |
| `Result` | Output values and success state |
| `Service` | Lifecycle-managed components |
| `Message` | Broadcast events |
| `Query` | Request-response lookups |
| `Task` | Side-effecting work items |
## `Option` and `Options`
`Option` is one key-value pair. `Options` is an ordered slice of them.
```go
opts := core.Options{
{Key: "name", Value: "brain"},
{Key: "path", Value: "prompts"},
{Key: "debug", Value: true},
}
```
Use the helpers to read values:
```go
name := opts.String("name")
path := opts.String("path")
debug := opts.Bool("debug")
hasPath := opts.Has("path")
raw := opts.Get("name")
```
### Important Details
- `Get` returns the first matching key.
- `String`, `Int`, and `Bool` do not convert between types.
- Missing keys return zero values.
- CLI flags with values are stored as strings, so `--port=8080` should be read with `opts.String("port")`, not `opts.Int("port")`.
## `Result`
`Result` is the universal return shape.
```go
r := core.Result{Value: "ready", OK: true}
if r.OK {
fmt.Println(r.Value)
}
```
It has two jobs:
- carry a value when work succeeds
- carry either an error or an empty state when work does not succeed
### `Result.Result(...)`
The `Result()` method adapts plain Go values and `(value, error)` pairs into a `core.Result`.
```go
r1 := core.Result{}.Result("hello")
r2 := core.Result{}.Result(file, err)
```
This is how several built-in helpers bridge standard-library calls.
## `Service`
`Service` is the managed lifecycle DTO stored in the registry.
```go
svc := core.Service{
Name: "cache",
Options: core.Options{
{Key: "backend", Value: "memory"},
},
OnStart: func() core.Result {
return core.Result{OK: true}
},
OnStop: func() core.Result {
return core.Result{OK: true}
},
OnReload: func() core.Result {
return core.Result{OK: true}
},
}
```
### Important Details
- `OnStart` and `OnStop` are used by the framework lifecycle.
- `OnReload` is stored on the service DTO, but CoreGO does not currently call it automatically.
- The registry stores `*core.Service`, not arbitrary typed service instances.
## `Message`, `Query`, and `Task`
These are simple aliases to `any`.
```go
type Message any
type Query any
type Task any
```
That means your own structs become the protocol:
```go
type deployStarted struct {
Environment string
}
type workspaceCountQuery struct{}
type syncRepositoryTask struct {
Name string
}
```
## `TaskWithIdentifier`
Long-running tasks can opt into task identifiers.
```go
type indexedTask struct {
ID string
}
func (t *indexedTask) SetTaskIdentifier(id string) { t.ID = id }
func (t *indexedTask) GetTaskIdentifier() string { return t.ID }
```
If a task implements `TaskWithIdentifier`, `PerformAsync` injects the generated `task-N` identifier before dispatch.
## `ServiceRuntime[T]`
`ServiceRuntime[T]` is the small helper for packages that want to keep a Core reference and a typed options struct together.
```go
type agentServiceOptions struct {
WorkspacePath string
}
type agentService struct {
*core.ServiceRuntime[agentServiceOptions]
}
runtime := core.NewServiceRuntime(c, agentServiceOptions{
WorkspacePath: "/srv/agent-workspaces",
})
```
It exposes:
- `Core()`
- `Options()`
- `Config()`
This helper does not register anything by itself. It is a composition aid for package authors.

View file

@ -0,0 +1,152 @@
---
title: Services
description: Register, inspect, and lock CoreGO services.
---
# Services
In CoreGO, a service is a named lifecycle entry stored in the Core registry.
## Register a Service
```go
c := core.New()
r := c.Service("audit", core.Service{
OnStart: func() core.Result {
core.Info("audit started")
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("audit stopped")
return core.Result{OK: true}
},
})
```
Registration succeeds when:
- the name is not empty
- the registry is not locked
- the name is not already in use
## Read a Service Back
```go
r := c.Service("audit")
if r.OK {
svc := r.Value.(*core.Service)
_ = svc
}
```
The returned value is `*core.Service`.
## List Registered Services
```go
names := c.Services()
```
### Important Detail
The current registry is map-backed. `Services()`, `Startables()`, and `Stoppables()` do not promise a stable order.
## Lifecycle Snapshots
Use these helpers when you want the current set of startable or stoppable services:
```go
startables := c.Startables()
stoppables := c.Stoppables()
```
They return `[]*core.Service` inside `Result.Value`.
## Lock the Registry
CoreGO has a service-lock mechanism, but it is explicit.
```go
c := core.New()
c.LockEnable()
c.Service("audit", core.Service{})
c.Service("cache", core.Service{})
c.LockApply()
```
After `LockApply`, new registrations fail:
```go
r := c.Service("late", core.Service{})
fmt.Println(r.OK) // false
```
The default lock name is `"srv"`. You can pass a different name if you need a custom lock namespace.
For the service registry itself, use the default `"srv"` lock path. That is the path used by `Core.Service(...)`.
## `NewWithFactories`
For GUI runtimes or factory-driven setup, CoreGO provides `NewWithFactories`.
```go
r := core.NewWithFactories(nil, map[string]core.ServiceFactory{
"audit": func() core.Result {
return core.Result{Value: core.Service{
OnStart: func() core.Result {
return core.Result{OK: true}
},
}, OK: true}
},
"cache": func() core.Result {
return core.Result{Value: core.Service{}, OK: true}
},
})
```
### Important Details
- each factory must return a `core.Service` in `Result.Value`
- factories are executed in sorted key order
- nil factories are skipped
- the return value is `*core.Runtime`
## `Runtime`
`Runtime` is a small wrapper used for external runtimes such as GUI bindings.
```go
r := core.NewRuntime(nil)
rt := r.Value.(*core.Runtime)
_ = rt.ServiceStartup(context.Background(), nil)
_ = rt.ServiceShutdown(context.Background())
```
`Runtime.ServiceName()` returns `"Core"`.
## `ServiceRuntime[T]` for Package Authors
If you are writing a package on top of CoreGO, use `ServiceRuntime[T]` to keep a typed options struct and the parent `Core` together.
```go
type repositoryServiceOptions struct {
BaseDirectory string
}
type repositoryService struct {
*core.ServiceRuntime[repositoryServiceOptions]
}
func newRepositoryService(c *core.Core) *repositoryService {
return &repositoryService{
ServiceRuntime: core.NewServiceRuntime(c, repositoryServiceOptions{
BaseDirectory: "/srv/repos",
}),
}
}
```
This is a package-authoring helper. It does not replace the `core.Service` registry entry.

View file

@ -0,0 +1,158 @@
---
title: Subsystems
description: Built-in accessors for app metadata, embedded data, filesystem, transport handles, i18n, and CLI.
---
# Subsystems
`Core` gives you a set of built-in subsystems so small applications do not need extra plumbing before they can do useful work.
## Accessor Map
| Accessor | Purpose |
|----------|---------|
| `App()` | Application identity and external runtime |
| `Data()` | Named embedded filesystem mounts |
| `Drive()` | Named transport handles |
| `Fs()` | Local filesystem access |
| `I18n()` | Locale collection and translation delegation |
| `Cli()` | Command-line surface over the command tree |
## `App`
`App` stores process identity and optional GUI runtime state.
```go
app := c.App()
app.Name = "agent-workbench"
app.Version = "0.25.0"
app.Description = "workspace runner"
app.Runtime = myRuntime
```
`Find` resolves an executable on `PATH` and returns an `*App`.
```go
r := core.Find("go", "Go toolchain")
```
## `Data`
`Data` mounts named embedded filesystems and makes them addressable through paths like `mount-name/path/to/file`.
```go
c.Data().New(core.Options{
{Key: "name", Value: "app"},
{Key: "source", Value: appFS},
{Key: "path", Value: "templates"},
})
```
Read content:
```go
text := c.Data().ReadString("app/agent.md")
bytes := c.Data().ReadFile("app/agent.md")
list := c.Data().List("app")
names := c.Data().ListNames("app")
```
Extract a mounted directory:
```go
r := c.Data().Extract("app/workspace", "/tmp/workspace", nil)
```
### Path Rule
The first path segment is always the mount name.
## `Drive`
`Drive` is a registry for named transport handles.
```go
c.Drive().New(core.Options{
{Key: "name", Value: "api"},
{Key: "transport", Value: "https://api.lthn.ai"},
})
c.Drive().New(core.Options{
{Key: "name", Value: "mcp"},
{Key: "transport", Value: "mcp://mcp.lthn.sh"},
})
```
Read them back:
```go
handle := c.Drive().Get("api")
hasMCP := c.Drive().Has("mcp")
names := c.Drive().Names()
```
## `Fs`
`Fs` wraps local filesystem operations with a consistent `Result` shape.
```go
c.Fs().Write("/tmp/core-go/example.txt", "hello")
r := c.Fs().Read("/tmp/core-go/example.txt")
```
Other helpers:
```go
c.Fs().EnsureDir("/tmp/core-go/cache")
c.Fs().List("/tmp/core-go")
c.Fs().Stat("/tmp/core-go/example.txt")
c.Fs().Rename("/tmp/core-go/example.txt", "/tmp/core-go/example-2.txt")
c.Fs().Delete("/tmp/core-go/example-2.txt")
```
### Important Details
- the default `Core` starts with `Fs{root:"/"}`
- relative paths resolve from the current working directory
- `Delete` and `DeleteAll` refuse to remove `/` and `$HOME`
## `I18n`
`I18n` collects locale mounts and forwards translation work to a translator implementation when one is registered.
```go
c.I18n().SetLanguage("en-GB")
```
Without a translator, `Translate` returns the message key itself:
```go
r := c.I18n().Translate("cmd.deploy.description")
```
With a translator:
```go
c.I18n().SetTranslator(myTranslator)
```
Then:
```go
langs := c.I18n().AvailableLanguages()
current := c.I18n().Language()
```
## `Cli`
`Cli` exposes the command registry through a terminal-facing API.
```go
c.Cli().SetBanner(func(_ *core.Cli) string {
return "Agent Workbench"
})
r := c.Cli().Run("workspace", "create", "--name=alpha")
```
Use [commands.md](commands.md) for the full command and flag model.

View file

@ -0,0 +1,118 @@
---
title: Testing
description: Test naming and testing patterns used by CoreGO.
---
# Testing
The repository uses `github.com/stretchr/testify/assert` and a simple AX-friendly naming pattern.
## Test Names
Use:
- `_Good` for expected success
- `_Bad` for expected failure
- `_Ugly` for panics, degenerate input, and edge behavior
Examples from this repository:
```go
func TestNew_Good(t *testing.T) {}
func TestService_Register_Duplicate_Bad(t *testing.T) {}
func TestCore_Must_Ugly(t *testing.T) {}
```
## Start with a Small Core
```go
c := core.New(core.Options{
{Key: "name", Value: "test-core"},
})
```
Then register only the pieces your test needs.
## Test a Service
```go
started := false
c.Service("audit", core.Service{
OnStart: func() core.Result {
started = true
return core.Result{OK: true}
},
})
r := c.ServiceStartup(context.Background(), nil)
assert.True(t, r.OK)
assert.True(t, started)
```
## Test a Command
```go
c.Command("greet", core.Command{
Action: func(opts core.Options) core.Result {
return core.Result{Value: "hello " + opts.String("name"), OK: true}
},
})
r := c.Cli().Run("greet", "--name=world")
assert.True(t, r.OK)
assert.Equal(t, "hello world", r.Value)
```
## Test a Query or Task
```go
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
if q == "ping" {
return core.Result{Value: "pong", OK: true}
}
return core.Result{}
})
assert.Equal(t, "pong", c.QUERY("ping").Value)
```
```go
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
if t == "compute" {
return core.Result{Value: 42, OK: true}
}
return core.Result{}
})
assert.Equal(t, 42, c.PERFORM("compute").Value)
```
## Test Async Work
For `PerformAsync`, observe completion through the action bus.
```go
completed := make(chan core.ActionTaskCompleted, 1)
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if event, ok := msg.(core.ActionTaskCompleted); ok {
completed <- event
}
return core.Result{OK: true}
})
```
Then wait with normal Go test tools such as channels, timers, or `assert.Eventually`.
## Use Real Temporary Paths
When testing `Fs`, `Data.Extract`, or other I/O helpers, use `t.TempDir()` and create realistic paths instead of mocking the filesystem by default.
## Repository Commands
```bash
core go test
core go test --run TestPerformAsync_Good
go test ./...
```