Conversation
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>
There was a problem hiding this comment.
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.mddescribing v3 goals, architecture targets, increment plan, and verification checklist.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # 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. |
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>
v3 architectural foundation — landedAll 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
Test posture
Backend flip
Deferred to follow-up branchesSee
Each is independently scoped and individually shippable on its own branch. 🤖 Generated with Claude Code |
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>
Summary
v3 brings v1's load-bearing implementation pieces back into v2's kernel-shaped architecture:
See
V3_PLAN.mdfor the increment plan.Increment plan
crates/git-http)git http-backendshell adapterSPEC_COVERAGE.md,TODO.md,start.shEach increment ships as its own commit so the PR shows a real progression.
Test plan
🤖 Generated with Claude Code