Skip to content

v3: Comtrya — combine v1 Smart-HTTP/OCI/federation with v2 kernel discipline#1

Open
rawkode wants to merge 312 commits into
mainfrom
v3
Open

v3: Comtrya — combine v1 Smart-HTTP/OCI/federation with v2 kernel discipline#1
rawkode wants to merge 312 commits into
mainfrom
v3

Conversation

@rawkode
Copy link
Copy Markdown
Member

@rawkode rawkode commented May 11, 2026

Summary

v3 brings v1's load-bearing implementation pieces back into v2's kernel-shaped architecture:

  • v2 keeps: kernel-vs-features split, OIDC + SpiceDB + CUE + CloudEvents + WIT/Component Model, spec discipline.
  • v1 returns: pure-Rust Smart HTTP v2 Git server, OCI extension distribution, federated GraphQL with typed resolver dispatch.

See V3_PLAN.md for the increment plan.

Note: the product is being renamed from Forgepoint to Comtrya; rename is a follow-up commit in this PR.

Increment plan

  1. ✅ Plan + PR (this commit)
  2. Port v1 pure-Rust Smart HTTP v2 (crates/git-http)
  3. Wire it in, drop git http-backend shell adapter
  4. Typed WIT resolver ABI (kill numeric-proof shape)
  5. Per-extension SQLite host capability
  6. Federated GraphQL composer + planner
  7. OCI extension distribution
  8. CUE config extended
  9. Replace fixture-sourced request paths
  10. Frontend extension-host refactor
  11. First-party pull-requests extension end-to-end
  12. Receive-pack/push
  13. Reconcile SPEC_COVERAGE.md, TODO.md, start.sh

Each increment ships as its own commit so the PR shows a real progression.

Test plan

  • `start.sh --reset && start.sh` end-to-end with no fixture-sourced product data on request path
  • `git clone` and `git push` succeed against pure-Rust Git server
  • First-party extension installable from OCI artifact and live
  • Uninstall extension and product surface disappears cleanly
  • `SPEC_COVERAGE.md` maps every visible surface to Git / SQLite / WIT resolver

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 11, 2026 17:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a v3 planning document describing how the project will reintroduce v1’s Smart-HTTP/OCI/federation capabilities within v2’s kernel/extension architecture, along with an increment-by-increment roadmap and verification criteria.

Changes:

  • Add V3_PLAN.md describing v3 goals, architecture targets, increment plan, and verification checklist.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread V3_PLAN.md Outdated
Comment on lines +1 to +3
# Forgepoint v3 — Plan

This branch combines what v1 (the original Forgepoint at `forgepoint-dev/forgepoint`) and v2 (the spec-driven rewrite, which this repo is) each got right into a single coherent kernel.
@rawkode
Copy link
Copy Markdown
Member Author

rawkode commented May 11, 2026

v3 architectural foundation — landed

All 8 increments planned for this initial branch are committed and pushed. The v3 architecture combining the best of v1 (pure-Rust Git Smart HTTP v2, OCI extension distribution) with v2 (kernel-vs-features split, OIDC + SpiceDB + CUE + CloudEvents + WIT/Component Model, spec discipline) is in place at the contract and crate level.

What's done

# Increment Commit
1 V3 plan + PR opened 8468183
2 Rename Forgepoint → Comtrya across source, manifests, env vars, WIT, crate names 696b743
3 Port v1's pure-Rust Smart HTTP v2 Git crate (~1880 LOC) as comtrya-git-http 7c1d9c8
4 Wire pure-Rust Git into the server; default upload-pack path no longer shells to git http-backend 4f1c399
5 Define typed WIT host interfaces (host-log, host-events, host-storage, host-git, host-http, host-secrets, host-jobs) 048c173
6 Add comtrya-extension-oci (OciExtensionFetcher + content-addressed cache, ported from v1) bfbe492
7 ExtensionInstallConfig + ExtensionSource::{Local, Oci} + OciReference::{Tag, Digest} in core; CUE example added d8b6064
8 Add V3_STATUS.md + reconcile V3_PLAN.md with reality 8f06814

Test posture

cargo test --workspace138 tests passing:

  • 80 core (incl. new ExtensionInstallConfig validation)
  • 16 git-http (Smart HTTP v2, ls-refs, pack generation)
  • 12 extension-oci (cache + fetcher offline-mode paths)
  • 29 server (Git endpoint, extensions runtime, GraphQL, auth, events)
  • 1 MVP integration

Backend flip

