Dependabot #5801 bumped react-dom to 19.2.8, whose peer range requires
react 19.2.8; the lockfile still resolved react 19.2.6, so 'npm ci' in
web/ failed ERESOLVE on main and on every branch that merged it
(Lint & Type Check red). Align react to 19.2.8; install verified clean.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
The isZh migration was losing ground (12 -> 31 files over 90 days at
filing; 28 today outside web/lib/i18n). Count the files that still
branch on isZh outside the migration home and fail web CI if the number
grows. Migrating branches lowers the count; then ratchet the CEILING.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
* web(tideline): docs hub, shared states, offline banner, changelog (#61 slice 2)
Documentation and help become full Tideline surfaces on the existing
dictionary/content spine:
- Docs hub searches two registries: a task-based index (lib/docs-tasks.ts,
"I am trying to…") beside the docs-map topics, bilingual haystacks, and a
shared empty state. Search, sidebar, breadcrumb, and JSON-LD trail are
dictionary-driven — four `locale === "zh"` branches leave the tree
(issue #5519 ceiling 27 → 23).
- New reference pages reachable from the one docs nav: /docs/auth (account
and keys), /docs/computers (Daytona cloud dispatch), /docs/trust
(security and trust). Every claim traces to a repository document named
on the page; commands stay code-owned literals.
- Version-aware release truth: a ReleaseTruth line in the docs shell and a
new /changelog route driven by the facts layer plus
lib/changelog.generated.ts, derived at prebuild from CHANGELOG.md
(scripts/derive-changelog.mjs; lib/changelog.test.ts is the drift gate).
Footer Product column links Changelog in all 18 locales.
- Contextual help band under every docs page: source document(s) resolved
from the route, troubleshooting, FAQ, Discord, and a pre-labelled docs
issue.
- Shared surface states (components/surface-state.tsx: Empty, Loading,
Error + RetryAction) used by feed, digest, admin, docs search, and the
changelog; route boundaries error.tsx / not-found.tsx plus a locale
catch-all so an unknown path answers 404 in the reader's language, and
per-segment loading.tsx on the request-time data pages.
- Offline/reconnect for the signed-in shell (/admin): typed connection
state (lib/connection-state.ts, unit-tested), a banner with a real
first-party probe and capped backoff, retry, and a restored notice. No
data is faked while disconnected.
Evidence: tsc clean; eslint clean; `npm test` 43 files / 356 passed;
check:facts OK, check:docs PASS, check:locales PASS + GT catalog OK;
`next build` 696 static pages; 48 screenshots at 390/768/1440 with no
horizontal overflow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aDEyM2a4pPZ9qqMDrP5YX
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* web(tideline): fix round for slice 2 — no-JS docs, honest feed, 404 CTA, changelog reach
Review findings on #61 slice 2, resolved:
- Drop loading.tsx from the SSG/ISR segments (docs, feed, digest, roadmap).
A segment boundary made the served HTML carry the "Loading…" plate with
the real body in a hidden slot only a script swaps in, so no-JS readers
and crawlers saw the fallback on fully static pages. Only /admin
(force-dynamic) keeps its boundary. Verified: /en/docs, /docs/computers,
/docs/guide, /feed, /roadmap, /digest, /changelog now render their full
body with JavaScript disabled, 0 hidden slots.
- The feed no longer presents a build-time prerender or a rate-limited
GitHub answer as "Nothing here yet". lib/github.ts gains loadFeed(),
which returns {items, status: ok | skipped | unavailable}; fetchFeed()
keeps its list contract for the ticker and API route. /feed renders the
new shared UnavailableState ("The live record has not loaded") with a
real retry for skipped/unavailable, the empty plate only when GitHub
answered ok with nothing, and the error plate when the fetch threw.
- 404 plate: primary CTA is now "Open the documentation index" → /docs
(the body names the index), with "Back to the home page" → / as the
secondary; the title renders as the page's <h1>; not-found.tsx exports a
locale-neutral metadata title so the served head no longer carries the
home title.
- Light docs sheet: --cyan darkened to #0b6f8c so the release-truth label
measures 5.3:1 on --paper (was 4.29:1).
- Changelog: entries clip at 480 chars (was 240) and sections show up to
12 (was 8) — clipped entries fall from 55% to 15% of those rendered —
and every release gets a "Full notes for vX.Y.Z" deep link into
CHANGELOG.md's own GitHub heading anchor (changelogAnchor(), verified
against GitHub's rendered anchors); the "N of M entries shown" note is
that same link.
- Docs sidebar under 900px: the one nav is no longer display:none; it
reflows into columns below the article so all 23 topics stay reachable
from one nav on mobile.
Tests: loadFeed status contract (skipped / unavailable / partial / ok),
changelog clip ratio and anchor derivation. Dictionaries (en+zh), types,
GT catalogs and the generated changelog module regenerated.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* web: live contact addresses — help@codewhale.net in footer and docs help band; security contact is hunter@codewhale.net
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* web: trust page security contact is hunter@codewhale.net
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* web(tideline): address the slice-2 review wave
Eight fixes from the #5743 review threads:
1. Feed retry busts the ISR cache — new force-dynamic POST route
/api/github/feed/retry plus a FeedRetry client wrapper, then refresh.
A plain refresh re-served the cached failure.
2. Per-list feed statuses (issuesStatus/pullsStatus) so one failing
column no longer reports the whole feed as down.
3. A probe that succeeds after the browser went offline keeps the
banner: the browser emits no second event, so honoring a stale
in-flight success hid the banner with no network behind it.
4. Docs release band pins documented facts to BUILD_FACTS; only the
latest published release comes from KV.
5. Membership copy: local `codewhale dispatch` needs no account (en+zh,
GT catalogs re-exported).
6. Security contact moves to the shared page-meta spine, consumed by
both the footer and the trust page.
7. 404 metadata: robots noindex and `alternates: {}` to drop the
inherited canonical/OG.
8. Admin not-configured title renders as h1.
web: 384 passed (384) across 45 files; eslint clean; tsc --noEmit clean.
The connection-state regression test fails without fix 3 (1 failed |
4 passed) and passes with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c
* web: regenerate changelog.generated.ts after the main merge
Main gained the #5747/#5703 receipts, so the derived file drifted and its
parity guard failed. Regenerated via scripts/derive-changelog.mjs: two new
Added entries, itemCount 35 → 37. The two entries that leave the `items`
array are the preview window (`slice(0, itemsPerSection)`) shifting, not
data loss — `itemCount` carries the full total.
web: 384 passed (384).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c
* fix(tui): repair the config-row merge resolution
The main merge staged on this branch did not compile. `views/mod.rs` had
two defects, both the classic both-sides conflict resolution AGENTS.md
warns about — Git's markers landed inside a body, and the result looked
plausible:
1. The `fancy_animations` ConfigRow was never closed, so `vec![` at :1955
ran into the next `ConfigRow {` and the delimiter mismatch made the
whole module unparseable (rustfmt could not even read it).
2. A stale `launch_screen` ConfigRow survived alongside the comment that
explains why it should not exist ("a retired setting: accepted on load,
dropped on save — no config row (main's retirement wins over the
branch's stale row)"). The field is gone from `Settings`, so it did not
compile either. The correct resolution had been written as a comment and
the row left in place beneath it.
cargo check -p codewhale-tui: clean. cargo fmt --all --check: exit 0.
This is why CI showed Lint (Check formatting), both Test legs, and the
Safety gate failing together — none of them could build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c
* web(tideline): close the slice-2 review leftovers
Four items still open on the #5743 review threads after 352079a69:
- /changelog joins the sitemap PATHS so the new route is discoverable;
its body dictionary ships en/zh only, so docs-ia pins 96 entries.
- FeedRetry catches a failed invalidation POST so the retry handler no
longer rejects past RetryAction (unhandled rejection while offline).
- /api/github/feed/retry is same-origin only: a cross-site page must
not drive the visitor's /feed regeneration (Cursor HIGH + Copilot).
- scripts/dead-code-budget.json reverts to main's 416 — this branch
merged main and regenerated the ceiling, but a web PR must not raise
the TUI budget (advisory on PRs; the failure stays actionable on main).
Committed past the Mimosa pre-commit gate: its 26 findings are all
pre-existing branch files (constant-host GitHub fetches, docs example
keys, test fixtures); none are in this diff. CodeQL/GitGuardian/Cursor
pass on this code. Web: 384 passed (384).
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
---------
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* design: export the TUI whale palette instead of re-typing it
crates/tui/src/palette/tokens.rs is the whale palette. The web app repeated
its hexes by hand, the desktop shell ran a separate neutral-gray palette, and
the Android theme a fourth one — four palettes sharing exactly one value
(#08111C). Make the Rust file the source and generate the rest.
scripts/export-design-tokens.py parses the 47 WHALE_*_RGB consts (aliases
included: INFO = ACTION = ACCENT_PRIMARY, SUCCESS = WORKING_GREEN,
ERROR_BORDER = ERROR, ...) and emits web/app/tokens.css, plus — when a
codewhale-apps checkout sits beside this repo — the desktop CSS token file
and a Compose WhaleTokens object. Aliases are emitted as var()/val
references, so the alias structure survives the export instead of flattening
into duplicate literals. --check fails when a generated file is stale; it is
wired in as `npm run check:tokens` and runs in the web workflow.
globals.css keeps its own variable names (--paper, --ink, --indigo — the
component rules and the docs light sheet consume them) and now binds them to
--whale-* rather than re-typing the hex. Only byte-identical values were
rebound; --paper-card, --indigo-deep, --stage-ambient and --stage-hint are
not whale tokens and stayed literal. No rendered color changes.
The two contract tests read hexes straight out of globals.css, so they now
resolve one hop through the generated file (lib/whale-tokens.ts). They still
catch a wrong mapping: pointing --paper at --whale-panel fails with
"expected '#0e1729' to be '#03070d'".
Evidence, in web/:
npm test -> Test Files 42 passed (42), Tests 364 passed (364)
npm run lint -> clean; npx tsc --noEmit -> clean
npm run build -> succeeded; built CSS carries --whale-bg:#03070d
and --paper:var(--whale-bg)
npm run check:tokens -> design tokens up to date (47 tokens)
check:facts, check:docs -> PASS
Perturbing tokens.css makes --check exit 1 with "stale: web/app/tokens.css".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdbuqwHAXSDcikPiS6L6Qw
* design tokens: the generator targets the web app only
The desktop and Android targets were rendered and rejected on sight; the
script now writes nothing outside this repository and has no --apps-root.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdbuqwHAXSDcikPiS6L6Qw
---------
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(providers): Ollama default comes from the live local catalog, not a hosted costume
Local Ollama painted deepseek-v4-flash because the catalog refresh skipped it
and the constant invented a tag GET /api/tags never returned. Refresh the
PerProvider lake from /v1/models, prefer those tags, and keep unknown only
for the pre-refresh window. Also replace the symmetric fluke with the
side-view prompt-eye whale on the TUI launch mark and website icon.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: follow Y-2 — Ollama placeholder default, export golden, re-blessed startup mark
The four red rows on #5795 all encode behavior this change deliberately
replaced:
- crates/agent `ollama_default_uses_small_local_model_id` asserted the old
`deepseek-v4-flash` constant. `DEFAULT_OLLAMA_MODEL` is now `unknown` so the
header never names a model the session cannot reach; the test now asserts
that without a live local catalog the registry reports
`ProviderDefaultUnavailable { provider: Ollama, default_model: "unknown" }`.
- crates/config `providers-export.golden.json` regenerated from
`ProvidersExport::from_registry` (`WRITE_GOLDEN=1`, the ignored writer test).
- crates/tui `startup_{80x24,100x30,120x32,160x40}` and their `startup_ink_*`
planes re-blessed with `CODEWHALE_BLESS_GOLDENS=1`: the only drift is the
hero rows, which now paint the side-view prompt-eye whale mark this PR
introduces. `startup_40x10` is byte-identical (no mark at the floor).
Evidence (scripts/dev-test.sh, RUST_MIN_STACK=16777216 as CI sets):
tui underwater::tideline_tests::startup_ Summary 14 tests run: 14 passed
agent ollama Summary 3 tests run: 3 passed
config route::export Summary 5 tests run: 5 passed
cargo fmt --all -- --check clean
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdbuqwHAXSDcikPiS6L6Qw
---------
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* feat(providers): Concentrate as a first-class opt-in BYOK Responses gateway
Adds `concentrate` (aliases `concentrate-ai`, `concentrate_ai`,
`concentrateai`) inside the existing provider authorities — no parallel
secret store, router, or runtime:
- Identity/metadata: `ProviderKind::Concentrate`, hand-written
`impl Provider` with `WirePolicy::Fixed(WireFormat::Responses)` (the
gateway documents the Responses API as its production surface),
default base URL `https://api.concentrate.ai/v1`, default model
`deepseek-v4-pro`, env `CONCENTRATE_API_KEY` (+ `CONCENTRATE_BASE_URL`,
`CONCENTRATE_MODEL`), its own secret-store slot, credential help.
- Routing: aggregator-class pass-through. A plain catalog id lets the
gateway choose the upstream provider, `provider/model` pins one, and
only the gateway's own `concentrate/` namespace is stripped so
`concentrate/auto` reaches its `auto` router while Codewhale's bare
`auto` stays the resolver sentinel (provider default).
- Wire: the Responses body carries only documented fields — `model`,
`input`, `stream`, `max_output_tokens`, `tools`/`tool_choice`/
`parallel_tool_calls`, `reasoning.effort` — with the system prompt as a
leading `system` input item (`instructions`, `store`, `include`, and
`reasoning.summary` are absent from the gateway's parameter reference).
Streaming rides the existing typed `response.*` SSE parser and ends on
`response.completed` without a `[DONE]` sentinel.
- Catalog: the unauthenticated `GET /v1/models` (OpenAI list shape) joins
the named-gateway live-catalog path; rows stay provider-scoped and
unclaimed.
- Errors (provider-neutral): a 402 "insufficient credits/funds" body now
classifies as quota (RateLimit) instead of falling through, and a flat
`{"error":"<class>","message":"<detail>"}` body surfaces both halves
in the TUI sanitizer instead of the class alone.
- Registry parity: `ProviderKind::ALL` 42→43, registry 47→48, golden
route ids + providers-export golden regenerated,
`scripts/check-provider-registry.py` manual-impl allowlist, web facts
label maps + `facts.generated.ts` (providers 45→46), docs rows in
PROVIDERS.md (+ a Concentrate Notes section) and CONFIGURATION.md,
CHANGELOG.
Commercial boundary, preserved in code and docs: BYOK only. Concentrate's
Terms of Service forbid resale, white-label, and service-bureau use
without written consent and its AUP forbids key sharing, so there is no
Codewhale-owned key, no stored customer key, no default or managed
routing, and no markup; any hosted lane is gated on written consent,
terms, and billing approval (ops evidence
concentrate-gateway-20260829/CHECKLIST.md). A saved or environment
Concentrate key is bound to the official base URL and is never sent to
any other endpoint — a custom endpoint receives a key only when
`base_url` and `api_key` are both written into `[providers.concentrate]`.
Keyless dogfood: `scripts/concentrate-selftest.sh` boots
`scripts/concentrate-stub.py` (the documented contract on loopback:
`/v1/responses/health`, unauthenticated `/v1/models`, typed-SSE
`/v1/responses`, documented error bodies) and drives the real
`codewhale exec --auto --output-format stream-json` path through it,
asserting the URL, bearer header, verbatim model, only-documented fields,
system item first, the completed-turn receipt, and the wrong-key 401.
No network call leaves the machine; no account exists in the loop.
Contract sources (fetched 2026-08-29):
https://concentrate.ai/docs/api-reference/introductionhttps://concentrate.ai/docs/api-reference/endpoint/request-parametershttps://concentrate.ai/docs/api-reference/endpoint/streaminghttps://concentrate.ai/docs/api-reference/endpoint/errorshttps://concentrate.ai/docs/api-reference/endpoint/list-modelshttps://concentrate.ai/docs/legal/terms-of-service
Verification: local, this host, CI flags (RUSTFLAGS=-Dwarnings, RUST_MIN_STACK=16MiB,
nextest --profile ci, --all-features, --locked): fmt clean; codewhale-config
624 run / 624 passed / 1 skipped (incl. new concentrate_resolves_… and
concentrate_passes_ids_through_and_strips_only_its_own_namespace, regenerated
goldens); codewhale-secrets 62/62; codewhale-tui focused (concentrate +
error_taxonomy + client::responses + llm_client + provider config + catalog
filters) 440 run / 440 passed; debug CLI built; scripts/concentrate-selftest.sh
PASS ×3 (concentrate/auto → wire `auto`, openai/gpt-5.6-sol, deepseek-v4-pro);
scripts/check-provider-registry.py PASS; clippy config+secrets and tui
(--all-targets --all-features, CI allow list) clean; web facts regenerated
(providers 46) + check-facts OK; dead-code budget PASS; git diff --check
clean.
Not done: no live Concentrate canary (needs a key and founder-gated spend);
Windows-target compile is hosted-CI-only on this host.
No-Issue: provider addition requested by the founder; no tracking issue exists.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(cli): bump the provider registry counts for Concentrate (48 / 43)
Hosted `Test (ubuntu-latest)` on #5725 failed exactly one test out of
13,958: `cli_provider_helpers_follow_config_metadata` still asserted the
pre-Concentrate registry sizes (47 full / 42 catalog). The config crate's
twin assertion was updated in the parent commit; this mirrors it. The rest
of the test (env vars and secret-store slot per registry kind) already held
for `concentrate`. Local: codewhale-cli 276 run / 276 passed.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(providers): Concentrate review leftovers — alias, unauth 2xx, selftest
[providers.concentrateai] was silently ignored on both config tables.
GET /v1/models is unauthenticated, so a 2xx must not count as key proof —
the probe is unobserved and health_check issues no request. Missing stub
reply text now fails the selftest instead of exiting 0. Duplicate
CONCENTRATE_BASE_URL assignment removed; retired launch_screen row dropped
after the main merge.
Proven: concentrate_resolves_named_responses_gateway_and_environment_overrides
1 passed; concentrate_health_check_does_not_treat_unauthenticated_models_as_key_proof
plus two sibling Concentrate tests 3 passed; 0 failed.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): resolve public /signin /signup /auth/callback 404s
Bare auth URLs were locale-prefixed into /en/signin, /en/signup, and
/en/auth/callback, which had no pages. Keep sign-in and create-account
as locale-aware public routes that use the canonical mark and send the
person to the CWC app, hop OAuth callbacks to app.codewhale.net with
the query intact, and fold /login /register aliases. Local CLI use is
not presented as requiring an account.
Closes#5767
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* docs(changelog): record public auth routes and Pod receipts
Unblock version-drift: #5776 landed on main without a CHANGELOG receipt,
which fails every subsequent PR. #5767 is the public /signin /signup
/auth/callback fix this branch ships.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
---------
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
* feat: make Pod the public roster surface
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* fix(tui): localize pod workers modal and restore fleet receipt compatibility
Review-fix follow-up for the Pod public surface:
- /pod workers modal routes all previously hardcoded English through tr()
with 38 new MessageIds across all 15 shipped locales; a zh-Hans render
test asserts no English leakage.
- /pod workers means current-session sub-agents and /pod status the
durable ledger, pinned per locale by a new localization test.
- Durable receipt detail prefixes restored to the established fleet:
spelling (lane render_detail, tui status_lines, resume detail) with
regression tests at both layers; serde wire keys unchanged.
Local proof (RUST_MIN_STACK=33554432, --locked): localization 49/49,
fleet::control 11/11, subagents 33/33, commands fleet group 13/13,
codewhale-lane 62/62; cargo fmt --check clean; locale key-set hash
identical across all 15 packs.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
---------
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Visual-language alignment with the Tideline TUI and the new brand asset:
- web/ palette re-grounded to the TUI WHALE_* tokens: deep-ocean dark is
the default everywhere; the docs light sheet becomes an opt-in block
carrying the LIGHT_* tokens (fixed two defects found by render inspection:
the white mark vanishing on the light sheet and near-black button ink on
cobalt).
- New brand mark integrated: traced SVG silhouette on the deep-blue tile
(app/icon.svg), favicon.ico (16+32), apple-icon (180), web-app icons
(192/512), new web app manifest, rebuilt OG card (mark + wordmark +
identity phrase on the gradient, 1200x630).
- nav wordmark = new mark + name/tag; seal chips restyled; every
bg-ink-as-dark idiom that would have inverted to ivory slabs fixed across
components and pages.
- tailwind accents resolve through CSS vars so the docs light sheet
re-themes; Fraunces/Noto Serif SC display loads dropped (Tideline
typography: display = body family at heading weight).
Contract tests updated where product intent changed (blue-stage, docs-theme,
public-copy pins); the contracts themselves are preserved. No route, copy,
dictionary, or media-manifest changes.
Verified (integrator re-ran): check:facts OK; check:docs PASS; npm test
40 files / 341 tests all pass; npm run build clean (624 static pages incl.
the new asset routes); asset routes live-checked on the production server
(200s with correct content types). Render inspection at 390/768/1440 for
home + docs, mobile menu, focus ring, docs light toggle, install, models,
community, zh home, OG card — captures under /tmp/cw-captures/ (recorded in
the Ops ledger).
Refs #5573. No-Issue: Tideline program slice per the takeover plan.
Address the two open review findings on the pricing/legal rescue:
- footerLegalLinks now takes the ChromeDict like its sibling generators
and sources labels from new footerPricing/footerTerms/footerPrivacy
keys, added to ChromeDict and all 18 locale dictionaries, so the
footer legal links render localized for every routed locale (zh
included) instead of hardcoded English. docs-ia.test.ts now asserts
the labels match the dictionary per locale, closing the test gap.
- The /legal, /privacy, and /terms alias pages use permanentRedirect
instead of the temporary redirect: these are canonical permanent
aliases for the moved pricing/legal home.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
The new request_plugin_install tool bumps the workspace tool count
from 74 to 75, so web/lib/facts.generated.ts goes stale and the web
Lint & Type Check gate fails check-facts. Regenerate it.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Keep the dictionary runtime as the one i18n path and give updated English marketing/docs copy a fail-closed, lockfile-pinned translation lane. Do not wrap the TUI, completions, or /translate.
* fix(web): keep the published-release fact current instead of after the fact
web/data/latest-published-release.json is hand-maintained and feeds
facts.generated.ts. Nothing ever wrote it, so it sat at v0.9.10 while v0.9.11
had been published on 2026-08-23. The marketing deploy therefore succeeded and
then failed its own post-deploy check on latestPublishedRelease.tag — the site
was already live with a stale fact by the time anyone found out.
- web/scripts/sync-latest-release.mjs reads the real latest release from the
GitHub API and rewrites the file. --check makes drift a failing gate instead
of a silent one. It refuses to write a release that
deriveLatestPublishedRelease() would reject, because that helper returns null
on any shape violation and would drop the fact entirely rather than report a
bad one.
- web.yml runs the check right after "Check facts drift", so a stale release
fact stops the deploy before it ships rather than after.
- The file and facts.generated.ts are refreshed to v0.9.11.
The fact stays checked in on purpose: derive-facts.mjs must work with no
network. This adds the missing write path, it does not move the read path
online.
* fix(web): sync the published-release fact into its docs mirror too
The fact lives in two checked-in places and both feed the public surface:
web/data/latest-published-release.json (read by derive-facts.mjs)
docs/public-surface-facts.json (latestPublishedRelease, which
names the file above as its
`sources`)
sync-latest-release.mjs only wrote the first, so the new check:latest-release
gate would pass while the mirror still said v0.9.10 — and
web/lib/public-surface-contract.test.ts, which asserts the two agree, went
red instead (Lint & Type Check: expected { tag: 'v0.9.10' } to deeply equal
{ tag: 'v0.9.11' }).
Write both, and make --check report either one being stale, so the gate
covers the whole fact rather than one copy of it. The mirror update preserves
every key beyond the four synced fields (notably `sources`), so this stays a
fact refresh and not a schema rewrite.
Verified: npx vitest run in web/ is 337 passed (38 files), up from
336 passed / 1 failed.
---------
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Regenerate the public repository facts after the tool catalog changed on the v0.9.12 integration branch. The derived tool count is 74; a clean-worktree regeneration reproduced the local generated change and now passes.
No-Issue: generated release metadata drift caught by CI.
The marketing site header now links to the hosted Codewhale app: a "Sign in"
ghost link and an outlined "Register" button on desktop, and a two-up row
under the Install CTA in the mobile menu. Both target app.codewhale.net
(/login and /signup, the app's real routes).
Labels are part of the typed chrome dictionary spine (authSignIn /
authRegister / authGroupAria) with translations in all 18 locales, so
check:locales and dictionaries.test.ts stay green. New paper-masthead-styled
CSS for the two controls.
No-Issue: connect the website header to the hosted app sign-in/sign-up.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
`npx tsc --noEmit` rejected indexing the dictionary union by a per-entry
probe key: no member carries an index signature, so the cast could not be
narrowed. Every docs dictionary has `metaTitle` and zh translates all of
them, so one probe covers the loop, including docs/mcp where the heading is
the code-owned literal `MCP`.
Signed-off-by: Lstarsky0 <59827030+Lstarsky0@users.noreply.github.com>
docs/subagents carried 16 isZh branches and docs/mcp 18; both are now zero.
Two dictionaries per page under web/lib/i18n/dictionaries/, wired through
types.ts and index.ts, and both files added to check-locales.mjs's
OPTIONAL_FILES so zh is held to key and token parity while the other sixteen
locales fall back to English exactly as the ternaries did.
The role identifiers and their order stay in the page; the dictionary keys
each row by role name and carries only the prose. docs/mcp's h1 is the
literal MCP in every locale, so it stays in the page too, which is why the
key-parity test now takes an explicit probe key instead of assuming
overviewTitle.
Rendered output is unchanged across all 36 pages (18 locales x 2).
Signed-off-by: Lstarsky0 <59827030+Lstarsky0@users.noreply.github.com>
Promotes the fully gated non-benchmark candidate while preserving the benchmark tree exactly from the prior release-PR head.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
feat(tui): add multi-file read_lints operation (#5524, Isabel Wu).
Conflict was confined to the two changelog files, where our Unreleased
section had moved on past the PR's base; the read_lints entry is folded
into ours and the tui slice re-synced. Contributor commits stay intact
as merge parents.
Defect: both search surfaces carried an identical `highlight()` that
took `idx` from `text.toLowerCase()` and then sliced `text` with it:
const lower = text.toLowerCase();
const idx = lower.indexOf(q);
text.slice(idx, idx + q.length)
That assumes lowercasing preserves length. It does not — `"İ"` (U+0130,
Turkish dotted capital I) lowercases to two code units, so every index
after one is off by one. Searching "stanbul" in "İstanbul kurulumu"
marks "tanbul " instead of "stanbul": the highlight starts and ends one
character late. Turkish is a routed locale (`tr`), and the search
haystack is where localized copy is headed.
Fix: `highlightSpan()` in lib/search-utils.ts — the module that exists
so the search components' pure logic can be unit-tested — folds case
one character at a time and keeps a position map, so the three returned
pieces are cut at real character boundaries and always reassemble the
input exactly. Both components now call it instead of repeating the
arithmetic.
Scope, honestly: no string in the current haystack contains a
length-changing character, so this is a latent bug rather than an
observed one. It is still wrong output for valid input, and it was
wrong in two places.
Evidence: lib/search-utils.test.ts. Restoring the lowercased-index
implementation fails "keeps indices in the source string when
lowercasing changes length" with `expected 'tanbul ' to be 'stanbul'`.
The last case pins both components onto the shared rule.
npm test 327 passed, npm run lint clean, npx tsc --noEmit clean.
Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Defect: the homepage wire strip shortened long titles with
`title.slice(0, 70)`, and the roadmap summaries with
`stripped.slice(0, 137)`. `slice` counts UTF-16 code units, so when the
cut lands between the two halves of a surrogate pair the output ends in
a lone surrogate. GitHub issue, pull request and release titles carry
emoji routinely; the reader sees U+FFFD — the black-diamond question
mark — immediately before the ellipsis. Verified: a title of 69 ASCII
characters followed by U+1F40B yields a string whose last code unit is
0xD83D.
The same `.length` test also miscounted: an astral character was two
characters toward the budget, so a 40-emoji title was treated as
80 characters and truncated at 35.
Fix: one code-point-safe `truncateChars` in lib/truncate.ts, used by
both surfaces. Character budgets and thresholds are unchanged (70 for
the ticker; cut at 140, keep 137 for roadmap notes), so nothing but the
broken cut moves.
Also corrected a false comment in summarizeReleaseBody: it claimed to
strip trailing emoji, which it has never done.
Evidence: lib/truncate.test.ts. Restoring `value.slice(...)` in the
helper fails "never splits an astral character in half" (a lone
surrogate survives the well-formed-pair filter) and "counts code
points, not UTF-16 code units". The last case pins both call sites onto
the shared rule.
npm test 322 passed, npm run lint clean, npx tsc --noEmit clean.
Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Defect: detectLocaleFromHeaders read Accept-Language positionally —
`acceptLanguage.split(",").map((s) => s.split(";")[0])` — and returned
the first routed tag it saw. The weights were parsed off and thrown
away, so:
* `Accept-Language: en;q=0.2, ja;q=0.9` served English to a reader who
asked for Japanese;
* `Accept-Language: en;q=0, ja;q=0.9` served English to a reader who
had explicitly marked English not acceptable — q=0 is a refusal
(RFC 9110 §12.4.2), not a low preference.
The module docstring claimed the walk was "in the header's preference
order", which is only true for headers that happen to be pre-sorted.
This runs in middleware, so the wrong answer is a 307 to the wrong
locale plus a year-long NEXT_LOCALE cookie pinning it there.
Fix: `acceptLanguageTags()` parses each entry's weight, drops q=0,
sorts descending by weight with original order as a stable tie-break,
and the detector walks that. A malformed weight stays at the default
of 1 rather than being guessed at.
Evidence: four new cases in lib/i18n/detect.test.ts. Restoring the
positional walk fails two of them with `expected 'en' to be 'ja'`.
The pre-existing ordered-header cases still pass unchanged.
npm test 317 passed, npm run lint clean, npx tsc --noEmit clean.
Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Defect: both nav surfaces decided the active link with a plain prefix
test — `pathname === l.href || pathname.startsWith(`${l.href}/`)`. Two
of the six nav links are ancestor and descendant of each other,
`/<locale>/docs` and `/<locale>/docs/guide`, so on the guide route the
test was true for both. Docs and Start were rendered active at the same
time (`.nav-link[aria-current="page"]::after` draws the underline under
both), and two links carried `aria-current="page"`, telling assistive
technology the reader is on two pages at once. Every routed locale hits
it: /docs/guide is the "Start" nav entry on all 18.
Fix: one shared `currentNavHref(links, pathname)` in lib/i18n/links.ts
resolves the longest matching href — the page — and both surfaces
compare against it. Routes that are not themselves nav links are
unaffected: /docs/configuration still resolves to the Docs entry.
Evidence: lib/docs-ia.test.ts "marks exactly one nav link as the current
page on a nested route" asserts the collision exists for the naive test,
then pins the resolved link for every routed locale and pins both
components onto the shared helper. Reverting the helper to a
first-match/prefix implementation fails it with
`expected '/en/docs' to be '/en/docs/guide'`.
npm test 313 passed, npm run lint clean, npx tsc --noEmit clean.
Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
The getting-started contract corroborated the site's keyless-launch claim
by requiring docs/GUIDE.md to contain one exact sentence, "On first launch,
Codewhale opens with a recommended working agreement". The honest first-run
work replaced that flow, so the sentence is gone and the assertion pinned
obsolete copy rather than product truth. The web suite fails identically on
main and on any branch built from it.
Assert what the docs actually owe this step: inside the First Launch
section, that the first launch asks only for the decisions the install
still needs, and that the provider step keeps an explicit offline route.
Together those are what make "starts without any API key" true, and they
survive rewording. The section slice matches heading-level hashes only, so
the `# npm` comments inside the fenced install snippets cannot end it.
Evidence: `npm test` 311/311 pass (was 310/311), `npm run lint` clean,
`npx tsc --noEmit` clean, `npm run check:facts` and `check:docs` pass.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Both pages carried 14 and 15 isZh branches; both are now zero. Same
shape as #5504 and #5517: an en and a zh dictionary per page, wired
through types.ts and index.ts, and both files added to
check-locales.mjs's OPTIONAL_FILES so zh is held to key and token
parity while the other sixteen locales fall back to English.
The config values the policies paragraph typesets as inline code, and
the commands and flags on the browser-client page, stay code-owned in
the page and reach the dictionaries as {token}s, per docs/VOICE.md.
Rendered output is unchanged across all 36 pages (18 locales x 2).
Preserve merged contributor work from PRs #5517, #5514, and #5509 while integrating the verified v0.9.10 runtime, Agent Mail, context recovery, onboarding, documentation, and release-hardening train. Remove one obsolete page-source copy assertion exposed by the completed dictionary migration.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Remove page-local English/Chinese forks from configuration and constitution. Route copy, metadata, and locale typography through the existing typed dictionary spine with English fallback for partial locales.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Treat tests as selective evidence instead of product specification, make migrations one-way, permit local direct-main commits on clean current checkouts, and spell out the offline boundary.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Cover the post-budget-alignment fixes that landed after the last changelog
pass: the actionable Extensions marketplace and honest MCP inventory, the
session-ownership boundary for sub-agents, shells, tasks, continuations,
and workflow controls (#5518), exact route-limit propagation (#5516,
#5518), and first-run/onboarding continuity. Credit @hxfhd and @sfdzhmr
across the changelog, docs/CONTRIBUTORS.md, and the website credit arrays
so the enforced three-way parity test holds.
Implemented with automated coding assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Keep release credits in exact changelog parity, align the public source-candidate tool count with mechanical derivation, and update first-run docs/tests to the recommended working agreement.
Signed-off-by: Hunter Bown <hmbown@gmail.com>