Commit graph

758 commits

Author SHA1 Message Date
Snider
40dccb2a14 feat(agentic): implement §11 Admin UI Livewire components (FleetOverview + BrainExplorer + CreditLedger) (#850)
Additive-only — no existing files modified.

- FleetOverview: node list + status badges + dispatch button + stats panel
- BrainExplorer: semantic-recall search with DB fallback + forget action
- CreditLedger: balance display + transaction list + deduct/refund actions

Flux Pro components (no vanilla Alpine). Uses existing
fleet/brain/credit actions+services in this package.

Pest Feature tests _Good/_Bad/_Ugly per AX-10 — load classes directly
since composer.json + Boot.php were left untouched per scope. Future
follow-up: wire PSR-4 + view registration in Boot.php.

pest skipped (vendor binaries missing in sandbox).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=850
2026-04-25 05:16:50 +01:00
Snider
09054fbdab feat(mcp): implement §3 Services (ToolRegistry + McpQuotaService + QueryAuditService + ToolDependencyService) (#851)
Additive-only — no existing files modified.

- ToolRegistry: register/resolve/listTools/buildDependencyGraph
  - Singleton via registerSingleton() entry point (no Boot.php wire-in
    per scope; tests cover the binding path)
- McpQuotaService: workspace-scoped checkQuota/consume/reset
- QueryAuditService: log/query/aggregate (expects mcp_audit_entries
  table; tests create inline as migration was out-of-scope)
- ToolDependencyService: validateDependencies via graph traversal

Data DTOs: ToolMetadata, QuotaResult, AuditEntry as readonly.
Pest Feature tests _Good/_Bad/_Ugly per AX-10.
pest skipped (vendor binaries missing).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=851
2026-04-25 05:14:15 +01:00
Snider
f293d48006 fix(agent): tighten workspace file perms 0644→0600 to protect extracted secrets (Cerberus #324)
.core/reference/fs.go (canonical) + pkg/lib/workspace/default/.core/reference/fs.go (embedded copy):
- Write/WriteAtomic/Create/Append default to 0600
- Parent directories use 0700 (was 0755)
- WriteMode reapplies the requested mode after writes so overwriting an
  existing file also tightens permissions

Test (pkg/lib/lib_test.go) keeps embedded fs.go synced with canonical +
asserts extracted workspaces carry the secure permission defaults.

tests/cli/extract copy not hand-edited — that flows from regeneration.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=324
2026-04-25 04:19:30 +01:00
Snider
93c57fd487 chore(security): add .gitleaksignore for 18 documented false-positives
Closes Mantis #325 (agent portion).

Each fingerprint listed is a documentation placeholder, test constant, or
env-clearing call manually verified to be safe — not a real secret. The
fingerprint format anchors per-commit so a future legitimate leak in the
same file/rule will still be caught.

Categories:
- pkg/agentic/prep_test.go    — t.Setenv("CORE_BRAIN_KEY", "") env-clear
- pkg/orchestrator/security_test.go — MaskToken test fixture
- php/docs/api-keys.md        — curl-auth-header documentation example
- php/View/Blade/admin/api-key-manager.blade.php — same
- php/tests/Unit/ClaudeServiceTest.php — 'test-api-key' literal
- php/tests/Feature/AgentApiKeyTest.php — 'ak_test_key_*' fixture
- php/Services/AgentDetection.php — docblock example
- src/php/* — older path of same files (pre-migration commits)

Verification: gitleaks detect → 19 → 0 findings.

Co-Authored-By: Argus <argus@lthn.ai>
Co-Authored-By: Athena <athena@lthn.ai>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-25 01:13:00 +01:00
Snider
ba8de0c0bb fix(agent): purge sync.Once from pkg/agentic via core.Once (§14A)
Closes Mantis #863 ([agent] Phase 2: purge sync stdlib).

Per RFC plans/code/core/go/RFC.primitives-lifecycle.md §14A (landed core/go
dev 8995a80), swaps the four sync.Once usages to core.Once and the two
sync.Once{} reset-pattern callsites to core.Once.Reset():

pkg/agentic/statestore.go:
- Drop `import "sync"`.
- stateStoreRef.once: sync.Once → core.Once
- closeStateStore reset: `s.stateOnce = sync.Once{}` → `s.stateOnce.Reset()`

pkg/agentic/workspace_stats.go:
- Drop `import "sync"`.
- workspaceStatsRef.once: sync.Once → core.Once
- closeWorkspaceStatsStore reset: `s.workspaceStatsOnce = sync.Once{}` →
  `s.workspaceStatsOnce.Reset()`

pkg/agentic/prep.go:
- Drop `import "sync"`.
- PrepSubsystem.stateOnce + .workspaceStatsOnce: sync.Once → core.Once

The Reset() pattern matches stdlib semantics (see RFC §14A "Tradeoff: Once.
Reset semantics") — caller serialises via the existing closeStateStore /
closeWorkspaceStatsStore structure that nests Reset inside the lifecycle
inverse, so no concurrent Do races are introduced.

Net: 3 files, +7/-11. Mechanical line-edit per RFC §16 migration plan.

Audit re-check post-commit:
  grep -n '"sync"\|sync\.Once\|sync\.Mutex' pkg/agentic/{statestore,workspace_stats,prep}.go
  → empty (lib local variable named `sync` in mirror.go is unrelated; not
    in scope of this ticket).

Pre-flight verification: core.Once + Reset symbols verified present on
core/go dev 8995a80. Local AX-10 build blocked by the same pre-existing
workspace forge dep break that affects all consumers (root cause: fake
v0.8.0-alpha.1 pins per task #28); CI in healthy env will validate.

Co-Authored-By: Athena <athena@lthn.ai>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-25 00:58:49 +01:00
Snider
34010f6d35 feat(ax-10): bring agent to v0.8.0-alpha.1 + CLI test scaffold
- Bump dappco.re/go/* deps to v0.8.0-alpha.1 in go.mod (any forge.lthn.ai/core/* paths migrated to canonical dappco.re/go/* form)
- Update Go source imports across 29 .go files
- Add tests/cli/agent/Taskfile.yaml AX-10 scaffold (build/vet/test under default deps), per RFC-CORE-008-AGENT-EXPERIENCE.md §10

Co-Authored-By: Athena <athena@lthn.ai>
2026-04-24 23:48:34 +01:00
Snider
c616ff1e32 fix(brain): close openbrain audit gaps — org scoping + index cleanup + reindex flags + MCP schemas + circuit layer
Closes the 5 PARTIAL items flagged in docs/AUDIT-openbrain-20260424.md.

- Gap A (org scoping persisted on writes): new migration adds `org`
  nullable+indexed column to brain_memories; BrainMemory fillable;
  RememberKnowledge action forwards org; BrainService::remember
  persists it.

- Gap B (supersede/forget Elastic cleanup): BrainService::forget
  dispatches DeleteFromIndex (handles both Qdrant + Elastic); supersede
  path dispatches cleanup for the old memory id before replacing it.
  DeleteFromIndex itself untouched — already handled both indexes.

- Gap C (brain:reindex flags): --org, --project, --stale (null OR
  >14d old), --dry-run (count+stop), --elastic-only added to the
  artisan command.

- Gap D (MCP schemas expose org): brain_remember, brain_recall,
  brain_list now accept `org` in input schema + forward into
  action/service.

- Gap E (resilience uneven): brain_list now wrapped in
  withCircuitBreaker('brain', ...) matching the pattern used by
  BrainRemember/Recall/Forget. BrainService gains retryableHttp()
  helper — 100/300/900ms exponential backoff, retries only on 5xx +
  connection errors, not on 4xx. Qdrant calls route through it;
  Ollama left alone (EmbedMemory job has its own retry).

Tests (Good/Bad/Ugly per gap):
- Feature/Brain/OrgScopingTest.php
- Feature/Brain/SupersedeForgetIndexCleanupTest.php
- Feature/Brain/ReindexFlagsTest.php
- Feature/Mcp/BrainSchemaOrgTest.php
- Feature/Brain/CircuitBreakerTest.php

php -l clean on all 13 files. Pest binary not in this checkout —
CI path validates the full suite.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-24 08:14:06 +01:00
Snider
8f3b39983a docs(openbrain): alignment audit 2026-04-24 — PARTIAL verdict
Section-by-section verdict with file:line evidence against
RFC-OPENBRAIN.md. Headline gaps:

- org scoping not persisted on writes (migration, model fillable,
  remember action all only handle project)
- supersede path leaves Qdrant/Elastic stale
- forget path leaves Elastic stale
- brain:reindex missing --org/--project/--stale/--dry-run/
  --elastic-only flags
- MCP tool schemas don't expose org for write/list
- Resilience uneven: brain_list skips circuit breaker;
  BrainService HTTP layer has no retry/circuit

Verdict: PARTIAL. Queueing pipeline, Qdrant/Ollama/Elastic
plumbing, and MCP tool surface are in place; scoping +
index-consistency-on-mutation are the follow-up work.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-24 05:36:59 +01:00
Snider
4c1fa56d17 fix(brain): wire Qdrant api-key header from BRAIN_QDRANT_API_KEY
BrainService::http() was building a PendingRequest with no auth
header, so when Qdrant has auth enabled (the production lthn.sh
deploy does) every upsert/lookup returned 401. The circuit breaker
logged the 401 via Cache::store('file'), which was the red-herring
cache-write error chased in the first #97 iteration.

Changes:
- BrainService loads + trims a Qdrant api key from
  config('brain.qdrant.api_key') in the constructor.
- New qdrantHttp() helper returns a PendingRequest with the
  api-key header when the key is non-empty, or the plain client
  otherwise. Ollama + Elasticsearch call sites still use http()
  (separate auth shapes).
- php/config.php adds a brain.qdrant.api_key entry reading
  env('BRAIN_QDRANT_API_KEY').
- Good/Bad/Ugly Pest tests cover: configured key → header sent,
  unset → header absent, empty-string → header absent.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-24 05:21:15 +01:00
Snider
7639f56c2d test(brain): partial MCP smoke-test for remember/list/forget
Exercises the 3 MCP handlers that work MariaDB-only (no Qdrant
dependency): brain_remember writes + returns id, brain_list
surfaces it, brain_forget removes. Negative case: brain_forget on
a non-existent id returns a proper error response (not TypeError).
brain_recall is out of scope — needs the Qdrant collection +
embedding pipeline.

Implementation note: handlers use `type` + workspace context for
scoping, not a `scope` parameter; the test matches the actual
signatures.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:57:22 +01:00
Snider
c0c326967c docs(openbrain): deprecate stale per-repo RFCs, redirect to authoritative spec
Replaced both docs/php-agent/RFC.openbrain-{design,impl}.md with
~12-line redirect pointers to plans/project/lthn/ai/RFC-OPENBRAIN.md
(the 728-line authoritative spec). Agents dropped into core/agent
will no longer implement the obsolete single-collection /
nomic-embed-text / synchronous model.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:40:10 +01:00
Snider
a50e3d8291 test(agentic): add HTTPS cert regression tests + fleet sync audit
Fleet registration in pkg/agentic already goes through the shared
&http.Client{Timeout: 30s} at transport.go:13 — no InsecureSkipVerify,
no custom TLS transport. This audit documents that finding and adds
regression coverage so future refactors can't silently strip TLS
validation from the /v1/fleet/register path.

Verdict: OK. No production bug. Tests pass trusted TLS server case
and reject untrusted cert with a wrapped error that surfaces the
certificate / x509 / tls signal in the message.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:40:02 +01:00
Snider
9a90b9a651 docs(camofox): fix stale pip install URL to forge.lthn.ai
Picks up the residual dappcore→core rename hit that was out of scope
for #92's narrow allowlist.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:22:37 +01:00
Snider
1095795b1c docs(fleet): document fleet task ≠ AgentSession separation
Fleet tasks deliberately do not create AgentSession records.
AgentSession's work_log / artefacts / handoff / replay semantics are
designed for interactive, replayable, handoff-capable work — fleet
tasks are atomic assign→complete events with no in-between state
to replay. If a fleet-task handler needs session semantics, it
should start its own AgentSession via AgentSessionService when the
work begins.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:21:40 +01:00
Snider
2a8b847e29 refactor(plugins): complete dappcore→core rename, add Gemini stub
Rename legacy dappcore/dAppCore/dappco.re plugin-metadata identifiers
to the canonical core namespace: marketplace name is core-agent,
plugin manifests point at https://lthn.ai and
https://forge.lthn.ai/core/agent.git. Align the two .mcp.json files
on the new "core mcp serve" command. Scaffold google/gemini-cli with
a minimal plugin.json + README so the cross-agent plugin family is
complete.

Out of this ticket's scope: live Go module import paths at
dappco.re/go/core (that's a separate migration), and a stale pip
install URL in claude/camofox_mcp/README.md (follow-up child ticket).

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:21:40 +01:00
Snider
0aee349d57 feat(plugins): scaffold core-go/core-php/infra plugin directories
Creates three new Claude plugin family directories per the
plugin-restructure RFC: core-go (Go-family tooling), core-php
(PHP-family tooling), infra (ops / devops / deploy tooling). Each
carries .claude-plugin/plugin.json, YAML marketplace.yaml, README,
and commands/agents/skills stubs. Marketplace format is YAML per
RFC; the legacy JSON marketplace for core-agent is left untouched.

Appends a Resolution section to the plugin-restructure RFC recording
the YAML-over-JSON decision and noting that the dappcore→core rename
at cross-plugin metadata level is scoped to #92.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:21:40 +01:00
Snider
d4f2fa9204 refactor(forge): route ScanForWork + ManagePullRequest through MetaReader
ScanForWork and ManagePullRequest now depend on the MetaReader
interface (added in #89) instead of reading raw Forgejo body /
description / PR text. Epic child-linkage comes from
EpicMeta.children, PR merge decisions come from PRMeta.state /
mergeability / checkStatuses. The returned shape drops issue_body
and replaces it with structural issue_state / issue_labels.

Adds a feature test that injects a mocked MetaReader carrying
intentionally-tainted body/description/review_text fields and
recursively asserts none of those keys appear in the output of
either action — the regression fence for the RFC rule that body
content must never reach pipeline decisions.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:21:28 +01:00
Snider
3f5f4d15fe fix(mcp): SessionArtifact passes description as metadata array
AgentSession::addArtifact expects ?array $metadata in the third
argument slot; the MCP tool was passing the optional description
string directly, producing a TypeError whenever a caller supplied a
non-null description. Wrap the description into a metadata array so
the call matches the model signature, and add a feature test that
exercises the MCP handler end-to-end to prevent regression.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:10:21 +01:00
Snider
5e2aecd68a feat(sync): update WorkspaceState workflow progress on dispatch push
Extend PushDispatchHistory so /v1/agent/sync writes four sync.*
workflow-progress keys into WorkspaceState (last_dispatch_at,
last_agent_type, last_findings_count, last_status) in addition to the
existing BrainMemory + SyncRecord persistence. Plan resolves via
agent_plan_id first, plan_slug fallback. Missing plan is treated as
non-fatal — state writes are skipped, BrainMemory still persists.

Adds a three-case feature test covering direct id, slug fallback, and
the missing-plan safety branch.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:10:17 +01:00
Snider
e83c3d811d feat(pipeline): add MetaReader contract + Forgejo-backed implementation
Introduce a pipeline metadata surface that enforces "no body content
ever reaches pipeline decisions". MetaReader is an interface with four
methods (getPRMeta, getEpicMeta, getIssueState, getCommentReactions),
each returning a readonly DTO carrying only structural fields —
state, mergeability, SHAs, branches, reaction counts, child linkage.
ForgejoMetaReader projects raw Forgejo API payloads into these DTOs
and drops body/description/review text before the caller can see it.

Unit test mocks rich Forgejo payloads containing body, description,
review_text, and comment_body, then asserts the DTO toArray output
never exposes those keys — the regression fence for the RFC rule.

Downstream callers (ScanForWork, ManagePullRequest) still use the
raw ForgejoService today; that refactor lands under Mantis #90.

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

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 18:09:54 +01:00
Snider
d6ddb9f2e6 feat(claude): add hermes_runner_mcp stdio MCP server
Python MCP server that dispatches sandboxed Hermes runs from Claude Code.
Exposes hermes_dispatch / hermes_status / hermes_fetch as MCP tools so
Cladius can offload work to Hermes runners via `claude mcp add hermes-runner`.
Passes --agents JSON through for dynamic subagent composition.

Closes tasks.lthn.sh/view.php?id=78
Co-authored-by: Codex <noreply@openai.com>

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 17:44:14 +01:00
Snider
b5783a16f2 feat(claude): add camofox_mcp stdio MCP server
Python MCP server wrapping the Camofox browser HTTP API on lthn.sh.
Exposes navigate/read_page/screenshot/click/fill/close_tab as MCP tools
so any Claude Code session can drive Camofox via `claude mcp add camofox`.

Closes tasks.lthn.sh/view.php?id=77
Co-authored-by: Codex <noreply@openai.com>

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 17:43:30 +01:00
Snider
124fa6e6d7 docs(hermes): add openbrain-recall + openbrain-remember SKILL.md
Two Hermes skill files that auto-register OpenBrain memory tools when
the MemoryProvider plugin loads. Each names the triggering phrases,
the tool contract, and an example invocation so Hermes can route
recall/remember prompts without coaching.

Closes tasks.lthn.sh/view.php?id=75
Co-authored-by: Codex <noreply@openai.com>

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 17:38:00 +01:00
Snider
711e2eef72 feat(hermes): add openbrain_context.py ContextEngine plugin
Python plugin implementing Hermes ContextEngine backed by OpenBrain.
compress() does centrality-ranked retrieval over a candidate pool
pulled via brain_recall rather than linear turn truncation. Falls
back to naive head+tail truncation when recall is unavailable so the
caller never sees a raised exception.

Closes tasks.lthn.sh/view.php?id=74
Co-authored-by: Codex <noreply@openai.com>

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 17:29:50 +01:00
Snider
5a851c2f4a feat(hermes): add openbrain_memory.py MemoryProvider plugin
Python plugin implementing Hermes MemoryProvider ABC backed by OpenBrain
(Qdrant + Postgres + PHP BrainService HTTP API). Exposes is_available,
initialize, get_tool_schemas for the four brain_* MCP tools,
handle_tool_call dispatch, sync_turn non-blocking writes, Librarian-stance
system_prompt_block, on_session_end flush.

Closes tasks.lthn.sh/view.php?id=73
Co-authored-by: Codex <noreply@openai.com>

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 17:12:10 +01:00
Snider
e41d3f4d1e fix(composer): autoload php/ not src/php/ to match actual tree layout
After the e2d1d32 → revert cycle the code moved back to php/ but
composer.json autoload paths stayed pointing at src/php/ (which
does not exist). package:discover fails with "Class
Core\\Mod\\Agentic\\Boot not found" as soon as a downstream
consumer composer-installs the dev branch.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 15:43:39 +01:00
Snider
62dc3f3b69 docs(agent): verification audit of pipeline + plugin restructure + session lifecycle
Three-way audit against RFC.pipeline.md, RFC.plugin-restructure.md, and
RFC.md §13. Produces proposed follow-up ticket summaries per gap found.
No source code modified.

Closes tasks.lthn.sh/view.php?id=36
Co-authored-by: Codex <noreply@openai.com>

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 15:25:43 +01:00
Snider
296ed59b1b feat(brain): add GET /v1/brain/search — ES full-text endpoint
New endpoint GET /v1/brain/search?q=<query>&org=<>&project=<>&limit=<N>
that full-text-searches brain_memories via Elasticsearch using
BrainService::elasticSearch(). Separate from /v1/brain/recall (which is
vector/semantic via Qdrant) — this one is keyword/lexical.

Sits under the existing brain.read-auth middleware group.

Pest coverage (Http::fake for ES): Good (matches), Bad (invalid limit),
Ugly (empty query + filters).

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=64
2026-04-23 13:54:11 +01:00
Snider
a13f4c4bbd feat(brain): add GET /v1/brain/tags + /v1/brain/scopes
Two introspection endpoints for OpenBrain:
- GET /v1/brain/tags — ES terms aggregation over tags.keyword, returns
  {tag: count} pairs for UI filter chips
- GET /v1/brain/scopes — composite aggregation over {org, project},
  returns the scope hierarchy present in the index

Sits under the existing brain.read-auth group in Routes/api.php. New
BrainService helpers for aggregation shape; reuses the elasticSearch
HTTP path added in #59.

Pest coverage with Http::fake for ES.

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=65
2026-04-23 13:52:50 +01:00
Snider
6da45637f5 feat(brain): extend /v1/brain/recall with org + keywords + boost_keywords
Hybrid recall: vector (Qdrant) + keyword (ES) with dedupe-by-id and
score re-ranking via configurable boost multiplier.

- org: string → adds to Qdrant must-filter + ES filter
- keywords: array<string> → when present, ES full-text hits merge into
  results; without keywords, path stays purely vector
- boost_keywords: array<string> → each matched boost-keyword amplifies
  the memory's score by mcp.brain.boost_keywords_multiplier (default 1.5)

BrainService gains a hybridRecall() helper; BrainController::recall()
delegates to it. Existing request fields (query, limit, workspace_id,
project, type) unchanged.

php/tests/Feature/Api/BrainRecallExtendedTest.php — Pest coverage with
Http::fake for both Qdrant + ES, asserting dedupe + boost behaviour.

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=63
2026-04-23 13:51:37 +01:00
Snider
34525038a8 feat(brain): add brain:prune artisan command
New artisan command brain:prune {--older-than=90} {--chunk=100} {--dry-run}
that completes the soft-delete → hard-delete lifecycle by:
1. selecting BrainMemory::onlyTrashed() where deleted_at < now - N days
2. dispatching DeleteFromIndex for each (Qdrant + ES cleanup)
3. forceDelete()'ing the rows

--dry-run counts without dispatching.

Complements brain:clean (which cleans recent soft-deletes) with a
retention-bounded terminal cleanup.

Pest coverage: Good (dispatch + forceDelete on aged trashed rows), Bad
(invalid chunk), Ugly (--dry-run skips both dispatch and delete).

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=62
2026-04-23 13:36:41 +01:00
Snider
638091f3b3 feat(brain): add brain:clean artisan command
New artisan command brain:clean {--chunk=100} {--dry-run} that dispatches
the DeleteFromIndex job for soft-deleted BrainMemory rows (those in
onlyTrashed scope). Cleans up orphaned Qdrant + Elasticsearch index
entries that remain after a memory is soft-deleted.

--dry-run counts without dispatching.

php/tests/Feature/Console/BrainCleanCommandTest.php covers Good
(dispatches on trashed), Bad (invalid chunk), Ugly (--dry-run prevents
dispatch).

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=61
2026-04-23 13:30:27 +01:00
Snider
677d890308 feat(brain): make BrainService::remember() async via EmbedMemory job
remember() now writes the brain_memories row with indexed_at=null and
dispatches EmbedMemory::dispatch($memory->id) for async Qdrant + ES
indexing, instead of calling qdrantUpsert() synchronously. Response shape
matches the row state — caller gets the memory immediately, the Job
flips indexed_at once the Qdrant write succeeds.

Superseded rows still soft-delete synchronously (part of the remember
contract, not the indexing path).

php/tests/Feature/Services/BrainServiceRememberTest.php uses Queue::fake()
to assert EmbedMemory is dispatched and BrainService::qdrantUpsert() is
NOT called directly (subclass probe).

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=55
2026-04-23 13:29:30 +01:00
Snider
47f241d880 feat(brain): add brain:reindex artisan command
New artisan command brain:reindex {--all} {--chunk=100} that dispatches
the EmbedMemory job for brain memories needing (re)indexing. Without
--all, only memories where indexed_at IS NULL are dispatched; --all
re-embeds every memory (useful after a Qdrant collection wipe or
embedding model change). Uses chunkById for memory-safe iteration at
scale.

php/tests/Feature/Console/BrainReindexCommandTest.php covers Good
(unindexed-only default), Bad (invalid chunk), Ugly (--all flag).

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=60
2026-04-23 13:29:18 +01:00
Snider
f223a77daa feat(brain): add Elasticsearch indexing to BrainService
Fills in the elasticIndex/elasticDelete stubs added by #56 and #57, plus a
new elasticSearch() method used by the upcoming /v1/brain/search endpoint
(#64).

- elasticIndex(BrainMemory) → PUT /brain_memories/_doc/{id}
- elasticDelete(string $id) → DELETE /brain_memories/_doc/{id}
- elasticSearch(string $query, array $filters) → POST /brain_memories/_search
- ES URL default http://127.0.0.1:9200 (config override via
  BRAIN_ELASTICSEARCH_URL env var)
- RuntimeException on HTTP failures (same pattern as qdrantUpsert)

php/tests/Feature/Services/BrainServiceElasticTest.php covers Good/Bad/Ugly
for index, delete, and search using Http::fake.

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=59
2026-04-23 13:11:29 +01:00
Snider
8d520adb5e feat(brain): add DeleteFromIndex job
Inverse of the EmbedMemory job (#56): removes a memory from Qdrant (and
the future Elasticsearch index) when brain_forget fires or a memory is
soft-deleted.

- php/Jobs/DeleteFromIndex.php — Laravel Job, 3 retries with backoff
- BrainService: qdrantDelete() private→public and now throws on HTTP
  failure (was silent Log::warning — wouldn't trigger Job retry)
- elasticDelete() stub added (fills in with the ES integration ticket)
- php/tests/Feature/Jobs/DeleteFromIndexTest.php — success + HTTP-failure
  paths via mocked Http

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=57
2026-04-23 12:55:45 +01:00
Snider
4dc5ed8d14 feat(brain): add EmbedMemory job + indexed_at tracking
Implements the async-embedding pipeline's worker side:

- php/Jobs/EmbedMemory.php — Laravel Job that calls BrainService::embed()
  + qdrantUpsert() and sets indexed_at on success
- php/Migrations/…_add_indexed_at_to_brain_memories.php — nullable
  timestamp + index, portable across pgsql/mariadb (hasColumn guard)
- BrainMemory: +indexed_at fillable + datetime cast + PHPDoc
- BrainService: qdrantUpsert() private→public so the Job can use it;
  elasticIndex() stub added (to be implemented by the ES ticket)
- php/tests/Feature/Jobs/EmbedMemoryTest.php — Pest tests for success
  path and Qdrant-failure path

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=56
2026-04-23 12:47:10 +01:00
Snider
0741fba88f fix(codex): config.toml compat with codex CLI 0.122+
- model_reasoning_effort: "extra-high" → "xhigh" (the variant name was
  tightened in a recent codex CLI release; "extra-high" now fails config
  load)
- Remove [model_providers.ollama] and [model_providers.lmstudio]
  overrides — codex CLI 0.122+ reserves these as built-in provider IDs
  and rejects overrides. The same localhost endpoints are used by the
  built-ins, so the overrides were redundant anyway. Profiles that
  reference `model_provider = "ollama"` continue to work via the
  built-in.
2026-04-23 12:32:58 +01:00
Snider
bd060c8aa0 feat(brain): add org filter to BrainService::buildQdrantFilter()
Adds an `org` match filter between workspace_id and project in the Qdrant
payload filter chain. Multi-org isolation for OpenBrain memory retrieval.

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=58
2026-04-23 12:32:58 +01:00
Snider
4e190dc7ec fix(brain): Postgres portability for brain connection + migrations
Three related fixes so the brain DB works on Postgres, not just MariaDB:

1. config.php — brain charset/collation was hardcoded to utf8mb4 which
   Postgres rejects as client_encoding. Now driver-aware: utf8 for
   pgsql, utf8mb4 otherwise. Override via BRAIN_DB_CHARSET env var.

2. Migration 000008 (create_brain_memories) — self-referential FK on
   supersedes_id was declared inside Schema::create{}, causing Postgres
   to evaluate it before the PK index existed ('no unique constraint
   matching given keys'). Split into Schema::create + separate
   Schema::table to guarantee PK is in place when FK is added.

3. Migration 000009 (drop workspace FK) — try/catch inside the Blueprint
   closure couldn't catch deferred SQL failures. Replaced with a
   constraint-exists pre-query against information_schema, supporting
   both pgsql and mariadb/mysql drivers. Fresh installs no longer fail
   trying to drop a constraint that was never created.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 12:32:58 +01:00
e58986a3b4 revert fcb9c189e5
revert fix(agentic): harden TODO workspace write

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 12:32:57 +01:00
Codex
cbc262add4 fix(agentic): harden TODO workspace write
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-23 12:32:57 +01:00
Snider
401487301a feat(agent): gpt-5.4-mini/mature pass 5
- `go test ./... -count=1 -timeout 60s`

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-18 08:55:41 +01:00
Snider
b6d67ae634 feat(agent): gpt-5.4-mini/mature pass 4
Commit landed on `dev` at `a7c16de9715a653bc335d076982eaf9ce04b54bc`.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-18 08:48:06 +01:00
Snider
beff657e57 feat(agent): gpt-5.4-mini/mature pass 3
- `go test ./pkg/agentic -count=1 -timeout 60s`

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-18 08:42:14 +01:00
Snider
60f4cb6fdb feat(agent): gpt-5.4-mini/mature pass 2
- `git add` / `git commit` fail with `Operation not permitted` on `.git/index.lock`
  - even a plain `touch .git/...` is blocked

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-18 08:34:52 +01:00
Snider
651783e1f5 feat(agent): gpt-5.4-mini/mature pass 1
Commit:

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-18 08:32:10 +01:00
Snider
43568cae01 test(agentic): cover message and dispatch sync contracts
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 21:01:10 +01:00
Snider
2daabf27f7 fix(agentic): check append write failures
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 20:52:37 +01:00
Snider
4cea9555d4 fix(agentic): reject empty MCP session ids
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 20:50:17 +01:00