Skip to content

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

Open
rawkode wants to merge 228 commits into
mainfrom
v3
Open

v3: Comtrya — combine v1 Smart-HTTP/OCI/federation with v2 kernel discipline#1
rawkode wants to merge 228 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

Brings forward v2's kernel-vs-features split, standards-based foundation,
and spec discipline. Pulls in v1's pure-Rust Smart HTTP v2 Git server,
OCI extension distribution, and federated GraphQL schema composition.

This branch executes the plan incrementally; each increment is its own
commit so the PR shows a real progression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 and others added 7 commits May 11, 2026 18:16
Mass rename:
- Forgepoint -> Comtrya (display name)
- forgepoint -> comtrya (identifiers, paths, package names)
- FORGEPOINT -> COMTRYA (env vars)
- wit/forgepoint-extension.wit -> wit/comtrya-extension.wit
- frontend/src/server/forgepoint.ts -> frontend/src/server/comtrya.ts
- Cargo crate names: forgepoint-{core,server,cli} -> comtrya-{core,server,cli}
- WIT package: forgepoint:extension -> comtrya:extension

cargo check --workspace passes. The one historical reference to the v1
directory path on disk (forgepoint-dev/forgepoint) is preserved in V3_PLAN
since that is its actual filesystem location.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings forepoint v1's crates/git-http into v3 as comtrya-git-http.
Contents (~1880 LOC): pkt-line codec, protocol v2 negotiation, pack
generation, repo provider trait, axum handler scaffolding. Supports
upload-pack (clone/fetch); receive-pack is a follow-up increment.

Adjustments:
- Crate renamed to comtrya-git-http and added to workspace.
- Env vars renamed: FORGE_GIT_HTTP_EXPORT_ALL -> COMTRYA_GIT_HTTP_EXPORT_ALL,
  FORGE_GIT_SMART_V2_ADVERTISE/BACKEND -> COMTRYA_GIT_SMART_V2_ADVERTISE/BACKEND.
- Default advertise/backend flipped to "rust" (pure-Rust path, not git shell-out).
- tokio-util "io" feature enabled for ReaderStream.

Next increment: wire into crates/server, drop git http-backend shell adapter
for upload-pack.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the shell adapter (git http-backend) on the upload-pack path
with comtrya-git-http's pure-Rust implementation. The shell adapter
remains as a fallback selectable via COMTRYA_GIT_BACKEND=legacy.

Changes:
- crates/server: add PureRustGitState (RepositoryProvider + GitHttpState)
  living on AppState, constructed from runtime.demo_repository.project_root.
- crates/server git_endpoint: parse {seg}/{seg}/{suffix} and dispatch into
  comtrya_git_http::v2::dispatch with the parsed segments + service.
- crates/server demo repo seeding: touch git-daemon-export-ok on init
  (and on the idempotent re-open path) so the new lib treats it as exported.
- crates/git-http: add pub `dispatch` entry-point for ergonomic host wiring,
  make `handle_upload_pack` pub, prefix advertisement with the
  Smart HTTP service banner (matches git http-backend behavior).
- crates/git-http: tests now disable gpg signing so local gitsign configs
  don't break seeding.
- extensions: refresh entryIntegrity hashes after the Comtrya rename.

cargo test --workspace passes: 77 core, 16 git-http, 29 server, 1 MVP.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends comtrya-extension.wit with first-class host imports: host-log,
host-events, host-storage, host-git, host-http, host-secrets, host-jobs.
Each interface documents its scope, isolation guarantees, and expected
host enforcement.

This locks in the v3 extension contract. The current minimal .wat
components still ship a numeric proof export; a follow-up increment
wires the host Linker to provide these imports and migrates one
first-party extension to the typed protocol end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New crate comtrya-extension-oci, ported from v1's
crates/server/src/extensions/{oci_fetcher,cache}.rs. Provides:

- OciExtensionFetcher: fetch by registry+image+(tag|digest) with retries,
  exponential backoff, offline-mode fallback, optional checksum verification.
- ExtensionCache: content-addressed cache under a host-managed dir,
  per-entry CacheMetadata (provenance + integrity).

12 ported tests pass. Next increment: extend CUE config to reference
extensions by OCI ref and wire the fetcher into the host's
load_extension_runtime path so first-party extensions can ship via OCI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds typed extension installation config to the core contract:
- ExtensionSource::{Local{path}, Oci{registry,image,reference}}
- OciReference::{Tag, Digest}
- ExtensionInstallConfig with id, source, enabled
- InstanceConfig now carries Vec<ExtensionInstallConfig>; validate()
  rejects empty fields and duplicate ids.

config/config.cue gains an example extensions block referencing three
first-party packages via OCI. The hand-rolled CUE loader does not yet
parse this section; the values are documentation today and become
authoritative once the CUE parser is upgraded (separate increment).

Three new core unit tests cover OCI validation, duplicate-id rejection,
and empty-reference rejection. cargo test --workspace: 138 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
V3_STATUS.md is an up-to-date snapshot of what shipped in this branch
and what's deferred. V3_PLAN.md's increment list now marks completed
work and explicitly tracks the deferred items for follow-up branches.

138 tests passing. Pure-Rust Git is the default upload-pack path;
legacy git http-backend is still reachable for regression compare.
Extension distribution and config contracts are in place; their host
wiring is the principal piece of follow-up work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@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 30 commits May 15, 2026 03:25
Iteration 47. Resolves the long-pending user-requested TODO.
ShortcutsOverlay shed its hand-edited "Global" list that had
drifted from reality after iters 18-24's library migration.
The overlay now has two kinds of sections.

