Commit graph

34 commits

Author SHA1 Message Date
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
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
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
ccedf536d6 fix(agent-tool-registry): harden rate limiting and api key identifiers
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 20:48:29 +01:00
Snider
618dd5470f hardening(agent): validate prep workspace writes
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 20:41:48 +01:00
Snider
70a3e93983 fix(agentic): write TODO.md in workspace prep
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 20:40:07 +01:00
Snider
1e8af462f2 fix(agentic): harden tool execution and template validation
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-17 20:25:19 +01:00
Snider
39914fbf14 refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.

Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00
Virgil
425008f855 feat(brain): expose supersession metadata
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 07:53:58 +00:00
Virgil
c4f5b77786 feat(agentic): add canonical generate command
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 04:54:20 +00:00
Virgil
ff24898cd4 feat(session): persist handoff notes on end
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 04:03:12 +00:00
Virgil
6cb5a9f39a feat(agentic): add workspace state aliases
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 03:59:30 +00:00
Virgil
c8a2d62d27 feat(brain): recurse seed-memory scans
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 03:55:45 +00:00
Virgil
886461ca28 feat(session): expose replay context on read scope
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 02:19:57 +00:00
Virgil
1f333fc53d fix(agent-session): preserve handed-off sessions
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 21:53:19 +00:00
Virgil
036c09c235 feat(php-agent): stream fleet events continuously
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 21:45:39 +00:00
Virgil
47bc3dc0fa feat(agentic): add task update and toggle console actions
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 12:55:04 +00:00
Virgil
25ee288bd2 fix(agentic): align php state and fleet runtime
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 08:08:04 +00:00
Virgil
073938ca6f fix(agent): harden sync fallback and state model
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 07:54:29 +00:00
Virgil
6c69005aff feat(agent): implement fleet and sync RFC surfaces
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 07:27:15 +00:00
Snider
bb88604045 feat(core): wire Core framework into agentic + monitor subsystems
Phase 2 of Core DI migration:
- Add *core.Core field + SetCore() to PrepSubsystem and monitor.Subsystem
- Register agentic/monitor/brain as Core services with lifecycle hooks
- Mark SetCompletionNotifier and SetNotifier as deprecated (removed in Phase 3)
- Fix monitor test to match actual event names
- initServices() now wires Core refs before legacy callbacks

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 14:44:53 +00:00
Snider
be1130f470 agent updates 2026-03-21 11:10:44 +00:00