/git/* now uses the pure-Rust path by default. Set COMTRYA_GIT_BACKEND=legacy to fall back to the git http-backend shell adapter for regression comparison.

Deferred to follow-up branches

See V3_STATUS.md for the full list. Headlines:

  • Typed WIT resolver execution — interfaces exist, host Linker + bindgen still needed.
  • Per-extension SQLite via host-storage — WIT defined, host impl pending.
  • OCI fetcher wiring into load_extension_runtime — crate is ready, integration is next.
  • Federated GraphQL composer that dispatches fields to extension components.
  • Astro shell as real extension host — strip product panels.
  • First-party ext_pull_requests end-to-end through the new path (the proof).
  • Receive-pack/push on top of crates/git-http.
  • CUE loader upgrade so the extensions: block drives behavior.

Each is independently scoped and individually shippable on its own branch.

🤖 Generated with Claude Code

rawkode and others added 26 commits May 12, 2026 13:43
…ames

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add split_repo_path helper and build_repository_summary to derive groups
from the path field, count open pull requests, compute check pass/total
summary, and carry lastCommitAt. Wire workspace.repositories into the
GraphQL response as enriched summaries alongside repositoryByPath.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gthen tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d:true v1 flag

Adds three host-side aggregated viewer fields consumed by the home
your-work widget; the aggregated:true flag marks them for replacement
by the federated GraphQL planner (V3_PLAN item 9).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `filter_events_for_viewer` helper that keeps only activity events
whose `repositoryID` is in the caller-supplied visible set (events with
no `repositoryID` are dropped defensively).  Wire it into the workspace
GraphQL projection as `workspace.events`; v1 visible set is all repos in
the workspace.  Leave `activityEvents` top-level field untouched.  Two
unit tests cover include/exclude logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…SDK setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion guards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…K setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the ext_workspace_home first-party extension with v2 UI
manifest contributing home.your-work, home.repositories, home.activity,
and home.instance slots via four custom elements. Also registers the
extension in FIRST_PARTY_EXTENSIONS so the server loader picks it up on
startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Appends smoke assertions that verify the new Astro routes introduced in
Tasks 12-14 produce the expected SSR HTML and status codes: home-shell
on /, repo-dashboard on /r/<path>, extension-page on /x/pulls/, 404 for
unknown repo and extension prefixes, and the /instance -> /#instance
redirect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures all in-flight v3 scaffolding from prior sessions before the M0
audit begins. Everything in this commit is contract / scaffolding —
none of it is wired into the live request path, by design. The cutover
that wires it (and deletes legacy code) starts here.

Includes:
- Platform WIT contract @ comtrya:platform@0.1.0 (extensions/wit/...)
- Host-side WIT bindings module (crates/server/src/wasm_host.rs)
- WIT codegen crate (crates/wit-codegen/)
- Per-extension WITs + updated manifests for the five first-party extensions
- Bundler script (extensions/bundler/build-extension.sh)
- Framework-agnostic + Vue + Preact SDK packages (frontend/packages/)
- Design + phase log + extensions docs (docs/)
- GOAL.md describing the cutover plan and working process
- M0 deliverable: docs/v3-deletion-inventory.md

The deletion inventory drives every subsequent milestone.
Adversarial review caught real accuracy bugs in the first draft. Fixed:

- Arm count corrected from 28 to 29
- `comments_delete_mutation` end line corrected to 3079 (was 3093)
- All five storage-seed line numbers corrected (off by 25-157 each)

Added rows for artifacts the first draft missed:
- `fn graphql_response` at 3350 — the monolithic snapshot path the
  Astro frontend depends on; dies in M9
- `fn extension_resolver_payload` at 1657 — legacy resolver dispatch
  for ext_checks/ext_workspace_home/ext_code_browser; phased out
  through M5/M11
- `fn checks_resolver_output` (4873), `pull_request_resolver_output`
  (4842), `code_browser_resolver_output` (4821)
- `struct CheckSummary` (4959) + `fn check_summary` (4966)
- Frontend test files list under M10
- `frontend/src/pages/[...path].ts` API-proxy callout

Added an `ext_code_browser` status section — it's now core, not an
extension; the in-process resolver branch is retained with an open
question about whether to fold it into a dedicated GraphQL query
before M11 deletes its host (`extension_resolver_payload`).

Flips M0 boxes 1-7 in GOAL.md. WIT lock and pre-flight decisions
follow in their own commits.
Adds a clear LOCKED banner to package.wit specifying the revision-bump
procedure. wasm-tools still parses cleanly. No drive-by edits to the
WIT during the cutover.

Flips the M0 lock box in GOAL.md.
Locks the five GOAL.md pre-flight questions into docs/v3-decisions.md:
SPA (not SSR), Vite (not Astro), seed payload + WASM-driven seeder,
keep GraphQL schema, pin wasmtime 43.0.2 + wit-parser 0.235 (cargo-
component pinned at M1 first use).

Flips the final M0 box. M0 is complete.
First real Component-Model WASM in the project. The component
implements every op declared in extensions/first-party/ext_issues/wit/
issues.wit (open-issue, close-issue, reopen-issue, get-issue,
list-issues) backed entirely by platform host imports:

- ids.mint("issue") for opaque IDs
- storage.{create,get,update-begin,update-commit,list-all} for state
- events.append for dev.comtrya.issues.{opened,closed,reopened}
- time.now-iso for timestamps
- identity.current-principal for the actor

The reactor export returns empty in 0.1.0 — ext_issues subscribes to
nothing. ext_pull_requests will subscribe in M6.

Build chain:
- cargo-component 0.21.1 + wit-bindgen-rt 0.44 pinned
- Component built for wasm32-unknown-unknown (NOT wasm32-wasip1) so no
  WASI adapter imports leak into the component's import set. The
  .cargo/config.toml in the component crate locks this target.
- Workspace exclude added for extensions/first-party/*/component so
  the host workspace doesn't try to compile WASM crates.
