Mindlap
← All projects

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

markdown

LapWire 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/*, requires Authorization: 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 code 414141)
  • 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; only role === "admin" passes the gateway (else admin_required).
  • Tokens expire (~8 days). On any 401 / invalid_token, prompt the user to /meta link again.
  • Secret values are never returned by reads: repo.token_set and env[].is_set are 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 /meta with 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 acme with crispr-bench (healthy: cloud on, heartbeat armed, one enabled lap) and d4-analytics (idle: cloud off, heartbeat off, one disabled lap); admin testuser@mindlap.dev (OTP 414141); non-admin member@acme.dev (403 admin_required). State mutates so a start/stop is reflected by a subsequent status read. Time is injected (no Date.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 the metaClient call 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 link with email + OTP 414141 stores an encrypted link and /meta whoami reports Linked 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 unlink removes the row; subsequent /meta whoami says "Not linked".
  • AC-4: /meta fleet lists both fixture projects with correct health glyphs.
  • AC-5: /meta status crispr-bench renders heartbeat + enabled entry + repo + cloud + default tool; unknown project -> friendly not-found.
  • AC-6: Start opens a modal prefilled from defaults; submitting calls factory/start with enable_heartbeat: true and 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 refreshed expires_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-bench shows repo/cloud/defaults/env/creds/members; editing a default calls PUT /defaults and 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 receives admin_required and no mutation occurs.
  • AC-13 (expiry): With an expired fixture token, /meta whoami reports "link expired" and any meta command prompts /meta link instead 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.

Laps & stories

mindlap
AboutFAQPrivacyTerms

© 2026 mindlap