feat(architect): host-mode agent bootstrap (M18-4) #180
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!180
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/165"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Bootstraps the
architectagent type per M18-4: a host-mode, singletonplanning + spec-writing agent that runs inside the claude-hooks
service process (no container) and is driven through a chat-style
HTTP API rather than webhook dispatch.
types.architectinconfig/agents.json(container.enabled: false,plugins: [],default_model: "claude-opus-4-7[1m]").container.enabled: falsebehind a hard-codedHOST_MODE_TYPESallowlist in
webhook-config.ts— any other type trying to opt outof container isolation fails
loadWebhookConfigat boot.ResolvedAgent.host_modeat merge time;runAgentTaskbranches on the flag to skip worktree / container / docker-exec
plumbing and invoke the SDK inline with
cwd: process.cwd().apps/server/src/architect.ts—architect_sessionsSQLitetable + CRUD +
runArchitectTurnthat resumes the claude sessionid between chat turns, and the HTTP handlers for the four new
endpoints.
POST /architect/chat,GET /architect/sessions,GET /architect/sessions/:id,DELETE /architect/sessions/:id,GET /architect/stream/:task_id. The SSE stream reuses the globalbroadcastSSEfeed with a per-subscriber filter ontask_id, soevents match the existing envelope.
resolveAgentByUser/resolveAgentByTypenow skip host-mode types, and a newNON_DISPATCHABLE_TYPESset inwebhook-routing.tscarries theexplicit allowlist check used by tests.
handleAgentsCreaterefuses a secondarchitectrow with409 Conflict;db.ensureDefaultForTypesseeds
architect-defaulton first boot after upgrade.canUseTooldeniesgit push origin main/masterpatterns and
mcp__forgejo__merge_pull_requestoutright — thearchitect delegates implementation instead of performing it.
Agent section with chat API reference + provisioning runbook.
architect.test.ts(22 tests — SDK options, SQLite CRUD,HTTP handlers with a fake worker), host-mode validation in
webhook-config.test.ts, routing sanity inwebhook-routing.test.ts. Full suite: 668 pass / 0 fail.scripts/smoke-creds.shlearns to probe host-mode agents:filesystem + token-file check, skipping every
docker execpath.Closes #165
Test plan
just qa— typecheck, lint, format, tests all green.grepconfirms).just agent-env-sync architectandverify
~/.config/claude-hooks/agent-env/architect/populated.curl POST /architect/chatreturns 202 with session_id+ task_id once architect token is provisioned.
curl GET /architect/stream/:task_idstreams SSEevents matching the
/eventsenvelope shape.Review: feat(architect): host-mode agent bootstrap (M18-4)
CI: ✅ Green (run #1770, sha
839c7ae, 3m59s)Acceptance criteria — all met
types.architectin agents.json (container.enabled: false,plugins: [], model 1M)ensureDefaultForTypesseedsarchitect-default;POST /agents409 on secondResolvedAgent.host_modederived fromcontainer.enabled === falseat merge timerunAgentTaskbranches onhost_mode: skips worktree/container, usesprocess.cwd()HOST_MODE_TYPESallowlist gate — other types withenabled: falsefail boot loudhostModeConfigDir(); fallback to deterministic pathcanUseTooldeniesgit push origin main/masterandmcp__forgejo__merge_pull_requestresolveAgentByUser/resolveAgentByTypeskip host-mode typesNON_DISPATCHABLE_TYPEScarries explicit webhook blockPOST /architect/chat,GET/DELETE /architect/sessions, stream endpointarchitect_sessionsSQLite table with id/created_at/updated_at/title/messages/claude_session_idbroadcastSSEfeed filtered ontask_idclaude_session_idhidden from the GET transcript responsearchitect.test.ts— 22 tests; webhook-config + webhook-routing testssmoke-creds.shprobes host-mode path (filesystem + token file, no docker exec)Code notes (non-blocking)
apps/server/src/architect.ts—merge_pull_requestin advertised surfaceFORGEJO_TOOLS_ALLOWLISTincludesmerge_pull_request, sobuildArchitectSdkOptionshands it to Claude inallowedTools. ThecanUseToolcallback correctly denies it with a message at execution time, which is the right approach for a deny-with-explanation (vs. silent capability hiding). Functionally sound — the denial is SDK-enforced, Claude can't bypass it. A future hardening pass could additionally filter the entry out ofallowedToolsfor the architect for defense-in-depth, but it's not a correctness issue here.apps/server/src/architect.ts::runArchitectTurn— no unit test for the session-capture + message-append pathThe session resume /
setClaudeSessionId/appendMessagelogic insiderunArchitectTurnis exercised only through the real SDK in production, not in tests. The HTTP-handler tests use a fake worker sorunArchitectTurnis never entered. The SQLite CRUD tests cover the DB primitives in isolation. This is an acceptable tradeoff given SDK calls can't be easily mocked, and the individual building blocks are all tested directly.apps/server/src/db.ts::ensureDefaultForTypes— clean upgrade path for the architect-default row on existing non-empty deployments. Idempotent on every boot. Exactly right.Summary
Clean, well-scoped implementation. The host-mode isolation boundary is properly gated, the singleton invariant is enforced at both boot (DB seed) and runtime (409 guard), security rails are in place, and the chat API plumbing is well-separated from the regular webhook-dispatch path. Test coverage is solid for a feature at this complexity level. Merging.