- Bundler script (extensions/bundler/build-extension.sh) updated:
  builds the component under <ext_root>/component/, copies wasm to
  <ext_root>/dist/, and runs wit-codegen with the platform WIT as a
  dep so the per-extension WIT's `use comtrya:platform/...` resolves.
- wit-codegen now skips exports whose interface lives in a different
  package from the per-extension WIT — so reactor exports (from
  comtrya:platform) no longer pollute the dispatch table.

Verified end-to-end:
- `bash extensions/bundler/build-extension.sh extensions/first-party/ext_issues`
  emits dist/ext_issues.wasm (108KB), ext_issues.handlers.rs (5 ops),
  ext_issues.client.ts.
- `wasm-tools component wit dist/ext_issues.wasm` reports the
  component imports only comtrya:platform/* interfaces and exports
  comtrya:ext-issues/issues@0.1.0 + comtrya:platform/reactor@0.1.0.

Flips M1 boxes 1-9 in GOAL.md. Box 10 (Wasmtime integration test
calling close-issue) lands in its own commit.

Toolchain pins recorded in docs/v3-decisions.md.
Box 10: wasm_host::m1_ext_issues_smoke::close_issue_round_trip loads
ext_issues.wasm, builds a HostState with a tempdir-backed
ExtensionRuntimeStore, instantiates the component through
make_platform_linker, calls open-issue then close-issue via a
second wasmtime::component::bindgen! pointed at the ext-issues WIT,
and asserts the persisted issue record's state == "closed". Proves
the full host-import contract round-trips: ids.mint, storage.create,
storage.update-begin/commit, events.append, time.now-iso,
identity.current-principal all behave under real WASM dispatch.

Milestone-boundary adversarial review fixes:

- I1 (next_issue_number O(N) + 1024 collision cap): replaced
  list_all scan with OCC compare-and-swap on a per-repository
  counter in a _meta collection. No more silent duplicate numbers.
- I2 (UlidMinter kernel-internal kinds): added
  UlidMinter::with_kernel_kinds() that pre-seeds event, relation,
  comment so neither the test nor M2 production wiring has to
  remember the kernel-internal kind list.
- I7 (test panics if WASM missing): now skips with an actionable
  message so `cargo test` on a fresh clone still works.

Review findings tracked as named follow-ons in GOAL.md:
- M2: mint_internal helper for kernel-initiated mints (I3)
- M2: bundler guard for missing .cargo/config.toml (I5)
- M2: enforce ids.mint -> storage.create registry contract
- M2: extension load path must read the real wasm artifact, not
  manifest.wasmComponent's component.wat stub (C2 deferred to M2,
  not a M1 blocker — current resolver path still reads .wat)
- M4: persist close-issue input.reason (I4)

Reviewer claim C1 (wit-bindgen-rt 0.53 required) was incorrect —
verified via `cargo search wit-bindgen-rt`: 0.44 IS the latest
published version. wasmtime 43.0.2 transitively brings wit-bindgen
0.51 and 0.57; the canonical Component-Model ABI is stable across
this gap for the M1 surface (plain records, scalars, lists, options,
results, variants). Empirically confirmed by the passing smoke test.
Recorded in docs/v3-decisions.md.

M1 complete: every box ticked, every CRITICAL/IMPORTANT finding
either resolved or tracked at a named milestone.
build.rs walks extensions/first-party/ and, for any extension that
declares platformWitVersion AND ships a component/ cargo-component
crate, runs comtrya-wit-codegen against its wit/ and emits
<OUT_DIR>/<ext_id>.handlers.rs. Composes a top-level
<OUT_DIR>/dispatch_table.rs that defers to each per-extension
dispatch_route_<ext_id> function and re-exports DispatchInfo.

The build script:
- Issues cargo:rerun-if-changed for every WIT file + manifest, so
  edits trigger a rebuild.
- Skips extensions whose manifest declares platformWitVersion but
  don't yet have a component/ crate (cargo:warning surfaces the
  skip in build logs). Today that's ext_epics + ext_pull_requests
  pending M5 migration.
- Auto-creates the wit/deps/platform symlink so a fresh clone +
  cargo build works without first running the bundler.

main.rs gains:
- mod generated_dispatch { include!(... dispatch_table.rs ...); }
- Two tests verifying the codegen → include pipeline works
  end-to-end: ext_issues.issues.close-issue resolves to a
  DispatchInfo with the right fields; an unknown route returns None.

Flips M2 boxes 1-2.
Extensions whose manifest declares platformWitVersion AND ship a
real <ext_root>/dist/<ext_id>.wasm now take a new load path:

- The kernel builds a single WasmRegistry on boot, holding one
  Arc<Linker<HostState>> + Arc<Engine> + the loaded-extension map.
- Each platform extension's manifest is parsed into HostManifest
  (allowedEmits, allowedEventReads, allowedCrossCalls, reactor
  allowlists, contributes.resourceKinds, hostImports).
- The WASM binary is read from dist/<id>.wasm (not the legacy
  manifest.wasmComponent which still points at component.wat for the
  legacy resolver path).
- The registry pre-seeds kernel-internal kinds in the UlidMinter via
  with_kernel_kinds; each registered extension adds its declared
  kinds via the new IdMinter::register_kind trait method.
- Extensions without platformWitVersion keep the legacy resolver
  path (Linker::<()> + resolve()) until their migration milestone.

RegistryDispatcher implements OpsDispatcher backed by the registry.
For M2, cross-extension calls return error-code::Unavailable with a
clear message pointing at M5+ when per-extension typed bindings land
— the registry lookup, manifest enforcement, and depth tracking all
work; only the WASM-side typed call is stubbed.

Side fixes surfaced while wiring:
- Pre-existing bug in is_valid_permission_grammar accepted ".write"
  as valid (leading dot). Rewrote using split('.') with non-empty
  halves; all original cases still pass plus the leading-dot case
  now correctly rejects.
- IdMinter gained a register_kind(&str, &str) trait method so the
  registry doesn't need to downcast to UlidMinter at load time.

Tests:
- registry_loads_ext_issues_from_manifest verifies a real
  ext_issues.wasm + manifest produces a LoadedExtension with the
  right principal, contributes_resource_kinds, allowed_emits.
- registry_get_unknown_returns_none verifies the not-found path.
- parse_host_manifest_handles_missing_optional_fields verifies the
  manifest parser doesn't panic on a minimal manifest.
- m1_ext_issues_smoke and dispatch_table tests still pass.

Flips M2 boxes 3, 4 (added), 5, 7. Box 6 (live request path) stays
for M3 — that's GraphQL handler wiring.
- Add HostState::mint_internal for kernel-initiated mints (events.append,
  relations.create, comments.post). The Host trait impls now use this
  helper, so the split between extension-initiated mints (manifest-
  gated, return Forbidden on missing kind) and kernel-initiated ones
  (no gate, fail Internal if the kind isn't registered with the
  minter) is explicit at every call site rather than implicit.
- Bundler script (build-extension.sh) now FAILS if a component crate
  lacks .cargo/config.toml. Previously cargo-component would silently
  fall back to wasm32-wasip1 and drag every wasi:* import into the
  component, making it un-instantiable under our Linker<HostState>.
rawkode and others added 30 commits May 18, 2026 05:10
The repo overview rail already surfaces projects, bookmarks, labels
and the workspace-wide activity stream, but nothing on the page
answered "what just landed in this repo?". The kernel already pre-
computes the last 8 commits via `git_commits` and exposes them as
`repository.commits { oid shortOid subject author time }`; this
slice just renders them.

A "Recent commits" panel sits between the bookmarks panel and the
labels catalog in the overview rail. Each row shows the short oid
(full oid as the cell tooltip), the subject, the author, and git's
own `--date=relative` string — same shape GitHub's repo-home uses,
but our copy stops at 8 instead of paginating so the rail stays
scannable.

The panel hides itself when there are no commits, so freshly-
imported empty repos don't earn a placeholder. Once the jj
change-id workflow lands (see memory), this is the row that grows
a third column for the change-id.

Verified live on /r/comtrya/comtrya (2 commits) and
/r/comtrya/dogfood (1 commit): heading "Recent commits", rows
render with `<oid> <subject> <author> <time-ago>` and link
through to the underlying full oid via tooltip.

This commit was created with the assistance of a LLM.
…aceholders

Iter 47-48 added issue / pull / epic title search to the palette;
this slice rounds out the entity surface by surfacing every
workspace repository as a fourth group ("Repos"). Selecting a repo
routes to `/r/<path>`, the same destination the registered
`core.switch-to-repository.*` commands already produce — but the
new rows carry the repo description, so the palette gives more
context per row than the bare command titles do.

Empty-query reveals the first 8 repos, which makes the palette
double as a quick repo switcher (the way Linear's `cmd+K` reveals
recents on open) rather than only filtering when the user starts
typing.

Both placeholder strings get the truth update: the topbar `cmdk`
button no longer claims to search "file, ref" (which the palette
has never done), and the palette input's "Search commands"
becomes "Search repos, issues, pulls, epics, commands…".

Verified live: cmd-K with no query shows a "Repos" group with all
5 workspace repos; typing `dogfood` narrows to one row; clicking
the row navigates to `/r/comtrya/dogfood`. The registered-command
"Repositories" group stays alongside (visually distinct header)
until a later refactor.

This commit was created with the assistance of a LLM.
The comment-thread element was read-and-post only; once a comment
was posted there was no way to fix a typo or drop a stray remark.
The kernel already exposes `comments.update` and `comments.delete`
through the GraphQL dispatcher; this slice surfaces them in the row.

Each comment now grows an actions strip (revealed on row hover /
focus) with `edit` and `delete` buttons. Edit swaps the rendered
body for a textarea + Save / Cancel chrome; Save calls
`comments.update`, replaces the body with the new markdown render,
and stamps an `edited` marker into the meta row. Delete confirms
through `window.confirm` and calls `comments.delete`, dropping the
row optimistically. Both modes accept Cmd / Ctrl-Enter as the
save chord, matching the composer's iter 52 keyboard.

Verified live on /x/issues/.../2: clicking edit, replacing the body,
and clicking save reflows the row with the new markdown and the
`edited` marker; clicking delete drops the row from 4 → 3.

This commit was created with the assistance of a LLM.
IssuesList was returning issues in whatever order `list-issues`
emitted (effectively number-desc), so a closed issue from last week
sat next to a freshly-opened one with no visual cue about which had
recent activity. Bring it in line with PullsQueue (which already
sorts by `updatedAt ?? createdAt` desc) so every queue surface
reads the same way.

Falls back through `updatedAt → createdAt → number` so a batch of
issues sharing a second-resolution timestamp still surfaces the
newest number first instead of looking randomly ordered. The
`updatedAt` field was wired through in iter 43, so the data is
already on every row.

Verified live on /x/issues/?state=all: closing issue #1 via the
ops endpoint floats it to the top of the list ("just now"); the
remaining issues, all sharing "1h ago", fall back to number-desc
(#6, #5, #4, #3, #2).

This commit was created with the assistance of a LLM.
The thread's status line was a one-shot — "Loading…" → either
"No comments yet" or removed entirely once rows landed. Promote it
to a persistent count header so a reader can see "3 comments" at
a glance without scanning the list. Updates live on post, delete,
and the initial fetch.

The element also now dispatches `comment-thread-update` events
(detail.count, bubbles) on every count change. Host pages can
listen and render their own counted heading — e.g. "Activity (3)"
in IssueDetail's section header — without scraping the DOM.

Verified live on issue #2: status reads "3 comments" → posting via
cmd+Enter flips it to "4 comments" → delete drops it back to
"3 comments"; the event payload tracks (4, 3) on the listener.

This commit was created with the assistance of a LLM.
…faces

Iter 58 made the comment-thread element dispatch a
`comment-thread-update` CustomEvent on every row change; this slice
wires the three extension detail surfaces (IssueDetail,
PullsDetail, EpicDetail) to that event so each header reads
"Activity (3)" / "Discussion (3)" without scraping the custom
element's DOM.

EpicDetail didn't have a header above its comment thread at all —
the section just hung off the page. Added a `<header>` with
"Discussion" so it matches the other two surfaces.

Verified live:
  /x/issues/.../2 → "Activity (3)"
  /r/comtrya/comtrya/pulls/pul_…/42 → "Discussion (0)"
  /x/epics/<ws>/epc_… → "Discussion (0)"

This commit was created with the assistance of a LLM.
Inbox rows show number / title / labels / project / repo / state,
but nothing about *when* the work last moved. A triager pulling up
the inbox to decide what to tackle had to click through each row to
see the age. Add a small "X ago" chip on the right of the meta row
for both Open pull requests and Open issues panels, mirroring the
chip IssuesList and PullsQueue already use. Full timestamp is in
the chip's `title` for ISO-precision hover.

Inputs are already sorted by `updatedAt` desc (iter 32), so the chip
sequence reads top-down as "most recent first" — which is what the
sort silently promised but didn't visualise.

Verified live on /inbox: both panels render "2h ago" age chips on
their rows.

This commit was created with the assistance of a LLM.
Cmd-K results were a wall of titles with no visual indication of why
each row matched the query. Borrow the Spotlight / VSCode / Linear
convention: wrap the matched substring inside each result title in
a `<mark>` styled as a bold span with an accent-orange underline.

Highlight applies to issues, pulls, epics, and repos. The repo row
shows it on the path (`comtrya/<mark>dogfood</mark>`); the entity
rows show it on the title (`Q4 <mark>platform</mark> launch`). The
helper HTML-escapes its input before wrapping the match, so user-
provided titles can't smuggle markup through the v-html sink.

Verified live: typing `platform` underlines the matched substring
in the "Q4 platform launch" epic; typing `dogfood` underlines the
substring in the `comtrya/dogfood` repo path.

This commit was created with the assistance of a LLM.
Iter 55 added a "Repos" entity-search group to the palette, with
each row carrying the repo description — strictly more informative
than the "Switch to repository <path>" commands
`bindRepositoryCommands` registered. The duplication showed up as
two groups with overlapping items, so iter 55 routed around by
calling the new group "Repos" while the old one kept the
"Repositories" header.

Now that the entity group is canonical, drop `repository-commands.ts`
entirely and rename the entity group back to "Repositories". The
palette emits a single source of repo nav; the bundle loses one
SSE subscription and a per-repo command registration loop.

Verified live on /: cmd-K with no query shows one "Repositories"
group; no "Switch to repository …" rows appear elsewhere.

This commit was created with the assistance of a LLM.
First slice of the change-id workflow (see memory
project_jj_change_id_workflow.md). Plumbing + UI consumer in the
same iteration per loop bias:

  * kernel: `git_commits` extends the `git log` format to capture
    the `Change-Id:` trailer via git's built-in
    `%(trailers:key=Change-Id,valueonly)`. `-z` separates commits
    with NUL because the trailer format unconditionally prints a
    trailing newline. `repo_git_data` previously had its own inline
    duplicate of the commit-parsing loop — refactor it to call
    `git_commits` so the change-id (and future commit-shape
    additions) live in one place. The fn also now reads the
    default branch instead of hardcoding `main`.

  * frontend: RepoHome's RepositoryCommit interface gains
    `changeId?: string | null`; the GraphQL query asks for it; the
    Recent commits row renders an accent-teal short-id chip next
    to the short oid when a trailer is present, with the full
    change-id in the cell tooltip. Commits without a trailer
    simply omit the chip — git-only repos stay clean.

To make the rendering visible in the running stack, the demo
repo's "Wire live Git and extension demo data" seed commit now
carries a `Change-Id:` trailer in its body (one of two demo
commits — the seed commit stays trailer-free so both shapes are
exercised).

Verified live on /r/comtrya/comtrya: top commit renders
`c35bc75 [I9d2c3f7] Wire live Git ...` with the full change-id in
the chip's tooltip; second commit renders without the chip.

This commit was created with the assistance of a LLM.
Iter 39 wired `c` to navigate to `/x/issues/new` on any surface
without an inline quick-add. On detail pages — IssueDetail,
PullsDetail, EpicDetail — that left a reviewer who pressed `c`
mid-thought yanked away from the page they were reading. Composing
a reply is the natural "create" verb on a detail surface.

The create chord now checks for a visible
`[data-smoke="comment-thread-composer"]` first; if its textarea is
findable, focus it (and scrollIntoView so the composer lands in
the viewport on tall pages) instead of navigating. Falls through
to the iter 41 routing logic everywhere else, so Inbox still
routes to `/x/issues/new`.

Shortcut overlay copy updates to match: "Comment on this surface
— or create new (Inbox, detail pages)".

Verified live: `c` on /x/issues/.../2 focuses the composer
textarea without changing the URL; `c` on /inbox still navigates
to /x/issues/new.

This commit was created with the assistance of a LLM.
ActivityStream's `dev.comtrya.comment.posted` case already reads
`stringField(payload, "body")` and slices it to ~80 chars for the
stream row text — but the kernel was emitting the event with only
`{commentID, target, parent, authorRef}`. The reader fell through to
"comment", leaving the stream row opaque ("commented · comment").

Add a 200-char body preview to the event payload (the activity row
re-truncates to 80, so the cap is just a guardrail against multi-
kilobyte comments inflating event-log size). No client change
needed — the ActivityStream code already projects the field.

Not visually verified in this dev env: workspace.events resolves
to a hardcoded demo array in v1, and the SSE channel requires an
access token the dev browser doesn't carry. The change is a small,
correct enabler for the existing reader; production deployments
gain the preview without any further wiring.

This commit was created with the assistance of a LLM.
A small localStorage-tracked list of the user's last 5 visited
routes, surfaced as a new "Recent" section in the sidebar above the
Repositories list. Each entry shows a friendly label
(`issue #4`, `pull pul_…`, `epic <short>`, `<repo>/<tab>`, or the
repo path) and links to the route. The list is browser-local — no
kernel state, no sync, no per-account history surface yet.

`recordRouteVisit(path, label)` fires from `router.afterEach`. The
shouldRecord guard filters out routes the sidebar already exposes
via direct links (`/`, `/inbox`, `/new`) and any `*/new` form so
the section reads as recall-worthy destinations only.

Verified live: clearing localStorage, then navigating issue #2 →
/r/comtrya/comtrya → /r/comtrya/dogfood/issues produces a
"Recent" section with three rows ordered most-recent first.

This commit was created with the assistance of a LLM.
Cmd-K on a fresh palette open now shows the iter 66 recent-routes
list as the first group, above Navigation / Create / Projects /
Repositories. Each row carries the friendly label
(`issue #4`, `<repo>/<tab>`, etc.) plus the raw path in the
meta-code slot for disambiguation, and selecting it routes there.