**Live sections** (badged `live`) sourced from
`listCommands() / subscribeCommands()`. Every `registerCommand`
entry with a `.shortcut` field appears here, grouped by
category — `Navigation` (g-chord nav), `Projects`,
`Repositories`. Re-renders automatically when commands
register/unregister (project + repo commands change when you
navigate between repos per iter 22/25). Subscription torn
down on unmount.

**Static sections** — shortcuts that are intrinsically scoped
per surface (j/k inside lists, m/x on PullsDetail, n/p in
DiffView). Don't live in the global registry because they only
make sense when the relevant surface is focused. Kept as a
hand-curated cheat sheet:

- `Global (always)` — Cmd-K + `?`
- `Lists` — j/k/↵/`/`/c/o/x/a
- `Issue / epic detail` — j/k/↵/Esc
- `PR detail` — m/x/n/p/[/]/Esc

Static list updated to match the iter 17 / 19 / 20 reality (`c`
for create, `x` for closed filter, `n/p/[/]` for diff nav).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iteration 48. Mirrors iter 35 (assignee filter on IssuesList)
for epics: click any owner chip on an EpicCard, queue narrows
to that owner's epics, URL becomes `/x/epics/?owner=<urn>`.

EpicCard becomes a controlled component for owner UI:
- `activeOwner?: string | null` prop drives the chip's active
  state (inverted ink/paper when matching).
- `@owner-click` emit fires on click with the URN; parent owns
  the filter state. Chip is now a real `<button>` with
  `@click.prevent.stop` so it doesn't navigate.

EpicsList:
- `ownerFilter` ref URL-synced as `?owner=<urn>` via the
  existing read/write/popstate trio from iter 37. Only accepts
  canonical `comtrya://` URNs.
- `epics` computed narrows by `ownerRef === ownerFilter` when
  set.
- `toggleOwnerFilter(ref)` handler bound to `@owner-click`.
- New `.epics-owner-filter` indicator strip below the state
  row when a filter is active: `owner · rawkode · clear ✕`.

Combines with state filter:
- `/x/epics/?state=in_progress&owner=comtrya://user/rawkode`
- `/x/epics/?state=planned&owner=comtrya://team/platform-maintainers`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iteration 49. Mirrors iter 46 (project filter on IssuesList)
onto epics. Click any project chip on an EpicCard, queue
narrows to that Project, URL becomes
`/x/epics/?project=kernel`.

EpicCard:
- New `activeProject?: string | null` prop drives chip's active
  state (inverted ink/paper).
- New `@project-click` emit; chip becomes a `<button>` with
  `@click.prevent.stop`.

EpicsList:
- New `projectFilter` ref URL-synced as `?project=<name>`
  validated against `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`.
- **Prop wins** rule (iter 46 pattern): when on a project
  page, URL filter is ignored and not written.
- `toggleProjectFilter(name)` wired to `@project-click`.
- `.epics-project-filter` indicator strip when active:
  `project · ◇ kernel · clear ✕`.

Combined filters now work:
- `/x/epics/?state=in_progress&project=kernel`
- `/x/epics/?owner=comtrya://user/rawkode&project=frontend`
- `/x/epics/?state=planned&owner=...&project=kernel`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iteration 50. Mirrors iter 35 (issue assignee filter) onto the
PR queue: click any author chip on a PR row, queue narrows to
that author, URL becomes `/x/pulls/?author=<urn>`.

PullsQueue:
- `authorFilter` ref URL-synced as `?author=<urn>` via the
  existing read/write/popstate trio. Only accepts canonical
  `comtrya://` URNs.
- `filtered` narrows by `pull.authorRef === authorFilter`.
- Row author chip → `<button>` with `@click.prevent.stop` so
  clicking filters instead of navigating. Active chip flips to
  inverted ink/paper; hover gets a dashed border.
- `.pulls-author-filter` indicator strip when active:
  `authored by · <chip> · clear ✕`.
- Dropped redundant agent/bot/bot `.author-badge` spans —
  data-author-kind colour conveys it (same cleanup iter 41
  did on PullsDetail).

Combines with state + search:
`/x/pulls/?state=merged&author=comtrya://user/rawkode&q=auth`.

Identity-axis filter now complete across all three queues:
- Issues `?assignee=` (iter 35)
- Epics `?owner=` (iter 48)
- Pulls `?author=` (iter 50)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iteration 51. First concrete progress on the "Bulk actions
everywhere" macro bet. Linear pattern: space toggles row
selection, action bar appears above the list with count +
bulk-close, Esc clears.

IssuesList:
- `selectedIds: Set<string>` with `toggleSelection(id)` swapping
  the Set immutably for Vue reactivity.
- `space` shortcut added to useShortcuts — toggles the focused
  row's id.
- `Esc` shortcut clears selection (compounds with the existing
  search-input @keydown.esc; global Escape only triggers when
  no input is focused AND `selectedIds.size > 0`).
- `.selected` row class adds a 3px `--ink` inset shadow on the
  left edge; turns `--accent-teal` when also focused.

Bulk action bar:
- Sticky above the list, inverted ink/paper.
- `N selected · close N · clear · esc` plus `space toggle row`
  hint.
- `closeSelected()` fires `Promise.allSettled(closeIssue(id))`
  in parallel. Failures stay selected for retry; successes get
  optimistically swapped into `loaded.value`.

Three rows selected → click `close 3` → all three flip to
CLOSED in one round-trip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires `repository.blobs { path preview size }` into the shell
repo home and renders the top-level `README{,.md,.mdx}` blob
through a tiny markdown shim, so the repo header now lands
on actual content instead of jumping straight to the slot
stack.

