← back to The Ai Factory
[overnight] pre-debate baseline
75e44f63f3952cb1272d51fdd99cafb82f81cc8b · 2026-05-04 01:08:09 -0700 · SteveStudio2
Files touched
A .env.exampleA .gitignoreA CHANGES.mdA GA_CONSOLIDATION.mdA PLAN.mdA README.mdA output/clickthrough/01-table.pngA output/clickthrough/02-events-expanded.pngA output/clickthrough/summary.jsonA output/run-4/shopify-cost-breakdown/SKILL.mdA output/run-5/sql-tldr-summarizer/SKILL.mdA output/run-6/pm2-crash-watcher.mdA package-lock.jsonA package.jsonA public/index.htmlA scripts/clickthrough.jsA scripts/run.jsA server.jsA sql/001_init.sqlA src/llm.jsA src/pipeline.jsA src/stages/critic.jsA src/stages/document.jsA src/stages/intake.jsA src/stages/iterate.jsA src/stages/memory_write.jsA src/stages/research.jsA src/stages/scaffold.jsA src/stages/smoke_test.jsA viewer.js
Diff
commit 75e44f63f3952cb1272d51fdd99cafb82f81cc8b
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 4 01:08:09 2026 -0700
[overnight] pre-debate baseline
---
.env.example | 15 +
.gitignore | 4 +
CHANGES.md | 14 +
GA_CONSOLIDATION.md | 75 ++
PLAN.md | 117 +++
README.md | 28 +
output/clickthrough/01-table.png | Bin 0 -> 255857 bytes
output/clickthrough/02-events-expanded.png | Bin 0 -> 480415 bytes
output/clickthrough/summary.json | 10 +
output/run-4/shopify-cost-breakdown/SKILL.md | 65 ++
output/run-5/sql-tldr-summarizer/SKILL.md | 40 +
output/run-6/pm2-crash-watcher.md | 48 +
package-lock.json | 1293 ++++++++++++++++++++++++++
package.json | 21 +
public/index.html | 150 +++
scripts/clickthrough.js | 57 ++
scripts/run.js | 23 +
server.js | 201 ++++
sql/001_init.sql | 47 +
src/llm.js | 72 ++
src/pipeline.js | 179 ++++
src/stages/critic.js | 57 ++
src/stages/document.js | 42 +
src/stages/intake.js | 42 +
src/stages/iterate.js | 39 +
src/stages/memory_write.js | 53 ++
src/stages/research.js | 63 ++
src/stages/scaffold.js | 93 ++
src/stages/smoke_test.js | 35 +
viewer.js | 15 +
30 files changed, 2898 insertions(+)
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..f5f607a
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,15 @@
+# The AI Factory — env template
+# Copy to .env and fill in real values.
+
+ANTHROPIC_API_KEY=
+
+PG_HOST=/tmp
+PG_PORT=5432
+PG_DATABASE=ai_factory
+PG_USER=stevestudio2
+PG_PASSWORD=
+
+ORCHESTRATOR_PORT=9890
+VIEWER_PORT=9891
+ADMIN_TOKEN=
+CLAUDE_CLI=claude
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..daf1dd0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.env
+*.log
+.DS_Store
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..91db73c
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,14 @@
+## Applied
+- `src/llm.js:10` — replaced the user-specific absolute Claude CLI fallback path with `claude` — removes a hardcoded `/Users/...` path while preserving env override via `CLAUDE_CLI`.
+- `.env.example:15` — added `CLAUDE_CLI=claude` placeholder — documents the configurable CLI path without introducing a real secret or machine-local path.
+
+## Deferred (needs Steve)
+- P0 — `server.js:31`, `server.js:67`, `server.js:95` — no auth plus wildcard CORS allows browser-origin localhost POSTs — deferred because adding token auth/CORS policy is outside the safe-fix list and changes operator workflow.
+- P0 — `server.js:95`, `server.js:104`, `server.js:143` — activation can copy generated files into `~/.claude` and `?force=true` bypasses approval — deferred because removing force or adding local confirmation/diff changes activation semantics.
+- P0 — `src/stages/scaffold.js:16`, `src/stages/smoke_test.js:20` — generated subagents may include dangerous tools and smoke tests only verify presence — deferred because safe remediation requires a frontmatter parser and artifact-specific allowlist design.
+- P1 — `server.js:78` — fire-and-forget pipeline can leave runs stuck after a process crash — deferred because a resumable worker/stale-job recovery is a behavioral change beyond the safe list.
+- P1 — `src/stages/memory_write.js:32` — read-modify-write append to `MEMORY.md` can race under concurrent activations — deferred because lock/atomic append behavior needs concurrency testing.
+- P1 — `src/pipeline.js:52`, `src/llm.js:51` — no concurrency limit around Ollama/Claude CLI — deferred because queue sizing and backpressure policy need Steve’s decision.
+- P1 — `README.md:17`, `package.json:8` — docs say `npm run start` boots orchestrator and viewer, but it only starts the orchestrator — deferred because docs/public-facing command behavior changes were not in the safe-fix list.
+- P1 — `PLAN.md:106` — completed items are duplicated as open TODOs — deferred because plan/status cleanup is not in the safe-fix list.
+- P1 — `package.json:15`, `.env.example:4`, `src/llm.js:5` — Anthropic SDK/API key remain despite “No Anthropic API” — deferred because dependency/env removal is not a safe autonomous fix and may require package-lock decisions.
diff --git a/GA_CONSOLIDATION.md b/GA_CONSOLIDATION.md
new file mode 100644
index 0000000..db0d287
--- /dev/null
+++ b/GA_CONSOLIDATION.md
@@ -0,0 +1,75 @@
+# GA4 Fleet Consolidation — Process Plan
+
+**Started:** 2026-04-30
+**Owner:** Steve
+**Process .md per `feedback_suggest_process_md.md`**
+
+---
+
+## Goal
+
+Bring every GA4 property visible to `steve@designerwallcoverings.com` under **one** GA account.
+
+## Locked decisions
+
+| # | Decision | Value | Why |
+|---|---|---|---|
+| Q1 | Move vs recreate | **recreate** | Fast, scriptable. Lose history; new G-IDs everywhere. |
+| Q2 | Scope | **literal — all properties, business + personal** | Steve: "yes all under steve@dw" (2026-04-30) |
+| Q3 | Target account | **`371997142` (IntelligentHome)** *(working assumption — flag if wrong)* | Default from URL Steve shared; renameable later |
+| Q4 | Inventory auth | **oneshot-adc** | Env-var-scoped to a single audit run; no `_auth.py` change. SA-per-account is too slow for the audit's purpose. |
+
+Decisions still owed before **Phase 4** (the destructive phase):
+- **Q3 confirm/replace** — is IntelligentHome really where everything should land, or should we create a fresh "Steve Personal" account?
+- **DW + studentdebtcrisis carve-outs** — analytics SKILL.md operating rule #1 already excludes these from injection. For *consolidation*, treat any DW-Shopify or SDCC GA property as DO-NOT-MOVE on sight.
+- **Cleanup policy for OLD properties** in source accounts after recreate: delete / archive / leave-dormant?
+
+## Phases
+
+### Phase 1 — Permission *(in progress)*
+- Inventory uses **oneshot ADC** (Steve's identity) → no per-account SA grants needed.
+- Service account stays on the bench unless we hit a write where SA scoping is preferred.
+
+### Phase 2 — Inventory *(blocked on Phase 1)*
+- `GA_USE_USER_AUTH=1 python3 ~/.claude/skills/analytics/scripts/audit_fleet.py`
+- Output: full grouped JSON of every account / property / web stream visible.
+
+### Phase 3 — Plan *(after inventory)*
+- Build a per-property table in this file: `KEEP_IN_PLACE / RECREATE_IN_TARGET / ARCHIVE / MERGE`.
+- Pre-flag DW-Shopify GA + SDCC + any client/partner properties as `KEEP_IN_PLACE`.
+- Steve reviews the table line-by-line before Phase 4.
+
+### Phase 4 — Execute (recreate path) *(human-gated, batched)*
+- For each `RECREATE_IN_TARGET` property:
+ 1. Use Admin API (`create_property.py`) to create new property in account `371997142`.
+ 2. Add web data stream with same `default_uri` as source.
+ 3. Capture new `G-XXXXXXXX` measurement ID into a recreated→source mapping JSON.
+- **Never auto-execute the full batch.** Steve approves each batch (5–10 properties) explicitly.
+
+### Phase 5 — Verify
+- Re-run `audit_fleet.py`.
+- Diff against the Phase 3 plan: every `RECREATE_IN_TARGET` property must now exist in target account; sources untouched.
+
+### Phase 6 — Re-tag *(mandatory because we recreated)*
+- For every recreated property, find the live site(s) using the OLD `G-XXXXXXXX`.
+- Use `inject_gtag.py --measurement-id <new-G-ID>` to push the new ID.
+- Respect SKILL.md operating rule #5: **never auto-replace UA-XXXXX tags.** Flag, don't replace.
+
+### Phase 7 — Cleanup *(human-gated)*
+- Source accounts now have stranded properties + a paper trail of empty accounts.
+- For each source account: `KEEP / DELETE_PROPERTY / DELETE_ACCOUNT`. Steve approves.
+
+## Standing-rules check (from /best-practices, 2026-04-30)
+
+- ✅ `feedback_suggest_process_md.md` — this file
+- ✅ `feedback_ask_before_doing.md` — Phase 3 + Phase 4 + Phase 7 are explicitly Steve-gated, not auto-executed
+- ✅ `feedback_dns_donotouch.md` (analytics SKILL.md rule #1) — DW-Shopify GA + SDCC carved out as `KEEP_IN_PLACE`
+- ✅ Auth scope — ADC is env-var-only on a single read-only command, not a default change
+- ✅ `feedback_no_cloud_agents.md` — all of this runs locally on Mac Studio 2
+
+## Sharp edges to remember
+
+- **Recreate = new G-IDs.** Every site needs gtag re-deployed. If any site is hardcoded with a measurement ID we can't reach, it's stranded.
+- **Recreate ≠ delete.** Old properties stay in source accounts until Phase 7. During the gap, both old and new emit, which can double-count if any site still has the old gtag while the new one is also injected. Sequence: create new → switch gtag → confirm new is receiving → only then delete old.
+- **GA Admin API rate limits** are tight (10 writes/sec per project). Batch sizes of 5–10 with 90s gaps between batches is the safe rhythm.
+- **Search Console / Ads / BigQuery export links** on any source property break on recreate. Inventory must capture which properties have these.
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..62ed7a1
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,117 @@
+# The AI Factory — PLAN.md
+
+**Status:** scaffold (2026-04-30)
+**Owner:** Steve
+**Local path:** `~/Projects/the-ai-factory/`
+**Ports:** orchestrator `9890`, viewer `9891`
+**Pattern reference:** Site Factory (`~/Projects/site-factory/`, ports 9880/9881)
+
+---
+
+## What it is (working hypothesis — confirm before building features)
+
+Orchestrator that turns a single prompt ("build me an X") into a **fully-shipped AI artifact**: a Claude-Code subagent, a `/skill`, a small standalone agent (pm2 service), or a one-shot script. The Factory owns the pipeline, the critic loop, and the registry — humans review the output, not every step.
+
+This is a meta-factory. Site Factory builds sites. Hormuzy builds design. The AI Factory builds the AI things themselves.
+
+> If this hypothesis is wrong, edit this section first — every other choice below flows from it.
+
+## Pipeline (mirrors Site Factory's 11-stage pattern)
+
+1. **Intake** — prompt → structured spec (artifact_type, name, surface, scope)
+2. **Research** — pull adjacent skills/agents from `~/.claude/skills/` + `~/.claude/agents/` so we don't duplicate
+3. **Scaffold** — write files (subagent .md, skill SKILL.md, agent index.js, etc.)
+4. **Wire** — register with pm2 / register in MEMORY.md / add to skill index
+5. **Smoke test** — invoke the artifact end-to-end against a canned input
+6. **Critic (Manager pass)** — second-model review using `claude-api` skill rules: caching, model choice, prompt clarity
+7. **Iterate** — apply critic notes, re-test
+8. **Document** — README.md inside the artifact + one-line entry in this Factory's registry
+9. **Memory write** — emit a memory entry for `MEMORY.md` so future sessions know it exists
+10. **Activate** — flip status to `live`
+11. **Watchdog hook** — if it's a long-running service, register with `process-hawk`
+
+## Stack
+
+- **Runtime:** Node 20 + Express
+- **DB:** PostgreSQL local (`ai_factory` schema in `dw_unified` DB, or its own DB — TBD on first run)
+- **Auth:** none initially (loopback only); add SSO via `dw-sso-integration` skill before exposing externally
+- **LLM:** **NO Anthropic API** (Steve 2026-04-30: "if api costs money.. no"). Hybrid:
+ - **Ollama (local, free)** for mechanical stages — `qwen3:14b` for spec extraction / scaffold-template fill / doc generation. `gemma3:4b` for cheap classification.
+ - **Claude CLI subprocess** (`/Users/stevestudio2/.local/bin/claude -p ...`) for the Manager critic pass only. Goes through Steve's Max sub, not the API → no per-token charges. One call per run keeps quota burn near zero.
+ - Adapters live in `src/llm.js`.
+- **Process manager:** pm2; service name `ai-factory-orchestrator` (`:9890`) and `ai-factory-viewer` (`:9891`)
+- **Watchdog:** add to `com.steve.pd-hawk` style launchd OR `process-hawk` skill once stable
+
+## File layout
+
+```
+the-ai-factory/
+├── PLAN.md ← this file
+├── README.md
+├── package.json
+├── .env.example ← ANTHROPIC_API_KEY, PG_*, ADMIN_TOKEN
+├── server.js ← Express orchestrator on :9890
+├── viewer.js ← static viewer on :9891 (matches sf-viewer)
+├── src/
+│ ├── pipeline.js ← 11-stage runner
+│ ├── stages/ ← one file per stage
+│ ├── critic.js ← Manager pass
+│ └── registry.js ← what we've built
+├── sql/
+│ └── 001_init.sql ← ai_factory.runs / ai_factory.artifacts / ai_factory.events
+├── public/
+│ └── index.html ← viewer
+└── scripts/
+ └── run.js ← CLI entry: `node scripts/run.js "build me a foo"`
+```
+
+## Decisions (2026-04-30)
+
+1. **Artifact scope (Q1)** — START NARROW: Claude Code subagents (`~/.claude/agents/*.md`) + `/skills` (`~/.claude/skills/<name>/SKILL.md`) only. pm2 agents come in v2 once the critic loop is proven on the simpler artifacts. *(default — flag if you want broader from day 1)*
+2. **DB home (Q2)** — **STANDALONE DB `ai_factory`** (not inside dw_unified — Steve flagged 2026-04-30: "be careful, create a new stand alone db"). Tables in `public`. Orchestrator boots with a hard safety guard that refuses `PG_DATABASE=dw_unified`.
+3. **Public-facing? (Q3)** — **NO. All backroom internal.** Steve confirmed 2026-04-30. Loopback-only on Mac Studio 2 forever; no Cloudflare, no domain, no SSO, no Kamatera mirror. `127.0.0.1` binding stays.
+4. **Trigger surface (Q4)** — viewer form (`:9891`) + CLI (`scripts/run.js`). A `/ai-factory` slash command is a nice-to-have once the v1 pipeline ships; not blocking. *(default)*
+
+## Standing rules that apply
+
+- Stays local on Mac Studio 2 (no cloud agents).
+- **Loopback only** — `127.0.0.1` binding for both `:9890` and `:9891`. No external exposure ever.
+- **NO Anthropic API** — Steve 2026-04-30. Use local Ollama + Claude CLI subprocess only.
+- Any new long-running service → `pm2 start && pm2 save`.
+- If the Factory generates a *site*, hand off to Site Factory / `dw-site-build`. Don't reimplement.
+- If the Factory generates a *domain* + site, hand off to Onboard Domain Agent.
+
+## Next actions
+
+- [x] Decisions Q1–Q4 locked (2026-04-30)
+- [x] MEMORY.md entry written
+- [x] Standalone DB `ai_factory` created + schema applied
+- [x] Orchestrator + viewer boot clean on `:9890` / `:9891`
+- [x] No-API decision locked (hybrid Ollama + Claude CLI)
+- [x] Stage 1 (intake) WIRED — qwen3:14b extracts spec as JSON
+- [x] Stage 3 (scaffold) WIRED — qwen3:14b template-fills subagent .md OR skill SKILL.md, writes to `output/run-<id>/...`, frontmatter validated
+- [x] Stage 6 (critic) WIRED — Claude CLI subprocess returns `{approved, score, issues, fix_instructions}` JSON
+- [x] Stage 7 (iterate) WIRED — qwen3:14b applies fix_instructions, overwrites file (single pass)
+- [x] Verified end-to-end on `shopify-cost-breakdown` skill prompt: 145s total, critic scored v1 at 62 → iterate applied 10 fixes → final SKILL.md is shippable. See `output/run-4/`.
+- [x] **Re-critic loop WIRED** — after iterate we re-score v2; the run's final status reflects the revised review, not the v1 review. Verified on run-5 (`sql-tldr-summarizer`): v1=62/false → iterate(8 fixes) → v2=82/**true** → status=`awaiting_activation_v2`.
+- [x] **Activate-with-approval WIRED** — `POST /runs/:id/activate` is the only path that touches `~/.claude/agents/` or `~/.claude/skills/`. Gates: (a) run status must start with `awaiting_activation` unless `?force=true`, (b) won't overwrite an existing target unless `?overwrite=true`, (c) refuses to copy from outside the sandbox dir. Viewer surfaces an Activate button on eligible runs.
+- [x] **Stage 9 (memory_write) WIRED** — runs as part of the activate handler (best-effort; activation succeeds even if memory append fails). Appends one line to `MEMORY.md` of the form `- AI Factory shipped: ``<name>`` (<type>) — <scope> [run #N, YYYY-MM-DD, lives in ~/.claude/...]`. Skips if the same artifact name is already in the index. Verified on run-5: line appears, file lives at `~/.claude/skills/sql-tldr-summarizer/SKILL.md`, run status=activated.
+- [x] **Stage 2 (research) WIRED** — pure-local bigram-Jaccard scan of `~/.claude/agents/` and `~/.claude/skills/` for similar names. Logs an `exact-name` warning if a conflict exists. Non-blocking; runs in parallel with scaffold (Promise.all).
+- [x] **Stage 5 (smoke_test) WIRED** — pure-local structural validation (frontmatter, name match, body length, no unrendered placeholders, tools/model for subagents). Runs after intake's first-pass scaffold; re-runs on v2 if iterate fired.
+- [x] **Stage 8 (document) WIRED** — for skills only, drops a `README.md` next to `SKILL.md` in the artifact dir. Subagents skipped (single-file, self-documenting). Runs in parallel with critic.
+- [x] **Stage 4 (wire) explicit no-op** — for v1 subagent/skill artifacts there's no external registration step (the harness auto-discovers). Logged as no-op for clarity.
+- [x] **Stage 11 (watchdog_hook) explicit no-op** — only relevant for v2 service-type artifacts.
+- [x] **Pipeline parallelization** — research‖scaffold and document‖critic run as `Promise.all`. Should shave ~10–30s off the ~145s baseline (qwen3:14b scaffold ≈30s parallel with research's instant local read; document's instant write parallel with critic's ~25s Claude CLI call).
+- [x] **Viewer drill-down** — clicking a run row expands it inline to show the full event timeline (stage, name, level, message, payload as collapsible JSON). No separate page.
+- [ ] Confirm working hypothesis (one-line edit at top) — Steve
+- [ ] Stage 8 (document) — generate a README inside the artifact dir (skills only)
+- [ ] Stage 11 (watchdog_hook) — for service-type artifacts only (v2)
+- [ ] Viewer drill-down — per-run event timeline (currently only lists runs)
+
+## Out of scope (per Q3 = backroom internal)
+
+- ~~Domain registration~~
+- ~~SSO via dw-sso-integration~~
+- ~~Cloudflare zone~~
+- ~~Kamatera mirror~~
+- ~~theaifactory.com~~
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d071b04
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# The AI Factory
+
+Meta-orchestrator that turns one prompt into a shipped AI artifact (subagent, skill, agent, or script).
+
+- **Path:** `~/Projects/the-ai-factory/`
+- **Orchestrator:** `:9890`
+- **Viewer:** `:9891`
+- **Plan:** see `PLAN.md` (read first)
+- **Pattern:** mirrors Site Factory (`~/Projects/site-factory/`)
+
+## Quick start
+
+```bash
+cd ~/Projects/the-ai-factory
+cp .env.example .env # fill in ANTHROPIC_API_KEY, PG_*
+npm install
+npm run start # boots orchestrator (:9890) + viewer (:9891)
+```
+
+CLI:
+
+```bash
+node scripts/run.js "build me a subagent that watches the freezer for expired vendors"
+```
+
+## Status
+
+Scaffold only. See `PLAN.md` → "Open questions" before adding pipeline logic.
diff --git a/output/clickthrough/01-table.png b/output/clickthrough/01-table.png
new file mode 100644
index 0000000..8d42b42
Binary files /dev/null and b/output/clickthrough/01-table.png differ
diff --git a/output/clickthrough/02-events-expanded.png b/output/clickthrough/02-events-expanded.png
new file mode 100644
index 0000000..da4a2e8
Binary files /dev/null and b/output/clickthrough/02-events-expanded.png differ
diff --git a/output/clickthrough/summary.json b/output/clickthrough/summary.json
new file mode 100644
index 0000000..fa88ea5
--- /dev/null
+++ b/output/clickthrough/summary.json
@@ -0,0 +1,10 @@
+{
+ "viewer_status": "reachable",
+ "row_count": 6,
+ "activate_buttons": 1,
+ "console_messages": 0,
+ "screenshots": [
+ "01-table.png",
+ "02-events-expanded.png"
+ ]
+}
\ No newline at end of file
diff --git a/output/run-4/shopify-cost-breakdown/SKILL.md b/output/run-4/shopify-cost-breakdown/SKILL.md
new file mode 100644
index 0000000..1317737
--- /dev/null
+++ b/output/run-4/shopify-cost-breakdown/SKILL.md
@@ -0,0 +1,65 @@
+---
+name: shopify-cost-breakdown
+description: Generates cost, MAP (Minimum Advertised Price), retail, and margin breakdown for Shopify product variants when given a product handle and user-supplied cost data. Auto-triggers on "cost breakdown for handle" or similar phrases. Requires handle + unit_cost; no live Shopify API integration.
+---
+
+# Shopify Cost Breakdown
+
+## Trigger
+- "cost breakdown for handle"
+- "variant cost breakdown"
+- "shopify product cost"
+- "price breakdown for handle"
+- "cost analysis for handle"
+
+## When to use
+When a user provides a Shopify product handle (e.g., `summer-dress`) and explicitly supplies unit_cost (e.g., $15.00) to calculate cost, MAP, retail, and margin.
+
+## What it does
+- Calculates total cost per variant using user-provided unit_cost
+- Computes MAP using user-defined minimum advertised price or default formula (MAP = cost × 1.5)
+- Estimates retail price using user-specified markup percentage or default formula (retail = cost / (1 - 0.3))
+- Determines gross margin and net profit per unit based on inputs
+- Outputs structured tables with currency formatting
+
+## How to invoke
+1. Provide the Shopify product handle (e.g., `summer-dress`)
+2. Supply **required** cost data: unit_cost (e.g., $15.00)
+3. Optionally specify MAP (e.g., $25.00), markup percentage (e.g., 30%), or retail price
+4. Choose output format: table, JSON, or markdown
+
+## Input contract
+**Required fields**:
+- `handle`: Shopify product handle (e.g., `summer-dress`)
+- `unit_cost`: Base cost per unit (e.g., $15.00)
+
+**Optional fields**:
+- `map`: Minimum Advertised Price (e.g., $25.00)
+- `markup_percent`: Retail markup percentage (e.g., 30%)
+- `retail_price`: Final retail price (e.g., $30.00)
+- `currency`: ISO currency code (e.g., USD)
+
+## Example
+**Input**:
+```plaintext
+handle: summer-dress
+unit_cost: 15.00
+map: 25.00
+currency: USD
+```
+
+**Output**:
+| Metric | Value |
+|--------------|----------|
+| Unit Cost | $15.00 |
+| MAP | $25.00 |
+| Retail Price | $30.00 |
+| Gross Margin | $15.00 |
+| Margin % | 50.00% |
+
+## Out of scope
+- Fetching live Shopify API data
+- Processing multiple handles simultaneously
+- Calculating margins without explicit cost data
+
+---
\ No newline at end of file
diff --git a/output/run-5/sql-tldr-summarizer/SKILL.md b/output/run-5/sql-tldr-summarizer/SKILL.md
new file mode 100644
index 0000000..8ac6ad2
--- /dev/null
+++ b/output/run-5/sql-tldr-summarizer/SKILL.md
@@ -0,0 +1,40 @@
+---
+name: sql-tldr-summarizer
+description: Generates a 3-bullet executive summary from SQL query results when triggered by pasted SQL output and a "TLDR" request. Triggers include "TLDR", "summarize this", "executive summary", or "3 bullet points".
+
+---
+
+# SQL TLDR Summarizer
+
+## When to use
+- Paste SQL output and request a summary
+- Ask for "executive summary" with SQL data
+- Demand "3 bullet points" from SQL results
+
+## How to summarize
+1. Parse the SQL result table or output
+2. Identify:
+ - (1) Headline number/finding (e.g., "Total sales: $1.2M")
+ - (2) Notable trend or comparison (e.g., "Q2 revenue up 18% YoY")
+ - (3) Outlier or caveat (e.g., "30% of regions below target")
+3. Format as 3 bullets, each ≤20 words, leading with the metric
+
+## Example
+**Input:**
+```
+| Region | Sales |
+|--------|--------|
+| North | 450000 |
+| South | 750000 |
+| East | 250000 |
+```
+
+**Output:**
+- Total sales: $1.4M across regions
+- South region leads with 750k sales (54% of total)
+- East region underperforms with 250k sales (18% of total)
+
+## Out of scope
+- Execute SQL queries
+- Modify original data
+- Handle non-SQL content
\ No newline at end of file
diff --git a/output/run-6/pm2-crash-watcher.md b/output/run-6/pm2-crash-watcher.md
new file mode 100644
index 0000000..573b56e
--- /dev/null
+++ b/output/run-6/pm2-crash-watcher.md
@@ -0,0 +1,48 @@
+---
+name: pm2-crash-watcher
+description: Monitors PM2-managed processes for crashes in the last hour, reporting metadata like crash time, exit code, and memory usage. Use this subagent when investigating recent process failures or auditing system stability.
+tools: Bash
+model: sonnet
+---
+
+## Your job
+When invoked, analyze PM2 processes to identify crashes in the last hour, extracting structured metadata and relevant log snippets.
+
+## What to check / do
+1. Use `pm2 jlist` to get structured process metadata (pm2_env.exit_code, pm2_env.restart_time, monit.memory, pm_uptime)
+2. Filter processes with restart_time within the last 60 minutes
+3. For each candidate process, run `pm2 logs <name> --err --lines 10 --nostream` to capture recent error logs
+4. Correlate log timestamps with process metadata to confirm crashes
+5. Format results as a human-readable list showing:
+ - Process name
+ - Time since crash (e.g., "12m ago")
+ - Exit code
+ - Memory usage
+ - Last error line from logs
+
+## Output format
+Default: Human-readable summary (one line per crashed agent)
+Example:
+```text
+[ERROR] agent-worker (12m ago) - Exit code 1 - 512MB used - "UncaughtException: TypeError..."
+```
+JSON available on request:
+```json
+{
+ "crashed_agents": [
+ {
+ "name": "agent-worker",
+ "crash_time": "2024-03-20T14:30:00Z",
+ "exit_code": 1,
+ "memory_used": "512MB",
+ "log_snippet": "UncaughtException: TypeError..."
+ }
+ ]
+}
+```
+
+## Out of scope
+- Restarting processes
+- Historical analysis beyond 1 hour
+- Modifying PM2 configurations
+- Continuous log tailing (agent is invoked per request)
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..07fb464
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1293 @@
+{
+ "name": "the-ai-factory",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "the-ai-factory",
+ "version": "0.1.0",
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.32.0",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "pg": "^8.12.0",
+ "playwright": "^1.59.1"
+ }
+ },
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.32.1",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
+ "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4c96e53
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "the-ai-factory",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Meta-orchestrator: prompt -> shipped AI artifact (subagent, skill, agent, script).",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "viewer": "node viewer.js",
+ "run": "node scripts/run.js",
+ "pm2:start": "pm2 start server.js --name ai-factory-orchestrator && pm2 start viewer.js --name ai-factory-viewer && pm2 save",
+ "pm2:logs": "pm2 logs ai-factory-orchestrator"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.32.0",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "pg": "^8.12.0",
+ "playwright": "^1.59.1"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..8c7d3df
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,150 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width,initial-scale=1" />
+<title>The AI Factory</title>
+<style>
+ :root {
+ --bg: #0e0e10;
+ --panel: #16161a;
+ --line: #2a2a30;
+ --text: #e8e8ea;
+ --muted: #8a8a92;
+ --accent: #d4af37;
+ }
+ html, body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif; }
+ header { padding: 24px 32px; border-bottom: 1px solid var(--line); display: flex; align-items: baseline; gap: 16px; }
+ header h1 { margin: 0; font-size: 20px; letter-spacing: 0.02em; }
+ header .sub { color: var(--muted); font-size: 12px; }
+ main { padding: 24px 32px; max-width: 1100px; }
+ form { display: flex; gap: 8px; margin-bottom: 24px; }
+ input[type=text] { flex: 1; background: var(--panel); border: 1px solid var(--line); color: var(--text); padding: 10px 12px; border-radius: 6px; font: inherit; }
+ button { background: var(--accent); color: #1b1b1b; border: 0; padding: 10px 16px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer; }
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
+ .activate-btn { padding: 4px 10px; font-size: 12px; }
+ tr.run-row { cursor: pointer; }
+ tr.run-row:hover { background: #1a1a1f; }
+ tr.events-row td { background: #0a0a0c; padding: 0; border-bottom: 1px solid var(--line); }
+ .events-pane { padding: 12px 16px; }
+ .events-pane h4 { margin: 0 0 8px; font-size: 11px; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; }
+ .event { display: grid; grid-template-columns: 60px 110px 50px 1fr; gap: 12px; padding: 4px 0; border-bottom: 1px dotted #222; font-size: 12px; }
+ .event:last-child { border-bottom: 0; }
+ .event .stg { color: var(--muted); }
+ .event .nm { color: var(--accent); }
+ .event .lv { font-size: 10px; text-transform: uppercase; }
+ .event .lv.warn { color: #f4b400; }
+ .event .lv.error { color: #ff6b6b; }
+ .event .lv.info { color: #5fb878; }
+ .event .ms { color: var(--text); }
+ .event details { margin-top: 4px; }
+ .event details summary { color: var(--muted); cursor: pointer; font-size: 10px; }
+ .event pre { background: #050507; color: #d0d0d4; padding: 8px; margin: 4px 0 0; border-radius: 4px; font-size: 11px; overflow-x: auto; max-height: 200px; }
+ table { width: 100%; border-collapse: collapse; }
+ th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line); font-size: 13px; }
+ th { color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
+ td.muted { color: var(--muted); }
+ td.status { font-weight: 600; }
+ .empty { color: var(--muted); padding: 24px 0; text-align: center; }
+ .err { color: #ff6b6b; font-size: 12px; margin-top: 8px; }
+</style>
+</head>
+<body>
+<header>
+ <h1>The AI Factory</h1>
+ <span class="sub">prompt → shipped AI artifact · :9890 orchestrator · :9891 viewer</span>
+</header>
+<main>
+ <form id="newRun">
+ <input type="text" id="prompt" placeholder="Describe the AI artifact to build (subagent, skill, agent, script)…" />
+ <button type="submit">Build</button>
+ </form>
+ <div id="err" class="err"></div>
+ <table>
+ <thead><tr><th>#</th><th>Prompt</th><th>Type</th><th>Name</th><th>Stage</th><th>Status</th><th>Created</th><th></th></tr></thead>
+ <tbody id="runs"><tr><td class="empty" colspan="8">No runs yet.</td></tr></tbody>
+ </table>
+</main>
+<script>
+const ORCH = 'http://127.0.0.1:9890';
+async function loadRuns() {
+ const tbody = document.getElementById('runs');
+ const errEl = document.getElementById('err');
+ try {
+ const res = await fetch(ORCH + '/runs');
+ const data = await res.json();
+ if (data.error) { errEl.textContent = data.error + (data.hint ? ' — ' + data.hint : ''); return; }
+ errEl.textContent = '';
+ if (!data.runs || !data.runs.length) {
+ tbody.innerHTML = '<tr><td class="empty" colspan="8">No runs yet.</td></tr>';
+ return;
+ }
+ tbody.innerHTML = data.runs.map(r => {
+ const canActivate = String(r.status).startsWith('awaiting_activation');
+ const isActivated = r.status === 'activated';
+ const action = canActivate
+ ? `<button data-id="${r.id}" class="activate-btn">Activate</button>`
+ : (isActivated ? '<span class="muted">activated</span>' : '');
+ return `
+ <tr class="run-row" data-id="${r.id}">
+ <td class="muted">${r.id}</td>
+ <td>${escapeHtml(r.prompt).slice(0, 100)}</td>
+ <td class="muted">${r.artifact_type || '—'}</td>
+ <td class="muted">${escapeHtml(r.artifact_name || '—')}</td>
+ <td class="muted">${r.current_stage}</td>
+ <td class="status">${r.status}</td>
+ <td class="muted">${new Date(r.created_at).toLocaleString()}</td>
+ <td>${action}</td>
+ </tr>
+ <tr class="events-row" id="events-${r.id}" style="display:none;"><td colspan="8"><div class="events-pane" id="events-pane-${r.id}">loading…</div></td></tr>`;
+ }).join('');
+ document.querySelectorAll('.activate-btn').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const id = btn.getAttribute('data-id');
+ if (!confirm(`Activate run ${id}? This copies the file to ~/.claude/.`)) return;
+ btn.disabled = true;
+ const res = await fetch(`${ORCH}/runs/${id}/activate`, { method: 'POST' });
+ const data = await res.json();
+ if (data.error) alert(`Activate failed: ${data.error}\n\n${data.hint || ''}`);
+ else alert(`Activated → ${data.dest}`);
+ loadRuns();
+ });
+ });
+ document.querySelectorAll('.run-row').forEach(row => {
+ row.addEventListener('click', async () => {
+ const id = row.getAttribute('data-id');
+ const evRow = document.getElementById(`events-${id}`);
+ const evPane = document.getElementById(`events-pane-${id}`);
+ if (evRow.style.display === 'none') {
+ evRow.style.display = '';
+ const r = await fetch(`${ORCH}/runs/${id}/events`);
+ const d = await r.json();
+ if (d.error) { evPane.textContent = d.error; return; }
+ evPane.innerHTML = `<h4>Events for run ${id}</h4>` + (d.events || []).map(ev => {
+ const payloadHtml = ev.payload ? `<details><summary>payload</summary><pre>${escapeHtml(JSON.stringify(ev.payload, null, 2))}</pre></details>` : '';
+ return `<div class="event"><span class="stg">stage ${ev.stage}</span><span class="nm">${ev.stage_name}</span><span class="lv ${ev.level}">${ev.level}</span><span class="ms">${escapeHtml(ev.message || '')}${payloadHtml}</span></div>`;
+ }).join('') || '<div class="muted">no events</div>';
+ } else {
+ evRow.style.display = 'none';
+ }
+ });
+ });
+ } catch (e) {
+ errEl.textContent = 'Orchestrator unreachable on :9890 — is it running?';
+ }
+}
+function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+document.getElementById('newRun').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const prompt = document.getElementById('prompt').value.trim();
+ if (!prompt) return;
+ await fetch(ORCH + '/runs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) });
+ document.getElementById('prompt').value = '';
+ loadRuns();
+});
+loadRuns();
+setInterval(loadRuns, 4000);
+</script>
+</body>
+</html>
diff --git a/scripts/clickthrough.js b/scripts/clickthrough.js
new file mode 100644
index 0000000..828e058
--- /dev/null
+++ b/scripts/clickthrough.js
@@ -0,0 +1,57 @@
+// Live click-through of The AI Factory viewer.
+// Opens :9891, screenshots the run table, clicks a row to expand events, screenshots that.
+
+const { chromium } = require('playwright');
+const path = require('node:path');
+const fs = require('node:fs/promises');
+
+const VIEWER = 'http://127.0.0.1:9891';
+const ORCH = 'http://127.0.0.1:9890';
+const OUT = path.join(__dirname, '..', 'output', 'clickthrough');
+
+(async () => {
+ await fs.mkdir(OUT, { recursive: true });
+ const browser = await chromium.launch({ headless: true });
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
+ const page = await ctx.newPage();
+ const consoleMsgs = [];
+ page.on('console', m => consoleMsgs.push(`[${m.type()}] ${m.text()}`));
+ page.on('pageerror', e => consoleMsgs.push(`[PAGEERROR] ${e.message}`));
+
+ console.log('STEP 1: table view');
+ await page.goto(VIEWER, { waitUntil: 'networkidle', timeout: 15000 });
+ await page.waitForTimeout(1500);
+ await page.screenshot({ path: path.join(OUT, '01-table.png'), fullPage: true });
+
+ const rowCount = await page.locator('tr.run-row').count();
+ console.log(` run rows: ${rowCount}`);
+ const activateCount = await page.locator('.activate-btn').count();
+ console.log(` activate buttons: ${activateCount}`);
+
+ // Click first row to expand events
+ if (rowCount > 0) {
+ console.log('STEP 2: click first row');
+ await page.locator('tr.run-row').first().click();
+ await page.waitForTimeout(1000);
+ await page.screenshot({ path: path.join(OUT, '02-events-expanded.png'), fullPage: true });
+ const eventCount = await page.locator('.event').count();
+ console.log(` events visible: ${eventCount}`);
+ }
+
+ await fs.writeFile(path.join(OUT, 'console.log'), consoleMsgs.join('\n'), 'utf8');
+ const summary = {
+ viewer_status: 'reachable',
+ row_count: rowCount,
+ activate_buttons: activateCount,
+ console_messages: consoleMsgs.length,
+ screenshots: ['01-table.png', '02-events-expanded.png']
+ };
+ await fs.writeFile(path.join(OUT, 'summary.json'), JSON.stringify(summary, null, 2));
+ console.log('SUMMARY:', JSON.stringify(summary));
+
+ const health = await (await fetch(`${ORCH}/health`)).json();
+ const runs = await (await fetch(`${ORCH}/runs`)).json();
+ console.log(`ORCH /health: ${JSON.stringify(health)} runs: ${runs.runs?.length || 0}`);
+
+ await browser.close();
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/run.js b/scripts/run.js
new file mode 100644
index 0000000..0c070d7
--- /dev/null
+++ b/scripts/run.js
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+// CLI entry: node scripts/run.js "build me a foo"
+
+const prompt = process.argv.slice(2).join(' ').trim();
+if (!prompt) {
+ console.error('Usage: node scripts/run.js "<prompt>"');
+ process.exit(1);
+}
+
+const ORCH = `http://127.0.0.1:${process.env.ORCHESTRATOR_PORT || 9890}`;
+
+(async () => {
+ const res = await fetch(`${ORCH}/runs`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt })
+ });
+ const data = await res.json();
+ console.log(JSON.stringify(data, null, 2));
+})().catch(err => {
+ console.error('Orchestrator unreachable:', err.message);
+ process.exit(2);
+});
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..b315f6b
--- /dev/null
+++ b/server.js
@@ -0,0 +1,201 @@
+// The AI Factory — orchestrator (:9890)
+// Stub server. Pipeline stages are no-ops; see PLAN.md before wiring.
+
+require('dotenv').config();
+const express = require('express');
+const { Pool } = require('pg');
+
+const PG_DATABASE = process.env.PG_DATABASE || 'ai_factory';
+
+// Hard safety guard: this project must NEVER touch dw_unified.
+// Steve's standing rule (2026-04-30) — The AI Factory has its own standalone DB.
+if (PG_DATABASE === 'dw_unified') {
+ console.error('[ai-factory] FATAL: PG_DATABASE=dw_unified is forbidden. Use the standalone `ai_factory` DB.');
+ process.exit(1);
+}
+
+const fs = require('node:fs/promises');
+const path = require('node:path');
+const os = require('node:os');
+const { runPipeline } = require('./src/pipeline');
+const { runMemoryWrite } = require('./src/stages/memory_write');
+
+const CLAUDE_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
+const CLAUDE_SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills');
+
+const app = express();
+app.use(express.json({ limit: '1mb' }));
+
+// Loopback-only CORS so the viewer (:9891) can fetch /runs from this orchestrator (:9890).
+// Server binds to 127.0.0.1 — '*' is safe since no remote origin can hit us.
+app.use((req, res, next) => {
+ res.set('Access-Control-Allow-Origin', '*');
+ res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ res.set('Access-Control-Allow-Headers', 'Content-Type');
+ if (req.method === 'OPTIONS') return res.sendStatus(204);
+ next();
+});
+
+const PORT = Number(process.env.ORCHESTRATOR_PORT || 9890);
+
+// Build pool config; only include `password` if one is actually set, so that
+// Unix-socket peer auth (the default on this Mac) works without forcing SCRAM.
+const poolCfg = {
+ host: process.env.PG_HOST || '/tmp',
+ port: Number(process.env.PG_PORT || 5432),
+ database: PG_DATABASE,
+ user: process.env.PG_USER || 'stevestudio2'
+};
+if (process.env.PG_PASSWORD) poolCfg.password = process.env.PG_PASSWORD;
+const pg = new Pool(poolCfg);
+
+app.get('/health', (_req, res) => {
+ res.json({ ok: true, service: 'ai-factory-orchestrator', port: PORT, db: PG_DATABASE });
+});
+
+app.get('/runs', async (_req, res) => {
+ try {
+ const { rows } = await pg.query(
+ 'SELECT id, prompt, artifact_type, artifact_name, status, current_stage, created_at, finished_at FROM runs ORDER BY id DESC LIMIT 100'
+ );
+ res.json({ runs: rows });
+ } catch (err) {
+ res.status(500).json({ error: err.message, hint: 'Did you `createdb ai_factory` and apply sql/001_init.sql?' });
+ }
+});
+
+app.post('/runs', async (req, res) => {
+ const { prompt, artifact_type = null, artifact_name = null } = req.body || {};
+ if (!prompt || typeof prompt !== 'string') {
+ return res.status(400).json({ error: 'prompt (string) required' });
+ }
+ try {
+ const { rows } = await pg.query(
+ 'INSERT INTO runs (prompt, artifact_type, artifact_name) VALUES ($1,$2,$3) RETURNING *',
+ [prompt, artifact_type, artifact_name]
+ );
+ const run = rows[0];
+ // Fire-and-forget pipeline; errors are persisted to events/runs.
+ runPipeline(pg, run).catch(err => console.error('[ai-factory] pipeline crash:', err));
+ res.json({ run });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+/**
+ * Manual activation: copy a sandboxed artifact into ~/.claude/agents or ~/.claude/skills.
+ * Hard-gated:
+ * - Run status must be `awaiting_activation*` (the critic approved the final version),
+ * unless `?force=true` is passed.
+ * - Will not overwrite an existing target file unless `?overwrite=true` is passed.
+ * - Reads the artifact location from the artifacts table — only files in our sandbox
+ * under ~/Projects/the-ai-factory/output/ are eligible.
+ */
+app.post('/runs/:id/activate', async (req, res) => {
+ const runId = Number(req.params.id);
+ const force = req.query.force === 'true';
+ const overwrite = req.query.overwrite === 'true';
+ try {
+ const { rows: runRows } = await pg.query('SELECT * FROM runs WHERE id=$1', [runId]);
+ if (!runRows.length) return res.status(404).json({ error: 'run not found' });
+ const run = runRows[0];
+
+ if (!force && !String(run.status).startsWith('awaiting_activation')) {
+ return res.status(409).json({
+ error: `run status is ${run.status}; activation requires awaiting_activation* (or ?force=true)`,
+ hint: 'critic did not approve. Inspect the artifact, fix manually, and pass ?force=true to override.'
+ });
+ }
+
+ const { rows: artRows } = await pg.query(
+ "SELECT * FROM artifacts WHERE run_id=$1 ORDER BY id DESC LIMIT 1",
+ [runId]
+ );
+ if (!artRows.length) return res.status(409).json({ error: 'no artifact for this run' });
+ const art = artRows[0];
+
+ const sandboxRoot = path.resolve(path.join(__dirname, 'output'));
+ const src = path.resolve(art.location);
+ if (!src.startsWith(sandboxRoot + path.sep)) {
+ return res.status(400).json({ error: `refusing to activate from outside sandbox: ${src}` });
+ }
+
+ let dest;
+ if (art.artifact_type === 'subagent') {
+ dest = path.join(CLAUDE_AGENTS_DIR, `${art.artifact_name}.md`);
+ } else if (art.artifact_type === 'skill') {
+ dest = path.join(CLAUDE_SKILLS_DIR, art.artifact_name, 'SKILL.md');
+ } else {
+ return res.status(400).json({ error: `unsupported artifact_type: ${art.artifact_type}` });
+ }
+
+ let existed = false;
+ try { await fs.access(dest); existed = true; } catch { /* ok */ }
+ if (existed && !overwrite) {
+ return res.status(409).json({
+ error: `target already exists: ${dest}`,
+ hint: 'pass ?overwrite=true to replace'
+ });
+ }
+
+ await fs.mkdir(path.dirname(dest), { recursive: true });
+ await fs.copyFile(src, dest);
+
+ await pg.query("UPDATE artifacts SET status='activated', location=$1 WHERE id=$2", [dest, art.id]);
+ await pg.query("UPDATE runs SET status='activated', updated_at=now() WHERE id=$1", [runId]);
+ await pg.query(
+ "INSERT INTO events (run_id, stage, stage_name, level, message, payload) VALUES ($1,$2,$3,$4,$5,$6)",
+ [runId, 10, 'activate', 'info', existed ? 'overwrote target' : 'wrote target', JSON.stringify({ src, dest, force, overwrite })]
+ );
+
+ // Stage 9 — memory_write. Best-effort: log but don't fail activation if the
+ // index append blows up. Spec is recovered from the intake event payload.
+ let memory = null;
+ try {
+ const { rows: specRows } = await pg.query(
+ "SELECT payload FROM events WHERE run_id=$1 AND stage_name='intake' AND message='spec extracted' ORDER BY id DESC LIMIT 1",
+ [runId]
+ );
+ const spec = specRows[0]?.payload?.spec || null;
+ memory = await runMemoryWrite({
+ runId,
+ artifactType: art.artifact_type,
+ artifactName: art.artifact_name,
+ spec,
+ dest
+ });
+ await pg.query(
+ "INSERT INTO events (run_id, stage, stage_name, level, message, payload) VALUES ($1,$2,$3,$4,$5,$6)",
+ [runId, 9, 'memory_write', 'info',
+ memory.skipped ? `skipped: ${memory.reason}` : 'appended to MEMORY.md',
+ JSON.stringify({ line: memory.line })]
+ );
+ } catch (err) {
+ await pg.query(
+ "INSERT INTO events (run_id, stage, stage_name, level, message, payload) VALUES ($1,$2,$3,$4,$5,$6)",
+ [runId, 9, 'memory_write', 'error', 'memory_write failed', JSON.stringify({ error: err.message })]
+ );
+ }
+
+ res.json({ ok: true, src, dest, existed_before: existed, memory });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/runs/:id/events', async (req, res) => {
+ try {
+ const { rows } = await pg.query(
+ 'SELECT id, stage, stage_name, level, message, payload, created_at FROM events WHERE run_id=$1 ORDER BY id ASC',
+ [Number(req.params.id)]
+ );
+ res.json({ events: rows });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.listen(PORT, '127.0.0.1', () => {
+ console.log(`[ai-factory] orchestrator listening on http://127.0.0.1:${PORT} (db=${PG_DATABASE})`);
+});
diff --git a/sql/001_init.sql b/sql/001_init.sql
new file mode 100644
index 0000000..1b9a98a
--- /dev/null
+++ b/sql/001_init.sql
@@ -0,0 +1,47 @@
+-- The AI Factory — schema for the standalone `ai_factory` database.
+--
+-- This file MUST be applied against the standalone `ai_factory` DB only.
+-- DO NOT run inside dw_unified. The orchestrator's safety guard will refuse
+-- to boot if PG_DATABASE=dw_unified, but pgsql files don't have that guard,
+-- so verify the connection target before running:
+--
+-- createdb ai_factory # one-time
+-- psql -d ai_factory -f sql/001_init.sql # apply schema
+--
+-- Tables live in the default `public` schema since this DB is single-purpose.
+
+CREATE TABLE IF NOT EXISTS runs (
+ id BIGSERIAL PRIMARY KEY,
+ prompt TEXT NOT NULL,
+ artifact_type TEXT,
+ artifact_name TEXT,
+ status TEXT NOT NULL DEFAULT 'pending',
+ current_stage INTEGER NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ finished_at TIMESTAMPTZ
+);
+
+CREATE TABLE IF NOT EXISTS events (
+ id BIGSERIAL PRIMARY KEY,
+ run_id BIGINT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
+ stage INTEGER NOT NULL,
+ stage_name TEXT NOT NULL,
+ level TEXT NOT NULL DEFAULT 'info',
+ message TEXT,
+ payload JSONB,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE IF NOT EXISTS artifacts (
+ id BIGSERIAL PRIMARY KEY,
+ run_id BIGINT REFERENCES runs(id) ON DELETE SET NULL,
+ artifact_type TEXT NOT NULL,
+ artifact_name TEXT NOT NULL,
+ location TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'draft',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS events_run_idx ON events (run_id, stage);
+CREATE INDEX IF NOT EXISTS artifacts_name_idx ON artifacts (artifact_type, artifact_name);
diff --git a/src/llm.js b/src/llm.js
new file mode 100644
index 0000000..b980496
--- /dev/null
+++ b/src/llm.js
@@ -0,0 +1,72 @@
+// The AI Factory — LLM adapters.
+//
+// Hybrid design (2026-04-30): Ollama (local, free, fast) for mechanical stages;
+// Claude CLI subprocess (Steve's Max sub, no per-token charge) for the critic pass.
+// No Anthropic API calls — Steve's standing rule "if api costs money.. no".
+
+const { spawn } = require('node:child_process');
+
+const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://127.0.0.1:11434';
+const CLAUDE_CLI = process.env.CLAUDE_CLI || 'claude';
+
+/**
+ * Call a local Ollama model. If `format` is 'json', the model is forced to
+ * emit valid JSON and the result is parsed before returning.
+ */
+async function ollama({ model, system, prompt, format = null, temperature = 0.2 }) {
+ const body = {
+ model,
+ stream: false,
+ options: { temperature },
+ messages: [
+ ...(system ? [{ role: 'system', content: system }] : []),
+ { role: 'user', content: prompt }
+ ]
+ };
+ if (format === 'json') body.format = 'json';
+
+ const res = await fetch(`${OLLAMA_HOST}/api/chat`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ });
+ if (!res.ok) throw new Error(`ollama ${model} http ${res.status}: ${await res.text()}`);
+ const data = await res.json();
+ const content = data?.message?.content ?? '';
+ if (format === 'json') {
+ try { return JSON.parse(content); }
+ catch (e) { throw new Error(`ollama ${model} did not return valid JSON: ${content.slice(0, 200)}`); }
+ }
+ return content;
+}
+
+/**
+ * Run Claude CLI as a one-shot subprocess. Used only for the Manager critic pass
+ * where quality matters more than speed. Counts against Steve's Max quota.
+ *
+ * Pipes the prompt via stdin instead of `-p prompt` arg style — avoids silent
+ * exit-1 failures on prompts containing JSON, newlines, or non-ASCII chars.
+ * Discovered while building Visual Factory 2026-04-30.
+ */
+function claudeCli({ prompt, timeoutMs = 120000 }) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(CLAUDE_CLI, ['-p'], { stdio: ['pipe', 'pipe', 'pipe'] });
+ let stdout = '';
+ let stderr = '';
+ const timer = setTimeout(() => {
+ child.kill('SIGTERM');
+ reject(new Error(`claude CLI timed out after ${timeoutMs}ms`));
+ }, timeoutMs);
+ child.stdout.on('data', d => { stdout += d; });
+ child.stderr.on('data', d => { stderr += d; });
+ child.on('error', err => { clearTimeout(timer); reject(new Error(`claude CLI spawn error: ${err.message}`)); });
+ child.on('close', code => {
+ clearTimeout(timer);
+ if (code !== 0) return reject(new Error(`claude CLI exit ${code}: ${stderr.slice(0, 400) || '(empty stderr)'}`));
+ resolve(stdout.trim());
+ });
+ child.stdin.end(prompt);
+ });
+}
+
+module.exports = { ollama, claudeCli };
diff --git a/src/pipeline.js b/src/pipeline.js
new file mode 100644
index 0000000..6ecf205
--- /dev/null
+++ b/src/pipeline.js
@@ -0,0 +1,179 @@
+// The AI Factory — pipeline runner.
+// 11 stages mirroring Site Factory.
+// Parallelization (2026-04-30): research ‖ scaffold, document ‖ critic.
+
+const { runIntake } = require('./stages/intake');
+const { runResearch } = require('./stages/research');
+const { runScaffold } = require('./stages/scaffold');
+const { runDocument } = require('./stages/document');
+const { runSmokeTest } = require('./stages/smoke_test');
+const { runCritic } = require('./stages/critic');
+const { runIterate } = require('./stages/iterate');
+
+const STAGES = [
+ 'intake', 'research', 'scaffold', 'wire', 'smoke_test',
+ 'critic', 'iterate', 'document', 'memory_write', 'activate', 'watchdog_hook'
+];
+
+async function logEvent(pg, runId, stage, stageName, level, message, payload) {
+ await pg.query(
+ 'INSERT INTO events (run_id, stage, stage_name, level, message, payload) VALUES ($1,$2,$3,$4,$5,$6)',
+ [runId, stage, stageName, level, message, payload ? JSON.stringify(payload) : null]
+ );
+}
+async function setStage(pg, runId, stage, status) {
+ await pg.query('UPDATE runs SET current_stage=$1, status=$2, updated_at=now() WHERE id=$3', [stage, status, runId]);
+}
+async function finishRun(pg, runId, status) {
+ await pg.query('UPDATE runs SET status=$1, finished_at=now(), updated_at=now() WHERE id=$2', [status, runId]);
+}
+async function setArtifactMeta(pg, runId, type, name) {
+ await pg.query('UPDATE runs SET artifact_type=$1, artifact_name=$2, updated_at=now() WHERE id=$3', [type, name, runId]);
+}
+
+async function runPipeline(pg, run) {
+ const runId = run.id;
+
+ try {
+ // === Stage 1: intake ===============================================
+ await setStage(pg, runId, 1, 'running');
+ await logEvent(pg, runId, 1, 'intake', 'info', 'starting', null);
+ const { spec, errors: intakeErrors } = await runIntake({ prompt: run.prompt });
+ if (intakeErrors.length) {
+ await logEvent(pg, runId, 1, 'intake', 'error', 'spec validation failed', { spec, errors: intakeErrors });
+ await finishRun(pg, runId, 'failed_intake');
+ return;
+ }
+ await setArtifactMeta(pg, runId, spec.artifact_type, spec.artifact_name);
+ await logEvent(pg, runId, 1, 'intake', 'info', 'spec extracted', { spec });
+
+ // === Stages 2 + 3 in parallel: research ‖ scaffold =================
+ await setStage(pg, runId, 3, 'running'); // surface the longer-running one
+ const [researchResult, scaffold] = await Promise.all([
+ runResearch({ spec }).catch(e => ({ error: e.message })),
+ runScaffold({ runId, spec })
+ ]);
+
+ // log research outcome
+ if (researchResult.error) {
+ await logEvent(pg, runId, 2, 'research', 'warn', 'research failed (non-blocking)', { error: researchResult.error });
+ } else {
+ const conflictMsg = researchResult.exact_conflict
+ ? `EXACT NAME CONFLICT — ~/.claude/${spec.artifact_type === 'subagent' ? 'agents' : 'skills'}/${spec.artifact_name} already exists`
+ : researchResult.matches.length
+ ? `${researchResult.matches.length} similar artifact(s) found`
+ : 'no conflicts';
+ await logEvent(pg, runId, 2, 'research',
+ researchResult.exact_conflict ? 'warn' : 'info',
+ conflictMsg,
+ researchResult);
+ }
+
+ // log scaffold outcome
+ if (scaffold.errors.length) {
+ await logEvent(pg, runId, 3, 'scaffold', 'warn', 'validation issues', { errors: scaffold.errors, outPath: scaffold.outPath });
+ } else {
+ await logEvent(pg, runId, 3, 'scaffold', 'info', 'wrote file', { outPath: scaffold.outPath, bytes: scaffold.content.length });
+ }
+ await pg.query(
+ 'INSERT INTO artifacts (run_id, artifact_type, artifact_name, location, status) VALUES ($1,$2,$3,$4,$5)',
+ [runId, spec.artifact_type, spec.artifact_name, scaffold.outPath, 'draft']
+ );
+
+ // === Stage 4: wire (no-op for v1 subagent/skill artifacts) =========
+ await setStage(pg, runId, 4, 'running');
+ await logEvent(pg, runId, 4, 'wire', 'info', 'no-op for v1 artifacts (no external registration needed)', null);
+
+ // === Stage 5: smoke_test (cheap, local) ============================
+ await setStage(pg, runId, 5, 'running');
+ const smoke = await runSmokeTest({ outPath: scaffold.outPath, spec });
+ await logEvent(pg, runId, 5, 'smoke_test',
+ smoke.passing ? 'info' : 'warn',
+ `${smoke.passed}/${smoke.total} checks passing`,
+ smoke);
+
+ // === Stages 6 + 8 in parallel: critic ‖ document ===================
+ // Document only runs for skills — for subagents we just skip and Promise.all
+ // gets back an immediate { skipped: true }.
+ await setStage(pg, runId, 6, 'running');
+ await logEvent(pg, runId, 6, 'critic', 'info', 'starting (claude CLI subprocess) ‖ document', null);
+ const [criticResult, documentResult] = await Promise.all([
+ runCritic({ artifactType: spec.artifact_type, content: scaffold.content, spec })
+ .catch(e => ({ approved: false, score: 0, issues: [`critic failed: ${e.message}`], fix_instructions: [] })),
+ runDocument({ runId, spec, scaffoldOutPath: scaffold.outPath })
+ .catch(e => ({ skipped: true, error: e.message }))
+ ]);
+
+ let review = criticResult;
+ await logEvent(pg, runId, 6, 'critic',
+ review.approved ? 'info' : 'warn',
+ `score=${review.score} approved=${review.approved}`,
+ { review });
+ await logEvent(pg, runId, 8, 'document',
+ documentResult.skipped ? 'info' : 'info',
+ documentResult.skipped ? `skipped: ${documentResult.reason || documentResult.error || 'n/a'}` : 'wrote README.md',
+ documentResult);
+
+ // === Stage 7: iterate (only if rejected, with re-critic) ===========
+ await setStage(pg, runId, 7, 'running');
+ let finalReview = review;
+ let didIterate = false;
+ if (!review.approved && review.fix_instructions.length) {
+ await logEvent(pg, runId, 7, 'iterate', 'info', `applying ${review.fix_instructions.length} fixes`, { fixes: review.fix_instructions });
+ try {
+ const iter = await runIterate({ outPath: scaffold.outPath, content: scaffold.content, spec, review });
+ scaffold.content = iter.content;
+ didIterate = true;
+ await logEvent(pg, runId, 7, 'iterate', 'info', 'rewrote file', { bytes: iter.content.length });
+
+ await logEvent(pg, runId, 7, 'iterate', 'info', 're-running critic on v2', null);
+ try {
+ const review2 = await runCritic({ artifactType: spec.artifact_type, content: iter.content, spec });
+ finalReview = review2;
+ await logEvent(pg, runId, 7, 'iterate', review2.approved ? 'info' : 'warn',
+ `re-critic: score=${review2.score} approved=${review2.approved} (was ${review.score})`,
+ { review: review2 });
+ } catch (err) {
+ await logEvent(pg, runId, 7, 'iterate', 'error', 're-critic failed', { error: err.message });
+ }
+ } catch (err) {
+ await logEvent(pg, runId, 7, 'iterate', 'error', 'iterate failed', { error: err.message });
+ }
+ } else {
+ await logEvent(pg, runId, 7, 'iterate', 'info', review.approved ? 'critic approved — no changes' : 'no fix_instructions', null);
+ }
+
+ // Re-run smoke test on revised content if iterate ran
+ if (didIterate) {
+ const smoke2 = await runSmokeTest({ outPath: scaffold.outPath, spec });
+ await logEvent(pg, runId, 5, 'smoke_test',
+ smoke2.passing ? 'info' : 'warn',
+ `post-iterate: ${smoke2.passed}/${smoke2.total} checks passing`,
+ smoke2);
+ }
+
+ // Persist the final review on the artifact row so activate can gate on it.
+ await pg.query("UPDATE artifacts SET status=$1 WHERE run_id=$2",
+ [finalReview.approved ? 'awaiting_activation' : 'unapproved', runId]);
+
+ // === Stage 9 + 10: deferred to POST /runs/:id/activate =============
+ await setStage(pg, runId, 9, 'running');
+ await logEvent(pg, runId, 9, 'memory_write', 'info', 'deferred — fires inside activate handler', null);
+ await setStage(pg, runId, 10, 'running');
+ await logEvent(pg, runId, 10, 'activate', 'info', 'awaiting manual activation (POST /runs/:id/activate)', null);
+
+ // === Stage 11: watchdog_hook (no-op for v1) ========================
+ await setStage(pg, runId, 11, 'running');
+ await logEvent(pg, runId, 11, 'watchdog_hook', 'info', 'no-op for v1 artifacts (only relevant for service-type artifacts in v2)', null);
+
+ const finalStatus = finalReview.approved
+ ? (didIterate ? 'awaiting_activation_v2' : 'awaiting_activation')
+ : 'completed_unapproved';
+ await finishRun(pg, runId, finalStatus);
+ } catch (err) {
+ await logEvent(pg, runId, run.current_stage || 0, STAGES[run.current_stage - 1] || 'unknown', 'error', err.message, { stack: err.stack });
+ await finishRun(pg, runId, 'failed');
+ }
+}
+
+module.exports = { STAGES, runPipeline };
diff --git a/src/stages/critic.js b/src/stages/critic.js
new file mode 100644
index 0000000..605ce94
--- /dev/null
+++ b/src/stages/critic.js
@@ -0,0 +1,57 @@
+// Stage 6 — Manager critic. Single Claude CLI subprocess call (Steve's Max sub).
+// Reads the scaffolded file, returns a JSON review.
+
+const { claudeCli } = require('../llm');
+
+const CRITIC_PROMPT = (artifactType, content, spec) => `You are reviewing a draft Claude Code ${artifactType} file produced by an automated pipeline. Be a strict reviewer.
+
+The artifact spec was:
+${JSON.stringify(spec, null, 2)}
+
+The draft file content:
+---BEGIN FILE---
+${content}
+---END FILE---
+
+Evaluate against Anthropic's guidelines for Claude Code ${artifactType}s:
+- YAML frontmatter is valid (name, description${artifactType === 'subagent' ? ', tools, model' : ''})
+- description is specific enough to trigger the right way (not too vague, not too narrow)
+- ${artifactType === 'subagent' ? 'tools list is the minimum needed (no over-granting)' : 'WHEN to use is unambiguous'}
+- Body sections are present and useful
+- No hallucinated file paths, env vars, or commands
+- No prose padding; every line earns its place
+
+Return ONLY a JSON object, no prose, no fences:
+{
+ "approved": <true|false>,
+ "score": <0-100>,
+ "issues": [<short string per concrete problem>],
+ "fix_instructions": [<short string per actionable revision the next stage should apply>]
+}`;
+
+async function runCritic({ artifactType, content, spec }) {
+ const raw = await claudeCli({
+ prompt: CRITIC_PROMPT(artifactType, content, spec),
+ timeoutMs: 180000
+ });
+
+ // Claude CLI may wrap JSON in fences or add prose; pull out the first {...} block.
+ const match = raw.match(/\{[\s\S]*\}/);
+ if (!match) {
+ return { approved: false, score: 0, issues: ['critic returned no JSON'], fix_instructions: [], raw_excerpt: raw.slice(0, 300) };
+ }
+ let review;
+ try { review = JSON.parse(match[0]); }
+ catch (e) {
+ return { approved: false, score: 0, issues: [`critic JSON parse failed: ${e.message}`], fix_instructions: [], raw_excerpt: match[0].slice(0, 300) };
+ }
+
+ return {
+ approved: !!review.approved,
+ score: Number(review.score) || 0,
+ issues: Array.isArray(review.issues) ? review.issues : [],
+ fix_instructions: Array.isArray(review.fix_instructions) ? review.fix_instructions : []
+ };
+}
+
+module.exports = { runCritic };
diff --git a/src/stages/document.js b/src/stages/document.js
new file mode 100644
index 0000000..cc8861b
--- /dev/null
+++ b/src/stages/document.js
@@ -0,0 +1,42 @@
+// Stage 8 — document. For skills only, drop a README.md into the artifact dir
+// alongside SKILL.md so Steve can read what it does without opening SKILL.md.
+// No-op for subagents (they're a single .md and self-documenting).
+
+const fs = require('node:fs/promises');
+const path = require('node:path');
+
+async function runDocument({ runId, spec, scaffoldOutPath }) {
+ if (spec.artifact_type !== 'skill') return { skipped: true, reason: 'subagent — README not needed' };
+
+ const dir = path.dirname(scaffoldOutPath);
+ const readmePath = path.join(dir, 'README.md');
+
+ const triggers = (spec.triggers || []).length
+ ? spec.triggers.map(t => `- "${t}"`).join('\n')
+ : '- (none specified in spec)';
+ const oos = (spec.out_of_scope || []).length
+ ? spec.out_of_scope.map(o => `- ${o}`).join('\n')
+ : '- (none specified)';
+
+ const content = `# ${spec.artifact_name}
+
+${spec.scope || ''}
+
+## Triggers
+
+${triggers}
+
+## Out of scope
+
+${oos}
+
+---
+
+Generated by The AI Factory · run #${runId} · ${new Date().toISOString().slice(0, 10)}
+`;
+
+ await fs.writeFile(readmePath, content, 'utf8');
+ return { skipped: false, readmePath };
+}
+
+module.exports = { runDocument };
diff --git a/src/stages/intake.js b/src/stages/intake.js
new file mode 100644
index 0000000..9604aff
--- /dev/null
+++ b/src/stages/intake.js
@@ -0,0 +1,42 @@
+// Stage 1 — intake. Free-text prompt → structured artifact spec via local Ollama.
+
+const { ollama } = require('../llm');
+
+const SYSTEM = `You convert a Steve-style request into a structured plan for "The AI Factory".
+The Factory builds Claude Code subagents and /skills (v1 scope).
+Return ONLY a JSON object with these keys:
+ artifact_type one of: "subagent" | "skill"
+ artifact_name short kebab-case identifier (e.g. "freezer-watcher")
+ surface where it runs: "claude-code" (subagents/skills) | "cli" | "service"
+ scope one short sentence describing what the artifact does
+ triggers array of short phrases that should activate it (max 5)
+ out_of_scope array of things it explicitly should NOT do (max 3)
+No prose, no markdown, no code fences. Just the JSON.`;
+
+async function runIntake({ prompt }) {
+ const spec = await ollama({
+ model: 'qwen3:14b',
+ system: SYSTEM,
+ prompt,
+ format: 'json',
+ temperature: 0.1
+ });
+
+ // Normalize and validate the shape so downstream stages can trust it.
+ const errors = [];
+ if (!spec.artifact_type || !['subagent', 'skill'].includes(spec.artifact_type)) {
+ errors.push(`artifact_type must be "subagent" or "skill", got ${JSON.stringify(spec.artifact_type)}`);
+ }
+ if (!spec.artifact_name || !/^[a-z0-9-]{2,60}$/.test(spec.artifact_name)) {
+ errors.push(`artifact_name must be kebab-case 2-60 chars, got ${JSON.stringify(spec.artifact_name)}`);
+ }
+ if (!spec.scope || typeof spec.scope !== 'string') {
+ errors.push('scope is required (string)');
+ }
+ spec.triggers = Array.isArray(spec.triggers) ? spec.triggers.slice(0, 5) : [];
+ spec.out_of_scope = Array.isArray(spec.out_of_scope) ? spec.out_of_scope.slice(0, 3) : [];
+
+ return { spec, errors };
+}
+
+module.exports = { runIntake };
diff --git a/src/stages/iterate.js b/src/stages/iterate.js
new file mode 100644
index 0000000..bd29228
--- /dev/null
+++ b/src/stages/iterate.js
@@ -0,0 +1,39 @@
+// Stage 7 — iterate. If critic rejected, qwen3:14b applies the fix_instructions
+// to the scaffolded file and overwrites it. Single retry only.
+
+const fs = require('node:fs/promises');
+const { ollama } = require('../llm');
+
+const ITERATE_SYSTEM = `You revise a Claude Code subagent or skill file in place. Output ONLY the revised file content — same format, same frontmatter shape, no code fences, no commentary.`;
+
+async function runIterate({ outPath, content, spec, review }) {
+ if (review.approved) return { changed: false, content };
+
+ const userPrompt = `Original spec:
+${JSON.stringify(spec, null, 2)}
+
+Current file content:
+---BEGIN FILE---
+${content}
+---END FILE---
+
+Issues found by critic:
+${review.issues.map((i, n) => `${n + 1}. ${i}`).join('\n')}
+
+Apply these fixes:
+${review.fix_instructions.map((f, n) => `${n + 1}. ${f}`).join('\n')}
+
+Output the revised file content now.`;
+
+ const revised = (await ollama({
+ model: 'qwen3:14b',
+ system: ITERATE_SYSTEM,
+ prompt: userPrompt,
+ temperature: 0.2
+ })).replace(/^```(?:markdown|md)?\s*\n/, '').replace(/\n```\s*$/, '').trim();
+
+ await fs.writeFile(outPath, revised, 'utf8');
+ return { changed: true, content: revised };
+}
+
+module.exports = { runIterate };
diff --git a/src/stages/memory_write.js b/src/stages/memory_write.js
new file mode 100644
index 0000000..e842a4c
--- /dev/null
+++ b/src/stages/memory_write.js
@@ -0,0 +1,53 @@
+// Stage 9 — memory_write. Appends one line to Steve's MEMORY.md index after a
+// successful activation. We do NOT write a separate memory file per artifact —
+// that would pollute the memory dir; the index entry is enough for future
+// sessions to learn the artifact exists.
+
+const fs = require('node:fs/promises');
+const path = require('node:path');
+const os = require('node:os');
+
+const MEMORY_INDEX = path.join(
+ os.homedir(),
+ '.claude',
+ 'projects',
+ '-Users-stevestudio2',
+ 'memory',
+ 'MEMORY.md'
+);
+
+function todayStamp() {
+ return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
+}
+
+function buildLine({ runId, artifactType, artifactName, spec, dest }) {
+ const scope = (spec?.scope || '').replace(/\s+/g, ' ').trim();
+ const triggers = Array.isArray(spec?.triggers) && spec.triggers.length
+ ? ` triggers: ${spec.triggers.slice(0, 3).join(', ')};`
+ : '';
+ const surface = artifactType === 'subagent' ? '~/.claude/agents/' : '~/.claude/skills/';
+ return `- AI Factory shipped: \`${artifactName}\` (${artifactType}) — ${scope}${triggers} [run #${runId}, ${todayStamp()}, lives in ${surface}]`;
+}
+
+async function appendMemoryIndex(line) {
+ let existing = '';
+ try { existing = await fs.readFile(MEMORY_INDEX, 'utf8'); }
+ catch (e) {
+ if (e.code !== 'ENOENT') throw e;
+ }
+ // Avoid double-entry if activate is called twice for the same artifact.
+ if (existing.includes(line.split(' [run #')[0])) {
+ return { skipped: true, reason: 'already present' };
+ }
+ const trailing = existing.endsWith('\n') ? '' : (existing ? '\n' : '');
+ await fs.writeFile(MEMORY_INDEX, existing + trailing + line + '\n', 'utf8');
+ return { skipped: false };
+}
+
+async function runMemoryWrite({ runId, artifactType, artifactName, spec, dest }) {
+ const line = buildLine({ runId, artifactType, artifactName, spec, dest });
+ const result = await appendMemoryIndex(line);
+ return { line, ...result };
+}
+
+module.exports = { runMemoryWrite, MEMORY_INDEX };
diff --git a/src/stages/research.js b/src/stages/research.js
new file mode 100644
index 0000000..0d0ffbd
--- /dev/null
+++ b/src/stages/research.js
@@ -0,0 +1,63 @@
+// Stage 2 — research. Scan ~/.claude/agents/ and ~/.claude/skills/ for artifacts
+// that look similar to spec.artifact_name. Non-blocking: this only WARNS via an
+// event so Steve doesn't accidentally activate a near-duplicate. Activation can
+// still proceed; the warning lives in the events log.
+
+const fs = require('node:fs/promises');
+const path = require('node:path');
+const os = require('node:os');
+
+const AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
+const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills');
+
+async function listExisting() {
+ const out = [];
+ try {
+ for (const f of await fs.readdir(AGENTS_DIR)) {
+ if (f.endsWith('.md')) out.push({ type: 'subagent', name: f.replace(/\.md$/, '') });
+ }
+ } catch (e) { if (e.code !== 'ENOENT') throw e; }
+ try {
+ for (const d of await fs.readdir(SKILLS_DIR, { withFileTypes: true })) {
+ if (!d.isDirectory() || d.name.startsWith('_') || d.name.startsWith('.')) continue;
+ out.push({ type: 'skill', name: d.name });
+ }
+ } catch (e) { if (e.code !== 'ENOENT') throw e; }
+ return out;
+}
+
+// Bigram Jaccard similarity, 0.0 – 1.0. Cheap, deterministic, no LLM call.
+function similarity(a, b) {
+ const bigrams = (s) => {
+ const out = new Set();
+ const lower = s.toLowerCase();
+ for (let i = 0; i < lower.length - 1; i++) out.add(lower.slice(i, i + 2));
+ return out;
+ };
+ const A = bigrams(a);
+ const B = bigrams(b);
+ if (!A.size || !B.size) return 0;
+ let inter = 0;
+ for (const x of A) if (B.has(x)) inter++;
+ const union = A.size + B.size - inter;
+ return union === 0 ? 0 : inter / union;
+}
+
+async function runResearch({ spec }) {
+ const existing = await listExisting();
+ const matches = [];
+ for (const e of existing) {
+ if (e.type !== spec.artifact_type) continue;
+ if (e.name === spec.artifact_name) {
+ matches.push({ ...e, similarity: 1.0, conflict: 'exact-name' });
+ continue;
+ }
+ const s = similarity(e.name, spec.artifact_name);
+ if (s >= 0.5) matches.push({ ...e, similarity: Number(s.toFixed(2)) });
+ }
+ matches.sort((a, b) => b.similarity - a.similarity);
+ const exact = matches.find(m => m.conflict === 'exact-name');
+ return { existing_count: existing.length, matches: matches.slice(0, 5), exact_conflict: !!exact };
+}
+
+module.exports = { runResearch };
diff --git a/src/stages/scaffold.js b/src/stages/scaffold.js
new file mode 100644
index 0000000..36e1be3
--- /dev/null
+++ b/src/stages/scaffold.js
@@ -0,0 +1,93 @@
+// Stage 3 — scaffold. qwen3:14b template-fills a Claude Code subagent .md or skill SKILL.md
+// and writes it to a sandbox dir. Never writes to ~/.claude/{agents,skills}/ — that's stage 10.
+
+const fs = require('node:fs/promises');
+const path = require('node:path');
+const { ollama } = require('../llm');
+
+const SANDBOX_ROOT = path.join(__dirname, '..', '..', 'output');
+
+const SUBAGENT_SYSTEM = `You write Claude Code subagent files. Output ONLY the raw file content, no code fences, no commentary.
+
+Format:
+---
+name: <kebab-name from spec>
+description: <one detailed paragraph that includes WHEN to use this subagent and what it does. Use "PROACTIVELY" if it should be invoked without explicit user request. 80-200 words.>
+tools: <comma-separated tools the subagent needs: Read, Write, Edit, Bash, Grep, WebSearch, WebFetch — pick only what is needed>
+model: sonnet
+---
+
+<system-prompt body in markdown. Sections to include:
+ ## Your job (1 sentence)
+ ## Inputs (what the orchestrator hands you)
+ ## What to check / do (bullet list)
+ ## Output format (exact structure of what you return)
+ ## Out of scope (from spec.out_of_scope)
+Keep it under 400 words total.>`;
+
+const SKILL_SYSTEM = `You write Claude Code skill SKILL.md files. Output ONLY the raw file content, no code fences, no commentary.
+
+Format:
+---
+name: <kebab-name from spec>
+description: <one detailed paragraph including WHEN this skill should auto-trigger. Mention specific trigger phrases from spec.triggers. 60-180 words.>
+---
+
+# <Title from name>
+
+<Skill body in markdown. Sections:
+ ## When to use (bullet list of triggers)
+ ## What it does (bullet list)
+ ## How to invoke (concrete steps or commands)
+ ## Out of scope (from spec.out_of_scope)
+Keep under 350 words.>`;
+
+async function runScaffold({ runId, spec }) {
+ const isSubagent = spec.artifact_type === 'subagent';
+ const system = isSubagent ? SUBAGENT_SYSTEM : SKILL_SYSTEM;
+
+ const userPrompt = `Spec:
+${JSON.stringify(spec, null, 2)}
+
+Write the file content now.`;
+
+ const content = await ollama({
+ model: 'qwen3:14b',
+ system,
+ prompt: userPrompt,
+ temperature: 0.2
+ });
+
+ const cleaned = stripFences(content).trim();
+
+ const runDir = path.join(SANDBOX_ROOT, `run-${runId}`);
+ await fs.mkdir(runDir, { recursive: true });
+ const outPath = isSubagent
+ ? path.join(runDir, `${spec.artifact_name}.md`)
+ : path.join(runDir, spec.artifact_name, 'SKILL.md');
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
+ await fs.writeFile(outPath, cleaned, 'utf8');
+
+ const errors = validateScaffold(cleaned, spec);
+ return { outPath, content: cleaned, errors };
+}
+
+function stripFences(s) {
+ // qwen3:14b sometimes wraps output in ```markdown fences despite instructions.
+ return s.replace(/^```(?:markdown|md)?\s*\n/, '').replace(/\n```\s*$/, '');
+}
+
+function validateScaffold(content, spec) {
+ const errors = [];
+ if (!/^---\s*\n/.test(content)) errors.push('missing YAML frontmatter opening ---');
+ if (!/\n---\s*\n/.test(content)) errors.push('missing YAML frontmatter closing ---');
+ if (!new RegExp(`name:\\s*${escapeRe(spec.artifact_name)}`).test(content)) {
+ errors.push(`frontmatter name does not match spec.artifact_name (${spec.artifact_name})`);
+ }
+ if (!/description:\s*\S/.test(content)) errors.push('frontmatter missing description');
+ return errors;
+}
+
+function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
+
+module.exports = { runScaffold };
diff --git a/src/stages/smoke_test.js b/src/stages/smoke_test.js
new file mode 100644
index 0000000..4cb7c6b
--- /dev/null
+++ b/src/stages/smoke_test.js
@@ -0,0 +1,35 @@
+// Stage 5 — smoke_test. Cheap structural validation on the final scaffold file.
+// Pure-local, deterministic — no LLM, no Claude CLI. Catches the most common
+// failure modes (missing frontmatter, name drift, empty body) without burning
+// quota or wall-clock.
+
+const fs = require('node:fs/promises');
+
+function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
+
+async function runSmokeTest({ outPath, spec }) {
+ const content = await fs.readFile(outPath, 'utf8');
+ const checks = [
+ { name: 'has_frontmatter_open', pass: /^---\s*\n/.test(content) },
+ { name: 'has_frontmatter_close', pass: /\n---\s*\n/.test(content) },
+ { name: 'name_matches_spec', pass: new RegExp(`^name:\\s*${escapeRe(spec.artifact_name)}\\s*$`, 'm').test(content) },
+ { name: 'has_description', pass: /^description:\s*\S/m.test(content) },
+ { name: 'body_nonempty', pass: content.length > 200 },
+ { name: 'no_unrendered_template', pass: !/<<<|>>>|TODO_FILL|XXX_REPLACE/.test(content) }
+ ];
+ if (spec.artifact_type === 'subagent') {
+ checks.push({ name: 'has_tools', pass: /^tools:\s*\S/m.test(content) });
+ checks.push({ name: 'has_model', pass: /^model:\s*(sonnet|opus|haiku)\s*$/m.test(content) });
+ }
+
+ const passed = checks.filter(c => c.pass).length;
+ return {
+ checks,
+ passed,
+ total: checks.length,
+ passing: passed === checks.length,
+ failing_checks: checks.filter(c => !c.pass).map(c => c.name)
+ };
+}
+
+module.exports = { runSmokeTest };
diff --git a/viewer.js b/viewer.js
new file mode 100644
index 0000000..17603ec
--- /dev/null
+++ b/viewer.js
@@ -0,0 +1,15 @@
+// The AI Factory — viewer (:9891)
+// Static viewer that polls the orchestrator's /runs endpoint.
+
+require('dotenv').config();
+const express = require('express');
+const path = require('path');
+
+const app = express();
+const PORT = Number(process.env.VIEWER_PORT || 9891);
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.listen(PORT, '127.0.0.1', () => {
+ console.log(`[ai-factory] viewer listening on http://127.0.0.1:${PORT}`);
+});
(oldest)
·
back to The Ai Factory
·
tighten .gitignore: add missing standing-rule patterns (tmp/ 20486fe →