The group hides itself the moment the user starts typing — at that
point the intent is search, not recall, and recents become noise.
Hooks into the same `recentRoutes` ref the sidebar reads from, so
the two surfaces stay in lockstep without an extra subscription.

Verified live (after the iter 66 navigation seeded three recents):
empty palette shows "Recent" with 3 rows leading the list;
typing `platform` collapses to just the Epics match.

This commit was created with the assistance of a LLM.
A small `×` icon sits next to the "Recent" header, calling
`clearRecents()` — the new export on `recents.ts`. Drops the
reactive ref to an empty list and writes `[]` to localStorage so
the reset survives reloads. Useful before screen-sharing or to
purge a stale jump-list.

The section is `v-if="recents.length > 0"`, so it disappears
cleanly the moment the list empties — no awkward "Recent (empty)"
state. Reuses the existing `.sb-icon-link` chrome so the button
matches the `+` next to "Repositories".

Verified live: starting from 3 recent rows, clicking the clear
button drops the panel from the DOM and writes `[]` to
`localStorage["comtrya.recentRoutes"]`.

This commit was created with the assistance of a LLM.
Each comment row gains a "reply" action alongside edit / delete.
Clicking mounts an inline composer (same shape as the edit-mode
textarea, with Cmd-Enter to post) and posts a child comment with
`parent` set to the parent's URI. Replies render nested inside a
`comment-thread-replies` UL indented under their parent, with a
thin rule on the left edge so the hierarchy reads.

