Bots/Hermes
Lap Wire
A Slack ops app for meta engineers - link MindLap, watch every client factory, and start, stop, and re-arm laps without leaving the channel.
Want the codebase? Get the Git repo emailed to you.
About this project
LapWire is a Slack ops app for meta engineers - operators who run client factories on the MindLap platform - built end-to-end by the agent factory. From Slack, a linked admin can link their MindLap account over an OTP flow, see the live status of every client factory, start, stop, and re-arm laps, and view and edit a factory's configuration. It is a standalone TypeScript service on Slack Bolt in Socket Mode that talks to MindLap purely as an HTTP client of the gateway and auth APIs under each admin's JWT, and stores its own Slack-to-MindLap identity map in a local SQLite file with the token encrypted at rest. v0.1 covers four features - link & identity, on-demand factory status, lifecycle controls with tiered guardrails, and config view & edit - with secret values shown only as set or not-set, never echoed.
Demo
At a glance
3.6h
Agent hours
29M
Tokens
2
Laps
18
Stories
29M
tokens
Codex · 79%
Others · 21%
Tokens by stage
Implement
15M
Code Review
8M
Merge
3M
commit
1M
revise-implementation
3M
Pipeline
Stage
Runs
Tokens
Duration
Implement
18
15M
1.7h
Code Review
19
8M
1h
Merge
6
3M
0.3h
commit
2
1M
0.1h
revise-implementation
7
3M
0.4h
Engines used
Codex
Tokens
23M
Runs
40
Agent hours
2.8h
Success
100%
Others
Tokens
6M
Runs
15
Agent hours
0.8h
Success
86.7%
Agent team
developer
37 runs · 3.3h
37 passed · 0 failed
engineering_manager
24 runs · 1.3h
22 passed · 2 failed
product_manager
3 runs · 0.3h
3 passed · 0 failed
Artifacts
PRD
markdownLapWire v0.1 - Factory Build Spec
Slack ops app for meta engineers running MindLap client factories
1. What LapWire is
LapWire is a Slack ops app for "meta engineers" - operators who run client factories on the MindLap platform. From Slack, a linked admin can: link their MindLap account, see the live status of every client factory, start / stop / re-arm factories, and view & edit a factory's configuration.
It is a standalone TypeScript service using Slack Bolt in Socket Mode. It talks to MindLap purely as an HTTP client of the MindLap gateway (/api/meta/*) and auth (/api/auth/*) APIs, using each linked admin's JWT. It stores its own state (Slack ↔ MindLap identity map) in a local SQLite file.
It is not a developer task-runner, and v0.1 deliberately excludes alerting, background polling, debugging, provisioning wizards, and digests.
2. Goals & Non-goals
Goals (the 4 v0.1 features)
- G1 - Link & Identity:
/meta link,/meta whoami,/meta unlink(OTP-based account linking). - G2 - Factory Status (on-demand):
/meta fleet(all factories) and/meta status <project>(one). - G3 - Lifecycle Controls: start lap, stop lap, re-arm heartbeat, disable heartbeat - from buttons + slash.
- G4 - Config View & Edit:
/meta config <project>- view all config; edit repo, cloud, defaults, env, credentials.
Non-goals (explicitly OUT for v0.1)
- Background poller / cached fleet snapshot (status is fetched live, per-command).
- Proactive alerting / #factory-ops feed / edge-trigger state.
- Debugging assistant / log retrieval.
- Provisioning wizard for creating new projects (config of existing projects only).
- Digests, cost/usage, LLM triage, App Home tab, persistent dashboards.
- Any new MindLap backend/gateway endpoint. LapWire only calls the frozen contract.
3. Tech stack & conventions
- Runtime: Node 20+, ESM (
"type": "module"). Language: TypeScript, strict. - Dependencies:
@slack/bolt,better-sqlite3,dotenv; dev:typescript,tsx,vitest,eslint. - No code generation / no external SDK. LapWire calls MindLap with a hand-written typed fetch wrapper (
metaClient.ts) against the frozen contract.
Project layout (feature modules under src/):
src/
index.ts Bolt bootstrap (Socket Mode) + register all handlers
config.ts env loading (dotenv) + typed config object
metaClient.ts typed fetch wrapper (injects per-user JWT, maps errors)
store.ts SQLite identity store (better-sqlite3), JWT encrypted at rest
crypto.ts AES-256-GCM encrypt/decrypt for stored JWTs
auth.ts OTP flow helpers (send-otp / verify-otp / me)
errors.ts error types + gateway envelope mapping
link/ F1 handlers: link, whoami, unlink
status/ F2 handlers + Block Kit cards: fleet, status
lifecycle/ F3 action handlers + modals: start/stop/rearm/disable-hb
config/ F4 handler + per-domain edit modals
blocks/ shared Block Kit helpers
4. Frozen MindLap API contract (self-containment core)
Two origins:
- API_BASE - auth only, unauthenticated / OTP. Direct to backend.
- META_BASE_URL - everything else, the gateway
/api/meta/*, requiresAuthorization: Bearer <jwt>and the user must be a global admin.
Error envelope (non-2xx): { "ok": false, "error": { "code": string, "message": string } }. Known codes: unauthorized (401), invalid_token (401 -> re-link), admin_required (403), not_found (404). Surface error.message verbatim.
4.1 Auth (origin = API_BASE, no bearer)
POST /api/auth/send-otp->{ ok: true }(OTP emailed; dev magic code414141)POST /api/auth/verify-otp->{ access_token: string }(JWT; 401 on bad OTP)GET /api/auth/me->{ id, email, role: "admin" | "member" }
4.2 Projects & status (origin = META_BASE_URL, bearer)
GET /api/meta/orgs/{org}/projects-> array of{ id, name, type, org_id, created_at }GET /api/meta/projects/{pid}/factory/status?org={org}-> project, repo (url + token_set), cloud_enabled, defaults (tool/model), heartbeat (enabled + expires_at), harnesses[], cron (file_id + entries[]), recent_activity[].
Frozen CronEntry: name, schedule, instruction, agent?, tool?, model?, harness?, attached_files[], queue (local|cloud), enabled, last_run, last_result, created_at, updated_at.
4.3 Lifecycle
POST /api/meta/projects/{pid}/factory/start->{ entry: CronEntry, heartbeat }POST /api/meta/projects/{pid}/factory/stop->{ entry: CronEntry|null, heartbeat }GET /api/meta/projects/{pid}/heartbeat->{ project_id, enabled, schedule, rule_name, expires_at }POST /api/meta/projects/{pid}/heartbeat({ enabled }) -> same shape
4.4 Config domains
GET/PUT /api/meta/projects/{pid}/repo(url + token_set; token write-only)PUT /api/meta/projects/{pid}/cloud({ enabled })GET/PUT /api/meta/projects/{pid}/defaults(tool / model)GET/PUT/DELETE /api/meta/projects/{pid}/env[/{key}](ENV_* keys; value never returned)GET/POST/DELETE /api/meta/projects/{pid}/credentials[/{id}](provider + owner only)GET /api/meta/projects/{pid}/members(view-only in v0.1)
4.5 Secrecy invariants (frozen)
- Auth is the only unauthenticated surface; everything else needs a bearer JWT.
- The JWT carries
role; onlyrole === "admin"passes the gateway (elseadmin_required). - Tokens expire (~8 days). On any 401 / invalid_token, prompt the user to
/meta linkagain. - Secret values are never returned by reads:
repo.token_setandenv[].is_setare booleans; credential lists show provider / owner only. LapWire shows set / not-set, never a value.
5. Features
F1 - Link & Identity
/meta link opens an email modal -> send-otp -> OTP modal -> verify-otp -> GET /me -> store { slack_user_id, mindlap_user_id, email, jwt(enc), role, exp } -> ephemeral confirm. /meta whoami shows email, role, token expiry (or link-expired). /meta unlink deletes the row. The JWT is AES-256-GCM encrypted before insert; tokens and OTPs are never logged.
F2 - Factory Status (on-demand)
/meta fleet lists every project with a health glyph (heartbeat + enabled entries): green = heartbeat enabled & >= 1 entry enabled; warning = heartbeat expiring < 24h; red = heartbeat off or no enabled entries. /meta status <project> resolves the project by name and renders a detail card: heartbeat, repo, cloud, default tool, cron entries, and action buttons. Status is fetched live per command (no background polling).
F3 - Lifecycle Controls
Buttons on the status card (plus slash fallback) map to: Start lap (factory/start), Stop lap (factory/stop), Re-arm heartbeat (heartbeat {enabled:true}), Disable heartbeat (heartbeat {enabled:false}). Start opens a modal prefilled from factory.status.defaults. Tiered guardrails are mandatory: native confirm on stop, type-to-confirm (retype project name) on disable-heartbeat (a project-wide kill). Every successful action posts an attribution line and re-renders the card.
F4 - Config View & Edit
/meta config <project> renders a config hub (repo, cloud, defaults, env, credentials, members) read in parallel. Per-domain edit modals write back (repo, cloud toggle, defaults, env set/delete, credential add/delete) and re-render the hub from a fresh fetch. Secret values are entered via modal inputs and shown only as set / not-set - never echoed. Member mutations are out (view-only).
6. Data model (SQLite)
Single links table created idempotently on boot: slack_user_id (PK), mindlap_user_id, email, jwt_enc (BLOB, AES-256-GCM; plaintext never stored or logged), role, exp (unix seconds from JWT), created_at. crypto.ts provides encrypt/decrypt using a 32-byte key derived from LINK_ENC_KEY, random IV per encryption stored with ciphertext + tag.
7. Slack integration contract
- Transport: Socket Mode (
socketMode: true, bot + app tokens). - Single slash command
/metawith a subcommand word (link, whoami, unlink, fleet, status, config, start, stop, rearm). - Stable action IDs:
start_lap,stop_lap,rearm_hb,disable_hb,cfg_edit_repo,cfg_toggle_cloud,cfg_edit_defaults,cfg_manage_env,cfg_manage_creds,view_status. - Modal callback IDs:
link_email,link_otp,start_form,disable_hb_confirm,cfg_repo,cfg_defaults,cfg_env,cfg_creds. - Replies are ephemeral; cards refresh via
chat.update; actions post attribution lines.
8. Testing & gates
- Runner: vitest. All tests run against an in-process mock MindLap that implements the frozen contract exactly - no network, no live MindLap, no real Slack socket.
- Mock fixtures: org
acmewithcrispr-bench(healthy: cloud on, heartbeat armed, one enabled lap) andd4-analytics(idle: cloud off, heartbeat off, one disabled lap); admintestuser@mindlap.dev(OTP414141); non-adminmember@acme.dev(403 admin_required). State mutates so a start/stop is reflected by a subsequent status read. Time is injected (noDate.now()in fixtures) for deterministic relative displays. - Handler tests invoke the registered Slack listener with fakes (
ack,respond,client.views.open,client.chat.update) and assert both themetaClientcall and the Block Kit payload. - Gates (green before a story is CLOSED):
vitest run,tsc --noEmit,eslint .,pnpm build.
9. Story decomposition (phased, file-disjoint, parallel-safe)
- Phase 0 - foundation: S0 Bootstrap (deps + config + Bolt app), S1 Store + crypto, S2 API client (typed wrappers for every route + error mapping), S3 Mocks (mock MindLap + Slack fakes + fixtures).
- Phase 1 - identity: S4 F1 Link / whoami / unlink.
- Phase 2 - features (file-disjoint, parallel): S5 F2 Status, S6 F3 Lifecycle, S7 F4 Config, S8 shared Block Kit helpers.
10. Acceptance Criteria (falsifiable)
- AC-1 (decisive):
/meta linkwith email + OTP414141stores an encrypted link and/meta whoamireportsLinked as <email> (admin). Wrong OTP -> the 401 message, no link stored. - AC-2: A stored JWT is never present in plaintext in the SQLite file or any log line.
- AC-3:
/meta unlinkremoves the row; subsequent/meta whoamisays "Not linked". - AC-4:
/meta fleetlists both fixture projects with correct health glyphs. - AC-5:
/meta status crispr-benchrenders heartbeat + enabled entry + repo + cloud + default tool; unknown project -> friendly not-found. - AC-6: Start opens a modal prefilled from defaults; submitting calls
factory/startwithenable_heartbeat: trueand the card re-renders to enabled. - AC-7: Stop carries a native confirm; confirming calls
factory/stop; the entry shows disabled after. - AC-8: Re-arm calls
POST /heartbeat {enabled:true}and the card shows a refreshedexpires_at. - AC-9 (guardrail): Disable heartbeat requires typing the exact project name; a mismatch shows a modal error and makes no API call.
- AC-10:
/meta config crispr-benchshows repo/cloud/defaults/env/creds/members; editing a default callsPUT /defaultsand the hub shows the new value. - AC-11 (secret safety): Entering an env value or credential token never causes the value to appear in any rendered Block Kit text or log.
- AC-12 (auth attribution + gate): Every action runs under the caller's stored JWT; a
member-role caller receivesadmin_requiredand no mutation occurs. - AC-13 (expiry): With an expired fixture token,
/meta whoamireports "link expired" and any meta command prompts/meta linkinstead of calling the gateway.
11. Definition of done
A story is CLOSED only when its ACs pass against the mock, no stub remains for covered behavior, all four gates are green on changed files, and its patch is merged to lap/<N>. The lap is done when all of S0-S8 are CLOSED, the full vitest suite is green, and pnpm build succeeds from a clean pnpm install.