- frontend/src/markdown.ts: paper-thin markdown renderer
  (headings, paragraphs, fenced code, lists, inline marks).
  Deliberately mirrors `ext_docs/ui/src/markdown.ts` and
  `ext_epics/ui/src/markdown.ts`; future shiki + rehype
  upgrade swaps internals without changing the call site.
- frontend/src/routes/RepoHome.vue: extends the query +
  identity type with `blobs[]`, picks the first top-level
  README match (case-insensitive, prefers `README.md`), and
  renders the preview as a paper-card section between the
  ProjectsPanel and the slot stack. Surfaces a `preview`
  chip when the kernel returned a truncated preview.
- frontend/src/styles.css: `.repo-readme*` rules — paper
  card with mono header strip and editorial-class body
  typography matching the chip-row aesthetic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ering

Pulls the three diverging markdown renderers
(`frontend/src/markdown.ts`, `ext_docs/.../markdown.ts`,
`ext_epics/.../markdown.ts`) into a single canonical
implementation in `@comtrya/sdk-vue`, and wires the missing
fourth consumer — PR detail descriptions — which had been
rendering as `<pre>{{ bodyMarkdown }}</pre>` since the iter-2
PullsDetail rewrite.

- frontend/packages/sdk-vue/src/markdown.ts: canonical
  `renderMarkdown(body)` + `bodyExcerpt(body, limit?)`.
  Tokens: headings 1-6, paragraphs, fenced code with
  `data-lang`, ordered + unordered lists, inline
  code/bold/italic, plus bare-URL autolinking with
  `rel="noopener noreferrer"`. Everything else escapes to
  HTML-safe text. Code spans are stashed behind PUA
  sentinels before the URL/bold/italic pass and restored,
  so a backticked literal can't be re-parsed mid-emphasis.
- frontend/packages/sdk-vue/src/index.ts: re-exports
  `renderMarkdown` + `bodyExcerpt`.
- Deletes the three duplicate `markdown.ts` files and
  updates `RepoHome.vue`, `DocsPanel.vue`, and
  `EpicDetail.vue` to import from `@comtrya/sdk-vue`.
- extensions/first-party/ext_pull_requests/ui/src/
  PullsDetail.vue: swaps the `pre-wrap` description block
  for `<div v-html="renderedBody">` and adds a
  `.pulls-detail-body-prose` editorial typography block
  matching the prose styles already on `EpicDetail` and
  `RepoHome`'s README section.
- Rebundled `ext_pull_requests`, `ext_docs`, `ext_epics`;
  manifest `entryIntegrity` hashes refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n Closes panel

The PR detail "Closes" panel (originally surfacing the
merge-reactor's `closes` relations) becomes Project-spine
aware and keyboard-first.

- api.ts: `LinkedIssue` gains `projectName` and
  `workspaceId`. `resolveIssue` reads `projectName` from the
  `ext_issues/by-ref-issue` shape and parses `workspaceId`
  from the issue's repository / workspace URN.
- PullsDetail.vue: linked issue rows now render
  `[ # ][ title ][ ◇ project ][ state ]`. The project chip
  hyperlinks to `/x/issues/?project=<name>` so a click jumps
  straight into the issue queue filtered to that project -
  matching the URL filter recipe already on IssuesList.
- New `focusedLinkedIdx` ref + `j`/`k`/`Enter` shortcuts
  through `useShortcuts`. Hover sets focus; the focused row
  gets a 3px ink inset shadow on the left and a paper-tint
  background. Footer hint chip: `j k walk · ↵ open`.
- Subscribes to `dev.comtrya.issues.{opened,closed,reopened}`
  on mount and re-runs `loadLinked()` on each event so
  linked issues update without a refresh; tears down on
  unmount.
- Link href fix: previously built `/x/issues/<id>` which did
  not match the registered route `/:workspaceId/:number`.
  Now uses the resolved workspaceId + number so the link
  actually opens the issue.

Verified end-to-end on dogfood: PR #43 (the merge-reactor
PR) resolves its `closes` relation, the resolved issue
renders with state + project chip, j/k navigation walks the
rows, Enter opens the issue. Bundle rebuilt,
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…hared parseQueryFilters

Adds `is:open`, `is:draft`, `is:merged`, `is:closed`, `is:all`,
and `author:<urn>` token syntax to the PR queue search input,
plus an inline chip strip that renders the parsed filters
and surfaces "unknown filter" hints for unrecognised keys.

The parser lives in `@comtrya/sdk-vue` as a generic helper
so IssuesList and EpicsList can adopt the same vocabulary
next iteration without duplicating the tokeniser.

- frontend/packages/sdk-vue/src/parse-query.ts: new
  `parseQueryFilters(input, knownKeys)` → `{ text, filters,
  unknown }`. Single-pass regex tokeniser with strict key
  character class so URN-shaped values
  (`comtrya://user/rawkode`) parse as values, not nested
  keys. Quoted values keep internal whitespace.
- frontend/packages/sdk-vue/src/index.ts: re-exports.
- extensions/first-party/ext_pull_requests/ui/src/
  PullsQueue.vue:
  - `effectiveStateFilter` derives the row state from `is:`
    tokens, falling back to the row chip when no token
    matches.
  - `effectiveAuthorFilter` accepts any `comtrya://` URN
    from `author:` tokens.
  - `filtered` reads from the parsed-text remainder so
    `is:draft code` finds DRAFT PRs whose title matches.
  - `queueFilterChips` renders an editorial chip strip
    above the queue (tealed `is · draft`, ink `author ·
    <classifier label>`, dashed-warn `unknown · <key>:`).
  - Placeholder hint advertises the syntax.

