diff --git a/AUDIT.md b/AUDIT.md index 0158e80c..1284085a 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,260 +1,93 @@ -# AUDIT.md — wecanjustbuildthings.dev architecture & Path-A audit - -**Mode:** read-only / plan. No source files were modified to produce this report. -**Branch:** `claude/kind-albattani-tnycq4` -**Scope:** the four builder components (Build Studio, Mentor Engine, Skills Creator, -Catalog), the shared build-session, the Content-Layer catalog, all LLM-inference -pathways, existing Goose wiring, framework/host config, and vendor-exclusion posture. -**Goal context:** wire the platform to drive **Goose** as a user-facing agent runtime, -while the deployed platform itself stays "Model-free Path A" (zero inference, one -documented exception). Findings only — **no solutions proposed.** - -> Note: the project's PreToolUse guard blocks writing the literal excluded-vendor -> API host strings (OpenAI/xAI/Meta). Where those hosts are relevant below, they are -> cited by `file:line` rather than reproduced verbatim. Permitted hosts (Anthropic, -> OpenRouter, DeepSeek, Google) are shown normally. - ---- - -## 1. Architecture map — the four components - -All four are Svelte 5 islands (or inline logic within one) mounted into Starlight -`.mdx` pages with `client:load`. Pure logic is factored into `src/lib/*.ts` and -`src/modules/cost-estimator/`. Everything client-side is deterministic. - -### Build Studio -The orchestrator; hosts Movements 1–4 and the Skills Creator inline. - -| File | Role | -|---|---| -| `src/components/BuildStudio.svelte` (~1,525 ln) | The island: intent → blueprint → skills → handoff. Reads/writes the build session. | -| `src/content/docs/build.mdx:18` | Mount: `` (`/build/`); es at `src/content/docs/es/build.mdx:18`, ar under `src/content/docs/ar/…` | -| `src/lib/studio-stack.ts` | Stack eligibility, advisory ranking, `pinnedDependencies` (#3/#4). | -| `src/lib/chemistry.ts` | Tool "chemistry" — capability role-graph pairs/conflicts/order. | -| `src/lib/mentor-engine.ts` | Movement 1 signals/questions/constraints (see below). | -| `src/lib/skill-doc.ts` | Movement 3 `slugifySkill` / `skillToMd` (SKILL.md renderer). | -| `src/lib/build-session.ts` | The shared session object (see §2). | -| `enforcement/matcher.ts` | Imported into the island to **re-verify** the assembled stack in-browser. | - -**State:** reads the shared build session on mount (`BuildStudio.svelte:232-252`), -persists every change via a `$effect` (`:283-310`). The downloadable starter -(zip / GitHub / Goose recipe) is assembled in `starterFiles()` (`:806-822`). - -### Mentor Engine -| File | Role | -|---|---| -| `src/lib/mentor-engine.ts` | **Pure, deterministic, stateless.** `detectSignals`, `pickQuestions`, `reflect` over a curated trilingual lexicon (`:33-42`) + protocol signals (`:45-50`). No model, no I/O. | -| `src/lib/mentor-engine.test.ts` | Unit tests incl. structural invariants. | - -**State:** holds none itself. BuildStudio computes `reflect(signals, answers)` -(`BuildStudio.svelte:547`) and persists the result into `session.converged` -(`:295-296`). The deterministic `reflect()` is what CLAUDE.md calls the -"structured-reflection step"; it makes **no** inference call (see §4 nuance). - -### Skills Creator -| File | Role | -|---|---| -| `src/components/BuildStudio.svelte:962-981` | **Inline** Movement 3 UI + capture/dedupe (`captureSkill`, `authoredSkills`, `removeAuthored`). | -| `src/lib/skill-doc.ts` | Pure `slugifySkill` + `skillToMd` (YAML-frontmatter SKILL.md). | -| (external) `github.com/MartinMontero/knowledge-to-skills-pipeline` | Referenced by generated README/skills folder (`BuildStudio.svelte:780`); not in this repo. | - -**State:** authored skills live in component state (`authoredSkills`) and are -persisted into `session.skills` (`BuildStudio.svelte:297`). There is **no** standalone -Skills Creator component or route — it is part of Build Studio. - -### Catalog -| File | Role | -|---|---| -| `src/content/docs/catalog/**/*.md(x)` | The entries themselves (Content-Layer docs; see §3). | -| `src/content.config.ts` | Collection definition (`docsLoader` + `docsSchema({ extend: catalogExtend })`). | -| `src/schema/catalog.ts` | The Zod frontmatter schema. | -| `src/pages/catalog.json.ts` | Build-time static JSON API at `/catalog.json` (filters to `INDEXED_TYPES`). | -| `src/components/CatalogExplorer.svelte` | Interactive island (search/facets/sort) at `/catalog/` (`catalog/index.mdx:21`). | -| `src/lib/catalog-filter.ts` | Pure filter/facet/sort logic used by the island. | -| `src/components/CatalogList.astro` | Static server-rendered lists (e.g. PIE pages). | -| `data/*.json` + `scripts/build-catalog.ts` | Build-time seeds → catalog md generator (`package.json:19`). | - -**State:** the island fetches `/catalog.json` once on mount -(`CatalogExplorer.svelte:237`) and filters entirely client-side. It does **not** use -the shared build session — its only handoff to Build Studio is a URL param -(`?seed=`, read at `BuildStudio.svelte:271-277`). - ---- - -## 2. The shared client-side build-session object - -**Defined in:** `src/lib/build-session.ts` — `interface BuildSession` (`:54-90`). - -**Persistence:** **`localStorage`** (not IndexedDB, not a framework store). -- Key: `wcb.build-session.v1` (`build-session.ts:92`). -- Change notification: same-tab `CustomEvent('wcb:session-change')` (`:93`, dispatched in `saveSession` `:161`) + cross-tab `storage` event (`subscribeSession` `:194-206`). -- Defensive load/coerce via `migrate()` (`:120-181`); silent degradation if storage is unavailable (`saveSession` `:158-160`). - -**Fields** (`build-session.ts:54-90`): -| Field | Line | Owner / meaning | +# AUDIT.md — SHIP Phase-1 findings (2026-07-11) + +Read-only research pass for the backlog-ship + design-elevation effort. +Placed at `audit/AUDIT.md` because root `AUDIT.md` is the (historical, +completed) Goose-pivot audit — promotion/renaming is a gate decision. +Companion docs: `BACKLOG.md` (consolidated ledger), `DESIGN.md` (direction), +`audit/screens/` (39 screenshots, 13 flows × 3 widths). + +## Baselines (executed this session) + +- `npm run verify:all` — **green**: astro check 0 errors/0 warnings, both + typechecks clean, **352/352 tests**, enforce + enforce:skills green, + production build 6,653 pages. +- `npx tsx enforcement/cli.ts all --tree .` — exits 1, but **every hit is + self-referential**: the engine's own signal-definition YAMLs and its + negative-test fixtures. **Zero hits in `src/`, `worker/`, `scripts/`, + `public/`.** Refinement (SHIP-GATE-R1 2c): CI **already dogfoods Layers 1–2 + full-tree** (`verify.yml:42-43`, green); only Layer 3 full-tree is + unsatisfiable by construction. Exact exempt paths, enumerated: + `enforcement/excluded-organizations.yaml`, + `enforcement/excluded-provider-signals.yaml`, `enforcement/tests/**`. + DoD-prose fix suffices — no engine change needed — gate item G3. +- `npm audit --omit=dev` — 4 low (esbuild via astro; Windows-dev-server-only, + prod unaffected). Full audit: 14 (7 low/6 moderate/1 high) — the high is + `tmp` via `@lhci/cli`, CI-tooling only. +- Enforcement full-tree + verify baselines contradict nothing in CI (10 + workflows, none disabled; the only two `continue-on-error` are + documented-deliberate: `security-pr.yml:42`, `security-cron.yml:58`). + +## P0 — security / broken / binding-doc conflicts + +| # | Finding | Evidence | |---|---|---| -| `v: 1`, `updatedAt` | 55-56 | schema version + timestamp | -| `movement: 1\|2\|3\|4` | 58 | wayfinding | -| `intent{projectName,problem,goal,success,protocols,answers}` | 60-67 | Movement 1 raw intent + Socratic answers | -| `converged` | 69 | Movement 1 deterministic reflection | -| `adjustments{swaps,removed,extra}` | 71-75 | Movement 2 blueprint edits | -| `stack: SessionStackItem[]` | 77 | Movement 2 resolved tools + receipts | -| `skills: SessionSkill[]` | 79 | Movement 3 authored skills | -| `seededTool` | 81 | Catalog "Build with this" seed | -| `handoff` | 83 | Movement 4 method (`zip\|github\|goose\|kickoff`) | -| `usage?`, `costEstimate?` | 88-89 | Hosting Cost Estimator I/O | - -**Readers/writers** (only three modules import it): -- `src/components/BuildStudio.svelte` — read + write (primary). -- `src/modules/cost-estimator/ui/CostEstimator.svelte` — read. -- `src/modules/cost-estimator/core/usage-profile.ts` — write (`usage` / `costEstimate`). - -**Components NOT wired to the build session (flagged):** -- `src/components/CatalogExplorer.svelte` — independent; hands off only via `?seed=` URL param, **not** the session object. (So a Catalog→Studio handoff carries a single tool id, nothing else.) -- `src/components/PolicyChecker.svelte` (`/check/`) — fully standalone; no session read/write. -- `src/components/AccountWidget.astro` — uses a **separate** server-side auth session (HttpOnly cookie → KV → D1), unrelated to the client build session. - ---- - -## 3. Catalog inventory (Astro Content Layer) - -**Content Layer:** confirmed. `src/content.config.ts` uses Starlight's -`docsLoader()` (a glob/file loader over `src/content/docs/`) with -`docsSchema({ extend: catalogExtend })` — i.e. the Zod schema in -`src/schema/catalog.ts` layered onto Starlight docs frontmatter. - -**Counts** (English content; `es`/`ar` catalog dirs contain only an `index.mdx` — -translations not yet generated): -- **2,182** entry files under `src/content/docs/catalog/`. -- **827** of those are datasets (`src/content/docs/catalog/datasets/`). -- **1,354** tool-class entries — authoritative count from `npm run enforce:layer1` - (`✓ 1354 catalog entries clean`). (1,354 + 827 + 1 index ≈ 2,182.) -- `/catalog.json` ships `INDEXED_TYPES` = tools + datasets (`catalog.json.ts:9-15`). - -**Schema fields** (`src/schema/catalog.ts:80-134`, all optional at top level; -conditionally required via `superRefine` `:140-207`): -- Discriminator: `entry_type` (`:82`) ∈ `ENTRY_TYPES` (`:28-40`). -- Dependency: `dependency_name, ecosystem, category, what_it_does, homepage_url, repo_url, registry_url, protocols` (`:85-92`). -- License (accountability core): `license_spdx, license_source_url, license_source_commit_sha` (`:95-97`). -- Maintenance: `maintenance_status, last_release_at, version` (`:100-105`). -- AOS adoption: `aos_repos_using, aos_repos_list` (`:108-109`). -- Nav/i18n: `pie_anchor` (`:112`), `machine_translated` (`:117`). -- Enforcement: `provider_agnostic, verification_status, verification_blocked_reason, origin_advisory, verified_at` (`:120-126`). -- **Recipe contract** (required when `entry_type==='recipe'`): `recipe_type, target_entry, target_entry_slug, excluded_providers_unreachable_when[], verification_steps[]` (`:129-133`, refined `:181-206`). - -**How datasets / extensions / skills are represented:** -- **Datasets** — first-class: `entry_type: 'dataset'` (`schema:34`), 827 files, surfaced in `/catalog.json` as `kind: 'dataset'` (`catalog.json.ts:21`). Seed: `data/datasets.json`. -- **Extensions (Goose/MCP)** — **NOT a first-class type.** `ENTRY_TYPES` (`schema:28-40`) has no `extension`. MCP servers exist only as generic catalog **tools** (e.g. `data/agentic-tools.json:1173-1230`, "MCP Ecosystem"). Relevant to the Goose pivot, where extensions are a distinct concept. -- **Skills** — **NOT a catalog type.** No `skill` in `ENTRY_TYPES`. Skills are a build-flow artifact (authored in Build Studio → `skill-doc.ts` → SKILL.md) and a build-time seed (`data/skills-seed.json`); the authoring how-to lives in the external knowledge-to-skills-pipeline. -- **Recipes** — first-class `entry_type: 'recipe'` (`schema:35`) but currently only **2** files under `src/content/docs/recipes/`. These are *provider-lockdown policy recipes* (exclusion exceptions), **distinct** from the Goose run-recipe YAML the Build Studio generates (§5). +| P0-1 | **CLAUDE.md documents an i18n freshness/security-gate layer that does not exist** — incl. a 2-review gate for `security_sensitive` pages. One page carries the flag today (`guides/encrypted-group-messaging-marmot.mdx:10`) and gets **no extra protection**; the schema doesn't even define the field. CLAUDE.md is binding session instructions, so this is the highest-risk doc/reality conflict. | No `scripts/i18n-freshness.mjs`, `scripts/i18n-security-gate.mjs`, `i18n:stamp` script, `TranslationStatus.astro`, `src/config/i18n.mjs`, `TRANSLATING.md`, `.github/CODEOWNERS`, or i18n workflows (verified absent) | +| P0-2 | **PolicyChecker island ignores its `lang` prop** — the verdict surface (the product's thesis) renders English-only inside `/es/check/` and `/ar/check/`. | `src/components/PolicyChecker.svelte` (no `$props()`); pages pass `lang="es|ar"` (`es/check.mdx:18`, `ar/check.mdx:17`); hardcoded strings `:83-127` | ---- +(P0-3 in the first revision — CLI jargon in the checker verdict — is +**reclassified to P1-15** per the prompt's severity ladder: P0 is +security/broken/policy; copy register is UX. Reclassification requested and +ruled at SHIP-GATE-R1 2b.) -## 4. LLM inference calls (Path-A check) +## P1 — correctness / UX / design debt -**Result: NOT zero.** There is exactly **one** server-side inference pathway, -reached from two client call sites. It is BYOK (the user's key), allowlist-gated, -and appears to be the documented "one permitted exception" — but see the nuance. +Kill-list hits (each with file:line; full table in the design-audit record): -**The inference call (server-side, in the Worker):** -- `worker/index.ts:199` — `const res = await fetch(p.url, { ...p.build(model, prompt, apiKey), … })` inside `kickoffHandler` (`:182-207`), routed at `/api/agent/kickoff` (`:463`). -- Provider hosts it can reach (`PROVIDERS`, `worker/index.ts:68-99`): - - `:70` Anthropic Messages API (`api.anthropic.com`) - - `:80` OpenRouter chat-completions (`openrouter.ai`) - - `:90` DeepSeek chat-completions (`api.deepseek.com`) - -**Client call sites (same-origin → the Worker brokers; the browser never calls a model directly):** -- `src/components/BuildStudio.svelte:1012` — builds the **MENTOR structured-reflection** prompt (returns JSON `proposals` + `skills`). -- `src/components/BuildStudio.svelte:904` — the **"Try a step with AI" kickoff** handoff (`handoff: 'kickoff'`). - -**Excluded-vendor host strings elsewhere are NOT calls** — they are the enforcement -engine's *negative signals* and tests (the OpenAI/xAI host literals live at -`enforcement/excluded-provider-signals.yaml:11,31`, and in -`enforcement/tests/recipe.test.ts:46`, `enforcement/tests/layer3.test.ts:25,33`). - -**Nuance / discrepancy to surface:** -- CLAUDE.md's "exactly one permitted exception: the single **deterministic** - structured-reflection step" is worded ambiguously. In code, the *deterministic* - reflection is `mentor-engine.reflect()` (`BuildStudio.svelte:547`) and makes **no** - model call. The actual *inference* exception is the `/api/agent/kickoff` broker, - which is used for **two** purposes: structured reflection (`:1012`) **and** a - free-form "try a step" kickoff (`:904`). So the single permitted endpoint backs - two product features, only one of which is "structured reflection." -- This sits in tension with the new goal ("the platform makes zero inference calls; - Goose runs inference on the user's machine"). The broker is the lone exception - today. (Flagged in Gaps & Risks; no fix proposed here.) - ---- - -## 5. Existing Goose integration - -**State: partial, recipe-file level — already wired, no deeplink/MCP.** - -- **Handoff option:** `handoff` includes `'goose'` (`BuildStudio.svelte:200`), with UI strings "Run it with Goose" / "Download the Goose recipe" / "Copy the run command" (`:43,58-59`, es `:96,111-112`, ar `:149,164-165`). -- **Recipe generation:** `gooseRecipe` derived (`BuildStudio.svelte:733-742`) emits a Goose recipe YAML — `version: "1.0.0"`, `title`, `description`, `instructions: |`, `prompt: |` (the full agent prompt). Bundled into the starter zip as `${slug}.goose-recipe.yaml` (`:814`) and individually downloadable (`:1360`). -- **Run command:** `goose run --recipe ${slug}.goose-recipe.yaml`, shown and copyable (`:1361-1362`). -- **Catalog presence:** Goose is a catalog entry — `data/agentic-tools.json:693-699`, `source_url: https://github.com/aaif-goose/goose`. -- **Docs:** Goose is referenced throughout prose (`astro.config.mjs:99,148`; README `:14,47,171,184`; guides incl. `guides/get-started-with-goose`, `TRANSLATIONS.md:44`). - -**Absent today (relevant to the pivot):** -- **No `goose://` deeplink** anywhere (searched `src/`, none). -- **No MCP / extension wiring** in the platform — MCP appears only as catalog *content* (`data/agentic-tools.json` "MCP Ecosystem"), not as integration code. -- The generated recipe is a **static template** (`instructions` + `prompt`); it does not yet encode `extensions:`, `activities:`, `parameters:`, or provider/settings blocks from the Goose recipe schema. -- The provider-lockdown **policy** recipes (`src/content/docs/recipes/`, `entry_type:'recipe'`) are unrelated to the generated **run** recipe — two different "recipe" concepts share the word. - ---- - -## 6. Framework / adapter / build config - -- **Astro:** `^6.4.6` (`package.json:44`). *(Note: the task brief said "Astro 5"; the repo is on Astro 6.)* -- **Starlight:** `@astrojs/starlight ^0.40.0` (`package.json:41`). Svelte `^5.56.3`; `@astrojs/svelte ^8.1.2`. -- **Build output mode:** `output: 'static'` (`astro.config.mjs:92`), `trailingSlash: 'ignore'` (`:93`). -- **Cloudflare adapter:** **none.** There is no `@astrojs/cloudflare` adapter and no `adapter:` in `astro.config.mjs`. Instead the site is a **static build served by a hand-rolled Worker**: `wrangler.jsonc` sets `main: worker/index.ts` with a Static-Assets binding (`assets.directory: ./dist`, `binding: ASSETS`, `not_found_handling: "404-page"`), KV (`SESSIONS`, `ATPROTO`), and D1 (`DB`). Static HTML is served by the asset layer (which bypasses the Worker); `/api/*` is handled in `worker/index.ts`. -- **Security headers:** emitted at build into `dist/_headers` via a custom integration (`astro.config.mjs:44-69`); hash-based CSP ships **Report-Only** by default (`:63`), `CSP_MODE=enforce` to enforce. -- **i18n:** root locale `en`, plus `es` and `ar` (RTL) (`astro.config.mjs:106-111`). - ---- - -## 7. AOS vendor-exclusion posture (Meta / OpenAI / xAI) - -**Posture: clean — no Meta/OpenAI/xAI used as an inference/model provider; Google permitted.** - -- **Dependencies:** no Meta/OpenAI/xAI SDK in `package.json` (`:39-66`). LLM-adjacent deps are only `@atproto/*` and `nostr-tools` (not model SDKs). No `openai`, `@anthropic-ai/sdk` (the broker calls Anthropic over raw `fetch`, not an SDK), `@ai-sdk`, or `langchain`. -- **Broker allowlist** (`worker/index.ts`): - - Direct providers offered: `anthropic`, `openrouter`, `deepseek` (`:68-99`). **OpenAI and xAI are not offered**, and the error message says so (`:189`). - - OpenRouter (a gateway to *all* vendors) is constrained by a **default-deny allowlist** `PERMITTED_ROUTER_VENDORS` (`:106-109`) and a hard block `isExcludedRouterModel` (matches the excluded-org vendor segments, `:118-121`), enforced before the call (`:195-197`). - - **Google is explicitly permitted** — `'google'` ∈ the allowlist (`:107`). -- **Catalog-level exclusion** is enforced separately by the three-layer engine (`enforcement/`, blocking CI gate `verify.yml`) and the `EXCLUDED_PROVIDER_IDS = ['meta','openai','xai','aws','oracle']` registry (`src/modules/cost-estimator/registry/providers.ts:16`), shared by the Model Compass registry. -- **Google services** (Fonts/Analytics/etc.): the only analytics is **Plausible** (opt-in, `astro.config.mjs:74-88`) — not Google. No Google Fonts/Maps/reCAPTCHA found. (Per policy Google is permitted; none is currently wired, so nothing to flag.) -- **Anthropic / DeepSeek** as offered providers are **not** policy violations — only Meta/OpenAI/xAI are excluded. - ---- - -## Gaps & Risks (ranked by severity) - -1. **[High · Path-A definition vs. code] The lone inference exception backs two features and overlaps with the Goose pivot.** - `/api/agent/kickoff` (`worker/index.ts:182-207`, fetch `:199`) is the only inference call, but it serves both the structured-reflection (`BuildStudio.svelte:1012`) and a free-form "Try a step with AI" kickoff (`:904`). CLAUDE.md authorizes *one* "deterministic structured-reflection step," yet the deterministic reflection (`reflect()`) makes no model call — so the wording and the code don't line up, and the "kickoff" is arguably a second use of the exception. As Goose takes over user-facing inference (on the user's machine), this server-side broker's necessity should be re-examined. *(Observation only.)* - -2. **[High · Goose readiness] Extensions and skills are not first-class catalog types; the generated Goose recipe is a minimal template.** - `ENTRY_TYPES` (`schema:28-40`) has no `extension` or `skill`; MCP servers are only generic `tool` entries (`data/agentic-tools.json`). The emitted recipe (`BuildStudio.svelte:733-742`) carries only `instructions`+`prompt` — no `extensions`, `parameters`, `activities`, or provider/settings blocks from the Goose recipe schema. Driving Goose richly will need a representation for extensions and a fuller recipe. - -3. **[Medium · Docs accuracy] Stated facts diverge from the code.** - - Brief/CLAUDE.md imply **Astro 5**; repo is **Astro `^6.4.6`** (`package.json:44`). - - CLAUDE.md says "~842 YAML entries"; actual is **1,354 tool entries + 827 datasets** (`enforce:layer1`; `find` counts). Entries are Markdown/MDX, not raw YAML. - - These don't break anything but undermine trust in the written invariants. - -4. **[Medium · State coupling] Three islands are not on the shared session.** - `CatalogExplorer` hands off only a single `?seed=` id (`BuildStudio.svelte:271`), and `PolicyChecker` / `AccountWidget` are fully independent. Any future flow that wants Catalog selections, a policy-check result, or the signed-in identity to travel into the Build Studio has no channel today. (`AccountWidget` is deliberately a *separate* auth session — not a defect, but worth noting it is unrelated to the build session.) - -5. **[Medium · CSP] The site-wide CSP is still Report-Only.** - `astro.config.mjs:63` ships `report-only` unless `CSP_MODE=enforce`. Until promoted, the hash-based CSP is observational. (Collection was made durable earlier in this branch.) - -6. **[Low · i18n] Catalog translations are effectively absent.** - `src/content/docs/es/catalog/` and `…/ar/catalog/` contain only an `index.mdx`; the 1,354+827 entries exist in English only. The Build Studio / Catalog UIs are translated, but entry bodies are not (a `translate-catalog` script exists, `package.json:27`). - -7. **[Low · naming] Two distinct "recipe" concepts share the word.** - Policy provider-lockdown recipes (`entry_type:'recipe'`, `src/content/docs/recipes/`) vs. the generated Goose run-recipe YAML (`${slug}.goose-recipe.yaml`). A reader can easily conflate them; worth disambiguating in any Goose-facing work. - ---- - -*End of audit. No source files were modified. Awaiting direction before proposing changes.* +| # | Finding | Evidence | +|---|---|---| +| P1-1 | Landing feature sections are stock Starlight `CardGrid`/`Card icon=…` + 3 `LinkCard` grids | `src/content/docs/index.mdx:37-99` | +| P1-2 | Footer chrome is untouched Starlight default (only un-overridden chrome left) | `astro.config.mjs:146-153` | +| P1-3 | Catalog explorer: 1,359 entries as a single-column card wall, 60/page (desktop page renders 20,562px tall); native checkboxes in a scroll box; no `accent-color` anywhere in src | `CatalogExplorer.svelte:513,505-510,298`; screenshot `catalog-explorer-desktop.png` | +| P1-4 | Checker verdict = text line + default-styled table | `PolicyChecker.svelte:101-123` | +| P1-5 | Island style dialects split: token-native vs raw `--sl-*`/hex/off-scale rems. Worst: ModelCompass hex ramp (`#d33 #e80 …`), CostEstimator all-`--sl-*`, BuildStudio mixes both in one stylesheet, lone raw box-shadow | `ModelCompass.svelte:144-152`; `CostEstimator.svelte:240-276`; `BuildStudio.svelte:1355` vs `:1360-1364`; `CatalogExplorer.svelte:536` | +| P1-6 | `TODO: confirm` is user-visible product copy (zero-fabrication semantics, placeholder register) | `model-compass/ui/i18n.ts:73`; rendered `CostEstimator.svelte:220`, `ModelCompass.svelte:48,53,62`; advertised `build/cost.mdx:13`, `build/models.mdx:13` | +| P1-7 | Dev-jargon first-contact copy on organizer surfaces (landing, /build, /check, /build/models, /start) | `index.mdx:87,89`; `build.mdx:13-16`; `check.mdx:12-14`; `models.mdx:10-12`; `start/index.mdx:11-13` | +| P1-8 | i18n strays: BuildStudio hardcoded English beside its own STR table (`:1074,1027,1036,1084,1265,1272,1314-1322`); catalog CTA hardlinks English `/build/` from es/ar (`CatalogExplorer.svelte:358` — locale-aware `buildBase` exists at `:287` unused); AccountWidget ar placeholder (`:79`); English data-layer prose rendered in localized UIs (compass `models.ts:57`, estimator `providers.ts:64-68`, `CatalogList.astro:55-62`) | as cited | +| P1-9 | All 16 island mounts `client:load`; zero `client:visible/idle`. ModelCompass ships its whole registry as eager JS with no no-JS render | grep `client:`; `build/models.mdx:16` | +| P1-10 | `/console/` styled with raw hex outside the token system (own dark-mode media query) | `AdminConsole.svelte:470-523`; `console/index.astro:33-44` | +| P1-11 | Arabic gets no display face (identity absent for RTL readers) | `tokens.css:12-13` | +| P1-12 | CSP still Report-Only (mechanism complete; operator flip pending) | `astro.config.mjs:63`; `security-headers.ts:112-180` | +| P1-13 | Cost Estimator: 12× `unitPrice: null` + 3× `lastVerified: null`; tier bands/scale placeholders. Model Compass: 7 models `score: null` (runbook undercounts as 3) | `providers.ts:62-97`; `tiers.ts:26-30`; `models.ts:37-182` | +| P1-14 | Admin panel phases 4–8 unbuilt (per spec + wrangler binding comments); staging follow-ups (publish→PR, enforcement wiring, expiry job) logged | `docs/admin-panel-spec.md:73-136`; `wrangler.jsonc:48-62` | +| P1-15 | CLI jargon inside the checker verdict aimed at non-developers: "run `npx tsx enforcement/cli.ts all --tree .` on a real project." (reclassified from P0 at SHIP-GATE-R1 2b) | `PolicyChecker.svelte:126-127` | + +## P2 — polish / docs / deferred + +- Doc drift set: ROADMAP Done stops at #35 (repo at #55); admin-spec header + says "not yet built"; PLAN.md footer says "no feature code written"; README + describes Pages-dashboard deploy vs actual Workers Builds topology + (`README.md:127-148`); `TRANSLATING.md` referenced but file is + `TRANSLATIONS.md`; catalog-count disagreement (CLAUDE.md 1,355 / TRANSLATIONS + ~2,182 / disk 1,360); runbook §B2 undercount; MOBILE_FIXES header cites a + merged branch; `operational_advisory` name→workflow mapping lives only in + guard.py. +- `/build/models/` color-contrast: 89 axe nodes (en/es) from opacity + hex + fallbacks (a11y score still ≥ budget) — folds into P1-5's tokenization. +- Catalog prose translation run (pipeline built, needs `ANTHROPIC_API_KEY` + + waves); native-speaker review of ~25 machine-translated pages. +- Dependabot remainder (5 dev-only alerts); the **Astro 7 coupled upgrade** + (astro 7.0.7 + starlight 0.41.3 + @astrojs/svelte 9 — 0.40 is the newest + Starlight compatible with Astro 6; breaking-change list verified, none of + the removed APIs used in-repo) — a decision, not an obligation. +- NIP-07 one-line note in public docs; Plausible remains the only sanctioned + (opt-in, off-by-default) third-party origin. +- Stack otherwise current: wrangler `ratelimits` GA (not beta), + `run_worker_first` current, no deprecated Astro/nostr-tools API usage found. + +## What is NOT broken (verified, for the record) + +Intentional identity system exists and is substantial (tokens/theme/components ++ 5 overrides + Build Plate + self-hosted subset fonts). No purple gradients, +no glassmorphism, no lorem, no stock illustration, no font CDNs. Path A holds +(broker retired; CI-gated). Mobile fixes 1–8 merged. `/api/health`, +`/api/license`, auth, admin auth+staging all live and tested (352 tests). diff --git a/BACKLOG.md b/BACKLOG.md index 87b5dd68..fb9ac691 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -14,9 +14,9 @@ check. Nothing found was dropped. **Scope key** — the column the gate decides: | ID | Item | Source/evidence | Status | Pri | Acceptance criteria | |---|---|---|---|---|---| -| D1 | Receipt/verdict component system (the signature): shared visual language for checker verdicts, catalog verification badges, Studio policy gate, enforcement page | DESIGN §1; audit P1-4 | absent | P1 | One shared component/token set renders clean/blocked/chain verdicts; used by ≥4 surfaces; axe clean | +| D1 | Receipt/verdict component system (the signature): shared visual language for checker verdicts, catalog verification badges, Studio policy gate, enforcement page | DESIGN §1; audit P1-4 | absent | P1 | One shared component/token set renders clean/blocked/chain verdicts; used by ≥4 surfaces; axe clean; **status never encoded by color alone (WCAG 1.4.1 — icon + word + color; reads correct in grayscale)** [SHIP-GATE-R2 rider] | | D2 | Checker verdict redesign + island i18n (fixes P0-2/P0-3) | `PolicyChecker.svelte` | broken i18n | **P0** | Island honors `lang` (house STR pattern, es/ar shipped); verdict = receipt band; CLI jargon behind a disclosure; localized empty/error states | -| D3 | Catalog explorer density redesign: responsive 1→2→3-col receipt cards, designed facets w/ counts, `accent-color`, designed zero-result/empty/error states at full 1,359-entry dataset; locale-aware CTA (`:358`) | audit P1-3, P1-8 | default-ish | P1 | Desktop page no longer a 20k-px column; facets on-token; all states designed; CTA locale-aware | +| D3 | Catalog explorer density redesign: responsive 1→2→3-col receipt cards, designed facets w/ counts, `accent-color`, designed zero-result/empty/error states at the full dataset (count = the E6 generated figure, never a literal) [SHIP-GATE-R2 rider]; locale-aware CTA (`:358`) | audit P1-3, P1-8 | default-ish | P1 | Desktop page no longer a 20k-px column; facets on-token; all states designed at the full dataset; CTA locale-aware | | D4 | Token-only island rule: retire `--sl-*`/raw-hex/off-scale rems from BuildStudio, CostEstimator, ModelCompass, CatalogExplorer; add `--shadow-1`, verdict washes, global `accent-color` | audit P1-5 | split dialects | P1 | grep: zero `--sl-color` refs in island styles; zero raw hex; `/build/models/` axe color-contrast = 0 (closes the 89-node finding) | | D5 | Landing: replace stock CardGrid/LinkCard sections with bespoke WorkOrderGrid; copy-register pass | `index.mdx:37-99`; audit P1-1, P1-7 | stock | P1 | No Starlight Card/LinkCard on the landing; copy passes the register rules (§4) | | D6 | Footer override (last stock chrome) | audit P1-2 | stock | P1 | Designed, token-native, locale-aware footer on all pages | @@ -24,7 +24,7 @@ check. Nothing found was dropped. **Scope key** — the column the gate decides: | D8 | `TODO: confirm` → designed provenance chip ("unverified — awaiting human check"), same semantics | audit P1-6 | placeholder register | P1 | No user-visible "TODO"; zero-fabrication semantics preserved; localized | | D9 | BuildStudio stray strings → STR table; AccountWidget ar placeholder; data-layer prose i18n keys (compass cautions, estimator labels) | audit P1-8 | strays | P1 | grep finds no hardcoded user-facing English in islands with i18n tables | | D10 | `/console/` joins the token layer (raw hex → tokens, `.btn`/`.field`); stays standalone-CSP, stays English (documented operator exception) | audit P1-10 | off-system | P1 | Zero raw hex in console surfaces; CSP wss: rule unchanged (invariance test stays green) | -| D11 | Arabic display face: self-hosted subset OFL face (Readex Pro candidate) wired via `:lang(ar)` | audit P1-11; DESIGN §2/§7 | absent | P1 | ≤45 KB woff2, OFL license committed, `/ar/` headings render the face; perf budgets hold | +| D11 | Arabic display face: self-hosted subset OFL face (Readex Pro candidate) wired via `:lang(ar)` | audit P1-11; DESIGN §2/§7 | absent | P1 | Final face pick + measured subset size recorded as a PLAN.md checkpoint BEFORE implementation [SHIP-GATE-R2 rider]; ≤45 KB woff2, OFL license committed, `/ar/` headings render the face; perf budgets hold | | D12 | Hydration pass: `client:visible` for below-fold islands; ModelCompass considered for static render + filter-only island | audit P1-9 | all-eager | P1 | No LCP regression on /catalog/; JS transferred on /build/models/ drops; e2e stays green | | D13 | Page-weight ceilings recorded + measured (reading ≤250 KB, tool ≤450 KB, landing ≤350 KB) | DESIGN §8 | new | P2 | Measured per page type in VERIFICATION.md; over-ceiling pages fixed | diff --git a/DESIGN.md b/DESIGN.md index f726898e..76f8894b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -23,8 +23,10 @@ surfaces are the product's thesis, so that is exactly backwards. (clean / blocked / chain-to-excluded), the Studio's policy gate, the enforcement page's verdict band, admin audit views. A receipt is: a stamped edge (2px structural rule), a status color that is never decorative -(`--ok/--warn/--danger`), the evidence inline (commit SHA, org chain, license -id), and a plain-verb sentence a non-developer can act on. Boldness is spent +(`--ok/--warn/--danger`) **and never the only carrier — icon + word + color, +correct in grayscale (WCAG 1.4.1; SHIP-GATE-R2 rider)** — the evidence inline +(commit SHA, org chain, license id), and a plain-verb sentence a non-developer +can act on. Boldness is spent here — the blocked-dependency moment should feel like a customs stamp coming down — and everywhere else stays quiet: paper, ink, structure. diff --git a/PLAN.md b/PLAN.md index 4f1d4940..3b0eb2a5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,453 +1,148 @@ -# PLAN.md — drive Goose as a user-facing agentic runtime via ONE build session - -**Source:** `AUDIT.md` (read-only audit, branch `claude/kind-albattani-tnycq4`). -**Mode:** PLAN ONLY — no feature code. Contracts below are *specifications* (types + -signatures, no bodies). Execution happens slice-by-slice after review. -**Deliverable of the pivot:** the four components (Build Studio, Mentor Engine, Skills -Creator, Catalog) converge on ONE typed client-side build session, which serializes to -a Goose recipe and launches via a `goose://recipe?config=` deeplink — all pure -client-side, model-free. - ---- - -## 0. Encoded constraints, reconciliations & out-of-scope - -**Hard constraints (carried into every slice):** -1. **Stack:** Astro (Starlight) + Cloudflare Pages; Catalog on Astro Content Layer - (`docsLoader` + Zod), per `src/content.config.ts` + `src/schema/catalog.ts`. -2. **ONE shared client-side build session** wired across the four components - (`src/lib/build-session.ts`). -3. **Model-free Path A:** ZERO inference API calls from the platform. Exactly ONE - deterministic structured-reflection exception that performs **no LLM call** — it - *reads* structured JSON the user brings back from their own Goose run - (`response.json_schema` output). -4. **Goose = STABLE primitive only:** client-side recipe deeplinks - `goose://recipe?config=` via `btoa(JSON.stringify(recipe))` behind a - UTF-8-safe wrapper (reuse the pattern in `src/components/AccountWidget.astro:86-95`). -5. **Mentor Engine** = Yoda/Morpheus/J.A.R.V.I.S./Neo persona shipped as a Goose - **SKILL** + deterministic reflection over the recipe's `response.json_schema` - output (`src/lib/mentor-engine.ts`). -6. **Vendor exclusions = Meta/OpenAI/xAI ONLY** as inference providers; Google - permitted. Goose is model-agnostic — **never hardcode a provider**; omit - `settings.goose_provider`/model so the user's own Goose config selects it. -7. **MCP safety:** the **Catalog is the trust boundary**. Only allowlisted, vetted - extensions/skills may appear in a generated recipe. Lean on Goose "Trust & - Execute" consent + extension allowlist + least privilege. Never surface raw MCP - server config to non-devs. Treat tool descriptions as untrusted unless sourced - from a verified Catalog entry. - -**Reconciliations with `AUDIT.md` (facts the brief mis-stated — plan honors reality):** -- Framework is **Astro `^6.4.6`**, not 5 (`AUDIT.md §6`, `package.json:44`). Content - Layer API is identical for this work; the plan is version-agnostic and pins nothing - to a major version. -- Catalog is **1,354 tool-class + 827 dataset** entries (`AUDIT.md §3`), not "~842". - Entries are MD/MDX with YAML frontmatter. Counts only matter to Slice B's allowlist - scope. -- There is **no `@astrojs/cloudflare` adapter**; static build + hand-rolled Worker - (`AUDIT.md §6`, `wrangler.jsonc`). All new logic is **pure client-side**, so the - serializer/deeplink never touch the Worker. - -**Out of scope (note as FUTURE ENHANCEMENT only — do not build now):** -- `goose serve` / ACP-over-HTTP / Goose TS SDK (experimental mid-2026). Recorded in - §6 "Future" so a later slice can pick them up; no design dependency on them. -- Retiring the existing `/api/agent/kickoff` broker is IN scope (Slice D) because it is - the lone inference call standing between us and Path-A zero. - ---- - -## 1. Cross-cutting invariants & global gates (apply to ALL slices) - -These are the exit gates every slice is measured against, in addition to its own: - -- **G-PathA (zero inference):** no platform code fetches a model host. Enforced by a - new additive test/CI check (Slice D) — extends the negative-signal approach already - in `enforcement/excluded-provider-signals.yaml` and `enforcement/tests/layer3.test.ts`. -- **G-Vendor:** no Meta/OpenAI/xAI as a provider; recipe emits no provider/model. - Reuse `EXCLUDED_PROVIDER_IDS` (`src/modules/cost-estimator/registry/providers.ts:16`) - + the broker allowlist logic pattern (`worker/index.ts:106-130`) as the vetting source. -- **G-Trust (MCP):** every extension in a generated recipe ∈ the Catalog allowlist; - property-tested so a non-allowlisted extension is unrepresentable. -- **G-Gate:** the blocking `operational_advisory` CI gate is never weakened; all new CI - is ADDITIVE in new workflow files (CLAUDE.md #4; `AUDIT.md §6`). The project guard - (`.claude/hooks/guard.py`) stays in force. -- **G-Determinism:** all new serializer/deeplink/reflection code is pure and - deterministic — same session ⇒ byte-identical recipe & deeplink. -- **G-Green:** the existing **185-test** suite stays green (`npm test`), `astro check` - 0 errors, and a production `npm run build` succeeds, after every slice. -- **G-NonDev:** no raw MCP/server config, env vars, or shell strings shown to the - builder; all attacker-influenceable text rendered via `textContent` (reuse the - XSS-safe pattern already used in `src/components/AccountWidget.astro`). - ---- - -## 2. Sequencing / gating - -``` -A (typed session + Catalog wiring) -└─> B (recipe serializer + Catalog extension allowlist) - ├─> C (deeplink + "Open in Goose" UX) - ├─> D (Mentor persona SKILL + structured-reflection; retire broker → Path-A zero) - └─> E (Skills Creator → session + Catalog sub-recipes) -``` -A is the foundation. B depends on A. C, D, E each depend on B; C/D/E are independent of -one another and may land in any order once B is in. Each slice is its own PR, gated on -its benchmark + the global gates in §1. - ---- - -## SLICE A — Formalize the shared build session; wire all four components - -**Goal.** Turn `BuildSession` into a single typed, runtime-validated, versioned schema -and ensure all four components read/write the *same* object — closing `AUDIT.md` Gap #4 -for the Catalog. (PolicyChecker/AccountWidget are intentionally NOT among "the four" -and stay independent — `AUDIT.md §2`.) - -**Gate (entry):** none (foundation). - -**Files to change.** -- `src/lib/build-session.ts` — formalize the schema (see contract); bump `v: 1 → 2` - with a `migrate` v1→v2 path (extend the existing `migrate()` at `:120-181`, same - defensive style); add session fields the recipe needs: `extensions`, `mentorReflection`. -- `src/lib/build-session.test.ts` — extend (round-trip, v1→v2 migrate, coercion). -- `src/components/CatalogExplorer.svelte` — wire to the session: write selected - tools/extensions; on mount read prior selections (reuse the subscribe pattern from - `src/components/BuildStudio.svelte:232-252` and `src/modules/cost-estimator/ui/CostEstimator.svelte:33-35`). -- `src/components/BuildStudio.svelte` — consume the new typed fields where it already - reads/writes the session (`:232-310`); no behavior change beyond new fields. -- (Mentor Engine + Skills Creator already persist through BuildStudio — formalize their - field ownership in the schema doc, no new component file.) - -**Contract / interface.** -```ts -// src/lib/build-session.ts (Zod, mirroring the astro/zod pattern in src/schema/catalog.ts) -export const SESSION_VERSION = 2 as const; - -export interface SessionExtension { // an allowlisted Goose extension ref (Slice B/G-Trust) - catalogId: string; // slug of the verified Catalog entry it came from - name: string; - kind: 'builtin' | 'mcp_stdio' | 'mcp_sse'; // resolved from the Catalog, never user-typed -} -export interface SessionMentorReflection { // deterministic output of Slice D - schemaVersion: 1; - constraints: string[]; // ConstraintId[]; from mentor-engine - proposals: Array<{ action: 'add'|'swap'|'remove'; name: string; why: string }>; -} -export interface BuildSession { // EXTENDS the current interface at :54-90 - /* …all existing fields… */ - v: 2; - extensions: SessionExtension[]; // NEW — selected, allowlisted extensions - mentorReflection: SessionMentorReflection | null; // NEW — Slice D writes; null until run -} -export function migrate(parsed: unknown): BuildSession; // v1 docs upgrade to v2 (extensions:[], mentorReflection:null) -``` -Component contract (unchanged API surface): all four use only -`loadSession/saveSession/updateSession/subscribeSession` (`build-session.ts:143-206`). - -**Test plan.** -- *Unit:* extend `build-session.test.ts` — v1→v2 migrate fills `extensions: []` / - `mentorReflection: null`; Zod rejects malformed extensions; round-trip + cross-tab - event still fire (reuse the fake-DOM harness already in that file). -- *Integration:* a jsdom-free node test that simulates Catalog write → BuildStudio read - through `localStorage` + `subscribeSession` (extend the existing pub/sub tests). - -**Rollback.** Single revert of the slice commit; `migrate` is forward-only and -v1-compatible, so reverting to v1 readers ignores the new fields (they were additive & -optional in storage). No data migration to undo. - -**Benchmark / threshold (proves it works).** -- All four components demonstrably read+write the one object in an integration test - (Catalog selection observable in a BuildStudio-side read): **pass = 1 object, 4 - participants**. -- v1→v2 migrate property test: **100%** of randomly-mutated v1 blobs coerce to a valid - v2 (no throw, schema-valid). -- `npm test` stays green at **≥185** tests; `astro check` 0 errors. - ---- - -## SLICE B — Pure-client recipe serializer (session → Goose recipe) - -**Goal.** Deterministically turn a `BuildSession` into a Goose recipe object -(`extensions` allowlisted from the Catalog, `parameters`, `activities`, -`response.json_schema`, `instructions`, `prompt`) — superseding the minimal template at -`src/components/BuildStudio.svelte:733-742`. Model-agnostic (no provider/model). - -**Gate (entry):** Slice A merged (needs `session.extensions` + typed schema). - -**Files to change.** -- **New** `src/lib/goose-recipe.ts` — the pure serializer (extraction pattern of - `src/lib/catalog-filter.ts` / `src/lib/skill-doc.ts`). -- **New** `src/lib/goose-recipe.test.ts`. -- `src/schema/catalog.ts` — add a first-class **extension** representation (closes - `AUDIT.md` Gap #2): either `entry_type: 'extension'` in `ENTRY_TYPES` (`:28-40`) or a - `goose_extension` block, gated by `verification_status === 'verified'` in the existing - `superRefine` (`:140-207`). -- **New** `src/pages/extensions.json.ts` — build-time static allowlist of vetted - extensions for the client serializer (same Content-Layer query pattern as - `src/pages/catalog.json.ts`). -- `src/components/BuildStudio.svelte` — replace the inline `gooseRecipe` derived - (`:733-742`) with a call to `buildGooseRecipe(session, allowlist)`. -- `enforcement/` — extend the layer-3 recipe contract (`enforcement/cli.ts` + - `enforcement/types.ts:92` `RecipeFinding`) to assert generated recipes reference only - allowlisted extensions; add fixtures under `enforcement/tests/`. - -**Contract / interface.** -```ts -// src/lib/goose-recipe.ts -export interface GooseRecipe { // the stable recipe primitive we emit - version: string; // "1.0.0" - title: string; - description: string; - instructions: string; - prompt: string; - extensions: GooseExtensionRef[]; // ONLY from the allowlist - parameters: GooseParameter[]; // typed inputs the user fills in Goose - activities: string[]; // suggested next actions - response: { json_schema: JsonSchema }; // forces structured output (Slice D reads it) - // NOTE: no `settings.goose_provider` / model — model-agnostic by construction (G-Vendor) -} -export interface ExtensionAllowlist { // fetched from /extensions.json - byId: Record; -} -export function buildGooseRecipe( - session: BuildSession, - allow: ExtensionAllowlist, -): GooseRecipe; // pure; drops any extension not in `allow` -export const RESPONSE_JSON_SCHEMA: JsonSchema; // the shared schema Slice D reflects over -``` - -**Test plan.** -- *Unit:* `goose-recipe.test.ts` — determinism (same session ⇒ deep-equal recipe); - **G-Trust property test**: for arbitrary `session.extensions`, every emitted - extension ∈ `allow` and any non-allowlisted ref is dropped (cannot appear); - no provider/model field ever present (G-Vendor); recipe validates against the Goose - recipe JSON-schema (vendored as a test fixture). -- *Integration:* `enforcement` layer-3 run over a generated recipe fixture passes; - a recipe built from a representative session validates end-to-end. - -**Rollback.** Revert the slice; BuildStudio's `gooseRecipe` derived returns to the -`:733-742` template (kept in git history). Schema addition is additive/optional, so -existing catalog entries still validate. - -**Benchmark / threshold.** -- **0** non-allowlisted extensions emitted across a 1,000-case property test. -- **0** recipes carrying a provider/model field. -- Recipe schema-validation: **100%** of generated recipes valid against the vendored - Goose recipe schema. -- `enforcement` layer-3 passes on generated recipes; `npm test` green. - ---- - -## SLICE C — Deeplink generator + "Open in Goose" UX - -**Goal.** Encode a recipe into `goose://recipe?config=` (UTF-8-safe), launch via -an "Open in Goose" action with an **explain-before-launch** panel and a **copy-link** -(and recipe-file) fallback when the deeplink exceeds the length budget. - -**Gate (entry):** Slice B merged (needs `GooseRecipe`). - -**Files to change.** -- **New** `src/lib/goose-deeplink.ts` — pure encode/decode + length guard. The - UTF-8-safe base64 reuses the wrapper pattern in - `src/components/AccountWidget.astro:86-95` (encode) and its inverse for round-trip. -- **New** `src/lib/goose-deeplink.test.ts`. -- `src/components/BuildStudio.svelte` — the `handoff: 'goose'` branch (`:1357-1363`): - add "Open in Goose" (anchor to the deeplink), the explain panel, and the existing - copy/download fallbacks (`copy()` `:825`, `blobDownload()` `:828`). Keep the - `goose run --recipe …` file path as the >budget fallback. -- (Optional) `src/components/CatalogExplorer.svelte` — a single-tool "Open in Goose" - using a minimal one-extension recipe (only if Slice A wired selection; else defer). - -**Contract / interface.** -```ts -// src/lib/goose-deeplink.ts -export const DEEPLINK_MAX_BYTES = 8192; // budget; above this → fallback (tune in benchmark) -export function encodeRecipeConfig(recipe: GooseRecipe): string; // UTF-8-safe base64 -export function decodeRecipeConfig(b64: string): GooseRecipe; // inverse (for tests) -export function recipeDeeplink(recipe: GooseRecipe): { - url: string; // `goose://recipe?config=…` - bytes: number; - withinBudget: boolean; // false ⇒ UX shows copy-link/file fallback -}; -export interface ExplainModel { // what the explain-before-launch panel renders - title: string; - extensions: Array<{ name: string; why: string }>; // rendered via textContent (G-NonDev) - willDoNothingUntilUserConsents: true; // copy: Goose "Trust & Execute" prompts at runtime -} -export function explainRecipe(recipe: GooseRecipe): ExplainModel; -``` - -**Test plan.** -- *Unit:* `goose-deeplink.test.ts` — round-trip `decode(encode(r)) === r` incl. - **non-ASCII** (es/ar/emoji) titles/prompts (proves the UTF-8-safe wrapper); URL shape - `goose://recipe?config=`; `withinBudget` flips correctly at the boundary; - `explainRecipe` lists every extension (no omissions). -- *Integration:* build a recipe (Slice B) → deeplink → decode → recipe equals input; a - jsdom/Playwright check that the explain panel renders every extension via `textContent` - (no HTML injection) and the fallback appears when over budget. - -**Rollback.** Revert; the `goose` handoff falls back to the existing recipe-file + -`goose run` command (`:1360-1362`), which is unaffected. - -**Benchmark / threshold.** -- Round-trip fidelity: **100%** over a fuzz corpus including multibyte text. -- A representative full-stack recipe encodes to **< `DEEPLINK_MAX_BYTES`**; over-budget - recipes deterministically take the fallback path (asserted). -- Explain panel enumerates **100%** of recipe extensions; **0** raw MCP config strings - in the DOM (asserted). - ---- - -## SLICE D — Mentor persona SKILL + deterministic structured-reflection; retire the broker - -**Goal.** Ship the Yoda/Morpheus/J.A.R.V.I.S./Neo guidance as a Goose **persona SKILL** -inside the recipe, and make the platform's ONLY model-output consumption a -**deterministic** read of the recipe's `response.json_schema` JSON the user brings back -— then **retire `/api/agent/kickoff`** so the platform makes zero inference calls -(Path-A zero; `AUDIT.md §4`, Risk #1). - -**Gate (entry):** Slice B merged (needs `RESPONSE_JSON_SCHEMA` + recipe). - -**Files to change.** -- **New** `src/lib/mentor-persona.ts` — builds the persona SKILL.md via the existing - `skillToMd` (`src/lib/skill-doc.ts`); included by the serializer (Slice B) as a recipe - skill/instruction. Pure, deterministic, no model. -- `src/lib/mentor-engine.ts` — add `reflectFromResponse(json)` that deterministically - maps a `RESPONSE_JSON_SCHEMA`-conformant object → `SessionMentorReflection` (reuses - the existing `reflect()` constraint ordering `:155-186`). **No fetch, no LLM.** -- `src/lib/mentor-engine.test.ts` — extend. -- `src/components/BuildStudio.svelte` — replace the two broker calls - (`:904`, `:1012`) with: (a) generate recipe+deeplink (Slices B/C), (b) an - "import structured result" affordance that pastes/loads the Goose `response` JSON and - runs `reflectFromResponse` into `session.mentorReflection`. -- `worker/index.ts` — **remove** `kickoffHandler` (`:182-207`), its route (`:463`), the - `PROVIDERS` table + router-allowlist (`:68-130`). (Keep `coerceUsageProfile`/pricing.) -- `worker/tests/worker.test.ts` — drop the now-removed broker tests; keep the rest. -- **New** `.github/workflows/path-a.yml` — ADDITIVE CI asserting zero model-host fetches - in `src/` + `worker/` (extends the signal set in - `enforcement/excluded-provider-signals.yaml`). Does NOT touch `verify.yml` (G-Gate). - -**Contract / interface.** -```ts -// src/lib/mentor-engine.ts (additive) -export function reflectFromResponse( - response: unknown, // a RESPONSE_JSON_SCHEMA-shaped object (validated) -): SessionMentorReflection; // deterministic; null-safe; never calls a model - -// src/lib/mentor-persona.ts -export function mentorPersonaSkill(lang: 'en'|'es'|'ar'): DraftSkill; // → skillToMd → recipe -``` - -**Test plan.** -- *Unit:* `reflectFromResponse` is deterministic and total (malformed input → safe empty - reflection, mirroring `coerceUsageProfile`'s defensive style); persona SKILL.md - validates (parse frontmatter with `yaml`, as in `src/lib/skill-doc.test.ts`). -- *Integration:* recipe (Slice B) → its `response.json_schema` → a conformant sample → - `reflectFromResponse` → stable `SessionMentorReflection` written to the session. -- *Gate test:* `path-a.yml` check fails if any model host appears in platform code. - -**Rollback.** The broker removal is its own commit; revert restores `/api/agent/kickoff` -and the two client calls. The persona/reflection additions are independent and need not -be reverted with it. - -**Benchmark / threshold.** -- **G-PathA = 0**: zero model-host fetches in `src/` + `worker/` (new CI check passes). -- `reflectFromResponse`: deterministic across a 500-case corpus; **0** throws on - malformed input. -- No reachable `/api/agent/kickoff` route remains (worker route test asserts 404). - ---- - -## SLICE E — Skills Creator authoring → session + Catalog sub-recipes - -**Goal.** Make the inline Skills Creator (Movement 3, `src/components/BuildStudio.svelte:962-981`) -write authored skills into the session AND emit them as Goose **sub-recipes** the main -recipe references, with an optional contribution path into the Catalog as vetted, -reusable artifacts. - -**Gate (entry):** Slice B merged (sub-recipes are emitted by the serializer). - -**Files to change.** -- `src/lib/skill-doc.ts` — add a sub-recipe projection of a `DraftSkill` (a skill → - a minimal `GooseRecipe` fragment / sub-recipe ref); pure. -- `src/lib/skill-doc.test.ts` — extend. -- `src/lib/goose-recipe.ts` (Slice B) — include `session.skills` as recipe - sub-recipes/activities. -- `src/components/BuildStudio.svelte` — Skills Creator writes to `session.skills` - (already does `:297`) and now also surfaces "include as sub-recipe." -- `src/schema/catalog.ts` — optional **skill** representation (parallel to the extension - work in Slice B) so a contributed skill can become a verified Catalog entry; gated by - `verification_status`. -- `public/admin/config.yml` — add a skills collection for the contribution path (mirrors - the existing `recipes` collection at `:41-52`), so non-devs contribute via Sveltia CMS - → PR → enforcement. - -**Contract / interface.** -```ts -// src/lib/skill-doc.ts (additive) -export function skillToSubRecipe(s: DraftSkill): GooseSubRecipe; // pure; no model -export interface GooseSubRecipe { name: string; instructions: string; activities: string[] } -``` - -**Test plan.** -- *Unit:* `skillToSubRecipe` deterministic; SKILL.md still valid YAML (existing - `skill-doc.test.ts` harness); a skill with hostile text stays safe (reuse the - `yamlDoubleQuoted` coverage already there). -- *Integration:* author skill → `session.skills` → serializer emits it as a sub-recipe → - recipe still schema-valid (Slice B benchmark) and deeplink round-trips (Slice C). -- *Contribution:* a CMS-authored skill file passes `enforcement` layer-3. - -**Rollback.** Revert; `session.skills` continues to flow into the starter zip as today -(`src/components/BuildStudio.svelte:806-822`); no sub-recipe emission. - -**Benchmark / threshold.** -- Authored skill round-trips session ⇄ SKILL.md ⇄ sub-recipe with **0** data loss. -- Recipes including N authored sub-recipes stay schema-valid and **within the deeplink - budget** (else fallback) for N up to a documented cap. -- `enforcement` layer-3 green on contributed skills. - ---- - -## 3. Risks carried forward (from `AUDIT.md`) + new - -| # | Risk | Source | Handled in | -|---|---|---|---| -| R1 | Lone inference exception overlaps the pivot | AUDIT Risk #1 | **Slice D** (retire broker → Path-A zero) | -| R2 | Extensions/skills not first-class; recipe too thin | AUDIT Risk #2 | **Slice B/E** (schema + serializer) | -| R3 | Deeplink length limits truncate large recipes | new | **Slice C** budget + fallback | -| R4 | Untrusted MCP tool descriptions / raw config leak | constraint #7 | **G-Trust + G-NonDev** (allowlist, textContent) | -| R5 | Hardcoding a provider breaks model-agnosticism | constraint #6 | **Slice B** (no settings/provider; G-Vendor) | -| R6 | Catalog translations absent (es/ar recipes) | AUDIT Risk #6 | recipe i18n via `lang` param (Slice C/D), tracked | -| R7 | CSP still Report-Only; `goose://` scheme + new UX | AUDIT Risk #5 | verify CSP allows the scheme before enforce (note for CSP-promote work) | -| R8 | "recipe" overload (policy recipe vs run recipe) | AUDIT Risk #7 | naming: `GooseRecipe`/sub-recipe vs `entry_type:'recipe'` policy recipes | - ---- - -## 4. Definition of Done (whole pivot) - -- One typed build session; the four components read/write it (Slice A). -- Pure client serializer → schema-valid, model-agnostic, allowlist-only Goose recipe - (Slice B). -- `goose://recipe?config=` deeplink with explain-before-launch + fallback - (Slice C). -- Mentor persona SKILL + deterministic structured-reflection; **`/api/agent/kickoff` - retired ⇒ platform inference calls = 0** (Slice D). -- Skills Creator → session + sub-recipes + Catalog contribution path (Slice E). -- All global gates (§1) hold: Path-A=0, vendor-clean, MCP-trust-boundary, additive CI, - determinism, suite green, build green. - ---- - -## 5. Future enhancements (explicitly deferred — do NOT build now) - -- `goose serve` / ACP-over-HTTP / Goose TS SDK for a hosted, live agent loop - (experimental mid-2026). Would replace the bring-back-JSON step in Slice D with a live - session — design Slice D's `reflectFromResponse` so it can later accept a streamed - response without changing its deterministic contract. -- Server-side recipe signing / shareable recipe links (needs the Worker; out of the - pure-client scope here). -- Full catalog translation so es/ar recipes ship localized extension copy. - ---- - -*Plan only — no feature code written. Awaiting review of `PLAN.md` before executing -Slice A.* +# PLAN.md — ship the approved backlog + design elevation (Phase 3) + +Authority: SHIP-GATE-R1/R2 rulings — G1 approved as marked (+riders a/b), +G2 = Option 2 (+riders), G3 confirmed (paths named), G4 executed (see +`docs/archive/`), G5 confirmed (Astro 7 out), G6 approved (+riders a/b). +Scope = BACKLOG.md SHIP column only: **D1–D13, B1-Option-2, B10, E1–E10** +(E10 already executed). OWNER and DEFER items are out of scope. No further +gates except Rule 9 (destructive/irreversible actions). + +Discipline per milestone: vertical slice → its own PR → `npm run verify:all` ++ `npm run enforce` green before close → owner merges (never pushed to main). +One logical change per commit. Every user-facing string lands en/es/ar +together (except the documented console exception). + +## Milestones + +### M0 — Governance truth (B1-Option-2, E5) — first, it is the P0 +- `src/schema/catalog.ts`: add `security_sensitive: z.boolean().optional()` + to the docs schema extension so the existing flag on the Marmot guide is + real, validated data (English source only; translations inherit by path). +- CLAUDE.md: rewrite the i18n-governance section to the truth — what exists + (locales, translate-catalog pipeline, fallback), what is planned (freshness + stamps, banner, roles, TRANSLATING.md), and the minimal gate below. E5: the + `TRANSLATING.md` reference is removed/marked planned; no dangling reference + survives. E9 rides here: constraint 4 names the protected workflow set + {verify, security-pr, quality}. +- **The minimal gate (mechanism, per G2 rider):** new ADDITIVE workflow + `.github/workflows/i18n-security-gate.yml`, triggers `pull_request` + + `pull_request_review`. Steps: diff changed files vs base; select files + whose frontmatter has `security_sensitive: true` OR that are locale + variants (`es/…`, `ar/…`) of a flagged English source (path-mapped); if + any selected, count DISTINCT approving reviewers via the REST reviews API; + **fail the check while approvals < 2**. It never modifies or weakens + `operational_advisory` workflows (additive file, constraint 4). +- **B4 dependency, stated plainly:** as a workflow this gate goes red but + GitHub only *blocks* merge once the check is marked REQUIRED in branch + protection (OWNER item B4). Until B4 flips, the gate is a loud advisory — + the P0 is fully closed only by this milestone + B4 together. PLAN records + this; the owner flips B4. +- Acceptance: schema validates the flagged page; a test PR touching it shows + the gate red at 0–1 approvals, green at 2; CLAUDE.md contains no claim a + fresh clone can falsify. +- Non-goals: freshness stamps, banner, CODEOWNERS, TRANSLATING.md content — + the full layer is its own later effort. + +### M1 — System layer (parts of D1, D4, D6) +- tokens.css: `--verdict-ok-bg/--verdict-warn-bg/--verdict-danger-bg` (both + themes), `--shadow-1`; global `accent-color: var(--structure)`. +- theme.css: explicit `@layer` ordering vs Starlight's `starlight` layer. +- `.workbench` page class (frontmatter-driven) for the five tool pages. +- **Receipt primitive**: shared component (Astro for static surfaces + a + Svelte twin or class-contract for islands) rendering status as + icon + word + color (grayscale-correct, WCAG 1.4.1 — G6 rider), stamped + edge, inline evidence slot, plain-verb line. Variants: ok / warn / danger / + neutral-pending. +- Footer override (D6): designed, token-native, locale-aware; registered in + astro.config components. +- Acceptance: tokens exist + consumed by the primitive; footer on all pages + incl. es/ar/RTL; axe clean; visual check at 3 widths; no `--sl-*` in new code. + +### M2 — Receipts on the trust surfaces (D1 complete, D2; closes P0-2, P1-15) +- PolicyChecker: house i18n pattern (`lang` prop + STR en/es/ar); verdict + band via the receipt primitive — clean / blocked / chain-to-excluded with + the ownership chain rendered as a chain; CLI detail behind a "for + developers" disclosure; designed empty/error states. +- Catalog entry badges + `/policies/enforcement/` verdict band + Studio + policy gate adopt the same primitive. +- Acceptance: D1's ≥4 surfaces met; `/es/check/` + `/ar/check/` fully + localized (grep: no hardcoded English in the island); axe clean; e2e green. + +### M3 — Catalog explorer density (D3) +- Responsive 1→2→3-col receipt-card grid; designed facet groups with counts; + designed zero-result/empty/error states at the full dataset (count = E6 + generated figure); locale-aware CTA (`buildBase` used at `:358`). +- Acceptance: desktop `/catalog/` no longer a single-column 20k-px page; + states verified at full dataset; CTA locale-verified on es/ar; LCP budget + holds (explorer is the LCP element). + +### M4 — Island tokenization + honesty chips (D4, D8, D9) +- Retire `--sl-*`/raw hex/off-scale rems from BuildStudio, CostEstimator, + ModelCompass, CatalogExplorer (the `#d33…` ramp, `0 6px 24px` shadow, etc.). +- D8: `TODO: confirm` → provenance chip ("unverified — awaiting human check", + localized; semantics unchanged). +- D9: BuildStudio stray strings → STR; AccountWidget ar placeholder; compass + cautions + estimator labels via i18n keys. +- Acceptance: grep zero `--sl-color` and zero hex literals in island styles; + `/build/models/` axe color-contrast = 0 (closes the 89-node finding); grep + finds no user-visible "TODO"; 352+ tests stay green. + +### M5 — Landing + register (D5, D7, B10) +- WorkOrderGrid replaces stock CardGrid/LinkCard sections; copy-register pass + on landing, /build, /check, /build/models, /start intros (en/es/ar in the + same commit); B10: NIP-07 one-liner (nos2x/Alby) near sign-in docs. +- Acceptance: no Starlight Card/LinkCard on the landing; register rules + (DESIGN §4) hold; screenshots at 3 widths vs DESIGN intent. + +### M6 — Console tokens, Arabic face, hydration, ceilings (D10–D13) +- **CHECKPOINT (G6 rider b, recorded here before implementation):** Arabic + display face — final pick: _____ ; measured subset woff2 size: _____ KB + (budget ≤ 45 KB); OFL text committed. Filled in by the M6 PR itself; the + face does not land until this line is filled. +- D10: console token-compliance only (raw hex → tokens; `.btn`/`.field`); + CSP wss: invariance test stays green; PANEL-RESPEC-v2 room untouched. +- D12: `client:visible` for below-fold islands; ModelCompass static-render + evaluated; no `/catalog/` LCP regression (measured). +- D13: page-weight ceilings measured + recorded in VERIFICATION.md. +- Acceptance: zero raw hex in console; `/ar/` headings render the face; + before/after JS-transfer numbers recorded. + +### M7 — Doc-drift closeout (E1–E8; E9 landed in M0, E10 done) +One commit each: ROADMAP through #56+; admin-spec phase ledger; E4 README +deploy section → Workers Builds topology (+ CLAUDE.md "Cloudflare Pages" +phrasing); E6 one generated catalog count everywhere; E7 runbook §B2 = 7 +models + maple-ai; E8 mobile-doc headers. +- Acceptance: README claims match built reality line by line (DoD); no doc + names a file or number a fresh clone can falsify. + +### M8 — Verification loop + ship report +Per the ship prompt Phase 5: two consecutive full passes — clean-state +`verify:all`, every flow walked at full dataset, 3-width screenshots vs +DESIGN intent, axe + Lighthouse vs budgets, reduced-motion on/off — +evidence in VERIFICATION.md; failures fixed and re-run. Then SHIP.md: +ledger (item → status → evidence), before/after screens, known limitations, +residual risks, deploy steps. CHANGELOG records MVP → v1 deltas. + +## DoD reconciliation (SHIP-GATE-R1 2d — the prompt's DoD, restated against reality) + +| Prompt DoD line | Reconciled criterion | +|---|---| +| Fresh clone `npm install && npm run verify:all` passes | Unchanged — as written | +| Backlog ledger 100% shipped-or-BLOCKERS | Scope = BACKLOG SHIP column; OWNER/DEFER items listed in SHIP.md as owner-court, not blockers | +| Design implemented, zero banned defaults, all values via tokens | Unchanged; deviations documented in SHIP.md | +| "All three enforcement layers pass on the repo's own tree (`cli.ts all --tree .`)" | **G3 wording:** Layers 1–2 pass `--tree .` (already CI-enforced, `verify.yml:42-43`); Layer 3 passes on every path except its own definitions/fixtures: `enforcement/excluded-organizations.yaml`, `enforcement/excluded-provider-signals.yaml`, `enforcement/tests/**` | +| Catalog integrity (count, hand-authored untouched, catalog.json consumed) | Unchanged; count asserted via the E6 generated figure | +| Quality gates ≥ budgets | Unchanged + DESIGN §8 page-weight ceilings | +| Privacy: zero third-party requests | Unchanged; Plausible documented as the sole opt-in, off-by-default exception | +| Security: osv-scanner clean/accepted; secrets scan; input validation; CSP+headers; no-secrets build | Unchanged; CSP ENFORCE flip itself = OWNER item B2 (mechanism already shipped) | +| i18n parity or gaps logged | Parity for all SHIP-scope UI; catalog-entry corpus gap = OWNER item B8, logged not shipped | +| Worker `/api/license`+`/api/health` verified; error/rate-limit documented | Unchanged (both live; rate-limit binding GA) | +| Responsive + reduced-motion | Unchanged | +| Docs: README matches reality; CHANGELOG; CONTRIBUTING accurate | Unchanged (M7/M8) | +| **"Deploy-ready: Cloudflare Pages per README (build `npm run build`, output `dist`, NODE_VERSION=22)"** | **Reconciled:** deploy = Cloudflare **Workers** (static assets from `dist/` + `run_worker_first: ["/api/*"]`), auto-deployed by **Workers Builds on merge to main only** — branch pushes upload preview versions, verified against the deployments ledger (SHIP-GATE-R2 item 0). `wrangler.jsonc` is canonical; README's Pages section is fixed by E4. Worker deploy path documented in the runbook | + +## Non-goals (whole effort) +Everything in BACKLOG OWNER/DEFER columns; QA-REMEDIATE-v1 and +PANEL-RESPEC-v2 (external, un-consolidated — BACKLOG §D2); Astro 7 (G5); +full i18n governance layer (G2); IA changes; new features; any weakening of +POLICIES.md, the enforcement engine, or CI gates. diff --git a/ROADMAP.md b/ROADMAP.md index 7e0444d8..8a153fdd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -77,7 +77,7 @@ project in a new session or conversation. - **Required checks** — mark `path-a` / `e2e` / `skills` as required in branch protection. - **Dependabot remainder** — leave (dev-only) or schedule a framework-upgrade pass (Astro 6→7). -## 🔭 Deferred — FUTURE (see `docs/OPERATOR-RUNBOOK.md` Part B and `PLAN.md` §5) +## 🔭 Deferred — FUTURE (see `docs/OPERATOR-RUNBOOK.md` Part B and `docs/archive/PLAN-goose-pivot.md` §5) Goose live agent loop (`goose serve`/ACP) · server-side recipe signing + shareable links · full es/ar recipe-content translation. diff --git a/audit/AUDIT.md b/audit/AUDIT.md deleted file mode 100644 index 1284085a..00000000 --- a/audit/AUDIT.md +++ /dev/null @@ -1,93 +0,0 @@ -# AUDIT.md — SHIP Phase-1 findings (2026-07-11) - -Read-only research pass for the backlog-ship + design-elevation effort. -Placed at `audit/AUDIT.md` because root `AUDIT.md` is the (historical, -completed) Goose-pivot audit — promotion/renaming is a gate decision. -Companion docs: `BACKLOG.md` (consolidated ledger), `DESIGN.md` (direction), -`audit/screens/` (39 screenshots, 13 flows × 3 widths). - -## Baselines (executed this session) - -- `npm run verify:all` — **green**: astro check 0 errors/0 warnings, both - typechecks clean, **352/352 tests**, enforce + enforce:skills green, - production build 6,653 pages. -- `npx tsx enforcement/cli.ts all --tree .` — exits 1, but **every hit is - self-referential**: the engine's own signal-definition YAMLs and its - negative-test fixtures. **Zero hits in `src/`, `worker/`, `scripts/`, - `public/`.** Refinement (SHIP-GATE-R1 2c): CI **already dogfoods Layers 1–2 - full-tree** (`verify.yml:42-43`, green); only Layer 3 full-tree is - unsatisfiable by construction. Exact exempt paths, enumerated: - `enforcement/excluded-organizations.yaml`, - `enforcement/excluded-provider-signals.yaml`, `enforcement/tests/**`. - DoD-prose fix suffices — no engine change needed — gate item G3. -- `npm audit --omit=dev` — 4 low (esbuild via astro; Windows-dev-server-only, - prod unaffected). Full audit: 14 (7 low/6 moderate/1 high) — the high is - `tmp` via `@lhci/cli`, CI-tooling only. -- Enforcement full-tree + verify baselines contradict nothing in CI (10 - workflows, none disabled; the only two `continue-on-error` are - documented-deliberate: `security-pr.yml:42`, `security-cron.yml:58`). - -## P0 — security / broken / binding-doc conflicts - -| # | Finding | Evidence | -|---|---|---| -| P0-1 | **CLAUDE.md documents an i18n freshness/security-gate layer that does not exist** — incl. a 2-review gate for `security_sensitive` pages. One page carries the flag today (`guides/encrypted-group-messaging-marmot.mdx:10`) and gets **no extra protection**; the schema doesn't even define the field. CLAUDE.md is binding session instructions, so this is the highest-risk doc/reality conflict. | No `scripts/i18n-freshness.mjs`, `scripts/i18n-security-gate.mjs`, `i18n:stamp` script, `TranslationStatus.astro`, `src/config/i18n.mjs`, `TRANSLATING.md`, `.github/CODEOWNERS`, or i18n workflows (verified absent) | -| P0-2 | **PolicyChecker island ignores its `lang` prop** — the verdict surface (the product's thesis) renders English-only inside `/es/check/` and `/ar/check/`. | `src/components/PolicyChecker.svelte` (no `$props()`); pages pass `lang="es|ar"` (`es/check.mdx:18`, `ar/check.mdx:17`); hardcoded strings `:83-127` | - -(P0-3 in the first revision — CLI jargon in the checker verdict — is -**reclassified to P1-15** per the prompt's severity ladder: P0 is -security/broken/policy; copy register is UX. Reclassification requested and -ruled at SHIP-GATE-R1 2b.) - -## P1 — correctness / UX / design debt - -Kill-list hits (each with file:line; full table in the design-audit record): - -| # | Finding | Evidence | -|---|---|---| -| P1-1 | Landing feature sections are stock Starlight `CardGrid`/`Card icon=…` + 3 `LinkCard` grids | `src/content/docs/index.mdx:37-99` | -| P1-2 | Footer chrome is untouched Starlight default (only un-overridden chrome left) | `astro.config.mjs:146-153` | -| P1-3 | Catalog explorer: 1,359 entries as a single-column card wall, 60/page (desktop page renders 20,562px tall); native checkboxes in a scroll box; no `accent-color` anywhere in src | `CatalogExplorer.svelte:513,505-510,298`; screenshot `catalog-explorer-desktop.png` | -| P1-4 | Checker verdict = text line + default-styled table | `PolicyChecker.svelte:101-123` | -| P1-5 | Island style dialects split: token-native vs raw `--sl-*`/hex/off-scale rems. Worst: ModelCompass hex ramp (`#d33 #e80 …`), CostEstimator all-`--sl-*`, BuildStudio mixes both in one stylesheet, lone raw box-shadow | `ModelCompass.svelte:144-152`; `CostEstimator.svelte:240-276`; `BuildStudio.svelte:1355` vs `:1360-1364`; `CatalogExplorer.svelte:536` | -| P1-6 | `TODO: confirm` is user-visible product copy (zero-fabrication semantics, placeholder register) | `model-compass/ui/i18n.ts:73`; rendered `CostEstimator.svelte:220`, `ModelCompass.svelte:48,53,62`; advertised `build/cost.mdx:13`, `build/models.mdx:13` | -| P1-7 | Dev-jargon first-contact copy on organizer surfaces (landing, /build, /check, /build/models, /start) | `index.mdx:87,89`; `build.mdx:13-16`; `check.mdx:12-14`; `models.mdx:10-12`; `start/index.mdx:11-13` | -| P1-8 | i18n strays: BuildStudio hardcoded English beside its own STR table (`:1074,1027,1036,1084,1265,1272,1314-1322`); catalog CTA hardlinks English `/build/` from es/ar (`CatalogExplorer.svelte:358` — locale-aware `buildBase` exists at `:287` unused); AccountWidget ar placeholder (`:79`); English data-layer prose rendered in localized UIs (compass `models.ts:57`, estimator `providers.ts:64-68`, `CatalogList.astro:55-62`) | as cited | -| P1-9 | All 16 island mounts `client:load`; zero `client:visible/idle`. ModelCompass ships its whole registry as eager JS with no no-JS render | grep `client:`; `build/models.mdx:16` | -| P1-10 | `/console/` styled with raw hex outside the token system (own dark-mode media query) | `AdminConsole.svelte:470-523`; `console/index.astro:33-44` | -| P1-11 | Arabic gets no display face (identity absent for RTL readers) | `tokens.css:12-13` | -| P1-12 | CSP still Report-Only (mechanism complete; operator flip pending) | `astro.config.mjs:63`; `security-headers.ts:112-180` | -| P1-13 | Cost Estimator: 12× `unitPrice: null` + 3× `lastVerified: null`; tier bands/scale placeholders. Model Compass: 7 models `score: null` (runbook undercounts as 3) | `providers.ts:62-97`; `tiers.ts:26-30`; `models.ts:37-182` | -| P1-14 | Admin panel phases 4–8 unbuilt (per spec + wrangler binding comments); staging follow-ups (publish→PR, enforcement wiring, expiry job) logged | `docs/admin-panel-spec.md:73-136`; `wrangler.jsonc:48-62` | -| P1-15 | CLI jargon inside the checker verdict aimed at non-developers: "run `npx tsx enforcement/cli.ts all --tree .` on a real project." (reclassified from P0 at SHIP-GATE-R1 2b) | `PolicyChecker.svelte:126-127` | - -## P2 — polish / docs / deferred - -- Doc drift set: ROADMAP Done stops at #35 (repo at #55); admin-spec header - says "not yet built"; PLAN.md footer says "no feature code written"; README - describes Pages-dashboard deploy vs actual Workers Builds topology - (`README.md:127-148`); `TRANSLATING.md` referenced but file is - `TRANSLATIONS.md`; catalog-count disagreement (CLAUDE.md 1,355 / TRANSLATIONS - ~2,182 / disk 1,360); runbook §B2 undercount; MOBILE_FIXES header cites a - merged branch; `operational_advisory` name→workflow mapping lives only in - guard.py. -- `/build/models/` color-contrast: 89 axe nodes (en/es) from opacity + hex - fallbacks (a11y score still ≥ budget) — folds into P1-5's tokenization. -- Catalog prose translation run (pipeline built, needs `ANTHROPIC_API_KEY` + - waves); native-speaker review of ~25 machine-translated pages. -- Dependabot remainder (5 dev-only alerts); the **Astro 7 coupled upgrade** - (astro 7.0.7 + starlight 0.41.3 + @astrojs/svelte 9 — 0.40 is the newest - Starlight compatible with Astro 6; breaking-change list verified, none of - the removed APIs used in-repo) — a decision, not an obligation. -- NIP-07 one-line note in public docs; Plausible remains the only sanctioned - (opt-in, off-by-default) third-party origin. -- Stack otherwise current: wrangler `ratelimits` GA (not beta), - `run_worker_first` current, no deprecated Astro/nostr-tools API usage found. - -## What is NOT broken (verified, for the record) - -Intentional identity system exists and is substantial (tokens/theme/components -+ 5 overrides + Build Plate + self-hosted subset fonts). No purple gradients, -no glassmorphism, no lorem, no stock illustration, no font CDNs. Path A holds -(broker retired; CI-gated). Mobile fixes 1–8 merged. `/api/health`, -`/api/license`, auth, admin auth+staging all live and tested (352 tests). diff --git a/docs/OPERATOR-RUNBOOK.md b/docs/OPERATOR-RUNBOOK.md index d250b584..e8586150 100644 --- a/docs/OPERATOR-RUNBOOK.md +++ b/docs/OPERATOR-RUNBOOK.md @@ -116,7 +116,7 @@ no `wrangler deploy` in CI, so no `CLOUDFLARE_API_TOKEN` is needed in GitHub Act - **What:** Replace the current "bring back the JSON your Goose run produced" step with a live, streamed session. - **Why deferred:** Experimental (mid-2026), and it would mean the platform drives a live - loop — a meaningful architecture step (`PLAN.md` §5). `reflectFromResponse` was + loop — a meaningful architecture step (`docs/archive/PLAN-goose-pivot.md` §5). `reflectFromResponse` was deliberately designed to later accept a streamed response without changing its deterministic contract. - **What it needs:** A stable Goose serve/ACP endpoint to target, plus a decision on where @@ -126,14 +126,14 @@ no `wrangler deploy` in CI, so no `CLOUDFLARE_API_TOKEN` is needed in GitHub Act - **What:** Sign recipes and produce shareable links (today there's only the client-side `goose://` deeplink + the zip). - **Why deferred:** Needs the Worker (server-side signing/storage); out of the pure-client - scope the pivot stayed within (`PLAN.md` §5). + scope the pivot stayed within (`docs/archive/PLAN-goose-pivot.md` §5). - **What it needs:** A Worker endpoint + a signing key + a storage decision (KV/R2), and a UX for link expiry/revocation. ### 5. Full catalog translation for es/ar recipes - **What:** Localized extension/recipe **content** in Spanish and Arabic (the UI is translated; recipe/extension copy is not fully localized). -- **Why deferred:** Scope (`PLAN.md` §5). +- **Why deferred:** Scope (`docs/archive/PLAN-goose-pivot.md` §5). - **What it needs:** Run/extend `npm run translate:catalog` (needs `ANTHROPIC_API_KEY`) over recipe/extension content, then human review of the machine output (the `machine_translated` schema flag already exists for surfacing an advisory). diff --git a/docs/archive/AUDIT-goose-pivot.md b/docs/archive/AUDIT-goose-pivot.md new file mode 100644 index 00000000..0158e80c --- /dev/null +++ b/docs/archive/AUDIT-goose-pivot.md @@ -0,0 +1,260 @@ +# AUDIT.md — wecanjustbuildthings.dev architecture & Path-A audit + +**Mode:** read-only / plan. No source files were modified to produce this report. +**Branch:** `claude/kind-albattani-tnycq4` +**Scope:** the four builder components (Build Studio, Mentor Engine, Skills Creator, +Catalog), the shared build-session, the Content-Layer catalog, all LLM-inference +pathways, existing Goose wiring, framework/host config, and vendor-exclusion posture. +**Goal context:** wire the platform to drive **Goose** as a user-facing agent runtime, +while the deployed platform itself stays "Model-free Path A" (zero inference, one +documented exception). Findings only — **no solutions proposed.** + +> Note: the project's PreToolUse guard blocks writing the literal excluded-vendor +> API host strings (OpenAI/xAI/Meta). Where those hosts are relevant below, they are +> cited by `file:line` rather than reproduced verbatim. Permitted hosts (Anthropic, +> OpenRouter, DeepSeek, Google) are shown normally. + +--- + +## 1. Architecture map — the four components + +All four are Svelte 5 islands (or inline logic within one) mounted into Starlight +`.mdx` pages with `client:load`. Pure logic is factored into `src/lib/*.ts` and +`src/modules/cost-estimator/`. Everything client-side is deterministic. + +### Build Studio +The orchestrator; hosts Movements 1–4 and the Skills Creator inline. + +| File | Role | +|---|---| +| `src/components/BuildStudio.svelte` (~1,525 ln) | The island: intent → blueprint → skills → handoff. Reads/writes the build session. | +| `src/content/docs/build.mdx:18` | Mount: `` (`/build/`); es at `src/content/docs/es/build.mdx:18`, ar under `src/content/docs/ar/…` | +| `src/lib/studio-stack.ts` | Stack eligibility, advisory ranking, `pinnedDependencies` (#3/#4). | +| `src/lib/chemistry.ts` | Tool "chemistry" — capability role-graph pairs/conflicts/order. | +| `src/lib/mentor-engine.ts` | Movement 1 signals/questions/constraints (see below). | +| `src/lib/skill-doc.ts` | Movement 3 `slugifySkill` / `skillToMd` (SKILL.md renderer). | +| `src/lib/build-session.ts` | The shared session object (see §2). | +| `enforcement/matcher.ts` | Imported into the island to **re-verify** the assembled stack in-browser. | + +**State:** reads the shared build session on mount (`BuildStudio.svelte:232-252`), +persists every change via a `$effect` (`:283-310`). The downloadable starter +(zip / GitHub / Goose recipe) is assembled in `starterFiles()` (`:806-822`). + +### Mentor Engine +| File | Role | +|---|---| +| `src/lib/mentor-engine.ts` | **Pure, deterministic, stateless.** `detectSignals`, `pickQuestions`, `reflect` over a curated trilingual lexicon (`:33-42`) + protocol signals (`:45-50`). No model, no I/O. | +| `src/lib/mentor-engine.test.ts` | Unit tests incl. structural invariants. | + +**State:** holds none itself. BuildStudio computes `reflect(signals, answers)` +(`BuildStudio.svelte:547`) and persists the result into `session.converged` +(`:295-296`). The deterministic `reflect()` is what CLAUDE.md calls the +"structured-reflection step"; it makes **no** inference call (see §4 nuance). + +### Skills Creator +| File | Role | +|---|---| +| `src/components/BuildStudio.svelte:962-981` | **Inline** Movement 3 UI + capture/dedupe (`captureSkill`, `authoredSkills`, `removeAuthored`). | +| `src/lib/skill-doc.ts` | Pure `slugifySkill` + `skillToMd` (YAML-frontmatter SKILL.md). | +| (external) `github.com/MartinMontero/knowledge-to-skills-pipeline` | Referenced by generated README/skills folder (`BuildStudio.svelte:780`); not in this repo. | + +**State:** authored skills live in component state (`authoredSkills`) and are +persisted into `session.skills` (`BuildStudio.svelte:297`). There is **no** standalone +Skills Creator component or route — it is part of Build Studio. + +### Catalog +| File | Role | +|---|---| +| `src/content/docs/catalog/**/*.md(x)` | The entries themselves (Content-Layer docs; see §3). | +| `src/content.config.ts` | Collection definition (`docsLoader` + `docsSchema({ extend: catalogExtend })`). | +| `src/schema/catalog.ts` | The Zod frontmatter schema. | +| `src/pages/catalog.json.ts` | Build-time static JSON API at `/catalog.json` (filters to `INDEXED_TYPES`). | +| `src/components/CatalogExplorer.svelte` | Interactive island (search/facets/sort) at `/catalog/` (`catalog/index.mdx:21`). | +| `src/lib/catalog-filter.ts` | Pure filter/facet/sort logic used by the island. | +| `src/components/CatalogList.astro` | Static server-rendered lists (e.g. PIE pages). | +| `data/*.json` + `scripts/build-catalog.ts` | Build-time seeds → catalog md generator (`package.json:19`). | + +**State:** the island fetches `/catalog.json` once on mount +(`CatalogExplorer.svelte:237`) and filters entirely client-side. It does **not** use +the shared build session — its only handoff to Build Studio is a URL param +(`?seed=`, read at `BuildStudio.svelte:271-277`). + +--- + +## 2. The shared client-side build-session object + +**Defined in:** `src/lib/build-session.ts` — `interface BuildSession` (`:54-90`). + +**Persistence:** **`localStorage`** (not IndexedDB, not a framework store). +- Key: `wcb.build-session.v1` (`build-session.ts:92`). +- Change notification: same-tab `CustomEvent('wcb:session-change')` (`:93`, dispatched in `saveSession` `:161`) + cross-tab `storage` event (`subscribeSession` `:194-206`). +- Defensive load/coerce via `migrate()` (`:120-181`); silent degradation if storage is unavailable (`saveSession` `:158-160`). + +**Fields** (`build-session.ts:54-90`): +| Field | Line | Owner / meaning | +|---|---|---| +| `v: 1`, `updatedAt` | 55-56 | schema version + timestamp | +| `movement: 1\|2\|3\|4` | 58 | wayfinding | +| `intent{projectName,problem,goal,success,protocols,answers}` | 60-67 | Movement 1 raw intent + Socratic answers | +| `converged` | 69 | Movement 1 deterministic reflection | +| `adjustments{swaps,removed,extra}` | 71-75 | Movement 2 blueprint edits | +| `stack: SessionStackItem[]` | 77 | Movement 2 resolved tools + receipts | +| `skills: SessionSkill[]` | 79 | Movement 3 authored skills | +| `seededTool` | 81 | Catalog "Build with this" seed | +| `handoff` | 83 | Movement 4 method (`zip\|github\|goose\|kickoff`) | +| `usage?`, `costEstimate?` | 88-89 | Hosting Cost Estimator I/O | + +**Readers/writers** (only three modules import it): +- `src/components/BuildStudio.svelte` — read + write (primary). +- `src/modules/cost-estimator/ui/CostEstimator.svelte` — read. +- `src/modules/cost-estimator/core/usage-profile.ts` — write (`usage` / `costEstimate`). + +**Components NOT wired to the build session (flagged):** +- `src/components/CatalogExplorer.svelte` — independent; hands off only via `?seed=` URL param, **not** the session object. (So a Catalog→Studio handoff carries a single tool id, nothing else.) +- `src/components/PolicyChecker.svelte` (`/check/`) — fully standalone; no session read/write. +- `src/components/AccountWidget.astro` — uses a **separate** server-side auth session (HttpOnly cookie → KV → D1), unrelated to the client build session. + +--- + +## 3. Catalog inventory (Astro Content Layer) + +**Content Layer:** confirmed. `src/content.config.ts` uses Starlight's +`docsLoader()` (a glob/file loader over `src/content/docs/`) with +`docsSchema({ extend: catalogExtend })` — i.e. the Zod schema in +`src/schema/catalog.ts` layered onto Starlight docs frontmatter. + +**Counts** (English content; `es`/`ar` catalog dirs contain only an `index.mdx` — +translations not yet generated): +- **2,182** entry files under `src/content/docs/catalog/`. +- **827** of those are datasets (`src/content/docs/catalog/datasets/`). +- **1,354** tool-class entries — authoritative count from `npm run enforce:layer1` + (`✓ 1354 catalog entries clean`). (1,354 + 827 + 1 index ≈ 2,182.) +- `/catalog.json` ships `INDEXED_TYPES` = tools + datasets (`catalog.json.ts:9-15`). + +**Schema fields** (`src/schema/catalog.ts:80-134`, all optional at top level; +conditionally required via `superRefine` `:140-207`): +- Discriminator: `entry_type` (`:82`) ∈ `ENTRY_TYPES` (`:28-40`). +- Dependency: `dependency_name, ecosystem, category, what_it_does, homepage_url, repo_url, registry_url, protocols` (`:85-92`). +- License (accountability core): `license_spdx, license_source_url, license_source_commit_sha` (`:95-97`). +- Maintenance: `maintenance_status, last_release_at, version` (`:100-105`). +- AOS adoption: `aos_repos_using, aos_repos_list` (`:108-109`). +- Nav/i18n: `pie_anchor` (`:112`), `machine_translated` (`:117`). +- Enforcement: `provider_agnostic, verification_status, verification_blocked_reason, origin_advisory, verified_at` (`:120-126`). +- **Recipe contract** (required when `entry_type==='recipe'`): `recipe_type, target_entry, target_entry_slug, excluded_providers_unreachable_when[], verification_steps[]` (`:129-133`, refined `:181-206`). + +**How datasets / extensions / skills are represented:** +- **Datasets** — first-class: `entry_type: 'dataset'` (`schema:34`), 827 files, surfaced in `/catalog.json` as `kind: 'dataset'` (`catalog.json.ts:21`). Seed: `data/datasets.json`. +- **Extensions (Goose/MCP)** — **NOT a first-class type.** `ENTRY_TYPES` (`schema:28-40`) has no `extension`. MCP servers exist only as generic catalog **tools** (e.g. `data/agentic-tools.json:1173-1230`, "MCP Ecosystem"). Relevant to the Goose pivot, where extensions are a distinct concept. +- **Skills** — **NOT a catalog type.** No `skill` in `ENTRY_TYPES`. Skills are a build-flow artifact (authored in Build Studio → `skill-doc.ts` → SKILL.md) and a build-time seed (`data/skills-seed.json`); the authoring how-to lives in the external knowledge-to-skills-pipeline. +- **Recipes** — first-class `entry_type: 'recipe'` (`schema:35`) but currently only **2** files under `src/content/docs/recipes/`. These are *provider-lockdown policy recipes* (exclusion exceptions), **distinct** from the Goose run-recipe YAML the Build Studio generates (§5). + +--- + +## 4. LLM inference calls (Path-A check) + +**Result: NOT zero.** There is exactly **one** server-side inference pathway, +reached from two client call sites. It is BYOK (the user's key), allowlist-gated, +and appears to be the documented "one permitted exception" — but see the nuance. + +**The inference call (server-side, in the Worker):** +- `worker/index.ts:199` — `const res = await fetch(p.url, { ...p.build(model, prompt, apiKey), … })` inside `kickoffHandler` (`:182-207`), routed at `/api/agent/kickoff` (`:463`). +- Provider hosts it can reach (`PROVIDERS`, `worker/index.ts:68-99`): + - `:70` Anthropic Messages API (`api.anthropic.com`) + - `:80` OpenRouter chat-completions (`openrouter.ai`) + - `:90` DeepSeek chat-completions (`api.deepseek.com`) + +**Client call sites (same-origin → the Worker brokers; the browser never calls a model directly):** +- `src/components/BuildStudio.svelte:1012` — builds the **MENTOR structured-reflection** prompt (returns JSON `proposals` + `skills`). +- `src/components/BuildStudio.svelte:904` — the **"Try a step with AI" kickoff** handoff (`handoff: 'kickoff'`). + +**Excluded-vendor host strings elsewhere are NOT calls** — they are the enforcement +engine's *negative signals* and tests (the OpenAI/xAI host literals live at +`enforcement/excluded-provider-signals.yaml:11,31`, and in +`enforcement/tests/recipe.test.ts:46`, `enforcement/tests/layer3.test.ts:25,33`). + +**Nuance / discrepancy to surface:** +- CLAUDE.md's "exactly one permitted exception: the single **deterministic** + structured-reflection step" is worded ambiguously. In code, the *deterministic* + reflection is `mentor-engine.reflect()` (`BuildStudio.svelte:547`) and makes **no** + model call. The actual *inference* exception is the `/api/agent/kickoff` broker, + which is used for **two** purposes: structured reflection (`:1012`) **and** a + free-form "try a step" kickoff (`:904`). So the single permitted endpoint backs + two product features, only one of which is "structured reflection." +- This sits in tension with the new goal ("the platform makes zero inference calls; + Goose runs inference on the user's machine"). The broker is the lone exception + today. (Flagged in Gaps & Risks; no fix proposed here.) + +--- + +## 5. Existing Goose integration + +**State: partial, recipe-file level — already wired, no deeplink/MCP.** + +- **Handoff option:** `handoff` includes `'goose'` (`BuildStudio.svelte:200`), with UI strings "Run it with Goose" / "Download the Goose recipe" / "Copy the run command" (`:43,58-59`, es `:96,111-112`, ar `:149,164-165`). +- **Recipe generation:** `gooseRecipe` derived (`BuildStudio.svelte:733-742`) emits a Goose recipe YAML — `version: "1.0.0"`, `title`, `description`, `instructions: |`, `prompt: |` (the full agent prompt). Bundled into the starter zip as `${slug}.goose-recipe.yaml` (`:814`) and individually downloadable (`:1360`). +- **Run command:** `goose run --recipe ${slug}.goose-recipe.yaml`, shown and copyable (`:1361-1362`). +- **Catalog presence:** Goose is a catalog entry — `data/agentic-tools.json:693-699`, `source_url: https://github.com/aaif-goose/goose`. +- **Docs:** Goose is referenced throughout prose (`astro.config.mjs:99,148`; README `:14,47,171,184`; guides incl. `guides/get-started-with-goose`, `TRANSLATIONS.md:44`). + +**Absent today (relevant to the pivot):** +- **No `goose://` deeplink** anywhere (searched `src/`, none). +- **No MCP / extension wiring** in the platform — MCP appears only as catalog *content* (`data/agentic-tools.json` "MCP Ecosystem"), not as integration code. +- The generated recipe is a **static template** (`instructions` + `prompt`); it does not yet encode `extensions:`, `activities:`, `parameters:`, or provider/settings blocks from the Goose recipe schema. +- The provider-lockdown **policy** recipes (`src/content/docs/recipes/`, `entry_type:'recipe'`) are unrelated to the generated **run** recipe — two different "recipe" concepts share the word. + +--- + +## 6. Framework / adapter / build config + +- **Astro:** `^6.4.6` (`package.json:44`). *(Note: the task brief said "Astro 5"; the repo is on Astro 6.)* +- **Starlight:** `@astrojs/starlight ^0.40.0` (`package.json:41`). Svelte `^5.56.3`; `@astrojs/svelte ^8.1.2`. +- **Build output mode:** `output: 'static'` (`astro.config.mjs:92`), `trailingSlash: 'ignore'` (`:93`). +- **Cloudflare adapter:** **none.** There is no `@astrojs/cloudflare` adapter and no `adapter:` in `astro.config.mjs`. Instead the site is a **static build served by a hand-rolled Worker**: `wrangler.jsonc` sets `main: worker/index.ts` with a Static-Assets binding (`assets.directory: ./dist`, `binding: ASSETS`, `not_found_handling: "404-page"`), KV (`SESSIONS`, `ATPROTO`), and D1 (`DB`). Static HTML is served by the asset layer (which bypasses the Worker); `/api/*` is handled in `worker/index.ts`. +- **Security headers:** emitted at build into `dist/_headers` via a custom integration (`astro.config.mjs:44-69`); hash-based CSP ships **Report-Only** by default (`:63`), `CSP_MODE=enforce` to enforce. +- **i18n:** root locale `en`, plus `es` and `ar` (RTL) (`astro.config.mjs:106-111`). + +--- + +## 7. AOS vendor-exclusion posture (Meta / OpenAI / xAI) + +**Posture: clean — no Meta/OpenAI/xAI used as an inference/model provider; Google permitted.** + +- **Dependencies:** no Meta/OpenAI/xAI SDK in `package.json` (`:39-66`). LLM-adjacent deps are only `@atproto/*` and `nostr-tools` (not model SDKs). No `openai`, `@anthropic-ai/sdk` (the broker calls Anthropic over raw `fetch`, not an SDK), `@ai-sdk`, or `langchain`. +- **Broker allowlist** (`worker/index.ts`): + - Direct providers offered: `anthropic`, `openrouter`, `deepseek` (`:68-99`). **OpenAI and xAI are not offered**, and the error message says so (`:189`). + - OpenRouter (a gateway to *all* vendors) is constrained by a **default-deny allowlist** `PERMITTED_ROUTER_VENDORS` (`:106-109`) and a hard block `isExcludedRouterModel` (matches the excluded-org vendor segments, `:118-121`), enforced before the call (`:195-197`). + - **Google is explicitly permitted** — `'google'` ∈ the allowlist (`:107`). +- **Catalog-level exclusion** is enforced separately by the three-layer engine (`enforcement/`, blocking CI gate `verify.yml`) and the `EXCLUDED_PROVIDER_IDS = ['meta','openai','xai','aws','oracle']` registry (`src/modules/cost-estimator/registry/providers.ts:16`), shared by the Model Compass registry. +- **Google services** (Fonts/Analytics/etc.): the only analytics is **Plausible** (opt-in, `astro.config.mjs:74-88`) — not Google. No Google Fonts/Maps/reCAPTCHA found. (Per policy Google is permitted; none is currently wired, so nothing to flag.) +- **Anthropic / DeepSeek** as offered providers are **not** policy violations — only Meta/OpenAI/xAI are excluded. + +--- + +## Gaps & Risks (ranked by severity) + +1. **[High · Path-A definition vs. code] The lone inference exception backs two features and overlaps with the Goose pivot.** + `/api/agent/kickoff` (`worker/index.ts:182-207`, fetch `:199`) is the only inference call, but it serves both the structured-reflection (`BuildStudio.svelte:1012`) and a free-form "Try a step with AI" kickoff (`:904`). CLAUDE.md authorizes *one* "deterministic structured-reflection step," yet the deterministic reflection (`reflect()`) makes no model call — so the wording and the code don't line up, and the "kickoff" is arguably a second use of the exception. As Goose takes over user-facing inference (on the user's machine), this server-side broker's necessity should be re-examined. *(Observation only.)* + +2. **[High · Goose readiness] Extensions and skills are not first-class catalog types; the generated Goose recipe is a minimal template.** + `ENTRY_TYPES` (`schema:28-40`) has no `extension` or `skill`; MCP servers are only generic `tool` entries (`data/agentic-tools.json`). The emitted recipe (`BuildStudio.svelte:733-742`) carries only `instructions`+`prompt` — no `extensions`, `parameters`, `activities`, or provider/settings blocks from the Goose recipe schema. Driving Goose richly will need a representation for extensions and a fuller recipe. + +3. **[Medium · Docs accuracy] Stated facts diverge from the code.** + - Brief/CLAUDE.md imply **Astro 5**; repo is **Astro `^6.4.6`** (`package.json:44`). + - CLAUDE.md says "~842 YAML entries"; actual is **1,354 tool entries + 827 datasets** (`enforce:layer1`; `find` counts). Entries are Markdown/MDX, not raw YAML. + - These don't break anything but undermine trust in the written invariants. + +4. **[Medium · State coupling] Three islands are not on the shared session.** + `CatalogExplorer` hands off only a single `?seed=` id (`BuildStudio.svelte:271`), and `PolicyChecker` / `AccountWidget` are fully independent. Any future flow that wants Catalog selections, a policy-check result, or the signed-in identity to travel into the Build Studio has no channel today. (`AccountWidget` is deliberately a *separate* auth session — not a defect, but worth noting it is unrelated to the build session.) + +5. **[Medium · CSP] The site-wide CSP is still Report-Only.** + `astro.config.mjs:63` ships `report-only` unless `CSP_MODE=enforce`. Until promoted, the hash-based CSP is observational. (Collection was made durable earlier in this branch.) + +6. **[Low · i18n] Catalog translations are effectively absent.** + `src/content/docs/es/catalog/` and `…/ar/catalog/` contain only an `index.mdx`; the 1,354+827 entries exist in English only. The Build Studio / Catalog UIs are translated, but entry bodies are not (a `translate-catalog` script exists, `package.json:27`). + +7. **[Low · naming] Two distinct "recipe" concepts share the word.** + Policy provider-lockdown recipes (`entry_type:'recipe'`, `src/content/docs/recipes/`) vs. the generated Goose run-recipe YAML (`${slug}.goose-recipe.yaml`). A reader can easily conflate them; worth disambiguating in any Goose-facing work. + +--- + +*End of audit. No source files were modified. Awaiting direction before proposing changes.* diff --git a/docs/archive/PLAN-goose-pivot.md b/docs/archive/PLAN-goose-pivot.md new file mode 100644 index 00000000..648b8cd8 --- /dev/null +++ b/docs/archive/PLAN-goose-pivot.md @@ -0,0 +1,456 @@ +# PLAN.md — drive Goose as a user-facing agentic runtime via ONE build session + +**Source:** `AUDIT.md` (read-only audit, branch `claude/kind-albattani-tnycq4`). +**Mode:** PLAN ONLY — no feature code. Contracts below are *specifications* (types + +signatures, no bodies). Execution happens slice-by-slice after review. +**Deliverable of the pivot:** the four components (Build Studio, Mentor Engine, Skills +Creator, Catalog) converge on ONE typed client-side build session, which serializes to +a Goose recipe and launches via a `goose://recipe?config=` deeplink — all pure +client-side, model-free. + +--- + +## 0. Encoded constraints, reconciliations & out-of-scope + +**Hard constraints (carried into every slice):** +1. **Stack:** Astro (Starlight) + Cloudflare Pages; Catalog on Astro Content Layer + (`docsLoader` + Zod), per `src/content.config.ts` + `src/schema/catalog.ts`. +2. **ONE shared client-side build session** wired across the four components + (`src/lib/build-session.ts`). +3. **Model-free Path A:** ZERO inference API calls from the platform. Exactly ONE + deterministic structured-reflection exception that performs **no LLM call** — it + *reads* structured JSON the user brings back from their own Goose run + (`response.json_schema` output). +4. **Goose = STABLE primitive only:** client-side recipe deeplinks + `goose://recipe?config=` via `btoa(JSON.stringify(recipe))` behind a + UTF-8-safe wrapper (reuse the pattern in `src/components/AccountWidget.astro:86-95`). +5. **Mentor Engine** = Yoda/Morpheus/J.A.R.V.I.S./Neo persona shipped as a Goose + **SKILL** + deterministic reflection over the recipe's `response.json_schema` + output (`src/lib/mentor-engine.ts`). +6. **Vendor exclusions = Meta/OpenAI/xAI ONLY** as inference providers; Google + permitted. Goose is model-agnostic — **never hardcode a provider**; omit + `settings.goose_provider`/model so the user's own Goose config selects it. +7. **MCP safety:** the **Catalog is the trust boundary**. Only allowlisted, vetted + extensions/skills may appear in a generated recipe. Lean on Goose "Trust & + Execute" consent + extension allowlist + least privilege. Never surface raw MCP + server config to non-devs. Treat tool descriptions as untrusted unless sourced + from a verified Catalog entry. + +**Reconciliations with `AUDIT.md` (facts the brief mis-stated — plan honors reality):** +- Framework is **Astro `^6.4.6`**, not 5 (`AUDIT.md §6`, `package.json:44`). Content + Layer API is identical for this work; the plan is version-agnostic and pins nothing + to a major version. +- Catalog is **1,354 tool-class + 827 dataset** entries (`AUDIT.md §3`), not "~842". + Entries are MD/MDX with YAML frontmatter. Counts only matter to Slice B's allowlist + scope. +- There is **no `@astrojs/cloudflare` adapter**; static build + hand-rolled Worker + (`AUDIT.md §6`, `wrangler.jsonc`). All new logic is **pure client-side**, so the + serializer/deeplink never touch the Worker. + +**Out of scope (note as FUTURE ENHANCEMENT only — do not build now):** +- `goose serve` / ACP-over-HTTP / Goose TS SDK (experimental mid-2026). Recorded in + §6 "Future" so a later slice can pick them up; no design dependency on them. +- Retiring the existing `/api/agent/kickoff` broker is IN scope (Slice D) because it is + the lone inference call standing between us and Path-A zero. + +--- + +## 1. Cross-cutting invariants & global gates (apply to ALL slices) + +These are the exit gates every slice is measured against, in addition to its own: + +- **G-PathA (zero inference):** no platform code fetches a model host. Enforced by a + new additive test/CI check (Slice D) — extends the negative-signal approach already + in `enforcement/excluded-provider-signals.yaml` and `enforcement/tests/layer3.test.ts`. +- **G-Vendor:** no Meta/OpenAI/xAI as a provider; recipe emits no provider/model. + Reuse `EXCLUDED_PROVIDER_IDS` (`src/modules/cost-estimator/registry/providers.ts:16`) + + the broker allowlist logic pattern (`worker/index.ts:106-130`) as the vetting source. +- **G-Trust (MCP):** every extension in a generated recipe ∈ the Catalog allowlist; + property-tested so a non-allowlisted extension is unrepresentable. +- **G-Gate:** the blocking `operational_advisory` CI gate is never weakened; all new CI + is ADDITIVE in new workflow files (CLAUDE.md #4; `AUDIT.md §6`). The project guard + (`.claude/hooks/guard.py`) stays in force. +- **G-Determinism:** all new serializer/deeplink/reflection code is pure and + deterministic — same session ⇒ byte-identical recipe & deeplink. +- **G-Green:** the existing **185-test** suite stays green (`npm test`), `astro check` + 0 errors, and a production `npm run build` succeeds, after every slice. +- **G-NonDev:** no raw MCP/server config, env vars, or shell strings shown to the + builder; all attacker-influenceable text rendered via `textContent` (reuse the + XSS-safe pattern already used in `src/components/AccountWidget.astro`). + +--- + +## 2. Sequencing / gating + +``` +A (typed session + Catalog wiring) +└─> B (recipe serializer + Catalog extension allowlist) + ├─> C (deeplink + "Open in Goose" UX) + ├─> D (Mentor persona SKILL + structured-reflection; retire broker → Path-A zero) + └─> E (Skills Creator → session + Catalog sub-recipes) +``` +A is the foundation. B depends on A. C, D, E each depend on B; C/D/E are independent of +one another and may land in any order once B is in. Each slice is its own PR, gated on +its benchmark + the global gates in §1. + +--- + +## SLICE A — Formalize the shared build session; wire all four components + +**Goal.** Turn `BuildSession` into a single typed, runtime-validated, versioned schema +and ensure all four components read/write the *same* object — closing `AUDIT.md` Gap #4 +for the Catalog. (PolicyChecker/AccountWidget are intentionally NOT among "the four" +and stay independent — `AUDIT.md §2`.) + +**Gate (entry):** none (foundation). + +**Files to change.** +- `src/lib/build-session.ts` — formalize the schema (see contract); bump `v: 1 → 2` + with a `migrate` v1→v2 path (extend the existing `migrate()` at `:120-181`, same + defensive style); add session fields the recipe needs: `extensions`, `mentorReflection`. +- `src/lib/build-session.test.ts` — extend (round-trip, v1→v2 migrate, coercion). +- `src/components/CatalogExplorer.svelte` — wire to the session: write selected + tools/extensions; on mount read prior selections (reuse the subscribe pattern from + `src/components/BuildStudio.svelte:232-252` and `src/modules/cost-estimator/ui/CostEstimator.svelte:33-35`). +- `src/components/BuildStudio.svelte` — consume the new typed fields where it already + reads/writes the session (`:232-310`); no behavior change beyond new fields. +- (Mentor Engine + Skills Creator already persist through BuildStudio — formalize their + field ownership in the schema doc, no new component file.) + +**Contract / interface.** +```ts +// src/lib/build-session.ts (Zod, mirroring the astro/zod pattern in src/schema/catalog.ts) +export const SESSION_VERSION = 2 as const; + +export interface SessionExtension { // an allowlisted Goose extension ref (Slice B/G-Trust) + catalogId: string; // slug of the verified Catalog entry it came from + name: string; + kind: 'builtin' | 'mcp_stdio' | 'mcp_sse'; // resolved from the Catalog, never user-typed +} +export interface SessionMentorReflection { // deterministic output of Slice D + schemaVersion: 1; + constraints: string[]; // ConstraintId[]; from mentor-engine + proposals: Array<{ action: 'add'|'swap'|'remove'; name: string; why: string }>; +} +export interface BuildSession { // EXTENDS the current interface at :54-90 + /* …all existing fields… */ + v: 2; + extensions: SessionExtension[]; // NEW — selected, allowlisted extensions + mentorReflection: SessionMentorReflection | null; // NEW — Slice D writes; null until run +} +export function migrate(parsed: unknown): BuildSession; // v1 docs upgrade to v2 (extensions:[], mentorReflection:null) +``` +Component contract (unchanged API surface): all four use only +`loadSession/saveSession/updateSession/subscribeSession` (`build-session.ts:143-206`). + +**Test plan.** +- *Unit:* extend `build-session.test.ts` — v1→v2 migrate fills `extensions: []` / + `mentorReflection: null`; Zod rejects malformed extensions; round-trip + cross-tab + event still fire (reuse the fake-DOM harness already in that file). +- *Integration:* a jsdom-free node test that simulates Catalog write → BuildStudio read + through `localStorage` + `subscribeSession` (extend the existing pub/sub tests). + +**Rollback.** Single revert of the slice commit; `migrate` is forward-only and +v1-compatible, so reverting to v1 readers ignores the new fields (they were additive & +optional in storage). No data migration to undo. + +**Benchmark / threshold (proves it works).** +- All four components demonstrably read+write the one object in an integration test + (Catalog selection observable in a BuildStudio-side read): **pass = 1 object, 4 + participants**. +- v1→v2 migrate property test: **100%** of randomly-mutated v1 blobs coerce to a valid + v2 (no throw, schema-valid). +- `npm test` stays green at **≥185** tests; `astro check` 0 errors. + +--- + +## SLICE B — Pure-client recipe serializer (session → Goose recipe) + +**Goal.** Deterministically turn a `BuildSession` into a Goose recipe object +(`extensions` allowlisted from the Catalog, `parameters`, `activities`, +`response.json_schema`, `instructions`, `prompt`) — superseding the minimal template at +`src/components/BuildStudio.svelte:733-742`. Model-agnostic (no provider/model). + +**Gate (entry):** Slice A merged (needs `session.extensions` + typed schema). + +**Files to change.** +- **New** `src/lib/goose-recipe.ts` — the pure serializer (extraction pattern of + `src/lib/catalog-filter.ts` / `src/lib/skill-doc.ts`). +- **New** `src/lib/goose-recipe.test.ts`. +- `src/schema/catalog.ts` — add a first-class **extension** representation (closes + `AUDIT.md` Gap #2): either `entry_type: 'extension'` in `ENTRY_TYPES` (`:28-40`) or a + `goose_extension` block, gated by `verification_status === 'verified'` in the existing + `superRefine` (`:140-207`). +- **New** `src/pages/extensions.json.ts` — build-time static allowlist of vetted + extensions for the client serializer (same Content-Layer query pattern as + `src/pages/catalog.json.ts`). +- `src/components/BuildStudio.svelte` — replace the inline `gooseRecipe` derived + (`:733-742`) with a call to `buildGooseRecipe(session, allowlist)`. +- `enforcement/` — extend the layer-3 recipe contract (`enforcement/cli.ts` + + `enforcement/types.ts:92` `RecipeFinding`) to assert generated recipes reference only + allowlisted extensions; add fixtures under `enforcement/tests/`. + +**Contract / interface.** +```ts +// src/lib/goose-recipe.ts +export interface GooseRecipe { // the stable recipe primitive we emit + version: string; // "1.0.0" + title: string; + description: string; + instructions: string; + prompt: string; + extensions: GooseExtensionRef[]; // ONLY from the allowlist + parameters: GooseParameter[]; // typed inputs the user fills in Goose + activities: string[]; // suggested next actions + response: { json_schema: JsonSchema }; // forces structured output (Slice D reads it) + // NOTE: no `settings.goose_provider` / model — model-agnostic by construction (G-Vendor) +} +export interface ExtensionAllowlist { // fetched from /extensions.json + byId: Record; +} +export function buildGooseRecipe( + session: BuildSession, + allow: ExtensionAllowlist, +): GooseRecipe; // pure; drops any extension not in `allow` +export const RESPONSE_JSON_SCHEMA: JsonSchema; // the shared schema Slice D reflects over +``` + +**Test plan.** +- *Unit:* `goose-recipe.test.ts` — determinism (same session ⇒ deep-equal recipe); + **G-Trust property test**: for arbitrary `session.extensions`, every emitted + extension ∈ `allow` and any non-allowlisted ref is dropped (cannot appear); + no provider/model field ever present (G-Vendor); recipe validates against the Goose + recipe JSON-schema (vendored as a test fixture). +- *Integration:* `enforcement` layer-3 run over a generated recipe fixture passes; + a recipe built from a representative session validates end-to-end. + +**Rollback.** Revert the slice; BuildStudio's `gooseRecipe` derived returns to the +`:733-742` template (kept in git history). Schema addition is additive/optional, so +existing catalog entries still validate. + +**Benchmark / threshold.** +- **0** non-allowlisted extensions emitted across a 1,000-case property test. +- **0** recipes carrying a provider/model field. +- Recipe schema-validation: **100%** of generated recipes valid against the vendored + Goose recipe schema. +- `enforcement` layer-3 passes on generated recipes; `npm test` green. + +--- + +## SLICE C — Deeplink generator + "Open in Goose" UX + +**Goal.** Encode a recipe into `goose://recipe?config=` (UTF-8-safe), launch via +an "Open in Goose" action with an **explain-before-launch** panel and a **copy-link** +(and recipe-file) fallback when the deeplink exceeds the length budget. + +**Gate (entry):** Slice B merged (needs `GooseRecipe`). + +**Files to change.** +- **New** `src/lib/goose-deeplink.ts` — pure encode/decode + length guard. The + UTF-8-safe base64 reuses the wrapper pattern in + `src/components/AccountWidget.astro:86-95` (encode) and its inverse for round-trip. +- **New** `src/lib/goose-deeplink.test.ts`. +- `src/components/BuildStudio.svelte` — the `handoff: 'goose'` branch (`:1357-1363`): + add "Open in Goose" (anchor to the deeplink), the explain panel, and the existing + copy/download fallbacks (`copy()` `:825`, `blobDownload()` `:828`). Keep the + `goose run --recipe …` file path as the >budget fallback. +- (Optional) `src/components/CatalogExplorer.svelte` — a single-tool "Open in Goose" + using a minimal one-extension recipe (only if Slice A wired selection; else defer). + +**Contract / interface.** +```ts +// src/lib/goose-deeplink.ts +export const DEEPLINK_MAX_BYTES = 8192; // budget; above this → fallback (tune in benchmark) +export function encodeRecipeConfig(recipe: GooseRecipe): string; // UTF-8-safe base64 +export function decodeRecipeConfig(b64: string): GooseRecipe; // inverse (for tests) +export function recipeDeeplink(recipe: GooseRecipe): { + url: string; // `goose://recipe?config=…` + bytes: number; + withinBudget: boolean; // false ⇒ UX shows copy-link/file fallback +}; +export interface ExplainModel { // what the explain-before-launch panel renders + title: string; + extensions: Array<{ name: string; why: string }>; // rendered via textContent (G-NonDev) + willDoNothingUntilUserConsents: true; // copy: Goose "Trust & Execute" prompts at runtime +} +export function explainRecipe(recipe: GooseRecipe): ExplainModel; +``` + +**Test plan.** +- *Unit:* `goose-deeplink.test.ts` — round-trip `decode(encode(r)) === r` incl. + **non-ASCII** (es/ar/emoji) titles/prompts (proves the UTF-8-safe wrapper); URL shape + `goose://recipe?config=`; `withinBudget` flips correctly at the boundary; + `explainRecipe` lists every extension (no omissions). +- *Integration:* build a recipe (Slice B) → deeplink → decode → recipe equals input; a + jsdom/Playwright check that the explain panel renders every extension via `textContent` + (no HTML injection) and the fallback appears when over budget. + +**Rollback.** Revert; the `goose` handoff falls back to the existing recipe-file + +`goose run` command (`:1360-1362`), which is unaffected. + +**Benchmark / threshold.** +- Round-trip fidelity: **100%** over a fuzz corpus including multibyte text. +- A representative full-stack recipe encodes to **< `DEEPLINK_MAX_BYTES`**; over-budget + recipes deterministically take the fallback path (asserted). +- Explain panel enumerates **100%** of recipe extensions; **0** raw MCP config strings + in the DOM (asserted). + +--- + +## SLICE D — Mentor persona SKILL + deterministic structured-reflection; retire the broker + +**Goal.** Ship the Yoda/Morpheus/J.A.R.V.I.S./Neo guidance as a Goose **persona SKILL** +inside the recipe, and make the platform's ONLY model-output consumption a +**deterministic** read of the recipe's `response.json_schema` JSON the user brings back +— then **retire `/api/agent/kickoff`** so the platform makes zero inference calls +(Path-A zero; `AUDIT.md §4`, Risk #1). + +**Gate (entry):** Slice B merged (needs `RESPONSE_JSON_SCHEMA` + recipe). + +**Files to change.** +- **New** `src/lib/mentor-persona.ts` — builds the persona SKILL.md via the existing + `skillToMd` (`src/lib/skill-doc.ts`); included by the serializer (Slice B) as a recipe + skill/instruction. Pure, deterministic, no model. +- `src/lib/mentor-engine.ts` — add `reflectFromResponse(json)` that deterministically + maps a `RESPONSE_JSON_SCHEMA`-conformant object → `SessionMentorReflection` (reuses + the existing `reflect()` constraint ordering `:155-186`). **No fetch, no LLM.** +- `src/lib/mentor-engine.test.ts` — extend. +- `src/components/BuildStudio.svelte` — replace the two broker calls + (`:904`, `:1012`) with: (a) generate recipe+deeplink (Slices B/C), (b) an + "import structured result" affordance that pastes/loads the Goose `response` JSON and + runs `reflectFromResponse` into `session.mentorReflection`. +- `worker/index.ts` — **remove** `kickoffHandler` (`:182-207`), its route (`:463`), the + `PROVIDERS` table + router-allowlist (`:68-130`). (Keep `coerceUsageProfile`/pricing.) +- `worker/tests/worker.test.ts` — drop the now-removed broker tests; keep the rest. +- **New** `.github/workflows/path-a.yml` — ADDITIVE CI asserting zero model-host fetches + in `src/` + `worker/` (extends the signal set in + `enforcement/excluded-provider-signals.yaml`). Does NOT touch `verify.yml` (G-Gate). + +**Contract / interface.** +```ts +// src/lib/mentor-engine.ts (additive) +export function reflectFromResponse( + response: unknown, // a RESPONSE_JSON_SCHEMA-shaped object (validated) +): SessionMentorReflection; // deterministic; null-safe; never calls a model + +// src/lib/mentor-persona.ts +export function mentorPersonaSkill(lang: 'en'|'es'|'ar'): DraftSkill; // → skillToMd → recipe +``` + +**Test plan.** +- *Unit:* `reflectFromResponse` is deterministic and total (malformed input → safe empty + reflection, mirroring `coerceUsageProfile`'s defensive style); persona SKILL.md + validates (parse frontmatter with `yaml`, as in `src/lib/skill-doc.test.ts`). +- *Integration:* recipe (Slice B) → its `response.json_schema` → a conformant sample → + `reflectFromResponse` → stable `SessionMentorReflection` written to the session. +- *Gate test:* `path-a.yml` check fails if any model host appears in platform code. + +**Rollback.** The broker removal is its own commit; revert restores `/api/agent/kickoff` +and the two client calls. The persona/reflection additions are independent and need not +be reverted with it. + +**Benchmark / threshold.** +- **G-PathA = 0**: zero model-host fetches in `src/` + `worker/` (new CI check passes). +- `reflectFromResponse`: deterministic across a 500-case corpus; **0** throws on + malformed input. +- No reachable `/api/agent/kickoff` route remains (worker route test asserts 404). + +--- + +## SLICE E — Skills Creator authoring → session + Catalog sub-recipes + +**Goal.** Make the inline Skills Creator (Movement 3, `src/components/BuildStudio.svelte:962-981`) +write authored skills into the session AND emit them as Goose **sub-recipes** the main +recipe references, with an optional contribution path into the Catalog as vetted, +reusable artifacts. + +**Gate (entry):** Slice B merged (sub-recipes are emitted by the serializer). + +**Files to change.** +- `src/lib/skill-doc.ts` — add a sub-recipe projection of a `DraftSkill` (a skill → + a minimal `GooseRecipe` fragment / sub-recipe ref); pure. +- `src/lib/skill-doc.test.ts` — extend. +- `src/lib/goose-recipe.ts` (Slice B) — include `session.skills` as recipe + sub-recipes/activities. +- `src/components/BuildStudio.svelte` — Skills Creator writes to `session.skills` + (already does `:297`) and now also surfaces "include as sub-recipe." +- `src/schema/catalog.ts` — optional **skill** representation (parallel to the extension + work in Slice B) so a contributed skill can become a verified Catalog entry; gated by + `verification_status`. +- `public/admin/config.yml` — add a skills collection for the contribution path (mirrors + the existing `recipes` collection at `:41-52`), so non-devs contribute via Sveltia CMS + → PR → enforcement. + +**Contract / interface.** +```ts +// src/lib/skill-doc.ts (additive) +export function skillToSubRecipe(s: DraftSkill): GooseSubRecipe; // pure; no model +export interface GooseSubRecipe { name: string; instructions: string; activities: string[] } +``` + +**Test plan.** +- *Unit:* `skillToSubRecipe` deterministic; SKILL.md still valid YAML (existing + `skill-doc.test.ts` harness); a skill with hostile text stays safe (reuse the + `yamlDoubleQuoted` coverage already there). +- *Integration:* author skill → `session.skills` → serializer emits it as a sub-recipe → + recipe still schema-valid (Slice B benchmark) and deeplink round-trips (Slice C). +- *Contribution:* a CMS-authored skill file passes `enforcement` layer-3. + +**Rollback.** Revert; `session.skills` continues to flow into the starter zip as today +(`src/components/BuildStudio.svelte:806-822`); no sub-recipe emission. + +**Benchmark / threshold.** +- Authored skill round-trips session ⇄ SKILL.md ⇄ sub-recipe with **0** data loss. +- Recipes including N authored sub-recipes stay schema-valid and **within the deeplink + budget** (else fallback) for N up to a documented cap. +- `enforcement` layer-3 green on contributed skills. + +--- + +## 3. Risks carried forward (from `AUDIT.md`) + new + +| # | Risk | Source | Handled in | +|---|---|---|---| +| R1 | Lone inference exception overlaps the pivot | AUDIT Risk #1 | **Slice D** (retire broker → Path-A zero) | +| R2 | Extensions/skills not first-class; recipe too thin | AUDIT Risk #2 | **Slice B/E** (schema + serializer) | +| R3 | Deeplink length limits truncate large recipes | new | **Slice C** budget + fallback | +| R4 | Untrusted MCP tool descriptions / raw config leak | constraint #7 | **G-Trust + G-NonDev** (allowlist, textContent) | +| R5 | Hardcoding a provider breaks model-agnosticism | constraint #6 | **Slice B** (no settings/provider; G-Vendor) | +| R6 | Catalog translations absent (es/ar recipes) | AUDIT Risk #6 | recipe i18n via `lang` param (Slice C/D), tracked | +| R7 | CSP still Report-Only; `goose://` scheme + new UX | AUDIT Risk #5 | verify CSP allows the scheme before enforce (note for CSP-promote work) | +| R8 | "recipe" overload (policy recipe vs run recipe) | AUDIT Risk #7 | naming: `GooseRecipe`/sub-recipe vs `entry_type:'recipe'` policy recipes | + +--- + +## 4. Definition of Done (whole pivot) + +- One typed build session; the four components read/write it (Slice A). +- Pure client serializer → schema-valid, model-agnostic, allowlist-only Goose recipe + (Slice B). +- `goose://recipe?config=` deeplink with explain-before-launch + fallback + (Slice C). +- Mentor persona SKILL + deterministic structured-reflection; **`/api/agent/kickoff` + retired ⇒ platform inference calls = 0** (Slice D). +- Skills Creator → session + sub-recipes + Catalog contribution path (Slice E). +- All global gates (§1) hold: Path-A=0, vendor-clean, MCP-trust-boundary, additive CI, + determinism, suite green, build green. + +--- + +## 5. Future enhancements (explicitly deferred — do NOT build now) + +- `goose serve` / ACP-over-HTTP / Goose TS SDK for a hosted, live agent loop + (experimental mid-2026). Would replace the bring-back-JSON step in Slice D with a live + session — design Slice D's `reflectFromResponse` so it can later accept a streamed + response without changing its deterministic contract. +- Server-side recipe signing / shareable recipe links (needs the Worker; out of the + pure-client scope here). +- Full catalog translation so es/ar recipes ship localized extension copy. + +--- + +*STATUS (2026-07-11, E3): this plan was EXECUTED in full — Slices A–E all +merged (recipe serializer, deeplink, mentor persona + deterministic reflection, +sub-recipes) and the `/api/agent/kickoff` broker retired, making Path A = 0 +platform inference calls, CI-gated by `path-a.yml`. Archived as a historical +record of the Goose pivot; §5's deferred items are tracked in BACKLOG.md.* diff --git a/src/lib/goose-deeplink.ts b/src/lib/goose-deeplink.ts index 6640622b..01f7b386 100644 --- a/src/lib/goose-deeplink.ts +++ b/src/lib/goose-deeplink.ts @@ -5,7 +5,7 @@ * explain-before-launch panel. No DOM, no network, deterministic. * * STABLE primitive only: recipe deeplinks. `goose serve` / ACP / the TS SDK are out of - * scope (see PLAN.md §5). + * scope (see docs/archive/PLAN-goose-pivot.md §5). */ import type { GooseRecipe } from './goose-recipe.ts';