Render loop groups comments by parent before laying them out:
each top-level comment is rendered, and its replies recurse into
the nested UL. Replies whose parent isn't on the page (kernel
permits orphans after a parent is deleted) surface at the top
level so they stay visible.

Two kernel-contract details baked in:
  * `parent` on the wire is a `comtrya://comment/<id>` URI — the
    component wraps the bare id at post time and strips the
    prefix when grouping so the in-memory tree keys cleanly.
  * The `:scope > li.comment-row` count selector (iter 58) is
    unaffected because replies live in a separate UL, so the
    "N comments" header now counts top-level only.

Verified live on /x/issues/.../2: pressing "reply" on an existing
comment opens an inline composer; submitting via Cmd-Enter
appends the reply as a child row, the composer closes, and the
reply persists nested under its parent on reload.

This commit was created with the assistance of a LLM.
A freshly-posted comment shows "just now" and stayed that way
until the page reloaded. Add a 60-second tick that walks every
`<time datetime=...>` inside the thread and recomputes the text
via the same `relativeTime` helper used at render. After a minute,
"just now" rolls forward to "1m ago"; an hour later, "1h ago".

The interval is set up in `connectedCallback` and torn down in
`disconnectedCallback` so navigation away from the page doesn't
leak timers. Cheap walk — a few dozen `time` elements per thread
at most.