Verified end-to-end against dogfood (1 READY, 2 DRAFT, 1
MERGED): every token combination filters as expected;
typecheck + shell build + ext bundle clean;
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…/assignee:/is:

Adopts the iter 55 `parseQueryFilters` helper on IssuesList,
adding the Project-spine token `project:<name>` plus
`is:<state>` and `assignee:<urn>`. Two of the three planning
queues now share one tokeniser.

- `ISSUES_FILTER_KEYS = ["is", "assignee", "project"]`.
- `effectiveStateFilter` / `effectiveAssigneeFilter` /
  `effectiveProjectFilter` derive from parsed tokens; fall
  back to the URL-pinned refs when no matching token. Token
  wins for the duration of the search; clearing the input
  restores the chip state. `props.projectName` still wins
  regardless when the list is mounted on a project page.
- `STATE_TOKEN_TO_FILTER` maps `open`/`closed`/`reopened`/
  `all`; `reopened` collapses to OPEN so the queue matches
  the chip set.
- `filtered` reads the parsed-text remainder so
  `project:kernel reactor` finds kernel issues whose title
  matches "reactor".
- `queueFilterChips` renders the chip strip above the queue:
  tealed `is · open`, ink `→ <label>` for assignees,
  `--accent-blue` `◇ <project>` for projects (Project-spine
  tone), dashed-warn for unknowns. Placeholder hint
  advertises the syntax.

Verified parser × filter logic across a synthetic issue set
(dogfood seed has no projects/assignees yet); every
combination - including unknowns + free text - filters as
expected. typecheck + ext_issues bundle clean,
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…/project: completes the trio

EpicsList adopts the iter 55 `parseQueryFilters` helper so
all three planning queues (PullsQueue, IssuesList,
EpicsList) share one tokeniser and one chip-row aesthetic.
Project spine is now expressible as `project:<name>` across
every queue.

- Adds the previously-missing search input (EpicsList had
  URL-pinned chip filters but no free-text or token entry).
- `EPICS_FILTER_KEYS = ["is", "owner", "project"]`.
- `STATE_TOKEN_TO_FILTER` maps `planned`, `in-progress` /
  `in_progress` / `inprogress`, `done`, `canceled` /
  `cancelled`, `all`. Both spelling variants accepted so
  GitHub and Linear muscle memory work.
- `effectiveStateFilter` / `effectiveOwnerFilter` /
  `effectiveProjectFilter` derive from tokens with URL-
  pinned ref fallbacks. `props.projectName` still wins
  when mounted on a project page.
- `epics` computed reads the parsed-text remainder for
  substring match against title + owner short label +
  projectName.
- `queueFilterChips` renders the editorial chip strip:
  tealed `is · in progress`, ink `→ <owner>`,
  `--accent-blue` `◇ <project>`, dashed-warn for unknowns.
  Same palette / glyphs as iter 56.
- URL state extended to `?q=<search>` so tokenised views
  are shareable links.

Verified parser × filter logic across a synthetic epic set
covering all states + multiple owners + projects. typecheck
+ ext_epics bundle clean, `entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends `bindProjectCommands` so every CUE Project gets four
palette entries, not one. Compounds the iter 55-57 URL
filter shape: the queues already understand `?project=<name>`,
so the palette now binds a keyboard verb per Project for
each planning surface.

For each `comtryaConfig.projects[]` entry on the current
repo route:
- `Switch to project <name>` (already shipped).
- `Open issues in <name>` →
  `/x/issues/?project=<name>` (state defaults to OPEN).
- `Epics in <name>` → `/x/epics/?project=<name>`.
- `In-progress epics in <name>` →
  `/x/epics/?project=<name>&state=IN_PROGRESS`.

All four register/unregister on the same route-change cycle
as before. Verified against `/r/comtrya/dogfood`: the kernel
returns three projects (ext_docs, frontend, kernel), so the
palette adds 12 project commands on visit. typecheck + shell
build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Detail

When an issue or epic is scoped to a Project, the detail page
now surfaces the Project's CUE-declared `owners[]` as a
"Routed to" panel. Classifier-glyph chips keyed by author kind
(team teal, human ink, agent purple, bot blue, credential
yellow) plus a header link back to the project-scoped queue.

Makes the Projects spine visible at the ownership-routing
level, not just the filter level - iters 55-58 surfaced
`project:<name>` in queues and the palette; iter 59 surfaces
the ownership side of the same CUE config.

ext_issues:
- `IssueDetail.vue` imports `resolveIssuesPolicy` (already
  extracts `ownerRefs`).
- `policy` ref + watcher re-resolves on
  `issue.projectName` change.
- New sidebar `<section>` titled "Routed to" with `◇
  <project>` link header and chip list of classifier-toned
  owner refs. Footer attribution: `From package comtrya ·
  projects.<name>.owners`.

ext_epics:
- New `project-policy.ts` mirroring the issues policy shape,
  scoped to `ownerRefs`. Kept extension-local; can promote
  to sdk-vue when a third consumer appears.
- `EpicDetail.vue` adds `projectPolicy` ref + watcher and
  renders an `.epic-routed` paper-card between the progress
  bar and the body. Header link goes to
  `/x/epics/?project=<name>` (the iter 57 filter URL).
- `epic-detail-styles.ts` extended with the panel styles.

Verified against `/r/comtrya/dogfood`'s CUE config: three
projects each declare real owners (platform-maintainers,
frontend-maintainers, rawkode). typecheck + both ext bundles
clean, `entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ProjectHome was a passive dashboard with dead numbers; iter 60
turns the summary strip into the spine's primary navigation
surface and adds two quick-create entrypoints so opening work
in this Project is one click away.

