diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e18fba94..a8602a4c 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" @@ -16,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: @@ -118,6 +130,25 @@ 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 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 + + # 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 + 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 # dependency closure (`ink@7` / `cli-truncate` / `slice-ansi` → `>=22`); nothing runtime needs more. @@ -131,7 +162,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 +195,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: @@ -179,13 +210,13 @@ 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 (advisory) + name: engine coverage floor (llm, mcp) runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -214,8 +245,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 + @@ -270,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/.github/workflows/models-catalog.yml b/.github/workflows/models-catalog.yml index f686814d..44359506 100644 --- a/.github/workflows/models-catalog.yml +++ b/.github/workflows/models-catalog.yml @@ -60,6 +60,17 @@ 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. + # 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 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/roadmap/current.md b/docs/roadmap/current.md index 3f06f6f2..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: @@ -110,7 +115,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.)* @@ -241,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 @@ -264,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. @@ -337,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..de96207e 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) @@ -1044,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. @@ -1062,6 +1082,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 +1307,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 +1318,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 +1327,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..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 @@ -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..41de3bb5 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, @@ -892,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 analyzed in 2.6.P's ADR.) +- **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` / @@ -909,7 +929,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 +1057,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 +1086,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 +1132,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 +1247,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 +1460,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 +1500,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 +1513,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 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..dd881b40 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": "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", "lint:bundle-closure": "node tools/bundle-closure/check.mjs", @@ -25,7 +26,9 @@ "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": "node tools/db-sync/check.mjs", + "smoke:cli": "node tools/cli-smoke/check.mjs" }, "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/tools/cli-smoke/check.mjs b/tools/cli-smoke/check.mjs new file mode 100644 index 00000000..273b1e83 --- /dev/null +++ b/tools/cli-smoke/check.mjs @@ -0,0 +1,78 @@ +#!/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 `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 { mkdtempSync, rmSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; + +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'] }, + { + label: 'run --json (opens history.db → proves the migrations resolve)', + args: ['run', FIXTURE, '--input', 'n=21', '--json'], + }, +]; + +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 }); +} diff --git a/tools/coverage-gate/run.mjs b/tools/coverage-gate/run.mjs new file mode 100644 index 00000000..516612f0 --- /dev/null +++ b/tools/coverage-gate/run.mjs @@ -0,0 +1,41 @@ +#!/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 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 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', + 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); 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.'); diff --git a/vitest.config.ts b/vitest.config.ts index f9967c15..bddb4c17 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 } }), }, }, },