This commit was created with the assistance of a LLM.
OKLCH color palette, glass surfaces with backdrop-filter blur,
Geist/Instrument Serif type stack, 0.5px borders, and dark-first
color-scheme. Renames all design tokens across the shell and every
first-party extension UI bundle.
Adds the utility primitives from the Claude Design handoff bundle:
glass/glass-thin, hairlines, chips (chip-ok/warn/err/info/accent + chip-mono/dot),
buttons (btn/btn-ghost/btn-primary + btn-sm/btn-lg), avatar, tabs, progress bar,
sparkline strokes, window chrome dots, cmt-logo/cmt-glyph, code-row + diff +
syntax tokens, eyebrow, kbd, mark, plus --add/--del color tokens.

Additive only — coexists with the existing .chip.ok style API; no existing
selector is modified.
Below 640px:
- Topbar drops the search-text label, keeps the kbd hint
- Sidebar repository and recent lists scroll horizontally with snap points
- Footer nav (Instance/Health/Settings) wraps as a flex row
- Buttons grow to 32px height for thumb-friendly hit areas

Below 720px:
- Workspace summary grid drops to 2-up

Below 860px (existing breakpoint, refined):
- Workspace summary grid stays at 2-up instead of collapsing to 1
feat(frontend): dark glass design system + utility primitives + mobile responsive
Five new routes ported pixel-faithfully from the Claude Design handoff bundle:
- /pipelines — stage bar, job-graph DAG with bezier connectors, syntax-highlighted log viewer
- /releases — featured release hero (serif version, italic tagline), highlights, assets, verification box, past-release timeline
- /admin — Forge overview: gauges, services, traffic chart, recent admin events
- /admin/access — owner, collaborators, access tokens
- /admin/storage — storage cards, snapshot timeline