- `projectQueueHrefs` computed centralises the URL shape for
  every stat: open/closed issues, in-progress/planned/done
  epics. All use the URL filter recipe from iter 46
  (IssuesList) and iter 57 (EpicsList).
- Each `.stat` becomes a `<RouterLink>` with a hover
  underline. Doc-count keeps `.stat-static` (no doc-filter
  URL yet).
- New `.project-quick-actions` strip below the summary:
  `+ new issue` → `/x/issues/new?projectName=<name>`,
  `+ new epic` → `/x/epics/new?projectName=<name>`. Both
  routes already accept `projectName` from URL params and
  pre-fill the CUE policy.
- Inverted ink-on-hover treatment so the affordance is
  unmistakable.

Verified end-to-end against dogfood: three declared projects
(ext_docs, frontend, kernel) all surface in the switcher;
summary cards route to the correct filter URLs; new-issue /
new-epic routes resolve. typecheck + shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… PullsDetail

PullsDetail joins IssueDetail and EpicDetail in surfacing CUE
Project ownership. A PR routes through Projects two ways: the
diff's changed paths (iter 42 `affectedProjects`) and the
issues it closes (iter 54 `linkedIssues[].projectName`). The
new panel unions both, then resolves CUE-declared owners per
project.

- `CueProject` interface extended with `owners: { kind, slug,
  ref }[]` (the typed `#Ref` family from iter 26 - the field
  was already in the GraphQL response, just unread).
- `projectsFromLinks` computed dedupes `linkedIssues[].projectName`.
- `routedProjectsWithOwners` unions `affectedProjects` ∪
  `projectsFromLinks` and looks up each project's owners
  from the already-loaded `comtryaConfig.projects[]`. A
  docs-only PR closing a kernel issue surfaces both
  `ext_docs` and `kernel` even though the diff only touched
  the docs root.
- `classifyOwner()` matches the iter 59 IssueDetail /
  EpicDetail glyph palette so the panel reads identically
  across every detail surface.
- `<section class="pulls-routed">` renders between the
  Description and the Closes panel. Each project gets a
  header link to `/x/issues/?project=<name>` plus classifier-
  toned owner chips (team teal, human ink, agent purple,
  bot blue, credential yellow).

Verified against dogfood data: simulating a docs-only PR
closing kernel + frontend issues, the derivation returns
all three projects with their full owner sets. typecheck +
ext_pull_requests bundle clean, `entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…duplicates

The URN-to-visual-identity classifier was duplicated across
six call sites with drifting glyphs and missing kinds. Iter
62 promotes a single canonical helper to sdk-vue and
re-routes every caller through it.

- `frontend/packages/sdk-vue/src/classify-principal.ts`:
  `classifyPrincipal(value)` returning `{ kind, label, glyph,
  tone }`. Kind union covers humans (initial-letter glyph),
  agents (✦), bots (◆), credentials (⚙), and teams (◇) - the
  superset of all prior variants. Preserves the `tone` field
  the PR types used for tone-class lookup.
- `ext_pull_requests/types.ts`: replaces local `classifyAuthor`
  + `authorLabel` + `AuthorIdentity` + `AuthorKind` with
  re-exports from sdk-vue. AuthorKind now includes `team`.
- `ext_pull_requests/PullsDetail.vue`: deletes iter-61 inline
  classifyOwner; rebinds to the canonical helper.
- `ext_issues/IssuesList.vue` + `IssueDetail.vue`: deletes
  the inline `authorLabel` copies, imports the canonical.
- `ext_epics/issue-rows.ts`: replaces `classifyIssueAuthor`
  with the canonical re-export. Drops the bespoke `●`/`◉`/
  `○` glyphs so issue-row authors render identically to every
  other surface.
- `ext_epics/EpicDetail.vue`: deletes iter-59 inline
  classifyOwner; rebinds via classifyIssueAuthor.

Net: -110 lines duplicate classifier, six surfaces now share
one source of truth, the diverging ext_epics glyphs are gone.
typecheck + shell build + all three extension bundles clean,
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Compounds iter 62's cleanup pattern. The CUE projects fetch
was duplicated across `frontend/src/project-commands.ts`
(palette verbs per Project) and
`ext_epics/ui/src/project-policy.ts` (Routed-to panel on
EpicDetail). Promote to sdk-vue, share one reader.

- `frontend/packages/sdk-vue/src/comtrya-config.ts`:
  `fetchComtryaProjects(segments?)` returns the merged CUE
  projects array - reads segments from the location when
  not provided, returns `[]` on any failure.
  `resolveProjectOwners(projectName, segments?)` returns
  just the owner URNs for one project. `ComtryaProject`
  keeps an open shape so extension-specific sub-policies
  flow through without enumeration.
- `frontend/src/project-commands.ts`: drops the inline
  GraphQL query + fetchProjects + CueProject (~45 lines);
  calls `fetchComtryaProjects(segments)` instead. The
  four-verb palette registration loop is unchanged.
- `ext_epics/ui/src/project-policy.ts`: collapsed from 82
  lines to a thin facade over `resolveProjectOwners`. The
  `ProjectPolicy` interface and `EMPTY_POLICY` constant stay
  exported so EpicDetail.vue's import shape doesn't change.

Kept as-is for later iterations: ext_issues/policy.ts
(reads same config + issues sub-policy) and PullsDetail.vue
(batches CUE config with the diff fetch).

Verified against `/r/comtrya/dogfood`: canonical query
returns three projects with full owner sets - identical
shape to pre-refactor code. typecheck + shell build +
ext_epics bundle clean, `entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…from the canvas

