From 5449adca4a6fba7b47f98d10d259d2a6cedaa5c8 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 29 Jul 2026 18:37:38 +0300 Subject: [PATCH 1/6] ci(workflow): make the required gate execute and verify what it ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 0 of the Phase 2.5.5/2.6 execution order — the CI-truth batch. Every item below is one root cause: a guard that claimed a stronger guarantee than it enforced. Landed as one change because five of them edit the same file family and splitting them would leave the gate self-inconsistent in between. * The required `ci` job never ran the binary it built (#294). Only the advisory Windows leg and the tag-gated release smoke ever executed `apps/cli/dist/index.js`, so a bundle that compiled but could not boot merged green. Adds a `--version` / `--help` / fixture `run --json` smoke. * `apps/cli/drizzle/**` was not a declared turbo build output (#315), although `tsup`'s `onSuccess` copies the migrations there, outside `dist/`. A cache-hit replay could therefore leave `dist/index.js` fresh beside a stale or missing `drizzle/`, crashing on first DB touch with nothing red anywhere. Verified by deleting the directory and replaying from cache: 14 files restored after the fix, none before. Declared in a new `apps/cli/turbo.json` extending the root. * `pnpm ci` and `ci.yml` had diverged in both directions (#312): the root script ran `lint:tools`, which CI never did; CI ran a DB-migration-sync check the root script never did. Adds the missing step to CI and a `db:sync-check` script to the root, mirroring CI's `git status --porcelain` (which, unlike `git diff`, also catches an untracked new migration). * The >=90% engine coverage floor was documented as enforced and configured as advisory (#296, #152). Resolved by ruling rather than by wording: `llm` and `mcp` become a REQUIRED check; `core` stays measured-and-printed but non-blocking, because its branch margin is +0.83 (90.83, measured 2026-07-29) and Waves 1-3 edit `core` heavily. `RELAVIUM_COVERAGE_ENFORCED_ONLY=1` selects the CI subset; a local `pnpm coverage` still enforces all three, so the floor never silently relaxes for a developer. Behaviourally verified: with `core`'s floor raised to 99, `coverage:enforced` passes and `coverage` fails. * `floor-check` and `peer-dep-gate` were advisory without saying so (#320) — the `name:` is what the PR checks list shows, so both now carry `(advisory)`. * `THIRD_PARTY_EXTERNAL` was stale versus `package.json` (G27, #248): `string-width` shipped with ADR-0069 and was never added. Harmless today only because tsup auto-externalizes anything already in `dependencies`. * The bundle-closure guard read one output chunk without asserting there is one (#314). Correct today, silently narrowing the moment a dynamic `import()` splits a chunk — which 2.5.5.E's `driveHome` deferral will do deliberately. * `sync:models:check` was implemented, documented as the CI-facing staleness mode, and called by nothing (#317). Wired into the weekly job as `continue-on-error` so it reports drift without paging on ordinary churn. `testing.md` is corrected in the same change rather than later: the standard previously asserted an enforcement the pipeline did not perform, and stating the split only in `ci.yml` would have left that contradiction in the standard. Refs Phase 2.5.5 sub-stream H. G26 (release-ref ancestry) already landed in #77. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++------ .github/workflows/models-catalog.yml | 9 +++++ apps/cli/tsup.config.ts | 1 + apps/cli/turbo.json | 10 ++++++ docs/standards/testing.md | 11 ++++-- package.json | 6 ++-- tools/bundle-closure/check.mjs | 14 ++++++++ vitest.config.ts | 12 ++++++- 8 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 apps/cli/turbo.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e18fba94..3295edc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,16 @@ # Runs lint + typecheck + test across every workspace in dependency order with the # Turborepo cache, plus the formatting check and the no-vendor-type-across-the-seam fence. # -# Branch protection (set in the GitHub repo settings, not here): the `ci` job below is the -# REQUIRED status check to merge into `main`. The `peer-dep-gate` job is advisory until the -# surface packages land their peers in Phase 1. The `coverage` job is advisory too — it enforces -# the testing.md >=90% line+branch engine floor (exit criterion #5) but stays non-required until the -# thin core-branch margin is confirmed stable under CI's Node 24; promote it to a required check then. +# Branch protection (set in the GitHub repo settings, not here): `ci` and `coverage` are the REQUIRED +# status checks to merge into `main`. Every other job self-labels `(advisory)` in its `name:`, which is +# what shows in the PR checks list — `floor-check`, `peer-dep-gate` and `windows-concurrency`. +# +# `coverage` enforces the testing.md >=90% line+branch floor for `@relavium/llm` and `@relavium/mcp` only. +# `packages/core` is measured and printed by the same run but does not fail it: its branch margin is +0.83 +# (90.83 measured 2026-07-29) and Phase 2.5.5 Waves 1-3 edit `core` heavily, so blocking merges on a +# sub-1-point margin would red-CI real work for no defect. That is a scoped, dated ruling with a promotion +# trigger — Wave 3's test-coverage items — not an open-ended exemption. A local `pnpm coverage` still +# enforces all three, so the floor never silently relaxes for a developer. # # Caching: the always-on layer is the GitHub Actions `.turbo` cache (restored/saved below), # which makes a no-change re-run a Turborepo cache hit — the M0 "demonstrably hitting" @@ -118,6 +123,27 @@ jobs: - name: Engine dependency allowlist run: pnpm lint:engine-deps + # `tools/` is real code that gates real things (the seam fence, the bundle-closure guard, the + # models.dev sync). The root `ci` script has always linted it; this job never did, so a lint error in + # a guard script could merge while `pnpm ci` was red locally (#312). + - name: Lint the tooling scripts + run: pnpm lint:tools + + # RUN the artifact this job just built. Until now nothing in the required gate executed + # `apps/cli/dist/index.js` — only the advisory Windows leg and the tag-gated release smoke did (#294) — + # so a bundle that builds but cannot boot merged green. The `run --json` leg additionally proves the + # migrations resolve beside the bundle, which is the failure `apps/cli/drizzle/**` becoming a declared + # turbo output (#315) exists to prevent: a cache-hit replay used to leave `dist/` fresh next to a + # missing `drizzle/`, crashing on first DB touch with nothing red anywhere. + - name: Smoke the compiled binary + shell: bash + run: | + set -euo pipefail + node apps/cli/dist/index.js --version + node apps/cli/dist/index.js --help >/dev/null + node apps/cli/dist/index.js run apps/cli/src/harness/fixtures/sequential.relavium.yaml \ + --input n=21 --json >/dev/null + # Supported-floor gate (ADR-0067). TWO floors, deliberately different: # * the PUBLISHED floor is `apps/cli` `engines.node` = `>=22` — the max constraint in the CLI's RUNTIME # dependency closure (`ink@7` / `cli-truncate` / `slice-ansi` → `>=22`); nothing runtime needs more. @@ -131,7 +157,7 @@ jobs: # a property of the prod closure, verified at the ADR (see ADR-0067's amendment note) rather than here. # A SEPARATE job (the required check stays the ubuntu `ci` job on Node 24); promote once confirmed stable. floor-check: - name: node 22-line floor (22.13.0) · typecheck · test · build + name: node 22-line floor (22.13.0) · typecheck · test · build (advisory) runs-on: ubuntu-latest timeout-minutes: 15 # Fully isolate this job from the shared Turbo remote cache. Turbo's task hash does NOT include the @@ -164,7 +190,7 @@ jobs: # checkouts resolve while the surface packages' peers are not all in the tree yet). Catch # peer drift here without breaking local dev. peer-dep-gate: - name: strict peer-dependency check + name: strict peer-dependency check (advisory) runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -185,7 +211,7 @@ jobs: # the root-relative per-glob thresholds (packages/core, packages/llm) authoritative (vitest.config.ts). # Promote to a required check once the margin is confirmed stable under CI's Node 24. coverage: - name: engine coverage floor (advisory) + name: engine coverage floor (llm, mcp) runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -214,8 +240,13 @@ jobs: # each package still covers its OWN src via relative imports, so the floor stays src-accurate). - name: Build workspaces (so coverage resolves the @relavium/* package entries) run: pnpm turbo run build - - name: Engine coverage floor (>=90% line+branch) - run: pnpm coverage + # ENFORCED subset only: `llm` and `mcp` fail the build below 90% line+branch. `core` is measured and + # printed by the same run but does NOT fail it — its branch margin is +0.83 today and Phase 2.5.5 + # Waves 1-3 edit it heavily, so blocking on a sub-1-point margin would red-CI real work for no + # defect (maintainer ruling 2026-07-29). A bare `pnpm coverage` still enforces all three locally. + # Promote `core` here once Wave 3's test-coverage items land. + - name: Engine coverage floor (>=90% line+branch — llm, mcp enforced; core measured) + run: pnpm coverage:enforced # Cross-OS concurrency + headless gate (2.5.I S6). The DB write-path hardening (BEGIN IMMEDIATE + the # SQLITE_BUSY retry's Atomics.wait sleep + WAL locking) and the two-process concurrency e2e (a child spawn + diff --git a/.github/workflows/models-catalog.yml b/.github/workflows/models-catalog.yml index f686814d..8c20b252 100644 --- a/.github/workflows/models-catalog.yml +++ b/.github/workflows/models-catalog.yml @@ -63,6 +63,15 @@ jobs: - name: No ALREADY-SHIPPED model's price moved or vanished run: pnpm sync:models + # Staleness signal, informational only. `sync.mjs` documents `--check` as the CI-facing mode that fails + # when the COMMITTED snapshot has drifted from upstream, and a matching `sync:models:check` script has + # always existed — but no workflow ever called it (#317), so the mode was implemented and unused. + # `continue-on-error` keeps it a report: the step above is the guard that must stay red on a money + # change, while ordinary upstream churn (a new model, a renamed display name) should not page anyone. + - name: Snapshot freshness vs upstream (informational) + continue-on-error: true + run: pnpm sync:models:check + nightly-effort-conformance: name: effort conformance (live) # Only the nightly cron (and a manual run); the weekly cron skips this job. diff --git a/apps/cli/tsup.config.ts b/apps/cli/tsup.config.ts index 9ac326f8..6476d3a2 100644 --- a/apps/cli/tsup.config.ts +++ b/apps/cli/tsup.config.ts @@ -40,6 +40,7 @@ const THIRD_PARTY_EXTERNAL = [ 'quickjs-emscripten-core', 'react', 'smol-toml', + 'string-width', 'yaml', 'zod', ]; diff --git a/apps/cli/turbo.json b/apps/cli/turbo.json new file mode 100644 index 00000000..af497f8a --- /dev/null +++ b/apps/cli/turbo.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "drizzle/**"] + } + } +} diff --git a/docs/standards/testing.md b/docs/standards/testing.md index af63ec75..f1daf09b 100644 --- a/docs/standards/testing.md +++ b/docs/standards/testing.md @@ -113,12 +113,19 @@ journeys, not exhaustive logic — exhaustive logic belongs in engine unit tests ## Coverage expectations -- `packages/core`, `packages/llm`, and `packages/mcp`: high line **and branch** coverage - (enforced floor ≥ 90%), because branch coverage is what catches the error/fallback/edge +- `packages/core`, `packages/llm`, and `packages/mcp`: high line **and branch** coverage, + floor **≥ 90%**, because branch coverage is what catches the error/fallback/edge paths that matter here — and `packages/mcp` fences a security-critical seam (the SDK + `node:child_process`) plus the dependency-free JSON-Schema→Zod compiler. Coverage is a floor and a signal, not the goal — an uncovered branch is a question to answer, not a number to game. + - **Where the floor blocks a merge:** `packages/llm` and `packages/mcp` only. `packages/core` is + measured and reported by the same run but does not fail CI — its branch margin is **+0.83** + (90.83, measured 2026-07-29) and Phase 2.5.5's Waves 1–3 edit `core` heavily, so gating merges on + a sub-1-point margin costs more than it catches. It is promoted once Wave 3's test-coverage items + land. A local `pnpm coverage` enforces all three; `pnpm coverage:enforced` is the CI subset. + Stating this here rather than only in `ci.yml` is the point: the standard previously claimed an + enforcement the pipeline did not perform. - Every bug fix lands with a regression test that fails before the fix. - Surfaces (`apps/*`, `packages/ui`): smoke + critical-journey coverage; deep logic is pushed down into the engine and tested there. diff --git a/package.json b/package.json index 66234394..de81d9bc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "typecheck:tools": "tsc -p tsconfig.tools.json", "test": "turbo run test", "coverage": "vitest run --coverage", - "ci": "turbo run lint typecheck test && pnpm typecheck:tools && pnpm lint:tools && turbo run build format:check && pnpm lint:fence-check && pnpm lint:engine-deps && pnpm lint:bundle-closure", + "coverage:enforced": "RELAVIUM_COVERAGE_ENFORCED_ONLY=1 vitest run --coverage", + "ci": "turbo run lint typecheck test && pnpm db:sync-check && pnpm typecheck:tools && pnpm lint:tools && turbo run build format:check && pnpm lint:fence-check && pnpm lint:engine-deps && pnpm lint:bundle-closure", "lint:fence-check": "node tools/lint-fixtures/assert-fence.mjs", "lint:engine-deps": "node tools/engine-deps/check.mjs", "lint:bundle-closure": "node tools/bundle-closure/check.mjs", @@ -25,7 +26,8 @@ "format:check": "prettier --check .", "sync:models": "pnpm turbo run build --filter=@relavium/llm && node tools/sync-models-dev/sync.mjs", "sync:models:check": "pnpm turbo run build --filter=@relavium/llm && node tools/sync-models-dev/sync.mjs --check", - "lint:tools": "eslint tools --ignore-pattern 'tools/lint-fixtures/**'" + "lint:tools": "eslint tools --ignore-pattern 'tools/lint-fixtures/**'", + "db:sync-check": "pnpm --filter @relavium/db db:generate && test -z \"$(git status --porcelain packages/db/drizzle)\" || { git status --porcelain packages/db/drizzle; echo 'error: committed @relavium/db migration is out of sync with src/schema.ts — regenerate and commit: pnpm --filter @relavium/db db:generate'; exit 1; }" }, "devDependencies": { "@eslint/js": "catalog:", diff --git a/tools/bundle-closure/check.mjs b/tools/bundle-closure/check.mjs index f8cc48be..65bae7e4 100644 --- a/tools/bundle-closure/check.mjs +++ b/tools/bundle-closure/check.mjs @@ -47,6 +47,20 @@ if (outputKey === undefined) { process.exit(1); } +// This guard reads ONE output chunk. That is correct only while the build emits exactly one, which holds +// today (no dynamic `import()` in `apps/cli/src`). If a future dynamic import makes esbuild split a chunk, +// that chunk's external imports would never be inspected and the closure claim would silently narrow — so +// assert the precondition instead of trusting it. Deferring `driveHome` (2.5.5.E, #43/#44) will trip this +// deliberately: the fix then is to iterate every `.js` output, not to delete the assert. +const jsOutputs = Object.keys(meta.outputs ?? {}).filter((k) => k.endsWith('.js')); +if (jsOutputs.length !== 1) { + console.error( + `✗ ${METAFILE} has ${jsOutputs.length} .js outputs (${jsOutputs.join(', ')}), expected exactly 1.\n` + + ' The bundle was split. Iterate every chunk below before trusting the closure result.', + ); + process.exit(1); +} + const imported = new Set(); for (const imp of meta.outputs[outputKey].imports ?? []) { const spec = imp.path; diff --git a/vitest.config.ts b/vitest.config.ts index f9967c15..debdaa29 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -44,10 +44,20 @@ export default defineConfig({ // the root and still match there — a cwd-tolerant `src/**` would wrongly bind shared/db // package runs to the engine floor. The advisory `coverage` job (ci.yml) runs at the repo root, // which is exactly where this per-glob threshold is authoritative. + // `RELAVIUM_COVERAGE_ENFORCED_ONLY=1` narrows the failing set to the packages whose floor is a REQUIRED + // CI check, leaving `packages/core` measured and printed but non-blocking. That split is a maintainer + // ruling (2026-07-29), not a lowered standard: measured margins are `llm` +7.30 and `mcp` +5.54 on + // branch coverage, but `core` sits at 90.83 — **+0.83** — and Phase 2.5.5 Waves 1–3 edit `core` heavily + // (registry, budget-governor, engine, tools). Blocking merges on a sub-1-point margin would red-CI real + // work for no defect. `core` is promoted once Wave 3's test-coverage items land. A bare `pnpm coverage` + // (the local default) still enforces all three, so the floor never silently relaxes for a developer. thresholds: { 'packages/llm/src/**/*.ts': { lines: 90, branches: 90 }, - 'packages/core/src/**/*.ts': { lines: 90, branches: 90 }, // engine floor — core landed at 1.L 'packages/mcp/src/**/*.ts': { lines: 90, branches: 90 }, // the inbound-MCP fence + compiler — 2.R + ...(process.env.RELAVIUM_COVERAGE_ENFORCED_ONLY === '1' + ? {} + : // engine floor — core landed at 1.L + { 'packages/core/src/**/*.ts': { lines: 90, branches: 90 } }), }, }, }, From 2f855fc5e2d784defa9ba9b863effaf414fa857b Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 29 Jul 2026 18:54:26 +0300 Subject: [PATCH 2/6] fix(ci): bracket-access the coverage env var, and record that `pnpm ci` is unreachable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects in the Wave 0 change, both found by CI doing exactly what this PR added it to do. `vitest.config.ts` read `process.env.RELAVIUM_COVERAGE_ENFORCED_ONLY` with dot notation, which `noPropertyAccessFromIndexSignature` (tsconfig.base.json) rejects — TS4111. It escaped local verification because `pnpm typecheck:tools` is a plain `tsc -p tsconfig.tools.json`, NOT part of `turbo run typecheck`, and I ran the turbo tasks individually rather than the root script this very PR exists to align. Running that script would have caught it. Attempting to run it then surfaced the second defect: `pnpm ci` does not invoke the root `ci` script at all. pnpm 9 reserves `ci` as a builtin and answers `ERR_PNPM_CI_NOT_IMPLEMENTED`, so the script is reachable only as `pnpm run ci`. Finding #312 is therefore understated: the local mirror had not merely diverged from `ci.yml`, it was unreachable by the name every contributor types, which is why the divergence could persist unnoticed. Corrected in the `ci.yml` comment and in current.md's Wave 0 entry; renaming the script is left as a maintainer call. Verified with the full `pnpm run ci` — green end to end, including the `typecheck:tools` step that failed in CI. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 4 +++- docs/roadmap/current.md | 3 ++- vitest.config.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3295edc1..7fdb4300 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,7 +125,9 @@ jobs: # `tools/` is real code that gates real things (the seam fence, the bundle-closure guard, the # models.dev sync). The root `ci` script has always linted it; this job never did, so a lint error in - # a guard script could merge while `pnpm ci` was red locally (#312). + # a guard script could merge while `pnpm run ci` was red locally (#312). + # NB: it is `pnpm run ci`, never `pnpm ci` — pnpm reserves `ci` as a builtin and answers + # `ERR_PNPM_CI_NOT_IMPLEMENTED`, so the script is unreachable by the name everyone types. - name: Lint the tooling scripts run: pnpm lint:tools diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index 3f06f6f2..13b11a1b 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -110,7 +110,8 @@ checklists before ~30 security-gated PRs are reviewed against them. `0015` = 2.6.H · `0016` = 2.6.G's pins · `0017` = 2.6.N lineage; **ADR-0073+** for the nine unwritten 2.6 ADRs. 3. **One `ci.yml`/`turbo.json`/`tsup.config.ts` PR** (a five-way collision file — do not split): 2.5.5.H · the required gate never runs the compiled binary + undeclared `drizzle` output (#294, #315) → - local `pnpm ci` vs `ci.yml` divergence (#312) → the coverage-floor **ruling and its implementation** + local `pnpm run ci` vs `ci.yml` divergence (#312 — and `pnpm ci` is shadowed by a pnpm builtin, so the + script was unreachable by the name everyone types) → the coverage-floor **ruling and its implementation** (#296, #152) → the `(advisory)` labels (#320) → `THIRD_PARTY_EXTERNAL` (G27, #248) → bundle-closure single-chunk assert (#314) → `sync:models:check` (#317). *(`release.yml`'s ancestry check, `G26`, already landed in #77; only its tag-protection half remains, and that is now configured too.)* diff --git a/vitest.config.ts b/vitest.config.ts index debdaa29..bddb4c17 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -54,7 +54,7 @@ export default defineConfig({ thresholds: { 'packages/llm/src/**/*.ts': { lines: 90, branches: 90 }, 'packages/mcp/src/**/*.ts': { lines: 90, branches: 90 }, // the inbound-MCP fence + compiler — 2.R - ...(process.env.RELAVIUM_COVERAGE_ENFORCED_ONLY === '1' + ...(process.env['RELAVIUM_COVERAGE_ENFORCED_ONLY'] === '1' ? {} : // engine floor — core landed at 1.L { 'packages/core/src/**/*.ts': { lines: 90, branches: 90 } }), From 53df0e754fd710b8237557f4e1781003d308fc8c Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 29 Jul 2026 19:46:14 +0300 Subject: [PATCH 3/6] docs(roadmap): close the four blockers and the truth gaps from the 2026-07-29 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cross-document review raised 37 findings plus 5 proposal sections. Each was verified against the live files rather than taken on faith; 14 were dropped as cross-lane duplicates or declined on proportionality, and the review's own 10-step remediation order was rejected as a sequence — taken literally it puts nine documentation edits ahead of Wave 1's three CRITICALs. current.md's waves remain the execution authority. Notably, none of the findings was closed by the 2026-07-26 propagation pass: the review targets contract boundaries that pass never touched. THE FOUR BLOCKERS, all verified before acting: * 2.6.M instructed wiring the egress credential resolver into "the chat-session and workflow-run tool-environment factories" while the same document (Explicitly out of scope) and `build-engine.ts:87-89` both call the workflow-run egress/os arms permanently unwired and ADR-gated. An implementer following the plan literally would have breached a recorded security boundary. Scoped to the chat surfaces; the "every surface" acceptance clause now says "every surface that wires its capability arm". * 2.6.O routed a model-generated `.agent.yaml` through `validateAuthoredWorkflow`, which 2.6.A scopes as workflow-shaped — leaving the plan's highest-risk path (untrusted context → generated agent → tool grants → execution) with no correctly specified gate. Adds the agent-shaped `validateAuthoredAgent` with the parent-grant clamp enforced INSIDE the validator, not only at spawn, since the artifact is persisted. 2.6.O acceptance gains a reject-path criterion. * Visible session resume had no owner: deferred-tasks assigned it to "2.6.C's natural sibling or 2.6.G, whichever reaches it first", and 2.6.C shipped 2026-07-13 without it. Assigned to 2.6.G, with the repaint written into the task, the acceptance, the pull-in table and exit criterion 2. * Dynamic `invoke_workflow` was deferred by decision D57 and simultaneously carried in 2.6.P as an open "analysis gate". Recorded as deferred in the phase file and given its own deferred-tasks entry with the ADR + security review it needs to reopen. TRUTH AND AUTHORITY: * An authority notice now sits at the top of all three planning documents: current.md is canonical for progress and order, the phase files for scope and acceptance, and current.md may not redefine a security boundary or an acceptance criterion. The review found this model sound but undocumented. * Status banners under-reported: 2.5.5 still said "Planned, not started" with Wave 0 in flight; 2.6 omitted 2.6.C and 2.6.Q P1-P5. * The ADR table listed six Accepted ADRs (0067-0072) as "(new) — drafted when the workstream starts", with no link definitions. All six now resolve. * current.md described PR #76 as open, the snapshot regen as pending, and D3 as "blocked on D3b" — a row that never existed. That dangling reference was mine, introduced 2026-07-26. * Exit criterion 8 made the ABSENCE of an ADR a pass condition, which pressures against writing a genuine one. Reframed as the standing threshold: minting one is not a phase failure, skipping a genuine one is. * Exit criterion 4 named `pnpm turbo run lint typecheck test` as the gate; the real contract is `pnpm run ci` plus both required ci.yml checks. * Phase 2.5.5 claimed no new product surface while changing five public CLI contracts; they are now enumerated with the EXIT:4 reading that sanctions them. * deferred-tasks.md mixed three lifecycle states with declined items rendered as open checkboxes. Legend added, declined items converted to plain bullets. * The remaining five-language promise and the M2.6-3 milestone claim corrected. Docs-only. Two stale source comments naming the retired `MODEL_PRICING` table are folded into 2.5.5.F rather than fixed here. Co-Authored-By: Claude Opus 5 (1M context) --- docs/roadmap/current.md | 29 +++--- docs/roadmap/deferred-tasks.md | 27 ++++-- .../phase-2.5.5-hardening-and-remediation.md | 39 +++++--- .../phase-2.6-conversational-authoring.md | 90 +++++++++++++------ 4 files changed, 127 insertions(+), 58 deletions(-) diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index 13b11a1b..c97ebf22 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -2,10 +2,15 @@ > Status: Living > -> Last updated: 2026-07-26 +> Last updated: 2026-07-29 - **Related**: [README.md](README.md), [phases/phase-2.5-cli-consolidation.md](phases/phase-2.5-cli-consolidation.md), [phases/phase-2.5.5-hardening-and-remediation.md](phases/phase-2.5.5-hardening-and-remediation.md), [phases/phase-2-cli.md](phases/phase-2-cli.md), [deferred-tasks.md](deferred-tasks.md), [../project-structure.md](../project-structure.md), [../tech-stack.md](../tech-stack.md) +> **Authority (until Phase 2.5.5 and Phase 2.6 close):** This file is canonical for +> live progress and execution order; the phase documents stay canonical for scope and acceptance. Ordering +> differences between the two are deliberate. `current.md` may override recommended ordering — it may **not** +> redefine a security boundary, a scope line, or a milestone acceptance criterion. + This page tracks what is active **right now** and the immediate next concrete actions. The full phase plan and the global milestone spine are in [README.md](README.md). **Phase 2.5 (CLI Consolidation) is complete** (milestone **M2.5-4**, PR #69, 2026-07-08) — its @@ -25,14 +30,14 @@ PR #75, 2026-07-13) — see [Active now](#what-is-active-now). **2.6.Q** (dynami enrichment) is **no longer blocked**: [ADR-0071](../decisions/0071-models-dev-as-the-model-metadata-source.md) and [ADR-0072](../decisions/0072-model-metadata-in-the-db-behind-a-generated-offline-floor.md) are both **Accepted**, resolving the six open maintainer questions the phase file had cited, and the P1–P5 -implementation steps have landed on `development` (PR #76, open, review-folded). P6 and the -`/settings` → `/models` visibility toggle remain open, and the bundled offline snapshot regen is -pending a Gemini pricing decision. +implementation steps are **merged to `main`** (PR #76, 2026-07-26). P6 and the `/settings` → `/models` +visibility toggle remain open; the bundled offline snapshot was **regenerated** in PR #79 after the D3 +price ruling. **The unified build order across both phases is in [Execution order — Phase 2.5.5 + Phase 2.6](#execution-order--phase-255--phase-26-temporary) below** — a -temporary section, deleted when both phases close. Its baseline step is discharged (PRs #76 and #77 merged); -**the next unit of work is Wave 0's single `ci.yml`/`turbo.json`/`tsup.config.ts` PR.** +temporary section, deleted when both phases close. Its baseline step is discharged (PRs #76 and #77 merged) +and **Wave 0's CI-truth PR is in flight on `development` (PR #80)**. ## Execution order — Phase 2.5.5 + Phase 2.6 (temporary) @@ -51,8 +56,8 @@ temporary section, deleted when both phases close. Its baseline step is discharg The plan opened on a divergence neither phase file modelled: Phase 2.5.5's whole backlog is written against `f88b0e8`, which existed only on `development` behind PR #76, so every `file:line` citation in it resolved against a tree nobody was branching from. **PR #76 and #77 are both merged** — `f88b0e8` is on `origin/main`, -`origin/development` and `origin/main` hold identical trees, and there are no open PRs. Every citation in -this plan now resolves. +`origin/development` and `origin/main` held identical trees with no open PRs as of 2026-07-26. Every +citation in this plan now resolves. (PR #79 and PR #80 have opened since.) One consequence survives and is worth keeping until the phases close: @@ -242,7 +247,7 @@ The four Day-1-independent 2.6 workstreams everything downstream sits on — and Then 2.5.5.C · per-row isolation (#117) as pure propagation of H's skip-and-report ruling. 5. **2.6.A** (package extraction, `validateAuthoredWorkflow` back-port, the run-path tool pre-flight #37/G29, direct unit tests #6) and **2.6.D**. *2.6.A's `max_tokens` pre-flight moves to Wave 4b — it has - a hard dependency on 2.6.Q's `limit.output`.* **Closes M2.6-2 and M2.6-3.** + a hard dependency on 2.6.Q's `limit.output`.* **Closes M2.6-2.** (M2.6-3 also needs 2.6.B, which lands in Wave 6.) ### Wave 4b — Money floor and close-out @@ -265,7 +270,7 @@ adapter/catalog money floor, cut the release 2.6.Q P6 is gated on. ### Wave 5a — Paint before you build Restructure the render layer and settle the theme/config substrate **once**, before ~24 new screens and a -five-locale catalog land on it. +an `en`+`tr` string catalog land on it. 1. **ADR row 10 (i18n + theming) is this wave's first gate** — ahead of rows 7 and 8. 2.6.L's palette work is a render-layer freeze and must not open against an undecided contract. @@ -338,9 +343,9 @@ unanswered**; the remainder sit inline in their own phase-file bullet. |---|------|----------|----------------| | D1 | 0 | Mark the `ci` job a **required** check (open since Phase 0) | Yes — every CI item here is advisory until it flips | | D2 | 0 | Coverage floor: promote to required, or soften `testing.md`? | Promote, **and implement in the same PR** — a checked-in run already shows 92–97% margin | -| D3 | 0 | Accept the upstream price changes the ADR-0071 §9 guard is refusing? | ✅ **Ruled 2026-07-26: take current prices.** Verified: `gemini-flash-latest` moved $0.30→$1.50 in / $2.50→$9.00 out — the shipped floor under-prices by 5×. **Blocked on D3b below** | +| D3 | 0 | Accept the upstream price changes the ADR-0071 §9 guard is refusing? | ✅ **Ruled + executed 2026-07-26: take current prices.** Verified: `gemini-flash-latest` moved $0.30→$1.50 in / $2.50→$9.00 out (the shipped floor under-priced by 5×) and `gemini-flash-lite-latest` $0.10→$0.25 / $0.40→$1.50. The nine upstream retirements were accepted in the same ruling; snapshot regenerated and merged in **PR #79**. Nothing outstanding | | D4 | 0 | Confirm the number reservation (`0013`–`0017`, ADR-0073+) | As listed — one item per number, in landing order | -| D5 | 0 | The binding locale bar | ✅ **Ruled 2026-07-26: ship `en` + `tr`**, catalog architected for n locales, `es`/`fr`/`de` staged. EXIT:6, 2.6.L and 2.5.5.F amended | +| D5 | 0 | The binding locale bar | ✅ **Ruled + propagated 2026-07-26: ship `en` + `tr`**, catalog architected for n locales, `es`/`fr`/`de` staged. Amended at all seven sites | | D7 | 0 | Publish v0.1.1 as-is, or supersede with v0.2.0? | v0.2.0 — ADR-0067's Node `>=22` bump is breaking for 0.x | | D8 | 1 | Do already-persisted approval previews need a scrub? | Yes — migration 0013, same PR. Deleting `history.db` also destroys provider registrations | | D10 | 1 | `BudgetExceededError`/`BudgetPauseError`: adopt `.code`? | Adopt — must precede Wave 3's `RelaviumError` migration | diff --git a/docs/roadmap/deferred-tasks.md b/docs/roadmap/deferred-tasks.md index dc5138cb..1b7253f9 100644 --- a/docs/roadmap/deferred-tasks.md +++ b/docs/roadmap/deferred-tasks.md @@ -2,9 +2,12 @@ > Status: Living -> Last updated: 2026-07-08 — the Phase-2.6 rewrite triaged every open item; the now-doable ones carry a -> **Scheduled → 2.6.X** marker pointing at their [phase-2.6](phases/phase-2.6-conversational-authoring.md) -> workstream (they stay unchecked until the PR that lands them). +> Last updated: 2026-07-29 — the Phase-2.6 rewrite triaged every open item and the 2026-07-19 full-project +> review added the deliberately-unscheduled block at the end. +> +> **Lifecycle states**, because this file mixes three: `- [ ]` = unscheduled and actionable · +> **Scheduled → 2.6.X / → 2.5.5.X** = owned by that workstream, still unchecked until the PR that lands it · +> **DONE / DECLINED** = closed history, written as a plain bullet, never a checkbox. - **Related**: [current.md](current.md), [README.md](README.md), [phases/phase-0-foundations.md](phases/phase-0-foundations.md), [phases/phase-2.5.5-hardening-and-remediation.md](phases/phase-2.5.5-hardening-and-remediation.md) @@ -487,7 +490,7 @@ is a long-standing UX gap that would have added a DB read path and an inline beh The seam it needs (`SessionViewSeed.transcript` + `carriesSeedTranscript`) is already in place after 2.6.C Step 2, so the remaining work is the projection and the inline decision. -**Home:** a 2.6 workstream (2.6.C's natural sibling) or 2.6.G's session browser, whichever reaches it first. +**Scheduled → 2.6.G** (the `/agents > Sessions` browser owns resume-in-place). The earlier "2.6.C's natural sibling or 2.6.G, whichever reaches it first" hedge is spent: 2.6.C shipped 2026-07-13 without it. ## Cross-turn tool-call memory as a default-off toggle (2.6.C spin-off, 2026-07-12) @@ -1062,6 +1065,14 @@ future test cannot silently re-acquire it. > prior tracking entry in this file. Recorded 2026-07-08 so they don't get lost. Each maps to a > concrete later phase or decision gate. +- [ ] **Dynamic runtime `invoke_workflow` from a workflow agent node.** Deferred past Phase 2.6 by decision + **D57** (2026-07-26): a running workflow's agent node selecting a sub-workflow at runtime by model + judgment is the unauthored composition 2.6.P's *authored surface == executable surface* invariant + exists to prevent, and it has no cost-governance boundary. 2.6.P ships the **authored** `subworkflow` + node only. **Owner:** a dedicated ADR + security review covering target selection, the nested-run + event namespace, and cost/resource governance — before any implementation. + *(medium · `packages/core` engine + an `invoke_workflow` tool)* + - [ ] **File-snapshot undo (opencode-style revert of message + file changes).** Phase 2.6.E ships conversation-level `/rewind`/`/fork` only — reverting the file changes a message made requires an engine-level file-snapshot mechanism (tracking which tool calls modified which files at which @@ -1279,9 +1290,9 @@ future test cannot silently re-acquire it. *(medium · packages/core/src/engine, packages/shared/src/agent.ts, docs/reference/contracts/agent-yaml-spec.md; #142)* -### Declined findings (considered, not actioned) +### Declined findings (closed — recorded for the record, never actionable) -- [ ] **Sandbox `Math.random` fallback re-check narrowness — DECLINED, accepted residual.** +- **Sandbox `Math.random` fallback re-check narrowness — DECLINED, accepted residual.** `packages/core/src/expression/sandbox.ts:219-225` re-checks `typeof Math.random === 'function'` after the neutralizing `delete`, not the wider "is not undefined"; a hypothetical future QuickJS variant that left `Math.random` as some non-function, non-undefined value after a failed delete would slip past this @@ -1290,7 +1301,7 @@ future test cannot silently re-acquire it. `sandbox.test.ts` (lines 112-116, 533-545), and the finding's own analysis concludes no action is needed until the QuickJS variant changes. *(polish · packages/core/src/expression/sandbox.ts:219; #84)* -- [ ] **Alt-screen `restore()`'s idempotent latch-after-success design — DECLINED, verified strength, not a +- **Alt-screen `restore()`'s idempotent latch-after-success design — DECLINED, verified strength, not a defect.** `apps/cli/src/render/alt-screen.ts:88-109` sets its idempotence latch only AFTER the terminal-restore write succeeds, so a transient EIO/EPIPE on a half-dead TTY during one teardown net (finally / `process.on('exit')` / a signal handler) leaves the terminal recoverable by the NEXT net rather @@ -1299,7 +1310,7 @@ future test cannot silently re-acquire it. matches it) the rest of the codebase should be held to, not as an item to fix. *(polish · apps/cli/src/render/alt-screen.ts:88; #67)* -- [ ] **Full-repo `knip` dead-export inventory — DECLINED, no further action beyond its two spin-offs.** A +- **Full-repo `knip` dead-export inventory — DECLINED, no further action beyond its two spin-offs.** A full-repo `knip` pass surfaced 3 unused-file hits (2 false positives — ESLint fixture configs consumed via ESLint's file-path API, not a TS import), 7 "unused dependency" hits in `apps/cli/package.json` (all false positives — declared for ADR-0051's tsup bundle closure, not directly imported), 39 "unused" function/const diff --git a/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md b/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md index 215ab302..d9573cf9 100644 --- a/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md +++ b/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md @@ -1,6 +1,8 @@ # Phase 2.5.5 — Hardening and Remediation -> Status: 📋 **Planned, not started.** A full-project multi-agent code review completed +> Status: 🚧 **Open — Wave 0 in flight.** Live progress and execution order are canonical in +> [../current.md](../current.md#execution-order--phase-255--phase-26-temporary); this banner does not +> restate them. A full-project multi-agent code review completed > 2026-07-19 at HEAD `f88b0e8` — 377 findings, 373 surviving two-lens adversarial > verification (one agent per finding tried to refute it against the code, a second audited > its materiality). The toolchain was green throughout (lint, typecheck, all 226 test files @@ -8,8 +10,12 @@ > triaged into a 231-item baseline, then consolidated to **205 work items** (22 merges > absorbing 48 originals, 1 split separating a mis-bundled finding), with full 373-finding > coverage re-verified programmatically (0 missing, 0 duplicated). This file holds the items -> that are not a natural fit for an open Phase 2.6 workstream. No code has been written -> against this plan yet. +> that are not a natural fit for an open Phase 2.6 workstream. + +> **Authority (until Phase 2.5.5 and Phase 2.6 close):** [`current.md`](../current.md) is canonical for +> live progress and execution order; the phase documents stay canonical for scope and acceptance. Ordering +> differences between the two are deliberate. `current.md` may override recommended ordering — it may **not** +> redefine a security boundary, a scope line, or a milestone acceptance criterion. - **Related**: [phase-2.5-cli-consolidation.md](phase-2.5-cli-consolidation.md), [phase-2.6-conversational-authoring.md](phase-2.6-conversational-authoring.md), [../current.md](../current.md), [../deferred-tasks.md](../deferred-tasks.md), [../../standards/error-handling.md](../../standards/error-handling.md), [../../standards/security-review.md](../../standards/security-review.md), [../../standards/testing.md](../../standards/testing.md), [../../standards/logging-and-observability.md](../../standards/logging-and-observability.md), [../../standards/documentation-style.md](../../standards/documentation-style.md), [../../standards/architectural-principles.md](../../standards/architectural-principles.md), [../../decisions/README.md](../../decisions/README.md) @@ -20,9 +26,10 @@ first-class is a gap in **propagation, not competence**: the correct pattern alm already exists in the repo and simply never reached the next site — a redaction helper not called twenty lines away, terminal sanitization built for chat and never carried to `relavium run`, `withBusyRetry` applied to every writer but the three oldest, cancel-wins -implemented in four node handlers and omitted in the fifth. That makes this work cheap and -low-risk in the aggregate — the reference implementation is already in-tree at nearly every -site — while a small number of items are genuine CRITICAL defects (a secret leaking into a +implemented in four node handlers and omitted in the fifth. That makes each item cheap and low-risk **individually** — the reference implementation is already in-tree +at nearly every site — while the set is collectively **high-coordination-risk**: 176 items across ~40 files +shared with an active Phase 2.6, five intra-phase file overlaps with a required landing order, and ~14 +maintainer decisions — while a small number of items are genuine CRITICAL defects (a secret leaking into a persisted approval preview, an unhandled rejection that can crash the CLI mid-turn, an unsanitized terminal-injection hole on `relavium run`) that justify treating this as its own phase rather than a backlog label. @@ -127,7 +134,9 @@ reached. - Every maintainer-decision item in this phase (retention policy, `--verbose`/`--quiet` precedence, the `default_headers` plaintext-secret design, and others named in the work breakdown below) has a recorded ruling before its implementation lands. -- `pnpm turbo run lint typecheck test` stays green throughout; no item in this phase widens +- `pnpm run ci` stays green throughout — it is `pnpm run ci`, never `pnpm ci` (pnpm reserves `ci` as a + builtin) — and so do both required `ci.yml` checks, the `ci` job including its compiled-binary smoke and + the `coverage` job (`pnpm coverage:enforced`); no item in this phase widens the `LLMProvider` seam, adds a platform import to `packages/core`, or changes the `--json`/CI machine-output contract ([ADR-0049](../../decisions/0049-cli-machine-output-contract.md)). @@ -166,8 +175,13 @@ reached. - Anything requiring code in `apps/desktop`, `apps/vscode-extension`, `packages/ui`, `apps/api`, or `apps/portal` — all six are empty scaffolds or non-existent directories; no finding in this review's near-term allocation targets them. -- New product surface area of any kind — this phase adds no new tool, command, node type, - or seam capability; every item is a fix to something that already ships. +- **No net-new product capability** — no new tool, node type, or seam capability; every item is a fix to + something that already ships. It does change a small, enumerated set of **public CLI contracts** as + remediation: noun-verb aliases for `chat-resume`/`chat-list`/`chat-export` (`#309`), a confirmation / + `--force` gate on `provider remove-key` (`#19`), a dedicated exit code for an unclassified internal + fault (`G35`), `--help --json` (`#13`), and the `--quiet`/`--verbose` precedence (`#45`). Each is + additive or alias-preserving, and each lands with its `reference/cli/commands.md` update — see the + EXIT:4 reading recorded in [../current.md](../current.md). ## Work breakdown @@ -514,8 +528,11 @@ directly as a 2.6.A work item). **Intra-phase:** the five file overlaps recorded 6. Every maintainer-decision item flagged **blocked** in the work breakdown has a recorded ruling before its implementation lands. 7. The nine sub-streams' acceptance paragraphs (2.5.5.A–I above) each hold in full. -8. No new ADR is minted by this phase beyond what a blocked maintainer decision might - independently require — this is a remediation phase, not an architecture-change phase. +8. **No new ADR is expected** — this is a remediation phase, not an architecture-change phase — but the + standing ADR threshold still applies unchanged: any item that introduces a new seam or port, moves a + trust boundary, defines a storage lifecycle, changes a public CLI / machine-output contract, or adds a + runtime dependency mints an ADR on its own merits. Minting one is **not** a phase failure; skipping a + genuine one is. (D33's `deleted_at` retention lifecycle and D20's flag rename are the closest calls.) ## Required ADRs diff --git a/docs/roadmap/phases/phase-2.6-conversational-authoring.md b/docs/roadmap/phases/phase-2.6-conversational-authoring.md index 5b309f26..bf721304 100644 --- a/docs/roadmap/phases/phase-2.6-conversational-authoring.md +++ b/docs/roadmap/phases/phase-2.6-conversational-authoring.md @@ -1,7 +1,8 @@ # Phase 2.6 — Conversational Authoring and the First-Class CLI -> Status: **In progress** — the substrate workstream **2.6.F** (platform floor + full-screen TUI) is -> ✅ **Done** (merged to `main`, PR #74, 2026-07-11); the rest of the phase follows. Depends on the Phase 2.5 +> Status: **In progress** — **2.6.F** (platform floor + full-screen TUI, PR #74) and **2.6.C** (reseat +> transcript-carry + `/cost`, PR #75) are ✅ **Done**, and **2.6.Q P1–P5** landed with PR #76 (2026-07-26). +> Live progress is canonical in [../current.md](../current.md); the rest of the phase follows. Depends on the Phase 2.5 > spine (the wired tool-environment and the per-tool approval / mode system), which is **complete** (M2.5-4, > PR #69, 2026-07-08), so this phase is unblocked. > @@ -22,6 +23,11 @@ > PR #66, merged 2026-07-07); 2.6.C is retained for the residual per-model cost-breakdown read and as the > cross-reference home. +> **Authority (until Phase 2.5.5 and Phase 2.6 close):** [`current.md`](../current.md) is canonical for +> live progress and execution order; the phase documents stay canonical for scope and acceptance. Ordering +> differences between the two are deliberate. `current.md` may override recommended ordering — it may **not** +> redefine a security boundary, a scope line, or a milestone acceptance criterion. + - **Related**: [../README.md](../README.md), [phase-2.5-cli-consolidation.md](phase-2.5-cli-consolidation.md), [phase-2-cli.md](phase-2-cli.md), [phase-3-desktop.md](phase-3-desktop.md), [phase-5-managed-inference.md](phase-5-managed-inference.md), [node-runtime-upgrade.md](node-runtime-upgrade.md), [../deferred-tasks.md](../deferred-tasks.md), [../../reference/cli/commands.md](../../reference/cli/commands.md), [../../reference/cli/home.md](../../reference/cli/home.md), [../../reference/cli/chat-session.md](../../reference/cli/chat-session.md), [../../reference/shared-core/built-in-tools.md](../../reference/shared-core/built-in-tools.md), [../../reference/contracts/config-spec.md](../../reference/contracts/config-spec.md), [../../reference/contracts/workflow-yaml-spec.md](../../reference/contracts/workflow-yaml-spec.md), [../../reference/contracts/agent-yaml-spec.md](../../reference/contracts/agent-yaml-spec.md), [../../decisions/README.md](../../decisions/README.md) (ADR-0058–0060 + the new ADRs below) The second half of the consolidation work begun in @@ -30,8 +36,8 @@ the CLI as a product. It realizes the tagline — *"Start as an agent. Ship the **entirely inside the terminal**: a conversation authors a standards-valid workflow, agents spawn sub-agents and invoke workflows for complex tasks, the Home starts and monitors runs, the run history is drillable to per-node detail. Every management task (providers, models, MCP, settings, gates) is doable from the Home -without dropping to a shell subcommand. The chat renders syntax-highlighted code blocks; the CLI speaks -five languages. When this phase closes, `relavium` in a terminal is a first-class experience on par with the +without dropping to a shell subcommand. The chat renders syntax-highlighted code blocks; the CLI ships in +English and Turkish over an n-locale catalog. When this phase closes, `relavium` in a terminal is a first-class experience on par with the best agentic CLIs — while keeping the postures they lack: OS-keychain-only secrets, a fail-closed approval floor, and git-committable YAML artifacts. @@ -160,6 +166,12 @@ the same core. `validateWorkflowWithCatalog`, and **back-port** it into `create`/`import`/`export` (today those are parse-only; only the run path catalog-validates), so `create` can never accept a model/modality the run path rejects. Confirmed as a live gap by the review — `create`/`import`/`export` still parse-only today. + Also expose the agent-shaped sibling **`validateAuthoredAgent(yaml, catalog, parentGrants?)`** = + `parseAgent` **+** model/catalog validation **+** tool-ID validity **+** a caller-supplied parent-grant + clamp. The authoring *commands* do not need it (they author for a human to review), but **2.6.O's + model-generated agents execute behind it** — parse success proves none of catalog compatibility, tool + validity, or permission containment, and 2.6.O's clamp cannot live only at spawn if the artifact is + persisted. Unit-test its reject paths. *(L · `apps/cli/src/authoring/`; #2)* - **`nodeCatalogIssue` never resolves the agent's own model, only a node-level override** *(review finding, pre-existing bug in the function this workstream promotes):* `validate-catalog.ts`'s @@ -197,8 +209,9 @@ the same core. (regression-tested); **all three of `create`, `import` and `export`** run the same run-equivalent catalog pre-flight (`validateAuthoredWorkflow`, defined in the task above) that `relavium run` uses, so no authoring entrypoint can accept a **workflow** the run path would reject; the core is directly unit-tested. (All three -commands also accept `.agent.yaml` via `detectAndParse` → `parseAgent`; catalog pre-flight is -workflow-shaped, so an agent file stays parse-validated only — a deliberate scope line, not an oversight.) **Required ADR:** [ADR-0058](../../decisions/0058-relavium-authoring-package-and-conversational-authoring.md) +commands also accept `.agent.yaml` via `detectAndParse` → `parseAgent`; the authoring *commands* keep the +workflow-shaped pre-flight, but the package also exports the agent-shaped `validateAuthoredAgent`, +unit-tested on its reject paths — it is 2.6.O's execution gate, not a command-surface one.) **Required ADR:** [ADR-0058](../../decisions/0058-relavium-authoring-package-and-conversational-authoring.md) (Proposed → Accepted when this workstream begins). ### 2.6.B — Conversational + wizard authoring in the Home @@ -504,7 +517,9 @@ last drill-down position. sessions. - **`/agents` browser** (Home + chat): tabs **Defined | Sessions**. *Defined*: the agent catalog with **"start a chat with this agent"** (closing the Home's built-in-agent-only gap). *Sessions*: recent + - in-progress sessions — Enter resumes **in place** (the in-Home chat machinery), with a detail view + in-progress sessions — Enter resumes **in place**, **with the prior transcript repainted** — the `session_messages` → + `TranscriptEntry` projection into the 2.6.C view seed, including the policy for rows a `/compact`/`/trim` + dropped and the decision on whether the inline renderer repaints too (the in-Home chat machinery), with a detail view (transcript summary, cost, model attribution). Child sessions spawned by a parent agent are shown **indented beneath their parent** with a `└─` tree-drawing prefix and the sub-agent's name; a collapsed parent hides its children (toggle with `Space`). A filter (`/sessions --roots`) shows @@ -544,7 +559,7 @@ last drill-down position. **Acceptance:** every action above works from the Home without a shell command; the three-level drill-down is complete over 2.6.H's data; a run started in another terminal is watchable live at node granularity; zombie runs reconcile; the browsers degrade at <80×24; the machine-output contract is untouched -(harness-proven). **Required ADR:** management browsers + run drill-down contract (shared with 2.6.H). +(harness-proven). **Required ADR:** management browsers + run drill-down contract (shared with 2.6.H). A session resumed from the browser or `chat-resume` opens with its prior transcript **visible** (or the inline renderer's behaviour is documented as a deliberate difference), restores its agent/model identity and continues — asserted by extending the existing resume chain in `apps/cli/src/harness/session-chain.e2e.test.ts`, with a long-transcript and an alt-screen case. ### 2.6.H — Durable run detail: the history data layer @@ -863,7 +878,10 @@ sensitive-read floors). `apps/cli/src/engine/tool-host/egress.ts` but is **not wired** by any production caller (no `egressCredentialResolver` is passed in `session-host.ts` or `build-engine.ts`). The Phase 2.6 task is to: (a) add a per-provider search-API key store (keychain namespace `search:*`), (b) wire the - resolver in the chat-session and workflow-run tool-environment factories, (c) surface search-provider + resolver in the **chat-session and Home-chat** tool-environment factories **only** — the workflow-run + factory (`build-engine.ts`) keeps its `fs`+`process` boundary, which + [Explicitly out of scope](#explicitly-out-of-scope--phase-3--later) and `build-engine.ts`'s own comment + both call permanent and ADR-gated, so wiring it here would breach it silently, (c) surface search-provider configuration in `/providers` and the onboarding wizard, and (d) document the config-pinned provider contract in `built-in-tools.md`. - **`http_request` has no config path to ever populate `allowedDomains` for chat** *(review finding, @@ -894,7 +912,7 @@ sensitive-read floors). grant. - **Dynamic `invoke_workflow` from within a workflow agent node** — **moved to 2.6.P** (workflow composition), where it sits with the `subworkflow` node and the nested-run event namespace. (`invoke_agent` - is wired for chat in 2.6.N; the dynamic runtime `invoke_workflow` question is analyzed in 2.6.P's ADR.) + is wired for chat in 2.6.N; the dynamic runtime `invoke_workflow` question is **deferred past the phase** (D57); 2.6.P's ADR records the deferral, not an analysis.) - **Default chat agent grant review**: widen the built-in agent's grant to the new idempotent read tools (search/find/todo); write/exec/egress stay opt-in via mode + approval. - **Curl/wget as web-search substrate** *(research item)*: evaluate whether `curl` / `wget` / `httpie` / @@ -909,7 +927,9 @@ sensitive-read floors). **Acceptance:** the toolbelt covers read / edit / search / find / exec / web / todo / ask-user / `invoke_agent` (now wired for chat — 2.6.N); each tool -is YAML-selectable and correctly mode-gated on every surface; render v2 ships with diffs and the details +is YAML-selectable and correctly mode-gated on every **surface that wires its capability arm** — egress/OS +tools on the chat surfaces only, with 2.6.A's pre-flight rejecting an egress/OS grant in an authored workflow +rather than shipping a permanent `tool_unavailable`; render v2 ships with diffs and the details toggle behind a passed security review, including `write_file`'s approval card (content diff/preview, a post-write trace event, and visibility in `auto` mode); a chat `http_request` call reaches the approval prompt instead of deterministically failing `domain_not_allowed`; the approval cache is target-scoped; the @@ -1035,8 +1055,10 @@ generation capability ships. model describes a task with **no matching catalog agent** (e.g. "analyze this CSV and find outliers"), the engine uses the 2.6.B conversational-authoring infrastructure to generate a valid `.agent.yaml` (or one-off `.relavium.yaml`) on the fly — written to the central ephemeral root (2.6.N's artifact-store - port, `/agents/.agent.yaml`), **validated against the schema - (`validateAuthoredWorkflow`) before it can execute**, then spawned. The model is not limited to + port, `/agents/.agent.yaml`), **validated before it can execute** — + `validateAuthoredAgent` for a generated `.agent.yaml`, `validateAuthoredWorkflow` for a one-off + `.relavium.yaml` (both from 2.6.A), with the parent-grant clamp enforced **inside the validator**, not + only at spawn — then spawned. The model is not limited to pre-existing catalog agents — it **composes tooling on demand**. - **Generation security review (mandatory, dedicated — the plan's highest-risk surface):** generating and running model-authored agents is a **prompt-injection → code-execution** path. Untrusted content in the @@ -1062,7 +1084,9 @@ generation capability ships. **Acceptance:** a chat agent with no suitable catalog agent **generates** a valid one-off agent, which passes the pre-flight and runs, its result folded into the parent turn — with the generation security -review passed; a generated agent never escalates tools or carries a secret; a parent fans out to multiple +review passed; a generated agent never escalates tools or carries a secret; **a generated agent naming an +unknown tool, an uncatalogued model, or a grant wider than its parent's is rejected by +`validateAuthoredAgent` before spawn, reject-path tested**; a parent fans out to multiple children concurrently under an explicit error policy and collects their results in one turn. **Required ADR:** on-the-fly generation + parallel orchestration security (new — the generation-as-codegen threat model, tool-grant clamping, the parallel error-policy contract; sits on ADR-0060's taint precedent). @@ -1106,21 +1130,23 @@ migration story; depends on 2.6.N for the parent-child lineage substrate. lineage); its `run:*` events nest under a `run:child_*` namespace (or the bus carries a `parentRunId` discriminator — decided in the ADR). This enables **arbitrary workflow composition** — pipelines built from smaller, independently testable units without the engine knowing any workflow's domain. -- **Dynamic `invoke_workflow` from a workflow agent node** *(analysis gate, moved here from 2.6.M)*: an open - question — can a running workflow's agent node **dynamically** call `invoke_workflow` as a tool (as a chat - agent does), selecting a target at runtime by model judgment, enabling orchestrator workflows that branch - to sub-workflows? The ADR evaluates: (a) a separate tool vs an `invoke_agent` overload with a - `target: 'workflow'` discriminator; (b) the nested-run event namespace; (c) the cost/resource governance - boundary. Decided in this workstream's analysis step. +- **Dynamic `invoke_workflow` from a workflow agent node — DEFERRED past Phase 2.6** *(decision D57, + recorded in [current.md](../current.md#wave-gating-decisions))*: a running workflow's agent node selecting + a sub-workflow at runtime by model judgment is exactly the unauthored composition that 2.6.P's + *authored surface == executable surface* invariant exists to prevent, and it has no cost-governance + boundary yet. **2.6.P ships the authored `subworkflow` node only.** Its ADR records the deferral and the + rationale rather than deciding the design; reopening it needs its own ADR + security review covering + (a) target selection, (b) the nested-run event namespace, and (c) cost/resource governance. Tracked in + [../deferred-tasks.md](../deferred-tasks.md). **Acceptance:** a workflow invokes another via an authored `subworkflow` node under `schema_version: '1.1'`; existing `schema_version: '1.0'` files parse **unchanged** (no forced migration); a 1.0 file using `subworkflow` is rejected with a version-hint error; the child run carries `parentRunId` lineage and its events attribute to the parent; the parser/spec/migration-note establish the reusable version-migration path. **Required ADR:** `subworkflow` node + `schema_version` 1.1 additive migration (new — the -first versioned-schema bump; the nested-run event namespace; the dynamic-`invoke_workflow` decision). +first versioned-schema bump; the nested-run event namespace; the recorded deferral of dynamic `invoke_workflow`). -### 2.6.Q — Dynamic model-catalog enrichment (models.dev): per-model capability matrix + pricing long-tail +### 2.6.Q — Dynamic model-catalog enrichment (models.dev): per-model capability matrix + pricing long-tail — 🟡 **P1–P5 landed (PR #76, 2026-07-26); P6 and the `/settings` → `/models` toggle open** Added **2026-07-11** from three maintainer manual-test findings that share one root cause — **Relavium has no per-model economics or capability data beyond a ~12-entry hand-maintained registry**, and no provider API @@ -1219,6 +1245,7 @@ workstreams — each stays checked off **only** in the PR that lands it: | MCP `stdio` import-trust/consent gate + `npx` pinning (ADR-0052 §2) | 2.6.B | | Parse-time gate on system-bound fields (trusted `{{ctx}}`) | 2.6.D | | `@`-glob / directory expansion (ADR-0061) | 2.6.E | +| `chat-resume` opens on an empty viewport (`session_messages` → `TranscriptEntry` projection) | 2.6.G | | Cross-process gate-resolve TOCTOU (store uniqueness) | 2.6.H | | Run-resume torn-read wrap · chat-persister turn atomicity | 2.6.H | | Content-level workflow-identity guard on resume | 2.6.H | @@ -1431,7 +1458,7 @@ flowchart LR 1. `relavium` on a TTY opens the **full-screen Home**; `--json` / CI / non-TTY behavior is byte-identical (regression-harness proven); the inline renderer remains available. 2. **The Home manages everything**: providers/keys, models, MCP servers, settings, workflow - start/monitor/history with node-level drill-down, agent start/resume, gate + budget resolution, and + start/monitor/history with node-level drill-down, agent start/resume (a resumed session repaints its transcript, restores its agent/model identity, and continues), gate + budget resolution, and authoring — no routine task requires a shell subcommand. (Subcommands and the ADR-0049 machine surface are untouched and permanent; whether to de-emphasize interactive duplicates in `--help` is a phase-end review decision, never a removal.) @@ -1471,9 +1498,11 @@ flowchart LR |---|-----|-------|--------|------------| | 1 | [ADR-0058][] | `@relavium/authoring` + conversational authoring | Proposed | 2.6.A / 2.6.B | | 2 | [ADR-0059][] | Mid-session model reseat | **Accepted** (shipped 2.5.G) | 2.6.C (residual) | +| 2b | [ADR-0070][] | Durable per-model session cost attribution | **Accepted** | 2.6.C | | 3 | [ADR-0060][] | Session `{{ctx.*}}` interpolation | Proposed | 2.6.D | -| 4 | *(new)* | Node supported-floor bump (`>=22`; supersedes ADR-0021) | Drafted when 2.6.F starts | 2.6.F | -| 5 | *(new)* | Full-screen TUI renderer + component test harness | Drafted when 2.6.F starts | 2.6.F | +| 4 | [ADR-0067][] | Node supported-floor `>=22` (supersedes ADR-0021) | **Accepted** | 2.6.F | +| 5 | [ADR-0068][] | Full-screen TUI renderer + component test harness | **Accepted** | 2.6.F | +| 5b | [ADR-0069][] | `string-width` for the CLI renderer | **Accepted** | 2.6.F | | 6 | *(new)* | Management browsers + durable run detail (amends ADR-0036) | Drafted when 2.6.G starts | 2.6.G / 2.6.H | | 7 | *(new)* | MCP management surface + config-write extension (extends ADR-0063) | Drafted when 2.6.I starts | 2.6.I | | 8 | *(new)* | Onboarding auth paths + Relavium-account forward design (rides ADR-0012–0015) | Drafted when 2.6.J starts | 2.6.J | @@ -1482,12 +1511,19 @@ flowchart LR | 11 | *(new)* | Syntax highlighting dependency (`highlight.js` + `cli-highlight` via `ink-syntax-highlight`) + markdown rendering architecture | Drafted when 2.6.E starts | 2.6.E | | 12 | *(new)* | Child-session **foundation** (extends ADR-0024/ADR-0036; parent-child lineage schema, standardized I/O contract, the **host artifact-store port** + central ephemeral root + fs-floor home-anchoring, catalog spawn, cost roll-up, abort propagation, depth/concurrency guardrails) | Drafted when 2.6.N starts | 2.6.N | | 13 | *(new)* | On-the-fly **generation + parallel orchestration security** (the generation-as-codegen threat model, schema-valid≠safe, tool-grant clamping, the parallel error-policy contract; sits on ADR-0060's taint precedent) — **mandatory security review is the ship-gate** | Drafted when 2.6.O starts | 2.6.O | -| 14 | *(new)* | `subworkflow` node + **`schema_version` 1.1** additive migration (the first versioned-schema bump — additive/opt-in, subworkflow-only not `loop`; the nested-run event namespace; the dynamic-`invoke_workflow` decision) | Drafted when 2.6.P starts | 2.6.P | -| 15 | *(new)* | Dynamic model-catalog enrichment (models.dev): per-model **capability matrix** (accepted reasoning-effort tiers, output ceiling, temperature/structured_output/modalities → adapter clamp-or-reject before the wire) + **pricing long-tail** with a new `models-dev` `PricingSource` and precedence `registry > user > models-dev > none` feeding the cost cap; bundled snapshot floor + disable-able host-side fetch + integrity validation; SSRF floor if the URL is configurable (extends ADR-0064; rides ADR-0065/ADR-0066; ADR-0011 seam-respecting) — **mandatory security review co-gate** | Drafted when 2.6.Q starts | 2.6.Q | +| 14 | *(new)* | `subworkflow` node + **`schema_version` 1.1** additive migration (the first versioned-schema bump — additive/opt-in, subworkflow-only not `loop`; the nested-run event namespace; the recorded deferral of dynamic `invoke_workflow`) | Drafted when 2.6.P starts | 2.6.P | +| 15 | [ADR-0071][] | models.dev as the model-metadata source | **Accepted** | 2.6.Q | +| 15b | [ADR-0072][] | Model metadata in the DB behind a generated offline floor | **Accepted** | 2.6.Q | [ADR-0058]: ../../decisions/0058-relavium-authoring-package-and-conversational-authoring.md [ADR-0059]: ../../decisions/0059-cli-mid-session-model-reseat.md [ADR-0060]: ../../decisions/0060-session-ctx-prompt-interpolation.md +[ADR-0067]: ../../decisions/0067-node-supported-floor-22-reaffirm-better-sqlite3.md +[ADR-0068]: ../../decisions/0068-full-screen-tui-renderer-ink7-harness.md +[ADR-0069]: ../../decisions/0069-string-width-for-the-cli-renderer.md +[ADR-0070]: ../../decisions/0070-durable-per-model-session-cost-attribution.md +[ADR-0071]: ../../decisions/0071-models-dev-as-the-model-metadata-source.md +[ADR-0072]: ../../decisions/0072-model-metadata-in-the-db-behind-a-generated-offline-floor.md > **Deferred:** A future validator-dependency ADR will be needed for `output_schema` deep JSON-Schema > conformance (currently out of scope for this phase; tracked in From 81d9887f52fad53b60b9b97a952282e5e475e53f Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 29 Jul 2026 20:13:10 +0300 Subject: [PATCH 4/6] fix(ci): share the CLI smoke, make coverage:enforced cross-platform, order the sync guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six review findings on PR #80. Two are defects this PR introduced. * `coverage:enforced` used POSIX inline env-var syntax, which cmd.exe and PowerShell do not support — in a repo that runs a Windows CI leg. `cross-env` would be a new dependency needing an ADR (rule 2) for something ten lines of Node already do, so the script moved to `tools/coverage/enforced.mjs`, matching the existing `tools/*/check.mjs` convention. * The `sync:models:check` step I added to the weekly catalog job ran AFTER `pnpm sync:models`, which REWRITES `snapshot.ts` in the ephemeral checkout (sync.mjs's `writeFileSync`). It therefore compared upstream against the file the previous step had just regenerated and would have reported "current" every time — an informational step that could never inform. Moved ahead of the guard, where it reads the committed snapshot. * The compiled-binary smoke existed only in `ci.yml`, recreating #312's divergence in the opposite direction: the local script this PR exists to align still did not run the artifact. Extracted to `tools/cli-smoke/check.mjs` and called from both `ci.yml` and the root `ci` script, so the check cannot drift out of one of them again. * The coverage job's own comment still read "Advisory for now … promote to a required check once the margin is confirmed stable", contradicting the header this PR rewrote and the live branch protection, which now requires `engine coverage floor (llm, mcp)`. * MD028: the authority notice landed flush against the phase-2.5.5 status banner, leaving a bare blank line between two blockquotes. * 2.6.M said dynamic `invoke_workflow` was "moved to 2.6.P", which reads as a 2.6.P deliverable. It is tracked alongside 2.6.P — whose `subworkflow` node and nested-run event namespace are its natural home — but deferred past the phase (D57); 2.6.P's ADR records the deferral, not the design. SKIPPED: the finding that phase-2.5.5's "both required `ci.yml` checks" wording overclaims. Branch protection already lists both `lint · typecheck · test` and `engine coverage floor (llm, mcp)`, so the statement is accurate as written. Verified with the full `pnpm run ci` (green, including the new smoke leg) and by re-confirming the enforcement split behaviourally: with `core`'s floor raised to 99, `coverage:enforced` passes and `coverage` fails. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 22 ++++------ .github/workflows/models-catalog.yml | 18 ++++---- .../phase-2.5.5-hardening-and-remediation.md | 2 +- .../phase-2.6-conversational-authoring.md | 8 ++-- package.json | 7 +-- tools/cli-smoke/check.mjs | 43 +++++++++++++++++++ 6 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 tools/cli-smoke/check.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fdb4300..578c02c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,20 +131,16 @@ jobs: - name: Lint the tooling scripts run: pnpm lint:tools - # RUN the artifact this job just built. Until now nothing in the required gate executed + # RUN the artifact this job just built, through the SAME `pnpm smoke:cli` script the root `ci` script + # calls — a check that exists in only one of the two is exactly the #312 divergence this change closes. + # Until now nothing in the required gate executed # `apps/cli/dist/index.js` — only the advisory Windows leg and the tag-gated release smoke did (#294) — # so a bundle that builds but cannot boot merged green. The `run --json` leg additionally proves the # migrations resolve beside the bundle, which is the failure `apps/cli/drizzle/**` becoming a declared # turbo output (#315) exists to prevent: a cache-hit replay used to leave `dist/` fresh next to a # missing `drizzle/`, crashing on first DB touch with nothing red anywhere. - name: Smoke the compiled binary - shell: bash - run: | - set -euo pipefail - node apps/cli/dist/index.js --version - node apps/cli/dist/index.js --help >/dev/null - node apps/cli/dist/index.js run apps/cli/src/harness/fixtures/sequential.relavium.yaml \ - --input n=21 --json >/dev/null + run: pnpm smoke:cli # Supported-floor gate (ADR-0067). TWO floors, deliberately different: # * the PUBLISHED floor is `apps/cli` `engines.node` = `>=22` — the max constraint in the CLI's RUNTIME @@ -207,11 +203,11 @@ jobs: - name: Install with strict peers run: pnpm install --frozen-lockfile --config.strict-peer-dependencies=true - # Engine coverage floor (testing.md >=90% line+branch, exit criterion #5). Advisory for now — a - # SEPARATE job (not part of the required `ci` job) so it surfaces a regression without blocking merge - # while the core-package branch margin is thin. `pnpm coverage` is a repo-ROOT run, which is what makes - # the root-relative per-glob thresholds (packages/core, packages/llm) authoritative (vitest.config.ts). - # Promote to a required check once the margin is confirmed stable under CI's Node 24. + # Engine coverage floor (testing.md >=90% line+branch, exit criterion #5). A REQUIRED check named + # `engine coverage floor (llm, mcp)` — kept as a SEPARATE job from `ci` so a coverage regression is + # legible on its own line rather than buried in a 12-step job. It is a repo-ROOT vitest run, which is what + # makes the root-relative per-glob thresholds authoritative (vitest.config.ts). `packages/core` is measured + # but not enforced here; see the header for the ruling and its promotion trigger. coverage: name: engine coverage floor (llm, mcp) runs-on: ubuntu-latest diff --git a/.github/workflows/models-catalog.yml b/.github/workflows/models-catalog.yml index 8c20b252..44359506 100644 --- a/.github/workflows/models-catalog.yml +++ b/.github/workflows/models-catalog.yml @@ -60,18 +60,20 @@ jobs: # (sync.mjs says so in its own comments). New models "merge automatically" via the deferred auto-PR # (deferred-tasks.md); until it lands they are simply picked up by the next local `pnpm sync:models`. The # snapshot this job writes into the ephemeral CI checkout is discarded — only the exit code is the guard. - - name: No ALREADY-SHIPPED model's price moved or vanished - run: pnpm sync:models - - # Staleness signal, informational only. `sync.mjs` documents `--check` as the CI-facing mode that fails - # when the COMMITTED snapshot has drifted from upstream, and a matching `sync:models:check` script has - # always existed — but no workflow ever called it (#317), so the mode was implemented and unused. - # `continue-on-error` keeps it a report: the step above is the guard that must stay red on a money - # change, while ordinary upstream churn (a new model, a renamed display name) should not page anyone. + # Staleness signal, informational only — and it MUST run before the guard below. `sync:models` REWRITES + # `snapshot.ts` in the ephemeral checkout (sync.mjs's `writeFileSync`), so a `--check` placed after it + # would compare upstream against the file the previous step just regenerated and report "current" every + # time. `sync.mjs` documents `--check` as the CI-facing mode that fails when the COMMITTED snapshot has + # drifted, and a matching `sync:models:check` script has always existed — but no workflow ever called it + # (#317). `continue-on-error` keeps it a report: the guard below is what must stay red on a money change, + # while ordinary upstream churn (a new model, a renamed display name) should not page anyone. - name: Snapshot freshness vs upstream (informational) continue-on-error: true run: pnpm sync:models:check + - name: No ALREADY-SHIPPED model's price moved or vanished + run: pnpm sync:models + nightly-effort-conformance: name: effort conformance (live) # Only the nightly cron (and a manual run); the weekly cron skips this job. diff --git a/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md b/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md index d9573cf9..704e2af5 100644 --- a/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md +++ b/docs/roadmap/phases/phase-2.5.5-hardening-and-remediation.md @@ -11,7 +11,7 @@ > absorbing 48 originals, 1 split separating a mis-bundled finding), with full 373-finding > coverage re-verified programmatically (0 missing, 0 duplicated). This file holds the items > that are not a natural fit for an open Phase 2.6 workstream. - +> > **Authority (until Phase 2.5.5 and Phase 2.6 close):** [`current.md`](../current.md) is canonical for > live progress and execution order; the phase documents stay canonical for scope and acceptance. Ordering > differences between the two are deliberate. `current.md` may override recommended ordering — it may **not** diff --git a/docs/roadmap/phases/phase-2.6-conversational-authoring.md b/docs/roadmap/phases/phase-2.6-conversational-authoring.md index bf721304..41de3bb5 100644 --- a/docs/roadmap/phases/phase-2.6-conversational-authoring.md +++ b/docs/roadmap/phases/phase-2.6-conversational-authoring.md @@ -910,9 +910,11 @@ sensitive-read floors). (path prefix / host / MCP server) instead of tool id alone, and give `mcp_call`/`web_search` a structured `{server, tool}`/query preview so their blank-preview once-only downgrade becomes a real reviewable grant. -- **Dynamic `invoke_workflow` from within a workflow agent node** — **moved to 2.6.P** (workflow - composition), where it sits with the `subworkflow` node and the nested-run event namespace. (`invoke_agent` - is wired for chat in 2.6.N; the dynamic runtime `invoke_workflow` question is **deferred past the phase** (D57); 2.6.P's ADR records the deferral, not an analysis.) +- **Dynamic `invoke_workflow` from within a workflow agent node** — **not a 2.6.M deliverable, and not a + 2.6.P one either.** It is tracked *alongside* 2.6.P (workflow composition), whose `subworkflow` node and + nested-run event namespace are its natural home, but the capability itself is **deferred past Phase 2.6** + (D57): 2.6.P's ADR records the deferral and its rationale rather than implementing it. (`invoke_agent` is + wired for chat in 2.6.N — agent spawning is separate from workflow composition.) - **Default chat agent grant review**: widen the built-in agent's grant to the new idempotent read tools (search/find/todo); write/exec/egress stay opt-in via mode + approval. - **Curl/wget as web-search substrate** *(research item)*: evaluate whether `curl` / `wget` / `httpie` / diff --git a/package.json b/package.json index de81d9bc..0fca12e0 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "typecheck:tools": "tsc -p tsconfig.tools.json", "test": "turbo run test", "coverage": "vitest run --coverage", - "coverage:enforced": "RELAVIUM_COVERAGE_ENFORCED_ONLY=1 vitest run --coverage", - "ci": "turbo run lint typecheck test && pnpm db:sync-check && pnpm typecheck:tools && pnpm lint:tools && turbo run build format:check && pnpm lint:fence-check && pnpm lint:engine-deps && pnpm lint:bundle-closure", + "coverage:enforced": "node tools/coverage/enforced.mjs", + "ci": "turbo run lint typecheck test && pnpm db:sync-check && pnpm typecheck:tools && pnpm lint:tools && turbo run build format:check && pnpm lint:fence-check && pnpm lint:engine-deps && pnpm lint:bundle-closure && pnpm smoke:cli", "lint:fence-check": "node tools/lint-fixtures/assert-fence.mjs", "lint:engine-deps": "node tools/engine-deps/check.mjs", "lint:bundle-closure": "node tools/bundle-closure/check.mjs", @@ -27,7 +27,8 @@ "sync:models": "pnpm turbo run build --filter=@relavium/llm && node tools/sync-models-dev/sync.mjs", "sync:models:check": "pnpm turbo run build --filter=@relavium/llm && node tools/sync-models-dev/sync.mjs --check", "lint:tools": "eslint tools --ignore-pattern 'tools/lint-fixtures/**'", - "db:sync-check": "pnpm --filter @relavium/db db:generate && test -z \"$(git status --porcelain packages/db/drizzle)\" || { git status --porcelain packages/db/drizzle; echo 'error: committed @relavium/db migration is out of sync with src/schema.ts — regenerate and commit: pnpm --filter @relavium/db db:generate'; exit 1; }" + "db:sync-check": "pnpm --filter @relavium/db db:generate && test -z \"$(git status --porcelain packages/db/drizzle)\" || { git status --porcelain packages/db/drizzle; echo 'error: committed @relavium/db migration is out of sync with src/schema.ts — regenerate and commit: pnpm --filter @relavium/db db:generate'; exit 1; }", + "smoke:cli": "node tools/cli-smoke/check.mjs" }, "devDependencies": { "@eslint/js": "catalog:", diff --git a/tools/cli-smoke/check.mjs b/tools/cli-smoke/check.mjs new file mode 100644 index 00000000..1660db0e --- /dev/null +++ b/tools/cli-smoke/check.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +// Execute the COMPILED CLI bundle — the artifact that actually ships. +// +// Shared by `.github/workflows/ci.yml`'s required job and the root `pnpm run ci` script, so the local gate +// and the real gate cannot drift on this check the way they did on `lint:tools` and the migration-sync check +// (#312). Nothing in the required gate ran the binary before (#294): a bundle that compiled but could not +// boot merged green. +// +// The `run --json` leg is load-bearing beyond "it boots": it opens `history.db`, which proves the drizzle +// migrations resolved beside the bundle. That is the failure `apps/cli/drizzle/**` becoming a declared turbo +// output (#315) exists to prevent — a cache-hit replay leaving `dist/` fresh next to a missing `drizzle/`. +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; + +const BUNDLE = 'apps/cli/dist/index.js'; +const FIXTURE = 'apps/cli/src/harness/fixtures/sequential.relavium.yaml'; + +if (!existsSync(BUNDLE)) { + console.error(`✗ ${BUNDLE} is missing — run \`pnpm turbo run build\` first.`); + process.exit(1); +} + +const steps = [ + { label: '--version', args: ['--version'] }, + { label: '--help', args: ['--help'] }, + { + label: 'run --json (opens history.db → proves the migrations resolve)', + args: ['run', FIXTURE, '--input', 'n=21', '--json'], + }, +]; + +for (const { label, args } of steps) { + const r = spawnSync(process.execPath, [BUNDLE, ...args], { encoding: 'utf8' }); + if (r.status !== 0) { + console.error(`✗ compiled CLI failed: ${label} (exit ${r.status})`); + if (r.stderr) console.error(r.stderr.trim()); + process.exit(1); + } +} + +console.log( + '✓ compiled CLI smoke passed (boots, renders help, and runs a workflow against history.db).', +); From 0f2d2f7f7ce441e8014d0074445067432e63394a Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 29 Jul 2026 20:16:03 +0300 Subject: [PATCH 5/6] fix(ci): move the coverage gate out of a path .gitignore silently swallowed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tools/coverage/enforced.mjs` never reached the repository. `.gitignore:49` carries `coverage/` with no leading slash, so it matches a directory of that name at EVERY depth — `git add -A` skipped the new script without a word, the commit looked complete, and CI failed with MODULE_NOT_FOUND on a file that exists on every developer's disk. Renamed to `tools/coverage-gate/run.mjs`. The two obvious alternatives are both worse: anchoring the pattern to `/coverage/` would stop ignoring the per-package `packages/*/coverage` output it exists for, and a `!` negation cannot re-include a file whose parent directory is excluded. Verified the file is in the index this time, not merely on disk. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- tools/coverage-gate/run.mjs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tools/coverage-gate/run.mjs diff --git a/package.json b/package.json index 0fca12e0..018c8117 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typecheck:tools": "tsc -p tsconfig.tools.json", "test": "turbo run test", "coverage": "vitest run --coverage", - "coverage:enforced": "node tools/coverage/enforced.mjs", + "coverage:enforced": "node tools/coverage-gate/run.mjs", "ci": "turbo run lint typecheck test && pnpm db:sync-check && pnpm typecheck:tools && pnpm lint:tools && turbo run build format:check && pnpm lint:fence-check && pnpm lint:engine-deps && pnpm lint:bundle-closure && pnpm smoke:cli", "lint:fence-check": "node tools/lint-fixtures/assert-fence.mjs", "lint:engine-deps": "node tools/engine-deps/check.mjs", diff --git a/tools/coverage-gate/run.mjs b/tools/coverage-gate/run.mjs new file mode 100644 index 00000000..9fe4c71e --- /dev/null +++ b/tools/coverage-gate/run.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node +// Run the repo-root coverage with ONLY the packages whose floor is a required CI check. +// +// Why a script and not `RELAVIUM_COVERAGE_ENFORCED_ONLY=1 vitest …` inline: that syntax is POSIX-only and +// fails on cmd.exe / PowerShell, and this repo runs a Windows CI leg and supports Windows developers. A +// `cross-env` dependency would need an ADR (CLAUDE.md rule 2) for something ten lines of Node already do. +// +// `vitest.config.ts` reads the env var and drops `packages/core` from the failing threshold set; see the +// comment there for the ruling that scopes it. +import { spawnSync } from 'node:child_process'; + +const result = spawnSync('vitest', ['run', '--coverage'], { + stdio: 'inherit', + shell: process.platform === 'win32', // resolve the .cmd shim on Windows + env: { ...process.env, RELAVIUM_COVERAGE_ENFORCED_ONLY: '1' }, +}); + +if (result.error) { + console.error(`✗ could not start vitest: ${result.error.message}`); + process.exit(1); +} +process.exit(result.status ?? 1); From 4cb3e3aecaeade9248d4330e4414c0eccadeaf55 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 29 Jul 2026 20:23:30 +0300 Subject: [PATCH 6/6] fix(ci): make the gate scripts cross-platform and hermetic; route the Sonar sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four review findings plus a Sonar batch. * `pnpm smoke:cli` was writing to the developer's REAL `~/.relavium/history.db`. The reviewer suggested a temporary cwd; that would not have worked — the path is home-relative (`db/open.ts` → `paths.ts` → `os.homedir()`), not cwd-relative, and no config overrides that root. `os.homedir()` does honour `$HOME` / `%USERPROFILE%`, so the smoke now runs against a `mkdtemp` sandbox and removes it in a `finally`. Verified by mtime: the real database is untouched across a full `pnpm run ci`. This is the hazard already tracked in deferred-tasks.md, reached from a new direction. * The smoke also used repo-relative paths (broken from any other cwd) and had no timeout, so a hung CLI would have sat until the CI job's own 15-minute kill with no usable signal. Absolute paths, a 120 s per-step bound, and explicit `error` / `signal` / `status` reporting. * `db:sync-check` was POSIX-only shell (`test -z "$(…)" || { …; }`), the same break-on-Windows class as `coverage:enforced`. Extracted to `tools/db-sync/check.mjs`, keeping `git status --porcelain` — `git diff` does not see an untracked new migration. * `tools/coverage-gate/run.mjs` spawned a bare `vitest`, resolved through `PATH` (Sonar), and needed `shell: true` on Windows to find the `.cmd` shim — two injection surfaces for one convenience. It now resolves vitest's own entry through the module graph and spawns `process.execPath` with an absolute path: no PATH lookup, no shell. * Deleted `ci.yml`'s reserved Phase-1 conformance lanes and their TODO. All three adapters ship and the live nightly lane runs in `models-catalog.yml`, so the TODO named work that had already happened elsewhere. DECLINED — `--ignore-scripts` on the four `pnpm install` steps (8 Sonar findings). `pnpm.onlyBuiltDependencies` in the root package.json already allowlists the only two packages permitted to run lifecycle scripts (`better-sqlite3` for its native prebuild, `esbuild` for its platform binary); every other transitive dependency is already blocked. Adding the flag would break both and strengthen nothing. The rationale is now in `ci.yml`'s header so the next sweep does not re-raise it. ROUTED — the ~30 remaining Sonar findings are in already-merged code outside this diff, so they go to the existing `Sonar code-quality backlog` in deferred-tasks.md under that section's own standing policy. Two are called out as more than cosmetic: `references.ts:154`'s regex has super-linear backtracking on the `{{ }}` filter-argument path (2.5.5.A scope, not a nit), and `chat-ink.tsx:1151`'s cognitive complexity of 91 belongs to the god-file 2.5.5.I deliberately declines to decompose. Verified with the full `pnpm run ci` — green, including the new smoke leg. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 46 ++++------------------- docs/roadmap/deferred-tasks.md | 17 +++++++++ package.json | 2 +- tools/cli-smoke/check.mjs | 69 +++++++++++++++++++++++++--------- tools/coverage-gate/run.mjs | 25 ++++++++++-- tools/db-sync/check.mjs | 49 ++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 60 deletions(-) create mode 100644 tools/db-sync/check.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 578c02c7..a8602a4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,13 @@ # # Third-party actions are pinned to a full commit SHA (the `# vX.Y.Z` comment tracks the # human-readable release) so a moved tag can't inject unreviewed code; bumps are deliberate. +# +# Install-script posture: `pnpm install` runs WITHOUT `--ignore-scripts` deliberately. Supply-chain risk is +# handled more precisely one level up, by `pnpm.onlyBuiltDependencies` in the root package.json, which +# allowlists the ONLY two packages permitted to run lifecycle scripts (`better-sqlite3` for its native +# prebuild, `esbuild` for its platform binary). Every other transitive dependency is already blocked. +# Passing `--ignore-scripts` here would break both of those and gain nothing — it is a weaker, blunter form +# of a control the repo already applies. name: CI on: @@ -299,42 +306,3 @@ jobs: echo "::error::chat entered raw mode without a TTY (the driver-selection gate regressed)"; exit 1 fi echo "✓ windows headless no-TTY smoke passed" - -# --- Reserved Phase-1 lanes (TODO: enable with the first provider adapter) ------------ -# The per-provider conformance suite and the nightly live-API lane land WITH the adapters -# in Phase 1 (testing.md); only their CI slots are reserved here so the testing standard -# maps cleanly onto lanes from day one. Do not enable until `packages/llm` exists — and -# pin each action to a commit SHA (as above) when uncommenting. -# -# conformance: -# name: provider conformance (fixtures) -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@ # pin on enable -# - uses: pnpm/action-setup@ -# - uses: actions/setup-node@ -# with: { node-version-file: .nvmrc, cache: pnpm } -# - run: pnpm install --frozen-lockfile -# - run: pnpm turbo run test:conformance # fixture mode — no network, no keys -# -# Nightly live-API lane — runs the conformance suite against real providers using keys -# from CI secrets. Separate workflow trigger so it never gates a PR: -# # on: -# # schedule: -# # - cron: '0 7 * * *' # 07:00 UTC nightly -# live-api: -# name: provider conformance (live, nightly) -# if: github.event_name == 'schedule' -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@ # pin on enable -# - uses: pnpm/action-setup@ -# - uses: actions/setup-node@ -# with: { node-version-file: .nvmrc, cache: pnpm } -# - run: pnpm install --frozen-lockfile -# - run: pnpm turbo run test:conformance:live -# env: -# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} -# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} -# GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} -# -------------------------------------------------------------------------------------- diff --git a/docs/roadmap/deferred-tasks.md b/docs/roadmap/deferred-tasks.md index 1b7253f9..de96207e 100644 --- a/docs/roadmap/deferred-tasks.md +++ b/docs/roadmap/deferred-tasks.md @@ -1047,6 +1047,23 @@ future test cannot silently re-acquire it. > pass. The 1.O-diff findings (the `tryParseJson` fence regex → string ops, and the `#nodeEmit` > duplicate cases → fallthrough) were fixed in PR #18; they are **not** listed here. +- [ ] **2026-07-29 Sonar sweep — ~30 findings in already-merged code, outside the Wave 0 diff.** Recorded + per this section's standing policy: a behaviour-preserving refactor of merged, tested code is its own + change, not feature scope. Highest-value first: `chat-ink.tsx:1151` cognitive complexity **91 → 15** + (the same god-file 2.5.5.I declines to decompose — hand it to 2.6.M's render-v2), `bounding.ts:165/:180` + two regexes at complexity 38 and 57, `references.ts:154` a regex with **super-linear backtracking** + (the only finding here with a runtime-safety edge — it parses `{{ }}` filter arguments, so treat it as + 2.5.5.A scope, not cosmetic), `agent-session.ts:545` complexity 17, `create-prompter.ts:70` a nested + ternary, three `RunApp.tsx` array-index keys, two `'never' is overridden` union types, two + `.some()` → `.includes()`, `openai.ts:481` nested template literals, `node.ts:60` `String.raw`, plus + ~10 test-only nits (`toHaveLength`, parameterised tests, `test.skip()`). + **Not actioned, with reasons:** the `--ignore-scripts` findings on all four workflows are declined — + `pnpm.onlyBuiltDependencies` (root `package.json`) already allowlists the only two packages permitted + to run install scripts, and adding the flag would break both while weakening nothing else; the + `ci.yml` reserved-lane TODO was deleted in Wave 0 rather than deferred (it was superseded by + `models-catalog.yml`'s live nightly lane). + *(chore · a dedicated `chore: sonar cleanup` pass)* + - [ ] **Duplicated SQL literal in the initial migration (0.x)** — Sonar flags a 4× literal in the generated drizzle migration. Migrations are **append-only / generated** (never hand-edited), so this is informational — only act if the literal recurs in the *schema source* a future migration regenerates. diff --git a/package.json b/package.json index 018c8117..dd881b40 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "sync:models": "pnpm turbo run build --filter=@relavium/llm && node tools/sync-models-dev/sync.mjs", "sync:models:check": "pnpm turbo run build --filter=@relavium/llm && node tools/sync-models-dev/sync.mjs --check", "lint:tools": "eslint tools --ignore-pattern 'tools/lint-fixtures/**'", - "db:sync-check": "pnpm --filter @relavium/db db:generate && test -z \"$(git status --porcelain packages/db/drizzle)\" || { git status --porcelain packages/db/drizzle; echo 'error: committed @relavium/db migration is out of sync with src/schema.ts — regenerate and commit: pnpm --filter @relavium/db db:generate'; exit 1; }", + "db:sync-check": "node tools/db-sync/check.mjs", "smoke:cli": "node tools/cli-smoke/check.mjs" }, "devDependencies": { diff --git a/tools/cli-smoke/check.mjs b/tools/cli-smoke/check.mjs index 1660db0e..273b1e83 100644 --- a/tools/cli-smoke/check.mjs +++ b/tools/cli-smoke/check.mjs @@ -1,25 +1,38 @@ #!/usr/bin/env node // Execute the COMPILED CLI bundle — the artifact that actually ships. // -// Shared by `.github/workflows/ci.yml`'s required job and the root `pnpm run ci` script, so the local gate -// and the real gate cannot drift on this check the way they did on `lint:tools` and the migration-sync check -// (#312). Nothing in the required gate ran the binary before (#294): a bundle that compiled but could not -// boot merged green. +// Shared by `.github/workflows/ci.yml`'s required job and the root `ci` script, so the local gate and the +// real gate cannot drift on this check the way they did on `lint:tools` and the migration-sync check (#312). +// Nothing in the required gate ran the binary before (#294): a bundle that compiled but could not boot +// merged green. // // The `run --json` leg is load-bearing beyond "it boots": it opens `history.db`, which proves the drizzle // migrations resolved beside the bundle. That is the failure `apps/cli/drizzle/**` becoming a declared turbo // output (#315) exists to prevent — a cache-hit replay leaving `dist/` fresh next to a missing `drizzle/`. +// +// HERMETIC BY CONSTRUCTION. `history.db` lives at `~/.relavium/history.db` (`db/open.ts` → `paths.ts` +// → `os.homedir()`), and there is no config override for that root — so a naive smoke run opens and MIGRATES +// the developer's real database, the hazard already tracked in deferred-tasks.md. `os.homedir()` honours +// `$HOME` on POSIX and `%USERPROFILE%` on Windows, so pointing both at a throwaway directory is the one +// lever that isolates it. Setting `cwd` would NOT: the path is home-relative, not cwd-relative. import { spawnSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; +import { mkdtempSync, rmSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; -const BUNDLE = 'apps/cli/dist/index.js'; -const FIXTURE = 'apps/cli/src/harness/fixtures/sequential.relavium.yaml'; +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const BUNDLE = join(repoRoot, 'apps/cli/dist/index.js'); +const FIXTURE = join(repoRoot, 'apps/cli/src/harness/fixtures/sequential.relavium.yaml'); +const STEP_TIMEOUT_MS = 120_000; if (!existsSync(BUNDLE)) { console.error(`✗ ${BUNDLE} is missing — run \`pnpm turbo run build\` first.`); process.exit(1); } +const sandboxHome = mkdtempSync(join(tmpdir(), 'relavium-smoke-')); + const steps = [ { label: '--version', args: ['--version'] }, { label: '--help', args: ['--help'] }, @@ -29,15 +42,37 @@ const steps = [ }, ]; -for (const { label, args } of steps) { - const r = spawnSync(process.execPath, [BUNDLE, ...args], { encoding: 'utf8' }); - if (r.status !== 0) { - console.error(`✗ compiled CLI failed: ${label} (exit ${r.status})`); - if (r.stderr) console.error(r.stderr.trim()); - process.exit(1); +try { + for (const { label, args } of steps) { + const r = spawnSync(process.execPath, [BUNDLE, ...args], { + cwd: repoRoot, + encoding: 'utf8', + timeout: STEP_TIMEOUT_MS, + env: { ...process.env, HOME: sandboxHome, USERPROFILE: sandboxHome }, + }); + + // A hung CLI must fail this check, not sit until the CI job's own timeout kills the whole run with no + // usable signal. `spawnSync` reports a timeout kill via `signal`, and surfaces spawn faults via `error`. + if (r.error !== undefined) { + console.error(`✗ compiled CLI failed to run: ${label} — ${r.error.message}`); + process.exit(1); + } + if (r.signal !== null) { + console.error( + `✗ compiled CLI was killed (${r.signal}) on: ${label} — likely the ${STEP_TIMEOUT_MS} ms timeout.`, + ); + if (r.stderr) console.error(r.stderr.trim()); + process.exit(1); + } + if (r.status !== 0) { + console.error(`✗ compiled CLI failed: ${label} (exit ${r.status})`); + if (r.stderr) console.error(r.stderr.trim()); + process.exit(1); + } } + console.log( + '✓ compiled CLI smoke passed (boots, renders help, runs a workflow against an isolated DB).', + ); +} finally { + rmSync(sandboxHome, { recursive: true, force: true }); } - -console.log( - '✓ compiled CLI smoke passed (boots, renders help, and runs a workflow against history.db).', -); diff --git a/tools/coverage-gate/run.mjs b/tools/coverage-gate/run.mjs index 9fe4c71e..516612f0 100644 --- a/tools/coverage-gate/run.mjs +++ b/tools/coverage-gate/run.mjs @@ -3,15 +3,34 @@ // // Why a script and not `RELAVIUM_COVERAGE_ENFORCED_ONLY=1 vitest …` inline: that syntax is POSIX-only and // fails on cmd.exe / PowerShell, and this repo runs a Windows CI leg and supports Windows developers. A -// `cross-env` dependency would need an ADR (CLAUDE.md rule 2) for something ten lines of Node already do. +// `cross-env` dependency would need an ADR (CLAUDE.md rule 2) for something a few lines of Node already do. // // `vitest.config.ts` reads the env var and drops `packages/core` from the failing threshold set; see the // comment there for the ruling that scopes it. import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; -const result = spawnSync('vitest', ['run', '--coverage'], { +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); + +// Resolve vitest's own entry through the module graph rather than letting the OS search `PATH`. Spawning a +// bare `vitest` would run whatever a writeable `PATH` entry shadows it with, and would need `shell: true` on +// Windows to find the `.cmd` shim — a second injection surface. `process.execPath` + an absolute script path +// needs neither. +const require = createRequire(join(repoRoot, 'package.json')); +let vitestEntry; +try { + const pkgPath = require.resolve('vitest/package.json'); + vitestEntry = join(dirname(pkgPath), require('vitest/package.json').bin.vitest); +} catch (error) { + console.error(`✗ cannot resolve the vitest entry point: ${error.message}`); + process.exit(1); +} + +const result = spawnSync(process.execPath, [vitestEntry, 'run', '--coverage'], { + cwd: repoRoot, stdio: 'inherit', - shell: process.platform === 'win32', // resolve the .cmd shim on Windows env: { ...process.env, RELAVIUM_COVERAGE_ENFORCED_ONLY: '1' }, }); diff --git a/tools/db-sync/check.mjs b/tools/db-sync/check.mjs new file mode 100644 index 00000000..b7e67f7a --- /dev/null +++ b/tools/db-sync/check.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// The committed @relavium/db migration must match `src/schema.ts`. Regenerate; if that produces any change +// under `packages/db/drizzle`, the committed migration was stale — a silent staleness turned into a red gate. +// +// Node rather than an inline shell one-liner for the same reason as `tools/coverage-gate/run.mjs`: the +// `test -z "$(…)" || { …; }` form is POSIX-only and fails on cmd.exe / PowerShell, and this repo runs a +// Windows CI leg. `git status --porcelain` (not `git diff`) because a NEW migration file is untracked, and +// `git diff` does not see untracked files. +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const DRIZZLE_DIR = 'packages/db/drizzle'; + +function run(command, args) { + return spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8', shell: false }); +} + +// pnpm is the process manager already running this script, so it is on PATH by construction here. +const generated = run('pnpm', ['--filter', '@relavium/db', 'db:generate']); +if (generated.error !== undefined) { + console.error(`✗ could not run db:generate — ${generated.error.message}`); + process.exit(1); +} +if (generated.status !== 0) { + console.error(`✗ db:generate failed (exit ${generated.status})`); + if (generated.stderr) console.error(generated.stderr.trim()); + process.exit(1); +} + +const status = run('git', ['status', '--porcelain', DRIZZLE_DIR]); +if (status.error !== undefined) { + console.error(`✗ could not run git status — ${status.error.message}`); + process.exit(1); +} + +const drift = (status.stdout ?? '').trim(); +if (drift !== '') { + console.error(drift); + console.error( + `✗ the committed @relavium/db migration is out of sync with src/schema.ts (or an upstream ` + + `@relavium/shared enum the CHECKs derive from). Regenerate and commit: ` + + `pnpm --filter @relavium/db db:generate`, + ); + process.exit(1); +} + +console.log('✓ the committed migration matches src/schema.ts.');