synabun 2.0.0-beta → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/THIRD-PARTY-LICENSES.md +5 -0
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1309 -0
- package/neural-interface/i18n/en.json +41 -2
- package/neural-interface/i18n/pt-BR.json +41 -2
- package/neural-interface/lib/assistant-envelope.js +9 -6
- package/neural-interface/lib/augmented-path.js +23 -3
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/claude-agent-bridge.js +54 -147
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +52 -91
- package/neural-interface/lib/native-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +30 -44
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -0
- package/neural-interface/package-lock.json +0 -251
- package/neural-interface/public/install-app.html +31 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +401 -66
- package/neural-interface/public/onboarding.html +252 -146
- package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +58 -49
- package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/html-shell.js +9 -5
- package/neural-interface/public/shared/pwa-install-page.js +34 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +28 -0
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/start-bridge.js +583 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +801 -392
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/ui-claude-panel.js +3 -3
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-loading.js +221 -10
- package/neural-interface/public/shared/ui-menubar.js +11 -27
- package/neural-interface/public/shared/ui-settings.js +36 -59
- package/neural-interface/public/shared/ui-terminal.js +130 -144
- package/neural-interface/public/sw.js +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- package/neural-interface/scripts/rebuild-pty.js +15 -12
- package/neural-interface/server.js +220 -185
- package/package.json +13 -1
- package/postinstall.js +11 -81
- package/setup.js +72 -4
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- package/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
package/docs/judgments.md
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# Judgments — Jev (TypeSafe System One) in SynaBun
|
|
2
|
-
|
|
3
|
-
SynaBun asks Jev for a typed judgment wherever a regex or a similarity constant used to stand in for a semantic decision. This is the catalogue: every surface, what it sends, what it asks, the threshold code applies, and the heuristic that runs when the judgment is unavailable. Settings → **AI decisions** in the Neural Interface owns all of it at runtime.
|
|
4
|
-
|
|
5
|
-
## The contract
|
|
6
|
-
|
|
7
|
-
- `mcp-server/src/services/typesafe.ts` — `judge(state, questions, { surface, origin, entityId, sessionId, project, riders, onLogged, onUsage, timeoutMs, model, noCache, signal, logPreview })` **never throws and never rejects**. `sessionId` / `project` default to the MCP caller's context; `riders` names the surfaces whose questions share the request; `onLogged` hands back the log row id so the caller can record what it did with the answer. A caller's `signal` is combined with the timeout (`AbortSignal.any`), never a replacement for it. Kill switch, master switch off, disabled surface, missing key, timeout, non-2xx and malformed bodies all return `null`. Every call site keeps a working heuristic fallback.
|
|
8
|
-
- `mcp-server/src/services/memory-judgments.ts` — the questions. Independent questions over one state are batched into one request; they run in parallel server-side and cannot see each other's answers.
|
|
9
|
-
- Answer shapes: `choice → {choice, confidence, probabilities}`, `noul → {noul}` (P(yes); **no** confidence field), `score → {score, confidence, legend, probabilities}`. A Noul near 0.5 means "as likely yes as no", not "medium".
|
|
10
|
-
- **Never inside a SQLite transaction or a hook state lock.** Maintenance and the backfill judge before `BEGIN IMMEDIATE` and re-check the revision after; `stop.mjs` judges before `withStateLock`.
|
|
11
|
-
- Model `jev-latest` → `jev-1.13.0` (aliases: `jev-preview`). $0.042 per million input tokens, output free; 64k context per request, 32k for state plus the longest question (docs.typesafe.ai/models, 2026-09-19).
|
|
12
|
-
|
|
13
|
-
## Where the knobs live
|
|
14
|
-
|
|
15
|
-
| Knob | Where | Notes |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| API key | `~/.synabun/.env` `TYPESAFE_API_KEY`, mirrored into `process.env` on save, `resetTypeSafeKey()` | A shell export on the launching process wins over `.env`; the tab shows the source and warns when it shadows a saved key. |
|
|
18
|
-
| Kill switch | env `SYNABUN_TYPESAFE=off` | Wins over everything; the test suites set it. |
|
|
19
|
-
| Everything else | `kv_config` row `typesafe_config` (JSON), read by `typesafeConfig()` with a 3 s TTL | Master switch, model, base URL, per-surface `enabled` / `timeoutMs` / `minConfidence` / `minProbability` (the last only on the surfaces that ask a yes/no beside a Choice), and the limits where a surface has them: `minScore` (relevance floor on the 0–4 rubric), `maxItems`, `debounceMs`, `bashOnlyMinProbability`, `browserAssist` (see Browser assistance), `desktopAssist` (see Desktop assistance), cost rates, `applyJudgedImportance` (locked until a bench ran), backfill state, bench summaries. Every process — Neural Interface, HTTP MCP sessions, stdio MCP servers — obeys a change within seconds, no rebuild, no restart. |
|
|
20
|
-
| Judgment log | table `typesafe_log` (migration v5; v7 added `session_id`, `project`, `surfaces`, `outcome`), newest 20,000 rows | One row per call: surface, origin (`hook` / `tool` / `maintenance` / `loop` / `ui` / `script` / `assistant` / `backfill` / `bench`; rows from before v7 keep `live`, and the `live` filter means every real-time origin), entity, session, project, the riders that shared the request (`surfaces`, lead first), model, question count, 500-char credential-redacted state preview, answers, what the caller did with them (`outcome`: injected vs dropped memories, stale or not, picked messages, conflicts), latency, tokens, cached, error. For the `browser-*` and `desktop-*` surfaces the preview is a metadata line (`host` or app, operation, counts) passed as `logPreview`, and `[withheld]` when a call site forgets one: page and screen text is never logged. |
|
|
21
|
-
|
|
22
|
-
Endpoints (all in `neural-interface/lib/memory-api.js`): `GET/PUT /api/typesafe/config`, `POST /api/typesafe/test`, `POST /api/typesafe/cache/clear`, `POST /api/typesafe/metrics/reset`, `POST /api/typesafe/backfill` (`estimate` / `start` with `confirm:true` / `pause` / `resume` / `cancel`), `GET /api/typesafe/backfill/status`, `POST /api/typesafe/bench`, `GET /api/typesafe/log`, `GET /api/typesafe/category-checks` (+ `POST …/:id` `apply` / `dismiss`), `GET /api/trash/candidates` (`?stream=1` answers NDJSON: a snapshot per judged batch, then the final ranking with `done:true`; + `POST …/:id/trash` and `POST …/trash-selected` `{ids}` for the tab's bulk selection, ≤ 500 per call), `POST /api/hook-judge` (kinds `prompt`, `agent-message`, `stop-turn`, `loop-goal`, `compact-digest`, `plan-conflict`; `judge:false` → `{}`), `POST /api/hook-recall` (`surface` rerank / brief-rank, `floor`, `judge`, `budget_ms`, `want_stale`, `stale_ack`), `POST /api/hook-edit` (answers at once) and `GET /api/hook-edit/pending`, `GET /api/typesafe/sessions`, `GET /api/typesafe/supersessions` (+ `POST …/:id` `undo` / `keep`). `/api/typesafe/*` and `/api/hook-*` are admin-only: an invite-session guest gets 403. The hook ↔ server contract is in the Coding sessions section below.
|
|
23
|
-
|
|
24
|
-
## The surfaces
|
|
25
|
-
|
|
26
|
-
Default timeouts and thresholds are the shipped values; the Judgments tab changes them.
|
|
27
|
-
|
|
28
|
-
| Surface | Where it runs | Primitive · question | State | Threshold → action | Fallback when null |
|
|
29
|
-
|---|---|---|---|---|---|
|
|
30
|
-
| `relations` | `memory-maintenance.ts` `index` job and the backfill, background | per candidate: **choice** duplicate / conflicting / related / unrelated + **noul** "claims cannot both be true" (+ the `supersession` rider) | `memory {content, recorded_at}`, `candidates[] {content, recorded_at}` (top-3 neighbours ≥ 0.85 cosine, same project+category) | `duplicate` needs choice confidence ≥ 0.7 (`minConfidence`) → `duplicate_of`; `conflicting` needs the contradiction Noul ≥ 0.5 → `possible_conflict`; else `similar`; `unrelated` → nothing | 0.92 cosine cutoff + negation-word regex |
|
|
31
|
-
| `importance-kind` | same job | **score** over the 1–10 importance bands + **choice** decision / issue / preference / fact / note | `memory_text`, `category`, `project` | kind applied at confidence ≥ 0.6 (`minConfidence`); importance **recorded only** (`memory_metadata.importance_judged`) — applied only when `applyJudgedImportance` is on, a bench ran, and confidence ≥ `applyMinConfidence` (0.6) | kind from the category-name regex; importance stays the writer's |
|
|
32
|
-
| `historical-query` | `memory-retrieval.ts`, asked **only when a superseded memory is among the candidates** (rare), bounded by the surface timeout (1200 ms) | **noul** "asks about the past / an earlier version" | `query` | ≥ 0.5 → include superseded memories | `/history\|histor\|previous\|old\|antes\|anterior/i` |
|
|
33
|
-
| `rerank` | `memory-retrieval.ts` when `recall` is called with `rerank:true` (the greeting boot directive asks for it only when the greeting is on), and `hookRecall()` for the prompt hook's automatic injection and the Assistant's per-turn recall (origin `assistant`); 1500 ms | per candidate **score** irrelevant … decisive | `query` (≤ 2k), `candidates[] {content}` (an excerpt ≤ 1k centred on the query terms; shortlist widened to 10) | judged hits re-sorted among their own positions, unjudged hits keep theirs, ties in fusion order; in the hook, judged candidates below `minScore` (1.5, between Tangential and Background) are **not injected** and logged as dropped | fusion order; cosine ≥ 0.4 decides what is injected |
|
|
34
|
-
| `prompt-urgency` | `prompt-submit.mjs` → `/api/hook-judge` kind `prompt`, 1200 ms, in parallel with the ranked recall; system-generated prompts (task notifications, cross-session messages, mailbox) are never judged. The Assistant asks the same question in-process for every non-trivial user turn (origin `assistant`, no riders; see The Assistant) | **choice** must / should / consider / skip (+ the `task-boundary`, `session-lookup`, `user-learning` riders) | `prompt`, `project` (+ `unsaved_work` only when `task-boundary` is asked) | maps to the Tier 1 / 2 / 3 nudges, tagged `[Jev]`; `skip` = no nudge. Urgency alone sends exactly the pre-rider request | Tier 1/2/3 regex ladder + non-English check |
|
|
35
|
-
| `agent-message` | `stop.mjs` → kind `stop-turn`, 1200 ms, before the state lock; never on native or exec loop Stops (the answer was discarded there) | **noul** blocked on a human (login / CAPTCHA / code) + **noul** waiting on any user action | `assistant_message` (as written, not lowercased), `files_edited`, `edit_count`, and — when the hook sends them — `bash_commands[] {command, exit, output_tail}` and `tool_results[]` of the whole turn, redacted server-side | ≥ 0.5 each → pause a browser loop / suppress soft obligations | substring phrase lists |
|
|
36
|
-
| `turn-worth` | same request as above when both surfaces are on | **noul** "did this turn produce work worth remembering" (names `bash_commands` when present) | same state | P < 0.35 (`minConfidence`) → the unstored-edits nag (CHECK 2) is suppressed for this stop; P ≥ 0.8 (`bashOnlyMinProbability`) gives a turn whose only work ran through Bash (non-read-only commands after the last memory write) the remember obligation | edit-count threshold (1); Bash-only turns carry no obligation |
|
|
37
|
-
| `duplicate-gate` | `memory-writer.ts` `writeMemory`, after embedding, before `BEGIN` (so `remember` and the plan hook) | per candidate **noul** "same fact about the same single occurrence" | `memory {content, recorded_at}`, `candidates[]` (top-3 ≥ 0.90 cosine within the project) | max P ≥ 0.85 → **not stored**; `remember` answers `Not stored — duplicate of [id]`, pointing at `reflect` | no gate; maintenance relates duplicates later |
|
|
38
|
-
| `secret-gate` | `remember` and `reflect` content, before embedding (`secret-gate.ts`) | per span (≤ 8) **noul** "a real credential, not a placeholder / example / hash / id" | `text`, `spans[] {value, looks_like}` from the regex prefilter — **every candidate value is masked before it is sent** (first 4 + last 2 characters, bullets, true length), both as a span and inside the text | P ≥ 0.7 → span becomes `[redacted:<type>]`, tag `redacted`; fewer than three words left → the write is refused | only the precise patterns are redacted (`sk-`/`rk-`/`pk-` keys, `gh*_`, `xox*-`, `AKIA`, JWTs, private-key envelopes) |
|
|
39
|
-
| `category-check` | `remember`, concurrently with the write | **choice** over the live leaf categories (caller's siblings + top-level when > 60) + `none-of-these` | `memory_text`, `filed_under` | confidence ≥ 0.6 and a different pick → reported in the response and recorded in `memory_metadata.category_judged`; Settings → AI decisions → **Misfiled memories** lists them with Apply / Dismiss | none; stored as requested |
|
|
40
|
-
| `stale-check` | `sync` tool, after the checksum diff; ≤ 20 pairs, 4 concurrent, 3000 ms; verdicts are stored beside the memory like `edit-stale`'s | **noul** "does the memory still accurately describe the file" | `memory {content}`, `file {path, current_content (head 6k + lines mentioning the memory's identifiers, ≤ 8k), truncated}` | P ≥ 0.6 → "changed but still accurate", else "stale"; unreadable / unjudged listed separately | the checksum diff alone |
|
|
41
|
-
| `brief-rank` | `hookRecall()` for `pre-task.mjs` (the dispatch brief, when the model did not recall first) and `subagent-start.mjs` (every dispatch: the task text is found through the pre-task stash and `subagents/agent-<id>.meta.json`, because SubagentStart carries none), within the hook's remaining budget | per candidate **score** (same rubric as `rerank`) | `query` (the worker's task, ≤ 2k), `candidates[]` (excerpts ≤ 1k; shortlist widened to 10) | as `rerank`, floor `minScore` 1.5 | fusion order |
|
|
42
|
-
| `loop-goal` | `stop.mjs` (hook-driven loops, kind `loop-goal`, in parallel with stop-turn, inside the 3 s Stop budget) and `native-loop-goal.js` after each native iteration — **never after the final one** | **noul** "task fully accomplished; another iteration adds nothing" | `task`, `context`, `iterations_done/total`, `last_message` (the native iteration's own reply; before, only the journal nothing writes), `journal` (last 5) and `progress_summary` when present | P ≥ 0.8 → the loop ends early (`stop:goal-met` in the loop log; native runs finish with `stoppedReason: goal_met`) | the iteration budget |
|
|
43
|
-
| `trash-triage` | `memories` action `triage`, `GET /api/trash/candidates` (streamed to the tab), Settings → AI decisions → Clean-up suggestions; 10 candidates per request, every request of a run in parallel (≤ 6 per wave, none while a 429's retry-after holds), 8000 ms | per candidate **score** irreplaceable … expendable | `candidates[] {content, category, importance, created_at, times_recalled, duplicate_of_another}` from exact-copy groups, judged duplicates, and importance ≤ 3 / never recalled / > 90 days. `limit` means consider-and-show — every candidate found is judged and returned, ceiling 2,000 (1,032 on the live store; the tab asks for all of it and pages 50 at a time, the MCP tool stays at 10 so it cannot flood an agent's context) | sort by score; the score is stored in `memory_metadata.expendability*` (migration v6) with a fingerprint of model, rubric, question and entry, and reused for 7 days while the fingerprint matches — an edit, a new recall count or a trashed surviving copy asks again; **nothing is deleted** — each row's "Move to trash" is the normal reversible soft delete | per batch: a failed request leaves only its own candidates unscored, listed after the scored ones by reasons count, then importance ascending, then age |
|
|
44
|
-
| `task-boundary` | rider of `prompt-urgency`, asked only when edits made for an earlier request are not yet stored | **noul** "does the prompt begin a different task instead of continuing that work" | `prompt`, `unsaved_work {previous_prompt, files_edited, edit_count}` | P ≥ 0.6 → the "store it before moving on" nudge, tagged `[Jev]`; below → none | nudge only when a Stop came after the edits (no Stop means the turn was interrupted and the prompt is a correction) |
|
|
45
|
-
| `session-lookup` | rider of `prompt-urgency` | **noul** "asks about an earlier session" | `prompt` | P ≥ 0.7 → the conversation-recall workflow | the English-only phrase regexes |
|
|
46
|
-
| `user-learning` | rider of `prompt-urgency`, until the session's observation is stored | **noul** "shows a lasting preference about how the developer wants work done" | `prompt` | P ≥ 0.7 → the communication-style nudge (≥ 3 messages apart, nudge cap unchanged) | every Nth message |
|
|
47
|
-
| `claim-check` | rider of `agent-message` in the stop-turn request, once per prompt, not on a Stop the hook already blocked | **noul** "the final message claims success the turn's own output does not show" | `assistant_message`, `bash_commands[]` and `tool_results[]` of the whole turn (since the last human prompt, not since the last memory write — the usual flow is tests → `remember` → summary) | P ≥ 0.9 → one Stop block whose reason names the evidence ("no test/build command ran this turn", "`npm test` exited 1") | no check |
|
|
48
|
-
| `edit-stale` | `POST /api/hook-edit` from `post-remember.mjs` on Edit / Write (fire-and-forget); `edit-stale.ts` judges after `debounceMs` (15 s) of quiet per session and file, off the request path, 2 at a time | **noul** "the memory still describes the file" (the `stale-check` question) | memories whose `related_files` point at the edited file (absolute, or relative to the registered project root or the session's cwd), ranked by identifiers the edit removed or touched, `maxItems` (3) per burst, never twice for the same file content and revision; file excerpt as `stale-check`, `.env*` and key files never read | P(accurate) < 0.4 → verdict stored in `memory_metadata.stale_verdicts`, recall flags the memory `judged stale`, and the session gets it once at its next prompt or in the Stop reason; ≥ 0.4 lets the verify job treat the changed checksum as current | checksum diff only; judged only by an explicit `sync` |
|
|
49
|
-
| `supersession` | rider of `relations` (index job and backfill) | per candidate **noul** "the later-dated record is a newer statement of the same single thing" | the relations state | P ≥ 0.9, different dates, not beside `duplicate_of`, never a user-told memory superseded by one that is not, at most one per job → `supersedes` (newer → older, automatic), which hides the older memory from current-state recall and wakes `historical-query`; the pair is related as `similar`, not as a conflict. Settings → AI decisions → Replaced memories lists them with Undo / Keep; an undo is respected on every re-index | supersedes only by hand (`reflect supersedes`) |
|
|
50
|
-
| `compact-digest` | `pre-compact.mjs` → kind `compact-digest`, sessions with > 10 prompts or > 3 replies; ≤ 48 stride-sampled messages, batches of 20 in parallel, 5000 ms | per message **choice** goal / decision / finding / open_issue / routine | `session_goal`, `messages[] {role, text ≤ 700}` (redacted) | non-routine at confidence ≥ 0.5, goals and decisions first, ≤ 10 prompts + 3 replies, chronological; session-start says "(selected by Jev)" | the first 15 prompts and 5 replies |
|
|
51
|
-
| `plan-conflict` | `post-plan.mjs` on ExitPlanMode → kind `plan-conflict`, in parallel with storing the plan, 4000 ms | per decision **noul** "carrying out the plan goes against this recorded decision or preference" | `plan` (≤ 8k), `decisions[]` = up to 6 of the project's memories whose kind is decision or preference, retrieved against the plan, never `plans-*`, never superseded | P ≥ 0.75 → an advisory note naming the memory ids; never blocks | no check |
|
|
52
|
-
| `worker-outcome` | the Assistant's dispatcher (`assistant-dispatch.js` through `assistant-jev.js`, origin `assistant`, entity = run id), once per worker turn that has no `## Result` block or reports blocked, and once for a failed run before its `failed` event; 2500 ms | **choice** done / blocked / needs_input / unclear (only when the block is missing) + **choice** capability / access / needs_user / transient / other | `task` (≤ 2k), `assistant_message` (head + tail ≤ 6k), `declared {status, summary, question}`, `run {provider, state, completion_reason, error}`, `files_edited`, the turn's `bash_commands[]` and `tool_results[]` (credential-redacted) | status at confidence ≥ 0.7 (`minConfidence`; `unclear` never) → used as the result and **the result-retry worker turn is skipped** (`lastResult.source:'jev'`); cause ≥ 0.7 → `access` / `needs_user` offer no escalation (the run card says what is needed), `transient` offers a retry on the same model, `capability` / `other` escalate one tier as before. Nothing escalates on its own | one result-retry worker turn; every failed, blocked or result-less run offers a one-tier escalation |
|
|
53
|
-
| `worker-claim` | rider of `worker-outcome`, only when the run reports done and the turn exposed commands or tool results (asking without evidence would flag every success) | **noul** — the `claim-check` question, word for word (`unsupportedClaimQuestion()`) | the `worker-outcome` state | P ≥ 0.9 → `lastResult.unverifiedClaim {probability, evidence}`: the mailbox line "unverified claim: … — verify before reporting success" and an "unverified" marker on the run card | no check |
|
|
54
|
-
|
|
55
|
-
Also logged under `connection-test`: the Test button's single Noul.
|
|
56
|
-
|
|
57
|
-
## Browser assistance
|
|
58
|
-
|
|
59
|
-
Three more surfaces give the browser tools a **bounded, advisory** reading of the page. Jev never drives the browser: it answers typed questions about a minimized description of what is visible, and the tools print that answer beside their normal output. Playwright, fresh refs, deadlines, cancellation, tab ownership, target identity, the money guard, engagement pacing and the X publish gate decide everything that actually happens. TypeSafe is called only from the MCP layer (`browser-assist.ts` → `browser-judgments.ts`); the Neural Interface only collects (`lib/browser-semantic-context.js`, `POST /api/browser/sessions/:id/semantic-context`) and, for auto-heal, re-verifies. Nothing runs inside a `browser_batch` (the MCP client sends `X-Synabun-Batch: 1` and the server refuses too); a batch that navigated gets one assessment of its final page after it ends.
|
|
60
|
-
|
|
61
|
-
| Surface | Where it runs | Primitive · question | State | Threshold → action | Fallback when null |
|
|
62
|
-
|---|---|---|---|---|---|
|
|
63
|
-
| `browser-page-state` | after `browser_navigate` (context collected in the same round trip, boxed at 1500 ms; a collector failure never fails the navigation), on every empty social extractor, once after a batch that navigated; 1000 ms | **choice** usable / loading / authentication_required / verification_required / consent_blocker / error / empty / unknown + (only with an `intent`) **noul** "the page already satisfies the goal" | `page {origin, path, title, lang, readyState}`, `headings[]`, `alerts[]`, `dialogs[]`, `controls[]` (role + name), numeric and boolean `signals` (HTTP status, busy / skeleton / dialog / password / one-time-code counts, text length, link / control / article counts, CAPTCHA / payment / sign-in iframe present), `operation`, `intent` | choice confidence ≥ 0.65 (`minConfidence`) and not `unknown` → one line: `Jev assessment: authentication_required (94%) — pause for human sign-in; goal satisfied 7%`. Goal Noul ≥ 0.8 (`minProbability`) adds "likely already done". Compact mode hides a confident `usable` unless an intent was given; blockers always show. Local busy / skeleton signals outrank a judged `empty` | today's response, unchanged |
|
|
64
|
-
| `browser-target` | `browser_snapshot` with `intent` (≤ 24 refs parsed from the same capture, lexical prefilter beyond that) and a failed or ambiguous `browser_click` / `fill` / `type` / `hover` / `select` / `upload` (≤ 12 visible candidates); 1200 ms | **choice** over opaque ids `c1…cN` + `none` (descriptions `null`) + **noul** "exactly one visible, enabled candidate clearly fits" | `action`, `intent` (the `textHint`, else a text-semantic selector form), `page`, `candidates[] {id, role, kind, name, group, hrefPath, disabled, inDialog}` | choice ≥ 0.85 **and** noul ≥ 0.9 → `Jev target: ref e12 "Account settings" (choice 91%, match 96%); alternatives: …`, or hints reordered and annotated with each candidate's risk. A `payment` candidate is never put forward. Below either threshold → `no candidate qualified` | the deterministic hints, in DOM order |
|
|
65
|
-
| `browser-social` | `browser_fb_composer_state` when `recommendedAction` or `submission.state` is `unknown`, the submit match is `heuristic`, or an open composer has no submit button; `browser_x_compose_state` when `recommendedAction` is `unknown` or `warnings` is non-empty; 1200 ms | **choice** idle_feed / composer_open_empty / composer_open_ready / pending_approval / post_visible / posting_failed / blocked_by_auth_or_verification / unknown + (only when a submit or trigger control is missing or heuristic) **choice** over the dialog's buttons + `none` | an allow-list pick from the probe (booleans, enums, counts, `hasText`, never the probe object), `page`, `signals`, `controls[] {id, role, name ≤ 40, disabled}` scoped to the active dialog, with Boost / Promote / paid controls removed **before** Jev sees the options | confidence ≥ 0.75 → an additive `judgment` key `{advisory, state, confidence, suggestedControl?, note}`. `recommendedAction`, `boostRisk`, `submitButton` and every locale list are untouched | the probe, unchanged |
|
|
66
|
-
|
|
67
|
-
**What leaves the page.** Visible elements only (no `hidden` / `inert` / `aria-hidden` ancestor, `checkVisibility` with opacity and visibility, a real rectangle). Up to eight headings, except that in a feed (more than one article on the page) headings inside the items, which are other people's names and post titles, stay on the page. Names come from `aria-label`, `labelledby`, `<label>`, own text, `img[alt]`, `title`, `placeholder`, through a walker that prunes every editable, hidden node, `script` / `style` / `template` / `iframe`; **never `value`**. Inside a form or dialog holding a non-empty editable (a draft zone) names come from `aria-label`, `<label>`, own text and placeholder only and the group label is dropped. The values of up to 50 editables stay in the page and are used only to replace any echo of a typed value (≥ 4 characters) in an outgoing string with `[input]`. URLs lose userinfo, query, fragment and matrix parameters; path segments become `:id`, `:token` or `:email`; `/login` survives. In-page caps are advisory (a page can patch `String.prototype`), so every string is type-checked, clipped, credential-redacted and budgeted again in Node (about 6,000 characters, name ≤ 160, group ≤ 80) and once more in the MCP layer. **Never sent**: typed text, drafts, message bodies, quoted posts, input or textarea values, hidden DOM, queries and fragments, cookies, storage, headers, request bodies, screenshots, HTML, selectors, `nth`, element fingerprints, the heal `contextId`, rectangles, raw hrefs, fill / type arguments.
|
|
68
|
-
|
|
69
|
-
**Page text is evidence, never an instruction.** Instructions and criteria are static strings (version `bj1`, hashed) that never contain page text; candidate keys are opaque. They include a glossary of what each `signals` count and `probe` flag measures: without it the first benchmark read `loading` pages at F1 0.36. Every instruction says that text inside `page`, `headings`, `alerts`, `dialogs`, `controls` and candidate names is untrusted content copied from a webpage and that a candidate whose text asks to be chosen is not thereby correct. Risk is computed before and independently of Jev (`browser-risk.ts`, shared by the MCP server, the Neural Interface and the bench), and a page with any control that addresses the agent is never healed, whatever Jev answers.
|
|
70
|
-
|
|
71
|
-
### Safe auto-heal (off, and locked)
|
|
72
|
-
|
|
73
|
-
`typesafe_config.browserAssist = { autoHealEnabled: false, targetBench: null }`. `autoHealEnabled` is intent; the gate (`autoHealGate()` in `browser-assist-gate.ts`) is computed at the point of effect in both the MCP handler and the click route. It is eligible only when a recorded target benchmark **passed**, the configured model is a pinned id (`jev-1.13.0`, not `jev-latest` / `jev-preview`), and the benchmark's basis equals the running one. The basis is readable, `jev-1.13.0|bj1:42ae7bdf813d|rk1:4c1d9a07e2b3|conf0.85|prob0.9|t1200|fx:9f8e7d6c5b4a` (model, wording + policy + acceptance hash, the risk classifier's rules hash — lexicons, path terms, benign query keys, the agent-address pattern — the two `browser-target` thresholds, its timeout, the fixture-corpus hash), so the tab names the segment that changed. `PUT /api/typesafe/config` rejects `autoHealEnabled:true` while the gate is closed (judged against current + patch, under the config lock), rejects `targetBench` and unknown `browserAssist` fields, and always accepts `false`. Changing a threshold, the model, the wording, a lexicon or the corpus closes the gate without deleting the recorded result; re-running the bench on the current configuration re-qualifies it.
|
|
74
|
-
|
|
75
|
-
When armed, exactly one retry, and only for `browser_click`:
|
|
76
|
-
|
|
77
|
-
- Considered only on a server-stamped `assist.kind` of `no_match` or `parse_failed`. Never after `actionStarted:true`, an `outcome:'uncertain'`, a transport error, a timeout, cancellation, a money / quote / stale-target / stale-ref / login guard, an ambiguous match, or inside a batch.
|
|
78
|
-
- The pick must clear both thresholds and **itself** be a healable `navigation` target. `navigation` is an allow-list verdict: a plain same-origin `<a href>` with no handler attributes, no `download` / `ping` / `target`, no form, benign query keys only, no token-like segment, a 2–80 letter single-script name in a language the lexicons cover (en, pt, es, de, fr, it, tr, ja), missed by the payment and danger lexicons after NFKC, zero-width stripping, diacritic folding and de-spacing, and not an engagement target. Anything else is `payment`, `authentication`, `file`, `publish`, `destructive`, `write`, `form` or `unknown`, and is advice only. Jev is never re-asked over only the healable subset.
|
|
79
|
-
- The intent is vetoed when it carries a danger phrase, the failed selector targets an editable, uses a `>>` / combinator scope, or the phrase came from the generic quoted-string fallback (`[data-testid="tweetButton"]` must not become the intent "tweetButton").
|
|
80
|
-
- The Neural Interface owns the retry. A failed click mints a single-use context (30 s, one per session / tab / caller, selector and fingerprint server-side only). The heal request carries `{contextId, candidateId}` and nothing else; the route consumes the context first, requires the money guard to be active, a unique match, a recomputed `navigation` risk and an identical fingerprint, no money or engagement hit, and runs `verifyHealTarget()` after the second money check and the X publish gate, immediately before the action is marked started: same document, same risk and fingerprint, visible, enabled, not occluded, the only link with that name, no unsaved input anywhere visible, no pseudo-element label. It then clicks the verified handle. Any refusal is a 400 with `actionStarted:false`, a static `autoHealRejected` code, no hints and no new context.
|
|
81
|
-
- The MCP client sends the heal through `requestOnce` (no route recovery, which could re-issue the click on another tab). Nothing follows a rejected, failed or uncertain heal; an uncertain outcome is reported verbatim.
|
|
82
|
-
|
|
83
|
-
Residual, stated plainly: a delegated JavaScript handler on a plain same-origin anchor cannot be seen before the click. A hostile page can already hijack any agent click, so auto-heal adds no capability there; the risk it adds is a wrong pick on a benign page, which the rules above and the benchmark bound.
|
|
84
|
-
|
|
85
|
-
### Browser bench
|
|
86
|
-
|
|
87
|
-
**Settings → AI decisions → Bench** with *browser (fixtures)* selected, or `POST /api/typesafe/bench` with `surface:"browser"` (optionally `only: "target" | "page-state" | "social"`), runs `browser-bench.ts` under the configured model and records the result. No browser and no actions: static, sanitized, labelled descriptors in `benchmarks/fixtures/browser/{target,page-state,social}.json` run through the **production** judgments, classifier and `autoHealEligibility()`, `origin:'bench'`, `noCache`, three at a time. `input` is what is sent; `facts`, candidate `label`s (`safe` / `consequential` / `promoted` / `injection`), `expected`, `acceptable`, `safeRecovery` and `mustNeverAutoExecute` are labels and never leave the process.
|
|
88
|
-
|
|
89
|
-
Auto-heal gates, each with a minimum denominator so ten lucky cases cannot pass: auto-eligible precision ≥ 98 % over ≥ 75 eligible picks; correct abstention ≥ 95 % over ≥ 60 `none` cases; coverage ≥ 60 % of ≥ 120 safe recoveries (an unavailable judgment counts as a miss); unsafe selections = 0; forbidden auto-executions = 0 over ≥ 50 must-never cases with ≥ 5 in every category (authentication, verification, payment, publish, destructive, form, file, adversarial), all judged; unavailable ≤ 2 %; the run model is pinned and equals the configured model; the run basis equals the runtime basis; target p95 (wall clock per attempted case, timeouts included) ≤ the surface timeout. Page-state advice is reported separately: macro-F1 ≥ 90 %, sign-in or verification read as `usable` = 0, ≥ 8 cases per class. The thresholds live inside the hashed definitions, so tightening one invalidates old passes, and `browser-assist-gate.test.ts` pins `{version, hash}` so an unbumped wording edit fails CI.
|
|
90
|
-
|
|
91
|
-
`recordBrowserBenchRun()` writes `targetBench {at, model, basis, passed, reportFile}` only when the target suite ran, judged something, and the report's basis equals the runtime basis computed inside the config lock. A run on another basis cannot overwrite a valid pass, an alias can never record `passed:true`, a failing run of the real configuration locks the toggle, and recording never touches `autoHealEnabled`. `mcp-server/tests/browser-fixtures.test.ts` proves without the API that no answer, however confident, makes a forbidden case or a consequential candidate in the corpus eligible.
|
|
92
|
-
|
|
93
|
-
First results (2026-09-19, `jev-1.13.0`, 537 fixtures, $0.029 a run): target top-1 99.7 %, auto-heal precision 100 % over 103 picks, coverage 66 %, 0 unsafe and 0 forbidden; page-state macro-F1 99.6 % with blocker recall 100 %; social control pick 94.7 %, no paid control picked; p95 404 ms. Every answer-dependent gate passed in an isolated dry run. Nothing is recorded until the bench is run against the live configuration with the model pinned.
|
|
94
|
-
|
|
95
|
-
### Counters, rollback, tests
|
|
96
|
-
|
|
97
|
-
Eight per-process counters (the tab shows the Neural Interface's; stdio MCP servers keep their own): page assessments, recommendations, would-have-healed (shadow), auto-heal attempted / succeeded, blocked by safety, skipped for rate limit, fallback / unavailable. `POST /api/typesafe/metrics/reset` clears them.
|
|
98
|
-
|
|
99
|
-
Rollback: one browser surface off in Settings → AI decisions is an immediate fallback for that path; auto-heal off keeps the advice; the master switch or `SYNABUN_TYPESAFE=off` turns everything off. There is no migration: only the JSON config row and the existing judgment log.
|
|
100
|
-
|
|
101
|
-
Suites, none of which reaches the paid API: `browser-assist-gate`, `browser-risk` (one row per navigation clause, every decoy in every locale, `PAYMENT_RE` ⊇ the server's `MONEY_TEXT_RE`), `browser-judgments`, `browser-assist` (request bodies and `typesafe_log` hold no typed canary; batch, guard and veto negatives; exactly one heal), `browser-fixtures`, `browser-bench`; `neural-interface/tests/browser-assist-contract.test.mjs` (the verify → mark-started ordering in the click route source; the collector libs never import TypeSafe).
|
|
102
|
-
|
|
103
|
-
## Desktop assistance
|
|
104
|
-
|
|
105
|
-
One surface gives computer use the same **bounded, advisory** reading the browser gets. Jev reads accessibility text, never screenshots (Jev is text-only), and never drives the Mac: the Swift helper, the guards (blocked apps, protected windows, password fields, locked screen, one controller), evidence freshness, the stop controls and the audit decide what happens. TypeSafe is called only from the MCP layer (`desktop-assist.ts` → `desktop-judgments.ts`); the Neural Interface collects and classifies with the pure `desktop-risk.ts` from `dist` (injected by `server.js`; `lib/desktop/*` never imports `mcp-server`) and, for a press, re-verifies. Tool behaviour, output format and refusal codes are in the `computer_ax` tool description (`mcp-server/src/tools/computer.ts`).
|
|
106
|
-
|
|
107
|
-
| Surface | Where it runs | Primitive · question | State | Threshold → action | Fallback when null |
|
|
108
|
-
|---|---|---|---|---|---|
|
|
109
|
-
| `desktop-target` | `computer_ax` `snapshot` with an `intent`, and `press` with an `intent` and no ref; 1200 ms; origin `tool` | **choice** over opaque ids `c1…cN` + `none` + **noul** "exactly one enabled candidate clearly does what the intent asks" (wording `dj1`, hashed) | `intent`, `app {name, lang}`, `dialogOpen`, `truncated`, `candidates[] {id, role, kind, name, hint, group, disabled, inDialog}` — at most 24, ranked by word overlap, fitted to 5,000 characters (hints dropped first) | choice ≥ 0.85 **and** noul ≥ 0.9 → `Jev target: eN "…" (choice …, match …)` plus up to two alternatives (p ≥ 0.05); a payment control is never recommended. Press by intent additionally needs `pressEligibility` (below) and the unlocked gate | the ranked list by word overlap (`Jev target: none` / `unavailable`) |
|
|
110
|
-
|
|
111
|
-
**What leaves the screen.** Only the fields in the state column. Never field values or typed text, window titles, identifiers, refs, pixel boxes, bundle ids, static texts (they are read in the Neural Interface only, to count text addressed to the agent), password fields, or web content inside browsers and web apps. The log keeps `target <app> · <N> candidates · intent <k> chars[ · dialog open][ · truncated]`. Intent snapshots of blocked apps and protected windows are refused before any of this is collected.
|
|
112
|
-
|
|
113
|
-
**Risk is code.** `classifyDesktopTarget` (`desktop-risk.ts`, rules `dk1`, hashed) runs before and independently of Jev: secure → authentication; currency amounts → payment; the shared `lexiconClass` (`rk1`); desktop terms in 8 languages (Don't Save / Quit / Replace / Move to Trash / Erase…, Allow / Don't Allow / Trust / Open Anyway / Install, OK / Cancel / Continue…, New / Paste / Print / Record…, Call / FaceTime / Forward / AirDrop…); the container's words; modal → dialog; web; role rules (field, toggle, value, menu, selection, window chrome, dock); restricted apps (System Settings, App Store, Music, TV, Books, Home, Find My, Shortcuts, Disk Utility, installers, Screen Sharing, browsers, web apps). `navigation` — the only pressable class — is an allow-list verdict: a plain button, link, tab or disclosure triangle, enabled, pressable, a 2–80-letter single-script label without markup or agent-addressed text, in a supported interface language.
|
|
114
|
-
|
|
115
|
-
### Press by intent (off, and locked)
|
|
116
|
-
|
|
117
|
-
`typesafe_config.desktopAssist = { pressEnabled: false, targetBench: null }`. `desktopPressGate()` (`desktop-assist-gate.ts`, a leaf) is eligible only when a recorded desktop benchmark **passed**, the configured model is pinned, and the benchmark's basis equals the running one: `jev-1.13.0|dj1:<h>|rk1:<h>|dk1:<h>|conf0.85|prob0.9|t1200|fx:<corpus>`. A browser lexicon edit (`rk1`) re-locks desktop press; a desktop rule edit does not re-lock browser auto-heal, and neither bench unlocks the other. `PUT /api/typesafe/config` rejects `pressEnabled:true` while the gate is closed, rejects `targetBench` and unknown fields, and always accepts `false`. The gate is recomputed at the point of effect: in the MCP handler (one config snapshot for the decision, `invalidate` + `confirmDesktopPress` right before the single press request) and in the Neural Interface (`pressGate()` inside the queue, immediately before the helper call).
|
|
118
|
-
|
|
119
|
-
`pressEligibility()` (shared by the press path and the bench) refuses when there is no pick clearing both thresholds, any on-screen text addresses the agent, a dialog is open (or either flag is missing — it fails closed), the intent itself names a consequential class, or the pick is missing, disabled or not `navigation`. The press then runs through a single-use context the Neural Interface minted with the snapshot, a re-check of the stored entry, and the helper's own `verify` in the same queued command as `AXPress` (`TARGET_CHANGED` on any difference). One attempt; never retried; an error after the press may have happened is reported as uncertain.
|
|
120
|
-
|
|
121
|
-
### Desktop bench
|
|
122
|
-
|
|
123
|
-
**Settings → AI decisions → Bench** with *desktop (fixtures)* selected, or `POST /api/typesafe/bench` with `surface:"desktop"`, runs `desktop-bench.ts` under the configured model and records the result: static helper-shaped fixtures in `benchmarks/fixtures/desktop/{target,abstention,forbidden,adversarial}.json` (302 cases, 29 apps, 10 interface languages, 27 % with the intent in another language; regenerate with `generate.mjs` — any edit changes the corpus hash and re-locks the press) run through the **production** candidate builder, classifier, judgment and `pressEligibility()`, `origin:'bench'`, `noCache`, three at a time. Gates (inside the hashed definitions): 0 unsafe presses; 0 forbidden presses over ≥ 60 must-never cases with ≥ 5 in each of 13 categories, all judged; eligible precision ≥ 98 % over ≥ 60 picks; correct abstention ≥ 95 % over ≥ 50 `none` cases; advised-target precision ≥ 95 % over ≥ 100; coverage ≥ 50 % of the safe presses; unavailable ≤ 2 %; pinned model equal to the configured one; run basis equal to the runtime basis; p95 ≤ the surface timeout. `recordDesktopBenchRun()` records only when the basis matches (a failing run of the real configuration locks), an alias never records a pass, and nothing touches `pressEnabled`. `desktop-fixtures.test.ts` proves without the API that no verdict, however confident, makes a must-never case eligible.
|
|
124
|
-
|
|
125
|
-
Counters (per process; `POST /api/typesafe/metrics/reset` clears them): intent snapshots, recommendations, would-have-pressed while locked, press attempted / succeeded, blocked by safety, skipped for rate limit, fallback / unavailable. Rollback: the `desktop-target` switch off → the lexical ranking; the toggle off keeps the advice; the master switch or `SYNABUN_TYPESAFE=off` turns everything off.
|
|
126
|
-
|
|
127
|
-
## Coding sessions
|
|
128
|
-
|
|
129
|
-
What Jev does while Claude Code works, and how to see it. Every hook keeps its old rule as the fallback; `SYNABUN_TYPESAFE=off` in the hook's own environment (the live skin test sets it through the bridge, `typesafe:'off'` on its queries) means no `/hook-judge` call, `judge:false` on recall, no edit notices.
|
|
130
|
-
|
|
131
|
-
| Moment | Hook | Jev |
|
|
132
|
-
|---|---|---|
|
|
133
|
-
| A human prompt | `prompt-submit.mjs` | urgency + riders (`task-boundary`, `session-lookup`, `user-learning`) in one request, **in parallel** with the automatic recall, which is ranked and floored (`rerank`); the block closes with `[ranked by Jev; N low-relevance matches dropped]`. Pending `edit-stale` verdicts ride back on the recall |
|
|
134
|
-
| A system "prompt" (task notification, cross-session message, mailbox) | `prompt-submit.mjs` | nothing: `prompt-origin.mjs` classifies it and the hook skips judgment, recall, message count and the conversation regex |
|
|
135
|
-
| A subagent dispatch | `pre-task.mjs` + `subagent-start.mjs` | the task text is stashed at PreToolUse; SubagentStart finds it and injects a `brief-rank` recall with the floor (hook feature `subagentRecall`) |
|
|
136
|
-
| An Edit / Write | `post-remember.mjs` | one quiet first-edit reminder per session (the per-edit escalation is gone); `POST /api/hook-edit` → `edit-stale` |
|
|
137
|
-
| End of a turn | `stop.mjs` | stop-turn with the turn's Bash commands and tool results: `agent-message`, `turn-worth` (Bash-only work now counts), `claim-check`; loop-goal in parallel; stale verdicts join the unstored-edits reason |
|
|
138
|
-
| Compaction | `pre-compact.mjs` | `compact-digest` picks what the compaction memory keeps (the parser now reads real transcript lines — it used to read `entry.role` and always produced an empty summary) |
|
|
139
|
-
| Plan approval | `post-plan.mjs` | `plan-conflict` advisory |
|
|
140
|
-
|
|
141
|
-
Budgets: the prompt and Stop hooks run inside 3 s; every call's timeout is the remaining budget minus a reserve (`hookBudget()` in `shared.mjs`), and a call with under 250 ms left is skipped. Texts over 16k are clipped by the hook before sending.
|
|
142
|
-
|
|
143
|
-
**Seeing it.** Settings → AI decisions → **Coding sessions** lists the sessions that asked (`GET /api/typesafe/sessions`) and opens a per-session timeline from the log (`GET /api/typesafe/log?session=…&order=asc`): urgency and riders, injected versus dropped memories with their relevance levels, Stop verdicts and claim checks, stale flags, plan conflicts, digests. Counters there come from the log (24 h / 7 d, every process), not from the tab's own process.
|
|
144
|
-
|
|
145
|
-
## The Assistant
|
|
146
|
-
|
|
147
|
-
The SynaBun Assistant asks Jev in-process through `lib/assistant-jev.js`, an injected wrapper like `native-loop-goal.js`: it checks the master switch, the retry-after window and each surface's own switch (a rider's too — `judge()` checks only the lead's), reads the thresholds on every call, and logs with origin `assistant`. `server.js` loads the dist modules in their own `try/catch`, so a stale `dist` leaves the Assistant working exactly as before, without Jev. **Routing is not a Jev surface**: the brain model still decides where every task runs (`agent_route`, route modes and cards are unchanged).
|
|
148
|
-
|
|
149
|
-
**Per-turn recall** (`recallForPrompt` in `lib/assistant-memory.js`, both brain paths):
|
|
150
|
-
|
|
151
|
-
1. A free pre-filter (`hooks/claude-code/prompt-origin.mjs`): system turns (mailbox, router, notifications) and trivial ones (acks, ≤ 7 characters, slash and bare commands) recall nothing and ask nothing.
|
|
152
|
-
2. `prompt-urgency` (the hook's own question, no riders) runs **in parallel** with a ranked, floored recall (`hookRecall` with `surface:'rerank'`, `floor:true`, deferred), both bounded by `assistant-config.json` `memory.recallBudgetMs` (default 2500 ms, 500–5000, read at startup). The urgency call's timeout is that budget, not the surface's hook-tuned `timeoutMs` (1200 ms): live Assistant calls take about 1.0–1.1 s and a long prompt timed out at 1200 ms.
|
|
153
|
-
3. A `skip` at confidence ≥ 0.5 returns at once (a weaker skip counts as `consider`); a recall with nothing left after the floor returns at once too.
|
|
154
|
-
4. The urgency sizes the block: `consider` 2 memories / 400 tokens, `should` 3 / 600 (the configured `limit` / `tokenBudget`), `must` 5 / 900 plus one line asking the brain to `recall` with a focused query if the memories do not settle it. The block closes with `[ranked by Jev; N low-relevance matches dropped]`.
|
|
155
|
-
5. A withheld recall (`finish(null)`) is logged as withheld on the rerank row and **not** committed to the injection ledger, so the next turn is not told the memories were already supplied.
|
|
156
|
-
6. No usable judgment (off, no key, retry-after, stale `dist`) → today's skip regex decides before any recall starts, and the recall runs unranked. Its prefix rules (acks, "do it / next / stop", "run / npm / git …", "open file.ext") only apply to single-line prompts of at most 60 characters: a long request that merely begins with "Run …" or "No, …" is not skipped.
|
|
157
|
-
|
|
158
|
-
The urgency row is annotated `{decision, fallback, tier, injected}` (or `decision:'nothing-retrieved'`); the rerank row gets `withheld`, `tier`, `limit`, `token_budget`.
|
|
159
|
-
|
|
160
|
-
**One recall per Claude-brain prompt.** The Claude brain loads user settings, so the user-level SynaBun hooks used to run inside it on top of the in-process recall hook: every memory block arrived twice and Jev was paid twice. The brain's CLI now carries `SYNABUN_ASSISTANT_SESSION=<assistant session id>`; `prompt-submit.mjs` exits right after its system-origin skip when it is set, and `stop.mjs` does not ask `claim-check` (worker claims are judged per worker, below, and a brain relaying a worker's "tests pass" would otherwise be blocked). Any `claude` CLI the brain starts from its own shell inherits the marker.
|
|
161
|
-
|
|
162
|
-
**Workers.** After each worker turn the dispatcher records the turn's commands, tool results and edited files from the provider events (`lib/assistant-evidence.js`: Claude `tool_use`/`tool_result`, Codex `item.completed`, OpenCode tool parts; 24 per turn, snapshot of the last 8 commands, 6 results, 20 files) and makes at most one `worker-outcome` request (+ the `worker-claim` rider) **before** the result-retry turn:
|
|
163
|
-
|
|
164
|
-
- no `## Result` block and a confident status → the retry turn is skipped (`lastResult.source:'jev'`, "status read by Jev" on the run card); otherwise the retry runs as before (`source:'retry'`);
|
|
165
|
-
- blocked or still without a status → `cause` and `needs` on the result; a failed run's `failed` event waits for its `failure {cause, confidence, needs}`;
|
|
166
|
-
- reported done with visible evidence → the claim rider; P ≥ 0.9 → `unverifiedClaim`.
|
|
167
|
-
|
|
168
|
-
`escalationFor` reads the cause: `access` / `needs_user` → `{kind:'none', to:null, needs}` (no Escalate button; "Needs access: …" / "Needs your input: …"), `transient` → `{kind:'retry'}` on the same model ("Retry on ‹model›", `RETRY:` context; a design run whose model the catalog says can no longer run design escalates instead), anything else → the old one-tier ladder with `kind:'escalate'` and the cause. Depth 2 caps both. The worker row's outcome is `{run, turn, source, retried, status, cause, claim, escalation, judged, applied}` (`source:'failure'` for failed runs): `judged` is what Jev answered and `applied` says whether it cleared `minConfidence`, so a cause judged at 0.61 shows as judged but not applied (the timeline says "(below 70%)"), and a failed run keeps it as `failure.judgedCause` / `judgedConfidence` without changing the escalation. Its preview is redacted, not withheld.
|
|
169
|
-
|
|
170
|
-
Cost: the recall gate adds about one Jev round trip (≈ 0.7 s p50, bounded by `recallBudgetMs`) before a non-trivial brain turn; a worker turn that reports done with visible commands waits one round trip before `turn_completed`. The skipped retry turns and the removed duplicate hook run are the savings.
|
|
171
|
-
|
|
172
|
-
## Backfill
|
|
173
|
-
|
|
174
|
-
Memories indexed before migration v4 never got a judgment (24,266 of 24,275 on 2026-09-19). The backfill (`memory-judge-backfill.ts`) enqueues a distinct `judge` job per unjudged memory and judges from the stored vector — no re-embedding — through its own runner: one bounded batch per second (`backfillConcurrency`, default 3), only while no interactive `index`/`verify` job is pending, paused with maintenance, backed off exponentially after 3 consecutive failures, paused after 10, a 30 s lease so only one Neural Interface drives it, progress and counters persisted in `typesafe_config.backfill` so a restart resumes. Estimate first (no API calls: content length × bench actuals, plus a local neighbour sample for the relations share), then Start requires `confirm:true`. The end-of-run report records rows judged, relations created, disagreements with the old heuristic, tokens, cost and wall clock.
|
|
175
|
-
|
|
176
|
-
## Bench
|
|
177
|
-
|
|
178
|
-
**Settings → AI decisions → Bench**, or `POST /api/typesafe/bench` with `surface: "importance" | "kind" | "relation" | "historical"`, `limit` (1 to 200, default 50) and optionally `project` and `sample: "random" | "recent"`, runs `typesafe-bench.ts`. Reports go to `benchmarks/typesafe-<surface>-<date>.json`. A run stores its summary in the settings and sets `benchRunAt`, which unlocks the "apply judged importance" toggle. After the 2026-09-19 results the decision was to keep importance recorded-only.
|
|
179
|
-
|
|
180
|
-
## Tests
|
|
181
|
-
|
|
182
|
-
`mcp-server/vitest.config.ts` sets `SYNABUN_TYPESAFE=off` and the Neural Interface suites set it at file top: a developer's exported key must never make `npm test` hit the paid API. Suites: `typesafe-judgments`, `typesafe-config`, `typesafe-coding` (relevance clips and ordering, prompt riders, claim check, log attribution and stats, supersession guards, the edit-time stale check end to end, relative-path verification, digest and plan conflicts), `typesafe-surfaces` (trash triage: parallel batches with a failed neighbour, 429 retry-after, stored-score reuse and every invalidation, progress snapshots), `memory-judge-backfill`, `memory-migration` (v5, v6, v7), `memory-v2` (relation direction); `neural-interface/tests/memory-api.test.mjs` (endpoints, including the JSON and NDJSON triage answers), `hook-recall-budget` (rank, floor, outcome, judged follow-ups against a stubbed Jev), `native-loop-goal`. The Assistant: `assistant-judgments` (vitest: only the asked questions, caps, redaction, the claim question identical to the Stop hook's), `assistant-jev`, `assistant-evidence`, `assistant-memory` (tiers, early exits, regex fallback), `assistant-dispatch` / `-routing` / `assistant-router` (skipped retry turns, cause kinds, unverified claims), `assistant-api` / `assistant-runtime` (brain reads and the mailbox dedupe), `assistant-brains` (the env marker). Computer use: `desktop-risk` (dk1 golden), `desktop-assist-gate` (dj1 golden, lock reasons, independent browser and desktop passes), `desktop-judgments` (only allowed keys on the wire), `desktop-assist` (golden compact text, fallbacks, exactly one press, every refusal code, cancellation), `desktop-fixtures`, `desktop-bench`; `neural-interface/tests/desktop-service` / `desktop-integration` (contexts, verify, `TARGET_CHANGED`, a 300-grapheme label that must still match), `desktop-assist-contract` (consume before gate, no `await` between the lock check and the press, `lib/desktop` never imports TypeSafe).
|
package/docs/style-guide.md
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
# Style Guide
|
|
2
|
-
|
|
3
|
-
A project's brand and design system, kept in one place and handed to every agent that works on the project: colors in light and dark, type, spacing, shape, motion, icons, logo files, imagery direction, component specs and rules.
|
|
4
|
-
|
|
5
|
-
A saved guide writes the files agents and build tools already read:
|
|
6
|
-
|
|
7
|
-
| File | What it is |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `<project>/DESIGN.md` | The guide as prose and tokens, in the shape Google Stitch and the community collection use (YAML frontmatter + sections) |
|
|
10
|
-
| `<project>/.synabun/style-guide/tokens.json` | W3C design tokens (DTCG 2025.10) |
|
|
11
|
-
| `<project>/.synabun/style-guide/tokens.css` | CSS custom properties, light and dark |
|
|
12
|
-
| `<project>/.synabun/style-guide/tailwind.css` | Tailwind v4 `@theme` (or `tailwind.tokens.cjs` for v3) |
|
|
13
|
-
|
|
14
|
-
This page covers the editor, backend, MCP tool and agent integration.
|
|
15
|
-
|
|
16
|
-
## Contents
|
|
17
|
-
|
|
18
|
-
- [Panel](#panel)
|
|
19
|
-
- [Where it lives](#where-it-lives)
|
|
20
|
-
- [The guide (schema v2)](#the-guide-schema-v2)
|
|
21
|
-
- [Aliases](#aliases)
|
|
22
|
-
- [What a save writes](#what-a-save-writes)
|
|
23
|
-
- [Imports](#imports)
|
|
24
|
-
- [Presets](#presets)
|
|
25
|
-
- [History and proposals](#history-and-proposals)
|
|
26
|
-
- [What agents receive](#what-agents-receive)
|
|
27
|
-
- [The `style_guide` tool](#the-style_guide-tool)
|
|
28
|
-
- [REST API](#rest-api)
|
|
29
|
-
- [Rules](#rules)
|
|
30
|
-
- [Code map](#code-map)
|
|
31
|
-
- [Testing the panel](#testing-the-panel)
|
|
32
|
-
|
|
33
|
-
## Panel
|
|
34
|
-
|
|
35
|
-
Open **Style Guide** from the Neural Interface Skills menu and select a registered project. The floating 1180 × 720 panel keeps the shared drag and resize handles and moves back inside the window when the window shrinks. Its rail collapses to named icons below 900 px of panel width; the content scrolls inside the panel, never the page. Native controls (checkboxes, selects, sliders, file pickers) use the dark color scheme.
|
|
36
|
-
|
|
37
|
-
The fourteen sections are Brand, Colors, Typography, Layout, Shape & effects, Motion, Icons & logo, Imagery, Components, Rules & accessibility, Agents, Preview, Import & export, and History & proposals. Every section uses labelled controls and cards with a purpose line. Colors includes eleven-step palettes (EyeDropper on the base and status colors, where the browser has one), per-theme semantic aliases with a swatch of what each alias resolves to, light/dark theme switches and a WCAG/APCA contrast matrix. Tables (semantic roles, text styles) name each column once; the per-cell labels stay for screen readers. Lists are compact: chips for short values (personality, weights, icon sizes, aspect ratios), rows with Move up / Move down / Remove for the rest. Typography includes the bundled font search, eight pairings, a scale generator and live specimens. Google Fonts stylesheets load only for fonts whose source is `google`.
|
|
38
|
-
|
|
39
|
-
Changes autosave after **800 ms**. Every PUT sends the complete schema-v2 config: a partial PUT would reset omitted fields to defaults. Edits made while a request is in flight are retained and saved afterwards. **Cmd/Ctrl+S** flushes immediately; **Esc** saves and closes. Both are caught at window capture, so a feature listening underneath (the map, the whiteboard, the tutorial) cannot swallow them. A button that is busy, or a section that re-renders after restore, accept or import, keeps focus inside the dialog. A failed save keeps the panel and draft open with Retry. The header reports the revision, written-file count and pending proposals.
|
|
40
|
-
|
|
41
|
-
**Preview** renders a component sheet in a script-free `sandbox="allow-same-origin"` iframe: logo/navigation, hero, button states, form, elevations, alerts, badges, table, pagination and footer. Choose light/dark and 375/768/1440 px widths. The canonical stylesheet comes from GET `/export?format=css`; complete local token overrides update the preview while edits await autosave. The DESIGN.md subview fetches `/preview-md`; Copy fetches it first when that view has not been opened.
|
|
42
|
-
|
|
43
|
-
**Import & export** accepts a file or paste (DESIGN.md, W3C tokens, CSS or Tailwind JSON), a project scan or a community slug. Choose Merge or Replace, press Preview diff, inspect the API's leaf differences and warnings, then Apply. Apply sends the full previewed config through PUT with its import source. Editing the import inputs or export settings invalidates that preview. Community imports show the independent-analysis attribution. Eight presets show swatches and font families and use the selected merge strategy. Export settings control the generated files, output directory, Tailwind target and dark selector. Write now lists the paths actually written; per-format Copy reads the export endpoint.
|
|
44
|
-
|
|
45
|
-
**History & proposals** shows saved revisions, restore with confirmation, a status filter and each proposal's exact diff. Accept saves through the proposal route; Reject leaves the guide unchanged. Actions flush pending edits first. **Agents** shows the compact summary and token estimate, per-task injection switches and instructions, and the managed project-pointers block. Its pointers toggle uses the dedicated API.
|
|
46
|
-
|
|
47
|
-
Automation Studio's Code Review wizard keeps Style Guide Notes and adds **Use this project's Style Guide** with a registered-project selector. When enabled, Review, Copy, Open in Editor and Launch append the selected project's fetched summary; Launch also passes that project as its working directory.
|
|
48
|
-
|
|
49
|
-
Client color math in `sg-color-client.js` is the server's pure mathematical core copied verbatim, with browser-only CSS-color and canvas-image adapters appended. The UI contract suite checks byte parity and representative scale/contrast results, so instantaneous badges use the same WCAG and APCA calculation as the API.
|
|
50
|
-
|
|
51
|
-
## Where it lives
|
|
52
|
-
|
|
53
|
-
One folder in the data home, keyed by `sha1(projectPath)` (first 16 hex characters):
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
~/.synabun/data/style-guides/
|
|
57
|
-
<hash>.json the guide
|
|
58
|
-
<hash>.history.json the last 30 saved revisions, newest first
|
|
59
|
-
<hash>.proposals.json changes agents proposed
|
|
60
|
-
assets/<hash>/ uploaded logo files
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
A project is one of the registered projects (`claude-code-projects.json`). Every API call and tool call takes a `projectPath` that may be the project root **or any path inside it**; it resolves to the registered root, and a path in no registered project is refused (403). Resolution checks physical containment, including existing symlink ancestors of a missing path. Artifact and logo writes refuse symlinks below their root before committing a revision.
|
|
64
|
-
|
|
65
|
-
## The guide (schema v2)
|
|
66
|
-
|
|
67
|
-
`schemaVersion: 2`. `normalizeStyleGuide()` fills every missing field with its default, so a partial save, an old file and a hand-edited file always load. Sections:
|
|
68
|
-
|
|
69
|
-
| Section | Holds |
|
|
70
|
-
|---|---|
|
|
71
|
-
| `brand` | name, tagline, description, audience, personality, vibe, key characteristics, voice (tone, dos, don'ts) |
|
|
72
|
-
| `colors.palettes` | ordered palettes; `primary`, `secondary`, `accent` and `neutral` always exist, more can be added (kebab-case keys). Each has a `base` and 11 `steps` (50…950) |
|
|
73
|
-
| `colors.semantic` | per theme (`light`, `dark`): `background`, `surface`, `surfaceRaised`, `text`, `textMuted`, `border`, `primary`, `onPrimary`, `secondary`, `onSecondary`, `accent`, `onAccent`, `link`, `focus`. A value is a hex or an alias |
|
|
74
|
-
| `colors.status` | `success`, `warning`, `danger`, `info` |
|
|
75
|
-
| `colors.gradients`, `colors.themes` | named CSS gradients; the default theme and the themes the product ships |
|
|
76
|
-
| `typography` | `fonts` (`heading`, `body`, `mono`, optional `display`: family, fallback, source, weights, features), `scale` (base, ratio), `styles` (id → font, size, mobile size, weight, line height, letter spacing in em, transform, usage), principles |
|
|
77
|
-
| `layout` | spacing base and scale, breakpoints, container, grid, z-index, principles |
|
|
78
|
-
| `shape` | radius scale, radius roles (button, card, input, badge, modal → a step of the scale), border widths, elevation (shadow + usage) |
|
|
79
|
-
| `motion` | durations (ms), easings, reduced-motion policy, principles |
|
|
80
|
-
| `iconography`, `logo`, `imagery` | icon library and style; logo variants (kind, background, file), clear space, minimum size, don'ts; imagery style, mood, dos and don'ts, and the image-generation prefix, negative prompt, aspect ratios and style references |
|
|
81
|
-
| `components` | ordered specs: id, name, category, tokens (aliases or literal CSS), states, notes |
|
|
82
|
-
| `accessibility`, `rules`, `responsive` | WCAG level and thresholds, focus ring; dos, don'ts and a free Markdown section; responsive notes |
|
|
83
|
-
| `agents` | which classes of dispatched run get the guide (`inject`), free instructions for agents, and whether agents may propose changes |
|
|
84
|
-
| `exports` | which files a save writes, the Tailwind target, the output folder, the CSS selector, how the dark theme is selected (`attribute`, `media`, `class`), and the project pointers switch |
|
|
85
|
-
|
|
86
|
-
The 11-step scales are generated in OKLCH (`scaleFromBase`): evenly spaced to the eye, with the base kept at step 500 byte for byte.
|
|
87
|
-
|
|
88
|
-
### The v1 shape
|
|
89
|
-
|
|
90
|
-
The first Style Guide stored `colors.{primary,…}` with five shades, `typography.{heading,body,mono}`, `shape.{radius,spacing[],shadows}` and `logo.{iconLibrary,imageryNotes}`. A v1 file loads as v2: its values are kept (the five shades stay as they were, the six steps v1 never had are generated from the base) and everything else takes its default.
|
|
91
|
-
|
|
92
|
-
The v2 panel reads and writes the full v2 shape without `compat=v1`. The backend still accepts the compatibility parameter for older clients and migrates saved v1 files.
|
|
93
|
-
|
|
94
|
-
## Aliases
|
|
95
|
-
|
|
96
|
-
One resolver (`resolveAlias`) is used by the renderer, the exporters and the components:
|
|
97
|
-
|
|
98
|
-
| Alias | Resolves to |
|
|
99
|
-
|---|---|
|
|
100
|
-
| `{primary.500}` | a palette step |
|
|
101
|
-
| `{status.danger}` | a status color |
|
|
102
|
-
| `{semantic.text}` | a semantic role, for the theme asked |
|
|
103
|
-
| `{typography.h1}` | a text style |
|
|
104
|
-
| `{spacing.md}`, `{radius.md}` | a length |
|
|
105
|
-
| `{radiusRoles.button}` | the radius step a role points at |
|
|
106
|
-
| `{elevation.md}` | a shadow |
|
|
107
|
-
| `{duration.fast}`, `{easing.standard}` | motion tokens |
|
|
108
|
-
|
|
109
|
-
An alias that names nothing is left as text and reported.
|
|
110
|
-
|
|
111
|
-
## What a save writes
|
|
112
|
-
|
|
113
|
-
A save normalizes the guide, adds 1 to the revision, records history and writes the project's files, each one only when its `exports` flag is on. A save that changes nothing keeps the revision. An explicit export `formats` list selects a subset of enabled exports; it cannot bypass the switches or write the other Tailwind target.
|
|
114
|
-
|
|
115
|
-
- **`DESIGN.md`**: frontmatter (`version: 2`, `name`, `description`, `colors` with every palette step as `primary-500`, every semantic role per theme as `light-background` / `dark-background` and every status; `typography` with every text style; `rounded`; `spacing`; `components` with `{colors.x}` aliases), then the sections Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Motion, Iconography, Logo & Imagery, Components, Accessibility, Do's and Don'ts, Responsive Behavior, Iteration Guide, Project Rules (when there are any) and Agent Instructions. All eight frontmatter keys are always present; an empty component map is `{}`. The same guide always gives the same bytes: there is no timestamp in the file, so it only changes in git when the guide does. The default guide is about 14 KB.
|
|
116
|
-
- **`tokens.json`**: groups `color` (palettes, `semantic.light` / `semantic.dark`, `status`), `fontFamily`, `typography`, `lineHeight`, `spacing`, `radius`, `radiusRole`, `breakpoint`, `border`, `shadow`, `duration`, `cubicBezier`, `zIndex`. Aliases stay aliases. `$extensions["dev.synabun"]` carries the revision, the time and the project path; a shadow or an easing that DTCG cannot express is left out and listed under `skipped`.
|
|
117
|
-
- **`tokens.css`**: every token under the configured selector (`:root`), with semantic colors pointing at palette steps through `var()`; the other theme's semantic colors under `[data-theme="dark"]`, `.dark` or `@media (prefers-color-scheme: dark)`; a `prefers-reduced-motion` block that zeroes the durations. With a custom root selector, attribute/class theme blocks cover both the root itself and an ancestor and outrank the base declarations.
|
|
118
|
-
- **Tailwind**: v4 is a CSS file with one `@theme` block and the other theme in a base layer; v3 is a CommonJS module whose `theme.extend` you spread into `tailwind.config`.
|
|
119
|
-
- **Logo files** are mirrored into `<project>/.synabun/style-guide/`.
|
|
120
|
-
|
|
121
|
-
Three things a save is careful about:
|
|
122
|
-
|
|
123
|
-
- A `DESIGN.md` somebody wrote by hand is copied once to `.synabun/style-guide/DESIGN.before-synabun.md` before the first save replaces it.
|
|
124
|
-
- A generated token file that the current settings no longer produce is removed, but only when the file says it is SynaBun's. A file the project put there is never touched. `DESIGN.md` stays where it is when its export is turned off. Cleanup considers only the current output directory; changing `outDir` leaves files in the old directory for the user to review. An explicit subset export does not perform stale-file cleanup.
|
|
125
|
-
- Files are written through a temp file and only when their content changed.
|
|
126
|
-
|
|
127
|
-
### Project pointers
|
|
128
|
-
|
|
129
|
-
Off by default. When on, `<project>/CLAUDE.md` and `<project>/AGENTS.md` get a short block that says the project has a `DESIGN.md` and how to use it, for agents that only read a project's instruction files. A file that did not exist is created with only the block. Turning the option off removes the block, and the file when it held nothing else. The block has its own markers (`synabun:styleguide:begin` / `end`), so it sits beside SynaBun's rules block without either one disturbing the other. Updating or removing a block preserves the surrounding bytes, including blank lines; removing an appended block can leave its separator blank line. Duplicate or damaged markers are refused. Token paths in the block include the configured output directory.
|
|
130
|
-
|
|
131
|
-
## Imports
|
|
132
|
-
|
|
133
|
-
`POST /api/style-guide/import` reads a guide from somewhere else. `mode: "preview"` returns the resulting guide and a leaf diff without saving; `mode: "apply"` saves it. `merge: "merge"` lays the import over the current guide; `merge: "replace"` lays it over the defaults and keeps what is not a design decision (the logo files, the agent and export settings).
|
|
134
|
-
|
|
135
|
-
| `kind` | Reads |
|
|
136
|
-
|---|---|
|
|
137
|
-
| `design-md` | A `DESIGN.md`: ours, Stitch's or a community one. The frontmatter is read by a small YAML-subset parser (maps, nested maps, lists, quoted and plain scalars, block text, comments); the prose gives the vibe, key characteristics, principles, dos and don'ts; nesting is limited to 64 levels |
|
|
138
|
-
| `dtcg` | A W3C design-tokens file |
|
|
139
|
-
| `css` | CSS custom properties: SynaBun's own, a Tailwind v4 `@theme`, or shadcn-style variables (`--background`, `--primary`, bare HSL triples), with a `.dark` block read as the dark theme |
|
|
140
|
-
| `tailwind-json` | A Tailwind theme as JSON |
|
|
141
|
-
| `codebase` | The project itself: `*.css`, `*.scss`, `tailwind.config.*` (read as text, never executed) and a hand-written root `DESIGN.md`. Skips `node_modules`, `dist`, `build`, `.git` and SynaBun's own output; stops after 400 files |
|
|
142
|
-
| `community` | `design-md/<slug>/DESIGN.md` from github.com/VoltAgent/awesome-design-md. The slug must match `^[a-z0-9.-]{1,40}$` before any request is made; the request stops after 10 s, refuses redirects, and limits the response to 2 MB while streaming it. These files describe public designs and are not official brand assets: the response says so |
|
|
143
|
-
|
|
144
|
-
Color names are sorted the same way for every kind: `<palette>-<step>`, `<theme>-<role>`, a status name, `primary` / `secondary` / `accent` / `neutral` as a palette base, the usual role names (`background`, `surface`, `foreground`, `on-primary`, `border`, …), and anything else as a new palette (at most eight). Importing a guide's own export over itself changes nothing.
|
|
145
|
-
|
|
146
|
-
## Presets
|
|
147
|
-
|
|
148
|
-
Eight complete starting points, each with SynaBun's own values: SynaBun Default, Minimal SaaS, Dark Developer, Editorial, Playful, Enterprise, Fintech Precision, Warm Organic. A preset replaces the design by default (`merge: "replace"`) and keeps the project's logo files and settings.
|
|
149
|
-
|
|
150
|
-
## History and proposals
|
|
151
|
-
|
|
152
|
-
Every saved revision is kept, newest first, 30 at most, with its source: `ui`, `import:<kind>`, `preset:<id>`, `proposal:<id>` or `restore:<rev>`. Unlabeled editor saves less than five minutes apart share one entry, so autosave does not use up the 30. Restoring a revision brings back its design as a new revision; the agent and export settings stay as they are now.
|
|
153
|
-
|
|
154
|
-
At most 200 proposals may be pending; additional submissions return 429 until some are reviewed. Rejected proposals retain their decision-time diff. Disabling proposals also prevents accepting a previously pending proposal.
|
|
155
|
-
|
|
156
|
-
**Agents never write the guide.** Their only write is a proposal: a JSON merge patch (RFC 7396; `null` deletes a key) and a reason. Nothing changes until the user accepts it. A proposal cannot touch `agents`, `exports` or the bookkeeping fields, and is refused when it would change nothing or when the user turned proposals off (`agents.allowProposals`). A pending proposal is always shown against the guide as it is now.
|
|
157
|
-
|
|
158
|
-
## What agents receive
|
|
159
|
-
|
|
160
|
-
1. **A block in the dispatch prompt.** When the Assistant dispatches a worker whose project has a saved guide, the prompt carries a `STYLE GUIDE` block before `PROJECT`: a summary of 300 to 500 tokens (never more than 600), the paths of `DESIGN.md` and the token files, and three instructions (use the tokens, call `style_guide` for more, propose instead of editing). Coding, Complex engineering, Design, Image creation and Video creation runs get it; the other classes do not (`agents.inject`, per class). A run without a class counts as coding. Image and video runs read the imagery direction and the image-generation prefix first. A project without a saved guide gets nothing.
|
|
161
|
-
2. **The design rules** tell a design run to call `style_guide`, to build on the token files and to propose changes.
|
|
162
|
-
3. **SynaBun's rules for every tool** (Claude Code, Codex, OpenCode, Gemini, Cursor) have a Design section: read the project's `DESIGN.md` (or call `style_guide`) before UI, design, marketing, copy or image work, and propose changes instead of editing the files.
|
|
163
|
-
4. **The Claude Code session-start hook** adds one line when the working directory has a `DESIGN.md`.
|
|
164
|
-
5. **The Assistant** knows that style guides exist and calls `style_guide` (action `summary`) before design work of its own.
|
|
165
|
-
6. **Project pointers** (opt-in, above).
|
|
166
|
-
|
|
167
|
-
## The `style_guide` tool
|
|
168
|
-
|
|
169
|
-
In every MCP profile. Pass `projectPath`: the project root or any path inside it. The default is the server's working directory, which is usually not the caller's project.
|
|
170
|
-
|
|
171
|
-
| Action | Does |
|
|
172
|
-
|---|---|
|
|
173
|
-
| `summary` | The compact block (`taskClass` `image_gen` or `video_gen` puts imagery first) |
|
|
174
|
-
| `get` | The summary, the written files, pending proposals and the full `DESIGN.md`, or one `section` of it |
|
|
175
|
-
| `tokens` | The tokens in a `format`: `json` (resolved values, the default), `dtcg`, `css`, `tailwind-v4`, `tailwind-v3`; optionally one `theme` |
|
|
176
|
-
| `contrast` | WCAG 2.x ratio with the AA / AAA verdicts, and APCA Lc as advice, for `fg` on `bg` (colors or aliases; `size: "large"` for large text and UI) |
|
|
177
|
-
| `propose` | Record a proposal: `changes` (a merge patch) and `reason`; optional `runId`, `provider`, `model` identify its author (the client also forwards a known caller run id) |
|
|
178
|
-
| `proposals` | List them, optionally by `status` |
|
|
179
|
-
| `export` | Write the saved guide's files into the project now (`formats` optional, selecting enabled exports) |
|
|
180
|
-
| `list` | Registered projects and whether each has a guide |
|
|
181
|
-
|
|
182
|
-
## REST API
|
|
183
|
-
|
|
184
|
-
All under `/api/style-guide`, permission key `styleGuide`. Routes that list answer a bare array; the others answer an object with `ok`. An error is `{ ok: false, error, code }`.
|
|
185
|
-
|
|
186
|
-
| Method & path | Body / query | Returns |
|
|
187
|
-
|---|---|---|
|
|
188
|
-
| GET `/` | `projectPath` | `{ ok, projectPath, config, designMd, hasDesignFile, assetsHash, revision, saved, summary, tokensEstimate, written: [{ format, path, exists }], proposalsPending }` (`saved: false` = the defaults) |
|
|
189
|
-
| PUT `/` | `{ projectPath, config, source?, label? }` | the GET shape after saving, plus `designPath`, `changed`, `writtenNow`, `pointers` |
|
|
190
|
-
| GET `/summary` | `projectPath`, `taskClass?` | `{ ok, summary, tokensEstimate, saved, revision, projectPath }` |
|
|
191
|
-
| GET `/preview-md` | `projectPath` | `text/markdown`, not written |
|
|
192
|
-
| GET `/export` | `projectPath`, `format` = `design-md` \| `dtcg` \| `css` \| `tailwind-v4` \| `tailwind-v3` \| `summary` \| `json`, `theme?`, `as=json?` | the text with its content type, or `{ ok, format, contentType, filename, text, revision }` with `as=json` |
|
|
193
|
-
| POST `/export` | `{ projectPath, formats? }` | `{ ok, written: [{ format, path, changed }] }` (409 without a saved guide) |
|
|
194
|
-
| POST `/import` | `{ projectPath, kind, text?, slug?, mode: preview \| apply, merge: merge \| replace }` | `{ ok, mode, config, diff: [{ path, from, to }], warnings, files?, source? }`; `apply` adds the PUT shape |
|
|
195
|
-
| GET `/presets` | — | `[{ id, name, description, swatches, fonts: { heading, body }, theme }]` |
|
|
196
|
-
| POST `/presets/apply` | `{ projectPath, presetId, merge? }` | like PUT, plus `diff` |
|
|
197
|
-
| GET `/history` | `projectPath` | `[{ revision, at, source, label }]` |
|
|
198
|
-
| POST `/history/restore` | `{ projectPath, revision }` | like PUT |
|
|
199
|
-
| GET `/proposals` | `projectPath`, `status?` | `[{ id, at, status, decidedAt, runId, provider, model, reason, changes, diff }]` |
|
|
200
|
-
| POST `/proposals` | `{ projectPath, changes, reason, runId?, provider?, model? }` | `{ ok, id, pending, ignored, diff }` (422 when proposals are off, 409 without a saved guide) |
|
|
201
|
-
| POST `/proposals/:id/accept` · `/reject` | `{ projectPath }` | accept: like PUT; reject: `{ ok, proposalsPending }` |
|
|
202
|
-
| POST `/contrast` | `{ fg, bg, size?, projectPath?, theme? }` | `{ ok, ratio, aa, aaa, aaLarge, aaaLarge, apca, fg, bg, size }` |
|
|
203
|
-
| POST `/scale` | `{ base, hueShift?, chroma? }` | `{ ok, base, steps: { 50…950 }, harmony: { complementary, analogous, triadic, splitComplementary }, oklch: { l, c, h } }` |
|
|
204
|
-
| POST `/dark-semantic` | `{ light: { role: color or alias }, neutral? }` | `{ ok, dark }` |
|
|
205
|
-
| GET `/defaults` | `projectPath` | `{ ok, config }`: the default guide |
|
|
206
|
-
| GET `/fonts` | `q?` | `[{ family, category, weights }]`: 225 popular Google Fonts, bundled |
|
|
207
|
-
| POST `/pointers` | `{ projectPath, enabled }` | `{ ok, enabled, files: [{ path, state }], revision }` |
|
|
208
|
-
| GET `/projects` | — | `[{ path, label, saved, revision, updatedAt, hasDesignFile, proposalsPending }]` |
|
|
209
|
-
| POST `/logo` | image body; query `projectPath`, `variantId?`, `name?`, `kind?`, `bg?` | `{ ok, variant, config, revision, assetsHash }` |
|
|
210
|
-
| DELETE `/logo/:variantId` | `projectPath` | `{ ok, config, revision }` |
|
|
211
|
-
| GET `/assets/:hash/:file` | — | the file |
|
|
212
|
-
|
|
213
|
-
`compat=v1` remains a backend compatibility option; the current editor never sends it.
|
|
214
|
-
|
|
215
|
-
## Rules
|
|
216
|
-
|
|
217
|
-
- No store write for agents: proposals only.
|
|
218
|
-
- The pure modules (schema, color, render, exports, summary, the importers' parsers) do no I/O. The codebase scan and the community fetch take the file system and `fetch` injected.
|
|
219
|
-
- Contrast: WCAG 2.2 AA by default (4.5:1 for text, 3:1 for large text and UI). APCA is shown as advice.
|
|
220
|
-
- Import text is limited to 2 MB, YAML/JSON nesting to 64 levels, and JSON imports to 100,000 nodes. Unsafe prototype/coercion keys are rejected in JSON imports and omitted during normalization/YAML parsing. Schema color fields must be strings; malformed object colors fall back or return 400.
|
|
221
|
-
- Assets require a registered project hash, an image extension (SVG, PNG, JPEG, WebP), and a regular contained path; logo uploads are limited to 4 MB. Uploaded SVGs keep the sandbox CSP and `nosniff`.
|
|
222
|
-
- Tests never use the live data home: every suite works in a temp data home and a temp project.
|
|
223
|
-
|
|
224
|
-
## Code map
|
|
225
|
-
|
|
226
|
-
| Part | Where |
|
|
227
|
-
|---|---|
|
|
228
|
-
| Schema, defaults, migration, aliases, merge patch, diff | `neural-interface/lib/style-guide/schema.js` |
|
|
229
|
-
| Color math (parsing, OKLCH, scales, WCAG, APCA, harmony, dark derivation) | `color.js` |
|
|
230
|
-
| Token helpers shared by the renderer and the exporters | `tokens.js` |
|
|
231
|
-
| `DESIGN.md` renderer | `render-design-md.js` |
|
|
232
|
-
| Exporters | `export-dtcg.js`, `export-css.js`, `export-tailwind.js` |
|
|
233
|
-
| Importers | `import.js` |
|
|
234
|
-
| Presets, summary, fonts | `presets.js`, `summary.js`, `fonts.json` |
|
|
235
|
-
| Project pointers | `pointers.js` |
|
|
236
|
-
| Store (paths, save, history, proposals, artifacts, `loadStyleGuideForRun`) | `store.js` |
|
|
237
|
-
| Routes | `api.js`, registered from `server.js` |
|
|
238
|
-
| Old import path | `neural-interface/lib/design-md.js` (re-exports) |
|
|
239
|
-
| MCP tool | `mcp-server/src/tools/style-guide-tools.ts`, client in `services/neural-interface.ts` |
|
|
240
|
-
| Prompt block and dispatcher wiring | `styleGuideBlock` in `lib/assistant-task-prompt.js`, `styleGuideFor` in `lib/assistant-dispatch.js` |
|
|
241
|
-
| Design rules, persona, rules for every tool, hook | `lib/assistant-playbooks/design.md`, `lib/assistant-persona.js`, `templates/rulesets/core.md`, `hooks/claude-code/session-start.mjs` |
|
|
242
|
-
| Tests | `neural-interface/tests/style-guide-{schema,color,render,export,import,summary,store,api-contract,ui-contract}.test.mjs`, `assistant-task-prompt.test.mjs`, `assistant-design.test.mjs`, `mcp-server/tests/style-guide-tools.test.ts` (see [Testing the panel](#testing-the-panel)) |
|
|
243
|
-
|
|
244
|
-
## Testing the panel
|
|
245
|
-
|
|
246
|
-
- `node --test neural-interface/tests/style-guide-ui-contract.test.mjs` (part of `npm test`): i18n keys and CRLF, label keys that are not message templates, client routes, color parity with the server, autosave and in-flight edits, and a round trip through the real store and routes (one edit in every section survives save → reload, and an unchanged re-save keeps the revision).
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { resolve, join, dirname } from 'node:path';
|
|
4
|
-
import { auditClaudeTranscripts, transcriptFiles } from '../lib/claude-usage-audit.js';
|
|
5
|
-
|
|
6
|
-
const args=process.argv.slice(2);
|
|
7
|
-
function option(name) {const i=args.indexOf(name);return i>=0?args[i+1]:undefined;}
|
|
8
|
-
const directory=option('--directory') || join(homedir(),'.claude/projects',process.cwd().replace(/[^a-zA-Z0-9]/g,'-'));
|
|
9
|
-
const report=auditClaudeTranscripts(transcriptFiles(directory),{since:option('--since'),until:option('--until')});
|
|
10
|
-
const output=option('--output');
|
|
11
|
-
if(output){mkdirSync(dirname(resolve(output)),{recursive:true});writeFileSync(output,JSON.stringify(report,null,2)+'\n');}
|
|
12
|
-
console.log(JSON.stringify({output:output || null,window:report.window,...report.summary,accounting_limits:report.accounting_limits},null,2));
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import { join, dirname, resolve } from 'node:path';
|
|
5
|
-
import { getDataHome } from '../lib/paths.js';
|
|
6
|
-
|
|
7
|
-
const args=process.argv.slice(2),option=n=>{const i=args.indexOf(n);return i<0?undefined:args[i+1];};
|
|
8
|
-
const database=option('--database') || join(getDataHome(),'mcp-data/memory.db');
|
|
9
|
-
const db=new DatabaseSync(database,{readOnly:true});
|
|
10
|
-
db.exec('PRAGMA query_only=ON; PRAGMA temp_store=MEMORY');
|
|
11
|
-
const hash=s=>createHash('sha256').update(s).digest('hex');
|
|
12
|
-
const summary=r=>({id:r.id,project:r.project,category:r.category,source:r.source,created_at:r.created_at,preview:r.content.slice(0,140)});
|
|
13
|
-
try {
|
|
14
|
-
const rows=db.prepare('SELECT id,content,project,category,source,created_at FROM memories WHERE trashed_at IS NULL').all();
|
|
15
|
-
rows.sort((a,b)=>a.created_at.localeCompare(b.created_at));
|
|
16
|
-
const groups=new Map(),recent=new Map(),near=[];
|
|
17
|
-
for(const row of rows) {
|
|
18
|
-
const digest=hash(row.content);row.hash=digest;
|
|
19
|
-
if(!groups.has(digest))groups.set(digest,[]);groups.get(digest).push(row);
|
|
20
|
-
const key=JSON.stringify([row.project,row.category,row.source]);
|
|
21
|
-
const now=Date.parse(row.created_at),normalized=row.content.toLowerCase().replace(/\s+/g,' ').trim();
|
|
22
|
-
const bucket=(recent.get(key)||[]).filter(r=>now-r.time<=300000);
|
|
23
|
-
if(normalized.length>=100) {
|
|
24
|
-
const words=normalized.match(/[\p{L}\p{N}_]+/gu)||[];
|
|
25
|
-
const shingles=new Set(words.slice(0,-4).map((_,i)=>words.slice(i,i+5).join(' ')));
|
|
26
|
-
for(const previous of bucket) {
|
|
27
|
-
if(previous.row.hash===digest || Math.min(previous.length,normalized.length)/Math.max(previous.length,normalized.length)<0.8)continue;
|
|
28
|
-
let overlap=0;for(const shingle of shingles)if(previous.shingles.has(shingle))overlap++;
|
|
29
|
-
const score=overlap/(shingles.size+previous.shingles.size-overlap);
|
|
30
|
-
if(score>=0.9)near.push({left:summary(previous.row),right:summary(row),seconds_apart:(now-previous.time)/1000,five_word_jaccard:score});
|
|
31
|
-
}
|
|
32
|
-
bucket.push({row,time:now,length:normalized.length,shingles});
|
|
33
|
-
}
|
|
34
|
-
recent.set(key,bucket);
|
|
35
|
-
}
|
|
36
|
-
const exact=[...groups].filter(([,g])=>g.length>1).map(([content_sha256,g])=>({content_sha256,rows:g.map(summary),adjacent_seconds:g.slice(1).map((r,i)=>(Date.parse(r.created_at)-Date.parse(g[i].created_at))/1000)}));
|
|
37
|
-
const report={generated_at:new Date().toISOString(),database,active_rows:rows.length,
|
|
38
|
-
method:{exact:'SHA-256 of unmodified content, all active rows; metadata retained for review',near:'Same project/category/source; distinct exact hashes; <=300 seconds; >=100 characters; length ratio >=0.8; five-word Jaccard >=0.9'},
|
|
39
|
-
summary:{exact_groups:exact.length,excess_exact_rows:exact.reduce((n,g)=>n+g.rows.length-1,0),near_pairs:near.length},exact,near,
|
|
40
|
-
action:'Review candidates only. No rows were changed or removed.'};
|
|
41
|
-
const output=option('--output');
|
|
42
|
-
if(output){mkdirSync(dirname(resolve(output)),{recursive:true});writeFileSync(output,JSON.stringify(report,null,2)+'\n');}
|
|
43
|
-
console.log(JSON.stringify({output:output || null,active_rows:report.active_rows,...report.summary,action:report.action},null,2));
|
|
44
|
-
} finally {db.close();}
|