First visible product surface built on the iter 63
fetchComtryaProjects helper. Until now you had to visit a
specific repo before you could see its CUE Projects. The
workspace home now aggregates every Project declared by
every repo so the spine is visible from the canonical
canvas.

- New `projectRows: ProjectRow[]` ref aggregated by
  `refreshAllProjects()` after repos resolve. Calls
  `fetchComtryaProjects(segments)` per repo and flattens to
  `{ repoPath, segments, project }` rows. Sorted by project
  name then repo path so same-named projects across repos
  cluster together.
- `projectHomeHref()` builds `/r/<repo>/p/<project>`;
  `projectOwnerRefs()` surfaces the typed-#Ref owner URNs.
- New rail section `.home-projects` above the existing
  extension slots. Per row: `◇ project · repo path` on a
  clickable link, plus chip strip of owners via
  classifyPrincipal (iter 62 canonical) for glyph + tone
  palette. Footer attribution: `From package comtrya across
  every repo in this workspace`.
- New `.home-projects-*` styles in shell `styles.css`
  inheriting the editorial top-border + panel-heading
  aesthetic. Owner-chip tones key off `data-author-kind`
  matching every iter 59/61 routing surface.

Verified end-to-end against `/r/comtrya/dogfood` + the three
other seed repos: aggregation surfaces six rows total -
dogfood contributes ext_docs/frontend/kernel with full owner
sets, three other repos contribute their default `repo`
project. typecheck + shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… panel

Compounds iter 64. The Projects panel surfaced every CUE
Project across the workspace with owner chips, but rows were
informational - no work counts. Iter 65 makes each row
actionable: open-issue count, in-progress-epic count, and
closed count chip, each clickable into the iter 60 URL-filter
recipe.

- New `projectCounts: Record<string, ProjectCounts>` ref
  bucketing per-project work tallies (openIssues,
  closedIssues, epicsPlanned, epicsInProgress, epicsDone).
- `refreshProjectCounts()` runs two workspace-wide ops calls
  in parallel (`ext_issues/list-issues` +
  `ext_epics/list-epics`), buckets by projectName. Two ops
  total regardless of project count - much cheaper than
  per-project fan-out.
- Triggered on `[workspaceId, repositories]` change. Live-
  refreshed via existing issues SSE (extended to also fire
  refreshProjectCounts) plus new `dev.comtrya.epic.{created,
  state-changed}` subscriptions.
- Template renders three count chips per row - open issues
  / in-progress epics / closed - each a `<RouterLink>` to
  the filtered queue. Zero counts dim via `data-zero` so the
  eye lands on actionable numbers. Tabular-nums + display
  font for the count, mono faint label, hover underline.

Verified against dogfood: seed has no `projectName`-tagged
issues or epics yet, so counts render as 0 across all 6
panel rows. The instant a team stamps projects on
issues/epics, counts populate live without refresh. typecheck
+ shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… the dogfood loop

Closes the loop iters 59-65 exposed. Every Project-spine
surface (filter tokens, palette commands, owner panels,
workspace Projects panel, per-project counts) lights up the
moment an issue or epic carries `projectName`. Until iter
66, you had to navigate to a Project page or hand-craft the
URL to stamp it. The new forms let any operator pick a
project at creation from any entry point.

ext_issues issueNewForm:
- New `<select>` populated from `fetchComtryaProjects()`
  (iter 63). Defaults to `context.projectName` when set,
  otherwise `— no project —`.
- Wraps the existing label / closeOnMerge policy logic in
  `applyPolicy(projectName)` so chips + label hints refresh
  live on dropdown change. `lastPolicyAutoLabels` tracks
  auto-fill so swapping projects strips the prior labels
  while preserving user-typed ones.
- Header overline reflects the selection.
- Submit reads `projectSelect.value` instead of the route's
  context.

ext_epics epicNewForm:
- Same picker pattern (no CUE policy chips on epics yet).
- Heading updates on change.
- Submit reads `projectSelect.value`.

Verified against `/r/comtrya/dogfood`: helper resolves
ext_docs/frontend/kernel; both forms ship a populated
`<select>`. The seed's 0/0/0 project counts will populate
the instant a real issue/epic is opened with a project.
typecheck + ext_issues + ext_epics bundles clean,
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iter 66 closed the loop on new issues - the picker stamps
`projectName` at creation. Issues opened before iter 66 (and
the dogfood seed) stay untagged forever without a mutation.
Iter 67 ships the kernel/WIT slice that unlocks retroactive
Project assignment; iter 68 will add the UI editor.

- `wit/issues.wit`: bump 0.1.6 -> 0.1.7. New
  `assign-project-input { id, project-name: option<string> }`
  + `assign-project: func(input) -> result<issue, error>`.
- `component/src/lib.rs::assign_project`: reads + mutates the
  stored issue via the canonical `storage::update_begin ->
  update_commit` pattern. Trims/normalises the incoming name
  so blank/whitespace reads as `None` (matches `open-issue`
  shape). No-op writes (same project as currently stored)
  return the existing snapshot without emitting - keeps the
  SSE stream quiet on idempotent UI calls.
- Emits new `dev.comtrya.issues.project-changed` carrying
  both `previousProject` and `projectName` so the iter-65
  per-project counts on WorkspaceHome can shift their
  tallies in a single bucket-swap.
- `ProjectChangedPayload` sibling of `IssueEventPayload`.
- cargo-component built the new wasm; wit-codegen wrote 10
  ops to `dist/ext_issues.client.ts` (new `assignProject`).
- ext_issues UI bundle rebuilt to refresh the manifest's
  `entryIntegrity` (no UI changes in this slice).

