Skip to content

refactor: flatten monorepo to a single @vzn/run package - #1

Merged
Exelord merged 2 commits into
mainfrom
claude/evaluate-monorepo-structure-lLWPE
May 11, 2026
Merged

refactor: flatten monorepo to a single @vzn/run package#1
Exelord merged 2 commits into
mainfrom
claude/evaluate-monorepo-structure-lLWPE

Conversation

@Exelord

@Exelord Exelord commented May 11, 2026

Copy link
Copy Markdown
Member

Summary

The workspace only contained one package (@vzn/run), so the monorepo scaffolding was pure overhead. This flattens to a single-package layout.

  • Move packages/run/src/*src/* (git-detected as renames, 100% similarity)
  • Merge packages/run/package.json + root vzn-monorepo package into one @vzn/run at the root
  • Collapse tsconfig.base.json + leaf tsconfig.json into one root tsconfig.json (drop composite — no project references left)
  • Delete pnpm-workspace.yaml
  • Prune workspace-only .npmrc settings (shared-workspace-lockfile, link-workspace-packages, prefer-workspace-packages, save-workspace-protocol)
  • Update 3 doc paths (packages/run/src/src/) in docs/architecture.md, docs/schema.md, docs/modules/README.md
  • Regenerate pnpm-lock.yaml
  • Apply prettier formatting across the tree (was already failing pre-flatten)

Test plan

  • pnpm install succeeds with regenerated lockfile
  • pnpm build produces dist/
  • pnpm test — all 114 tests pass
  • pnpm format:check — clean

https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9


Generated by Claude Code

claude added 2 commits May 11, 2026 06:46
The workspace only had one package, so the monorepo scaffolding was pure
overhead. Move packages/run/src to ./src, merge both package.json files
into a single @vzn/run at the root, collapse tsconfig.base.json + the
leaf tsconfig.json into one, drop pnpm-workspace.yaml, and prune
workspace-only .npmrc settings. Regenerated lockfile. Build + 114 tests
pass.
@Exelord
Exelord merged commit 7bec46b into main May 11, 2026
Exelord pushed a commit that referenced this pull request May 13, 2026
… workers, bottlenecks, queue, task detail

Spec expansion + companion architect design doc.

## Spec (docs/design/tui.md) — extended

Original spec covered a single dashboard layout. Extended to five
top-level views (`1`-`5` keys):

- Overview — the original dashboard.
- Graph — full-screen project DAG (indented topological tree with
  cross-project ▶ arrows, status icons, per-task progress bars).
- Workers — N concurrency slots with per-slot 30s heatmap of
  busy/idle, utilization-over-time sparkline, parallelization %
  gauge.
- Bottlenecks — critical path (longest dependency chain by
  duration), tasks blocking the most dependents, slow-vs-historical
  callouts, cache-miss impact ranking.
- Queue — ready-but-waiting vs blocked-by-deps, queue-throughput
  sparkline.

Plus a Task Detail overlay (Enter from any view): full task
introspection with command, hash, inputs, outputs, deps, dependents,
historical stats from cache.db's runs table (last N runs, avg, p50,
p99, success rate, cache-hit rate), estimated progress bar with ETA,
live log pane.

Group tasks (▣ icon) and persistent tasks (⚡ icon) get first-class
rendering — groups roll up child status and are Space-expandable;
persistent show "ready since X" badges and a header ⚡<n> counter.

Header gains a parallel <P>% gauge (`floor(running / capacity * 100)`)
with threshold-tinted color (green ≥80, yellow 50-79, red <50).

Data-model section grew to include: worker slot map, per-slot 30-sample
heatmap ring buffers, historical-stats cache (batched SQL at runStart),
critical-path computation (recomputed each taskComplete), group rollup
state.

10 open questions for the architect (#1-3 from original, #4-10 new).

## Design doc (docs/design/tui-design.md) — appended §11

Architect pass returned with concrete decisions for the extended
scope:

- Historical stats: ONE batched SQLite query in prepareRun() returning
  a HistoryTable threaded into RunStartEvent. Capped at 50 rows per
  (project, task); client-side slice to 10 for Task Detail. Available
  to every Observer consumer, not just the TUI. Never-run tasks
  render `▱▱▱▱ ?`.
- Critical path: topo-order DP recomputed on every taskComplete
  (O(V+E), microseconds at ≤1000 nodes — no throttling needed).
  Persistent tasks excluded.
- Worker slots: runGraph allocates from a lowest-free-index free-list;
  execute() callback gains slot: number; Observer.taskStart grows the
  same field. Small scheduler change.
- Heatmap sampler: TUI-owned setInterval(1000ms) combined with the
  existing 1 Hz sparkline tick. Uint8Array(30) per slot.
- Multi-view: single store, state.activeView switches between view
  components under src/tui/views/. Task Detail + Help under
  src/tui/overlays/. Filter state per-view (filtering "test" in
  Workers is incoherent).
- Graph view: indented topo tree, cross-project deps inline. Critical-
  path overlay is a left-margin marker.
- Bottlenecks: four pure selectors. New Observer event cacheProbe
  fired from execute-task.ts populates cacheStatus for the cache-miss-
  impact ranking.
- Queue: ready vs blocked via dep-status predicate. Throughput
  sparkline counts waiting→ready transitions per second.
- Groups: transitive children precomputed at runStart; reverse-walk
  updates rollup counters on taskComplete. Per-group expanded flag
  survives view switches.

Implementation order in §11: scheduler slot allocation first
(unblocks the Observer event), then cacheProbe event, then batched
history SQL, then state/reducer extensions, then selectors, then
view components, then overlays, then critical-path recompute, then
the 1 Hz sampler.

No source changes in this commit — design only. Phase 1 implementation
will land in a separate PR.
Exelord pushed a commit that referenced this pull request May 13, 2026
User decision: use OpenTUI. Updates both spec and design doc to
reflect the renderer change. Ink remains documented as the
fallback if the OpenTUI bun-compile gate fails.

## Why OpenTUI over Ink

- Bun-first by design — renderer is a small native lib invoked via
  bun:ffi. No React-reconciler + Yoga (WASM) layout pass per frame;
  diff-based partial redraws happen in native code.
- Sidesteps the known yoga.wasm bun-compile bug (bun#13552 /
  bun#2034) that Ink hits inside a compiled binary.
- Built by the opencode team — its primitives are tuned for the
  same use case the spec calls "tiny stark level god."
- React ergonomics preserved via @opentui/react; component-tree
  mental model is identical to Ink.

## What changed in the spec (tui.md)

- Library-choice subsection rewritten with OpenTUI rationale.
- New "Compile-gate experiment" subsection with two paths (sibling-
  file install vs embed+extract shim) to validate bun build --compile
  works with the native lib before merging Phase 1.
- Module layout renames `App.tsx`'s import path: a new
  `src/tui/tui-shim.ts` is the only place that imports
  `@opentui/react`. All other components import from the shim — so
  swapping to Ink later is one file.
- Lifecycle step 2 says "Mount the OpenTUI app rooted at <App />
  via @opentui/react"; step 3 explains the renderer does native
  partial redraws (still throttle React component churn at 30 Hz).
- Testing section rewritten: pure-function layer is unchanged
  (reducer, selectors, sparkline math). Component-layer plan
  depends on whether @OpenTui ships a frame-capture testing
  helper; if not, we write a ~50-line stub renderer.
- Open questions #1, #13, #14 updated for OpenTUI.

## What changed in the design doc (tui-design.md)

- §1 question 7 answers "Bun compile + OpenTUI" with the native-lib
  bundling decision instead of yoga.wasm.
- §2 rewritten as "OpenTUI vs Ink vs hand-roll":
  - Recommendation: OpenTUI; Ink fallback if compile-gate fails for
    both bundling paths.
  - Why-OpenTUI-over-Ink rationale.
  - Why-Ink-stays-the-fallback (battle-tested, rich ecosystem).
  - Compile-gate experiment expanded: try sibling-file install
    first, then embed+extract. 30-60 min prototype.
  - Startup cost estimate revised: ~30-60ms cold (faster than Ink's
    ~80-120ms).
  - Bundle size: ~150-250 KB JS + 200-400 KB native lib per arch =
    ~400-650 KB compiled-binary contribution. Comparable to Ink.
- Sed-swept remaining Ink references throughout the rest of the doc
  (Observer wiring, store/reducer notes, lifecycle, resize, testing,
  module layout, extension-point notes) → either reworded as
  renderer-agnostic or explicitly OpenTUI.

## What's unchanged

The Observer surface, store / reducer architecture, fallback
decision (`shouldUseTui`), lifecycle (alt-screen, SIGINT, runEnd),
multi-view structure, all 5 views, Task Detail overlay,
historical-stats SQL pull, critical-path computation, worker-slot
allocation in the scheduler, and the implementation order are
unchanged — they're all renderer-agnostic by design.

The compile-gate experiment is the gate before merging Phase 1.
If OpenTUI's native lib can't be bundled cleanly (neither sibling-
file nor embed+extract works), Ink is the well-trodden fallback.

414 tests still pass; format + lint clean.
Exelord pushed a commit that referenced this pull request Jul 5, 2026
Owner (2026-07-05): 'No auto input and no global'. Mark both as REJECTED
non-goals so they aren't re-proposed. Global inputs/env: TS configs
compose (a shared preset spread into each config is the mechanism; the
migrate path already emits vx-preset.ts) — same rationale as the earlier
rejected named-inputs machinery. Auto-input tracing: was already
out-of-scope (2026-06, a multi-OS native-helper project incompatible
with vx's no-build-step distribution); reconfirmed. Explicit
cache.inputs stays the contract (Architecture principle #1).
Exelord pushed a commit that referenced this pull request Jul 10, 2026
The doc half of native-cache-wire-2026-07 Phases B+C. Live docs stop
describing the retired Turbo client / VX_REMOTE_CACHE_* env hatch and
describe the seams instead:

- comparison.md: the 'Remote cache wire' row reads vx-native /v1/cache
  (plugin-driven; Turbo = third-party plugin story); gaps #1 rewritten
  around the shipped wire; the HMAC row becomes the always-on
  structural x-vx-digest integrity.
- cli.md: the env-driven remote-cache section becomes 'Remote cache
  (plugin-driven)' pointing at cloud() + the extensibility recipe; the
  VX_REMOTE_CACHE_* table is gone; the serve section + route table
  speak /v1/cache with the digest/duration headers; vx info drops the
  remote-cache row.
- architecture/caching/execution/flows/patterns/README: the cache
  cluster describes RemoteCacheLayer as the wire seam, the layer
  resolution (injection > plugin > bare local), and the native wire as
  the first-party implementation.
- modules/: remote-cache.md + remote-cache-setup.md deleted (their
  sources are gone); layered-cache.md rewritten to the current surface
  (it still described the v13 tar.gz/stage-dir era); index + README +
  prepare.md synced.
- Site guides: remote-caching.md replaces the Turbo-compatible section
  with the bring-your-own-wire story + an artifact-integrity section;
  extensibility.md gains the full third-party Turbo plugin recipe from
  the design appendix (proof the seam suffices) and its acmeCache
  example now implements RemoteCacheLayer instead of constructing the
  deleted RemoteCache; plugins.md's cache recipe likewise;
  ci/distributed-ci/self-hosting/dashboard/from-turborepo re-point
  endpoints + env mentions.
- design/presigned-artifacts-2026-07.md gets a status note (wire
  premise superseded; the blob-offload phasing survives via the native
  client's one-hop 307 follow); design/remote-cache.md is marked
  RETIRED as the historical record.

Design docs otherwise stay frozen historical records; the generated
apps/docs copies of docs/ regenerate on the next site build.
Exelord pushed a commit that referenced this pull request Jul 13, 2026
Audit cycle 3, wave 2 — three findings actioned, one discovery documented.

CORE-1 (real bug, experimental --predict path): the graph Map inserts a
dependent BEFORE the deps it pulls in (pre-order from the requested
roots), and computePredictedPriorities' old traversal never actually
pushed anything (every unvisited node was already on the seeded stack),
so it folded in reverse-insertion order — every upstream's memo was
computed before its dependents existed and its priority collapsed to its
own historical duration. LPT ordering silently lost its lookahead on
every real graph; the existing tests passed nodes in topo order, which
made the broken scan accidentally correct. The fold is now an explicit
Kahn pass over the dependents relation (order-independent, O(N+E),
iterative). Two regression tests pin real pre-order insertion + a
diamond whose long branch hangs off a short shared head — both fail on
the old code.

DX-2: when a cloud connection resolved, the GitHub job summary gains
"▸ Open this run in the vx dashboard → <origin>/#/runs/<runId>" and the
check run sets details_url to the same page — a red check is one click
from the run's logs and artifacts. Absent a connection (summary-only
mode) nothing changes.

DX-5a: the UI vite dev server now proxies /v1, /health, /mcp, /events
and /stream to the platform (VX_CLOUD_DEV_PROXY overrides the
localhost:4321 default), making UI development same-origin so the
HttpOnly session cookie and CSRF header work exactly like the hosted
build; the UI README was rewritten from the retired "vx serve --ui reads
cache.db" story to the compose-platform recipe.

Documented in docs/design/audit-cycle-2026-07.md: P4/P5/P7/C3 are MOOT —
the queue protocol has zero server-side implementation left (the
P4-server fold deleted it), /v1/meta never advertises `queue`, /version
404s, so the spawn bar, queueRun, RunSession and the foreign-jobs poll
are unreachable dead code. Optimizing them would tune code that cannot
execute; the doc proposes repurposing the live surface onto the
platform's org-scoped /stream (restores dashboard lens #1) vs deleting
the machinery, as an owner decision for cycle 4.
Exelord pushed a commit that referenced this pull request Jul 25, 2026
The run-detail triage card (dev-scenarios #1) classifies each failed
task as flaky / pre-existing / new-failure, but the place a developer
actually stands when a check goes red is the PR page — so carry the
verdict there. When the run is connected to a platform AND a GitHub
surface is active (job summary or check), a red run fetches
GET /v1/triage/:runId once and annotates each failed row's status cell:

  🎲 flaky — not this change (same key passed N×)
  📌 already broken on the default branch
  🆕 new failure — this run changed its inputs

Formatter: GithubSummaryOptions.triage (per-taskId verdict map) +
triageMarker consulted ONLY by the failed branch of statusCell, so a
green row never changes. Plugin: fetchTriage after the ingest POST
(the triage query needs this run's rows), bearer-authenticated,
clearable 5s timeout, undefined on ANY problem — never-fail and purely
additive: no connection / green run / fetch error renders the rows
byte-identically to before. Both consumers (appendGithubSummary +
postGithubCheck) share the same GithubSummaryOptions, so the check and
the job summary always agree.

Pinned: 3 formatter cases (marker per verdict + green-row-never-
consults, bare new-failure when keyChanged is null, no-map additive)
+ 2 plugin e2e against a fake serve (verdict lands in the written
summary file with the bearer on the triage GET; a triage 500 degrades
to the plain failed cell). Docs: guides/ci.md PR-checks section +
cloud/api.md route row name the consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW7aso5j5CrBo7cjyET23D
Exelord pushed a commit that referenced this pull request Jul 26, 2026
…anks

Measured first. A probe seeding 1000 projects x 10 tasks (100k task_runs)
through the real ingest wire showed the failure is correctness, not
latency: /v1/projects?limit=500 returns a PAGE, and three dashboard
sources did "fetch a page then find in it", so

- every project past the page rendered a BLANK detail page,
- the ranking card claimed "vs 500 projects" when there were 1000, and
  ranked within the page, and
- the Projects filter box could never reach a tail project.

Payloads were fine (103 KB / 143 ms for 500 projects). The data was wrong.

Fixed where it can be right — server-side. listProjects gains ILIKE
search and exact-name fetch (the point lookup); countProjects is the true
denominator; rankProject computes per-axis ranks with window functions
over every project in one query, returning top-N per axis plus the named
project with its TRUE rank. /v1/projects answers {projects, total}; new
/v1/projects/rank. The client-side ranker is replaced by a thin shaper,
and the Projects table now says "showing N of M" instead of implying the
page is the workspace.

The guard caught a bug in the fix itself: = ANY($1) binds a JS array as a
malformed array literal on this driver, which would have 500'd the
point-lookup route. IN ${sql(array)} is the form, per provenanceForHashes.

Pinned by a scale test seeding 620 projects: the tail project resolves by
exact fetch and by search, ranks #1 by avg exec with total === 620, and a
mid-pack project reports a true rank past the top-8 window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW7aso5j5CrBo7cjyET23D
Exelord pushed a commit that referenced this pull request Jul 30, 2026
REVERSES a design decision I recorded earlier today, and the reversal is
the point of this commit.

The plan was to parse NODE_OPTIONS and fold its code-injecting flags
(--require/--import/--loader/--conditions) into the cache key, on the
grounds that those genuinely change what a task emits under an unchanged
key. That is true, and it is still the wrong fix for vx.

Architecture principle #1 is "explicit over magical", and this repo's
owner has rejected inference twice on exactly that basis — globalInputs
and auto-input tracing. A hidden key input derived by parsing an
environment variable is the same shape: vx would be guessing which part
of NODE_OPTIONS matters. And singling it out is inconsistent, because
LC_ALL changes `sort` collation and CI/TERM/FORCE_COLOR change the stdout
vx caches and replays. The whole allowlist has this property.

vx cannot hash the allowlist either: PATH, HOME and TERM differ on every
machine, so hashing them means a laptop and a CI runner can NEVER share a
remote cache entry — which is the entire point of a remote cache.

So the honest position, and the one already consistent with how vx
refuses to infer inputs anywhere else: the essentials are a deliberate,
documented, un-hashed pass-through, and a build whose output depends on
one declares it in `cache.inputs.env`. That mechanism already exists and
already works. What was missing was saying so — `docs/schema.md` spelled
out "NOT folded into the cache key" for passThrough one bullet below
while the essentials bullet said nothing at all, which reads as an
oversight rather than a decision.

Now documented with the three groups that can change a task's OUTPUT
named explicitly, the reason they are not hashed, and the one-line
escape hatch.

Pinned so neither direction can be reversed silently. Four mutations,
each verified applied: dropping NODE_OPTIONS or LC_ALL from the allowlist
fails 1 each (the forwarding half must not regress — a CI raising the
heap depends on it); forwarding everything from the host fails 3 (without
the boundary, "not hashed" would be a determinism disaster rather than a
trade); and folding NODE_OPTIONS into the key — the change this commit
decided against — fails 1.

A test of mine was VACUOUS and I caught it by reading, not by mutation:
it called `cache.key()` twice with identical arguments and asserted they
matched. `key()` takes envValues as a parameter and never reads
process.env, so it was equal whatever the essentials did. Rewritten
through `resolveInputs`, where the decision actually lives.

And one mutation NEVER APPLIED on its first attempt — the pattern missed
the real signature, so the "pass" it produced proved nothing. Caught by
checking `git diff --stat` before reading the result, which is exactly
what that rule is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW7aso5j5CrBo7cjyET23D
Exelord added a commit that referenced this pull request Aug 24, 2026
The fourth output mode (Turbo parity, comparison gap #7): one line per
task — outcome word, task id, cache key — with zero log output. The
run's audit trail of which key each task resolved to. discardsOutput
covers the new mode so output the mode promises never to print is
never buffered, the same reasoning none already had. Pinned byte-exact
(the exact-expected-set rule: a mangled leak sails past not.toContain)
across success/hit/failure/skip, plus parse pins for both spellings
and the typo message naming all four modes.

The comparison doc audit that sourced this also surfaced two stale
cloud references, corrected in place: gap #1 advertised the removed
platform's S3 blob backend (now points at the archived design doc),
and gap #12 claimed the deleted dashboard covers last-run replay —
its removal raises that gap's value, and the entry now says so.
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