Commit graph

808 commits

Author SHA1 Message Date
Snider
2f9ffd5324 feat(agent/pipeline): implement pipeline/budget + pipeline/training (#536)
Replaces the #535 stubs with full impl per RFC.pipeline.md.

Lands:
* pkg/agentic/pipeline_budget.go (extend) — budget/plan reads pool/rate
  config, counts logged dispatches from .core/db.duckdb (JSONL fallback),
  prints per-pool remaining budget. budget/log appends to
  .core/journal/dispatch.jsonl + mirrors to state store.
* pkg/agentic/pipeline_training.go (extend) — training/capture pulls PR
  meta via MetaReader, captures PR diff via Forge PR-diff endpoint with
  `git show` fallback, records structural CodeRabbit-equivalent finding
  counts from review-thread totals, appends to .core/training/journal.jsonl.
  training/stats aggregates totals + zero-finding counts by repo.
  training/export filters to zero-finding entries → .core/training/export.jsonl
  (clean LEM training data).
* pkg/agentic/training_journal.go (NEW) — shared journal helpers
* AX-10 tests replace stubs (pipeline_budget_test.go +
  pipeline_training_test.go)
* tests/cli/pipeline/Taskfile.yaml — end-to-end smoke covers all 5
  subcommands against isolated temp workspace + local Forge stub

LEM training data pipeline now feedable: merged PRs → training/capture
→ journal.jsonl → training/export (zero-finding filter) → ready for next
LEK iteration.

Sandbox blocked from go test by go.work + private-dep resolution;
gofmt clean. Forge PR diff endpoint shape verified against Gitea API
docs (1.19).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=536
2026-04-25 23:57:27 +01:00
Snider
23f4be5b85 feat(agent/pipeline): wire branch cleanup into createPR + cmdComplete success paths (#545)
Per RFC §15.5.1: agent branches (agent/*) must be deleted from Forge
after successful push or merge — stale branches pollute workspace prep
and cause clone confusion.

Lands:
* pkg/agentic/branch_cleanup.go (NEW) — cleanupBranch(ctx, repo, branch)
  helper. Refuses main/dev/master regardless of input (defensive).
  Normalises refs/heads/* prefix. Treats missing-remote-branch as
  harmless cleanup-success (idempotent).
* pkg/agentic/branch_cleanup_test.go (NEW) — AX-10 TestCleanupBranch_
  {Good_DeletesAgentBranch, Bad_RefuseProtected, Ugly_DeleteFailsForge}.
* pkg/agentic/pr.go — createPR success-on-push path now calls cleanupBranch.
* pkg/agentic/commands.go — cmdComplete success path also calls cleanupBranch.
* tests/cli/branch/Taskfile.yaml — end-to-end smoke + AX-10 unit hook.

agentic.branch.delete action was already registered in prep.go; this lane
routes the actual delete behaviour through the new helper instead of
editing the registration site.

Sandbox blocked from go test by outer go.work conflicting replacements;
gofmt clean. Supervisor's clean workspace catches.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=545
2026-04-25 23:56:34 +01:00
Snider
92b433ad76 feat(agent/sync): WorkspacePushed IPC → go-scm local repo sync (#546)
Per RFC §7 Post-Completion Repo Sync: workspace push → IPC event →
local clone fetch+reset so the supervisor's working tree always
matches Forge.

Lands:
* pkg/agentic/repo_sync.go — registers sync.fetch + sync.reset core
  actions; subscribes to WorkspacePushed IPC; exposes core-agent
  repo/sync --repo <name> [--reset] manual command
* commands.go — wires the subscriber + actions at startup
* pkg/agentic/repo_sync_test.go — AX-10: WorkspacePushed handler,
  branch-switch/reset behaviour, command path
* tests/cli/sync/{Taskfile.yaml,repo/Taskfile.yaml} — end-to-end
  smoke proving local clone matches Forge HEAD after sync

The existing 5min fallback fetch loop in fetch_loop.go is reused
unchanged — this lane fills the event-driven half of the contract.

Sandbox blocked from go test / go build by pre-existing go.work
dappco.re/go/api replacement conflict; supervisor's clean workspace
catches.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=546
2026-04-25 23:33:38 +01:00
Snider
5c942a8928 feat(agent/qa): post-run Poindexter workspace analysis per RFC §7 (#538)
Per RFC §7 Post-Run Analysis: analyseWorkspace() builds 5D Poindexter
points (tool_id, severity_score, file_hash, category_id, frequency),
clusters by distance 0.15, diffs against previous journal entries to
classify New / Resolved / Persistent (≥5 consecutive cycles).

Lands:
* pkg/agentic/qa_analysis.go — analyseWorkspace, DispatchReport,
  findingToPoint, diffFindings, persistentFindings; integrates with
  forge.lthn.ai/Snider/Poindexter (canonical path per memory)
* pkg/agentic/qa.go — wires analysis into runQAWithReport before
  ws.Commit() (sync.go untouched — ws.Commit lives in runQAWithReport
  in this branch)
* journal publication extended so summary text + analysis fields travel
  with the report
* qa_analysis_test.go — TestAnalyseWorkspace_{Good_EmptyFindings,
  Good_FiveClusters,Bad_NilWorkspace,Ugly_PoindexterPanic}; the panic
  test uses a panic-injecting clusterer override and asserts graceful
  recovery
* go.mod — adds forge.lthn.ai/Snider/Poindexter (canonical, NOT
  dappco.re — Poindexter is OG load-bearing math primitive)

Sandbox go test blocked by pre-existing unrelated issues in
commands_forge.go / fetch_loop.go / commands_flow_test.go (out of
allowlist); supervisor catches in clean workspace.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=538
2026-04-25 23:27:30 +01:00
Snider
53b46c33da feat(agent/pipeline): add core pipeline command tree (#535)
Per RFC.pipeline.md "core pipeline Command Tree": full audit/epic/
monitor/fix/onboard/budget/training subcommand tree wired into
core-agent.

Lands across 18 files:
* pkg/agentic/pipeline_commands.go — registry: audit, epic/create|run|
  status|sync, monitor, fix/reviews|conflicts|format|threads, onboard,
  budget/*, training/*
* pkg/agentic/commands.go — pipeline registration wired in
* pkg/agentic/pipeline_audit.go — audit issue expansion + bug fix:
  audit-created implementation issues no longer carry the 'audit' label,
  so epic + onboard see them as implementation candidates
* pkg/agentic/pipeline_epic.go — epic group/run/sync
* pkg/agentic/pipeline_monitor.go — open-PR watcher
* pkg/agentic/pipeline_fix.go — reviews/conflicts/format/threads helpers
* pkg/agentic/pipeline_onboard.go — chained audit → epic → dispatch
* pkg/agentic/pipeline_budget.go + pipeline_training.go — stubs
  returning blocked-on-sibling pointer (sibling tickets own deep impl)
* pkg/agentic/pipeline_*_test.go — AX-10 per handler
* tests/cli/pipeline/Taskfile.yaml — CLI smoke coverage

go test could not complete in this sandbox due to wider workspace
go.sum/private-module issues outside this ticket; supervisor catches
in clean workspace.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=535
2026-04-25 23:25:37 +01:00
Snider
b29d20f562 feat(agent/flow): audit pkg/lib/flow vs RFC + child ticket list (#544)
Read-only audit deliverable per ticket scope (no code changes).

docs/flow-audit-2026-04-25.md findings:
* Only 2 canonical RFC YAML flows present in pkg/lib/flow/
  (upgrade/v080-plan.yaml + upgrade/v080-implement.yaml).
* 13 of the 15 canonical RFC §3.1 flows are missing.
* pr/merge.yaml appears only as an extra RFC example path.
* Runner feature support:
  - flow: directive — preview-only, REJECTED in execution
  - when: conditional — NOT implemented
  - parallel: fan-out — preview-only
  - --dry-run — exists but is effectively a preview alias
  - embedded RFC YAML path lookup — missing (mounted lookup forces
    Markdown slugs instead of RFC directory structure)
* CLI probe: `./core-agent run/flow pkg/lib/flow/upgrade/v080-plan.yaml
  --dry-run` exits 0 with 5-step preview. `./core-agent run/flow
  upgrade/v080-plan --dry-run` exits 1 — confirms embedded path
  lookup gap.

Per the ticket: output is the audit + child ticket list, not impl.
Supervisor files the 13 missing-flow + 4 runner-feature child tickets
into Mantis.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=544
2026-04-25 23:11:33 +01:00
Snider
d47946ff82 feat(agent/process): add Timeout + GracePeriod + KillGroup to dispatch (#540)
Per RFC.pipeline.md "go-process Improvements Needed": hung agent
processes blocked dispatch slots forever. Now killed after configured
timeout, with SIGTERM-then-SIGKILL grace period and process-group kill
to prevent orphaned subprocesses.

Lands:
* pkg/agentic/dispatch.go — every c.Process().Run() that spawns an
  agent now passes Timeout (DispatchConfig.TimeoutMinutes, default 60),
  GracePeriod: 30s, KillGroup: true. Watchdog writes timeout-specific
  failure reason into workspace status.
* pkg/agentic/queue.go — DispatchConfig adds TimeoutMinutes int (YAML:
  timeout_minutes, default 60) so operators can tune per-deployment.
* dispatch_test.go — TestDispatch_Run_Bad_Timeout asserts slow process
  transitions to failed state with timeout reason
* queue_test.go — TestQueue_Config_Good_TimeoutDefault asserts default 60

Verified go-process exposes timeout/gracePeriod/killGroup option keys
in the local checkout — no BLOCKED sibling needed.

Plain go build blocked by unrelated go.work conflict + sibling go-ws
coreerr.Warn missing (out of allowlist); supervisor's clean workspace
build will catch any remaining compile.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=540
2026-04-25 23:08:19 +01:00
Snider
f96bd67bd6 feat(agent/admin+hub): RFC foundation — admin scaffold + Hub global components
Foundation slice for Mantis #843 php/Mod/Admin + php/Website/Hub RFC:

* php/Mod/Admin/Boot.php — search registry, menu registry, form component
  layer, HasRateLimiting concern, reusable form/view primitives under
  Mod/Admin/Forms
* php/Website/Hub/Boot.php — host-aware Hub route naming for secondary
  domains
* WorkspaceSwitcher and GlobalSearch global Hub Livewire components
* Foundation routed slice in Hub/Routes/admin.php: dashboard shell,
  workspace listing, site settings (with WordPress/webhook connector),
  account usage, platform user list+detail
* Foundation tests under php/tests/Feature/Mod/Admin/

53 PHP files. php -l clean. Pest unrunnable in sandbox (no vendor/).

Foundation slice only — composer.json kept off-limits so namespace stays
under Core\Mod\Agentic\... rather than standalone Core\Admin package.
Deferred: Profile, Settings, ServiceManager, ServicesAdmin, Honeypot,
Entitlement\{Dashboard,FeatureManager,PackageManager}, PromptManager,
WaitlistManager, Console, Databases, Deployments, Content,
ContentManager, ContentEditor, ActivityLog, Analytics, AIServices,
BoostPurchase. Lane was under-instructed by supervisor with stop-at
framing — follow-up tickets needed for remainder.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=843
2026-04-25 21:09:22 +01:00
Snider
5385385314 feat(agent/api): RFC foundation — API keys, webhooks, rate limiting, docs split
Foundation slice for Mantis #844 php/Mod/Api RFC implementation:

* New php/Mod/Api/ package: Boot, Controllers, Documentation, Jobs,
  Middleware, Models, RateLimit, Routes, Services
* Models: ApiKey, WebhookEndpoint, WebhookDelivery
* WebhookService::dispatch() with DB::transaction + afterCommit
* DeliverWebhookJob with retry/backoff
* WebhookSignature with timing-safe verification + 5-minute tolerance +
  dual-secret rotation support
* Sliding-window rate limiter in RateLimit/RateLimitService.php
* AuthenticateApiKey middleware: hk_ prefix + Sanctum fallback
* DocsController / DocumentationController split
* 3 root migrations: api_keys, webhook_endpoints, webhook_deliveries
* Foundation tests under php/tests/Feature/Mod/Api/
* FOLLOWUP.md tracks remaining RFC scope

php -l clean across 21 PHP files. Pest unrunnable in sandbox (no vendor/).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=844
2026-04-25 21:01:54 +01:00
Snider
429d1c0897 feat(agent/agentic): RFC foundation — atomic CompleteTask + credit ledger reconcile
Foundation slice for Mantis #841 php/Mod/Agent RFC implementation:

* CompleteTask now wraps in DB::transaction with idempotent credit awards
  and safe current_task_id clearing
* Credits/{Award,GetBalance,GetCreditHistory} updated for agent_id +
  fleet_task_id ledger support and richer balance totals
* GenerateCommand canonical agentic:generate wiring; legacy duplicate
  no longer registered
* Boot wires brain:clean / brain:prune / brain:reindex
* EmbedMemory exits early when memory already indexed
* 3 follow-on fleet migrations reconcile fleet_nodes pointer column,
  fleet_tasks/credit_entries fk/index hygiene, fleet+credit constraints
* 4 foundation tests under php/tests/Feature/Mod/Agent/

php -l clean on all modified files. pest unrunnable in sandbox (no vendor/).

Foundation slice only: remaining model/action parity, full MCP tool/
service sweep, fleet controller auth-context, and 41-tool/45-action
surface left for follow-up tickets.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=841
2026-04-25 20:59:38 +01:00
Snider
64852472de feat(agent/agentic): RFC §16.5 offline sync queue with exponential backoff
Adds remote_sync_queue.go with persistent queue + 1s→30s exponential
backoff drainer, max-attempt drop with audit record, ctx.Done() clean
shutdown. sync.go rewired to enqueue dispatches via the queue rather
than the legacy minute-ticker schedule.

CORE_AGENT_SYNC_MAX_ATTEMPTS / CORE_SYNC_MAX_ATTEMPTS env vars override
the default 100-attempt cap.

Targeted tests cover happy-path drain, retry/backoff with stub clock,
cancellation, max-attempt exhaustion, and file-backed restart persistence.
go test was blocked by go.work workspace dep resolution in sandbox; CI
catches the real run.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=231
2026-04-25 20:57:12 +01:00
Snider
929f6effbd docs(agent/brain): add BRAIN-CALLERS.md living map
Records HTTP /v1/brain/* and in-process Brain caller contracts, recent
hardening (#312, #1006, #998, #1052, #1055), and per-call-site request/
response shapes. Companion to docs/brain-callers-audit.md from #121.

Recovers artefact lost when the original #179 worktree was cleaned up
before cherry-pick landed on dev.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=179
2026-04-25 20:49:58 +01:00
Snider
35b327d47e fix(agent/agentic): nil-guard PrepSubsystem.Core() in handleComplete
Defensive nil check returns a typed error result when the subsystem
is constructed without a runtime, matching the pattern in adjacent
handlers. Adds Bad case test.

Co-authored-by: Cerberus <noreply@anthropic.com>
2026-04-25 20:49:57 +01:00
Snider
1872424cfd feat(agent/brain): lift OpenBrain discovery features (search/discoverTags/listScopes) (#180)
Bounded subset of RFC-OPENBRAIN lifted from lab/lthn.ai shim into the
OSS BrainService at php/Services/BrainService.php:

- search(query, filter, pagination): Elasticsearch path first, falls
  back to MariaDB if ES is unavailable. Operates on active/latest
  memories only.
- discoverTags(filter): tag-cloud / popular-tags discovery scoped to
  authenticated org(s).
- listScopes(filter): org/project distribution counts for the
  authenticated session.

All three:
- Enforce bounded inputs (per #1001 patterns)
- Honour org auth (per #312 patterns)
- Only operate on active/latest memories (active=1, deleted_at IS NULL)

Self-hosters now get the same discovery surface that lab/lthn.ai
exposes — no need to fork the OSS service to access these features.

Pest covers: bounds-violation rejection, fallback behaviour, scoped
discovery returning correct org/project breakdowns.

Lab-only features still out of scope for this lane (would pull in
extra schema/models/events): agentContext, recall feedback,
maintenance lifecycle (reindex/consolidate/clean/prune). Those need
follow-up tickets if/when bounded-lift becomes possible.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=180
2026-04-25 20:39:14 +01:00
Snider
9628e5d088 feat(agent/brain): non-Laravel callers route through shared OpenBrain client (#121)
Audit + remediation pass on non-Laravel Brain callers per #121.

docs/brain-callers-audit.md (NEW): audit baseline. Lists every PHP +
Go + script call site, current protections (org auth, retry, circuit
breaker), and gaps. Cross-references recent fixes (#312, #998, #1052,
#1055, #1006, #985).

New pkg/agentic/brain_client.go helper: routes Go callers through
the shared OpenBrain client path with org injection (CORE_BRAIN_ORG
fallback) and a shared circuit breaker.

Patched call sites:
- pkg/agentic/prep.go (line ~1200)
- pkg/agentic/session.go (line ~826)
- pkg/agentic/brain_seed_memory.go (line ~153)
- pkg/brain/direct.go (line ~106)

These previously made raw Brain HTTP posts. Now they go through the
hardened client path inheriting #998 (key-perm), #1052 (bearer SSRF
guard), #1055 (jitter), and the circuit breaker.

Remaining non-Laravel gaps (documented in audit doc, not patched here
because they're outside this lane's path-allowlist):
- core/mcp/pkg/mcp/agentic/prep.go
- Hermes Python plugins
- Claude shell hooks
- plugins/core-go api-endpoints SKILL example

Follow-up tickets needed for those (file separately).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=121
2026-04-25 20:33:33 +01:00
Snider
d00272401b chore(agent): close stale-fixed Mantis #124 — github+homelab+origin all present
Codex preflight confirmed:
- github  https://github.com/dappcore/agent.git
- homelab https://forge.lthn.sh/core/agent.git
- origin  ssh://git@forge.lthn.ai:2223/core/agent.git

The squash-to-public workflow has all three remotes wired. The github
remote was added during the Burst E publish-to-dappcore batch (session
memory). Closing as no-op.

Co-authored-by: Cladius <cladius@lthn.ai>
Closes tasks.lthn.sh/view.php?id=124
2026-04-25 20:30:11 +01:00
Snider
40728e68d1 feat(agent/agentic): Poindexter KD-tree clustering replaces exact-key bucketing in runQAWithReport
qa_cluster.go wires QA finding clustering through Poindexter instead
of the old exact (tool, severity, category, rule) bucketing. Hashed
feature vectors built from finding metadata + message text are
indexed in Poindexter KD-trees with cosine + Euclidean distance,
near-neighbours unioned, then emitted as the existing DispatchCluster
shape (so consumers don't break).

Old exact-key grouping kept as fallback if Poindexter tree
construction ever fails.

qa.go updated to describe new similarity-based contract;
runQAWithReport already consumes clusterFindings, picks up the new
grouping automatically.

Tests cover: 3 similar findings + 2 distinct → grouped correctly;
5 distinct findings → 5 clusters; 0 findings → empty/nil no panic;
sample-cap regression preserved.

Note: github.com/Snider/Poindexter added as a direct dep. Per the
non-negotiable migration policy on Snider personal-namespace deps
(see #219), file follow-up to migrate Poindexter to its canonical
core/* home if/when that lands.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=164
2026-04-25 20:25:05 +01:00
Snider
b42cf5a18c feat(agent/agentic): 5-min background git fetch loop for registered repos
fetch_loop.go starts at PrepSubsystem.OnStartup, ticks on configurable
interval (default 5min), respects ctx.Done() for clean shutdown.
Each tick runs `git fetch origin <DefaultBranch>` via s.Core().Process()
— no worktree mutation.

Repo discovery sources (priority):
1. agents.fetch_repos in runtime config
2. repos / agents.*.repos in agents.yaml
3. Fallback scan of WorkspaceRoot() per RFC §7

Interval config: agents.fetch_interval, dispatch.fetch_interval, or
top-level fetch_interval in agents.yaml.

Loop survives individual repo failures (logs + continues — one bad
repo can't kill the loop).

Tests cover tick timing, failure isolation, ctx cancellation.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=165
2026-04-25 20:24:21 +01:00
Snider
820d33ebec feat(agent/agentic): scaffold core pipeline command tree per RFC-AGENT-PIPELINE.md
Codex preflight found docs/RFC-AGENT-PIPELINE.md (not the speculative
RFC.pipeline.md the ticket title referenced). Implementation matches
the actual RFC tree:

- core (top-level)
- core pipeline (router)
- core pipeline epic / fix / budget / training (grouped routers)
- All RFC leaf commands under each grouped router

Routers print scoped help. Each leaf currently returns "not yet
implemented" with a concrete next doc/flow reference (e.g. docs/flow/
RFC.flow-audit-issues.md). Future tickets wire the leaves to real
handlers.

Tests cover registration, descriptions, --help routing through
core pipeline audit.

Note: docs/RFC.pipeline.md alias still missing — TODO note in
commands_core.go for that follow-up.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=228
2026-04-25 20:24:21 +01:00
Snider
8858545f63 feat(agent/lib/flow): YAML flow library — Parse + ParseFile + LoadEmbedded
New pkg/lib/flow package per RFC §Flow System:

types.Flow{Name, Description, Steps}, types.Step{Name, Cmd, Args,
ContinueOnError}.

Parse(reader io.Reader) (Flow, error): YAML decoder
ParseFile(path string) (Flow, error): reads via core.Fs, then Parse
LoadEmbedded(name string) (Flow, error): bundled flow templates;
.md files only treated as flows when they contain YAML frontmatter

Validation: steps may be absent (empty Steps slice OK); any declared
step must define cmd.

Pairs with #160 (run/flow command at pkg/agentic/flow.go) — that
consumes types from this library for sequential step execution.

Tests cover: valid YAML, continueOnError, empty input, malformed
YAML, missing cmd, temp-file ParseFile, missing embedded files,
markdown-template failure (current state — embedded markdown is
content not YAML).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=229
2026-04-25 20:21:33 +01:00
Snider
eed51d72b8 feat(agent/agentic): run/flow now executes sequential YAML steps
run/flow command now runs flow steps via the existing command tree:
- Each step's cmd is dispatched through s.Command(...)
- stdout/stderr captured per step
- Stops on first untolerated failure
- continueOnError: true allows step to fail without aborting flow
- Parse-time validation rejects unknown/non-executable commands
  BEFORE any step runs

flow/preview keeps the old inspection-only behaviour. Empty flows
succeed as no-ops.

Returns FlowRunOutput with summary: OK boolean, Executed/Passed/
Failed counts, per-step results.

Pest-equivalent Go tests cover: 3-step happy path, non-existent
cmd parse-time error, mid-flow failure (with + without
continueOnError), empty flow.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=160
2026-04-25 20:09:52 +01:00
Snider
55bc34c885 feat(agent/plugins): create plugins/{core-go,core-php,infra} tree (RFC.plugin-restructure §1+§2)
plugins/ subtree created from scratch (no dappcore-go/dappcore-php
existed to rename). Per docs/RFC-AGENT-PLUGIN-RESTRUCTURE.md:

plugins/core-go/:
- .claude-plugin/plugin.json (name: core-go)
- README.md, marketplace.yaml, .mcp.json (calls core mcp serve)
- commands/{commit,qa,review,verify}.md
- skills/{core,core-go,go-agent}/SKILL.md (seeded from existing repo material)
- skills/api-endpoints/SKILL.md (NEW per ticket)
- agents/go-developer.md
- hook scripts referenced by commands

plugins/core-php/: same structure, php-developer agent + php-specific
api-endpoints skill.

plugins/infra/: plugin.json, README.md, marketplace.yaml, agents/infra-ops.md.

Manifests use core-* not dappcore-*. .mcp.json files call core mcp serve.
No dappcore-* names left.

Note: seed skills copied from existing repo material per ticket spec —
some upstream Host UK examples remain in copied skill docs. Future PR
can purge those if Host UK references are out of scope for the public
plugin marketplace.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=234
2026-04-25 20:05:05 +01:00
Snider
fbee790859 chore(agent): close stale-fixed Mantis #173 — resolved by #222
#173 was a duplicate of #222 (Replace net/http in pkg/agentic/transport.go
with core Drive primitives). #222 landed at commit 99b0865 with the
annotated-as-AX-6-structural-exception resolution: net/http stays as the
intrinsic HTTP transport boundary behind core.Drive/core.API, with
explicit doc-comment rationale.

Co-authored-by: Cladius <cladius@lthn.ai>
Closes tasks.lthn.sh/view.php?id=173
2026-04-25 19:57:57 +01:00
Snider
99b0865303 fix(agent/agentic): annotate net/http as AX-6 structural exception
pkg/agentic/transport.go is the low-level HTTP boundary behind
core.Drive / core.API — there is no exported core/api generic
request/response wrapper that covers protocol-level MCP POST/SSE
exchange. Codex preflight confirmed: http.Client state, request
construction, response handling, and header mutation are all
present and intrinsic.

Resolution: annotation rather than purge. Per AX-6 doctrine, when
a core wrapper genuinely doesn't exist, explicit annotation is the
correct outcome (vs forcing through a wrapper that doesn't fit).

Doc comment now explains: "structural HTTP transport boundary for
core.API protocol streams and raw MCP POST/SSE exchange; no
exported core/api generic request wrapper covers this file."

Future PR: when core/api exposes a generic request wrapper
(potentially via #197 TransformerIn/Out gateway), revisit this
file and replace where feasible.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=222
2026-04-25 19:52:38 +01:00
Snider
b0118ef8ef feat(core/events): add WebhookRegistering lifecycle event (HIGH)
WebhookRegistering event exposes:
- register(string $type, array $spec): add a webhook type to the
  registry
- types(): array — queryable post-dispatch registry

CoreServiceProvider dispatches the event at app boot and exposes the
collected registry via webhookTypes() — matches the existing
ApiRoutesRegistering / ConsoleBooting / ClientRoutesRegistering
event-driven module pattern.

Pairs with #1034 ofm.bot WebhookRegistrar (just landed) — that
service can now also be wired through this event, allowing OTHER
modules and external apps using Core to register webhook types via
the standard Core lifecycle.

Note: real Core lifecycle dispatcher lives in a sibling read-only
framework checkout. CoreServiceProvider here is a local shim that
mirrors the dispatch behaviour. Upstream patch needed when that
sibling lands.

Pest covers: instantiation + register, boot-time dispatch, post-boot
registry lookup.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1013
2026-04-25 19:37:23 +01:00
Snider
b7bc526d50 test(agent/brain): regression coverage for filter field bounds (closes #1000)
#1000 was stale-fixed: BrainService::recall() validates filter input
via the shared validator at line 489, which already bounds org,
project, type, agent_id. forget() bounds id at line 499.

These tests pin the safety claim explicitly:
- project=129 chars rejected
- agent_id=65 chars rejected
- project="core" accepted (sanity)
- project=128 chars accepted (boundary)

Note: BrainList.php (separate MCP list path) still lacks explicit
max lengths for project + agent_id — file outside this lane's allow-
list. File a follow-up if that surface needs the same bounds.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1000
2026-04-25 19:23:41 +01:00
Snider
599544010e feat(agent/mcp): McpContext::getScopes() + hasScope() (HIGH)
McpContext exposes the authenticated session's authorisation scopes
via getScopes(): array and hasScope(string): bool.

Resolution order:
1. Explicit scope source passed to constructor
2. Session-like object linked to an API key
3. Authenticated Laravel request context (mcp_workspace_context,
   agent_api_key, api_key)
4. Empty array (default) — never null

Dedupes scope strings, normalises separators in hasScope() matching.

Closes the OFM MCP tool gap where scope-gated tools currently return
empty/incorrect handling. No call-site stubs found needing update in
this worktree — call sites pick up the new method directly.

Pest covers: session scopes returned, hasScope present/missing, empty
session defaults to [], request-context regression against real MCP
auth shape.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1014
2026-04-25 19:04:35 +01:00
Snider
385b89b3eb fix(agent/brain): cap remember()/recall()/forget() input field sizes
Bound input field sizes against memory/DB/Qdrant bloat (DoS-by-self):
- content: 65536 bytes via mb_strlen
- tags: max 100 entries; each tag max 128 chars
- agent_id, type: 64 chars each
- project, org: 128 chars each
- supersedes_id: ULID-shape only

validateRememberInput() throws InvalidArgumentException at every entry
point (remember, recall, forget) before any DB or upstream call. Field-
specific error messages so callers know which field violated.

Pest covers good-path, content-too-long, tags-array-too-large, tag-
length, exact-boundary cases.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1001
2026-04-25 18:58:41 +01:00
Snider
dea64f4099 fix(agent/brain): walk supersede chain to current head + cycle guard
remember() now resolves a stale supersedes_id to the current live head
before writing — when X has been superseded by Y, a retried call with
supersedes_id=X automatically links the new memory to Y instead of
silently dropping the supersede.

- Walk the chain from supplied supersedes_id to find the active head
- Cap the walk at depth 100 (cycle/runaway protection)
- Throw RuntimeException("Detected cycle while resolving supersede chain")
  on detected cycle, BEFORE any DB write
- Throw InvalidArgumentException("Superseded memory not found") when
  the original supersedes_id never existed
- deleteSupersededMemory no longer silently no-ops once the resolved
  head is expected to exist

Pest coverage extended:
- Direct chain link (X exists, succeeds with X→linked)
- Retry path (X→Y, then retry on X produces Z→Y, walks chain)
- Never-existed target (graceful error)
- Synthetic X↔Y cycle (caps walk + throws, no writes leak)

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=316
2026-04-25 18:42:52 +01:00
Snider
167ce9783e fix(agent/brain): authorise org against MCP context at every entry point
remember(), recall(), forget(), and elasticSearch() now resolve the
allowed-orgs set from the authenticated request context (mcp_workspace_context),
preferring explicit authorised_orgs/authorized_orgs, falling back to the
authenticated workspace's org/slug. A mismatched org throws
AuthorizationException BEFORE any Qdrant/Elasticsearch call or destructive
DB action — closes the horizontal-priv-escalation vector where an MCP
client could recall/remember/forget memories scoped to ANY org by
setting org="other-org" in the request body.

Pest coverage in OrgScopingTest covers good path, unauthorised recall
(asserts no HTTP), cross-org forget (asserts no DB delete), unauthorised
remember (asserts no embed/index jobs).

Note: BrainList free-form org filter is a separate ticket — outside this
lane's allowlist.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=312
2026-04-25 18:32:19 +01:00
Snider
a1a0981b06 fix(agent/brain): retryableHttp narrows retryable set + 6-attempt budget
retryableHttp() now retries only 408 (Request Timeout), 429 (Too Many
Requests), and 503 (Service Unavailable). 500-and-other-5xx fail
immediately so the circuit-breaker registers them as a single
failure rather than smearing across retry attempts. Retry-After
honoured (numeric + HTTP-date), capped reasonably.

Attempt budget bumped to 6 so a burst of 5 transient 503s can recover
within ONE circuit-permitted call — the original concern from #311.

Note: CircuitBreaker is already applied OUTSIDE the logical Brain
operation by the MCP tool layer, not around each HTTP retry. The
nesting report was stale at this code shape; the real drift was the
retryableHttp() retry set + budget.

Pest coverage in CircuitBreakerTest:
- Recovered 503 burst → circuit stays closed, no failure registered
- Exhausted 503 burst → ONE breaker failure (not five)
- 429 + Retry-After 1 → sleeps 1s, no breaker failure
- 500 → immediate breaker failure, no retry

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=311
2026-04-25 18:14:40 +01:00
Snider
b6565263f3 fix(agent/brain): lock forget+supersede paths against late index writes
Cache::lock keyed by memory id wraps the delete path in BrainService::
forget(); supersede cleanup in remember() lifted to the same idiom.
forget() now ALWAYS queues DeleteFromIndex on a successful delete
(was previously skipped when indexed_at was null — left late writes
from stale preloaded models a window to land entries after the
underlying memory was gone).

Index write paths (qdrantUpsert / elasticIndex) re-check that the
memory row still exists before writing — defence-in-depth against any
future caller that holds a stale model reference past a forget.

Pest coverage extended in SupersedeForgetIndexCleanupTest:
- never-indexed forget queues cleanup
- late stale-model index writes are skipped after forget
- never-indexed supersede cleanup queues deletion
- late stale-model index writes are skipped after supersede

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=999
2026-04-25 18:04:55 +01:00
Snider
167be2f396 chore(security): add .gitleaks.toml for working-tree path allowlist (Athena #325 dev-exp)
The .gitleaksignore file uses per-commit fingerprints which only match
gitleaks's default with-git mode. Developers running `gitleaks detect --no-git`
locally (e.g. for working-tree review) saw 7 false positives that the
fingerprint format couldn't address.

This .gitleaks.toml adds path-based allowlists that apply to BOTH modes,
covering the same documented placeholder/test/example sites the .gitleaksignore
covers in history form:
- .core/vm/ Traefik cert keys (mode 0600, untracked, generated for local VM)
- php/docs/ + blade.php API placeholders
- php/tests/ test fixtures
- php/Services/AgentDetection.php docblock examples
- pkg/agentic/prep_test.go t.Setenv env-clearing literal

Verified: `gitleaks detect --no-git -c .gitleaks.toml` returns "no leaks found".
Default `gitleaks detect` (with-git) still uses .gitleaksignore + this config
together — both modes now report 0 leaks for the documented false positives.

Co-authored-by: Codex <noreply@openai.com>
2026-04-25 16:36:47 +01:00
Snider
f2b6ff29bd fix(agent): tighten directory perms in .core/reference/ siblings (Athena #988)
Mantis #324 narrowly tightened fs.go from 0644/0755→0600/0700. Athena audit
during task #20 closure-verification (2026-04-25) found sibling files in the
same directory still using 0755 for MkdirAll, leaving parent dirs world-listable
even when file content is 0600.

This commit applies the same hardening to:
- .core/reference/error.go:393 — crash-report parent dir 0755→0700
- .core/reference/embed.go:514/567/656 — workspace template extract dirs 0755→0700
- .core/reference/embed.go:595/660 — os.Create→os.OpenFile(...0600) for
  template renders + standard-file copies (default umask 0644 was leaking
  workspace-template content to other users on shared hosts)
- pkg/lib/workspace/default/.core/reference/error.go:414 — same crash-report fix
- pkg/lib/workspace/default/.core/reference/embed.go:518/571/660 — same template fixes

Workspace-template duplicates are kept in sync so newly-scaffolded workspaces
inherit the hardened perms instead of regressing to 0755/0644.

Closes Mantis #988.

Co-authored-by: Codex <noreply@openai.com>
2026-04-25 16:29:28 +01:00
Snider
6be6cb095c feat(agent/brain): adopt shared T1 client + propagate org through actions (#177)
#177 (T3/5 — direct subsystem adopts shared client):
- pkg/brain/direct.go: HTTP transport now delegates to shared T1 client
  in core/mcp's pkg/mcp/brain/client (retry, circuit breaker, org propagation)
- pkg/brain/actions.go: org now survives from action options through
  remember/recall/list calls
- pkg/brain/direct_test.go + actions_test.go: tests updated for org propagation

Tickets deferred:
- #179 (T5/5 — cross-runtime contract test + BRAIN-CALLERS.md): needs
  cross-repo edits to mcp + external runtime consumers
- #180 (lift RFC-OPENBRAIN features into vendored BrainService):
  base schema lacks memory_scope; no agentBoot, brain:consolidate,
  agent-context endpoint, or lifecycle events present

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=177
2026-04-25 16:22:38 +01:00
Snider
6832d40587 fix(agent/brain): batch — org maxLength + retry semantics + forget index cleanup
Codex 5.5 batch lane processed 8 brain Mantis tickets. 4 implemented,
1 stale, 3 deferred.

Tickets implemented:
- #313 — MCP schemas (BrainRemember/Recall/List): org field maxLength=128 with runtime validation; recall filter.org also bounded; pest test coverage added
- #314 — BrainList: removed withCircuitBreaker('brain') from DB-only handler; CircuitBreakerTest updated to assert no breaker call
- #315 — BrainService.retryableHttp(): now retries 408 (request-timeout), 429 (rate-limit), and 5xx; honours Retry-After header; focused retry tests added
- #326 — BrainService.forget(): dispatches DeleteFromIndex only when row has indexed_at (was unconditional); SupersedeForgetIndexCleanupTest covers never-indexed case

Tickets stale-fixed: #316 (RememberKnowledge already rejects missing/deleted supersedes target before dangling retry)
Tickets deferred: #121 (cross-surface audit), #311 (retry-inside-breaker architectural redesign), #312 (no authoritative org claim in MCP request context yet)

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=313
Closes tasks.lthn.sh/view.php?id=314
Closes tasks.lthn.sh/view.php?id=315
Closes tasks.lthn.sh/view.php?id=326
2026-04-25 14:55:40 +01:00
Snider
bf10d16f49 feat(agent): batch — sprint MCP tools + cmd cleanup (#142 #225 #226 #227)
Codex 5.5 batch lane processed 26 open Mantis tickets. 13 stale-fixed,
4 implemented, 9 deferred.

Tickets implemented:
- #142 — agentic_sprint_start + agentic_sprint_complete MCP tools wired to /v1/sprints/{id}/{start,complete} platform endpoints with tests
- #225 — cmd/core-agent/commands.go: removed raw flag parsing; startupArgs() uses Core arg filtering + local log-level strip
- #226 — cmd/core-agent/main.go: syscall.Exit(1) → core.Exit(1)
- #227 — pkg/agentic/dispatch.go: runtime.GOOS → Core environment-backed OS detection

Tickets stale-fixed: #161, #162, #163, #166, #167, #168, #171, #172, #223, #224, #230, #231, #232, #233
Tickets deferred: #160, #164, #165, #173, #222, #228, #229, #234

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=142
Closes tasks.lthn.sh/view.php?id=225
Closes tasks.lthn.sh/view.php?id=226
Closes tasks.lthn.sh/view.php?id=227
2026-04-25 14:55:23 +01:00
Snider
56a97e9178 fix(agent/brain): AX-6 sweep on direct.go — net/url → core.URLEncode + core.Join
url.Values manipulation replaced with []string builder + core.URLEncode
+ core.Join("&", params...). net/url import removed.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=966
2026-04-25 13:34:18 +01:00
Snider
a0ba74d220 fix(agent/monitor): AX-6 sweep on sync.go — net/url → core.URLEncode
url.QueryEscape → core.URLEncode for checkin URL agent param.
net/url import removed.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=965
2026-04-25 13:32:52 +01:00
Snider
14b0ef529c fix(agent/agentic): AX-6 sweep on scan.go — net/url → core.URLEncode
url.QueryEscape → core.URLEncode in listRepoIssues label encoding.
net/url import removed.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=963
2026-04-25 13:30:27 +01:00
Snider
fedb1f3b00 fix(agent/monitor): AX-6 sweep on monitor.go — net/url → core.URLEncode
url.QueryEscape → core.URLEncode for inbox URL agent param encoding.
net/url import removed.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=962
2026-04-25 13:30:27 +01:00
Snider
33a538a699 test(agent): NOSONAR annotations on httpx.MockTransport http:// fixtures (#939)
Cerberus DREAD review: SonarCloud S5332 fires on literal http:// strings,
but all 7 hits are in httpx.MockTransport closures (mock-only, never reach
real network). NOT REACHABLE.

Added per-line `# NOSONAR python:S5332` markers + module-level pattern
note in:
- claude/camofox_mcp/tests/test_server.py (lines 30, 32, 212, 214)
- claude/hermes_runner_mcp/tests/test_server.py (lines 23, 43, 56)

Closes tasks.lthn.sh/view.php?id=939

Co-authored-by: Codex <noreply@openai.com>
2026-04-25 12:12:56 +01:00
Snider
b118236410 fix(agent): replace bytes with core.NewBuffer in cmd/core-agent/commands.go
Removed bytes import. Used core.NewBuffer() for commandLine output buffer.

Co-authored-by: Codex <noreply@openai.com>
2026-04-25 10:20:20 +01:00
Snider
9ed15cbb42 fix(agent): replace sync/atomic plan ID generator with core.ID() (#863)
Removed local sync/atomic + crypto/rand + encoding/hex based plan ID
generator from pkg/agentic/plan.go. Switched planID() to core.ID()
primitive. Preserves id-{counter}-{suffix} shape via Core's primitive.

prep.go, sync.go, pkg/brain/*.go scanned — no sync imports remained
in non-test files.

Closes tasks.lthn.sh/view.php?id=863

Co-authored-by: Codex <noreply@openai.com>
2026-04-25 09:53:09 +01:00
Snider
e04f018b4c test(agent): add AX-10 unit tests for dispatch/session/sync/tools/content (#169)
Append-only — no existing tests modified.

- dispatch_test.go: TestDispatch_agentCommand_{Good,Bad,Ugly}
- session_test.go: TestSession_normaliseSessionAgentType_{Good,Bad,Ugly}
- sync_test.go: TestSync_syncBackoffSchedule_{Good,Bad,Ugly}
- tools_test.go: TestTools_RememberInput_{Bad,Ugly} (Good was pre-existing)
- content_test.go: TestContent_contentSchemaType_{Good,Bad,Ugly}

gofmt clean. Test verification deferred (private dappco.re/go/* deps
missing go.sum entries with GOWORK=off — would resolve under workspace).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=169
2026-04-25 07:57:30 +01:00
Snider
91551dec9b feat(mcp): implement extended RFC services + transport (#842)
Additive-only — no existing files modified.

Services (php/Mcp/Services/):
- CircuitBreaker (3-state, Cache::add trial lock)
- DataRedactor (28 sensitive + 16 PII keys, partial-redact algorithm)
- McpHealthService (YAML registry + JSON-RPC stdio ping protocolVersion 2024-11-05)
- McpMetricsService (p50/p95/p99 linear interpolation)
- McpWebhookDispatcher (mcp.tool.executed → WebhookEndpoints)
- OpenApiGenerator (OpenAPI 3.0.3)
- ToolRateLimiter (Cache::put first, Cache::increment after — no reset)
- AgentSessionService (php/Mod/Mcp/Services/ namespace per spec)

Transport (php/Mcp/Transport/):
- McpContext (transport-agnostic callbacks)
- Contracts/McpToolHandler interface

Resources (php/Mcp/Resources/):
- AppConfig, ContentResource, DatabaseSchema

Config: php/resources/mcp/registry.yaml.
Pest Feature tests _Good/_Bad/_Ugly per AX-10 for each new class.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=842
2026-04-25 05:50:16 +01:00
Snider
dffdad8418 feat(api): implement §3 fleet+credits+subscription+sync+agent-auth routes (#848)
Additive-only — appended to php/Routes/api.php (existing routes
preserved). Existing /v1/fleet/{nodes,heartbeat,stats} +
/v1/agent/auth/provision left untouched.

New routes:
- /v1/agent/auth/register
- /v1/fleet/dispatch + /v1/fleet/stream
- /v1/credits/{balance,deduct,refund,ledger}
- /v1/subscription/{status,upgrade,cancel}
- /v1/agent/sync/{push,pull}

New controllers under php/Controllers/Api/{Fleet,Credits,Subscription,
Sync,AgentAuth}/. Reference FleetService/CreditService/SessionService
when available with fallbacks to current action/model layer (pre #849).

Pest Feature coverage under php/tests/Feature/Api/. pest skipped
(vendor binaries missing in sandbox).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=848
2026-04-25 05:43:51 +01:00
Snider
470ce0de99 feat(agentic): implement §9 Services (FleetService + CreditService + SessionService) (#849)
Additive-only — no existing files modified.

- FleetService: wraps fleet actions+models, register/heartbeat/dispatch
  (direct or queued), node health snapshots, typed fleet stats
- CreditService: workspace-level balance/refund/deduct/ledger over
  credit_entries, returns typed CreditTransaction DTOs
- SessionService: RFC-§7 lifecycle session creation + guarded state
  transitions + SSE-style emission via Laravel events

DTOs: FleetStats, CreditTransaction (readonly).
Pest Feature tests _Good/_Bad/_Ugly per AX-10. pest skipped (vendor missing).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=849
2026-04-25 05:28:49 +01:00
Snider
066e1fee51 feat(mcp): implement §8 Console Commands (3 commands) (#853)
Additive-only — no existing files modified.

- McpAgentServerCommand: line-oriented JSON-RPC stdio loop over
  ToolRegistry with McpQuotaService + QueryAuditService hooks
- PruneMetricsCommand: prunes stale mcp_tool_metrics rows + aggregate
  reporting, fails cleanly when table missing
- McpMonitorCommand: status / alerts / export / report / prometheus
  subcommands, --json flag

Pest Feature tests _Good/_Bad/_Ugly per AX-10 for each command.
Boot.php registration deferred per scope (additive-only). pest skipped
(vendor binaries missing).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=853
2026-04-25 05:27:48 +01:00
Snider
8091bad2c0 feat(mcp): implement §4 Middleware (5 middleware classes) (#852)
Additive-only — no existing files modified.

- McpApiKeyAuth: validates Bearer or X-MCP-API-Key header, attaches
  workspace context
- CheckMcpQuota: consumes via McpQuotaService, exposes MCP quota headers
- ValidateWorkspaceContext: normalises + enforces authenticated workspace scope
- ValidateToolDependencies: JSON-RPC + flat tool-call payload validation
  via ToolDependencyService
- McpAuthenticate: combined auth gate chaining the full stack

Pest Feature tests _Good/_Bad/_Ugly per AX-10 for each middleware.
pest skipped (vendor binaries missing in sandbox).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=852
2026-04-25 05:25:09 +01:00