The op is in the wasm binary but the running kernel still
serves 0.1.6 - a kernel restart picks up the new dispatch
table. Iter 68 will wire the UI editor that calls it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires iter 67's `assign-project` op into IssueDetail.
Retroactive Project tagging now works from any existing
issue's detail page; the workspace per-Project counts (iter
65) shift in real-time via the new
`dev.comtrya.issues.project-changed` SSE topic.

ext_issues:
- `api.ts`: `assignIssueProject(id, projectName)` routes
  through the wit-codegen `assignProject` client method.
- `IssueDetail.vue`:
  - `availableProjects` loaded once via
    `fetchComtryaProjects()` (iter 63 sdk-vue).
  - New "Project" sidebar panel above "Routed to": `<select>`
    populated from CUE projects, defaulting to the issue's
    current `projectName`. Header surfaces a live link to
    `/x/issues/?project=<name>`.
  - `onProjectChange()` optimistic-updates `loadedIssue.value`
    so hero chip row + Routed-to refresh immediately. Rolls
    back on failure with inline error display.
  - `projectActionState` gates the select while the op flies.
- `.issue-project-select` styling matches the existing
  form-input aesthetic.

frontend WorkspaceHome:
- Adds `dev.comtrya.issues.project-changed` subscription
  alongside existing topics; each event re-fires
  `refreshProjectCounts()` so per-Project tallies reflect
  the swap on the workspace canvas.

The dogfood kernel still runs 0.1.6 so live calls return
"op not found" until restart. On restart, every untagged
seed issue can be retroactively scoped via the picker -
populating the iter 65 counts and iter 64 Projects panel
with real numbers.

typecheck + shell build + ext_issues bundle clean,
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cDetail

Symmetric move on the epic side after iters 67+68 shipped it
for issues. One cohesive iteration covering WIT + handler +
UI + shell SSE wiring. Retroactive Project assignment now
works for both issues and epics.

- `wit/epics.wit` 0.1.1 -> 0.1.2: new `assign-project-input`
  + `assign-project: func(input) -> result<epic, error>`.
- `component/src/lib.rs::assign_project`: mirror of iter 67's
  issues impl - trim/normalise, no-op write returns existing
  snapshot without an event, storage::update_begin -> commit,
  emit `dev.comtrya.epic.project-changed`.
- `EpicProjectChangedPayload { epicID, workspaceId,
  previousProject, projectName }` - same camelCase shape as
  the issues payload so consumers parse identically.
- `ui/src/api.ts`: new `assignEpicProject(id, projectName)`.
- `ui/src/EpicDetail.vue`: inline "Project" sidebar panel
  above "Routed to". `<select>` from `fetchComtryaProjects`,
  defaults to `epic.projectName`, optimistic update + rollback
  on failure.
- `ui/src/epic-detail-styles.ts`: `.epic-project-select`
  matches the iter 68 IssueDetail picker.
- `frontend/src/routes/WorkspaceHome.vue`: subscribes to
  `dev.comtrya.epic.project-changed` so iter 65 per-Project
  counts update on retroactive reassignment.

The dogfood kernel still runs 0.1.1; on restart, the seed's
untagged epics can be retroactively scoped via the picker.
typecheck + shell + ext_epics bundle clean, `entryIntegrity`
refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror of IssuesList iter 17 - inline Linear-style epic
creation directly on the queue. Compounds iter 66's
new-epic form + iter 57's filter-token URL so a contributor
working on `kernel` epics never leaves the list to spin up a
new one.

- `quickAddTitle` / `quickAddBusy` / `quickAddError` refs.
- `quickAddProject` computed prefers `props.projectName`,
  then `effectiveProjectFilter` (iter 57 URL filter or
  `project:` token), then `null`.
- `quickAddPlaceholder` reads `New epic in <project>...` or
  `New epic...`.
- `submitQuickAdd()` calls `createEpic` with the current
  workspaceId + projectName. Optimistic prepend, then
  server refresh.
- `c` shortcut focuses; `Esc` clears + blurs.
- Inline `<form>` template above the list with `+` glyph,
  display-class title input, `◇ <project>` chip when in
  scope, hint `↵ create · esc clear · c focus`.
- SFC-scoped styles match the IssuesList aesthetic.

typecheck + ext_epics bundle clean, `entryIntegrity`
refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes a three-iteration arc:
- Iter 51 - bulk selection + bulk close.
- Iter 67/68 - kernel assign-project op + inline picker on
  IssueDetail.
- Iter 71 - bulk-pick the project from the bulk action bar so
  a team can drag a batch of untagged issues into their right
  Project from the queue canvas without leaving it.

- New `availableProjects` ref loaded once on mount via
  `fetchComtryaProjects()` (iter 63).
- `reprojectSelected(projectName)` mirrors `closeSelected`:
  `Promise.allSettled` over `assignIssueProject(id, ...)`,
  optimistic local update, failures stay in `selectedIds` for
  retry, error message names the chosen project.
- `onBulkReprojectChange()` maps a `__NONE__` sentinel to
  `null` (distinct from the placeholder option's empty
  string), resets the control so a repeat-pick still fires.
- Bulk action bar gets a new `reproject →` `<select>` with
  placeholder + "(no project)" + one option per CUE project.
- `.bulk-reproject*` styles match the inverted dark bar.

typecheck + ext_issues bundle clean, `entryIntegrity`
refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Symmetric to iters 51 + 71 - epics get the same bulk
selection + reproject affordance issues already had. One
cohesive iteration since neither half existed yet on epics.

- New `focused` + `selectedIds: Set<string>` refs with a
  watch on `epics` that clamps focus when the list shrinks.
- `availableProjects` loaded once on mount via
  `fetchComtryaProjects()`.
- `toggleSelection` / `clearSelection` /
  `reprojectSelected(projectName)` mirror the iter-71
  IssuesList shape - Promise.allSettled over
  `assignEpicProject`, optimistic update, failures stay
  selected.
- `onBulkReprojectChange()` maps a `__NONE__` sentinel to
  `null`.
- `useShortcuts` extended with `j`/`k` row nav, `" "` toggle
  selection, `Escape` clear. Literal-space key matches the
  iter-51 convention.
- Bulk action bar with count + reproject `<select>` +
  clear button + hint. Rows get `focused` / `selected`
  classes + hover sync.
- Footer hint `j k navigate · space select · c create`.
- SFC styles for focused/selected affordances + the
  inverted dark bulk-bar.

Keyboard vocabulary now matches across IssuesList +
EpicsList. typecheck + ext_epics bundle clean,
`entryIntegrity` refreshed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The repo home gets a Linear-style tab strip surfacing the
five sub-surfaces a repo has: Overview / Code / Pulls /
Issues / Checks. Clicks to find a repo's PR queue drop from
sidebar-travel to one tab, and this is the seed for the
LOOP_TODO Workbench-layout macro bet.

- New `frontend/src/components/RepoTabs.vue`. Props
  `segments` + `repositoryId`. Five `<RouterLink>` tabs:
  Overview -> `/r/<segments>`, Code -> `#code` anchor,
  Pulls/Issues/Checks -> `/x/<ext>/?repositoryId=<id>` using
  the queue URL param the extensions already accept.
- Active highlight follows `route.path`. Overview is active
  on `/r/<segments>`; other tabs intentionally don't
  highlight today (they navigate away). When the Workbench
  arc lands and queues nest under `/r/<repo>/`, the same
  `route.path` check picks up the active state.
- Mounted in `RepoHome.vue` below the clone command so the
  strip sits with the repo identity.
- `.shell-app .repo-tabs` styles: editorial bottom-border
  underline on active, no background pill, mono labels.

typecheck + shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The workspace canvas's "what just happened" feed gains a
keyboard-free project scope. Compounds iters 33 + 64: the
chip strip above the stream lets users pivot the
workspace-wide view to any single project's events without
leaving the home.

- New `activityProjectFilter` ref + `uniqueActivityProjects`
  computed that dedupes the iter-64 `projectRows` by name so
  cross-repo same-named projects collapse into one chip.
- `toggleActivityProject(name)` toggles off on repeat click.
- `<ActivityStream :project-name="activityProjectFilter ||
  undefined" />` lets the empty value pass through as no
  filter.
- `.activity-project-filter` chip strip rendered above the
  stream. Editorial bottom-border underline on the active
  chip matches iter-73 RepoTabs + iter-46 IssuesList toggles.

Verified against dogfood (3 projects → 4 buttons rendered).
typecheck + shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings the iter-65 workspace Projects-panel vocabulary to the
RepoHome ProjectsPanel. Each project card now shows open
issues / in-progress epics / closed counts, navigable to the
filtered queue, live-refreshing via the SSE topics iter 67-69
plumbed.

- New `projectCounts: Record<string, ProjectCounts>` ref +
  `refreshProjectCounts()` that runs `list-issues` +
  `list-epics` in parallel and buckets by `projectName`.
- Live-refreshed via the same seven topics WorkspaceHome
  listens to (issues opened/closed/reopened/project-changed +
  epic created/state-changed/project-changed). Tears down on
  unmount.
- `countsFor(name)` + `projectFilterHref(surface, name,
  state?)` helpers for the template.
- New `.project-counts` row inside each `.project-card-head`
  with three `<RouterLink>`s: open issues / in-progress
  epics / closed. Zero counts dim via `data-zero`.
- `.project-count*` styles match the iter-65 workspace
  aesthetic so the per-Project tallies read the same on both
  surfaces.

Verified against dogfood: 3 project cards render the count
chips (currently all 0 since the seed has no project-tagged
issues/epics yet). typecheck + shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Compounds iter 62 + 63 cleanup pattern. The per-Project work
counts logic was duplicated in WorkspaceHome (iter 65) and
ProjectsPanel (iter 75) - same two-ops fetch, same bucketing,
same seven SSE topic subscribers. Iter 76 promotes the whole
shape to a single sdk-vue composable.

- `frontend/packages/sdk-vue/src/use-project-counts.ts`:
  `useProjectCounts(options?)` returns `{ counts, isReady,
  refresh, countsFor }`. Runs parallel `list-issues` +
  `list-epics` on mount, buckets by `projectName`, subscribes
  to the seven topics that mutate project-tagged work
  (issues opened/closed/reopened/project-changed + epic
  created/state-changed/project-changed). Auto-cleans up.
- `emptyProjectCounts()` exposed so templates bind
  unconditionally.
- `index.ts` re-exports `useProjectCounts`,
  `emptyProjectCounts`, `ProjectCounts`,
  `UseProjectCountsOptions`.
- WorkspaceHome.vue: drops the inline `refreshProjectCounts`
  impl, the `ProjectCounts` / `IssueLite` / `EpicLite`
  interfaces, the inline SSE subscribers, and the
  workspace-id-trigger watch. Per-repo open-issue
  subscriptions stay - those track repo state.
- ProjectsPanel.vue: drops the inline `refreshProjectCounts`,
  the lite interfaces, the inline SSE subscribers + their
  `onUnmounted` cleanup, the `WORKSPACE_URI` constant.

Net effect: ~165 lines of duplicate fetch + subscribe + bucket
code retired. Both surfaces share one composable; any future
Project-spine surface can opt in with one line.

Verified: typecheck + shell build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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