Shared Vue primitives extracted to /components: Icon (SVG icon library), Chip, Avi (gradient avatar), Sparkline, Kv (key-value), AdminNav.

All pages use the design-system utility classes (.glass, .chip-*, .btn, .eyebrow, .mono, .serif) and tokens added in the prior PR. Data is hardcoded; backing APIs will land in follow-up extensions.
…row widths

Below 640px the workspace sidebar is hidden and a fixed glass tab bar
takes over as the primary nav. Five items: Home, Inbox, CI, Releases,
Admin. Rounded 22px corners, backdrop-blur, safe-area-aware padding.

Page content gets bottom padding so the tab bar doesn't overlap the
last row. Topbar gets safe-area-inset-top so it doesn't sit under the
notch on iOS.
Adds the 56px SideRail icon nav from the Claude Design handoff, sitting
to the left of the workspace sidebar (VS Code-style activity bar +
explorer pattern). Five primary icons (Home, Repositories, Inbox,
Pipelines, Releases) and two footer icons (Forge admin, Settings),
each with an accent rail indicator on the active route.

The "Repositories" rail icon links to a new /repos page that lists
every repo in the workspace as a grid of glass cards. The wide
workspace sidebar collapses below 860px and both rails hide entirely
below 640px, where the floating MobileTabBar takes over.
Replaces RepoHome view=code with a dedicated RepoCode route that mirrors
the Claude Design handoff: repo header with tabs, branch + go-to-file
sub-toolbar, 260px file tree, file viewer with breadcrumbs, last-commit
strip, source/blame/history toolbar, syntax-highlighted code body
(tok-key/str/num/cm/fn/ty/pn), and a co-pilot file digest panel.
New /r/:groups+/:repo/pulls/:id/review route mirroring the Claude Design
handoff: PR header with stack progress strip, 280px stack rail with
layer items + reviewers + checks, main diff pane with AI layer digest,
syntax-highlighted diff rows (add/del marker columns, line-number
gutters), inline conversation thread with co-pilot suggested
resolution, /command compose bar, and layer-level approve controls.
New /r/:groups+/:repo/issues/board route mirroring the Claude Design
handoff: serif page title, search box with kbd hint, saved views +
new-issue buttons, 220px filter rail (state / type / milestones /
people with avatars), List/Board/Timeline view toggle, milestone
progress glass card with bar, and a card of issue rows with tag chips,
state icon, comments, assignee avatars, reactions, and pinned-issue
accent background.
Stronger glass panel — opaque rgba(14,16,20,0.86) background with
blur(36px) saturate(180%) and a softer rgba(255,255,255,0.18) border
plus three-layer box-shadow. Light-mode variant included.
Group eyebrows now float without surface backgrounds or dividers
(8px top margin). Active row gets rounded 8px corners with
accent-soft background + accent-line border. Mark highlights switch
from underlined-bold to accent-tinted background pills. Footer
darkens to rgba(0,0,0,0.25) with tighter alignment.
Adds /r/:groups+/:repo/pipelines and /r/:groups+/:repo/releases
mounting the existing Pipelines.vue and Releases.vue components.
The repo header tabs in RepoCode (Overview / Code / PRs / Issues /
CI / Releases) are now RouterLinks so the CI and Releases tabs jump
to the per-repo pages.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants