diff --git a/.github/labels.yml b/.github/labels.yml index 7c14b8904b..83deb6ff00 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -154,10 +154,10 @@ # release gates (publish / e2e-cli-prod*). - name: "ci:full" color: "d4c5f9" - description: "Force both expensive e2e-cli jobs to run (overrides docs-only + skip labels)" + description: "Force all expensive e2e-cli jobs to run (overrides docs-only + skip labels)" - name: "ci:skip-e2e" color: "d4c5f9" - description: "Skip the scaffold-runtime (aspire/docker/postgres) e2e job" + description: "Skip both scaffold-runtime e2e tiers (sqlite and aspire/docker/postgres)" - name: "ci:skip-scaffold" color: "d4c5f9" description: "Skip the scaffold-static (deno-only) scaffold gate" diff --git a/.github/scripts/ci-classify-changes.test.ts b/.github/scripts/ci-classify-changes.test.ts index ba77cb7cbf..cb5d7c6052 100644 --- a/.github/scripts/ci-classify-changes.test.ts +++ b/.github/scripts/ci-classify-changes.test.ts @@ -25,6 +25,20 @@ function vector(d: Decision) { const ALL_TRUE = { deno: true, docker: true, desktop: true, docs: true, surface: true }; const ALL_FALSE = { deno: false, docker: false, desktop: false, docs: false, surface: false }; +function workflowJob(source: string, id: string): string | undefined { + const lines = source.split('\n'); + const start = lines.indexOf(` ${id}:`); + if (start < 0) return undefined; + let end = lines.length; + for (let index = start + 1; index < lines.length; index++) { + if (/^ {2}[a-z][a-z0-9-]*:$/.test(lines[index])) { + end = index; + break; + } + } + return lines.slice(start + 1, end).join('\n'); +} + // ── rename-hole regression (adversarial review, defect 1) ──────────────────── Deno.test('regression: packages/cli/a.ts -> docs/a.md rename is NOT docs-only', () => { @@ -184,6 +198,7 @@ Deno.test('SAFETY: an unrecognised path forces EVERY output true', () => { }, `expected full escalation for: ${p}`); const d = decide({ eventName: 'pull_request', files: [p], labels: [] }); assertEquals(d.runStatic, true, p); + assertEquals(d.runRuntimeSqlite, true, p); assertEquals(d.runRuntime, true, p); assertEquals(vector(d), ALL_TRUE, p); } @@ -196,6 +211,7 @@ Deno.test('SAFETY: the classifier own sources force everything (.github/scripts) labels: [], }); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, true); assertEquals(d.runRuntime, true); assertEquals(vector(d), ALL_TRUE); }); @@ -446,6 +462,7 @@ Deno.test('decide: docs-only PR skips both jobs', () => { }); assertEquals(d.docsOnly, true); assertEquals(d.runStatic, false); + assertEquals(d.runRuntimeSqlite, false); assertEquals(d.runRuntime, false); }); @@ -490,6 +507,7 @@ Deno.test('decide: one code file forces both jobs', () => { }); assertEquals(d.docsOnly, false); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, true); assertEquals(d.runRuntime, true); }); @@ -497,6 +515,7 @@ Deno.test('decide: empty diff runs EVERYTHING (cannot classify)', () => { const d = decide({ eventName: 'pull_request', files: [], labels: [] }); assertEquals(d.docsOnly, false); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, true); assertEquals(d.runRuntime, true); assertEquals(vector(d), ALL_TRUE); }); @@ -508,7 +527,12 @@ Deno.test('decide: ci:skip-e2e skips runtime only', () => { labels: ['ci:skip-e2e'], }); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, false); assertEquals(d.runRuntime, false); + assertEquals( + d.reason.includes('scaffold-runtime-sqlite skipped by ci:skip-e2e'), + true, + ); }); Deno.test('decide: ci:skip-scaffold skips static only', () => { @@ -518,7 +542,14 @@ Deno.test('decide: ci:skip-scaffold skips static only', () => { labels: ['ci:skip-scaffold'], }); assertEquals(d.runStatic, false); + // ci:skip-scaffold is not an independent sqlite-runtime override. The + // derived tier is false here because its run_static prerequisite is false. + assertEquals(d.runRuntimeSqlite, false); assertEquals(d.runRuntime, true); + assertEquals( + d.reason.includes('scaffold-runtime-sqlite skipped: scaffold-static signal is off'), + true, + ); }); Deno.test('decide: both skip labels skip both jobs', () => { @@ -528,6 +559,7 @@ Deno.test('decide: both skip labels skip both jobs', () => { labels: ['ci:skip-scaffold', 'ci:skip-e2e'], }); assertEquals(d.runStatic, false); + assertEquals(d.runRuntimeSqlite, false); assertEquals(d.runRuntime, false); }); @@ -551,8 +583,13 @@ Deno.test('decide: ci:full overrides docs-only and forces the ENTIRE vector', () labels: ['ci:full'], }); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, true); assertEquals(d.runRuntime, true); assertEquals(vector(d), ALL_TRUE); + assertEquals( + d.reason.includes('scaffold-runtime-sqlite forced by ci:full'), + true, + ); }); Deno.test('decide: ci:full overrides skip labels', () => { @@ -562,12 +599,14 @@ Deno.test('decide: ci:full overrides skip labels', () => { labels: ['ci:full', 'ci:skip-e2e', 'ci:skip-scaffold'], }); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, true); assertEquals(d.runRuntime, true); }); Deno.test('decide: workflow_dispatch runs everything (no diff)', () => { const d = decide({ eventName: 'workflow_dispatch', files: [], labels: [] }); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, true); assertEquals(d.runRuntime, true); assertEquals(vector(d), ALL_TRUE); }); @@ -579,9 +618,123 @@ Deno.test('decide: workflow_dispatch honours skip labels', () => { labels: ['ci:skip-e2e'], }); assertEquals(d.runStatic, true); + assertEquals(d.runRuntimeSqlite, false); assertEquals(d.runRuntime, false); }); +Deno.test('decide: workflow_dispatch honours ci:skip-scaffold for sqlite runtime', () => { + const d = decide({ + eventName: 'workflow_dispatch', + files: [], + labels: ['ci:skip-scaffold'], + }); + assertEquals(d.runStatic, false); + assertEquals(d.runRuntimeSqlite, false); + assertEquals(d.runRuntime, true); + assertEquals( + d.reason.includes('scaffold-runtime-sqlite skipped: scaffold-static signal is off'), + true, + ); +}); + +Deno.test('decide: sqlite runtime reason follows the scaffold signal', () => { + const d = decide({ + eventName: 'pull_request', + files: ['packages/cli/mod.ts'], + labels: [], + }); + assertEquals(d.runRuntimeSqlite, true); + assertEquals( + d.reason.includes('scaffold-runtime-sqlite: scaffold-static signal is on'), + true, + ); +}); + +Deno.test('workflow: sqlite runtime uses sibling diff guard and fails closed', async () => { + const workflow = await Deno.readTextFile('.github/workflows/e2e-cli.yml'); + const cliSurface = await Deno.readTextFile( + 'packages/cli/e2e/src/domain/cli-surface.ts', + ); + const suiteId = /RUNTIME_SQLITE:\s*'([^']+)'/.exec(cliSurface)?.[1]; + assertEquals(typeof suiteId, 'string'); + const sqliteJob = workflowJob(workflow, 'scaffold-runtime-sqlite'); + assertEquals(typeof sqliteJob, 'string'); + assertEquals( + sqliteJob!.includes( + "if: ${{ !cancelled() && needs.classify.result != 'skipped' && needs.classify.outputs.diff_unavailable != 'true' }}", + ), + true, + ); + assertEquals( + sqliteJob!.includes( + "RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.run_runtime_sqlite == 'true' }}", + ), + true, + ); + + const classifyJob = workflowJob(workflow, 'classify'); + assertEquals(typeof classifyJob, 'string'); + assertEquals( + classifyJob!.includes( + 'run_runtime_sqlite: ${{ steps.decide.outputs.run_runtime_sqlite }}', + ), + true, + ); + + assertEquals( + sqliteJob!.includes( + `deno task e2e:cli run ${suiteId} --cleanup --format pretty`, + ), + true, + ); + assertEquals( + sqliteJob!.includes('group: e2e-scaffold-runtime-sqlite-global'), + true, + ); + assertEquals( + sqliteJob!.includes('name: e2e-cli-scaffold-runtime-sqlite-report'), + true, + ); + assertEquals( + sqliteJob!.includes('.llm/tmp/**/report*.ndjson'), + true, + ); + + const postgresJob = workflowJob(workflow, 'scaffold-runtime'); + assertEquals(typeof postgresJob, 'string'); + assertEquals( + postgresJob!.includes('group: e2e-scaffold-runtime-global'), + true, + ); + assertEquals( + postgresJob!.includes('name: e2e-cli-scaffold-runtime-report'), + true, + ); + assertEquals( + sqliteJob!.includes('group: e2e-scaffold-runtime-global\n'), + false, + ); + assertEquals( + sqliteJob!.includes('name: e2e-cli-scaffold-runtime-report\n'), + false, + ); + + const visibilityJob = workflowJob(workflow, 'lane-visibility'); + assertEquals(typeof visibilityJob, 'string'); + assertEquals( + visibilityJob!.includes( + 'needs: [classify, scaffold-static, scaffold-runtime, scaffold-runtime-sqlite, desktop-native-linux]', + ), + true, + ); + assertEquals( + visibilityJob!.includes( + 'printf \'| `scaffold-runtime-sqlite` | %s |\\n\' "$runtime_sqlite_outcome"', + ), + true, + ); +}); + Deno.test('parseLabels: JSON array and comma forms', () => { assertEquals(parseLabels('["a","b"]'), ['a', 'b']); assertEquals(parseLabels('a, b ,c'), ['a', 'b', 'c']); diff --git a/.github/scripts/ci-classify-changes.ts b/.github/scripts/ci-classify-changes.ts index 5da82a55bc..196e09e524 100644 --- a/.github/scripts/ci-classify-changes.ts +++ b/.github/scripts/ci-classify-changes.ts @@ -5,9 +5,11 @@ * `.github/workflows/ci.yml`, and `.github/workflows/surface-diff.yml` * (#1152; paths are the mechanism, labels are the override): * - * - `run_static` -> scaffold-static (any scaffold-impacting change) - * - `run_runtime` -> scaffold-runtime; docker is the exception tier, - * reached on the docker signal (v1: deliberately wide) + * - `run_static` -> scaffold-static (any scaffold-impacting change) + * - `run_runtime_sqlite` -> scaffold-runtime-sqlite (the cheap runtime tier, + * reached when static runs unless e2e is skipped) + * - `run_runtime` -> scaffold-runtime; docker is the exception tier, + * reached on the docker signal (v1: deliberately wide) * - `needs_deno` -> check-test / quality: any change the Deno toolchain * checks or tests (root `deno test` discovers * `.llm/tools` and `.github/scripts` tests, so code @@ -30,8 +32,9 @@ * * Label precedence (highest first) — the set is frozen at exactly three: * 1. `ci:full` -> force EVERY output true. - * 2. `ci:skip-scaffold` -> skip `scaffold-static`. - * `ci:skip-e2e` -> skip `scaffold-runtime`. + * 2. `ci:skip-scaffold` -> skip `scaffold-static` and its derived + * `scaffold-runtime-sqlite` tier. + * `ci:skip-e2e` -> skip both runtime tiers. * (Skip labels keep their scaffold-tier-only semantics; they never * widen to the required trio or desktop.) * 3. otherwise -> paths decide. @@ -258,6 +261,7 @@ export interface DecisionInput { export interface Decision { runStatic: boolean; + runRuntimeSqlite: boolean; runRuntime: boolean; docsOnly: boolean; needsDeno: boolean; @@ -268,9 +272,10 @@ export interface Decision { reason: string; } -function fullDecision(docsOnly: boolean, reason: string): Decision { +function fullDecision(docsOnly: boolean, reason: string, sqliteReason: string): Decision { return { runStatic: true, + runRuntimeSqlite: true, runRuntime: true, docsOnly, needsDeno: true, @@ -278,7 +283,7 @@ function fullDecision(docsOnly: boolean, reason: string): Decision { needsDesktop: true, needsDocs: true, needsSurface: true, - reason, + reason: `${reason}. ${sqliteReason}`, }; } @@ -297,21 +302,38 @@ export function decide(input: DecisionInput): Decision { // unless an explicit skip label is present. `ci:full` still wins. if (input.eventName !== 'pull_request') { if (forceFull) { - return fullDecision(false, `${input.eventName}: ci:full -> run everything`); + return fullDecision( + false, + `${input.eventName}: ci:full -> run everything`, + 'scaffold-runtime-sqlite forced by ci:full', + ); } + const runStatic = !skipScaffold; + const runRuntimeSqlite = !skipScaffold && !skipE2e; + const sqliteReason = skipE2e + ? 'scaffold-runtime-sqlite skipped by ci:skip-e2e' + : !runStatic + ? 'scaffold-runtime-sqlite skipped: scaffold-static signal is off' + : 'scaffold-runtime-sqlite: scaffold-static signal is on'; return { ...fullDecision( false, `${input.eventName}: no diff to classify -> run (skip labels honoured)`, + sqliteReason, ), - runStatic: !skipScaffold, + runStatic, + runRuntimeSqlite, runRuntime: !skipE2e, }; } const changed = input.files.map(normalise).filter((p) => p.length > 0); if (changed.length === 0) { - return fullDecision(false, 'empty diff: nothing to classify -> run everything'); + return fullDecision( + false, + 'empty diff: nothing to classify -> run everything', + 'scaffold-runtime-sqlite: scaffold-static signal is on', + ); } const rootConfigChanged = changed.some((p) => p === 'deno.json' || p === 'deno.jsonc'); @@ -334,7 +356,11 @@ export function decide(input: DecisionInput): Decision { const docsOnly = impacting.length === 0; if (forceFull) { - return fullDecision(docsOnly, 'ci:full label present -> force everything'); + return fullDecision( + docsOnly, + 'ci:full label present -> force everything', + 'scaffold-runtime-sqlite forced by ci:full', + ); } // scaffold-static @@ -365,6 +391,15 @@ export function decide(input: DecisionInput): Decision { runtimeReason = 'scaffold-runtime: docker-tier change detected'; } + // scaffold-runtime-sqlite (the cheap runtime tier follows the static + // scaffold signal; ci:skip-e2e remains authoritative over both runtimes). + const runRuntimeSqlite = runStatic && !skipE2e; + const sqliteReason = skipE2e + ? 'scaffold-runtime-sqlite skipped by ci:skip-e2e' + : !runStatic + ? 'scaffold-runtime-sqlite skipped: scaffold-static signal is off' + : 'scaffold-runtime-sqlite: scaffold-static signal is on'; + const impactingNote = docsOnly ? `${changed.length} file(s), all docs-only` : `${impacting.length}/${changed.length} impacting file(s), e.g. ${ @@ -375,6 +410,7 @@ export function decide(input: DecisionInput): Decision { return { runStatic, + runRuntimeSqlite, runRuntime, docsOnly, needsDeno: caps.deno, @@ -382,7 +418,7 @@ export function decide(input: DecisionInput): Decision { needsDesktop: caps.desktop, needsDocs: caps.docs, needsSurface: caps.surface, - reason: `${impactingNote}. ${staticReason}; ${runtimeReason}. ${vectorNote}`, + reason: `${impactingNote}. ${staticReason}; ${sqliteReason}; ${runtimeReason}. ${vectorNote}`, }; } @@ -484,6 +520,7 @@ async function main(): Promise { const lines = [ `run_static=${decision.runStatic}`, + `run_runtime_sqlite=${decision.runRuntimeSqlite}`, `run_runtime=${decision.runRuntime}`, `docs_only=${decision.docsOnly}`, `needs_deno=${decision.needsDeno}`, @@ -500,6 +537,7 @@ async function main(): Promise { console.log(` labels: ${labels.join(', ') || '(none)'}`); console.log(` changed: ${files.length} file(s)`); console.log(` run_static: ${decision.runStatic}`); + console.log(` run_runtime_sqlite: ${decision.runRuntimeSqlite}`); console.log(` run_runtime: ${decision.runRuntime}`); console.log(` docs_only: ${decision.docsOnly}`); console.log(` needs_deno: ${decision.needsDeno}`); diff --git a/.github/workflows/e2e-cli.yml b/.github/workflows/e2e-cli.yml index d22e4b491c..904e4a0284 100644 --- a/.github/workflows/e2e-cli.yml +++ b/.github/workflows/e2e-cli.yml @@ -21,26 +21,36 @@ name: e2e-cli # unverified and likely needs a debugging pass before this job is promoted to a # required check via branch protection. # +# - scaffold-runtime-sqlite — the cheaper full-runtime tier +# (`deno task e2e:cli run scaffold.runtime.sqlite --cleanup`): the same +# generated-project runtime path with sqlite and no Postgres/Redis resources, +# avoiding Postgres and Redis containers. The postgres tier above remains +# the merge-readiness bar. +# # Triggers: every PR to main or an integration branch (`feat/**`, `epic/**`), PLUS # any other PR carrying the `e2e-cli-gate` label, PLUS manual dispatch. # # Skip policy (see `.github/scripts/ci-classify-changes.ts`): the `classify` job -# emits the #1152 capability vector (`run_static`, `run_runtime`, `needs_*`) -# from the PR diff and labels. All three expensive jobs still START and report +# emits the #1152 capability vector (`run_static`, `run_runtime_sqlite`, +# `run_runtime`, `needs_*`) +# from the PR diff and labels. All four expensive jobs still START and report # SUCCESS, but short-circuit to a "skipped-by-policy" step when their work is # not needed: # # - `run_static`: any scaffold-impacting change (`packages/`/`plugins/`/ # `apps/`, tier-defining workflows, toolchain `deno.json*`/`deno.lock`, # or any unrecognised path). +# - `run_runtime_sqlite`: the cheap runtime tier — `run_static` unless +# `ci:skip-e2e` is present. # - `run_runtime`: the docker tier — same signal v1 (deliberately wide; # tighten only against observed green history, per #1152). # - `needs_desktop`: the `.deb`/updater surface — `packages/cli/`, tier # workflows, toolchain config (#1151/#1152). # - Only `e2e-cli.yml` and `ci.yml` escalate the scaffold tiers; other # workflow edits and tasks-only root `deno.json` diffs do not (#1122). -# - `ci:skip-scaffold` -> skip scaffold-static; `ci:skip-e2e` -> skip -# scaffold-runtime; `ci:full` -> force everything regardless. +# - `ci:skip-scaffold` -> skip scaffold-static and its derived sqlite runtime +# tier; `ci:skip-e2e` -> skip both runtime tiers; `ci:full` -> force +# everything regardless. # # The jobs run + report SUCCESS rather than using `paths-ignore`/job-level `if`, # so they never strand a required status check if promoted to one later. ci.yml @@ -88,6 +98,7 @@ jobs: outputs: diff_unavailable: ${{ steps.diff.outputs.diff_unavailable }} run_static: ${{ steps.decide.outputs.run_static }} + run_runtime_sqlite: ${{ steps.decide.outputs.run_runtime_sqlite }} run_runtime: ${{ steps.decide.outputs.run_runtime }} docs_only: ${{ steps.decide.outputs.docs_only }} needs_desktop: ${{ steps.decide.outputs.needs_desktop }} @@ -306,6 +317,90 @@ jobs: **/e2e-report*.json if-no-files-found: ignore + scaffold-runtime-sqlite: + name: scaffold-runtime-sqlite (aspire + sqlite + garnet) + needs: classify + concurrency: + group: e2e-scaffold-runtime-sqlite-global + cancel-in-progress: false + # Job always starts so a policy skip still reports SUCCESS. The sqlite + # runtime follows the scaffold-impact signal, while ci:skip-e2e remains + # authoritative over both runtime tiers. + # FAIL-CLOSED: if `classify` FAILED, this job still runs and RUN defaults to + # true — a skip requires classify to have SUCCEEDED with an explicit + # `run_runtime_sqlite=false`. `skipped` classify (applicability gate) keeps + # the old skip behavior. + if: ${{ !cancelled() && needs.classify.result != 'skipped' && needs.classify.outputs.diff_unavailable != 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 40 + env: + RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.run_runtime_sqlite == 'true' }} + SKIP_REASON: ${{ needs.classify.outputs.reason }} + steps: + - name: Skipped by policy + if: env.RUN != 'true' + # REASON contains raw changed-file names — never interpolate it into + # shell source; pass via env and print with printf. + run: | + printf '::notice::scaffold-runtime-sqlite skipped by policy. %s\n' "$SKIP_REASON" + + - name: Checkout + if: env.RUN == 'true' + uses: actions/checkout@v5 + + - name: Setup Deno + if: env.RUN == 'true' + uses: denoland/setup-deno@v2 + with: + deno-version: "2.9.0" + + - name: Setup .NET + if: env.RUN == 'true' + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + + - name: Install Aspire CLI + if: env.RUN == 'true' + env: + ASPIRE_CLI_VERSION: "13.4.6" + run: | + dotnet tool install Aspire.Cli --tool-path "$HOME/.aspire/bin" --version "$ASPIRE_CLI_VERSION" + echo "$HOME/.aspire/bin" >> "$GITHUB_PATH" + + - name: Aspire CLI preflight + if: env.RUN == 'true' + run: | + aspire_version="$(aspire --version)" + echo "$aspire_version" + case "$aspire_version" in + 13.4.*) ;; + *) echo "Expected Aspire CLI 13.4.x, got $aspire_version" >&2; exit 1 ;; + esac + + - name: Install workspace dependencies + if: env.RUN == 'true' + run: deno install + + - name: SQLite scaffold runtime E2E (one pass, with cleanup) + if: env.RUN == 'true' + run: deno task e2e:cli run scaffold.runtime.sqlite --cleanup --format pretty --report .llm/tmp/e2e-report-scaffold-runtime-sqlite.json + + - name: Print failed E2E gate evidence + if: failure() && env.RUN == 'true' + run: deno run --allow-read .llm/tools/e2e/print-failed-report-steps.ts .llm/tmp/e2e-report-scaffold-runtime-sqlite.json + + - name: Upload E2E report artifact + if: always() && env.RUN == 'true' + uses: actions/upload-artifact@v5 + with: + name: e2e-cli-scaffold-runtime-sqlite-report + path: | + .llm/tmp/**/report*.json + .llm/tmp/**/report*.ndjson + **/e2e-report*.json + if-no-files-found: ignore + desktop-native-linux: name: desktop-native-linux (deb + signed updater) needs: classify @@ -397,7 +492,7 @@ jobs: lane-visibility: name: scaffold CI lane visibility - needs: [classify, scaffold-static, scaffold-runtime, desktop-native-linux] + needs: [classify, scaffold-static, scaffold-runtime, scaffold-runtime-sqlite, desktop-native-linux] if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -406,9 +501,11 @@ jobs: env: CLASSIFY_RESULT: ${{ needs.classify.result }} RUN_STATIC: ${{ needs.classify.outputs.run_static }} + RUN_RUNTIME_SQLITE: ${{ needs.classify.outputs.run_runtime_sqlite }} RUN_RUNTIME: ${{ needs.classify.outputs.run_runtime }} RUN_DESKTOP: ${{ needs.classify.outputs.needs_desktop }} STATIC_RESULT: ${{ needs.scaffold-static.result }} + RUNTIME_SQLITE_RESULT: ${{ needs.scaffold-runtime-sqlite.result }} RUNTIME_RESULT: ${{ needs.scaffold-runtime.result }} DESKTOP_NATIVE_LINUX_RESULT: ${{ needs.desktop-native-linux.result }} run: | @@ -431,6 +528,7 @@ jobs: fi static_outcome="$(describe_scaffold_lane "$RUN_STATIC" "$STATIC_RESULT")" + runtime_sqlite_outcome="$(describe_scaffold_lane "$RUN_RUNTIME_SQLITE" "$RUNTIME_SQLITE_RESULT")" runtime_outcome="$(describe_scaffold_lane "$RUN_RUNTIME" "$RUNTIME_RESULT")" desktop_outcome="$(describe_scaffold_lane "$RUN_DESKTOP" "$DESKTOP_NATIVE_LINUX_RESULT")" @@ -444,6 +542,7 @@ jobs: echo "| --- | --- |" printf '| `classify` | %s |\n' "$classify_outcome" printf '| `scaffold-static` | %s |\n' "$static_outcome" + printf '| `scaffold-runtime-sqlite` | %s |\n' "$runtime_sqlite_outcome" printf '| `scaffold-runtime` | %s |\n' "$runtime_outcome" printf '| `desktop-native-linux` | %s |\n' "$desktop_outcome" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.llm/harness/debt/arch-debt.md b/.llm/harness/debt/arch-debt.md index 3ef1863458..c1bc033b6e 100644 --- a/.llm/harness/debt/arch-debt.md +++ b/.llm/harness/debt/arch-debt.md @@ -143,8 +143,8 @@ finding into a debt entry. ## packages/cli — CommunityToolkit Deno/SQLite TypeScript AppHost re-enable deferred -- **Reason:** The scaffold still uses `_aspire-compat.mjs` plus generated `builder.addExecutable(...)` - registrations for Deno resources. Current Aspire 13.4 docs state +- **Reason:** The scaffold still uses `_aspire-compat.mjs` plus generated + `builder.addExecutable(...)` registrations for Deno resources. Current Aspire 13.4 docs state `CommunityToolkit.Aspire.Hosting.Deno` does not expose `addDenoApp` or `addDenoTask` APIs in the TypeScript SDK, and SQLite hosting is C#-only for AppHost APIs with TypeScript directed to `builder.addConnectionString(...)`. @@ -176,8 +176,7 @@ finding into a debt entry. design for ownership, conflict handling, generated DB artifacts, and user-edited files. - **Owner:** Future CLI plugin lifecycle program. - **Target:** Before marketplace uninstall is documented or exposed as supported. -- **Linked plan:** `.llm/tmp/run/issue-167-marketplace-plugin-install/plan.md` (Non-Scope, D5, - S12). +- **Linked plan:** `.llm/tmp/run/issue-167-marketplace-plugin-install/plan.md` (Non-Scope, D5, S12). - **Created:** 2026-06-28 - **Status:** closed by #1236 / PR #1237 (2026-08-04). - **Gate:** New uninstall contract and `e2e:cli` add/remove/re-add suite pass without orphaned @@ -190,13 +189,13 @@ finding into a debt entry. - **ID:** `ISSUE-167-MARKETPLACE-PORTAL-SIGNATURES` - **Title:** Marketplace portal and publisher-signature curation are future work. -- **Context:** Issue #167 establishes the JSR package scope, static plugin protocol, trust tiers, and - confirmation boundary needed for a marketplace, but it does not build `market.netscript.dev`, +- **Context:** Issue #167 establishes the JSR package scope, static plugin protocol, trust tiers, + and confirmation boundary needed for a marketplace, but it does not build `market.netscript.dev`, `plugin search`, curated registry metadata, publisher signatures, provenance attestations, or verified publisher workflows. -- **Why deferred:** The approved slice set was the install foundation. Portal discovery and signature - curation require product, trust, release, and governance decisions that should be user-gated and - evaluated separately from the installer mechanics. +- **Why deferred:** The approved slice set was the install foundation. Portal discovery and + signature curation require product, trust, release, and governance decisions that should be + user-gated and evaluated separately from the installer mechanics. - **Trigger to revisit:** Marketplace roadmap Phase 2/3 or any plan to advertise curated third-party plugin discovery; require a publisher trust model, provenance gate, registry ingestion policy, and UI/API ownership decision. @@ -214,7 +213,8 @@ finding into a debt entry. ## plugins/* — ISSUE-167-OPTION-B-PACKAGE-RENAME - **ID:** `ISSUE-167-OPTION-B-PACKAGE-RENAME` -- **Title:** Option B package rename from `@netscript/plugin-` to `@netscript/` deferred. +- **Title:** Option B package rename from `@netscript/plugin-` to `@netscript/` + deferred. - **Context:** D1 kept the existing published package names and added a bare-kind alias map (`workers` -> `@netscript/plugin-workers`, etc.) as the verified-scope guard. The cleaner marketplace identity of `@netscript/` was considered and explicitly deferred. @@ -252,8 +252,8 @@ finding into a debt entry. `@netscript/plugin`; standalone extraction is future work. - **Owner:** Plugin platform maintainers. - **Target:** Before protocol versioning needs an independent release cadence. -- **Linked plan:** `.llm/tmp/run/issue-167-marketplace-plugin-install/plan.md` (D8, D3 backlog, - S1 drift). +- **Linked plan:** `.llm/tmp/run/issue-167-marketplace-plugin-install/plan.md` (D8, D3 backlog, S1 + drift). - **Created:** 2026-06-28 - **Status:** open, DEBT_ACCEPTED. - **Gate:** New package plan proves export compatibility, migration path, publish dry-run, and @@ -264,9 +264,9 @@ finding into a debt entry. - **ID:** `ISSUE-167-PROD-JSR-SCAFFOLD-E2E` - **Title:** Production `deno x jsr:/scaffold` validation is post-publish. - **Context:** S4 drift established that local files must run with `deno run`, not `deno x`; S11 - therefore validates a true userland project outside the checkout with explicit `--local-path`. - The production JSR command shape remains `deno x jsr:@netscript/plugin-/scaffold` - and cannot be fully exercised until the new `./scaffold` exports are published. + therefore validates a true userland project outside the checkout with explicit `--local-path`. The + production JSR command shape remains `deno x jsr:@netscript/plugin-/scaffold` and + cannot be fully exercised until the new `./scaffold` exports are published. - **Why deferred:** Pre-merge validation cannot execute unpublished JSR package exports. Claiming prod-JSR green before alpha.13 would overstate the evidence. - **Trigger to revisit:** Immediately after alpha.13 publication; run `e2e-cli-prod` against the @@ -275,14 +275,14 @@ finding into a debt entry. gates; the post-publish production JSR leg remains a release validation item. - **Owner:** Release / e2e-cli-prod owner for alpha.13. - **Target:** Alpha.13 post-publish verification. -- **Linked plan:** `.llm/tmp/run/issue-167-marketplace-plugin-install/plan.md` (Hidden Scope, - Risk Register, S11 Scope Boundary); `.llm/tmp/run/issue-167-marketplace-plugin-install/drift.md` - (S4 local-path drift). +- **Linked plan:** `.llm/tmp/run/issue-167-marketplace-plugin-install/plan.md` (Hidden Scope, Risk + Register, S11 Scope Boundary); `.llm/tmp/run/issue-167-marketplace-plugin-install/drift.md` (S4 + local-path drift). - **Created:** 2026-06-28 - **Status:** open, DEBT_ACCEPTED until alpha.13 post-publish smoke passes. - **Gate:** `e2e-cli-prod` proves official plugin install through - `deno x jsr:@netscript/plugin-/scaffold` after alpha.13; close by recording raw exit code and - suite/test counts. + `deno x jsr:@netscript/plugin-/scaffold` after alpha.13; close by recording raw exit code + and suite/test counts. ## packages/cli — Deno KV cache backend TypeScript AppHost resource emission deferred @@ -372,8 +372,8 @@ finding into a debt entry. 11-entrypoint export set. - **F-6 (JSR publishability):** `deno publish --dry-run` green; `workspace-mutator` JSR rewrite-map covers every telemetry subpath (root, `/orpc`, `/otel`, `/query`, `/registry`, - `/testing`). Env config validated with Standard Schema. All consumers in `packages/` + `plugins/` - still compile; T1 TC-1..14 convention contract preserved. + `/testing`). Env config validated with Standard Schema. All consumers in `packages/` + + `plugins/` still compile; T1 TC-1..14 convention contract preserved. ## packages/triggers — doctrine verdict Restructure @@ -386,9 +386,9 @@ finding into a debt entry. this heading no longer exists; it was superseded by `packages/plugin-triggers-core` (plus the `plugins/triggers` connector) during the plugin re-architecture. The successor package carries a doctrine-compliant role-named layout (`domain/`, `ports/`, `runtime/`, `adapters/`, `stores/`, - `builders/`, `config/`, `contracts/`, `telemetry/`, `testing/`, `public/`) with no flat - root-level source files, so the Restructure concern (lift flat files into role folders) is fully - addressed by the successor rather than relocated. Heading retained as the historical record. + `builders/`, `config/`, `contracts/`, `telemetry/`, `testing/`, `public/`) with no flat root-level + source files, so the Restructure concern (lift flat files into role folders) is fully addressed by + the successor rather than relocated. Heading retained as the historical record. - **Gate:** F-3, F-11, F-13 ## packages/plugin-triggers-core — T4 slow-type publish carve-out @@ -430,10 +430,9 @@ finding into a debt entry. oRPC's Zod input parsing, which would consume and transform the body and break signature verification. Converging it to the `createPluginService` + contract-bound-implementer shape (as workers/sagas/auth now do) requires implementing the 8 missing routes AND adding a new - `createPluginService` raw-route escape hatch in `@netscript/plugin/service` for HMAC webhooks — - a feature build + package-core change, not a soundness refactor. Split out per user decision - ("Defer to planned slice"); the other four plugins (workers/sagas/triggers-core/auth) are now - SOUND. + `createPluginService` raw-route escape hatch in `@netscript/plugin/service` for HMAC webhooks — a + feature build + package-core change, not a soundness refactor. Split out per user decision ("Defer + to planned slice"); the other four plugins (workers/sagas/triggers-core/auth) are now SOUND. - **Owner:** #172 plugin convergence — triggers connector slice. - **Target:** Before #172 is considered fully converged (all five plugins on the canonical thin-connector shape). @@ -443,9 +442,9 @@ finding into a debt entry. - **Status:** open - **Gate:** triggers connector assembled via `triggersContractV1.$context<...>().router()` with all 10 business routes + describe implemented, the raw-body webhook served through a sanctioned - `createPluginService` raw-route capability, `main.ts` migrated to `createPluginService(...).serve()`, - zero `any` / `Record` handler maps; scoped check/lint/test green and - `deno publish --dry-run` Success without `--allow-slow-types`. + `createPluginService` raw-route capability, `main.ts` migrated to + `createPluginService(...).serve()`, zero `any` / `Record` handler maps; scoped + check/lint/test green and `deno publish --dry-run` Success without `--allow-slow-types`. ## plugins/streams — connector SOUND convergence deferred (`streams-connector-sound-deferred`) @@ -456,22 +455,22 @@ finding into a debt entry. workers/sagas/triggers-core/auth). The streams connector (`plugins/streams/services/src/main.ts`) is a **pure transparent proxy**: it starts the upstream `@durable-streams/server` `DurableStreamTestServer` on an internal port and fronts it with a Hono app that serves - `/health[/live|/ready]` and then `app.all('/*')` proxies every other request (including the - raw streaming body, via `c.req.raw.body` + `duplex: 'half'`) to the upstream. That catch-all + `/health[/live|/ready]` and then `app.all('/*')` proxies every other request (including the raw + streaming body, via `c.req.raw.body` + `duplex: 'half'`) to the upstream. That catch-all passthrough cannot be expressed as an oRPC router, and `createPluginService` (`@netscript/plugin/service`) is entirely `withRPC`-driven — so migrating the connector to the canonical `createPluginService(...).serve()` shape requires the **same raw-route escape hatch** the deferred triggers connector needs (see `triggers-connector-sound-deferred`). The connector - itself has no `any` / `Record` handler maps; its only escapes are a - platform-gap `@ts-ignore` for `RequestInit.duplex` (not yet in Deno's lib types) in the proxy - body. Pre-existing minor `-core` casts unrelated to service-typesafety: `import.meta.env as any` + itself has no `any` / `Record` handler maps; its only escapes are a platform-gap + `@ts-ignore` for `RequestInit.duplex` (not yet in Deno's lib types) in the proxy body. + Pre-existing minor `-core` casts unrelated to service-typesafety: `import.meta.env as any` cross-runtime env probe in `stream-url-resolver.ts` and two `as unknown as` bridges to the upstream `@durable-streams` generic types in `define-stream-schema.ts` — neither is an oRPC Hole-A/Hole-B hole; left as-is pending the connector slice. - **Owner:** #172 plugin convergence — proxy-connector slice (fold with - `triggers-connector-sound-deferred`: both connectors are blocked on the one - `createPluginService` raw-route capability and should land in the same PLAN-EVAL-gated, - daemon-attached WSL Codex slice that builds it once). + `triggers-connector-sound-deferred`: both connectors are blocked on the one `createPluginService` + raw-route capability and should land in the same PLAN-EVAL-gated, daemon-attached WSL Codex slice + that builds it once). - **Target:** Before #172 is considered fully converged (all five plugins on the canonical thin-connector shape). - **Linked plan:** to be authored jointly with the triggers connector slice — framework-source + @@ -479,9 +478,9 @@ finding into a debt entry. - **Created:** 2026-06-30 - **Status:** open - **Gate:** streams connector `main.ts` migrated to `createPluginService(...).serve()` with the - upstream proxy served through the sanctioned `createPluginService` raw-route capability, - base-meta (`describe` + health) preserved, the `duplex` platform escape kept only if no typed - Deno equivalent exists; scoped check/lint/test green and `deno publish --dry-run` Success without + upstream proxy served through the sanctioned `createPluginService` raw-route capability, base-meta + (`describe` + health) preserved, the `duplex` platform escape kept only if no typed Deno + equivalent exists; scoped check/lint/test green and `deno publish --dry-run` Success without `--allow-slow-types`. ## plugins/sagas — deferred Prisma SagaIdempotencyPort parity @@ -560,10 +559,10 @@ finding into a debt entry. - **Created:** 2026-04-29 - **Status:** RESOLVED 2026-07-06 (superseded) — the top-level `packages/workers` package named in this heading no longer exists; it was superseded by `packages/plugin-workers-core` (plus the - `plugins/workers` connector) during the plugin re-architecture. The 1,287 LOC monolith is gone: the - successor `packages/plugin-workers-core/src/abstracts/task-executor.ts` is a 26 LOC abstract, so the - supervisor/executor/dispatcher split concern is fully addressed by the successor rather than - relocated. Heading retained as the historical record. + `plugins/workers` connector) during the plugin re-architecture. The 1,287 LOC monolith is gone: + the successor `packages/plugin-workers-core/src/abstracts/task-executor.ts` is a 26 LOC abstract, + so the supervisor/executor/dispatcher split concern is fully addressed by the successor rather + than relocated. Heading retained as the historical record. - **Gate:** F-1, F-3, F-13 ## packages/sagas — AP-1 / doctrine verdict Refactor (list-transport.ts 847 LOC) @@ -835,11 +834,11 @@ finding into a debt entry. - **Linked plan:** `.llm/tmp/run/doc-harness-doctrine-refactor--harness-v2-plan/plan.md` - **Created:** 2026-04-29 - **Status:** RESOLVED 2026-07-06 (superseded) — the `packages/shared` package named in this heading - no longer exists; the whole package was removed during the re-architecture. `utils/datetime.ts` was - already deleted in Wave 0, and with the package itself gone the "shrink `shared` to cross-package - identifiers" concern and the residual `@shared/utils` compatibility tracking are both moot. Heading - retained as the historical record. (Prior note, retained: partially closed 2026-06-05 — datetime - helper deleted; published surface free of generic datetime helpers.) + no longer exists; the whole package was removed during the re-architecture. `utils/datetime.ts` + was already deleted in Wave 0, and with the package itself gone the "shrink `shared` to + cross-package identifiers" concern and the residual `@shared/utils` compatibility tracking are + both moot. Heading retained as the historical record. (Prior note, retained: partially closed + 2026-06-05 — datetime helper deleted; published surface free of generic datetime helpers.) - **Gate:** F-1 and F-2 closed for datetime; residual F-11 concern moot now that `packages/shared` is deleted @@ -1307,8 +1306,8 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe alpha-1 train. - **Created:** 2026-06-22. - **Status:** open, DEBT_ACCEPTED. -- **Gate:** Close when `deno task publish:dry-run` passes for the alpha-1 train after PR1 merges, and - scaffold output no longer emits forward-looking stable ranges. +- **Gate:** Close when `deno task publish:dry-run` passes for the alpha-1 train after PR1 merges, + and scaffold output no longer emits forward-looking stable ranges. ## plugins/auth — single active backend v1 boundary (`auth-single-active-backend-boundary`) @@ -1379,13 +1378,14 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe ## packages/cli — bare-metal deploy targets ship without rollback/secrets bodies (`cli-deploy-target-rollback-secrets-deferred`) - **Reason:** The canonical `DeployTargetPort` contract declares seven operations; the bare-metal - service targets (`WindowsServiceDeployTarget`, `LinuxServiceDeployTarget`) implement six - (`plan` / `emit` / `up` / `down` / `status` / `logs`) and leave `rollback?` / `secrets?` + service targets (`WindowsServiceDeployTarget`, `LinuxServiceDeployTarget`) implement six (`plan` / + `emit` / `up` / `down` / `status` / `logs`) and leave `rollback?` / `secrets?` declared-unsupported (omitted from the shared `ServiceDeployTarget` base per LD-4). The kernel-domain `DeployTargetRegistry` descriptors are also not yet wired to inject the public `OsServicePort` + compile pipeline (a kernel→public import would violate hexagonal layering — see worklog drift D-S8); the live deploy path runs `deploy-group.ts` → `install-service-deploy` / - `buildWindowsDeployment` directly, so the descriptors are scaffolding consumed only by tests today. + `buildWindowsDeployment` directly, so the descriptors are scaffolding consumed only by tests + today. - **Owner:** Deployment-hardening follow-up (#341). - **Created:** 2026-07-03 (#339/#340). - **Update (2026-07-04, #341 / PR #374):** the operation bodies + delegation seam landed. Deploy-S5 @@ -1393,8 +1393,8 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe `LinuxServiceDeployTarget` now advertise `rollback`/`secrets` and delegate to the shipped deploy-core primitives (`secrets-convention`/`rollback-convention`/`health-gate`/ `activation-convention`), with `up` health-gated, when the core ports are injected. The one - residual — composing the registry descriptors onto the public `OsServicePort` + compile pipeline so - `deploy up|rollback|secrets` executes end-to-end off the descriptor path — is a + residual — composing the registry descriptors onto the public `OsServicePort` + compile pipeline + so `deploy up|rollback|secrets` executes end-to-end off the descriptor path — is a kernel→public layering boundary and is now tracked distinctly by `DEPLOY-BAREMETAL-PUBLIC-WIRING`. - **Status:** RESOLVED 2026-07-04 (superseded) — the `rollback`/`secrets` (+ health/OTEL) bodies and the injected execution-delegation seam landed in #341/#374; superseded by @@ -1408,7 +1408,8 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **Reason:** The systemd adapter, unit renderer, and OS-routing are covered by unit + OS-routing e2e-lite tests, but the implementation host is Windows-only, so the end-to-end Linux systemd install/start/status path is not exercised against a live `systemctl`/`journalctl`. The - merge-readiness `scaffold.runtime` gate likewise runs on Windows and does not touch the Linux lane. + merge-readiness `scaffold.runtime` gate likewise runs on Windows and does not touch the Linux + lane. - **Owner:** Deployment CI follow-up. - **Created:** 2026-07-03 (#339/#340). - **Status:** open, DEBT_ACCEPTED. @@ -1590,8 +1591,8 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **Why it is debt:** The repo still has multiple cron-related surfaces (`@netscript/cron`, workers `.schedule()`, trigger scheduler adapters, and this preview helper). Follow-up should either upstream the preview semantics into the canonical cron primitive or replace this helper with an - equivalent shared engine once the cron subsystem decision is made, without regressing the #181 - DST table. + equivalent shared engine once the cron subsystem decision is made, without regressing the #181 DST + table. - **Owner:** `@netscript/plugin-triggers-core` runtime + cron subsystem maintainers. - **Target:** Cron subsystem unification follow-up after #181. - **Linked plan:** `.llm/tmp/run/feat-triggers-feature-backing--181/plan.md` Slice 5 / L6. @@ -1700,10 +1701,9 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe island into generated projects with no companion lockfile (`package-lock.json` / `deno.lock`). Consumers must run a manual `npm install` in `aspire/` before the AppHost resolves, and the install is unpinned. This is a pre-existing generated-project DX gap, unrelated to the Deno 2.9 - toolchain adoption — Deno 2.9's lock/install changes do not seed a lock for an emitted npm - package that has no foreign lock to import. The Deno 2.9 adoption plan (D5) decided this gap - out-of-scope; this entry makes that "pre-existing arch-debt" citation verifiable per PLAN-EVAL - finding F-3. + toolchain adoption — Deno 2.9's lock/install changes do not seed a lock for an emitted npm package + that has no foreign lock to import. The Deno 2.9 adoption plan (D5) decided this gap out-of-scope; + this entry makes that "pre-existing arch-debt" citation verifiable per PLAN-EVAL finding F-3. - **Owner:** CLI scaffold / Aspire AppHost generator maintainers. - **Target:** Before advertising the generated TS AppHost as zero-manual-step; revisit alongside the generated-project task DX follow-up (Deno 2.9 plan C6). @@ -1727,9 +1727,9 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe backing" message that oRPC maps to a server error (`subscribeEvents` is an async generator that throws immediately and never yields, so its SSE output schema is never violated). No backing was fabricated and no triggers-core capability was invented in this slice. -- **Why it is debt:** the deferred routes have no sound runtime seam in `@netscript/plugin-triggers-core` - yet, and two backed routes synthesize/omit fields because the domain lacks them. The net-new - triggers-core surface required to fully back the contract: +- **Why it is debt:** the deferred routes have no sound runtime seam in + `@netscript/plugin-triggers-core` yet, and two backed routes synthesize/omit fields because the + domain lacks them. The net-new triggers-core surface required to fully back the contract: 1. **Manual / test-fire helper** — a runtime entrypoint to dispatch a trigger on demand (backs `fireTrigger`) and a webhook test-delivery helper (backs `testWebhook`). 2. **Persistent enabled-state store** — there is no enabled/disabled store, so `listTriggers`/ @@ -1767,27 +1767,26 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **ID:** `DEPLOY-S7-APPHOST-COMPOSE-GEN` - **Title:** Aspire AppHost compose-publishing generation (compose environment + Deno - `denoland/deno:2` Dockerfile weave + config→`Parameters__*`) is deferred as a shared, - cross-slice primitive. -- **Context:** Deploy-S7 (#343) shipped the config member (S1), the Aspire compose/docker - adapter (S5, `aspire-compose-deploy-target.ts`), and the thin `deploy docker|compose` router - (S6). The adapter delegates `plan`→`aspire publish`. For `aspire publish` to actually emit a - docker-compose artifact, the generated `aspire/apphost.mts` must (a) declare the official - `Aspire.Hosting.Docker` NuGet in `aspire.config.json` and (b) call - `await builder.addDockerComposeEnvironment("compose")`. Aspire then auto-publishes all - resources as compose services. **But** NetScript registers Deno resources as - `builder.addExecutable('deno', …)` (see `generate-aspire-config.ts` L44-56); executables are - not containers, so publishing them to compose requires a `denoland/deno:2` + `denoland/deno:2` Dockerfile weave + config→`Parameters__*`) is deferred as a shared, cross-slice + primitive. +- **Context:** Deploy-S7 (#343) shipped the config member (S1), the Aspire compose/docker adapter + (S5, `aspire-compose-deploy-target.ts`), and the thin `deploy docker|compose` router (S6). The + adapter delegates `plan`→`aspire publish`. For `aspire publish` to actually emit a docker-compose + artifact, the generated `aspire/apphost.mts` must (a) declare the official `Aspire.Hosting.Docker` + NuGet in `aspire.config.json` and (b) call `await builder.addDockerComposeEnvironment("compose")`. + Aspire then auto-publishes all resources as compose services. **But** NetScript registers Deno + resources as `builder.addExecutable('deno', …)` (see `generate-aspire-config.ts` L44-56); + executables are not containers, so publishing them to compose requires a `denoland/deno:2` `addDockerfileBuilder` / container-resource weave across the shared `register-services` / `register-apps` / `register-background` generators, plus config→`Parameters__*` mapping. - **Why deferred:** (1) Runtime correctness — whether `aspire publish` emits a valid compose for - NetScript's Deno executable resources — cannot be validated without the Aspire .NET SDK + a - Docker daemon (`aspire restore`→`aspire publish`→`docker compose config`), i.e. the S8/S9 - merge-readiness environment; a snapshot test would lock the string but not prove Aspire - accepts it. (2) The per-resource compose-publishing generation is a SHARED publishing - convention (R-DEPLOY-3 / core-centralization law), and the concurrent #342 Deno Deploy adapter - needs the same apphost-publishing surface — forking it per-target on one branch would violate - the centralization law and risk a merge collision with the live deployment-epic agents. + NetScript's Deno executable resources — cannot be validated without the Aspire .NET SDK + a Docker + daemon (`aspire restore`→`aspire publish`→`docker compose config`), i.e. the S8/S9 merge-readiness + environment; a snapshot test would lock the string but not prove Aspire accepts it. (2) The + per-resource compose-publishing generation is a SHARED publishing convention (R-DEPLOY-3 / + core-centralization law), and the concurrent #342 Deno Deploy adapter needs the same + apphost-publishing surface — forking it per-target on one branch would violate the centralization + law and risk a merge collision with the live deployment-epic agents. - **Owner:** NetScript deployment epic (#327) coordinator; implement once as a shared apphost-publishing primitive in coordination with #342. - **Target:** Deployment-epic merge-readiness pass, before the Docker/Compose target is declared @@ -1797,8 +1796,8 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe `addDockerComposeEnvironment`, `publishAsDockerComposeService`, `addParameterFromConfiguration`, `addDockerfileBuilder`). - **Created:** 2026-07-03. -- **Status:** open — S5 adapter + S6 router shipped and work unchanged the moment the apphost - gains the compose environment; the generation is cleanly separable. +- **Status:** open — S5 adapter + S6 router shipped and work unchanged the moment the apphost gains + the compose environment; the generation is cleanly separable. - **Gate:** Close when scaffolded `apphost.mts` emits the compose environment + Deno `denoland/deno:2` service, `deno task e2e:cli run scaffold.runtime` stays green, and `netscript deploy compose plan` produces a `docker-compose.yaml` that passes @@ -1809,17 +1808,18 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **ID:** `DEPLOY-SECRETS-ROLLBACK-CORE` - **Title:** Centralized `secrets` + `rollback` deploy-convention primitives are not yet on main; adapters declare them unsupported until the core lands. -- **Context:** The 7-op `DeployTargetPort` (landed by S0/#370) includes `rollback` and `secrets` - as optional ops. Doctrine R-DEPLOY-3 requires both to be centralized in the deploy core - (shared, restricted-perm secret env-file backed by Aspire `Parameters__*`; rollback = redeploy - the last-good emitted artifact/image), never re-implemented per adapter. Those core primitives - do not exist on main yet — they are the #341 deployment-hardening work (PR #364). Per the port - doc comment, adapters may omit the two methods (declare unsupported) rather than provide silent +- **Context:** The 7-op `DeployTargetPort` (landed by S0/#370) includes `rollback` and `secrets` as + optional ops. Doctrine R-DEPLOY-3 requires both to be centralized in the deploy core (shared, + restricted-perm secret env-file backed by Aspire `Parameters__*`; rollback = redeploy the + last-good emitted artifact/image), never re-implemented per adapter. Those core primitives do not + exist on main yet — they are the #341 deployment-hardening work (PR #364). Per the port doc + comment, adapters may omit the two methods (declare unsupported) rather than provide silent no-ops. - **Why deferred:** Cross-slice dependency — forking secrets/rollback inside the Aspire adapter would violate R-DEPLOY-3. The Deploy-S7 adapter therefore ships the supported subset (`plan`/`emit`/`up`/`down`/`status`/`logs`) and omits `rollback`/`secrets`. -- **Owner:** #341 deployment-hardening core (deploy core in `packages/cli/src/kernel/domain/deploy`). +- **Owner:** #341 deployment-hardening core (deploy core in + `packages/cli/src/kernel/domain/deploy`). - **Target:** When #341/#364 lands the shared primitives; adapters then advertise the ops and call the core. - **Linked plan:** `.llm/tmp/run/deploy-s7-aspire/plan.md` (S3); drift `D2`. @@ -1838,12 +1838,13 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe delegating to them. - **Gate:** Close when the Aspire compose/docker adapter advertises both ops by delegating to the now-shipped deploy-core `secrets`/`rollback` primitives. -- **Gate:** Close each route as its triggers-core seam lands (manual/test-fire helper -> `fireTrigger` - + `testWebhook`; enabled-state store + `enabled`/`name` fields -> `enableTrigger`/`disableTrigger` - + un-synthesized `listTriggers`/`getTrigger`; cron preview engine -> `previewSchedule`; event SSE - seam -> `subscribeEvents`), each with its own contract-route assertion and the connector smoke - test extended to assert the now-backed behavior. Remove the webhook brand cast when a public brand - constructor lands. +- **Gate:** Close each route as its triggers-core seam lands (manual/test-fire helper -> + `fireTrigger` + - `testWebhook`; enabled-state store + `enabled`/`name` fields -> `enableTrigger`/`disableTrigger` + - un-synthesized `listTriggers`/`getTrigger`; cron preview engine -> `previewSchedule`; event SSE + seam -> `subscribeEvents`), each with its own contract-route assertion and the connector smoke + test extended to assert the now-backed behavior. Remove the webhook brand cast when a public + brand constructor lands. - **Closing evidence:** #181 per-slice gates passed with scoped core/connector check/lint/fmt, focused contract/connector tests, `deno publish --dry-run --allow-dirty` for `@netscript/plugin-triggers-core` and `@netscript/plugin-triggers`, and `deno task arch:check` @@ -1872,14 +1873,15 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **Reason:** Two coupled defects in three connectors that still ship runtime stores/adapters under `plugins//src/runtime/`. (1) **Placement:** by the #157→#172 thin-connector law these - port→backend adapters belong in `@netscript/plugin--core` (`adapters/`/`stores/`), leaving the - connector with specifics only. (2) **Primitive-bypass / engine lock:** the **sagas** and **triggers** - KV stores hardwire raw `Deno.Kv`/`Deno.openKv` + the Deno-native fluent atomic, bypassing the - engine-agnostic `@netscript/kv` primitive — locking them to Deno KV and forfeiting Redis / in-memory / - kvdex / reactive-watch. The **workers** `KvWorkerIdempotencyStore` already does it right (depends on - `@netscript/kv` types + a `KvStore`-shaped structural port) and is the reference. The PASSed - `feat/scaffold-surface-167` plan covered the scaffold-surface contract (S1–S7), not runtime-store - placement or engine choice — so this is net-new scope, PLAN-EVAL-gated separately. + port→backend adapters belong in `@netscript/plugin--core` (`adapters/`/`stores/`), leaving + the connector with specifics only. (2) **Primitive-bypass / engine lock:** the **sagas** and + **triggers** KV stores hardwire raw `Deno.Kv`/`Deno.openKv` + the Deno-native fluent atomic, + bypassing the engine-agnostic `@netscript/kv` primitive — locking them to Deno KV and forfeiting + Redis / in-memory / kvdex / reactive-watch. The **workers** `KvWorkerIdempotencyStore` already + does it right (depends on `@netscript/kv` types + a `KvStore`-shaped structural port) and is the + reference. The PASSed `feat/scaffold-surface-167` plan covered the scaffold-surface contract + (S1–S7), not runtime-store placement or engine choice — so this is net-new scope, PLAN-EVAL-gated + separately. - **Relocation + migration set (grounded against the worktree):** - **sagas (#172b):** `prisma-saga-store.ts` (dep-free structural Prisma delegate, relocate as-is), `kv-saga-store.ts`, `kv-saga-runtime-stores.ts` (**relocate + migrate `Deno.openKv` → @@ -1887,36 +1889,44 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe `@netscript/kv` dep — desired.** - **triggers (#172c):** `kv-trigger-runtime-stores.ts` (**relocate + migrate `Deno.openKv` → `@netscript/kv`**) + `cron-trigger-scheduler-adapter.ts` / `watchers-file-watcher-adapter.ts` → - `packages/plugin-triggers-core/src/{stores,adapters}/`. Adds **`@netscript/kv` + `@netscript/cron` - + `@netscript/watchers`** to triggers-core (currently `@std/assert`+`zod` only). + `packages/plugin-triggers-core/src/{stores,adapters}/`. Adds **`@netscript/kv` + + `@netscript/cron` + - `@netscript/watchers`** to triggers-core (currently `@std/assert`+`zod` only). - **workers (#172d):** `worker/worker-idempotency-store.ts` (already on `@netscript/kv`, relocate only) → `packages/plugin-workers-core/src/{stores,adapters}/`. Adds the `@netscript/kv` dep to workers-core. -- **Why it is debt (open):** authored but PLAN-EVAL-gated and not yet implemented. Decisions deferred to - PLAN-EVAL: (D-KV) migrate sagas+triggers KV stores onto `@netscript/kv` (`KvStore` + `AtomicCheck/ - Mutation/Result`, injected handle, workers structural-port pattern), preserving optimistic-concurrency - + idempotency semantics; (D2) the zero-compat public-surface break — stores move from - `@netscript/plugin-/runtime` to `@netscript/plugin--core/{stores,adapters}` with no shim. - **Note:** an earlier draft framed a "D1" decision over whether `-core` may take `@netscript/*` deps; - that was removed — `-core` depending on NetScript primitives is the **encouraged** direction (the - reference behavior for community plugin/plugin-core authors), not a tradeoff to weigh. +- **Why it is debt (open):** authored but PLAN-EVAL-gated and not yet implemented. Decisions + deferred to PLAN-EVAL: (D-KV) migrate sagas+triggers KV stores onto `@netscript/kv` (`KvStore` + + `AtomicCheck/ + Mutation/Result`, injected handle, workers structural-port pattern), preserving + optimistic-concurrency + - idempotency semantics; (D2) the zero-compat public-surface break — stores move from + `@netscript/plugin-/runtime` to `@netscript/plugin--core/{stores,adapters}` with no + shim. **Note:** an earlier draft framed a "D1" decision over whether `-core` may take + `@netscript/*` deps; that was removed — `-core` depending on NetScript primitives is the + **encouraged** direction (the reference behavior for community plugin/plugin-core authors), not + a tradeoff to weigh. - **Owner:** Plugin connectors + `@netscript/plugin--core` (framework architecture). - **Target:** PLAN-EVAL-gated relocation slices under #172; close when all three merge. -- **Linked plan:** `.llm/tmp/run/feat-scaffold-surface-167--adapter-relocation/plan.md` (+ `research.md`). +- **Linked plan:** `.llm/tmp/run/feat-scaffold-surface-167--adapter-relocation/plan.md` (+ + `research.md`). - **Created:** 2026-06-30. -- **Status:** open, PLAN-EVAL pending — research + plan authored; no implementation slice before PASS. -- **Gate:** per touched package scoped check/lint/fmt (`--ext ts,tsx`) + targeted `deno test - --unstable-kv` + `deno publish --dry-run --allow-dirty` (no new slow types) + `deno task arch:check` - (no connector→core leak); new workspace deps land via normal resolution, no `deno.lock` hand-edit. +- **Status:** open, PLAN-EVAL pending — research + plan authored; no implementation slice before + PASS. +- **Gate:** per touched package scoped check/lint/fmt (`--ext ts,tsx`) + targeted + `deno test + --unstable-kv` + `deno publish --dry-run --allow-dirty` (no new slow types) + + `deno task arch:check` (no connector→core leak); new workspace deps land via normal resolution, no + `deno.lock` hand-edit. ## packages/plugin-auth-core — AUTH-FITNESS-GATE-OVERFLAG - **ID:** `AUTH-FITNESS-GATE-OVERFLAG` -- **Reason:** `AS7/F-AUTH-CAST` over-flagged two sanctioned auth type-soundness sites: the centralized - oRPC error-map contract cast in `src/contracts/v1/auth.contract.ts`, and test-only - `@ts-expect-error` / `as unknown` guards in `tests/contracts/auth-contract-soundness_test.ts`. - The production auth code was already type-sound; the fitness gate was stricter than the AS7 intent - and stricter than the equivalent sagas/workers contract soundness test treatment. +- **Reason:** `AS7/F-AUTH-CAST` over-flagged two sanctioned auth type-soundness sites: the + centralized oRPC error-map contract cast in `src/contracts/v1/auth.contract.ts`, and test-only + `@ts-expect-error` / `as unknown` guards in `tests/contracts/auth-contract-soundness_test.ts`. The + production auth code was already type-sound; the fitness gate was stricter than the AS7 intent and + stricter than the equivalent sagas/workers contract soundness test treatment. - **Fix:** Narrowed `.llm/tools/fitness/check-doctrine.ts` so the auth scanner recognizes the exact centralized `Parameters[0]` contract cast, keeps the router `any` exemplar as the only router exception, and exempts test paths (`tests/`, `_test.ts`, `.test.ts`) from the auth @@ -1932,10 +1942,10 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe ## packages/cli — PLUGIN-LIST-MANIFEST-REGISTRATION-BLOCKER - **ID:** `PLUGIN-LIST-MANIFEST-REGISTRATION-BLOCKER` -- **Reason:** After the centralized scaffold-CLI bridge restored execution of all five official plugin - scaffolders, `scaffold.runtime` advanced past every `scaffold.plugin.*` install gate and failed at - `scaffold.plugin-list`. The generated project contains the plugin sample files under `workers/`, - `sagas/`, `triggers/`, `streams/`, and `auth/`, but `netscript plugin list` reads +- **Reason:** After the centralized scaffold-CLI bridge restored execution of all five official + plugin scaffolders, `scaffold.runtime` advanced past every `scaffold.plugin.*` install gate and + failed at `scaffold.plugin-list`. The generated project contains the plugin sample files under + `workers/`, `sagas/`, `triggers/`, `streams/`, and `auth/`, but `netscript plugin list` reads `plugins//scaffold.plugin.json`; the install path does not materialize that manifest under the generated `plugins/` registry tree. - **Why deferred:** This is a distinct host install/list registration defect, not the S-f scaffold @@ -1956,27 +1966,26 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe ## plugin packages — PLUGIN-RUNTIME-DEPENDENCY-ENTRYPOINT-EXPORTS - **ID:** `PLUGIN-RUNTIME-DEPENDENCY-ENTRYPOINT-EXPORTS` -- **Reason:** The #157 thin-dependency model forbids copying official plugin internals into generated - projects. After S-g reconciled config registration and `plugin list`, the full runtime E2E fails at - `runtime.wait.workers-api` because appsettings runtime resources still point at copied-package - workdirs such as `plugins/workers`, which are intentionally absent. Moving runtime resources to - package-spec execution requires an explicit public plugin executable-entrypoint contract for both - service and background processes. Service packages already expose `./services`; background - executables such as `@netscript/plugin-workers/bin/combined.ts` are not exported today. +- **Reason:** The #157 thin-dependency model forbids copying official plugin internals into + generated projects. After S-g reconciled config registration and `plugin list`, the full runtime + E2E fails at `runtime.wait.workers-api` because appsettings runtime resources still point at + copied-package workdirs such as `plugins/workers`, which are intentionally absent. Moving runtime + resources to package-spec execution requires an explicit public plugin executable-entrypoint + contract for both service and background processes. Service packages already expose `./services`; + background executables such as `@netscript/plugin-workers/bin/combined.ts` are not exported today. - **Why deferred:** Adding exported runtime executable subpaths is a new public package-surface contract, not an install/register/list reconciliation. S-g's escape hatch required stopping rather than folding that contract into this slice. - **Owner:** CLI plugin runtime / official plugin package maintainers. - **Linked evidence:** `.llm/tmp/run/feat-scaffold-surface-167--adapter-relocation/worklog.md` S-g. -- **Status:** closed by PR #172 runtime-launch finalization - (`8aaddbc1`, `4a991d16`). +- **Status:** closed by PR #172 runtime-launch finalization (`8aaddbc1`, `4a991d16`). - **Gate:** Close when official plugin packages expose supported executable entrypoints for the thin-dependency runtime and `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` reaches `failed=0` without copying plugin internals into generated user projects. -- **Closing evidence:** `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` - exited 0 on 2026-06-30 with `Summary: passed=48 failed=0`. The run used package-launched - service/background plugin resources, passed every service/background wait gate, accepted the - generic trigger webhook, listed trigger events, and validated the cross-service OTEL trace. +- **Closing evidence:** `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` exited 0 + on 2026-06-30 with `Summary: passed=48 failed=0`. The run used package-launched service/background + plugin resources, passed every service/background wait gate, accepted the generic trigger webhook, + listed trigger events, and validated the cross-service OTEL trace. ## packages/cli — DB-GENERATE-ASPIRE-COUPLING @@ -1993,13 +2002,14 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe in 243ms while passing on aspire-equipped boxes and in `scaffold-runtime`. The scaffolded project already ships a standalone Aspire-less path — the db-workspace `deno task db:generate` (`prisma generate --generator client` + `scripts/generate-zod.ts` + `scripts/fix-zod-imports.ts` - + `patch-prisma-client.ts`), which C14 wired into the static suites via the new `database.codegen` - gate. That harness workaround unblocks CI but does not remove the coupling from the CLI command. + - `patch-prisma-client.ts`), which C14 wired into the static suites via the new `database.codegen` + gate. That harness workaround unblocks CI but does not remove the coupling from the CLI command. - **Why deferred:** Decoupling `db generate` from Aspire is a cross-package refactor of the CLI database kernel (`DbOperationRunner` / operation classification): pure-codegen operations must run their prisma/zod pipeline directly (as the scaffolded `db:generate` task already does) without booting the AppHost, while connect-requiring operations (`init`/`migrate`/`seed`) keep the Aspire - path. That is a distinct DX slice from the #153 scaffold-surface work and out of scope for PR #208. + path. That is a distinct DX slice from the #153 scaffold-surface work and out of scope for PR + #208. - **Owner:** CLI database kernel maintainers. - **Target:** Follow-up DX slice (aspire-less `db generate`). - **Linked evidence:** `.llm/tmp/run/feat-scaffold-crud-surface--impl/drift.md` (2026-07-01 @@ -2048,16 +2058,16 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **Linked plan:** `.llm/tmp/run/deploy-s2-doctrine/plan.md` (Slice 3); `docs/architecture/doctrine/06-archetypes.md#archetype-7--deployment-target-adapter`. - **Created:** 2026-07-03 -- **Update (2026-07-04, #341 / PR #374):** the core-centralization obligation is materially advanced. - The convention-bearing primitives (health gating, OTEL, secrets, rollback) now live in the deploy - core `packages/cli/src/kernel/domain/deploy/` as pure, target-agnostic modules with injected ports - (`secrets-convention.ts`, `rollback-convention.ts`, `health-gate.ts`, `activation-convention.ts`, - `observability-convention.ts`), and the bare-metal reference binding proves them against systemd + - SERVY (see `DEPLOY-SECRETS-ROLLBACK-CORE`). The `windows-service`/`linux-service` - `ServiceDeployTarget` is now a genuine 7-op adapter (rollback/secrets advertised + delegated when - the core ports are injected). Still open for full closure: the primitives live in `packages/cli` - (not a standalone deployment core package), the target registry is CLI-local, and - `F-DEPLOY-1`/`F-DEPLOY-2` remain `reviewed` (not `gated`). +- **Update (2026-07-04, #341 / PR #374):** the core-centralization obligation is materially + advanced. The convention-bearing primitives (health gating, OTEL, secrets, rollback) now live in + the deploy core `packages/cli/src/kernel/domain/deploy/` as pure, target-agnostic modules with + injected ports (`secrets-convention.ts`, `rollback-convention.ts`, `health-gate.ts`, + `activation-convention.ts`, `observability-convention.ts`), and the bare-metal reference binding + proves them against systemd + SERVY (see `DEPLOY-SECRETS-ROLLBACK-CORE`). The + `windows-service`/`linux-service` `ServiceDeployTarget` is now a genuine 7-op adapter + (rollback/secrets advertised + delegated when the core ports are injected). Still open for full + closure: the primitives live in `packages/cli` (not a standalone deployment core package), the + target registry is CLI-local, and `F-DEPLOY-1`/`F-DEPLOY-2` remain `reviewed` (not `gated`). - **Status:** open, DEBT_ACCEPTED — convention primitives centralized (#341/#374); `F-DEPLOY-1/2` remain `reviewed` pending a standalone deployment core package + full adapter matrix. - **Gate:** Close when the deployment core owns the centralized health/OTEL/secrets/rollback @@ -2078,12 +2088,12 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe only the 6-op subset. The production composition that constructs a fully-wired target — resolving the release candidate + probe spec + secret bundle and injecting the real `OsServicePort`-backed activation store on the public deploy path — is intentionally not built here. -- **Why deferred:** The activation adapters depend on the public `OsServicePort` seam, so the - wiring is a public-layer composition concern (`public/adapters/**` + the deploy command - dependencies), out of the kernel-domain S5/S6 scope (hexagonal boundary; see run drift `D-S8`). - Wiring it also needs a release-id/candidate resolution + secret-source pipeline that has no - first-party caller yet. The delegation seam is fully unit-proven with fake ports; only the - composition root is missing. +- **Why deferred:** The activation adapters depend on the public `OsServicePort` seam, so the wiring + is a public-layer composition concern (`public/adapters/**` + the deploy command dependencies), + out of the kernel-domain S5/S6 scope (hexagonal boundary; see run drift `D-S8`). Wiring it also + needs a release-id/candidate resolution + secret-source pipeline that has no first-party caller + yet. The delegation seam is fully unit-proven with fake ports; only the composition root is + missing. - **Owner:** Deployment epic #327 — public deploy composition slice (systemd/#339, servy). - **Target:** When the public deploy path constructs `ServiceDeployTarget` with the real bare-metal ports so `deploy up|rollback|secrets` executes end-to-end. @@ -2123,11 +2133,11 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **Reason:** The doctrine-06 "Archetype 5 — Plugin Package" Minimum shape nests contribution folders (`services/`, `database/`, `jobs/`, `streams/`, `verify-plugin.ts`) under `src/` and names sibling contract packages `@netscript/sagas` / `@netscript/workers`. Neither matches reality: real - first-party `plugins/*` place those contribution folders at the **top level** as siblings of `src/` - (with top-level `contracts.ts`/`mod.ts`/`verify-plugin.ts`), and the sibling contract packages were - renamed to `@netscript/plugin-*-core` during the plugin re-architecture. The harness archetype-5 - profile already treats the observed `plugins/*` layout as authoritative until this chapter is - reconciled. + first-party `plugins/*` place those contribution folders at the **top level** as siblings of + `src/` (with top-level `contracts.ts`/`mod.ts`/`verify-plugin.ts`), and the sibling contract + packages were renamed to `@netscript/plugin-*-core` during the plugin re-architecture. The harness + archetype-5 profile already treats the observed `plugins/*` layout as authoritative until this + chapter is reconciled. - **Why deferred:** Reconciling the doctrine chapter is a rewrite that must land coherently with the plugin-v2 folder conventions (AI-stack #238 / plugin re-architecture sequencing), not a drive-by edit inside a chore run. The #306 harness/skills revamp is doc-and-spec scope; a doctrine @@ -2136,7 +2146,8 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe - **Target:** When plugin-v2 folder conventions are frozen; reconcile the doctrine-06 Minimum shape (top-level contribution folders + `@netscript/plugin-*-core` sibling names) and drop the "authoritative until reconciled" note from `ARCHETYPE-5-plugin.md`. -- **Linked plan:** `.llm/harness/archetypes/ARCHETYPE-5-plugin.md` § Minimum Folder Shape; issue #306. +- **Linked plan:** `.llm/harness/archetypes/ARCHETYPE-5-plugin.md` § Minimum Folder Shape; issue + #306. - **Created:** 2026-07-06 - **Status:** open, DEBT_ACCEPTED (assessed and deferred during the #306 remainder run). - **Gate:** Close when doctrine-06 Archetype 5 Minimum shape matches the observed authoritative @@ -2160,25 +2171,26 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe ## documentation lane authored framework source (`docs-lane-framework-source-1020`) - **Reason:** PR #1079 (documentation-sequencing slice, `docs/1068-task-routing`) landed framework - source on the documentation lane. Commit `fb5d7a0de` added `plugins/streams/services/src/durability.ts` - (new `describeStorageDurability` module, 29 lines), `durability_test.ts` (38 lines), a startup - warning wired into `plugins/streams/services/src/main.ts`, and a `plugins/streams/deno.json` task - change — plus JSDoc in `packages/fresh`, `packages/plugin-workers-core` and `packages/sdk`. - `CLAUDE.md`'s documentation-authoring exception is explicit that authoring touches **no - `packages/`/`plugins/` source code**, and that any framework-source change stays a WSL Codex - daemon-attached slice. This crossed that boundary. + source on the documentation lane. Commit `fb5d7a0de` added + `plugins/streams/services/src/durability.ts` (new `describeStorageDurability` module, 29 lines), + `durability_test.ts` (38 lines), a startup warning wired into + `plugins/streams/services/src/main.ts`, and a `plugins/streams/deno.json` task change — plus JSDoc + in `packages/fresh`, `packages/plugin-workers-core` and `packages/sdk`. `CLAUDE.md`'s + documentation-authoring exception is explicit that authoring touches **no `packages/`/`plugins/` + source code**, and that any framework-source change stays a WSL Codex daemon-attached slice. This + crossed that boundary. - **Root cause is upstream of the slice:** issue #1020 is labelled `type:docs`, but its acceptance ("the non-durable in-memory default is unsurfaced" → a startup warning) **requires code**. A - `type:docs` issue whose acceptance cannot be met by prose should not be routed to the documentation - lane. The slice followed its brief; the routing decision was wrong, and the orchestrator's - pre-merge review checked the router change (#1068) without auditing the changed-file list for - framework paths. + `type:docs` issue whose acceptance cannot be met by prose should not be routed to the + documentation lane. The slice followed its brief; the routing decision was wrong, and the + orchestrator's pre-merge review checked the router change (#1068) without auditing the + changed-file list for framework paths. - **Owner:** 0.0.4 release orchestrator (routing + merge review). - **Decision:** **DEBT_ACCEPTED, not reverted.** The change is small, additive, unit-tested, and was - required by the issue's own acceptance; it passed the full CI surface including `scaffold-runtime`, - `quality:scan` (byte-identical to the pre-change baseline) and `arch:check` (FAIL=0). Reverting - green, tested, required work to satisfy a lane boundary would cost more than it protects. The - deviation is recorded rather than buried. + required by the issue's own acceptance; it passed the full CI surface including + `scaffold-runtime`, `quality:scan` (byte-identical to the pre-change baseline) and `arch:check` + (FAIL=0). Reverting green, tested, required work to satisfy a lane boundary would cost more than + it protects. The deviation is recorded rather than buried. - **Created:** 2026-08-03 - **Status:** open, DEBT_ACCEPTED — **owner-reviewed 2026-08-03**: content assessed directly and judged acceptable ("mostly comments and test"). The code stands as merged. The **process** finding @@ -2188,3 +2200,47 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe review step for any documentation-lane PR audits the changed-file list for `packages/**` and `plugins/**` paths before merge. Until then, assume the docs lane can silently acquire source scope. + +## `@netscript/cli` scaffold — unreachable `Mode: 'Local'` cache arm (`cache-local-arm-unreachable-1158`) + +- **Reason:** `generate-register-infrastructure.ts:164-171` implements a `DenoKv` + `Mode: 'Local'` + arm that registers **no Aspire resource** (in-process `Deno.openKv()`), but **nothing in the + scaffold ever emits `Mode: 'Local'`**. `buildCacheBlock('deno-kv')` + (`generate-appsettings.ts:251-259`) emits `Mode: 'External'`, and plugin install's + `ensureSharedCache` writes `Mode: 'Auto'`. So the only genuinely container-free cache path in the + generator is dead code, reachable solely by hand-editing `appsettings.json`. +- **Why it matters, discovered by #1158:** the sqlite runtime tier set out to eliminate every + container. Because `Local` is unreachable, the tier had to fall back to the `Auto` arm, and when + the Docker-less Garnet **executable** sub-arm proved unreliable (drift D-14/R-3), the tier settled + for one Garnet container. A reachable `Local` arm would plausibly have delivered the original + zero-container goal. +- **Owner:** CLI scaffold / Aspire generator. +- **Target:** Revisit when the reduced-container tier is promoted, or when a consumer needs an + in-process KV cache. +- **Linked plan:** `.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan.md` (§ Arch-Debt + Implications); issue #1158; PR #1220. +- **Created:** 2026-08-04 +- **Status:** open — recorded, not fixed. #1158 deliberately did not change scaffold cache defaults. +- **Gate:** Close when either (a) a scaffold flag emits `Mode: 'Local'` and an E2E tier proves a + genuinely container-free cache, or (b) the arm is judged unwanted and removed so the generator + carries no unreachable branch. + +## `@netscript/cli` scaffold — default cache backend forces a container (`scaffold-default-cache-container-1158`) + +- **Reason:** `SCAFFOLD_DEFAULTS.CACHE_BACKEND` is `'redis'` + (`constants/scaffold/scaffold-defaults.ts:12`), and `buildCacheBlock('redis')` emits + `{ Engine: 'Redis', Mode: 'Container' }` — a hard Docker container with **no** Auto/Executable + fallback, unlike the `Garnet`/`Auto` entry plugin install adds. Every `netscript init` that does + not pass `--cache=false` therefore commits the user to Docker for the cache alone. +- **Why it matters, discovered by #1158:** this — not garnet — was the cache-side Docker cost the + research pass had to correct the carried-in plan about (research findings 3–5). The E2E works + around it per-suite with `--cache=false`; **users have no such default**. +- **Owner:** CLI scaffold defaults. +- **Target:** Consider alongside the `Local` arm above; a default change affects every scaffold, so + it needs its own decision and canary. +- **Linked plan:** `.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan.md` (§ Non-Scope, § Arch-Debt + Implications); issue #1158; PR #1220. +- **Created:** 2026-08-04 +- **Status:** open — explicitly out of scope for #1158, which must not change product defaults. +- **Gate:** Close when the default cache backend either gains a Docker-less fallback arm (as + `Garnet`/`Auto` has) or is changed with a recorded owner decision and a release canary. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/codex-thread-ids.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/codex-thread-ids.md new file mode 100644 index 0000000000..5ec5fe274b --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/codex-thread-ids.md @@ -0,0 +1,22 @@ +# ns1158-s1 — Codex implementation thread + +- **Thread / session id:** `019fcc83-4200-7421-a3db-d8eaaa9569b4` +- **Rollout:** + `/home/codex/.codex/sessions/2026/08/04/rollout-2026-08-04T13-23-02-019fcc83-4200-7421-a3db-d8eaaa9569b4.jsonl` +- **Worktree:** `/home/codex/repos/ns-1158` +- **Branch:** `test/e2e-sqlite-runtime-tier-1158` @ `8d75809d` (NO upstream by design). +- **Push rule:** explicit refspec only — + `git push origin HEAD:refs/heads/test/e2e-sqlite-runtime-tier-1158`. +- **Requested route:** provider=openai · model=gpt-5.6-sol · effort=high +- **Observed route:** provider=openai · model=gpt-5.6-sol · effort=high +- **Route verdict:** matched +- **Runtime:** approval=never · sandbox=dangerFullAccess +- **Brief (staged):** `/home/codex/ns1158-s1-brief.md` + +## Steering (same thread — never a second send-message-v2 at this worktree) + +```bash +codex exec resume 019fcc83-4200-7421-a3db-d8eaaa9569b4 -- "" +``` + +_Written by `.llm/tools/agentic/codex/launch-codex-slice.ts`._ diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/context-pack.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/context-pack.md new file mode 100644 index 0000000000..a95077b979 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/context-pack.md @@ -0,0 +1,153 @@ +# Context Pack: sqlite-backed E2E runtime tier (#1158) + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------------ | +| Run ID | `test-e2e-sqlite-runtime-tier--1158` | +| Branch | `test/e2e-sqlite-runtime-tier-1158` | +| Current phase | `implement` — S7 complete; closeout | +| Archetype | `6 - CLI / Tooling` | +| Scope overlays | `service` | + +## Current State + +**Complete.** All slices (S1–S7, plus review-driven S4a and S6a) landed and Tier-A signed off. +PLAN-EVAL `PASS`; IMPL-EVAL `PASS` on re-check after its `FAIL_DEBT` conditions were met. + +The `scaffold.runtime.sqlite` tier is green: **68 passed, 0 failed**, cleanup PASS, **net-zero +container delta**. Postgres and Redis are eliminated; one Garnet container is created and removed by +cleanup (the R-3 downgrade — the claim is "reduced containers", not "no docker", everywhere). + +**One open, non-attributable item:** the postgres merge bar (`scaffold.runtime`) fails at +`behavior.service-health`. Proven **pre-existing on `main`** by running the identical suite at +`c6f243da` in a clean worktree — same `passed=51 failed=1`, same gate, same `$queryRaw` error. This +branch neither causes it nor can fix it. Tracked as issue **#1259** (raised to `priority:p1`; the +repo's merge gate is red for everyone) and closed in `drift.md` as D-16 not-a-regression. + +## Completed + +- Skills activated: `netscript-harness`, `netscript-doctrine` (archetype + verdict), + `netscript-cli`, `netscript-pr`, `jsr-audit` (surface scan recorded in `research.md`). +- Research pass with 18 verified findings, each cited at `file:line`. +- Archetype 6 + `SCOPE-service` selected and justified; doctrine verdict recorded. +- Plan with 10 locked decisions, an open-decision sweep (3 "must resolve now", each resolved inside + its own slice), a 9-entry risk register, gate set, debt implications, and a validation plan. +- Design checkpoint: public surface, vocabulary, ports (none created, with rationale), constants, 7 + commit slices, deferred scope, contributor path. +- Branch created, run dir committed, draft PR opened. +- Rescoped S1 implemented with exact-once SQLite FFI, explicit-permission deduplication, pipeline + propagation, and byte-identical non-SQLite assertions. +- All six S1 gates passed: helper tests, scoped check/lint/fmt, `quality:scan`, and `arch:check`. +- S1 received Tier-A substantive review and sign-off at `d06e7c94`. +- S2 R-2 probe completed: `--no-cache` exit 2; `--cache=false` and `--cache false` exit 0. The + materialized no-cache config retained only the empty schema section `Cache: {}` and omitted + `PrimaryCache`. +- S2 implementation completed with 97 E2E tests passing and all scoped/fitness gates green. +- S2 received Tier-A substantive review with no findings and sign-off at `47caa6bb`. +- S3 implementation completed with precedence, database-gate filtering, and exact built-in options + regression coverage; 99 E2E tests and all five static/fitness commands passed. +- S3 received Tier-A substantive review with no findings; all six required gates were reproduced + independently. Sign-off recorded in `worklog.md`. +- S4 implementation completed with the additive suite profile, operator-first cache-mode seam, real + CLI default/override coverage, and wait/resource consistency assertions. All six required gates + passed: 104 E2E tests plus scoped check/lint/fmt, `quality:scan`, and `arch:check`. +- S4a lease correction completed with bidirectional runtime-tier contention coverage and the cheap + suite negative control. All six requested gates passed: 105 E2E tests, scoped check/lint/fmt, + `quality:scan`, and `arch:check`; `e2e:cli suites` listed both runtime tiers. +- S5 implementation completed with both Docker discovery failures tolerated, direct-stderr warning + visibility, no-container pruning, strict failed-removal preservation, and runner-level + `cleanup: true` coverage. All six requested gates passed: 110 E2E tests; 787-file scoped + check/lint/fmt scans; `quality:scan`; and `arch:check`. +- S5 received Tier-A substantive review with no findings and sign-off at `1335ab26`. +- S6 implementation completed with the full classifier matrix, failed-classifier and + `diff_unavailable` workflow assertions, lane visibility, and an explicit YAML parse. All four + requested gates passed; 54 classifier/draft-policy tests passed. +- S6a diagnostics remediation completed with all reason branches and named mutation holes pinned, + sibling-aligned report collection, explicit `@std/yaml` parsing, and all gates green: 56 tests + plus scoped check/lint/fmt with zero findings. +- S7 live runtime completed after the locked R-3 downgrade and R-4 provider-specific gate exclusion. + The selected suite passed 68/68 gates, including cleanup; all database gates, + `runtime.wait.garnet`, `runtime.wait.workers`, and the workers job/seed/trigger/execution chain + passed. R-5 therefore resolves positively. +- The complete package gate passed: 605 tests, scoped check/lint/fmt over 789 files, `quality:scan`, + `arch:check`, and suite discovery. The final container snapshot delta is empty after cleanup. + +## In Progress + +- **Implementation closeout only:** commit the S7 source/tests/artifacts, push the explicit branch + refspec, post the evidence comment to PR #1220, and stop. This lane does not review, self-certify, + dispatch a reviewer, or author a sign-off commit (D-7). + +## Next Steps + +1. External supervisor/reviewer evaluates the pushed S7 implementation and evidence; this lane does + not dispatch that work. +2. Later merge-readiness work retains the full Postgres `scaffold.runtime` regression and separate + IMPL-EVAL session. + +## Key Decisions + +| Decision | Source | Notes | +| ----------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------- | +| `D0` S1 extends SQLite `--allow-ffi` across permission-bearing resources | code — shared database-permissions helper | Services, background processors, and plugins; apps excluded. | +| `D1` additive suite id `scaffold.runtime.sqlite` | plan / owner constraint 1 | Default `scaffold.runtime` untouched. | +| `D2` reduced-container profile = sqlite + cache disabled + ambient Garnet arm | S7 / drift D-14 | Executable Garnet failed cross-process state semantics; Postgres and Redis remain eliminated. | +| `D3` boolean `RunOptions.cache`, **no** `cacheBackend` axis | code — `generate-appsettings.ts:251-259` | `deno-kv` emits `External`, not `Local`. | +| `D4` runtime waits unchanged; garnet **not** filtered | code — `runtime-gates.ts:390-405` | Same resource name in both arms. | +| `D5` per-suite `defaults` merged under caller overrides | code — `capability-suites.ts:168-192` | A suite id alone cannot pin an engine today. | +| `E5` classifier output `run_runtime_sqlite`, no new `ci:*` labels | code — `ci-classify-changes.ts:292-360` | Keeps `ci:skip-e2e` authoritative over both runtime tiers. | +| `D6` merge-readiness stays postgres | issue #1158 constraints | No change to `full-command.ts`. | +| `D8` Docker cleanup tolerant on both failure paths | code — `docker-resource-cleaner.ts:9-43` | Missing binary **and** non-zero `docker ps`. | + +## S7 Files Changed + +| Area | Notes | +| ----------------- | --------------------------------------------------------------------------------------------- | +| permissions | SQLite `--allow-all` expands to include FFI, with direct helper coverage. | +| maintainer init | `netscript-dev init` accepts and forwards `--cache`; public CLI unchanged. | +| runtime readiness | Workers scheduler and pool readiness gate plus builder coverage. | +| suite policy | Executable Garnet pin removed; sqlite excludes only `behavior.service-health`. | +| CI + artifacts | Workflow follows ambient Garnet; plan, drift, worklog, context, and leak evidence reconciled. | + +## Gates + +| Gate family | Current status | Evidence | +| ----------- | -------------- | ------------------------------------------------------------------------ | +| Static | `PASS` | 605 package tests; scoped check/lint/fmt over 789 files. | +| Fitness | `PASS` | `quality:scan` green with no findings; `arch:check` exit 0. | +| Runtime | `PASS` | `scaffold.runtime.sqlite`: 68 passed, 0 failed; cleanup passed. | +| Consumer | `PASS` | Database, workers, plugin, auth, AI, UI, and OTEL behavior gates passed. | +| Discovery | `PASS` | `deno task e2e:cli suites` lists both runtime tiers with honest titles. | + +## Open Questions + +1. Exact init spelling is resolved: both `--cache=false` and `--cache false` are accepted; + `--no-cache` is rejected. S2 uses the single-argv equals spelling, and no public-command fallback + was required; S7 separately corrected the maintainer command path under D-16. +2. R-3 is resolved negatively: executable Garnet starts but does not provide reliable cross-process + state semantics; D2 uses ambient container-backed Garnet. +3. R-4 found exactly one Postgres-shaped behavior gate: `behavior.service-health`; it is excluded + only from sqlite and retained unchanged in `scaffold.runtime`. +4. R-5 is resolved positively: plugin-add restores the Garnet primary-cache configuration before + runtime behavior; jobs, seed, trigger, and execution gates all pass. + +## Drift and Debt + +- **Drift:** D-1 supervisor lane override (minor); D-2 carried-in root cause wrong (significant); + D-3 #1191 fix is services-only, new blocker (significant); D-4 CI "no docker service" framing + (minor); D-5 apps have no permission-bearing command (significant); D-6 resumed cache-spelling + probe corrected the partial handoff evidence (minor); D-7 self-certification breach (significant); + D-8 owner-authorized supplementary verification lane (minor); D-9 adversarial-check escalation + order (minor); D-10 generic `run` defaults masked capability defaults (significant); D-11 + concurrent supervisor commit swept the S4 worktree (significant); D-12 assigns that sweep to the + supervisor (significant); D-13 records S4's omitted sqlite lease membership (significant); D-14 + records the executable-Garnet downgrade (significant); D-15 records the libSQL-incompatible + service-health gate (significant); D-16 records the maintainer-init `--cache` verification gap + (significant). All in `drift.md`. +- **Debt:** two entries to create at Close — the unreachable `Mode: 'Local'` cache arm, and + `SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'` forcing a container on every scaffold. + +## Commits + +- See the draft PR's commit list + per-slice PR comments (V3 retired `commits.md`). diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/drift.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/drift.md new file mode 100644 index 0000000000..1657797763 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/drift.md @@ -0,0 +1,511 @@ +# Drift Log: sqlite-backed E2E runtime tier (#1158) + +Drift is append-only. Record facts that diverge from the plan, RFC, doctrine, or current-state +documentation. + +## 2026-08-04 — D-1 supervisor lane is Opus 5, not the canonical Fable 5 + +- **What:** This run's supervisor (`planning_decisions`) is Claude Opus 5. +- **Source:** Owner directive at session start (Remote Control), after a GitHub Copilot cloud agent + (Grok 4.5) failed to produce anything on disk for this issue. +- **Expected:** `.llm/harness/workflow/lane-policy.md` binds `planning_decisions` to Claude · + Anthropic · Fable 5 · low. +- **Actual:** Claude · Anthropic · Opus 5, this session. +- **Severity:** minor +- **Action:** accept — recorded in `supervisor.md` § Recorded lane/eval overrides. The hard + invariants are unaffected: PLAN-EVAL/IMPL-EVAL run on the open-model evaluator lane in a separate + session, and no implementation lane self-certifies. +- **Evidence:** `supervisor.md`; `.llm/tmp/BRIEF-1158.md`. + +## 2026-08-04 — D-2 the carried-in draft misidentified the root cause + +- **What:** The carried-in proposal named "the runtime path always waits for garnet" as _the actual + blocker_, and proposed a `CACHE_BACKEND` axis plus garnet wait-filtering as the fix. +- **Source:** `.llm/tmp/BRIEF-1158.md` § "Research findings claimed" items 2 and 3; re-derived + against `main` @ `c6f243da`. +- **Expected:** garnet is an unavoidable Docker container that the sqlite tier must stop waiting + for. +- **Actual:** the `garnet` cache entry is written by plugin install (`workspace-mutator.ts:563-591`) + as `Mode: 'Auto'`, and `Auto` already resolves at apphost runtime to a Docker-less + `dotnet tool run garnet-server` executable when `docker info` fails or + `NETSCRIPT_CACHE_MODE=Executable` is set. The resource is named `garnet` in both arms, so the + existing wait gate passes without Docker. The real container-backed cache is **`redis`**, created + by `netscript init`'s default backend (`SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'`, + `Mode: 'Container'`, no fallback arm). Separately, `--cache-backend deno-kv` emits + `Mode: 'External'`, not the `Local` mode the draft assumed. +- **Severity:** significant +- **Action:** rescope the design — plan decisions D2, D3, and D4 supersede the draft's D2/D3/D4. No + cache-backend axis, no garnet filtering; instead disable init's cache and pin + `NETSCRIPT_CACHE_MODE=Executable`. +- **Evidence:** `research.md` findings 3–6; `generate-register-infrastructure.ts:164-212`; + `generate-appsettings.ts:229-261`; `scaffold-defaults.ts:12`. + +## 2026-08-04 — D-3 #1191's sqlite `--allow-ffi` fix is services-only (new blocker) + +- **What:** Only `generate-register-services.ts` adds `--allow-ffi` for a Sqlite database. + `generate-register-apps.ts`, `generate-register-background.ts`, and `generate-register-plugins.ts` + never receive `databaseEngine` and emit `resolvePermissions(...)` with no sqlite branch. +- **Source:** `grep -rn "allow-ffi" packages/cli/src/kernel/templates/aspire/helpers/register/` — + single hit; `helpers/types.ts:69` shows `databaseEngine` only on `RegisterServicesOptions`. +- **Expected:** the brief treated #1191 as having "made the sqlite runtime path viable". +- **Actual:** the sqlite runtime path is viable for the example service only. The workers, sagas, + triggers, streams, auth, and app resources exercised by `RUNTIME_GATES` would exit 1 at startup — + the same defect #1191 fixed, unfixed everywhere else. +- **Severity:** significant +- **Action:** fix — added to the plan as slice **S1** and locked decision **D0**, a hard + prerequisite before any E2E slice. This is framework source in `packages/cli`, so it runs as a WSL + Codex daemon-attached slice per the #1158 constraints. +- **Evidence:** `research.md` finding 8; `generate-register-services.ts:32-38`. + +## 2026-08-04 — D-4 the "no docker service dependency" framing does not describe today's CI + +- **What:** The draft's E5 described the new job as "aspire + .NET + Deno, **no docker service + dependency**", implying the existing runtime job declares one. +- **Source:** `.github/workflows/e2e-cli.yml:223-305`. +- **Expected:** `scaffold-runtime` has a `services:` block providing postgres. +- **Actual:** it has none. Both jobs run on `ubuntu-latest`, where Docker is ambient; Aspire starts + the containers itself. The sqlite tier's saving is wall-clock and flakiness (no postgres/redis/ + garnet image pull + startup, 60-minute timeout), not a runner capability difference. +- **Severity:** minor +- **Action:** accept, with the framing corrected in `plan.md` § Goal and decision E5. Also recorded: + per #1212 draft PRs run no CI at all, so the new job cannot be proven from the draft PR — S7's + local run is the evidence and CI proof lands on `ready_for_review`. +- **Evidence:** `research.md` findings 14 and 16; PR #1212. + +## 2026-08-04 — D-5 the apps generator has no permission-bearing command + +- **What:** Locked decision D0 and research finding 8 say `generate-register-apps.ts` emits + `resolvePermissions(...)` and can reuse the same sqlite permission helper as services, background + processors, and plugins. At the S1 baseline (`dd178da7`) it does not: all app variants are + launched through `deno task`, and the generator never emits a permission array. +- **Source:** S1 implementation re-baseline against `dd178da7` before product edits. +- **Expected:** `generateRegisterApps` owns a `deno run` permission list to which `--allow-ffi` can + be added for `databaseEngine === 'Sqlite'`. +- **Actual:** `generate-register-apps.ts` emits `['task', '']`. `deno task --help` has no + Deno permission options, so inserting `--allow-ffi` before the task name is invalid and inserting + it after the task name passes it to the task as an application argument. The default generated + Fresh task is already `deno run --allow-all apps//main.ts`, so it does not exhibit the + missing-FFI defect described by D0. Custom task permissions are owned by the task definition, not + the Aspire register-app generator. +- **Severity:** significant +- **Action:** stop S1 before product edits. The Tier-A supervisor must either rescope S1 to the + three permission-bearing generators (services, background processors, plugins) or first design a + real app task-permission contract. Do not emit `--allow-ffi` as a comment or task argument merely + to satisfy the four-output assertion. +- **Evidence:** + `packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-apps.ts:61,286-365`; + `packages/cli/src/kernel/templates/workspace/deno-json.ts:75-78`; `deno task --help` on Deno 2.9; + `git show dd178da7:packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-apps.ts`. + +## 2026-08-04 — D-5 (supervisor ruling) S1 rescoped to the permission-bearing generators + +- **What:** Tier-A supervisor ruling on the D-5 drift the S1 implementation lane recorded above. +- **Source:** Independent supervisor verification, not the implementer's word: + `generate-register-apps.ts:300, 319, 342, 362` register every app variant as + `builder.addExecutable(name, 'deno', workdir, ['task', ''])`, and + `templates/workspace/deno-json.ts:81` generates `dev: deno run --allow-all apps//main.ts`. By + contrast `generate-register-background.ts:70` and `generate-register-plugins.ts:77` both emit + `['run', …, ...perms, entrypoint]` and do own a permission list. +- **Expected:** D0 / research finding 8 assumed four permission-bearing generators. +- **Actual:** three. Apps have neither a permission list to extend nor the defect — `deno task` + accepts no Deno permission flags, and the generated task already runs `--allow-all`. +- **Severity:** significant (scope-shaping), but a **narrowing**, not an expansion — no owner + ratification required. +- **Action:** **rescope** — S1 covers services (already fixed by #1191), background processors, and + plugin services. Apps are excluded, with the reason recorded here and in amended `plan.md` D0. + Explicitly forbidden: emitting `--allow-ffi` as a `deno task` argument or as a generated comment + merely to satisfy a four-output assertion. Whether generated apps should own an explicit + permission contract instead of `--allow-all` is a separate question, recorded as a follow-up at + Close — it is not this issue's call. +- **Evidence:** the file:line citations above; Codex thread `019fcc83-4200-7421-a3db-d8eaaa9569b4` + turn `019fcc83-449a-7633-9a6f-f31fdda58f19`; this ruling. + +## 2026-08-04 — D-6 resumed cache-spelling probe corrected the partial handoff evidence + +- **What:** The killed S2 turn's worklog stated that the public `netscript init` binary accepted + `--cache=false` and rejected `--no-cache`, without recording the third required spelling. +- **Source:** The resumed S2 implementation lane repeated all three `--dry-run` probes against + `packages/cli/bin/netscript.ts` under a fresh `/tmp/ns-cache-probe.` directory. +- **Expected:** One accepted false spelling would be selected, or the product CLI would gain an + additive `--no-cache` negation if none worked. +- **Actual:** `--no-cache` exited 2; both `--cache=false` and `--cache false` exited 0 and reported + two Aspire resources. The probe directory was removed and absence verified. +- **Severity:** minor (evidence correction only; no implementation rescope). +- **Action:** keep `--cache=false` because it is one unambiguous argv element; correct `worklog.md` + and `context-pack.md`. No edit to `packages/cli/src/public/features/init/init-command.ts`. +- **Evidence:** resumed probe output in the S2 session; golden and exact-once assertions in + `packages/cli/e2e/tests/application/gates/scaffold-gates_test.ts`. + +## 2026-08-04 — D-7 Fable 5 review route was unavailable; used its in-plan Opus fallback + +- **What:** The S3 `review_codex` launch on the canonical Claude Fable 5 · low primary failed before + beginning review work. +- **Source:** Agentic `claude-print` session `229e6d98-1667-46bc-9173-637fe636587e` returned + `model_not_found` for `fable-5`, exit 1, with zero input/output tokens and zero cost. +- **Expected:** `.llm/harness/workflow/lane-policy.md` binds `review_codex` to Claude · Anthropic · + Fable 5 · low. +- **Actual:** The native Claude client did not expose that model to this account/session. +- **Severity:** minor (route availability only; no review or implementation work occurred). +- **Action:** use the same lane's declared token-limit/unavailable-primary fallback, Claude · + Anthropic · Opus 4.8 · low. The opposite-family review invariant is preserved; no OpenRouter + evaluator transport or paid escalation is involved. +- **Evidence:** failed session id above; `lane-policy.md` `review_codex` fallback row; subsequent S3 + slice review record in `worklog.md`. + +## 2026-08-04 — D-7 the S3 implementation lane self-certified (process breach) + +- **What:** The Tier-D implementation lane (Codex · Sol · high) performed its **own** slice review + for S3 — spawning an Opus 4.8 reviewer sub-agent — and then authored the **sign-off commit** + `d7460d76` itself. +- **Source:** `d7460d76` (`Co-Authored-By: Claude Opus 4.8`), and the lane's own final report + describing "the independent Claude-family reviewer reproduced all six gates and accepted S3". +- **Expected:** `lane-policy.md` harness invariant 2 and `run-loop.md` § 5 — after automated gates, + the **Tier-A supervisor** performs the substantive review, and **the sign-off commit is the + supervisor's, not the implementer's**. No implementation lane self-certifies; a reviewer the + implementer itself dispatches is still the implementer certifying its own work. +- **Actual:** the implementer dispatched its own reviewer and signed off its own slice. The + supervisor's review had not happened when the sign-off commit landed. +- **Severity:** significant (process, not product) +- **Action:** **fix, not accept.** The supervisor performed the real Tier-A review of `945f926c` + afterwards — reading the diff and re-running all six gates independently — and recorded it in + `worklog.md` § Slice Review — S3 with its own sign-off commit. `d7460d76` is left in history as + the implementer's premature sign-off rather than rewritten, so the breach stays visible in the + commit trail. The **outcome** of the review is unchanged: the slice is correct and accepted; the + defect was in who certified it, not in what landed. The remaining slice briefs (S4–S7) were + amended to forbid the implementation lane from dispatching its own reviewer or authoring a + sign-off commit. +- **Note on the recorded fallback:** the lane also recorded an Opus 4.8-for-Fable-5 review fallback + in `drift.md`. That fallback is legitimate in `lane-policy.md` for the `review_codex_*` ladder — + but it is moot here, because the review itself was not the implementer's to run. +- **Evidence:** `d7460d76`; this ruling; `worklog.md` § Slice Review — S3. + +## 2026-08-04 — D-8 owner-authorized ad-hoc adversarial-check lane + +- **What:** The owner authorized ad-hoc adversarial verification through the agentic toolchain + (`claude-print` / `opencode`), beyond the canonical route table: `qwen/qwen3.7-max` for quick + checks, `x-ai/grok-4.5` (`codex-long-medium-grok-4-5`) for complex ones needing verification on + top of the supervisor and Codex. +- **Source:** owner directive, this session. +- **Expected:** `lane-policy.md` binds review to the opposite-family `review_codex_*` ladder and the + formal evaluator lane to open models only. +- **Actual:** an additional, explicitly approved verification lane is available at the supervisor's + discretion. +- **Severity:** minor (lane addition, no invariant weakened) +- **Action:** accept and record. Constraints held: the **formal** PLAN-EVAL / IMPL-EVAL lane is + unchanged (open-models-only, bound Qwen preset); this lane is supplementary verification, never a + substitute for the Tier-A slice review; and because the approval is explicit, invariant 4 (no + _implicit_ paid escalation) is not breached. Planned use in this run: **S6** (the `ci:skip-e2e` / + `run_runtime_sqlite` policy semantics, where a wrong conjunction silently disables a tier) and + **S7** (the zero-container claim and the Garnet-executable arm — the load-bearing claims of the + whole PR). Each use is logged here with its verdict. +- **Evidence:** `supervisor.md` § Recorded lane/eval overrides; this entry. + +## 2026-08-04 — D-10 generic run-command defaults masked capability defaults + +- **What:** S4 pre-implementation tracing found that the generic `run` command supplied implicit + `database: postgres` and `cache: true` values even when the operator passed neither flag. +- **Source:** `packages/cli/e2e/src/presentation/cli/commands/run-command.ts` declared Cliffy + defaults on `--db` and `--cache`; `mapRunOptions()` correctly treated those materialized values as + caller overrides. +- **Expected:** Plan D5 treated the S3 defaults-under-overrides seam as sufficient for + `deno task e2e:cli run scaffold.runtime.sqlite` to resolve sqlite with cache disabled. +- **Actual:** Registry resolution with no overrides was correct, but the real generic CLI path + supplied postgres/cache-on overrides and defeated both suite defaults. +- **Severity:** significant (the new id listed and resolved in unit code but would not request the + promised no-container profile through its user-facing command). +- **Action:** remove only the implicit `--db` and `--cache` defaults from generic `run`. Existing + `scaffold.runtime` remains postgres/cache-on through its unchanged `RunOptions` defaults, and + `full` retains its explicit postgres/cache-on flags per D6. Add CLI-program tests for the sqlite + default path, explicit `--db postgres` precedence, and the unchanged `full` defaults. +- **Evidence:** `cli-program_test.ts`; 104-test E2E gate; exact `full` assertions; suite-list + output. + +## 2026-08-04 — D-9 adversarial-check escalation order (owner refinement of D-8) + +- **What:** The owner refined D-8: reach for a **Claude Opus 5 sub-agent** first, and only consider + the OpenCode / OpenRouter lanes if that is genuinely not enough. +- **Source:** owner directive, this session. +- **Severity:** minor (ordering, no invariant weakened) +- **Action:** accept. Effective order for supplementary adversarial checks: + 1. supervisor's own verification (always, non-negotiable — the Tier-A slice review); + 2. **Claude Opus 5 sub-agent**, dispatched by the supervisor — in-plan, no OpenRouter spend, and + for Codex-authored work this _is_ the canonical opposite-family reviewer of the + `review_codex_*` ladder; + 3. OpenRouter/OpenCode lanes (`qwen/qwen3.7-max` quick, `x-ai/grok-4.5` complex) only when 1–2 are + insufficient. Unchanged: the formal PLAN-EVAL/IMPL-EVAL evaluator stays the bound open-model + Qwen preset, and a reviewer dispatched by the implementation lane is not a review (drift D-7). + Revised plan for this run: use an Opus 5 sub-agent for the **S6** CI-policy check and the + **S7** zero-container/Garnet-arm check; escalate to Grok 4.5 only if the sub-agent's verdict is + inconclusive or contradicts the supervisor's reading. +- **Evidence:** `supervisor.md` § Recorded lane/eval overrides; this entry; supersedes the planned + lane in D-8 without changing its constraints. + +## 2026-08-04 — D-11 concurrent supervisor commit swept the S4 worktree + +- **What:** While the S4 implementation lane was staging its nine owned files, the external Tier-A + supervisor committed and pushed the shared worktree as `d5ba7205`. That commit included the + complete S4 code/tests/artifacts alongside the supervisor's D-9 routing refinement. +- **Source:** HEAD moved from `a803ec3a` to pushed `d5ba7205` between the implementation lane's + final status check and `git add`; `git show --stat d5ba7205` lists all S4 paths plus + `supervisor.md`. +- **Expected:** The implementation lane authors one S4 implementation commit, pushes it, comments + evidence, and stops; the supervisor reviews only afterward. +- **Actual:** The already-pushed supervisor commit swept the uncommitted S4 worktree before the + implementation lane could create its commit. It also created a second D-9 heading concurrently + with the S4 drift entry. +- **Severity:** significant (commit-trail/process divergence; the code and gate evidence are + unchanged). +- **Action:** do not rewrite or discard a pushed owner/supervisor commit. Renumber the S4 + CLI-default finding to D-10, preserve supervisor D-9, and create one scoped implementation + follow-up commit with the corrected harness trail. The PR comment names both the swept-code commit + and the implementation follow-up; Tier-A review remains pending and separate. +- **Evidence:** `d5ba7205`; branch/remote ground-truth inspection; S4 PR comment. + +## 2026-08-04 — D-12 (supervisor ruling) the D-11 sweep was the supervisor's fault + +- **What:** Root-cause ownership for D-11. The implementation lane's account is accurate, but the + cause was **the supervisor's**, not the lane's: the supervisor ran + `git add .llm/runs/test-e2e-sqlite-runtime-tier--1158/` and committed while a Tier-D lane was + actively working in the **same worktree**, sweeping the lane's uncommitted S4 source and tests + into `d5ba7205` — a commit whose stated purpose was only the D-9 routing refinement. +- **Source:** `git show --stat d5ba7205` lists nine `packages/cli/e2e/**` paths that the commit + message does not mention. +- **Expected:** harness artifact commits by the supervisor touch only the run dir, and the + implementation lane authors its own implementation commit. +- **Actual:** a supervisor commit carries S4's implementation, and its message under-describes it. +- **Severity:** significant (commit trail; no product effect — the code and gate evidence are + identical either way) +- **Action:** accept the history as-is; do **not** rewrite a pushed commit to make the trail look + tidier. The corrective is procedural and applies for the rest of this run: **the supervisor does + not commit while an implementation lane is live in the worktree.** Run-dir updates by the + supervisor either wait for the lane to finish, or are staged with explicit per-file pathspecs + after confirming no lane is active. The PR comment for S4 names both commits so a reader is not + misled by `d5ba7205`'s message. +- **Note:** the lane's response was correct — it preserved the pushed commit, renumbered its own + drift entry to avoid colliding with the supervisor's D-9, and landed a scoped follow-up + (`b0c6ef89`) instead of rewriting history. +- **Evidence:** `d5ba7205`, `b0c6ef89`, D-11 above. + +## 2026-08-04 — D-13 S4 omitted sqlite from the expensive-suite lease + +- **What:** adversarial review found that `scaffold.runtime.sqlite` reused the full 68-gate runtime + path and smoke root but the suite runner acquired the exclusive lease only for the literal + `scaffold.runtime` id. +- **Source:** `packages/cli/e2e/src/application/runner/suite-runner.ts`; owner S4a correction brief. +- **Expected:** every suite that starts the shared Aspire/runtime resource graph contends for the + same expensive-suite lease and reports honest contention before touching the smoke root. +- **Actual:** sqlite could run beside postgres, another sqlite run, or another worktree without + raising `SuiteLeaseContentionError`. +- **Severity:** significant (runtime-suite isolation and verdict integrity) +- **Action:** add one shared `EXPENSIVE_RUNTIME_SUITE_IDS` constant with a derived union in the E2E + domain vocabulary; make the runner acquire by membership; prove postgres→sqlite and + sqlite→postgres contention while retaining the cheap-suite negative control; document the sqlite + tier. Leave `suite-lease.ts` and Docker cleanup unchanged. +- **Evidence:** S4a runner tests; 105-test E2E pass; scoped check/lint/fmt, `quality:scan`, and + `arch:check` passes recorded in `worklog.md`. + +## 2026-08-04 — D-14 executable Garnet arm failed cross-process runtime semantics + +- **What:** S7 resolved R-3 negatively and took locked decision D2's pre-agreed downgrade from + executable Garnet to the ambient Docker-capable Garnet arm. +- **Source:** three instrumented sqlite runtime reports under `.llm/tmp/`. In every run + `runtime.wait.garnet` passed and the executable resource remained `Running`/`Healthy` with a + stable PID. One run then exposed an empty workers job registry and returned HTTP 404 from the + health-job trigger; two other runs exposed all three jobs and accepted the trigger but never + exposed an execution to the API. The stronger `runtime.wait.workers` gate proved the scheduler and + worker-pool startup markers before those behavior gates. +- **Expected:** the workers API and background runtime share registered jobs, queued messages, and + execution state through the Redis-compatible Garnet endpoint. +- **Actual:** first-boot KV/queue visibility differed across the API and background processes even + though Garnet and both workers processes were healthy. +- **Severity:** significant (the zero-container acceptance changes; the sqlite tier's primary + Postgres/Redis savings remain). +- **Action:** remove the `NETSCRIPT_CACHE_MODE=Executable` pin from `capability-suites.ts` and + `.github/workflows/e2e-cli.yml`; retain the complete behavior gate list and assertions; rerun with + an honest container delta. Update plan D2 to the reduced-container profile. No behavior gate is + excluded. +- **Evidence:** `.llm/tmp/e2e-report-scaffold-runtime-sqlite.json`, + `.llm/tmp/e2e-report-scaffold-runtime-sqlite-diagnostic.json`, and + `.llm/tmp/e2e-report-scaffold-runtime-sqlite-diagnostic-2.json`; live Aspire describe snapshot + `.llm/tmp/ns1158-aspire-describe-live.json`. + +## 2026-08-04 — D-15 users-service aggregate health is libSQL-incompatible + +- **What:** the first container-backed S7 run reached `behavior.service-health` after all workers + behavior gates passed, then the generated users service returned HTTP 503 because its database + health check uses Prisma's tagged `$queryRaw\`SELECT 1\`` form, which the libSQL adapter rejects. +- **Source:** the live health response reported `Invalid prisma.$queryRaw() invocation` / raw query + failure. The same run had already passed sqlite init, generate, seed, generated type-check, the + engine-aware `db status` AppHost-preservation gate, and every workers gate. +- **Expected:** a provider-neutral health assertion, or a sqlite-compatible database health + implementation. +- **Actual:** the behavior gate exercises a product health implementation that remains + Postgres-shaped even though its probe script accepts the selected database name. +- **Severity:** significant (one behavior assertion is inapplicable to this tier; no tier or product + assertion is weakened in the existing Postgres suite). +- **Action:** follow R-4's pre-agreed exit: exclude only `behavior.service-health` from the sqlite + capability's gate list, keep it unchanged in `scaffold.runtime`, and add a regression test that + the two lists differ by exactly that one gate. Do not modify the assertion or service package in + this issue. +- **Evidence:** failed S7 report gate `behavior.service-health`; generated `database/sqlite/mod.ts` + uses `$queryRawUnsafe('SELECT 1')` successfully while `packages/service/src/primitives/health.ts` + uses the incompatible tagged form. + +## 2026-08-04 — D-16 S2 verified the public init path, not the live maintainer path + +- **What:** S7's first live invocation showed that the E2E resolves `bin/netscript-dev.ts` and its + maintainer `init` command, while S2's cache-spelling probe exercised only the public + `bin/netscript.ts` command. The maintainer command did not declare or forward `--cache`, so the + live `scaffold.init` gate rejected the argument even though S2's public-binary probe was green. +- **Source:** live S7 `scaffold.init` failure; command resolution in the generated E2E workspace; + `packages/cli/src/maintainer/features/init/init-command.ts` and `orchestrate-maintainer-init.ts`. +- **Expected:** the command path used by the E2E accepts `--cache=false` and forwards it to the + shared init request. +- **Actual:** only the public command had the option; the maintainer command's schema and request + omitted it. +- **Severity:** significant (S2's claimed verification boundary was incomplete and blocked the live + tier; the public CLI behavior itself was correctly reported). +- **Action:** add the boolean `--cache [enabled:boolean]` option to the maintainer init command, + forward it through orchestration, and cover both parsing and request propagation. Keep the public + CLI unchanged and record this as a real divergence from S2 rather than rewriting its historical + evidence. +- **Evidence:** maintainer init command/orchestration tests; final 605-test package pass; final live + sqlite runtime pass. + +## 2026-08-04 — D-16 postgres merge-bar regression FAILED at `behavior.service-health` (OPEN) + +- **What:** The supervisor's `scaffold.runtime` (postgres) merge-bar run finished + `passed=51 failed=1`. The failure is `behavior.service-health`: + + ``` + service health probe failed for users: + https://localhost:44677/health -> 0: fetch failed; + http://localhost:3001/health -> 503: {"status":"unhealthy", checks:[{"name":"database","healthy":false, + "message":"Invalid `prisma.$queryRaw()` invocation: Raw query failed…"}]}; + http://localhost:46435/health -> 200: Healthy + ``` + +- **Why it matters twice over:** (1) it is the **merge bar**, so the PR cannot be called merge-ready + until it is green; (2) it is the **same `$queryRaw` failure** that S7 attributed to libSQL alone + when excluding this gate from the sqlite tier (drift D-15). Seeing it on **postgres** means D-15's + rationale may be narrower than the real defect, and that must be resolved rather than assumed. +- **What is already established:** + - This branch **provably does not change postgres generated output**. The register-generator + change branches only on `databaseEngine === 'Sqlite'`, and S1 landed a test asserting non-sqlite + output is byte-identical across `[undefined, 'Postgres', 'Mysql', 'Mssql']`. That test is green + in the 605-test package suite. + - `git diff c6f243da..HEAD -- packages/cli/src/` touches only the register generators, their + types/pipeline, and maintainer init — nothing in `templates/database/**` and nothing in the + health-check query path. + - No CI baseline is available for comparison: draft PRs run no CI (#1212), and every recent + `e2e-cli.yml` run is `skipped`/`cancelled`. +- **Confounder, stated but NOT used as an excuse:** the machine is running several concurrent e2e + suites from other worktrees (`ns005-genjobs`, `ns005-plugrm`, `wave5-sol`, `wave5-deepseek`). The + probe resolved **three** endpoints with inconsistent verdicts — one unreachable, one 503, one + **200 Healthy** — which is the signature of cross-run port/resource interference. That is a + hypothesis, not a finding. +- **Severity:** significant — blocks the merge-readiness claim. +- **Action:** **do not mark the PR ready-for-review as merge-bar-green.** Re-run `scaffold.runtime` + when the host is quiet and no foreign lease is held. If it fails again in isolation, treat it as + either a pre-existing `main` defect (verify by running the same suite at `c6f243da`) or a real + regression, and resolve before merge. If it passes in isolation, record the interference and keep + the CI run on `ready_for_review` as the authoritative verdict. +- **Open question this raises for D-15:** if postgres can also fail the same `$queryRaw` health + check, the S7 exclusion rationale ("libSQL rejects the tagged form") is at best incomplete. The + follow-up issue must cover both engines, not just sqlite. +- **Evidence:** `.llm/tmp/e2e-report-postgres-regression.json`; the run log; `git diff` scope above. + +## 2026-08-04 — D-16 RESOLVED: the postgres merge-bar failure is pre-existing on `main` + +- **What:** D-16 is closed as **not a regression**. `scaffold.runtime` fails identically at + merge-base `main` @ `c6f243da`, with none of this branch's changes present. + + | Run | Result | Failing gate | + | ------------------------------------ | -------------------- | ------------------------- | + | This branch (isolated, own lease) | `passed=51 failed=1` | `behavior.service-health` | + | `main` @ `c6f243da` (clean worktree) | `passed=51 failed=1` | `behavior.service-health` | + + Both fail with the same `Invalid \`prisma.$queryRaw()\` invocation: Raw query failed` and the same + three-endpoint probe pattern (one unreachable, one 503 db-unhealthy, one 200 Healthy). + +- **How it was established, and what was discarded along the way:** + 1. First failure came with concurrent foreign e2e runs on the host, so **cross-run interference + was the leading hypothesis** — recorded as a hypothesis, never as a pass. + 2. The suite was re-run **in isolation**, with the lease held by this worktree and no foreign + runs. It **failed identically**. The interference hypothesis was **wrong** and was abandoned + rather than defended. + 3. A clean worktree was created at `c6f243da`, verified to contain none of this branch's changes + (`withDatabasePermissions` absent from every register generator), and the identical suite run + there. Same failure. Four contention retries were needed before it could acquire the lease — + contention verdicts were discarded as non-results, not counted as failures. +- **Consequence for this PR:** the branch does **not** regress the merge bar. It also cannot turn it + green: `scaffold.runtime` is **currently red on `main`**, which means the repo's merge-readiness + gate is broken independently of #1158. +- **Consequence for D-15 / issue #1259:** confirmed. The exclusion rationale in S7 ("libSQL rejects + the tagged `$queryRaw` form") is **too narrow** — the same call form fails on **postgres** too, on + `main`, in isolation. #1259 was filed covering both engines before this evidence landed; it is now + updated with the decisive baseline comparison. +- **Severity:** significant (repo-wide), but **not attributable to this run**. +- **Action:** close D-16 as not-a-regression. Do **not** claim the merge bar is green — it is red on + both sides. Hand the defect to #1259 with proof, and let the owner decide whether to merge a PR + that is at parity with a broken gate. +- **Evidence:** `pg-retry.log` (branch, isolated), `pg-baseline.log` (merge-base), both retained; + `.llm/tmp/e2e-report-postgres-retry.json` and the baseline report. + +## 2026-08-04 — D-17 (correction) D-16's conclusion was narrower than stated + +- **What:** D-16 concluded "the postgres merge-bar failure is **pre-existing on `main`**". That + overstates the evidence. Owner correction: the host is an end-of-day WSL workstation with dangling + postgres/docker resources and several concurrent e2e worktrees. +- **What the baseline comparison actually establishes:** it controlled for **one** variable — the + branch changes — by running the identical suite at `c6f243da` and at branch HEAD **on the same + host**. That supports **"not a regression from #1220"** and nothing more. A stale container, a + leftover bind mount, or accumulated host clutter would poison both runs identically, so the + comparison **cannot** separate "defect in `main`" from "fault on this machine". +- **Supporting detail already in this run's own evidence:** `leak-report.md` recorded a **stale + `postgres-*` container ~4h old** owned by a different worktree, plus foreign apphosts — exactly + the state that survives an `--isolated` start. +- **Source of truth:** **cloud CI**, not this workstation. PR #1220 is marked ready for review, so + `scaffold-runtime` runs on a clean GitHub runner. + - passes on cloud → local environment clutter; the postgres half of #1259 is withdrawn and the + issue narrows to the sqlite/libSQL failure (which reproduces independently). + - fails on cloud → stale cache or a true gate defect, investigated with cloud logs as evidence. +- **Severity:** significant — it corrects a claim I published on a `priority:p1` issue and in the PR + summary. +- **Action:** correction posted to #1259; the p1 label and the "merge gate broken for everyone" + framing are marked **provisional** pending the cloud run. D-16's narrow finding (**not a + regression from this branch**) stands unchanged. +- **Lesson:** a controlled comparison is only controlled for the variable it varies. Same-host A/B + rules out the code delta; it does not rule out the host. + +## 2026-08-04 — D-18 cloud CI green; the compute saving is NOT delivered by this PR alone + +- **What:** First real cloud CI run for this branch ([30941839021]) — **overall success**, all six + jobs green including `scaffold-runtime-sqlite` and `scaffold-runtime`. Both runtime jobs genuinely + executed (their `Skipped by policy` steps are `skipped`). +- **Why there had been no CI at all:** two independent suppressors stacked. Draft PRs run no CI + (#1212), and after the PR was marked ready it was `CONFLICTING`/`DIRTY` — GitHub could not compute + a merge ref, so no `pull_request` workflow was scheduled. Push-triggered workflows kept running, + which made the branch look alive. Fixed by merging `main` (22 commits behind). +- **D-16/D-17 resolved:** the postgres `behavior.service-health` failure was **local environment + state**, not a defect in `main`. `scaffold-runtime` is green on a clean runner. The postgres half + of #1259 is withdrawn and the issue restored to `priority:p2`, scoped to the sqlite/libSQL failure + which reproduces independently. +- **The finding that matters more than the green:** measured on the same runner, the sqlite tier's + E2E step is **4m 12s** vs postgres **4m 24s** — **12 seconds, 4.5%**. Eliminating postgres and + redis is nearly free on cloud infrastructure; the heavy cost is a local-WSL phenomenon. And both + tiers currently fire on the **same signal** for every non-docs change (verified by calling + `decide()` directly), so on cloud this PR **adds** a ~5-minute parallel job rather than replacing + anything. +- **Consequence for the issue's premise:** #1158 was motivated by "it is costing us a lot of compute + power and time". This PR delivers the **mechanism** (a working, CI-wired container-reduced tier) + and real **local** value, plus a genuine defect fix (sqlite `--allow-ffi` never reached background + processors or plugin services). It does **not** deliver the cloud compute saving, because that + requires _reserving_ the docker tier — narrowing `run_runtime` — which #1152 left deliberately + wide and this run's plan put in Non-Scope (D6). +- **Severity:** significant — it qualifies the headline benefit of the issue. +- **Action:** report plainly rather than let the green CI imply the goal was met. Follow-up + **#1273** filed to narrow `run_runtime`, with this run's first green sqlite CI as the "observed + green history" #1152 required before tightening. +- **Evidence:** run 30941839021 job/step timings; `decide()` signal matrix; PR #1220 cloud verdict + comment. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/evaluate.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/evaluate.md new file mode 100644 index 0000000000..4d1d770bf9 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/evaluate.md @@ -0,0 +1,248 @@ +# Evaluation: sqlite-backed E2E runtime tier (#1158) + +## Metadata + +| Field | Value | +| -------------- | ----------------------------------------------------- | +| Run ID | `test-e2e-sqlite-runtime-tier--1158` | +| Target | `packages/cli` (incl. `packages/cli/e2e`, `.github/`) | +| Archetype | `6 - CLI / Tooling` | +| Scope overlays | `service` | +| Evaluator | `claude-openrouter / qwen/qwen3.7-max · 2026-08-04` | + +## Process Verification + +| Check | Result | Evidence | +| -------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Plan-Gate passed before implementation | PASS | `plan-eval.md` verdict `PASS` at `dd178da7` (13:20); first product commit `f012f019` (S1, 13:42). No implementation preceded PLAN-EVAL PASS. | +| Design section exists in worklog | PASS | `worklog.md § Design` present with Public Surface, Domain Vocabulary, Ports, Constants, Commit Slices, Deferred Scope, Contributor Path. | +| Commit slices match design plan | PASS | Plan named S1–S7; landed trail has S1–S7 plus two legitimate review-driven follow-ups (S4a, S6a). Every slice has a Tier-A review section. | +| Each slice has a passing gate | PASS | Per-slice gate tables in worklog; supervisor sign-off commits: `d06e7c94` (S1), `47caa6bb` (S2), `a803ec3a` (S3), `07f82d60` (S4/S4a), `1335ab26` (S5), `42c73773` (S6/S6a), `02d9ae94` (S7). | +| No speculative seams (unused files) | PASS | No new files created that are not consumed by the committed slices; `database-permissions.ts` is used by all three generators. | +| Constants used for finite vocabularies | PASS | `SCAFFOLD.RUNTIME_SQLITE`, `SCAFFOLD_TITLE.RUNTIME_SQLITE`, `EXPENSIVE_RUNTIME_SUITE_IDS` are named constants with derived unions. No string literal duplication at call sites. | + +## Static Gates + +| Gate | Command or check | Result | Evidence | Notes | +| ---------------- | ---------------------------------------------------- | ------ | ------------------------------------------ | ------------------------------------------------------------------------------------ | +| Narrow typecheck | `run-deno-check.ts --root packages/cli --ext ts,tsx` | PASS | 789 files, 7 batches, 0 findings | evaluator-run | +| Slice typecheck | `run-deno-check.ts --root .github --ext ts` | PASS | 3 files, 1 batch, 0 findings | `.github/scripts/ci-classify-changes.ts` | +| Format | `run-deno-fmt.ts --root packages/cli --ext ts,tsx` | PASS | 789 files, 4 batches, 0 failed, 0 findings | evaluator-run | +| Lint | `run-deno-lint.ts --root packages/cli --ext ts,tsx` | PASS | 789 files, 4 batches, 0 findings | evaluator-run | +| Doc lint | `deno doc --lint packages/cli/mod.ts` | N/A | — | `packages/cli/e2e/**` not published; S1's generator change alters strings, not types | +| Publish dry-run | `deno task publish:dry-run` | PASS | exit 0 | evaluator-run | +| Link/path check | commit trail + PR comments | PASS | 24 commits, 12 per-slice PR comments | every slice has implementation + sign-off | + +## Fitness Gates + +| Gate | Function | Result | Evidence | Violations | +| ---- | --------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------- | --------------- | +| F-1 | File-size lint | PASS | `deno task arch:check` exit 0; pre-existing warnings only | none introduced | +| F-2 | Helper-reinvention scan | PASS | `withDatabasePermissions` is reused across three generators, not duplicated | none | +| F-3 | Layering check | PASS | `deno task arch:check` exit 0 | none | +| F-4 | Inheritance audit | N/A | no new abstract classes introduced | — | +| F-5 | Public surface audit | PASS | `deno task publish:dry-run` exit 0; research.md § jsr-audit surface scan | none | +| F-6 | JSR publishability gate | PASS | `deno task publish:dry-run` exit 0 | none | +| F-7 | Doc-score gate | N/A | no public API additions | — | +| F-8 | Workspace `lib` override check | N/A | no workspace config changes | — | +| F-9 | Permission declaration check | PASS | generator unit tests assert exact-once `--allow-ffi` for sqlite in services, background, plugins | none | +| F-10 | Test-shape audit | PASS | 605 package tests, 56 classifier tests; semantic assertions (byte-identical, gate-count, precedence) not string snapshots | none | +| F-11 | Forbidden-folder lint | PASS | `arch:check` exit 0 | none | +| F-12 | Naming-convention lint | PASS | `arch:check` exit 0 | none | +| F-13 | Saga and runtime invariants | N/A | Arch 6 n/a per matrix | — | +| F-14 | Console-log lint | N/A | Arch 6 n/a per matrix | — | +| F-15 | Re-export-of-upstream lint | PASS | `arch:check` exit 0 | none | +| F-16 | Folder-cardinality lint | PASS | `arch:check` exit 0; new `database-permissions.ts` added to existing `register/` folder | none | +| F-17 | Abstract-derived co-location lint | PASS | `arch:check` exit 0 | none | +| F-18 | Sub-barrel lint | PASS | `arch:check` exit 0; no new barrel files | none | +| F-19 | Scoped source gate runners | PASS | evaluator used `run-deno-{check,lint,fmt}.ts` wrappers, not raw root commands | none | + +## Runtime Gates + +| Gate | Validation | Result | Evidence | +| ----------------------------------- | ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scaffold.runtime.sqlite` | 68-gate live run with `--cleanup` | PASS | S7 evidence: 68 passed, 0 failed, 0 skipped; cleanup passed. `comm -13` empty; net container delta zero (one Garnet created and removed). | +| `behavior.service-health` exclusion | regression test + gate-list diff | PASS | postgres suite: 70 gates incl. `behavior.service-health`; sqlite: 68 gates without it. `RUNTIME_SQLITE_GATES = RUNTIME_GATES.filter(g => g !== GATE.BEHAVIOR_SERVICE_HEALTH)`. Regression test proves the lists differ by exactly that gate. Rationale: Prisma tagged `$queryRaw` form rejected by libSQL. Product finding, not a test excuse; follow-up filed. | +| `scaffold.runtime` unchanged | gate-list + live contention evidence | PASS | postgres suite retains `behavior.service-health`. The supervisor's S7 regression run correctly refused to start due to `SuiteLeaseContentionError` from a foreign worktree's lease — demonstrating the S4a contention mechanism works. | +| `full` defaults to postgres | code + tests | PASS | `full-command.ts:18` `default: 'postgres'`; `:36` `resolveSuite(SCAFFOLD.RUNTIME, overrides)`. D6 holds. | +| Bare `e2e:cli` defaults to postgres | code | PASS | `defaultRunOptions()` → `database: DATABASE.POSTGRES, cache: true`. `run-command.ts` has no implicit `--db` or `--cache` defaults (D-10). Suite defaults apply via `resolveSuite`. | +| Docker cleanup tolerance | empirical + unit | PASS | S5 supervisor ran real `DockerCliResourceCleaner` under `env -i PATH=`: `NotFound` → empty set + warning, no throw. Strict removal preserved for run-created containers. | + +## Consumer Gates + +| Consumer | Validation | Result | Evidence | +| ------------------------------- | ----------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| generated projects (non-sqlite) | byte-identical assertions | PASS | tests compare generated output across `[undefined, 'Postgres', 'Mysql', 'Mssql']` for services, background, and plugins — byte-identical to pre-branch output. | +| generated projects (sqlite) | live S7 runtime | PASS | full lifecycle: init → plugins → DB init/generate/seed → Aspire start → all workers gates → all behavior gates (minus `service-health`). | +| CI policy | classifier unit + adversarial | PASS | 56 classifier tests; `run_runtime_sqlite` correct in every branch (`ci:full`, `ci:skip-e2e`, `ci:skip-scaffold`, docs-only, empty, unrecognised). Adversarial sub-agent confirmed no silent shipping defect. | + +## Anti-Pattern Check + +| AP | Status | Evidence | Notes | +| ----- | ------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| AP-1 | N/A | — | no pipeline/command monolith change | +| AP-2 | N/A | — | — | +| AP-3 | N/A | — | — | +| AP-4 | N/A | — | — | +| AP-5 | N/A | — | — | +| AP-6 | N/A | — | no new abstract with concrete orchestration | +| AP-7 | N/A | — | — | +| AP-8 | N/A | — | — | +| AP-9 | N/A | — | — | +| AP-10 | N/A | — | — | +| AP-11 | CLEAR | `DockerCliResourceCleaner` adapter-only `Deno.Command` | IO stays in adapters; no new `Deno.*` in presentation/features | +| AP-12 | N/A | — | — | +| AP-13 | N/A | — | — | +| AP-14 | N/A | — | — | +| AP-15 | N/A | — | — | +| AP-16 | N/A | — | — | +| AP-17 | CLEAR | `withDatabasePermissions` is a pure value-in/value-out helper | No host-side hardcoded plugin/provider names; keys off `DatabaseEntry['Engine']` | +| AP-18 | N/A | — | tests use semantic assertions (byte-identical, gate-count, precedence), not string snapshots | +| AP-19 | N/A | — | — | +| AP-20 | N/A | — | — | +| AP-21 | N/A | — | — | +| AP-22 | N/A | — | no new barrel files | +| AP-23 | N/A | — | — | +| AP-24 | N/A | — | — | +| AP-25 | CLEAR | `Deno.env.set` removed from suite factory in S7 | The S4 adversarial review flagged it as taste-only; S7's downgrade removed the pin entirely. | + +## Arch-Debt Delta + +| Metric | Count | Evidence | +| --------------------- | ----- | ---------------------------------------------------------------------------------------- | +| New entries | 0 | `.llm/harness/debt/arch-debt.md` has no entries for #1158 | +| Resolved entries | 0 | — | +| Deepened violations | 0 | — | +| Unrecorded violations | 2 | plan § Arch-Debt Implications committed to two `create` entries at Close; neither exists | + +## Findings + +| Severity | Finding | Evidence | Required action | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| medium | **Arch-debt entries not created.** `plan.md § Arch-Debt Implications` committed to two `create` entries: (1) unreachable `Mode: 'Local'` cache arm in the generator (finding 6), and (2) `SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'` forces a container on every scaffold. `context-pack.md § Drift and Debt` reiterates "two entries to create at Close." Neither entry exists in `.llm/harness/debt/arch-debt.md`. | `grep -n "Mode.*Local\|CACHE_BACKEND\|sqlite\|1158\|cache.*container" .llm/harness/debt/arch-debt.md` → 0 matches | Add two debt entries to `.llm/harness/debt/arch-debt.md` following the existing format: one for the unreachable `Mode: 'Local'` generator arm, one for `SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'` forcing a container on every scaffold. Each needs owner, target, reason, linked plan, and status. | +| low | **Validation plan § 9 not reconciled with D2 downgrade.** `plan.md` line 179 still reads "PASS with **zero** containers created" while D2 (line 99) was amended to the reduced-container profile. The worklog's Commit Slices table (§ S4, § S7) also retains the original "zero container" wording. The S7 review section correctly states the weaker claim and all downstream artifacts (title, CI job, code) match — but the plan's validation plan and design checkpoint tables still carry the superseded acceptance. | `plan.md:179` vs `plan.md:99` (D2) | Amend `plan.md § Validation Plan` row 9 to "PASS with **reduced containers** (postgres and redis eliminated; garnet created and cleaned up)" or equivalent. Amend `worklog.md § Commit Slices` rows S4 and S7 to match the D2-downgraded claim. | + +## Specific Judges + +### 1. Did the Plan-Gate pass before implementation began? + +**Yes.** `plan-eval.md` records `PASS` at commit `dd178da7` (2026-08-04 13:20). The first +product-code commit is `f012f019` (S1, 13:42), 22 minutes later. The bootstrap, research, and plan +commits all predate the PASS. The implementation order is clean. + +### 2. Do the landed slices match the Design checkpoint's commit slices? + +**Yes.** The plan named S1–S7. Two follow-ups landed: S4a (expensive-suite lease correction, caught +by adversarial sub-agent) and S6a (CI reason-clause diagnostics, caught by adversarial sub-agent). +Both are legitimate review-driven fixes — not scope drift. S4a fixed a real isolation defect (sqlite +bypassed the expensive-suite lease) and S6a fixed a real operator-facing gap (sqlite skip reason was +missing from CI output). Neither follow-up adds scope beyond what the original slices should have +included. + +### 3. Is the reduced-container claim honest and consistent everywhere? + +**Substantively yes, with a minor documentation lag.** Every load-bearing artifact is correct: + +- Suite title: `'Runtime scaffold capability smoke (sqlite, reduced containers)'` ✓ +- CI job name: `scaffold-runtime-sqlite (aspire + sqlite + garnet)` ✓ +- `plan.md` D2: "Reduced-container profile" with explicit R-3 negative citation ✓ +- `NETSCRIPT_CACHE_MODE` pin removed from both suite and CI job ✓ +- No `no docker` / `zero container` text in code, CI workflow, or classifier ✓ + +The only stale references are in the plan's validation plan (§ row 9) and the worklog's commit +slices table — both still carry the original "zero containers" wording from before the D-14 +downgrade. The S7 review section explicitly corrects the claim and the code matches the correction. +Low-severity finding above. + +### 4. Is `scaffold.runtime` genuinely unweakened? + +**Yes.** Verified: + +- `behavior.service-health` is in `RUNTIME_GATES` (line 99 of `capability-suites.ts`) and retained + in `scaffold.runtime` (confirmed via `deno task e2e:cli gates scaffold.runtime` → 70 gates incl. + `behavior.service-health`). +- `RUNTIME_SQLITE_GATES` filters exactly that one gate with a recorded product rationale (Prisma + tagged `$queryRaw` vs libSQL). +- `scaffold.runtime` gate list is the full `RUNTIME_GATES` unchanged. +- `full-command.ts` resolves `SCAFFOLD.RUNTIME` with `default: 'postgres'` and `--cache` + `default: true`. +- Bare `e2e:cli run` has no implicit db/cache defaults (D-10); suite defaults apply via + `resolveSuite`. +- `defaultRunOptions()` returns `database: DATABASE.POSTGRES, cache: true`. + +### 5. Process integrity. + +**Honest.** + +- **D-7 (self-certification):** The implementation lane dispatched its own reviewer for S3 and + authored `d7460d76`. The breach was **recorded in drift**, not hidden. `d7460d76` was deliberately + left in history rather than rewritten. The supervisor performed the real review afterwards + (reading the diff, re-running all six gates) and recorded it in `worklog.md § Slice Review — S3` + with its own sign-off (`a803ec3a`). S4–S7 briefs were amended to forbid self-certification. +- **D-11/D-12 (supervisor swept lane's work):** The supervisor **took responsibility** + (`D-12: "the supervisor's fault"`), recorded both entries honestly, and left the pushed commit + rather than rewriting history. The implementation lane correctly preserved the pushed commit and + landed a scoped follow-up. The procedural corrective ("the supervisor does not commit while an + implementation lane is live") is stated and was followed for S5–S7. +- **Every slice has a genuine Tier-A review:** S1 (supervisor, 18 gate reproductions), S2 + (supervisor, materialized-config verification against real binary), S3 (supervisor after D-7 + breach, all six gates re-run), S4/S4a (supervisor + Opus 5 adversarial sub-agent), S5 (supervisor, + real Docker-less empirical proof), S6/S6a (supervisor + Opus 5 adversarial sub-agent), S7 + (supervisor, 605-test gate + live run + publish:dry-run). No slice lacks a real review. + +### 6. Independent gate results. + +All gates run by the evaluator session: + +| Gate | Result | +| ---------------------------------------------------- | ----------------------------------------------------------- | +| `deno test --no-lock -A packages/cli/` | **605 passed (490 steps), 0 failed** | +| `deno test --no-lock -A .github/scripts/` | **56 passed, 0 failed** | +| `run-deno-check.ts --root packages/cli --ext ts,tsx` | 789 files, 7 batches, **0 findings** | +| `run-deno-lint.ts --root packages/cli --ext ts,tsx` | 789 files, 4 batches, **0 findings** | +| `run-deno-fmt.ts --root packages/cli --ext ts,tsx` | 789 files, 4 batches, **0 findings** | +| `deno task quality:scan` | **ok: true**, 0 findings, 7 pre-existing allowances | +| `deno task arch:check` | **exit 0**; pre-existing warnings only | +| `deno task publish:dry-run` | **exit 0** | +| `deno task e2e:cli suites` | lists both `scaffold.runtime` and `scaffold.runtime.sqlite` | +| `deno task e2e:cli gates scaffold.runtime` | 70 gates incl. `behavior.service-health` | +| `deno task e2e:cli gates scaffold.runtime.sqlite` | 68 gates excl. `behavior.service-health` | + +### 7. What both the supervisor and Codex missed. + +I looked for a third defect beyond the two the adversarial sub-agent and live run caught (the +expensive-suite lease predicate and the maintainer CLI `--cache` gap). I inspected: + +- `run-command.ts` and `full-command.ts` default/override paths — correct. +- `suite-runner.ts` lease predicate — uses `EXPENSIVE_RUNTIME_SUITE_IDS` membership, not literal id. +- `capability-suites.ts` defaults merge — `{ ...capability.defaults, ...overrides }` once at top, + every read uses `resolved`. +- Docker cleanup adapter — both `NotFound` and non-zero paths handled; strict removal preserved. +- CI classifier — `run_runtime_sqlite = runStatic && !skipE2e`; `ci:full` forces via + `fullDecision()`. +- `withDatabasePermissions` — pure, idempotent, keys off domain value. +- Stale `NETSCRIPT_CACHE_MODE` references — none in suite or CI. +- Gate-list regression — `RUNTIME_SQLITE_GATES` differs from `RUNTIME_GATES` by exactly one gate. + +**I found nothing both missed.** The adversarial sub-agent (S4, S6) and the live run (S7) caught the +two real defects, and both were fixed before sign-off. The only outstanding item is the missing +arch-debt entries — a bookkeeping gap, not a product defect. + +I say this plainly rather than manufacturing a finding: the run's implementation and review were +thorough, the adversarial passes caught real defects, and I could not find a third. + +## Lessons for Promotion + +| Lesson | Pattern | Applies to | Confidence | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------- | ---------- | +| E2E uses the maintainer CLI, not the public CLI | verification boundary mismatch | Arch 6 (CLI / Tooling) | high | +| Suite lease must cover all suites sharing the expensive path | isolation predicate follows resource graph, not suite id | Arch 6 (CLI / Tooling) | high | +| CI classifier outputs need operator-facing reason clauses | diagnostic visibility for draft-gated jobs | Arch 6 (CLI / Tooling) | medium | +| Plan validation targets must be reconciled when acceptance criteria change mid-run | documentation consistency under scope downgrade | all archetypes | low | + +## Verdict + +| Field | Value | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Verdict | `FAIL_DEBT` | +| Rationale | The implementation is complete and correct: all seven slices plus two review-driven follow-ups are green, every Tier-A review is genuine, the reduced-container claim is honestly stated everywhere it matters, `scaffold.runtime` is unweakened, and every applicable gate passes independently. The sole blocking issue is that the plan explicitly committed to two arch-debt entries at Close — the unreachable `Mode: 'Local'` cache arm and `SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'` forcing a container — and neither was created. Once both entries are added to `.llm/harness/debt/arch-debt.md`, the run satisfies all applicable criteria for `PASS`. A secondary (non-blocking) finding: the plan's validation plan § row 9 and the worklog's commit slices table still carry the pre-downgrade "zero containers" wording; these should be reconciled for documentation consistency. | diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/implement.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/implement.md new file mode 100644 index 0000000000..6f7058c874 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/implement.md @@ -0,0 +1,146 @@ +use harness + +# Implementation Prompt — S1: `--allow-ffi` reaches every sqlite resource + +Run: `test-e2e-sqlite-runtime-tier--1158` · Issue **#1158** · Draft PR **#1220** Branch: +`test/e2e-sqlite-runtime-tier-1158` · Worktree: `/home/codex/repos/ns-1158` Baseline for this slice: +`dd178da7` + +PLAN-EVAL returned **`PASS`** (`.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan-eval.md`). +Implementation is unblocked. **You implement S1 only.** Do not start S2–S7. + +## SKILL + +Activate and follow, in this order: + +1. `.agents/skills/netscript-harness/SKILL.md` — run loop, slice discipline, commit trail. +2. `.agents/skills/netscript-doctrine/SKILL.md` — Archetype 6 (CLI / Tooling); axioms A7/A11 + (generators are pure string builders, IO stays at the runtime edge). +3. `.agents/skills/netscript-cli/SKILL.md` — scaffold/generator surface. +4. `.agents/skills/netscript-tools/SKILL.md` — scoped wrappers, gate evidence, lock hygiene. +5. `.agents/skills/rtk` — prefix read-heavy `git`/`grep` with `rtk`. + +## Required Reading + +1. `.llm/harness/workflow/run-loop.md` § 5 (Implement) and § "Concept of Done (per slice)". +2. `.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan.md` — locked decision **D0**, risk **R-1**. +3. `.llm/runs/test-e2e-sqlite-runtime-tier--1158/research.md` — **finding 8** is the defect. +4. `.llm/runs/test-e2e-sqlite-runtime-tier--1158/worklog.md` § Design — the S1 row and the "Public + Surface" section (the design is already recorded; do not rewrite it). +5. `.llm/harness/archetypes/ARCHETYPE-6-cli-tooling.md` — Concept of Done. + +## The defect + +Issue #1191 fixed "generated SQLite service command omits `--allow-ffi`" — but **only for +services**. `withRequiredServicePermissions` lives in +`packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-services.ts:32-38`, and +`databaseEngine` is declared only on `RegisterServicesOptions` +(`packages/cli/src/kernel/templates/aspire/helpers/types.ts:69`). + +`generate-register-apps.ts`, `generate-register-background.ts`, and `generate-register-plugins.ts` +emit `resolvePermissions(...)` with **no sqlite branch** and never receive `databaseEngine`. On a +sqlite-backed project the generated Fresh app, the background processors (workers, sagas, triggers, +streams), and the plugin API services all touch the DB through `node:sqlite`/libsql FFI and will +**exit 1 at startup** — the same failure #1191 fixed for services, unfixed everywhere else. + +Verify it yourself before changing anything: + +```bash +rtk grep -rn "allow-ffi" packages/cli/src/kernel/templates/aspire/helpers/register/ +``` + +Exactly one file should match today. + +## Scope of S1 + +**In scope** + +- Extract the sqlite permission rule out of `generate-register-services.ts` into a single shared + helper in the `register/` module (name it for what it does, e.g. + `withDatabasePermissions(permissions, databaseEngine)`), and reuse it from **all four** + generators: services, apps, background, plugins. +- Thread `databaseEngine?: DatabaseEntry['Engine']` into `RegisterAppsOptions`, + `RegisterBackgroundOptions`, and `RegisterPluginsOptions` in `helpers/types.ts`, and pass it from + every call site that already knows the engine (follow the existing services call site). +- Unit tests in `packages/cli/src/kernel/templates/aspire/helpers/tests/` proving: + 1. sqlite ⇒ `--allow-ffi` appears **exactly once** in each of the four generated outputs (never + duplicated when the entry already declares it); + 2. **non-sqlite output is unchanged** — this is risk R-1 and is the most important test. Assert + the postgres/mysql/mssql/none outputs are byte-identical to today's. + +**Out of scope — do not touch** + +- `packages/cli/e2e/**` (that is S2–S5). +- `.github/**` (that is S6). +- `SCAFFOLD_DEFAULTS.CACHE_BACKEND`, `buildCacheBlock`, `ensureSharedCache`, or anything cache + related. +- The doctrine `Restructure` work on `@netscript/cli` (`pipeline.ts`, `official-plugin-copier.ts`) — + do not grow those files. +- Regenerating `embedded.generated.ts` unless the change genuinely requires it; if it does, say so + explicitly in the PR comment. + +## Doctrine constraints + +- Generators stay **pure string builders** (A11). The helper takes values and returns values; no IO, + no `Deno.env`, no process probing. +- The branch keys off `databaseEngine === 'Sqlite'` — an existing domain value from + `DatabaseEntry['Engine']`. **No hardcoded plugin names, no `kind === '…'` host-side coupling.** +- **No `any`, no `as unknown as`, no new `// deno-lint-ignore`.** Adding a lint-ignore to green a + wrapper is a review-blocking finding, not a pass. +- Finite vocabularies stay constants with derived unions. + +## Gates for this slice (all required, evidence goes in the PR comment) + +```bash +deno test packages/cli/src/kernel/templates/aspire/helpers/tests/ +deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx +deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx +deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx +deno task quality:scan +deno task arch:check +``` + +`quality:scan` and `arch:check` are **mandatory** — this slice touches `packages/**`. A green scoped +wrapper alone is not a verdict (that is the hole that let #745 merge). + +Do **not** run `deno task e2e:cli` for this slice. The expensive runtime suite runs at +merge-readiness, not per slice. + +## Concept of Done + +- Every new file is reachable from the public surface or a test. +- The helper is used by all four generators — no copy-pasted second implementation. +- Public functions carry a JSDoc one-liner (what it does, not how). +- Non-sqlite generated output is provably unchanged. +- The slice's gates pass. + +## Commit / push / comment + +1. Inspect `rtk git status --short` first; do **not** fold in unrelated changes. +2. Commit with a message naming **what the slice proves**, not what it contains. Suggested: + + ``` + fix(cli): sqlite --allow-ffi reaches apps, background processors, and plugins + + #1191 fixed the missing FFI permission for generated services only. Apps, + background processors, and plugin services never received databaseEngine, so + a sqlite-backed scaffold started them without --allow-ffi and they exited 1. + One shared permission helper now serves all four register generators. + + Refs #1158 + ``` + +3. Push the branch. +4. Comment on **PR #1220** with: slice scope, commit hash, and the raw result of every gate above. +5. Update `.llm/runs/test-e2e-sqlite-runtime-tier--1158/worklog.md` (Progress Log + Gate Results) + and `context-pack.md` **in the same slice** — a slice whose commit does not touch the run dir is + incomplete. Append `drift.md` if reality diverges from the plan. + +## Stop conditions — do not improvise + +- If threading `databaseEngine` into a generator requires a call site that genuinely does not know + the engine, **stop and record it in `drift.md`**, then report. Do not invent a lookup or reach for + global state. +- If a non-sqlite output changes, **stop**. That is R-1 materialising and needs the supervisor. +- Do not self-certify. After your gates pass, the Tier-A supervisor performs the slice review and + makes the sign-off. Report back with your evidence; do not proceed to S2. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/leak-report.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/leak-report.md new file mode 100644 index 0000000000..73f910125f --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/leak-report.md @@ -0,0 +1,12 @@ +# Run resource leak report + +Generated: 2026-08-04T15:20:10.805Z Worktree: `/home/codex/repos/ns-1158` Aspire probe: ok Docker +probe: ok + +## container: postgres-89449635 (97b90646098858f6cfe163b470fb9d57ff7033d5661f6fe2390c9300ff1ebaec) + +- Ownership: `foreign` +- Apparent owner: `/home/codex/repos/wave5-deepseek` +- Age: 19229411 ms +- Stale: true +- User command: `docker rm -f '97b90646098858f6cfe163b470fb9d57ff7033d5661f6fe2390c9300ff1ebaec'` diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan-eval.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan-eval.md new file mode 100644 index 0000000000..28b756a6da --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan-eval.md @@ -0,0 +1,106 @@ +# PLAN-EVAL — test-e2e-sqlite-runtime-tier--1158 + +- Plan evaluator session: `claude-openrouter` / `qwen/qwen3.7-max` +- Run: `test-e2e-sqlite-runtime-tier--1158` +- Surface / archetype: `packages/cli` (incl. `packages/cli/e2e`, `.github/`) / Archetype 6 (CLI / + Tooling) +- Scope overlays: `SCOPE-service.md` (S1 changes Aspire service/background/app resource + registration; S7 exercises a live AppHost) + +## Checklist results + +| Plan-Gate item | Result | Evidence / location | +| --------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Research present and current | PASS | `research.md` exists with 18 findings. Carried-in draft was re-baselined against `main @ c6f243da` with 5 corrections documented in a table (3 plan-shaping, 1 new blocker, 1 framing). The re-baseline is genuine: the generator re-derived every claim against the tree, not just the draft's stated blocker. Drift D-1, D-2, D-3, D-4 recorded in `drift.md` with severity and action. | +| Decisions locked | PASS | 10 decisions (D0-D9) in `plan.md § Locked Decisions`, each with rationale citing specific findings. D0-D4 correct the draft's root-cause analysis (garnet is not the blocker, redis is; `--allow-ffi` is services-only; `--cache-backend deno-kv` emits `Mode: 'External'` not `Local`). D5-D9 cover per-suite defaults, CI classifier, merge-readiness, provider-sensitive gates, Docker cleanup, and the harness-only-at-bootstrap constraint. All decisions are stated with rationale and cite code locations. | +| Open-decision sweep | PASS | 6 decisions listed in `plan.md § Open-Decision Sweep`. 3 marked "must resolve now" — each resolved **within the slice that depends on it** with pre-agreed fallback: (1) exact `--cache false` vs `--no-cache` spelling resolved in S2 with 2-line additive fallback; (2) Garnet executable arm on `ubuntu-latest` resolved in S7 with documented downgrade; (3) postgres-shaped behavior gates resolved in S7 with per-gate evidence. 3 marked "safe to defer" with rationale. No open decision would force rework if deferred. | +| Commit slices (< 30, gate + files each) | PASS | 7 slices (S1-S7) in `worklog.md § Commit Slices`, each naming what it proves, the proving gate, and files touched. Slice count is 7 (< 30 target). Order is a strict dependency chain: S1 unblocks runtime path (hard prerequisite), S2-S3 build seams, S4 assembles suite, S5 makes teardown safe, S6 wires CI, S7 proves it live. S1 is correctly placed first because without it the sqlite tier fails at Aspire start for every non-service resource (finding 8). | +| Risk register | PASS | 9 risks (R-1 to R-9) in `plan.md § Risk Register`, each with mitigation. Risks cover: S1's cross-scaffold permission change (R-1), Cliffy `--cache false` acceptance (R-2), Garnet dotnet-tool restore (R-3), postgres-shaped behavior gates (R-4), `PrimaryCache` ordering (R-5), `ci:skip-e2e` regression (R-6), draft-PR CI gap (R-7), concurrency contention (R-8), doctrine restructure creep (R-9). Mitigations are concrete: unit tests, documented downgrades, separate concurrency groups, explicit non-scope. | +| Gate set selected | PASS | `plan.md § Fitness Gates` selects F-1 (lint), F-3 (arch:check), F-5 (publish:dry-run + jsr-audit), F-6 (publish:dry-run), F-9 (permission declaration), F-10 (per-slice unit tests), F-19 (scoped wrappers only), quality:scan. Matches `archetype-gate-matrix.md` for Archetype 6. Validation plan lists 11 ordered gates with expected evidence. Scope overlay gates (contract check, service check, runtime health, trace/log review, consumer check) are addressed by S1's unit tests, S7's live run, and the per-slice gate table. | +| Deferred scope explicit | PASS | 5 items in `plan.md § Non-Scope` and `worklog.md § Deferred Scope`: merge-readiness flip (D6), `SCAFFOLD_DEFAULTS.CACHE_BACKEND` change (follow-up issue), `Mode: 'Local'` emission (debt entry), `--cache-backend` axis (superseded by D3), `env` on `CommandExecutor` port (not needed per finding 13). Each has rationale and owner (follow-up issue or debt entry). Arch-debt implications table lists 2 new debt entries to create at Close. | +| jsr-audit surface scan (pkg/plugin) | PASS | `research.md § jsr-audit surface scan` is present. Surface scanned: `packages/cli` public surface (`mod.ts`, `testing.ts`) and `packages/cli/e2e` internal surface. Verdict: **no slow-type / surface risks introduced**. Every planned export is a literal-typed constant object with a derived union (`SCAFFOLD.RUNTIME_SQLITE`, `SCAFFOLD_TITLE.RUNTIME_SQLITE`) or an added `readonly` field on an existing interface (`RunOptions.cache`). S1's generator change alters emitted **strings**, not types. `packages/cli/e2e/**` is not part of the published JSR surface. Required at gate time: `deno task publish:dry-run` + jsr-audit rubric on `packages/cli`. | + +## Open-decision sweep (evaluator-run) + +I ran the open-decision sweep myself. The plan lists 3 "must resolve now" decisions and claims each +is resolved within the slice that depends on it. I verified: + +1. **Exact `--cache false` vs `--no-cache` spelling** — resolved in S2 with a pre-agreed fallback + (add `--no-cache` negation to `init-command.ts` if neither works). The fallback is 2-line, + additive, and does not change defaults. S2 cannot land without a passing `scaffold.init` on the + sqlite suite, so the resolution is forced before commit. **No rework if deferred** — the fallback + is decided in advance. + +2. **Garnet executable arm on `ubuntu-latest`** — resolved in S7 with a documented downgrade: drop + `NETSCRIPT_CACHE_MODE=Executable` and accept ambient-Docker garnet (still no postgres, no redis). + S7 proves it locally before S6 pins the env var in CI. **No rework if deferred** — the downgrade + is pre-agreed and recorded in R-3. + +3. **Postgres-shaped behavior gates** — resolved in S7 as a full local run with per-gate evidence. + Fixes land inside S7 rather than as a later slice. **No rework if deferred** — S7 is the evidence + gate, and any fix is scoped to the sqlite suite's gate list (not deleted from the postgres + suite). + +I found **no open decision the plan did not flag that would force rework if deferred**. The three +"must resolve now" items are each resolved with pre-agreed fallbacks inside the dependent slice, so +deferral does not trigger rework. The "safe to defer" items are correctly classified: they are +product-default changes or latent-feature questions that do not affect the sqlite tier's viability. + +## Spot-check results + +I spot-checked the three most load-bearing findings against the tree at `main @ c6f243da`: + +1. **Finding 4 — `Mode: 'Auto'` garnet has a Docker-less `dotnet tool run garnet-server` arm + selected by `shouldUseContainerCache()` / `NETSCRIPT_CACHE_MODE`**: + - `generate-register-infrastructure.ts:202` emits `if (shouldUseContainerCache()) {` for + `Mode: 'Auto'` entries. + - `_aspire-compat.mts` (embedded in `embedded.generated.ts`) defines `shouldUseContainerCache()` + which honors `process.env.NETSCRIPT_CACHE_MODE` (`Container` → true, `Executable` → false) and + otherwise probes `docker info`. + - Line 356 emits `builder.addExecutable('${name}', 'dotnet', ...)` with + `['tool', 'run', 'garnet-server', '--port', '${CACHE_DEFAULT_PORT}']`. + - The resource is named `garnet` in both arms (the `${name}` comes from the cache entry key, + which is `'garnet'` per `ensureSharedCache` in `workspace-mutator.ts:563-591`), so + `runtime.wait.garnet` passes without Docker. + - **CONFIRMED**: the draft's stated blocker was wrong; the garnet wait is not a blocker. + +2. **Finding 5 — `netscript init` defaults to cache backend `redis` with `Mode: 'Container'` and no + fallback arm**: + - `scaffold-defaults.ts:12` has `CACHE_BACKEND: 'redis' as const`. + - `generate-appsettings.ts:233-241` emits + `{ Engine: 'Redis', Mode: 'Container', DataPath: '.data/redis' }` for `case 'redis'`. + - `init-interactive.ts:51-55` prompts with `defaultValue: SCAFFOLD_DEFAULTS.CACHE_BACKEND` + (redis). + - `validate-init.ts:156` falls back to `SCAFFOLD_DEFAULTS.CACHE_BACKEND` when + `options.cacheBackend` is undefined. + - **CONFIRMED**: the real cache Docker cost is `redis`, not `garnet`. Init's default is a hard + Docker container with no Auto/Executable fallback. + +3. **Finding 8 — #1191's sqlite `--allow-ffi` fix exists **only** in + `generate-register-services.ts`**: + - `grep -rn "allow-ffi" packages/cli/src/kernel/templates/aspire/helpers/register/` returns only + `generate-register-services.ts:35-36`. + - `types.ts:69` shows `databaseEngine?: DatabaseEntry['Engine']` only on + `RegisterServicesOptions`. + - `RegisterAppsOptions`, `RegisterBackgroundOptions`, `RegisterPluginsOptions` do not carry + `databaseEngine`. + - `generate-register-apps.ts`, `generate-register-background.ts`, `generate-register-plugins.ts` + emit `resolvePermissions(...)` with no sqlite branch. + - **CONFIRMED**: apps, background processors, and plugin services get no `--allow-ffi` on sqlite + → they exit 1 at startup. S1 is a hard prerequisite. + +All three load-bearing findings are correct at file:line. The plan's D0/D2/D3/D4 are sound. + +## Verdict + +`PASS` + +## Notes + +The generator did genuine re-baseline work. The carried-in draft's root-cause analysis was wrong on +two plan-shaping counts (garnet is not the blocker, redis is; `--cache-backend deno-kv` emits +`Mode: 'External'` not `Local`), and the generator corrected both with code evidence. The new +blocker (finding 8, `--allow-ffi` services-only) is correctly identified and placed first as S1. The +plan is additive, narrowly scoped, and does not touch the doctrine `Restructure` work on +`@netscript/cli`. The open-decision sweep is thorough and each "must resolve now" item has a +pre-agreed fallback that prevents rework. Implementation may begin. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan.md new file mode 100644 index 0000000000..c4209a2f9b --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/plan.md @@ -0,0 +1,201 @@ +# Plan: sqlite-backed E2E runtime tier (#1158) + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------------------------- | +| Run ID | `test-e2e-sqlite-runtime-tier--1158` | +| Branch | `test/e2e-sqlite-runtime-tier-1158` | +| Phase | `plan` | +| Target | `packages/cli` (+ `packages/cli/e2e`, `.github/`) | +| Archetype | `6 - CLI / Tooling` | +| Scope overlays | `SCOPE-service.md` | + +## Archetype + +**Archetype 6 (CLI / Tooling)** — the subject is `@netscript/cli`, which ships a binary and command +flows; the E2E harness under `packages/cli/e2e` is part of that package. Archetype 2 concerns (the +Aspire register-generators are an adapter over an external orchestrator) are folded inside, per "if +two archetypes apply, choose the larger one." `SCOPE-service.md` applies because S1 changes how +Aspire service/background/app resources are registered and S7 exercises a live AppHost. + +## Current Doctrine Verdict + +`@netscript/cli` — Archetype 6, 38,436 LOC, **Restructure**. Headline action: split `pipeline.ts` +(1,869) and `official-plugin-copier.ts` (1,203); apply the Archetype-6 layout +(`docs/architecture/doctrine/10-codebase-verdict-and-handoff.md:45`). **Not in scope here** — this +run adds a test tier and a narrowly-scoped generator fix; it must not grow those files. + +## Axioms in Play + +| Axiom | Why it matters | +| ----- | ----------------------------------------------------------------------------------------------------------------------------- | +| `A7` | IO stays at the runtime edge. The `NETSCRIPT_CACHE_MODE` / `docker info` probe lives in `_aspire-compat.mts`, not generators. | +| `A11` | Generators are pure string builders. S1 adds a permission branch to generators — it must stay pure and table-driven. | +| `A1` | Finite domain vocabularies are constants with derived unions — the new suite id and cache axis follow `SCAFFOLD`/`DATABASE`. | + +## Goal + +Add an **additive** `scaffold.runtime.sqlite` E2E suite that proves the full generated-project +runtime path (scaffold → plugins → DB init/generate/seed → Aspire start → behavior gates) with **no +Docker containers at all**, and wire it into CI as a cheap tier — while `scaffold.runtime` remains +the postgres-backed merge-readiness bar, unchanged. + +## Scope + +- `packages/cli/src/kernel/templates/aspire/helpers/register/**` — extend the #1191 sqlite + `--allow-ffi` permission fix from services to apps, background processors, and plugin services. +- `packages/cli/e2e/src/domain/**` — `RunOptions.cache`, `SCAFFOLD.RUNTIME_SQLITE`, + `SCAFFOLD_TITLE.RUNTIME_SQLITE`. +- `packages/cli/e2e/src/presentation/cli/options/run-options.ts` — `--cache` / `--no-cache` parse. +- `packages/cli/e2e/src/application/gates/scaffold/scaffold-gates.ts` — `scaffold.init` forwards the + cache decision. +- `packages/cli/e2e/suites/scaffold/capability-suites.ts` — per-suite `defaults`, new suite entry. +- `packages/cli/e2e/src/adapters/commands/docker-resource-cleaner.ts` — tolerate absent Docker. +- `.github/workflows/e2e-cli.yml`, `.github/scripts/ci-classify-changes.ts` — new + `scaffold-runtime-sqlite` job + `run_runtime_sqlite` classifier output + lane-visibility. + +## Non-Scope + +- **No merge-readiness flip.** `scaffold.runtime`, bare `deno task e2e:cli`, and `full` stay + postgres. (Owner-ratified constraint 7; issue #1158 "Constraints".) +- **No new `ci:*` labels.** The frozen three stay frozen (constraint 4). +- **No change to the default init cache backend.** `SCAFFOLD_DEFAULTS.CACHE_BACKEND` stays `redis`; + changing it would alter every user's scaffold. The E2E opts out per-suite instead. +- **No `--cache-backend` axis on the E2E runner.** Superseded — see D3. +- **No garnet wait filtering.** Superseded — see D2. +- No doctrine `Restructure` work on `@netscript/cli` (pipeline.ts / official-plugin-copier.ts). + +## Hidden Scope + +Found during research; each is real work the carried-in draft did not name: + +1. **The `--allow-ffi` gap (blocker).** #1191 fixed services only. Apps, background processors, and + plugin services never receive `databaseEngine` and never get `--allow-ffi`. On sqlite they exit 1 + at startup — the tier cannot go green until S1 lands. (`research.md` finding 8.) +2. **No per-suite default options exist.** `ScaffoldCapabilitySuite` is `{id,title,gates}`; + `createScaffoldCapabilitySuite` honours `overrides.database` only when truthy. A new suite id + alone still runs postgres. Needs a `defaults` field merged **under** caller overrides, so + `--db postgres` on the sqlite suite still wins. (finding 9.) +3. **The cache Docker cost is `redis`, not `garnet`.** Init's default backend is `redis` with + `Mode: 'Container'` and no Docker-less arm; garnet arrives later from plugin-add with + `Mode: 'Auto'`, which already has one. (findings 3–5.) +4. **`ci:skip-e2e` semantics.** It sets only `run_runtime=false`. If the sqlite job keys off + `run_static`, `ci:skip-e2e` would stop skipping the runtime family. Resolved by E5 without new + labels. +5. **Docker cleanup has two intolerant paths**, not one: non-zero `docker ps` _and_ a missing + `docker` binary (`Deno.Command` rejects `NotFound`). (finding 12.) +6. **`lane-visibility`** must list the new job in `needs:` and in its summary table or CI will not + surface it. (finding 14.) +7. **Draft PRs run no CI (#1212).** The new job cannot be proven from the draft PR; S7's local run + is the evidence, and the CI proof arrives on `ready_for_review`. (finding 16.) + +## Locked Decisions + +| ID | Decision | Rationale | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `D0` | **S1 first: extend the #1191 sqlite `--allow-ffi` fix to the permission-bearing generators — background processors and plugin services (services already have it).** Thread `databaseEngine` into those generators and reuse one shared permission helper. **Amended 2026-08-04 (drift D-5): apps are excluded** — `generate-register-apps.ts` launches apps via `deno task `, which owns no Deno permission list, and the generated `dev` task is already `deno run --allow-all`. | Without it the sqlite tier fails at Aspire start for background processors and plugin services. Same defect class as #1191, same fix shape. Blocker, not optional. (finding 8, amended by drift D-5) | +| `D1` | New suite id **`scaffold.runtime.sqlite`** (`SCAFFOLD.RUNTIME_SQLITE`), additive. Default `scaffold.runtime`, bare `e2e:cli`, and `full` are untouched. | Owner-ratified constraint 1. Additive tiers are how #1155 scoped expensive jobs without changing the merge bar. | +| `D2` | **Reduced-container profile = `--db sqlite` + cache disabled at init; leave `NETSCRIPT_CACHE_MODE` unset.** `runtime.wait.garnet` stays in the gate list unfiltered and the ambient Docker-capable arm supplies Garnet. | **S7 downgrade after R-3 resolved negatively.** sqlite emits no DB resource and disabling init's cache kills the `redis` container, but the executable Garnet arm showed inconsistent cross-process KV/queue visibility. The tier therefore accepts one Garnet container while still eliminating Postgres and Redis. (findings 3–5; drift D-14) | +| `D3` | **No `CACHE_BACKEND` axis.** Add a boolean `RunOptions.cache` (default `true`) plus `--cache` / `--no-cache` on the E2E runner; `scaffold.init` forwards it. | **Corrects draft D3.** `--cache-backend deno-kv` emits `Mode: 'External'` (an `addConnectionString` resource), not `Local`, and plugin-add re-adds garnet regardless — so the axis buys nothing the boolean does not. (finding 6) | +| `D4` | **Runtime waits stay as they are.** DB waits remain engine-filtered (already correct); the garnet wait is _not_ filtered. | **Corrects draft D4.** The garnet resource exists in both arms under the same name; filtering it would weaken the sqlite tier for no benefit and would desync `runtimeGateIds` from `runtimeResources`. | +| `D5` | **Per-suite defaults seam.** `ScaffoldCapabilitySuite` gains `readonly defaults?: Partial`, merged as `{...capability.defaults, ...overrides}` at the top of `createScaffoldCapabilitySuite`. | The only way a suite id can pin sqlite while `--db` on the command line still wins. Caller precedence preserved. (finding 9) | +| `E5` | **CI: new job `scaffold-runtime-sqlite`, gated on a new classifier output `run_runtime_sqlite = ci:full ? true : (run_static && !ci:skip-e2e)`.** No new labels. Skipped-by-policy + FAIL-CLOSED pattern copied from the existing jobs; `lane-visibility` gains the job in `needs:` and in its table. | Keeps the frozen three labels (constraint 4) while giving `ci:skip-e2e` authority over **both** runtime tiers. A classifier **output** is not a label; #1155 established the capability vector as the place this logic lives. Kept as `E5` to avoid colliding with #1152's `D5`. | +| `D6` | **Merge-readiness stays postgres.** No change to `full-command.ts` or the default `database: DATABASE.POSTGRES`. | Owner-ratified constraint 7; issue #1158 constraints. postgres wiring must still be proven where it can break. | +| `D7` | **Provider-sensitive gates take `database` and are verified, not assumed.** The sqlite suite excludes only `behavior.service-health`; every other runtime/behavior gate remains shared with `scaffold.runtime`. | S7 proved the users-service health implementation is product-provider-shaped: its tagged Prisma raw query fails under libSQL despite successful sqlite init/generate/seed. The Postgres merge-readiness suite retains the assertion; fixing the service health adapter is outside this tier. (drift D-15) | +| `D8` | **Docker cleanup tolerates a missing Docker on both paths** — absent binary (`NotFound`) and non-zero `docker ps` — returning an empty snapshot with a warning. Removal failures for containers the run _did_ create still throw. | Constraint 6. The postgres tier keeps strict cleanup because it always has containers; the sqlite tier must not fail on an empty/absent Docker. (finding 12) | +| `D9` | **Harness-only at bootstrap.** No product code before PLAN-EVAL `PASS`. | Constraint 8; `run-loop.md` § 4 hard stop. | + +## Open-Decision Sweep + +| Decision | Status | Notes | +| -------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Exact init spelling for disabling the cache (`--cache false` vs `--no-cache`) | **must resolve now** | Resolved as: S2 verifies against the real binary and wires whichever the CLI accepts; if neither works, S2 adds the `--no-cache` negation to `init-command.ts` (2-line, additive). Either way S2 cannot land without a passing `scaffold.init` on the sqlite suite. | +| Whether the Garnet **executable** arm starts and preserves cross-process runtime state | **must resolve now** | **Resolved negatively in S7.** The executable stayed healthy, but repeated runs inconsistently lost registered jobs or accepted a trigger without exposing an execution. The pre-agreed downgrade drops the env pin and accepts the ambient-Docker Garnet container (still no Postgres, no Redis). | +| Whether any behavior gate asserts postgres-shaped output | **must resolve now** | **Resolved in S7.** Only `behavior.service-health` is provider-shaped: the generated service's tagged Prisma raw query is rejected by libSQL. It is excluded only from the sqlite capability; all other behavior assertions remain unchanged and `scaffold.runtime` retains the gate. | +| Whether `SCAFFOLD_DEFAULTS.CACHE_BACKEND` should become `deno-kv` | safe to defer | A product-default change affecting every scaffold. Out of scope; file as a follow-up issue if the sqlite tier shows the redis container is dead weight for users too. | +| Whether `Mode: 'Local'` should ever be emitted by the scaffold | safe to defer | The generator arm exists but is unreachable (finding 6). Dead-code/latent-feature question for `@netscript/cli`, not for this tier. | +| Promoting the sqlite tier to merge-readiness | safe to defer | Explicitly out of scope per D6; revisit only after the tier has green history. | + +> No open decision would force rework if deferred: the three "must resolve now" items are each +> resolved **within the slice that depends on them**, with the fallback pre-agreed. + +## Risk Register + +| Risk | Mitigation | +| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `R-1` S1 changes emitted permissions for **every** scaffold, not just sqlite. | The branch is `databaseEngine === 'Sqlite'` only, mirroring the existing services helper. Unit tests assert non-sqlite output is byte-identical, plus `quality:scan` + `arch:check`. | +| `R-2` `--cache false` is not accepted by Cliffy's `--cache [enabled:boolean]`. | S2 verifies against the binary first; fallback is a declared `--no-cache` negation in `init-command.ts` (additive, no default change). | +| `R-3` Garnet dotnet-tool executable fails to restore/start on CI (10s best-effort restore). | S7 proves locally; documented downgrade = drop `NETSCRIPT_CACHE_MODE` and accept ambient-Docker garnet. Tier still drops postgres + redis. | +| `R-4` A behavior gate is silently postgres-shaped and fails on sqlite. | S7 is a full local run with per-gate evidence; fixes land in S7. If a gate is genuinely postgres-only it is excluded from the sqlite gate list with a recorded rationale (not deleted). | +| `R-5` Disabling init's cache leaves `PrimaryCache` unset until plugin-add runs, breaking an early gate. | `ensureSharedCache` sets `PrimaryCache ??= 'garnet'` during plugin install, which precedes every runtime gate in `RUNTIME_GATES`. S7 confirms ordering. | +| `R-6` `ci:skip-e2e` regression — the new job runs when the label says skip. | E5 folds `!skipE2e` into the classifier output, with classifier unit tests for the `ci:full` / `ci:skip-e2e` / docs-only matrices (the #1155 test shape). | +| `R-7` The new job cannot be validated on the draft PR (#1212). | Local S7 evidence is the gate; CI proof is captured on `ready_for_review` and recorded in the PR comment trail before merge. | +| `R-8` Two runtime tiers double the `e2e-scaffold-runtime-global` concurrency contention. | The sqlite job gets its **own** concurrency group (`e2e-scaffold-runtime-sqlite-global`), so it never queues behind the postgres tier. | +| `R-9` Slice count / scope creep into the doctrine `Restructure` work. | Non-scope is explicit; S1 adds a helper without growing `pipeline.ts` or `official-plugin-copier.ts`. | + +## Anti-Patterns to Resolve or Avoid + +| AP | Status | Plan | +| ----------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Host-side hardcoded plugin/provider names | risk | S1's branch keys off `databaseEngine === 'Sqlite'`, an existing domain value from `DatabaseEntry['Engine']`, not a plugin name. `quality:scan` enforces. | +| `any` + manual casting | risk | New code is literal-typed constants and `readonly` interface fields. No `as unknown as`, no new `deno-lint-ignore`. A new ignore is a review-blocking finding. | +| Duplicated finite vocabulary | new | The sqlite suite id, title, and cache axis are constants with derived unions — no string literals at call sites. | +| Two sites drifting apart | risk | `runtimeGateIds` and `runtimeResources` stay in sync for engine waits; the separate sqlite capability filter is pinned to exactly `behavior.service-health`. | + +## Fitness Gates + +| Gate | Required | Expected evidence | +| ------- | -------- | ----------------------------------------------------------------------------------------- | +| `F-1` | yes | `.llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | +| `F-3` | yes | `deno task arch:check` | +| `F-5` | yes | `deno task publish:dry-run` + jsr-audit rubric on `packages/cli` (S1 touches `src/**`) | +| `F-6` | yes | `deno task publish:dry-run` | +| `F-9` | yes | S1 **is** a permission-declaration change — unit tests assert the emitted permission sets | +| `F-10` | yes | Per-slice unit tests named in the slice table | +| `F-19` | yes | Scoped wrappers only (`run-deno-check/lint/fmt.ts`), never raw root `deno fmt --check` | +| quality | yes | `deno task quality:scan` — mandatory for any slice touching `packages/**` | + +## Arch-Debt Implications + +| Entry | Action | Notes | +| ------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------ | +| `@netscript/cli` Restructure verdict | none | Untouched by this run; no new debt added to it. | +| Unreachable `Mode: 'Local'` cache arm in the generator | create | Latent generator branch nothing emits (finding 6). Record as a debt entry at Close unless S7 finds a use for it. | +| `SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'` forces a container on every scaffold | create | Not this issue's call to change, but the sqlite tier makes the cost visible. Record at Close and file a follow-up issue. | + +## Validation Plan + +| Order | Gate | Command or check | Expected result | +| ----- | ------------------ | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | PASS | +| 2 | lint | `.llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | PASS | +| 3 | format | `.llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | PASS | +| 4 | code quality | `deno task quality:scan` | PASS (no `any`+cast, no hardcoded plugin names) | +| 5 | doctrine fitness | `deno task arch:check` | PASS | +| 6 | unit | `deno test packages/cli/src/kernel/templates/aspire/helpers/tests/` (S1) | PASS, incl. non-sqlite byte-identical assertions | +| 7 | unit | `deno test packages/cli/e2e/` (S2–S5) | PASS | +| 8 | classifier unit | `deno test .github/scripts/` (S6) | PASS, incl. `ci:full` / `ci:skip-e2e` matrices | +| 9 | **new tier, live** | `deno task e2e:cli run scaffold.runtime.sqlite --cleanup --format pretty` (S7) | PASS with postgres and redis eliminated and a **net-zero** container delta (one garnet container created then removed by cleanup — amended after R-3, drift D-14) | +| 10 | regression | `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` | PASS — the postgres merge bar is unchanged | +| 11 | publishability | `deno task publish:dry-run` | PASS | + +> Order 10 is the expensive existing gate. Per `AGENTS.md` it runs once, at merge-readiness — not +> per slice. + +## Dependencies + +- #1191 (closed) — sqlite `--allow-ffi` for services; S1 extends it. +- PR #1155 (merged) — the classifier capability vector S6 extends. +- PR #1212 (merged) — draft-PR CI guards S6 must preserve. +- Aspire CLI 13.4.x + .NET 10 on the runner (already installed by the existing runtime job). +- Ambient Docker-capable Garnet arm; the executable experiment was retired by D-14. + +## Drift Watch + +- `SCAFFOLD_DEFAULTS.CACHE_BACKEND` — if it changes, D2/D3 must be re-derived. +- `ensureSharedCache`'s hardcoded `'garnet'` key — if it becomes configurable, D2 changes. +- `shouldUseContainerCache()` / `NETSCRIPT_CACHE_MODE` — sqlite must retain the ambient + container-backed arm unless D-14 is revisited with new cross-process evidence. +- `runtimeGateIds` / `runtimeResources` — the two must stay in agreement. +- `ci-classify-changes.ts` outputs — S6 adds one; #1155's tests must stay green. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/research.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/research.md new file mode 100644 index 0000000000..49eb5eda07 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/research.md @@ -0,0 +1,86 @@ +# Research — test-e2e-sqlite-runtime-tier--1158 + +Issue: **#1158** — +`e2e(cli): sqlite-backed runtime tier so docker+postgres is reserved for +postgres-specific verification`. + +## Re-baseline + +- **Carried-in source:** a proposal pasted into chat by a GitHub Copilot cloud coding agent (Grok + 4.5). That run produced **nothing on disk** — every mutating tool was denied with + `Denied by preToolUse hook from "repo settings" (hook errored)`, and its stated cwd was the + Actions runner path `/home/runner/work/netscript/netscript`. Its findings were never verified + against a checkout. Reproduced verbatim in `.llm/tmp/BRIEF-1158.md` § "Carried-in draft". +- **Re-derived against `main` @ `c6f243da` (2026-08-04)**, in worktree `/home/codex/repos/ns-1158`. +- **What changed vs the carried-in version** — five corrections, three of them plan-shaping: + + | # | Carried-in claim | Verified reality | Impact | + | - | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | + | 1 | "The actual blocker: the runtime path always waits for garnet." | Waiting for `garnet` is **not** a blocker. The `garnet` resource is `Mode: 'Auto'` and already has a Docker-less arm (`dotnet tool run garnet-server`). The wait passes either way. | **plan-shaping** | + | 2 | "Existing suites keep the current init cache default (garnet)." | The init default is **`redis`** (`SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'`), emitted as `Mode: 'Container'` with **no** Docker-less arm. This — not garnet — is the cache Docker dependency. | **plan-shaping** | + | 3 | D2/D3: no-docker profile = `--cache-backend deno-kv` (Local); add a `CACHE_BACKEND` axis. | `--cache-backend deno-kv` emits `Mode: 'External'` (not `Local`) → `builder.addConnectionString('deno-kv')`, and plugin-add re-adds a `garnet` cache regardless. The axis does not buy the goal. | **plan-shaping** | + | 4 | Draft slices S1–S6 (no product-code prerequisite). | The #1191 `--allow-ffi` fix covers **services only**. Apps, background processors, and plugin services get no `--allow-ffi` on sqlite → they exit 1 at startup. The tier cannot be green without fixing that first. | **new blocker** | + | 5 | E5: sqlite CI job is "aspire + .NET + Deno, no docker service dependency". | `scaffold-runtime` has **no `services:` block today** — Docker is ambient on `ubuntu-latest`, not a declared service. The saving is wall-clock/flakiness, not a runner capability. | framing | + +## Findings + +| # | Finding | How to verify | +| -- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | The E2E already supports `--db sqlite`; DB wait gates are already filtered by engine, and sqlite contributes **no** Aspire DB resource. | `packages/cli/e2e/suites/scaffold/capability-suites.ts:234-244`; `packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts:426-439` (`DATABASE.SQLITE → []`) | +| 2 | `runtimeResources()` unconditionally appends `ASPIRE_RESOURCE.GARNET`, and `runtimeGateIds()` never filters `RUNTIME_WAIT_GARNET`. **Both are correct as-is** — see finding 4. | `runtime-gates.ts:390-405`; `capability-suites.ts:78, 238-243` | +| 3 | The `garnet` resource in a runtime run does **not** come from `netscript init`. Plugin install calls `ensureSharedCache(projectRoot, 'garnet')`, which appends `Cache.garnet = { Enabled: true, Engine: 'Garnet', Mode: 'Auto' }` and sets `PrimaryCache ??= 'garnet'`. | `packages/cli/src/kernel/adapters/plugin/workspace-mutator.ts:563-591` | +| 4 | `Mode: 'Auto'` is resolved **at apphost runtime**: `shouldUseContainerCache()` honours `NETSCRIPT_CACHE_MODE` (`Container` \| `Executable`) and otherwise probes `docker info`. Docker absent (or override `Executable`) → `builder.addExecutable('garnet', 'dotnet', …, ['tool','run','garnet-server', …])`. The resource is named `garnet` in **both** arms, so `runtime.wait.garnet` passes without Docker. | `generate-register-infrastructure.ts:182-212, 340-370`; `shouldUseContainerCache` / `isDockerAvailable` / `ensureGarnetToolManifest` in `packages/cli/src/kernel/assets/embedded.generated.ts` (`_aspire-compat.mts`) | +| 5 | `netscript init` defaults to `cacheBackend: 'redis'` in `--ci`/`--yes` mode, and `buildCacheBlock('redis')` emits `{ Engine: 'Redis', Mode: 'Container' }` — a hard Docker container with **no** Auto/Executable fallback. This is the cache-side Docker cost. | `scaffold-defaults.ts:12`; `init-interactive.ts:22-27, 45-55`; `validate-init.ts:155-156`; `generate-appsettings.ts:229-261` | +| 6 | `buildCacheBlock('deno-kv')` emits `{ Engine: 'DenoKv', Mode: 'External' }`, which generates `builder.addConnectionString('deno-kv')`. The `Mode: 'Local'` arm (no Aspire resource) exists in the generator but **nothing in the scaffold ever emits it**. | `generate-appsettings.ts:251-259`; `generate-register-infrastructure.ts:164-180` | +| 7 | `scaffold.init` passes `--db` and never a cache flag. `netscript init` does expose `--cache [enabled:boolean]` and `--cache-backend `. | `scaffold-gates.ts:25-52`; `packages/cli/src/public/features/init/init-command.ts:80-84` | +| 8 | **#1191's `--allow-ffi` fix is services-only.** `withRequiredServicePermissions` lives in `generate-register-services.ts` and `databaseEngine` is only on `RegisterServicesOptions`. `generate-register-apps.ts`, `generate-register-background.ts`, and `generate-register-plugins.ts` emit `resolvePermissions(...)` with no sqlite branch. | `generate-register-services.ts:32-38`; `helpers/types.ts:69`; `grep -rn "allow-ffi" packages/cli/src/kernel/templates/aspire/helpers/register/` returns only `generate-register-services.ts` | +| 9 | `ScaffoldCapabilitySuite` carries only `{ id, title, gates }`. `createScaffoldCapabilitySuite` applies `overrides.database` only `if (overrides.database)`, and `resolveSuite` merges caller overrides over `suite.defaultOptions`. **A new suite id alone cannot pin sqlite** — there is no per-suite default-options seam. | `capability-suites.ts:13-17, 168-192`; `presentation/cli/suites/registry.ts:20-27, 48-57` | +| 10 | Merge-readiness stays postgres by construction: `full` resolves `SCAFFOLD.RUNTIME` with `--db` default `postgres`; both `defaultRunOptions` factories default `database: DATABASE.POSTGRES`. | `full-command.ts:17-19, 34`; `suite-builder-options.ts:22`; `create-default-runner.ts:60` | +| 11 | `RunOptions` has `database` but no cache field; `extension-axes.ts` has `DATABASE` (incl. `SQLITE`) but no cache axis; `cli-surface.ts` `SCAFFOLD` has `RUNTIME` but no sqlite variant. | `run-context.ts:6-21`; `extension-axes.ts`; `cli-surface.ts:4-21` | +| 12 | Docker cleanup is intolerant on **two** paths, not one: `listContainers()` throws on a non-zero `docker ps`, **and** `new Deno.Command('docker', …).output()` rejects with `NotFound` when the binary is absent. Only the first was in the draft. | `adapters/commands/docker-resource-cleaner.ts:9-30, 32-43` | +| 13 | The E2E command port has **no `env` field** — subprocesses inherit the runner process environment. So `NETSCRIPT_CACHE_MODE` can be set process-wide (CI job `env:` or `Deno.env.set` in the suite) without changing the port contract. | `ports/command-executor.ts:3-25`; `adapters/commands/deno-command-adapter.ts:11-50` | +| 14 | CI today: `classify` emits the #1155 capability vector; `scaffold-static` gates on `run_static`, `scaffold-runtime` on `run_runtime` with the skipped-by-policy + FAIL-CLOSED pattern; `scaffold-runtime` declares **no `services:`** — Docker is ambient. `lane-visibility` `needs:` is `[classify, scaffold-static, scaffold-runtime, desktop-native-linux]` and renders a fixed summary table. | `.github/workflows/e2e-cli.yml:170-188, 223-250, 398-450` | +| 15 | The `ci:*` label set is frozen at three (`ci:full`, `ci:skip-e2e`, `ci:skip-scaffold`); `ci:skip-e2e` sets only `run_runtime=false`, `ci:full` forces every output true. | `.github/labels.yml:150-162`; `.github/scripts/ci-classify-changes.ts:292-360` | +| 16 | Per #1212, **draft PRs run no routine CI at all** — `classify` requires `pull_request.draft == false`, and `lane-visibility` is draft-gated. A new job therefore cannot be proven from a draft PR. | `.github/workflows/e2e-cli.yml:80, 401`; PR #1212 (merged, `Closes #1207`) | +| 17 | #1152 is an **issue**, not a PR; the capability-vector work landed as **PR #1155** ("scope every expensive job to a classifier capability vector"). #1191 is an **issue** (closed 2026-08-03), not a PR. | `gh pr view 1155`; `gh issue view 1158 / 1191` | +| 18 | The DB workflow gates already thread `--db` end to end (`database.init`, `database.generate`, `database.seed`, `database.codegen`), with an offline URL per engine — no postgres-only SQL in the gate layer. | `application/gates/scaffold/database-gates.ts:22-136` | + +### Corrected root-cause statement + +`scaffold.runtime` pays for **three** Docker consumers, and the issue title only names one: + +1. `postgres` container ← `--db postgres`. Avoidable today (`--db sqlite`; waits already filtered). +2. `redis` container ← init's default cache backend, `Mode: 'Container'`, **no** Docker-less arm. +3. `garnet` ← plugin-add's `Mode: 'Auto'`, which **already has** a Docker-less arm. + +So the sqlite tier needs: sqlite DB + **cache disabled at init** (kills #2) + +`NETSCRIPT_CACHE_MODE=Executable` (pins #3 to its executable arm). No new cache-backend axis, no +garnet wait filtering. + +## jsr-audit surface scan + +- Surface scanned: `packages/cli` public surface (`mod.ts`, `packages/cli/testing.ts`) and the + `packages/cli/e2e` internal surface. +- **Slow-type / surface risks: none introduced.** Every planned export is a literal-typed constant + object with a derived union (`SCAFFOLD.RUNTIME_SQLITE`, `SCAFFOLD_TITLE.RUNTIME_SQLITE`) or an + added `readonly` field on an existing interface (`RunOptions.cache`) — the same shape as the + existing `DATABASE`/`SCAFFOLD` axes, all explicitly annotated. `packages/cli/e2e/**` is not part + of the published JSR surface (it is a dev harness under the package root), so the only + publishable-surface delta is S1's generator change, which alters emitted **strings**, not types. +- Required at gate time regardless: `deno task publish:dry-run` + the `jsr-audit` rubric on + `packages/cli`, because S1 touches `packages/cli/src/**`. + +## Open questions + +1. **Does `netscript init` accept `--cache false` (or `--no-cache`) on the command line?** The + option is declared as `--cache [enabled:boolean]`; Cliffy's optional-value form accepts + `--cache=false`, but the auto-generated `--no-cache` negation is not declared. S2 must verify the + exact spelling against the real binary before wiring it into `scaffoldInitCommand`. → risk R-2. +2. **Does the Garnet dotnet-tool executable arm start reliably on `ubuntu-latest`?** + `ensureGarnetToolManifest` does a best-effort `dotnet tool restore` with a 10s timeout; a restore + failure surfaces only when the resource starts. S7 must prove it locally before S6 pins + `NETSCRIPT_CACHE_MODE=Executable` in CI. Fallback recorded in R-3. +3. **Do any behavior gates assert postgres-shaped health output?** `probe-service-health` takes a + `database` argument and matches aggregate health per engine (`runtime-gates.ts:441-480`), so it + appears engine-aware — but the sqlite path has never been exercised end to end. S7 is the first + real evidence. → risk R-4. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/supervisor.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/supervisor.md new file mode 100644 index 0000000000..0d9e47fcd2 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/supervisor.md @@ -0,0 +1,61 @@ +# Supervisor Identity — test-e2e-sqlite-runtime-tier--1158 + +Written at run start per `workflow/lane-policy.md` § Supervisor identity. A run dir without this +file is not activated. Other supervisors cross-peek a run by reading this file — it is how a run's +operating identity is discoverable without chat memory. + +| Field | Value | +| -------- | -------------------------------------------------------------------------------------------- | +| Model | Claude Opus 5 (`claude-opus-5`) | +| Session | `session_016dewo9Vp8tLwjeivARroL3` — https://claude.ai/code/session_016dewo9Vp8tLwjeivARroL3 | +| Host | WSL2 Linux 6.18.33.2-microsoft-standard-WSL2, user `codex` | +| Checkout | `/home/codex/repos/netscript` | +| Worktree | `/home/codex/repos/ns-1158` | +| Branch | `test/e2e-sqlite-runtime-tier-1158` | +| Baseline | `c6f243da` on `main` (2026-08-04) | +| Run ID | `test-e2e-sqlite-runtime-tier--1158` | + +Owner (Eric) is **remote**, steering this session through Remote Control. Reports are kept short and +decision-shaped. + +## Routes in force + +| Task lane | Provider / model / effort | Role in this run | +| ------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------------- | +| `planning_decisions` | Claude · Anthropic · Opus 5 · this session | Supervisor: research, plan, slice review, sign-off | +| `formal_evaluation` | Claude · OpenRouter · `qwen/qwen3.7-max` (`claude-openrouter` → `claude-print`) | PLAN-EVAL (separate session), later IMPL-EVAL | +| `complex_implementation` | Codex · OpenAI · GPT-5.6 Sol · high | S1 (framework `--allow-ffi` fix), S6 (CI policy) | +| `normal_implementation` | Codex · OpenAI · GPT-5.6 Sol · medium | S2–S5, S7 (e2e harness slices) | +| `review_codex_complex` | Claude · Anthropic · Fable 5 · medium | Adversarial review paired to the Sol·high slices | +| `review_codex` | Claude · Anthropic · Fable 5 · low | Adversarial review paired to the Sol·medium slices | + +Reference `.llm/harness/workflow/lane-policy.md`; do not copy its complete route table here. + +## Recorded lane/eval overrides + +- **Owner-authorized adversarial-check lane (2026-08-04).** In addition to the canonical routes, the + owner authorized ad-hoc adversarial verification through the agentic toolchain (`claude-print` / + `opencode`): + - **`qwen/qwen3.7-max`** — quick checks (already the bound open evaluator preset; no change). + - **`x-ai/grok-4.5`** (`codex-long-medium-grok-4-5` preset) — **complex** checks where extra + verification on top of the supervisor _and_ Codex is warranted. + + **Escalation order (owner refinement, 2026-08-04):** reach for a **Claude Opus 5 sub-agent** first + — dispatched _by the supervisor_, in-plan, no OpenRouter spend, and for Codex-authored work it is + the canonical opposite-family reviewer under the `review_codex_*` ladder. Only escalate to the + OpenRouter/OpenCode lanes above when a sub-agent is genuinely not enough. Two constraints survive + the refinement: a sub-agent is **never** the formal PLAN-EVAL/IMPL-EVAL evaluator (that stays the + open-model Qwen preset), and it must be dispatched by the **supervisor** — an implementation lane + dispatching its own reviewer is the D-7 breach, not a review. + + This is an **explicit** owner approval, so it does not violate `lane-policy.md` invariant 4 (no + _implicit_ paid or higher-effort escalation). It does **not** widen the **formal** PLAN-EVAL / + IMPL-EVAL lane, which stays open-models-only on the bound Qwen preset. Owner's framing: "in + principle you two should be enough — I let you judge." Judgement recorded per use in `drift.md`. + +- **Supervisor model is Opus 5, not the canonical Fable 5 `planning_decisions` primary.** The owner + started this session on Opus 5 through Remote Control after a GitHub Copilot cloud agent (Grok + 4.5) failed to produce anything on disk. Authorization: owner directive (session start). Mirrored + in `drift.md` as D-1. +- No other overrides. The evaluator lane, the implementation lane (Tier-D WSL Codex through + `.llm/tools/agentic/`), and the slice review gate are unchanged from `lane-policy.md`. diff --git a/.llm/runs/test-e2e-sqlite-runtime-tier--1158/worklog.md b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/worklog.md new file mode 100644 index 0000000000..526b5f3964 --- /dev/null +++ b/.llm/runs/test-e2e-sqlite-runtime-tier--1158/worklog.md @@ -0,0 +1,963 @@ +# Worklog: sqlite-backed E2E runtime tier (#1158) + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------------ | +| Run ID | `test-e2e-sqlite-runtime-tier--1158` | +| Branch | `test/e2e-sqlite-runtime-tier-1158` | +| Archetype | `6 - CLI / Tooling` | +| Scope overlays | `service` | + +## Design + +Recorded before any implementation file is created, per `workflow/run-loop.md` § 3b. + +### Public Surface + +Framework (`packages/cli` published surface) — S1 only: + +- No new package exports. `generateRegisterServices` / `generateRegisterBackground` / + `generateRegisterPlugins` keep their signatures; `RegisterBackgroundOptions` and + `RegisterPluginsOptions` gain an optional `readonly databaseEngine?: DatabaseEntry['Engine']`, + matching `RegisterServicesOptions`. +- One internal helper, `withDatabasePermissions(permissions, databaseEngine)`, extracted from + `generate-register-services.ts` into the shared `register/` module and reused by the three + permission-bearing generators. Not exported from the package. Apps remain unchanged per D-5. + +E2E harness (`packages/cli/e2e`, internal to the package, not a JSR surface): + +- `SCAFFOLD.RUNTIME_SQLITE` — new suite id `'scaffold.runtime.sqlite'`. +- `SCAFFOLD_TITLE.RUNTIME_SQLITE` — + `'Runtime scaffold capability smoke (sqlite, reduced containers)'`. +- `RunOptions.cache: boolean` — new run axis. +- `ScaffoldCapabilitySuite.defaults?: Partial` — per-suite default options. +- CLI: `--cache` / `--no-cache` on `run` and `full`. + +### Domain Vocabulary + +- `ScaffoldCapabilitySuite` — gains `defaults?: Partial`; the suite-level baseline that + caller overrides win over. +- `RunOptions.cache: boolean` — "scaffold a shared cache resource at `netscript init`". Boolean, not + an enum: the only decision the E2E needs is _whether init creates its own container-backed cache_. + The backend itself stays a product concern (D3). +- `DatabaseEntry['Engine']` — existing product vocabulary reused by S1; no new engine type. +- `CacheWiring` / `Mode: 'Auto' | 'Container' | 'Executable' | 'External' | 'Local'` — existing + generated-apphost vocabulary; this run consumes it, it does not extend it. + +### Ports + +None created. The existing `CommandExecutor` port is deliberately **not** extended with an `env` +field. S7 resolved the executable Garnet experiment negatively (D-14), so the sqlite suite and CI +job leave `NETSCRIPT_CACHE_MODE` unset and use the existing ambient Docker-capable arm. Adding an +`env` seam would be a speculative port for a need this run does not have. + +`DockerResourceCleaner` (existing port) keeps its contract; only the Deno adapter becomes tolerant. + +### Constants + +- `SCAFFOLD.RUNTIME_SQLITE = 'scaffold.runtime.sqlite'` (`e2e/src/domain/cli-surface.ts`) +- `SCAFFOLD_TITLE.RUNTIME_SQLITE = 'Runtime scaffold capability smoke (sqlite, reduced containers)'` +- `EXPENSIVE_RUNTIME_SUITE_IDS` — the shared tuple containing both runtime tiers, with the + `ExpensiveRuntimeSuiteId` union derived from it. +- Gate ids: **none added**. The sqlite suite derives its list from `RUNTIME_GATES`, excluding only + `behavior.service-health` because the generated service's tagged Prisma raw query is not supported + by libSQL (D-15). `scaffold.runtime` retains the complete list unchanged. + +### Commit Slices + +| # | Slice | Gate | Files | +| - | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | **`--allow-ffi` reaches every permission-bearing sqlite resource, not just services** — proves the #1191 fix is no longer services-only. | `deno test packages/cli/src/kernel/templates/aspire/helpers/tests/` + scoped wrappers + `quality:scan` + `arch:check` | `register/{database-permissions,generate-register-{background,plugins,services}}.ts`, `helpers/types.ts`, the pipeline call site, `helpers/tests/*` | +| 2 | **The E2E can scaffold a project with no cache resource** — `RunOptions.cache` + `--cache/--no-cache` + `scaffold.init` forwards it; omitting it reproduces today's command byte-for-byte. | `deno test packages/cli/e2e/` (incl. a golden `scaffoldInitCommand` assertion for the default path) | `e2e/src/domain/run-context.ts`, `presentation/cli/options/run-options.ts`, `presentation/cli/commands/{run,full}-command.ts`, `gates/scaffold/scaffold-gates.ts`, `builders/workspace/suite-builder-options.ts`, `create-default-runner.ts`, tests | +| 3 | **A capability suite can pin its own defaults while the CLI still overrides them.** | `deno test packages/cli/e2e/` — precedence test: suite default `sqlite` + `--db postgres` → postgres | `suites/scaffold/capability-suites.ts`, `presentation/cli/suites/registry.ts`, tests | +| 4 | **`scaffold.runtime.sqlite` exists, resolves, and requests no postgres/redis container resources.** _(originally "zero container resources"; amended after R-3 resolved negatively — see D2 and drift D-14.)_ | `deno task e2e:cli suites` lists it; registry + wait-matrix unit tests | `e2e/src/domain/cli-surface.ts`, `suites/scaffold/capability-suites.ts`, tests | +| 5 | **Cleanup survives a machine with no Docker and a run with no containers.** | `deno test packages/cli/e2e/` — absent-binary and non-zero-`docker ps` cases | `e2e/src/adapters/commands/docker-resource-cleaner.ts`, tests | +| 6 | **CI runs the cheap tier on scaffold changes and honours `ci:skip-e2e` / `ci:full` with no new labels.** | `deno test .github/scripts/` — `ci:full` / `ci:skip-e2e` / docs-only matrices | `.github/scripts/ci-classify-changes.ts`, `.github/workflows/e2e-cli.yml`, classifier tests | +| 7 | **The tier is real: a full local run passes with postgres and redis eliminated and a net-zero container delta.** _(originally "zero containers created"; amended after the R-3 downgrade — one garnet container is created and removed by cleanup.)_ | `deno task e2e:cli run scaffold.runtime.sqlite --cleanup --format pretty` + `docker ps -a` **net delta** = 0 | Fixes discovered by the run; `worklog.md` gate tables; run report artifact | + +Slice count: 7 (target < 30). Order is a strict dependency chain — S1 unblocks the runtime path, +S2–S3 build the seams, S4 assembles the suite, S5 makes teardown safe, S6 wires CI, S7 proves it. + +### Deferred Scope + +- **Promoting sqlite to merge-readiness** — D6; postgres stays the bar. +- **Changing `SCAFFOLD_DEFAULTS.CACHE_BACKEND`** — a product default affecting every user; follow-up + issue at Close. +- **Emitting `Mode: 'Local'` for deno-kv** — the generator arm exists but is unreachable; recorded + as debt, not fixed here. +- **A `cacheBackend` axis on the E2E runner** — superseded by D3; would be a speculative seam. +- **`env` on the `CommandExecutor` port** — not needed (finding 13). + +### Contributor Path + +To add another reduced-container tier (say mysql-less, or a bare-runtime tier), a contributor: + +1. adds the id + title to `SCAFFOLD` / `SCAFFOLD_TITLE` in `e2e/src/domain/cli-surface.ts`; +2. appends one entry to `scaffoldCapabilitySuites` in `suites/scaffold/capability-suites.ts` with a + `gates` list and a `defaults` object; +3. adds the id to `EXPENSIVE_RUNTIME_SUITE_IDS` when it shares the runtime smoke root and resources; +4. adds a CI job by copying the `scaffold-runtime-sqlite` block and its classifier output. + +No gate-filtering logic to touch: waits are derived from the suite's resolved options. + +## Progress Log + +| Time | Slice | Step | Notes | +| ---------- | --------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-08-04 | bootstrap | Research pass re-derived against `main` @ `c6f243da` | 5 corrections to the carried-in draft, 2 of them blockers. See `research.md` § Re-baseline. | +| 2026-08-04 | bootstrap | Run dir authored; branch + draft PR opened | Harness artifacts only — no product code (D9). | +| 2026-08-04 | S1 | Pre-implementation trace stopped on app-command drift | `generate-register-apps.ts` launches `deno task` and owns no permission list; recorded as significant drift D-5 before product edits. | +| 2026-08-04 | S1 | Resumed after supervisor D-5 ruling | Implemented the three-generator rescope; apps were not touched and receive neither task arguments nor generated comments. | +| 2026-08-04 | S1 | Implementation gates complete | All six required gates passed; slice is awaiting Tier-A substantive review and sign-off. | +| 2026-08-04 | S1 | **Tier-A slice review — ACCEPTED** | Supervisor read the diff and re-ran every gate independently. One cosmetic finding, no blocking findings. Sign-off commit follows. | +| 2026-08-04 | S2 | Resolved R-2 against the real public binary | `--no-cache` exited 2; `--cache=false` and `--cache false` both exited 0. The single-argv `--cache=false` spelling was selected. Dry-run reported two Aspire resources. Materialized config had `Cache: {}` and no `PrimaryCache`; the probe directory was removed. | +| 2026-08-04 | S2 | Implementation and generator gates complete | Added the default-true cache axis, CLI negation, exact init forwarding, workspace-builder plumbing, and focused regression tests. All six required gates passed; Tier-A review is pending. | +| 2026-08-04 | S2 | Resumed after external timeout | Re-read the partial diff, repeated all three public-binary spelling probes under a fresh `/tmp` directory, cleaned it, and independently re-ran all six required gates. The only correction was the second accepted false spelling, recorded as D-6. | +| 2026-08-04 | S2 | **Tier-A slice review — ACCEPTED** | Supervisor reproduced the six gates, verified the no-cache materialized config, and found no issues. Sign-off commit `47caa6bb`. | +| 2026-08-04 | S3 | Implementation and generator gates complete | Added the optional capability defaults contract, one top-level defaults-under-overrides merge, precedence + database-gate tests, and an exact options baseline for all eight existing built-ins. All six required gates passed; Tier-A review is pending. | +| 2026-08-04 | S4 | Implementation and generator gates complete | Added the sqlite runtime id/profile, inherited executable Garnet mode, registry/CLI precedence coverage, and wait/resource consistency checks. All six required gates passed; Tier-A review is pending. | +| 2026-08-04 | S4a | Expensive-suite lease correction complete | Centralized both runtime ids in one derived finite vocabulary, made sqlite and postgres contend in both directions, retained the cheap-suite negative control, and documented the sqlite tier. All six requested gates passed. | + +## Slice Review — S1 (Tier-A, supervisor) + +Reviewed at `f012f019`. The supervisor read the diff and **re-ran every gate independently** rather +than accepting the implementer's report (`lane-policy.md` invariant 2 — no lane self-certifies). + +**Independently reproduced gate results** + +| Gate | Command | Verdict | +| ------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| helper tests | `deno test --no-lock -A packages/cli/src/kernel/templates/aspire/helpers/tests/` | 18 passed, 171 steps, 0 failed | +| type-check | `.llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | 786 files, 7 batches, 0 findings | +| lint | `.llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | 786 files, 4 batches, 0 findings | +| quality:scan | `deno task quality:scan` | `ok: true`, 0 findings (7 pre-existing allowances, none in this slice) | +| arch:check | `deno task arch:check` | exit 0; warnings are pre-existing and out of scope | + +**Substantive review** + +- `withDatabasePermissions` is a pure value-in/value-out helper — A11 respected, no IO, no env + probing. It de-duplicates (`!permissions.includes('--allow-ffi')`), so an entry that already + declares the flag does not get a second one. +- The branch keys off `databaseEngine === 'Sqlite'`, an existing `DatabaseEntry['Engine']` domain + value. No hardcoded plugin names, no host-side `kind === …` coupling. +- The services generator now **consumes** the shared helper instead of keeping a private copy — one + implementation, not two. That is the point of the slice. +- The + `entryPermissions ? denoDefaults.Permissions : withDatabasePermissions(denoDefaults.Permissions, …)` + shape in background and plugins mirrors the services call site exactly, and is correct: + `resolvePermissions` prefers entry permissions when present, so the defaults argument only needs + the FFI flag on the path where it is actually used. +- `helpers-generator-pipeline.ts` hoists the existing + `config.Databases[config.PrimaryDatabase]?.Engine` expression rather than introducing a second + derivation — services behaviour is provably unchanged. +- Apps were **not** touched, per the D-5 ruling: no `RegisterAppsOptions.databaseEngine`, no + `--allow-ffi` smuggled in as a `deno task` argument or a generated comment. +- R-1 is closed by test, not by assertion: + `keeps non-SQLite {background,service,plugin} output + byte-identical` compares generated output + across `[undefined, 'Postgres', 'Mysql', 'Mssql']`. +- No `any`, no `as unknown as`, no new `// deno-lint-ignore`. +- Test imports use `jsr:@std/assert@^1`, matching the existing convention in that directory — not a + finding. + +**Findings** + +| # | Severity | Finding | Disposition | +| - | -------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1 | cosmetic | The commit body of `f012f019` contains literal `\n\n` escape sequences instead of newlines, so it renders as one long line. | Accepted as-is. Amending would rewrite a pushed hash already cited in the PR trail for zero functional gain. Noted here so the record is honest. | + +**Verdict: ACCEPTED.** S1 proves what it claims. Proceed to S2. + +## Slice Review — S3 (Tier-A, supervisor) + +Reviewed at `945f926c`. This is the Claude-family `review_codex` lane (Opus 4.8 fallback per drift +D-7; the canonical Fable 5 · low primary returned `model_not_found`). Gates were re-run +independently rather than accepted from the implementer's report (`lane-policy.md` invariant 2 — no +lane self-certifies). The exact diff reviewed is `47caa6bb..945f926c`. + +**Independently reproduced gate results** + +| Gate | Command | Verdict | +| ------------ | --------------------------------------------------------------- | ------------------------------------------------------------ | +| E2E tests | `deno test --no-lock -A packages/cli/e2e/` | 99 passed, 0 failed | +| type-check | `.llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | 786 files, 7 batches, 0 failed, 0 findings | +| lint | `.llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | 786 files, 4 batches, 0 findings | +| format | `.llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | 786 files, 4 batches, 0 failed, 0 findings | +| quality:scan | `deno task quality:scan` | `ok: true`, 0 findings (7 pre-existing allowances, none new) | +| arch:check | `deno task arch:check` | exit 0; warnings are pre-existing `ai`-plugin/out-of-scope | + +**Substantive review (each required verification)** + +1. **`ScaffoldCapabilitySuite` adds only `readonly defaults?: Partial`.** Confirmed — + `capability-suites.ts:17` is the sole interface addition; the diff touches no other field. +2. **One defaults-under-overrides merge; every former `overrides` read uses the resolved object.** + Confirmed — `const resolved = { ...capability.defaults, ...overrides };` + (`capability-suites.ts:173`) is the single merge, at the top. Every workspace/scaffold/reporting + read (`resolved.repoRoot`, `resolved.database`, `resolved.cache`, `resolved.samples`, + `resolved.format`, …) now reads `resolved`. The only remaining `overrides` reference in the + function is the parameter feeding line 173. Merge order (`defaults` first, `overrides` last) + gives caller precedence. +3. **`resolveSuite` keeps suite defaults under explicit caller overrides.** Confirmed — + `registry.ts:55` returns + `{ ...suite, defaultOptions: { ...suite.defaultOptions, ...overrides } }`. `suite.defaultOptions` + already carries the resolved capability default (baked in by + `createScaffoldCapabilitySuite(capability, overrides)`), and the final spread re-applies the + _same_ caller overrides idempotently. A capability default absent from `overrides` is never + overwritten, so it cannot be discarded by the final spread. +4. **A sqlite capability default resolves sqlite without overrides, postgres under an explicit + override, and `runtimeGateIds` follows the resolved database.** Confirmed by code and test — + `suite.defaultOptions.database` (the resolved value) is passed to `runtimeGateIds` + (`capability-suites.ts:225`); the new test + `capability defaults are a baseline and caller + overrides select database gates` asserts + sqlite→`[garnet]` with no override and postgres→`[postgres, garnet]` under + `{ database: POSTGRES }`. +5. **Every existing built-in suite remains default-free and resolves to exactly its prior options.** + Confirmed — the five `scaffoldCapabilitySuites` entries carry no `defaults`; the test asserts all + five `defaults === undefined` and pins the complete `RunOptions` for all eight `builtInSuites` + under deterministic overrides. +6. **No out-of-scope surface, no `any`/cast/ignore.** Confirmed — the diff touches only + `capability-suites.ts`, `suite-registry_test.ts`, and run artifacts. No suite id, `.github/**`, + cleanup adapter, or `packages/cli/src/**` file changed; no `any`, `as unknown as`, or new + `// deno-lint-ignore`. + +**Findings:** none. + +**Verdict: ACCEPTED.** S3 proves a capability suite can pin its own default options while a CLI +override still wins, without disturbing any existing suite's resolved options. Stop after S3; S4 +requires a new slice instruction. + +## Slice Review — S2 (Tier-A, supervisor) + +Reviewed at `8d960571`. Gates re-run independently; the load-bearing behavioural claim was verified +against the real binary rather than accepted from the implementer's report. + +**Independent verification of the claim the slice rests on** + +Ran `netscript init … --db sqlite --cache=false --ci --yes --no-git --force` and read the generated +`appsettings.json`: + +``` +Cache : {} +PrimaryCache : None +Databases : ['sqlite'] +PrimaryDatabase : sqlite +``` + +That is exactly the no-Docker profile D2 requires — no `redis` container resource, and sqlite +contributes no Aspire DB resource. R-2 is closed empirically: `--no-cache` exits 2, `--cache=false` +and `--cache false` exit 0, so the single-argv `--cache=false` form is used and **no product CLI +fallback was needed** — `init-command.ts` is untouched. + +**Reproduced gate results** + +| Gate | Verdict | +| ------------------------------------------ | -------------------------------- | +| `deno test --no-lock -A packages/cli/e2e/` | 97 passed, 0 failed | +| `run-deno-check.ts --root packages/cli` | 786 files, 7 batches, 0 findings | +| `run-deno-lint.ts --root packages/cli` | 786 files, 4 batches, 0 findings | +| `deno task quality:scan` | exit 0 | +| `deno task arch:check` | exit 0 | + +**Substantive review** + +- The golden test `scaffold init default command remains byte-identical` pins the full default argv + as a literal array — the regression guard for `scaffold.runtime` is a real assertion, not a smoke + test. +- `DISABLE_CACHE_ARGUMENT` is a named constant; the flag is spread from an array that is empty on + the default path, so the default argv is provably unchanged by construction as well as by test. +- `RunOptions.cache` defaults to `true` in **both** `defaultRunOptions` factories, so every existing + suite and the `full` command keep today's behaviour. +- `withCache` follows the existing `withCleanup` shape exactly, and `createScaffoldCapabilitySuite` + threads it with the same `!== undefined` guard — `false` is not swallowed as falsy. +- Commit body uses real newlines (the S1 cosmetic finding did not recur). +- No `any`, no `as unknown as`, no new lint-ignore. + +**Findings:** none. + +**Verdict: ACCEPTED.** Proceed to S3. + +## Slice Review — S3 (Tier-A, supervisor) + +Reviewed at `945f926c`. **Note:** the implementation lane had already authored a sign-off commit +(`d7460d76`) using a reviewer it dispatched itself — a breach of the no-self-certification +invariant, recorded as drift **D-7**. This is the supervisor's own review, performed afterwards. + +**Reproduced gate results (run by the supervisor, not read from the lane's report)** + +| Gate | Verdict | +| ------------------------------------------ | --------------------- | +| `deno test --no-lock -A packages/cli/e2e/` | 99 passed, 0 failed | +| `run-deno-check.ts --root packages/cli` | 786 files, 0 findings | +| `run-deno-lint.ts --root packages/cli` | 786 files, 0 findings | +| `deno task quality:scan` | exit 0 | +| `deno task arch:check` | exit 0 | + +**Substantive review** + +- The merge is a single expression at the top — + `const resolved = { ...capability.defaults, ...overrides }` — with every subsequent read switched + from `overrides` to `resolved`. Capability defaults are the baseline; caller overrides win. That + is exactly D5. +- The `!== undefined` guards on `cache` and `cleanup` are preserved, so a capability default of + `false` is not swallowed as falsy. +- `runtimeGateIds(capability.gates, suite.defaultOptions.database)` now sees the resolved engine, so + wait-gate filtering follows capability defaults without a second code path. +- `registry.ts` is **unchanged** — correctly. Its closing + `{ ...suite.defaultOptions, ...overrides }` still holds, because capability defaults have already + flowed into `suite.defaultOptions` via `withWorkspace`. I verified this rather than accepting it: + the precedence test resolves a sqlite-defaulted capability with `--db postgres` and gets postgres, + including the postgres wait gate. +- `existing built-in suites preserve their exact resolved options` asserts every built-in still has + `defaults === undefined` **and** pins each suite's resolved options — the no-regression guard the + slice needed. +- No existing suite gained defaults. No `any`, no casts, no new lint-ignore. + +**Findings** + +| # | Severity | Finding | Disposition | +| - | --------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | significant (process) | The implementation lane reviewed and signed off its own slice. | Recorded as drift D-7; the supervisor's review is this section, and the supervisor's sign-off commit follows. `d7460d76` is left in history so the breach stays visible. S4–S7 briefs amended to forbid it. | + +**Verdict on the code: ACCEPTED** — S3 proves what it claims. Proceed to S4. + +## Slice Review — S4 + S4a (Tier-A, supervisor) + +Reviewed at `b0c6ef89` (S4, plus the swept `d5ba7205`) and `8e78dee6` (S4a). This slice carries the +PR's value, so it received the supervisor's own review **plus** a supervisor-dispatched Claude Opus +5 adversarial sub-agent (drift D-9 escalation step 2) briefed to refute rather than agree. + +**Reproduced gate results (supervisor-run)** + +| Gate | Verdict | +| ------------------------------------------ | ---------------------------------------------------- | +| `deno test --no-lock -A packages/cli/e2e/` | 105 passed, 0 failed | +| `run-deno-check.ts --root packages/cli` | 786 files, 0 findings | +| `run-deno-lint.ts --root packages/cli` | 786 files, 0 findings | +| `deno task quality:scan` | exit 0 | +| `deno task arch:check` | exit 0 | +| `deno task e2e:cli suites` | lists `scaffold.runtime.sqlite` with its exact title | + +**Adversarial sub-agent verdict: ACCEPT-WITH-FINDING.** It probed the real Cliffy program rather +than trusting the tests. Results: + +| Question | Verdict | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `run-command.ts` default removal changes behaviour? | **SAFE** — `compactOptions` drops `undefined`, so the suite baseline applies; both `defaultRunOptions` copies are already postgres/cache-on, and `full` keeps its explicit postgres default (D6 holds). Probed: every pre-existing suite still resolves `postgres/true`. | +| `Deno.env.set` in the suite factory | **TASTE-ONLY** — listing suites does not trigger it (the registry stores closures); it cannot leak into a postgres run (one suite resolved per process, and `Deno.env.set` cannot escape to the parent shell); test-process residue is real but inert because nothing reads the variable at test time. | +| Gate-list correctness | **SAFE** — sqlite resolves with `runtime.wait.garnet` and none of postgres/mysql/mssql; `scaffold.runtime` is byte-identical; the wait matrix cross-checks `RUNTIME_GATES` against `runtimeResources(db)` rather than restating the implementation. | +| Other runtime risks | **DEFECT ×2** — see below. | + +**Defect 1 — fixed in S4a (this is why S4 was not signed off as landed).** `suite-runner.ts:61` +gated the expensive-suite lease on a literal `suite.id === SCAFFOLD.RUNTIME`. +`scaffold.runtime.sqlite` runs the same 68-gate runtime path against the same `.llm/tmp/cli-e2e` +smoke root, so it neither took the lease nor was blocked by one — the cheap tier, the one most +likely to be run alongside the postgres tier, would collide silently instead of producing the honest +`SuiteLeaseContentionError`. Confirmed at source by the supervisor before acting. S4a replaces it +with `EXPENSIVE_RUNTIME_SUITE_IDS` (constant + derived `ExpensiveRuntimeSuiteId`), adds +**bidirectional** postgres↔sqlite contention tests, and preserves the cheap-suite no-lease +regression. `suite-lease.ts`'s `isSuiteId` already accepted the new id and was correctly left alone. + +**Defect 2 — routed to S5, not fixed here.** `suite-runner.ts:69-71` calls +`dockerCleaner.captureSnapshot()` whenever `cleanup` is true, **outside any gate**, so a missing +`docker` binary throws a raw exception and kills the run. That is S5's scope (D8); the S5 brief was +amended to cover the **runner call site**, not just the adapter, and to require a runner-level test +with a Docker-less cleaner. + +**Also fixed in S4a:** `packages/cli/e2e/README.md` Built-in Suites table now lists the new tier — +an operator reading that table previously could not discover it. + +**Recorded, not fixed (accepted):** + +- The `Deno.env.set` call site is impure for a definition factory and fires before overrides are + considered, so `run scaffold.runtime.sqlite --cache` honours the operator's cache request in + appsettings while still forcing the Docker-less Garnet arm. Both arms are Redis-compatible, so no + wrong verdict is possible. Not worth destabilising the suite seam at this point in the run. +- The `run`-command equivalence now rests on two separate `defaultRunOptions` copies both staying at + postgres/cache-on, and nothing tests that invariant. + +Both are logged as follow-ups at Close rather than silently dropped. + +**Verdict: ACCEPTED** (S4 with S4a as its required fix). Proceed to S5. + +## Slice Review — S5 (Tier-A, supervisor) + +Reviewed at `65988b44`. + +**Reproduced gate results (supervisor-run)** + +| Gate | Verdict | +| ------------------------------------------ | --------------------- | +| `deno test --no-lock -A packages/cli/e2e/` | 110 passed, 0 failed | +| `run-deno-check.ts --root packages/cli` | 787 files, 0 findings | +| `run-deno-lint.ts --root packages/cli` | 787 files, 0 findings | +| `deno task quality:scan` | exit 0 | +| `deno task arch:check` | exit 0 | + +**Empirical proof, not just unit tests.** The unit tests inject a rejecting runner, which proves the +branch but not the real-world binding. I ran the **real, un-injected** `DockerCliResourceCleaner` +under `env -i PATH=` so `docker` was genuinely absent: + +``` +Warning: Docker cleanup could not inspect containers because the docker executable was not found; treating the container set as empty. +snapshot containerIds: [] +Warning: ... (same, from prune) +pruned: [] +NO THROW +``` + +A first attempt at this check was **invalid** — I trimmed `PATH` to `/usr/bin:/bin`, where +`/usr/bin/docker` still exists, so it silently exercised the happy path and found four containers. +Recording that here because a green-looking probe that tests nothing is exactly the failure mode +this slice exists to prevent. + +**Substantive review** + +- Both discovery failure modes are handled: `Deno.errors.NotFound` (binary absent) and a non-zero + `docker ps` (daemon down / permission denied). Any **other** error is re-thrown rather than + swallowed — the tolerance is narrow, not blanket. +- **Strictness preserved where it matters**: `docker rm -f` failing for a container the run _did_ + create still throws (`pruneCreatedResources`). The postgres tier's cleanup is not weakened. +- The `DockerResourceCleaner` **port is unchanged**, and `create-default-runner.ts` still constructs + `new DockerCliResourceCleaner()` — the constructor injection defaults to the real implementations, + so this is an adapter-internal testability seam, not a contract change. +- The runner call site flagged by the S4 adversarial review is covered: + `suite runner completes + cleanup with a Docker-less cleaner` drives a full run with + `cleanup: true` and asserts `report.ok === true` plus two warnings. That is the path S6's CI job + takes. +- Warning goes to `Deno.stderr` rather than `console.*`, consistent with the surrounding code and + the console-log lint posture. + +**Findings:** none. + +**Verdict: ACCEPTED.** Proceed to S6. + +## Slice Review — S6 + S6a (Tier-A, supervisor) + +Reviewed at `fafbe2b1` (S6) and `6728529f` (S6a). Because **draft PRs run no CI** (#1212), this +slice cannot be validated by observation before merge — a wrong boolean would ship silently. It +therefore received the supervisor's review **plus** a supervisor-dispatched Opus 5 adversarial +sub-agent (drift D-9 step 2) briefed to refute. + +**Reproduced gate results (supervisor-run)** + +| Gate | Verdict | +| ------------------------------------------- | -------------------------------------------------------------------------------- | +| `deno test --no-lock -A .github/scripts/` | 56 passed, 0 failed | +| `run-deno-check.ts --root .github --ext ts` | 0 findings | +| `run-deno-lint.ts --root .github --ext ts` | 0 findings | +| `quality:scan` / `arch:check` | **N/A** — no `packages/**` or `plugins/**` change (stated, not silently skipped) | + +**Adversarial verdict on S6: ACCEPT-WITH-FINDING — no shipping defect.** It executed `decide()` +across the full matrix rather than reading the diff narrative, and byte-diffed the preserved job: + +| Checked | Result | +| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `run_runtime_sqlite` in every branch (`ci:full`, both skips, docs-only, empty diff, unrecognised path, non-PR, `diff_unavailable`, classify-failure) | **SAFE** — matches E5 in all cases; no case runs when it should skip or skips when it should run | +| `ci:skip-e2e` skips **both** tiers | **SAFE** — classifier and workflow `RUN` expression | +| New job vs `scaffold-runtime` | **SAFE** — identical `if:`, fail-closed `RUN`, **all 10 steps guarded**, `printf`-quoted `$SKIP_REASON` (no injection), distinct concurrency group, distinct artifact name, correct suite id, `NETSCRIPT_CACHE_MODE: Executable` verified against `shouldUseContainerCache()` casing | +| Preservation | **SAFE** — `scaffold-runtime` **byte-identical**; #1212 draft guards intact; `lane-visibility` wired in `needs:`, env, and table; `labels.yml` structurally untouched | + +**The finding it did surface was real and operator-facing, and S6a fixes it.** The classifier gained +an _output_ but no _reason clause_: under `ci:skip-scaffold` the sqlite job's skip notice printed a +reason whose only runtime clause affirmatively said a runtime tier **was** running, with no +explanation for the sqlite skip. Since drafts run no CI, that notice is the first and only +diagnostic on the first live run. + +**Verified the fix empirically** by calling `decide()` directly: + +``` +ci:skip-scaffold sqlite=false … scaffold-runtime-sqlite skipped: scaffold-static signal is off … +ci:skip-e2e sqlite=false … scaffold-runtime-sqlite skipped by ci:skip-e2e … +ci:full sqlite=true … scaffold-runtime-sqlite forced by ci:full +(none) sqlite=true … scaffold-runtime-sqlite: scaffold-static signal is on … +``` + +**Also in S6a** + +- `labels.yml`: **`description:` text only** — `ci:skip-e2e` now says it skips both tiers, `ci:full` + says "all". No label added, renamed, or removed; the frozen three stay three (verified by diff). +- `ci:skip-scaffold` prose corrected in both the classifier header and the workflow: because + `run_runtime_sqlite` derives from `run_static`, it also drops the sqlite tier. E5-conformant and + now stated rather than left to be re-derived. +- Four test holes closed, each of which the review proved was a mutation that left all 54 tests + green: the `!skipScaffold` conjunct on the non-PR path, `lane-visibility`'s `needs:`/table row, + concurrency-group and artifact-name distinctness (R-8), and the workflow's suite-id string — now + asserted against the constant exported from `cli-surface.ts` rather than a duplicated literal, so + a typo is a test failure instead of a runtime-only failure nobody sees until merge. +- Artifact collection aligned with the postgres job's JSON/NDJSON globs. + +**Findings: none outstanding.** + +**Verdict: ACCEPTED.** Proceed to S7 — the live run. + +## Slice Review — S7 (Tier-A, supervisor) + +Reviewed at `51e6b08e`. This is the evidence slice; it survived two external interruptions (a +session restart and a driver timeout) and its uncommitted work was recovered and verified by the +supervisor before relaunch, not discarded. + +**Reproduced gate results (supervisor-run)** + +| Gate | Verdict | +| ----------------------------------------- | --------------------------------------------------------------------------------------- | +| `deno test --no-lock -A packages/cli/` | 605 passed (490 steps), 0 failed | +| `deno test --no-lock -A .github/scripts/` | 56 passed, 0 failed | +| `run-deno-check.ts --root packages/cli` | 789 files, 0 findings | +| `run-deno-lint.ts --root packages/cli` | 789 files, 0 findings | +| `deno task quality:scan` | exit 0 | +| `deno task arch:check` | exit 0 | +| `deno task publish:dry-run` | exit 0 | +| gate-list check (`resolveSuite`) | `scaffold.runtime` 69 gates incl. `behavior.service-health`; sqlite 67 gates without it | + +**The headline claim changed, and the artefacts were corrected to match.** The plan's original +acceptance was "**zero containers created**". R-3 resolved **negatively** — the Docker-less Garnet +executable arm showed inconsistent cross-process KV/queue visibility — so the pre-agreed downgrade +was taken. The honest claim is now: + +> **Postgres and Redis are eliminated. One Garnet container is created during the run and removed by +> cleanup, for a net delta of zero.** + +That is a materially weaker claim than "no Docker", and the run says so everywhere it matters rather +than letting the original wording stand: + +- suite title → `Runtime scaffold capability smoke (sqlite, reduced containers)` +- CI job name → `scaffold-runtime-sqlite (aspire + sqlite + garnet)` +- `plan.md` D2 rewritten to "Reduced-container profile", citing the negative R-3 result +- `NETSCRIPT_CACHE_MODE` pin removed from **both** the suite and the CI job; a regression test now + asserts the suite leaves the variable **unset** and still honours an operator-set value + +I verified each of those at source. A tier that still called itself "no docker" while starting a +container would have been the worst outcome of this run. + +**Bonus:** removing the pin also deleted the `Deno.env.set` inside the suite factory, which closes +the impurity the S4 adversarial review had recorded as taste-only. + +**R-4 — the excluded gate, checked rather than accepted.** `behavior.service-health` is excluded +from the sqlite suite only. The stated reason is that the generated users service's aggregate health +check uses Prisma's tagged `$queryRaw\`SELECT +1\``form, which the libSQL adapter rejects. I +confirmed the tagged form exists in the generated template (`embedded.generated.ts`contains both`queryRaw\`SELECT +1\``and`queryRawUnsafe(...)`), so the rationale is grounded in the product, not +a test-shape excuse. Critically: the gate is **retained unchanged in`scaffold.runtime`** +(verified — 69 vs 67 gates), so postgres coverage is not weakened. Recorded as drift D-15. + +**This exclusion is a product finding, not just a test decision.** A user scaffolding a sqlite +project today gets a service whose aggregate `/health` check fails against libSQL. That is a real +gap this run discovered and must not be buried in a drift note — it is filed as a follow-up at +Close. + +**R-5 resolved positively:** workers jobs, tasks, seed, trigger, and execution visibility all pass, +so nothing before plugin install depends on a primary cache. + +**Two real defects the live run caught that no unit test could** + +1. **The maintainer CLI gap.** The E2E resolves its entrypoint to `bin/netscript-dev.ts` (the + _maintainer_ CLI) via `defaultCliEntrypoint`, **not** the public `bin/netscript.ts`. S2 verified + `--cache=false` against the _public_ CLI, which accepts it; the maintainer CLI did not declare + the option, so the live run failed with `Unknown option "--cache"`. **This was a supervisor + review miss in S2** — the evidence was real but tested the wrong binary. I reproduced it by + stashing the fix and re-running. Fixed here by adding `--cache` to the maintainer init command + and threading it through the request; the public CLI is unchanged. +2. **`--allow-all` already grants FFI**, so sqlite no longer appends a redundant `--allow-ffi` — a + refinement of S1 that only a live run over the real generated apphost would surface. + +**Acceptance evidence:** `scaffold.runtime.sqlite: 68 passed, 0 failed, 0 skipped`, `cleanup: PASS`, +and `comm -13` over before/after container snapshots was **empty**. The only pre-existing container +in both snapshots is a **foreign** postgres container owned by another worktree — reported, never +touched. + +**Findings** + +| # | Severity | Finding | Disposition | +| - | -------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| 1 | product | Generated sqlite services fail their aggregate `/health` check (tagged `$queryRaw` vs libSQL). | Follow-up issue at Close. Not this issue's scope; the tier documents it rather than hiding it. | +| 2 | process | S2's verification used the public CLI where the E2E uses the maintainer CLI. | Supervisor miss, fixed in S7; recorded so the lesson is not lost. | + +**Verdict: ACCEPTED**, with the reduced-container claim stated plainly. The postgres merge-bar +regression is queued separately (see Gate Results). + +## IMPL-EVAL response — FAIL_DEBT items closed + +IMPL-EVAL (open-model Qwen lane, separate session) returned **`FAIL_DEBT`**: the implementation was +judged complete and correct, with the sole blocking issue that `plan.md` § Arch-Debt Implications +committed to two debt entries at Close and neither had been created. It was right — they were +promised and quietly dropped. + +**Blocking items — both now closed** + +| Item | Action | +| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Unreachable `Mode: 'Local'` cache arm | `arch-debt.md` → `cache-local-arm-unreachable-1158`. Records that the only genuinely container-free cache path in the generator is dead code, and that this is _why_ the tier could not reach its original zero-container goal once R-3 failed. | +| `SCAFFOLD_DEFAULTS.CACHE_BACKEND: 'redis'` forces a container | `arch-debt.md` → `scaffold-default-cache-container-1158`. Records that the E2E works around it per-suite with `--cache=false` while **users have no such default**. | + +**Non-blocking item — also fixed.** IMPL-EVAL caught that `plan.md` validation row 9 and the +worklog's commit-slices table (S4, S7) still carried the pre-downgrade **"zero containers"** +wording. The R-3 honesty fix had reached the suite title, the CI job name and D2, but not these — so +the correction was incomplete. Both are now amended in place, each marked as amended with a pointer +to drift D-14, rather than silently rewritten. + +**Follow-up filed:** issue **#1259** — generated service aggregate health check fails on +sqlite/libSQL (tagged `$queryRaw`), and the same error was seen once on postgres. Filed with the +postgres confounder stated explicitly, and with re-including `behavior.service-health` in the sqlite +tier as an acceptance criterion so the exclusion cannot become permanent by neglect. + +**Still open:** drift **D-16** — the postgres merge bar has not yet passed in isolation. The +`FAIL_DEBT` items are closed, but merge-readiness is gated on that run, not on this section. + +## Decisions + +| Decision | Reason | Source | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| Garnet wait is **not** filtered | The resource exists under the same name in both the container and executable arms | code (`generate-register-infrastructure.ts:182-212`) | +| Boolean `cache` axis instead of a `cacheBackend` axis | `--cache-backend deno-kv` yields `Mode: 'External'`, and plugin-add re-adds garnet anyway | code (`generate-appsettings.ts:251-259`) | +| S1 (`--allow-ffi`) precedes every E2E slice | Non-service resources exit 1 on sqlite without it | code (`generate-register-services.ts:32-38`) | +| Per-suite `defaults` merged **under** overrides | A suite id alone cannot pin an engine today | code (`capability-suites.ts:168-192`) | +| Generic `run` supplies only explicit db/cache overrides | Cliffy defaults otherwise mask capability defaults; `full` keeps its explicit D6 defaults | code (`run-command.ts`, drift D-10) | + +## Drift + +| Drift | Severity | Logged in drift.md | +| ---------------------------------------------------- | ----------- | ------------------ | +| Supervisor lane is Opus 5, not the canonical Fable 5 | minor | yes (D-1) | +| Carried-in draft's root-cause analysis was wrong | significant | yes (D-2) | +| #1191's fix is services-only — new blocker | significant | yes (D-3) | +| Apps own no permission-bearing command | significant | yes (D-5 + ruling) | +| Generic `run` defaults masked suite defaults | significant | yes (D-10) | +| Concurrent supervisor commit swept S4 worktree | significant | yes (D-11) | +| S4 omitted sqlite from the expensive-suite lease set | significant | yes (D-13) | + +## Gate Results + +### S5 Docker-less Cleanup + +`DockerCliResourceCleaner` now treats Docker discovery as an optional cleanup capability. Its +private list path catches `Deno.errors.NotFound` from a missing executable and converts a non-zero +`docker ps` result into the same empty container set. Both paths emit a visible warning through a +small injected writer whose production default writes directly to `Deno.stderr`; this mirrors the +existing reporter output seam without adding a reporter dependency or using `console.warn`. + +The tolerance ends at discovery. `docker rm -f` still runs for every container absent from the +snapshot, and a non-zero removal result still throws with the container id and stderr. The +`DockerResourceCleaner` port is unchanged. The runner call site is also unchanged by design: a +runner-level regression uses `cleanup: true` and the real adapter configured to raise `NotFound` on +both list calls, then proves the runner returns an `ok` report with no steps rather than leaking the +raw exception. + +| Gate | Command | Raw result | +| ---------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| E2E tests | `deno test --no-lock -A packages/cli/e2e/` | exit 0; 110 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | exit 0; 787 files, 7 batches, 0 failed batches, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | exit 0; 787 files, 4 batches, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | exit 0; 787 files, 4 batches, 0 failed batches, 0 findings | +| quality | `deno task quality:scan` | exit 0; `ok: true`, 0 findings, 7 pre-existing allowances | +| doctrine | `deno task arch:check` | exit 0; existing out-of-scope dependency/doctrine warnings only | + +**Focused assertions.** Adapter tests cover missing-binary `NotFound`, non-zero `docker ps`, an +unchanged snapshot returning `[]` without invoking removal, and a created container whose failed +`docker rm -f` still rejects. The runner regression observes two warnings — snapshot and prune — so +both formerly intolerant list calls are exercised through the `cleanup: true` call path. + +**Post-slice reconcile note.** Issue #1158 and PR #1220 remain open at `status:impl`, assigned to +milestone 23; the PR retains `Closes #1158` for the full seven-slice outcome and its taxonomy. The +latest PR comment is the Tier-A S4/S4a sign-off explicitly authorizing S5, and there are no review +threads or newer findings. S5 touches no `.github/**`, live runtime, `packages/cli/src/**`, port +contract, labels, or milestone. No plan/doctrine divergence occurred, so `drift.md` is unchanged. +This lane hands off one implementation commit and does not review, self-certify, dispatch a +reviewer, author a sign-off, or start S6. + +### S4a Lease-Contention Correction + +`EXPENSIVE_RUNTIME_SUITE_IDS` is the single finite vocabulary for suites that share the expensive +runtime path and lease. Its derived `ExpensiveRuntimeSuiteId` union keeps future additions tied to +that constant. `suite-runner.ts` now acquires the lease when the suite id is a member. The existing +`isSuiteId` parser in `suite-lease.ts` was verified unchanged: it already derives accepted ids from +all `SCAFFOLD` and `DEPLOY` values, so `scaffold.runtime.sqlite` is valid lease metadata. + +The runner test holds a postgres lease and starts sqlite, then holds sqlite and starts postgres. In +both directions the contender raises `SuiteLeaseContentionError` and identifies the held suite. The +existing `scaffold.service` test still records zero acquisitions. No Docker cleanup code or snapshot +call site changed. + +| Gate | Command | Raw result | +| ---------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| E2E tests | `deno test --no-lock -A packages/cli/e2e/` | exit 0; 105 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 7 batches, 0 failed batches, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 failed batches, 0 findings | +| quality | `deno task quality:scan` | exit 0; `ok: true`, 0 findings, 7 pre-existing allowances | +| doctrine | `deno task arch:check` | exit 0; existing out-of-scope dependency/doctrine warnings only | + +**Discovery evidence.** `deno task e2e:cli suites` exited 0 and listed both `scaffold.runtime` and +`scaffold.runtime.sqlite`. Per the owner boundary, no runtime suite was started. + +**Post-slice reconcile note.** S4a changes only the shared E2E vocabulary, runner predicate, runner +regressions, README suite table, and harness evidence. It does not touch `suite-lease.ts`, Docker +cleanup, `.github/**`, or `packages/cli/src/**`. The implementation lane performs one +commit/push/PR-comment handoff and stops without dispatching a reviewer or authoring a sign-off. + +### S4 Slice Gates + +| Gate | Command | Raw result | +| ---------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| E2E tests | `deno test --no-lock -A packages/cli/e2e/` | exit 0; 104 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 7 batches, 0 failed batches, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 failed batches, 0 findings | +| quality | `deno task quality:scan` | exit 0; `ok: true`, 0 findings, 7 pre-existing allowances | +| doctrine | `deno task arch:check` | exit 0; existing out-of-scope dependency/doctrine warnings only | + +**Suite evidence.** `deno task e2e:cli suites` exited 0 and listed +`scaffold.runtime.sqlite\tRuntime scaffold capability smoke (sqlite, no docker)`. The capability +resolves to `database: sqlite`, `cache: false`; explicit `--db postgres` wins; and the sqlite +resolution sets `NETSCRIPT_CACHE_MODE=Executable` only when the operator has not already supplied a +value. The sqlite and postgres wait-gate arrays exactly match `runtimeResources()` for their +engines. Both retain `runtime.wait.garnet`; sqlite has no postgres/mysql/mssql wait, while the +unchanged runtime suite has postgres and neither mysql nor mssql. + +**Post-slice reconcile note.** S4 remains partial work on #1158 / draft PR #1220. No closing +relationship, labels, milestone, `.github/**`, Docker cleanup, live runtime, or +`packages/cli/src/**` surface changed. Drift D-10 records the generic CLI defaults that masked the +new capability defaults; D-11 records the concurrently pushed supervisor commit that swept the S4 +worktree before the implementation commit. S4 is implementation-complete with green automated gates +and remains explicitly pending Tier-A review; S5 has not started. + +### S3 Slice Gates + +| Gate | Command | Raw result | +| ---------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| E2E tests | `deno test --no-lock -A packages/cli/e2e/` | exit 0; 99 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 7 batches, 0 failed batches, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 failed batches, 0 findings | +| quality | `deno task quality:scan` | exit 0; `ok: true`, 0 findings, 7 pre-existing allowances | +| doctrine | `deno task arch:check` | exit 0; existing out-of-scope dependency/doctrine warnings only | + +**Precedence evidence.** A synthetic runtime capability with +`defaults: { database: DATABASE.SQLITE }` resolves to sqlite with no caller overrides and filters +all database waits. Passing `{ database: DATABASE.POSTGRES }` resolves to postgres and selects the +postgres wait while retaining the garnet wait. A separate golden options table resolves every +existing built-in suite under deterministic path overrides and asserts the complete `RunOptions` +object; every existing scaffold capability also asserts `defaults === undefined`. + +**Post-slice reconcile note.** S3 remains partial work on #1158 / draft PR #1220, so it does not +change the PR closing relationship or begin S4. No existing capability received a `defaults` object, +and no suite id, `.github/**`, cleanup adapter, or `packages/cli/src/**` file changed. The run stays +at `status:impl`; S3 is implementation-complete with green automated gates but pending Tier-A +review. + +### S2 Slice Gates + +| Gate | Command | Raw result | +| ---------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| E2E tests | `deno test --no-lock -A packages/cli/e2e/` | exit 0; 97 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 7 batches, 0 failed batches, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | exit 0; 786 files, 4 batches, 0 failed batches, 0 findings | +| quality | `deno task quality:scan` | exit 0; `ok: true`, 0 findings, 7 pre-existing allowances | +| doctrine | `deno task arch:check` | exit 0; existing out-of-scope dependency/doctrine warnings only | + +**R-2 empirical evidence.** The public binary rejects `--no-cache` and accepts both `--cache=false` +and `--cache false`. `scaffold.init` emits the single-argv `--cache=false` spelling only when +`RunOptions.cache === false`; omitting the E2E option remains byte-identical by golden argv +assertion. The accepted probe's dry-run reported two Aspire resources; a materialized probe +confirmed no cache resource (`Cache: {}`) and no `PrimaryCache`. The product CLI fallback in +`init-command.ts` was not needed. The resumed probe used `/tmp/ns-cache-probe.` and verified +its removal afterward. + +**Post-slice reconcile note.** S2 remains partial work on #1158 / draft PR #1220, so the existing PR +closing keyword remains appropriate but no acceptance box can be completed yet. The sweep found both +the issue and PR still carrying stale `status:plan-eval`; the S2 phase comment reconciles them to +`status:impl`. No new reviewer findings appeared after the S1 sign-off comment. S2 is +implementation-complete with green automated gates but remains explicitly pending Tier-A review; S3 +has not started. + +### Static Gates + +| Gate | Command or check | Result | Notes | +| ------------ | --------------------------------------------------------------- | ------ | -------------------------------------------- | +| type-check | `.llm/tools/run-deno-check.ts --root packages/cli --ext ts,tsx` | `PASS` | 786 files; 7 batches; 0 failed; 0 findings. | +| lint | `.llm/tools/run-deno-lint.ts --root packages/cli --ext ts,tsx` | `PASS` | 786 files; 4 batches; 0 findings. | +| format | `.llm/tools/run-deno-fmt.ts --root packages/cli --ext ts,tsx` | `PASS` | 786 files; 4 batches; 0 findings. | +| quality:scan | `deno task quality:scan` | `PASS` | Repository scan found no violations. | +| arch:check | `deno task arch:check` | `PASS` | Exit 0; existing out-of-scope warnings only. | + +### Fitness Gates + +| Gate | Result | Evidence | Notes | +| ------ | --------- | ---------------------- | ----------------------------------------------- | +| `F-1` | `PASS` | scoped lint wrapper | 0 findings. | +| `F-3` | `PASS` | `deno task arch:check` | Exit 0. | +| `F-5` | `NOT_RUN` | — | Planned-surface scan recorded in `research.md`. | +| `F-6` | `NOT_RUN` | — | `publish:dry-run` at Gate phase. | +| `F-9` | `PASS` | generator tests | SQLite FFI exactly once in all three outputs. | +| `F-10` | `PASS` | helper test directory | 18 passed, 171 steps, 0 failed. | +| `F-19` | `PASS` | scoped wrappers | check/lint/fmt all passed. | + +### Runtime Gates + +| Gate | Result | Evidence | Notes | +| ----------------------------- | --------- | -------- | ------------------------------------------- | +| `scaffold.runtime.sqlite` | `NOT_RUN` | — | S7. Zero-container delta is the acceptance. | +| `scaffold.runtime` (postgres) | `NOT_RUN` | — | Merge-readiness regression run at Gate. | + +### Consumer Gates + +| Consumer | Result | Evidence | Notes | +| ------------------ | ------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| generated projects | `PASS` | semantic generator tests | Postgres, MySQL, MSSQL, and no-engine output is byte-identical to the pre-branch path for services, background processors, and plugins. | + +### S6 CI Policy and Cheap Runtime Job + +The classifier now emits `run_runtime_sqlite`. `ci:full` reaches the existing `fullDecision()` and +therefore forces it true. Every other classified PR derives it as `runStatic && !skipE2e`, keeping +`ci:skip-e2e` authoritative over both runtime tiers without introducing a label. `ci:skip-scaffold` +has no independent sqlite override: in the pinned scaffold-impacting case it makes `runStatic` +false, so the derived sqlite result is explicitly false. Docs-only changes are false, +scaffold-impacting changes are true, and conservative unrecognised/empty-diff decisions are true. + +`scaffold-runtime-sqlite` copies the existing runtime job's applicability, failed-classifier, +skipped-by-policy, toolchain setup, Aspire preflight, failed-report evidence, and artifact +structure. It keys `RUN` on the new output, sets `NETSCRIPT_CACHE_MODE=Executable` at job scope, +invokes `scaffold.runtime.sqlite --cleanup` with a distinct report path/artifact name, and uses +`e2e-scaffold-runtime-sqlite-global` so it never queues behind postgres. The 40-minute timeout is 20 +minutes below postgres while retaining headroom for Deno install, .NET/Aspire setup, Garnet tool +restore, and the full behavior suite. `lane-visibility` now needs and renders the sqlite job. The +existing draft guards, `scaffold-runtime` job, and `.github/labels.yml` are unchanged. + +| Gate | Command | Raw result | +| ---------- | ---------------------------------------------------------------------------------------- | ------------------------------------ | +| classifier | `deno test --no-lock -A .github/scripts/` | exit 0; 54 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root .github --ext ts` | exit 0; 3 files, 1 batch, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root .github --ext ts` | exit 0; 3 files, 1 batch, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root .github --ext ts` | exit 0; 3 files, 1 batch, 0 findings | +| YAML | `deno eval --no-lock` with `jsr:@std/yaml@^1.0.0` over `.github/workflows/e2e-cli.yml` | exit 0; parsed to a mapping | + +The first lint iteration found two `no-regex-spaces` findings in the new workflow-source test. The +test now uses a line-based job extractor; the complete final matrix above was rerun and passed. +`quality:scan` and `arch:check` are not applicable because S6 changes no `packages/**` or +`plugins/**` source. Per the owner brief, `deno task e2e:cli` was not run; S7 owns the first live +sqlite execution. + +**Post-slice reconcile note.** Issue #1158 and draft PR #1220 remain open at `status:impl` and +milestone 23; the PR retains `Closes #1158`. The latest PR comment is the Tier-A S5 sign-off that +explicitly authorizes S6, and there are no review threads. No labels or milestone require a change. +S6 matches locked decision E5 and risks R-6/R-7/R-8 without plan/doctrine divergence, so `drift.md` +is unchanged. This implementation lane hands off one commit and does not review, self-certify, +dispatch a reviewer, author a sign-off, or start S7. + +### S6a Adversarial Diagnostics Follow-up + +The sqlite classifier branch now contributes its own operator-facing reason clause. The clause +states whether `ci:skip-e2e` skipped the tier, the `scaffold-static` signal is off (including +`ci:skip-scaffold`), `ci:full` forced it, or the scaffold signal selected it. The existing +`runRuntimeSqlite = runStatic && !skipE2e` policy is unchanged. Non-PR coverage now pins the +`!skipScaffold` conjunct that was previously mutation-survivable. + +The workflow-source test now pins the sqlite job's `lane-visibility` dependency and summary row, its +concurrency group, its distinct artifact name, and its report globs. It reads the exported +`RUNTIME_SQLITE` value from `packages/cli/e2e/src/domain/cli-surface.ts` and asserts the workflow +invocation against that value, so the test contains no duplicate suite-id literal and no +`packages/**` edit. The sqlite artifact upload now uses the postgres sibling's three report globs, +including auxiliary `report*.ndjson` output. + +Only the requested policy prose changed: `ci:skip-scaffold` now plainly documents that the derived +sqlite tier also drops, and the two stale `ci:*` label descriptions now describe all expensive jobs +and both runtime tiers. The frozen label names/count remain unchanged. + +| Gate | Command | Raw result | +| ---------- | ---------------------------------------------------------------------------------------- | ------------------------------------ | +| classifier | `deno test --no-lock -A .github/scripts/` | exit 0; 56 passed, 0 failed | +| type-check | `deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root .github --ext ts` | exit 0; 3 files, 1 batch, 0 findings | +| lint | `deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root .github --ext ts` | exit 0; 3 files, 1 batch, 0 findings | +| format | `deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root .github --ext ts` | exit 0; 3 files, 1 batch, 0 findings | +| YAML | `deno eval --no-lock` with `jsr:@std/yaml@^1.0.0` over `.github/workflows/e2e-cli.yml` | exit 0; parsed to a mapping | + +`quality:scan` and `arch:check` are **N/A** because S6a changes no `packages/**` or `plugins/**` +source. The package file is read-only test input. No live runtime gate was run; S7 still owns the +first sqlite execution. + +**Post-slice reconcile note.** S6a is the owner-requested remediation for issue #1158 / PR #1220. It +stays inside E5 and risks R-6/R-8, changes no labels or milestone state, and introduces no plan, +doctrine, or scope divergence; `drift.md` is unchanged. This implementation lane will push one +commit, post its evidence comment, and stop without review or sign-off. + +## Handoff Notes + +- **Read `research.md` § Re-baseline first.** The carried-in draft's stated blocker was wrong; the + plan diverges from it deliberately at D2, D3, D4, and E5. +- The two claims most worth attacking: (a) that `Mode: 'Auto'` garnet really resolves to the + executable arm under `NETSCRIPT_CACHE_MODE=Executable` on CI, and (b) that S1's permission change + leaves every non-sqlite scaffold byte-identical. +- No product code exists at PLAN-EVAL time. Implementation begins only on `PASS`. +- S1–S5 are signed off. S6 + S6a are implementation-complete with green automated gates but are + **not self-certified**. Tier-A must review the classifier conjunction, workflow fail-closed + guards, reason clauses, artifact collection, independent concurrency, and lane visibility before + sign-off; do not start S7 from this handoff. + +## S7 Live SQLite Runtime Evidence + +This section supersedes the pre-S7 handoff above without rewriting its historical record. The +implementation lane resumed the externally timed-out worktree, preserved every existing change, and +continued from the already-isolated first-boot state-loss failure. + +Three instrumented executable-Garnet attempts kept `runtime.wait.garnet` green and the same healthy +Garnet PID alive, but produced inconsistent state across the workers API and background runtime: one +run exposed no jobs and returned 404 from the trigger, while two exposed the jobs and accepted the +trigger but never exposed an execution. Per the locked decision deadline, R-3 therefore resolved +negatively. The sqlite suite and CI job no longer pin `NETSCRIPT_CACHE_MODE=Executable`; the tier +uses ambient container-backed Garnet while still removing both Postgres and Redis (D-14). + +The first downgraded run passed the complete workers path and then failed only +`behavior.service-health`. The generated users-service health primitive invokes Prisma's tagged +`$queryRaw` form, which libSQL rejects, even though the generated sqlite database module's +`$queryRawUnsafe('SELECT 1')` succeeds. Per the pre-agreed R-4 exit, only that gate is filtered from +the sqlite capability list. `scaffold.runtime` retains the original gate and assertion unchanged, +and a regression test proves the lists differ by exactly this one id (D-15). + +The live run also corrected an S2 verification gap: E2E uses the maintainer +`bin/netscript-dev.ts init` path, not the public CLI path S2 probed. The maintainer command now +declares and forwards `--cache`; focused tests and the package test gate cover the correction +(D-16). The stronger `runtime.wait.workers` readiness gate waits for scheduler and worker-pool +startup markers before behavior checks. + +### Final Runtime Verdict + +Command: + +```text +deno task e2e:cli run scaffold.runtime.sqlite --cleanup --format pretty --report .llm/tmp/e2e-report-scaffold-runtime-sqlite.json +``` + +Result: **PASS — 68 passed, 0 failed, 0 skipped; cleanup passed.** + +| Gate family | Outcome | Rationale/evidence | +| ----------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------- | +| `runtime.wait.garnet` | `PASS` | Ambient container-backed Garnet became healthy. | +| `runtime.wait.workers` | `PASS` | Scheduler and worker-pool readiness markers observed. | +| `database.init`, `database.generate`, `database.seed` | `PASS` | SQLite lifecycle and seed completed. | +| `behavior.workers-*` | `PASS` | Health, jobs, tasks, seed, trigger, and execution visibility all passed; R-5 resolves positively. | +| remaining `behavior.*` | `PASS` | Sagas, triggers, auth, AI, UI, plugins, streams, and OTEL paths passed. | +| `behavior.service-health` | `N/A` in sqlite only | Provider-specific libSQL incompatibility under D-15; retained unchanged in Postgres runtime. | + +The before and after snapshots each contain only `97b906460988`, the foreign `postgres-89449635` +resource owned by `/home/codex/repos/wave5-deepseek`. `comm -13` is empty. Garnet was created during +the run and removed by run-owned cleanup, so the honest net container delta is **zero**. No foreign +resource was mutated. + +### Final Implementation Gates + +| Gate | Result | +| -------------------------------------- | ---------------------------------------------------------- | +| `deno test --no-lock -A packages/cli/` | `PASS` — 605 tests (490 steps), 0 failed | +| scoped check | `PASS` — 789 files, 7 batches, 0 findings | +| scoped lint | `PASS` — 789 files, 4 batches, 0 findings | +| scoped format | `PASS` — 789 files, 4 batches, 0 findings | +| `deno task quality:scan` | `PASS` — `ok: true`, 0 findings; 7 pre-existing allowances | +| `deno task arch:check` | `PASS` — exit 0; pre-existing warnings only | +| `deno task e2e:cli suites` | `PASS` — both runtime tiers listed | + +This is implementation evidence only. Under D-7 and the owner review boundary, this lane does not +dispatch a reviewer, add a `## Slice Review` section, self-certify, or author a sign-off commit. diff --git a/packages/cli/e2e/README.md b/packages/cli/e2e/README.md index 34d102d75c..56f2d2777e 100644 --- a/packages/cli/e2e/README.md +++ b/packages/cli/e2e/README.md @@ -113,6 +113,7 @@ and requested PR checks should use | `scaffold.infrastructure` | init, database init/generate/seed, typecheck | | `scaffold.plugins` | init, official plugins, registry generation, plugin doctor | | `scaffold.runtime` | full scaffold runtime behavior path | +| `scaffold.runtime.sqlite` | reduced-container sqlite runtime tier | ## Required Permissions diff --git a/packages/cli/e2e/src/adapters/commands/docker-resource-cleaner.ts b/packages/cli/e2e/src/adapters/commands/docker-resource-cleaner.ts index f9acf62b54..a579e857d6 100644 --- a/packages/cli/e2e/src/adapters/commands/docker-resource-cleaner.ts +++ b/packages/cli/e2e/src/adapters/commands/docker-resource-cleaner.ts @@ -4,23 +4,32 @@ import type { } from '../../ports/docker-resource-cleaner.ts'; const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +type DockerCommandOutput = Pick; +type DockerCommandRunner = ( + args: readonly string[], + stdout: 'null' | 'piped', +) => Promise; +type DockerWarningWriter = (message: string) => Promise; /** Docker CLI adapter that removes only containers created during a suite run. */ export class DockerCliResourceCleaner implements DockerResourceCleaner { + constructor( + private readonly runDocker: DockerCommandRunner = runDockerCommand, + private readonly writeWarning: DockerWarningWriter = writeDockerWarning, + ) {} + async captureSnapshot(): Promise { - return { containerIds: await listContainers() }; + return { containerIds: await this.listContainers() }; } async pruneCreatedResources(snapshot: DockerResourceSnapshot): Promise { const before = new Set(snapshot.containerIds); - const current = await listContainers(); + const current = await this.listContainers(); const created = current.filter((id) => !before.has(id)); for (const id of created) { - const output = await new Deno.Command('docker', { - args: ['rm', '-f', id], - stdout: 'null', - stderr: 'piped', - }).output(); + const output = await this.runDocker(['rm', '-f', id], 'null'); if (output.code !== 0) { const error = decoder.decode(output.stderr).trim(); throw new Error(`docker rm -f ${id} failed${error ? `: ${error}` : '.'}`); @@ -28,17 +37,42 @@ export class DockerCliResourceCleaner implements DockerResourceCleaner { } return created; } + + private async listContainers(): Promise { + let output: DockerCommandOutput; + try { + output = await this.runDocker(['ps', '-a', '--format', '{{.ID}}'], 'piped'); + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) throw error; + await this.writeWarning( + 'Warning: Docker cleanup could not inspect containers because the docker executable was not found; treating the container set as empty.', + ); + return []; + } + if (output.code !== 0) { + const error = decoder.decode(output.stderr).trim(); + await this.writeWarning( + `Warning: Docker cleanup could not inspect containers because docker ps failed${ + error ? `: ${error}` : '.' + } Treating the container set as empty.`, + ); + return []; + } + return decoder.decode(output.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + } } -async function listContainers(): Promise { - const output = await new Deno.Command('docker', { - args: ['ps', '-a', '--format', '{{.ID}}'], - stdout: 'piped', +function runDockerCommand( + args: readonly string[], + stdout: 'null' | 'piped', +): Promise { + return new Deno.Command('docker', { + args: [...args], + stdout, stderr: 'piped', }).output(); - if (output.code !== 0) { - const error = decoder.decode(output.stderr).trim(); - throw new Error(`docker ps failed${error ? `: ${error}` : '.'}`); - } - return decoder.decode(output.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean); +} + +function writeDockerWarning(message: string): Promise { + return Deno.stderr.write(encoder.encode(`${message}\n`)); } diff --git a/packages/cli/e2e/src/application/builders/workspace/suite-builder-options.ts b/packages/cli/e2e/src/application/builders/workspace/suite-builder-options.ts index c52699129e..4aeee355ff 100644 --- a/packages/cli/e2e/src/application/builders/workspace/suite-builder-options.ts +++ b/packages/cli/e2e/src/application/builders/workspace/suite-builder-options.ts @@ -29,6 +29,7 @@ export function defaultRunOptions(overrides: Partial = {}): RunOptio PLUGIN.AUTH, ], samples: true, + cache: true, cleanup: false, format: REPORT_FORMAT.NDJSON, reportPath: undefined, diff --git a/packages/cli/e2e/src/application/builders/workspace/workspace-builder.ts b/packages/cli/e2e/src/application/builders/workspace/workspace-builder.ts index 8f0ced255b..d3f1f180bd 100644 --- a/packages/cli/e2e/src/application/builders/workspace/workspace-builder.ts +++ b/packages/cli/e2e/src/application/builders/workspace/workspace-builder.ts @@ -11,6 +11,7 @@ export interface WorkspaceBuilder { withProjectName(name: string): WorkspaceBuilder; withDatabase(database: DatabaseEngine): WorkspaceBuilder; withPackageSource(source: PackageSource): WorkspaceBuilder; + withCache(enabled?: boolean): WorkspaceBuilder; withCleanup(enabled?: boolean): WorkspaceBuilder; buildOptions(): RunOptions; } @@ -44,6 +45,10 @@ export function createWorkspaceBuilder(initial: RunOptions): WorkspaceBuilder { options = { ...options, packageSource: source }; return this; }, + withCache(enabled = true) { + options = { ...options, cache: enabled }; + return this; + }, withCleanup(enabled = true) { options = { ...options, cleanup: enabled }; return this; diff --git a/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts b/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts index 5f77f284e0..8a29bb2376 100644 --- a/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts +++ b/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts @@ -61,6 +61,21 @@ function withPluginPort(script: string, previousPort: number, port: number): str } function runtimeWaitGate(resource: AspireResource): GateDefinition { + if (resource === ASPIRE_RESOURCE.WORKERS) { + return commandGate( + `runtime.wait.${resource}`, + `Wait for ${resource}`, + GATE_PHASE.RUNTIME, + (context) => [ + 'deno', + 'run', + '--allow-run=aspire', + `${context.project.repoRoot}/packages/cli/e2e/src/application/gates/scaffold/wait-for-workers-runtime.ts`, + context.project.appHost, + ], + ); + } + return commandGate( `runtime.wait.${resource}`, `Wait for ${resource}`, @@ -459,7 +474,8 @@ const ASPIRE_START_SCRIPT = [ ' return trimmed.slice(objectIndex);', '}', ].join('\n'); -function runtimeResources(database: DatabaseEngine): readonly AspireResource[] { +/** List the Aspire resources that a runtime suite waits for. */ +export function runtimeResources(database: DatabaseEngine): readonly AspireResource[] { return [ ...databaseRuntimeResources(database), ASPIRE_RESOURCE.GARNET, diff --git a/packages/cli/e2e/src/application/gates/scaffold/scaffold-gates.ts b/packages/cli/e2e/src/application/gates/scaffold/scaffold-gates.ts index 83edf4c863..5ed3127438 100644 --- a/packages/cli/e2e/src/application/gates/scaffold/scaffold-gates.ts +++ b/packages/cli/e2e/src/application/gates/scaffold/scaffold-gates.ts @@ -6,6 +6,8 @@ import type { PluginSuiteState } from '../../builders/scaffold/plugin-suite-stat import { cli, commandGate } from './gate-factory.ts'; import { createPluginInstallGates } from './plugin-install-gates.ts'; +const DISABLE_CACHE_ARGUMENT = '--cache=false'; + /** Create preflight gates for required CLI tooling. */ export function createPreflightGates(): readonly GateDefinition[] { return [ @@ -32,6 +34,8 @@ function scaffoldInitCommand(context: RunContext): readonly string[] { ); } + const cacheArgs = context.request.options.cache ? [] : [DISABLE_CACHE_ARGUMENT]; + return cli( context, 'init', @@ -40,6 +44,7 @@ function scaffoldInitCommand(context: RunContext): readonly string[] { context.project.smokeRoot, '--db', context.request.options.database, + ...cacheArgs, '--service', '--service-name', 'users', diff --git a/packages/cli/e2e/src/application/gates/scaffold/wait-for-workers-runtime.ts b/packages/cli/e2e/src/application/gates/scaffold/wait-for-workers-runtime.ts new file mode 100644 index 0000000000..ccea3d131d --- /dev/null +++ b/packages/cli/e2e/src/application/gates/scaffold/wait-for-workers-runtime.ts @@ -0,0 +1,67 @@ +const appHost = Deno.args[0]; +if (!appHost) throw new Error('AppHost path argument is required'); + +const resource = 'workers'; +const readyMarkers = [ + '[Scheduler] Started with', + 'Starting with Web Worker pool', +] as const; +const maxAttempts = 90; +const pollIntervalMs = 2_000; + +await runAspire([ + 'wait', + resource, + '--apphost', + appHost, + '--non-interactive', + '--nologo', +]); + +let lastLogs = ''; +for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const result = await runAspire([ + 'logs', + resource, + '--apphost', + appHost, + '-n', + '200', + ], false); + lastLogs = result.output; + if (result.success && readyMarkers.every((marker) => lastLogs.includes(marker))) { + console.info(`workers runtime ready after ${attempt} log probe(s)`); + break; + } + if (attempt === maxAttempts) { + throw new Error( + `workers process became healthy without runtime startup evidence; last logs:\n${ + tail(lastLogs) + }`, + ); + } + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); +} + +async function runAspire( + args: readonly string[], + requireSuccess = true, +): Promise> { + const result = await new Deno.Command('aspire', { + args: [...args], + stdout: 'piped', + stderr: 'piped', + }).output(); + const decoder = new TextDecoder(); + const stdout = decoder.decode(result.stdout); + const stderr = decoder.decode(result.stderr); + const output = `${stdout}\n${stderr}`.trim(); + if (requireSuccess && !result.success) { + throw new Error(`aspire ${args.join(' ')} failed: ${tail(output)}`); + } + return { success: result.success, output }; +} + +function tail(value: string): string { + return value.length > 4_000 ? value.slice(-4_000) : value; +} diff --git a/packages/cli/e2e/src/application/runner/suite-runner.ts b/packages/cli/e2e/src/application/runner/suite-runner.ts index 2ba4c0399c..4cfbd0eccb 100644 --- a/packages/cli/e2e/src/application/runner/suite-runner.ts +++ b/packages/cli/e2e/src/application/runner/suite-runner.ts @@ -3,7 +3,7 @@ import type { StepResult } from '../../domain/report.ts'; import type { GateDefinition } from '../../domain/gate-definition.ts'; import type { RunRequest } from '../../domain/run-context.ts'; import type { SuiteDefinition } from '../../domain/suite-definition.ts'; -import { GATE, SCAFFOLD } from '../../domain/cli-surface.ts'; +import { EXPENSIVE_RUNTIME_SUITE_IDS, GATE } from '../../domain/cli-surface.ts'; import type { Clock } from '../../ports/clock.ts'; import type { CommandExecutor } from '../../ports/command-executor.ts'; import type { DockerResourceCleaner } from '../../ports/docker-resource-cleaner.ts'; @@ -58,7 +58,7 @@ export interface SuiteRunner { export function createSuiteRunner(options: SuiteRunnerOptions): SuiteRunner { return { async run(suite, request) { - const lease = suite.id === SCAFFOLD.RUNTIME + const lease = EXPENSIVE_RUNTIME_SUITE_IDS.some((suiteId) => suiteId === suite.id) ? await options.suiteLeaseManager.acquire(suite.id, request.options.repoRoot) : undefined; try { diff --git a/packages/cli/e2e/src/create-default-runner.ts b/packages/cli/e2e/src/create-default-runner.ts index 1aa2748be4..f7a53f52ac 100644 --- a/packages/cli/e2e/src/create-default-runner.ts +++ b/packages/cli/e2e/src/create-default-runner.ts @@ -61,6 +61,7 @@ export function defaultRunOptions(overrides: Partial = {}): RunOptio packageSource: overrides.packageSource ?? PACKAGE_SOURCE.LOCAL, plugins: [PLUGIN.WORKER, PLUGIN.SAGA, PLUGIN.TRIGGER, PLUGIN.STREAM, PLUGIN.AUTH, PLUGIN.AI], samples: true, + cache: true, cleanup: false, format: REPORT_FORMAT.NDJSON, reportPath: undefined, diff --git a/packages/cli/e2e/src/domain/cli-surface.ts b/packages/cli/e2e/src/domain/cli-surface.ts index ed6e9c5eab..5dc7997fe1 100644 --- a/packages/cli/e2e/src/domain/cli-surface.ts +++ b/packages/cli/e2e/src/domain/cli-surface.ts @@ -7,9 +7,16 @@ export const SCAFFOLD = { INFRASTRUCTURE: 'scaffold.infrastructure', PLUGIN: 'scaffold.plugins', RUNTIME: 'scaffold.runtime', + RUNTIME_SQLITE: 'scaffold.runtime.sqlite', USERLAND_INSTALL: 'scaffold.userland-install', } as const; +/** Suite ids that require exclusive access to the expensive runtime path. */ +export const EXPENSIVE_RUNTIME_SUITE_IDS = [ + SCAFFOLD.RUNTIME, + SCAFFOLD.RUNTIME_SQLITE, +] as const; + /** Stable titles for built-in suites. */ export const SCAFFOLD_TITLE = { SERVICE: 'Service scaffold capability smoke', @@ -17,6 +24,7 @@ export const SCAFFOLD_TITLE = { INFRASTRUCTURE: 'Infrastructure scaffold capability smoke', PLUGIN: 'Official plugin scaffold smoke', RUNTIME: 'Runtime scaffold capability smoke', + RUNTIME_SQLITE: 'Runtime scaffold capability smoke (sqlite, reduced containers)', USERLAND_INSTALL: 'True userland plugin install smoke', } as const; @@ -153,6 +161,8 @@ export const ASPIRE_RESOURCE = { } as const; export type ScaffoldSuiteId = typeof SCAFFOLD[keyof typeof SCAFFOLD]; +/** Built-in suites that require exclusive access to the expensive runtime path. */ +export type ExpensiveRuntimeSuiteId = typeof EXPENSIVE_RUNTIME_SUITE_IDS[number]; export type DeploySuiteId = typeof DEPLOY[keyof typeof DEPLOY]; export type SuiteId = ScaffoldSuiteId | DeploySuiteId; export type GatePhase = typeof GATE_PHASE[keyof typeof GATE_PHASE]; diff --git a/packages/cli/e2e/src/domain/run-context.ts b/packages/cli/e2e/src/domain/run-context.ts index b3529de218..0f805f5fd6 100644 --- a/packages/cli/e2e/src/domain/run-context.ts +++ b/packages/cli/e2e/src/domain/run-context.ts @@ -12,6 +12,7 @@ export interface RunOptions { readonly packageSource: PackageSource; readonly plugins: readonly PluginKind[]; readonly samples: boolean; + readonly cache: boolean; readonly cleanup: boolean; readonly format: ReportFormat; readonly reportPath?: string; diff --git a/packages/cli/e2e/src/presentation/cli/commands/full-command.ts b/packages/cli/e2e/src/presentation/cli/commands/full-command.ts index 9de348d977..48df6ad72d 100644 --- a/packages/cli/e2e/src/presentation/cli/commands/full-command.ts +++ b/packages/cli/e2e/src/presentation/cli/commands/full-command.ts @@ -23,6 +23,8 @@ export function createFullCommand(createRunner: CliRunnerFactory) { .option('--plugins ', 'Comma-separated plugin kinds') .option('--samples', 'Include generated samples', { default: true }) .option('--no-samples', 'Skip generated samples') + .option('--cache', 'Scaffold a shared cache resource', { default: true }) + .option('--no-cache', 'Skip the shared cache resource') .option('--cleanup', 'Stop Aspire and remove suite-created Docker containers', { default: true, }) diff --git a/packages/cli/e2e/src/presentation/cli/commands/run-command.ts b/packages/cli/e2e/src/presentation/cli/commands/run-command.ts index f7f78600b5..7631d97ae8 100644 --- a/packages/cli/e2e/src/presentation/cli/commands/run-command.ts +++ b/packages/cli/e2e/src/presentation/cli/commands/run-command.ts @@ -14,15 +14,15 @@ export function createRunCommand(createRunner: CliRunnerFactory) { .option('--cli ', 'CLI entrypoint') .option('--smoke-root ', 'Generated project parent directory') .option('--name ', 'Generated project name') - .option('--db ', 'Database engine: postgres, mysql, sqlite, or mssql', { - default: 'postgres', - }) + .option('--db ', 'Database engine: postgres, mysql, sqlite, or mssql') .option('--source ', 'Package source: auto, starter, local, or jsr', { default: 'local', }) .option('--plugins ', 'Comma-separated plugin kinds') .option('--samples', 'Include generated samples', { default: true }) .option('--no-samples', 'Skip generated samples') + .option('--cache', 'Scaffold a shared cache resource') + .option('--no-cache', 'Skip the shared cache resource') .option('--cleanup', 'Stop Aspire and remove suite-created Docker containers', { default: false, }) diff --git a/packages/cli/e2e/src/presentation/cli/options/run-options.ts b/packages/cli/e2e/src/presentation/cli/options/run-options.ts index cd6f5bfaf4..88b400f6f0 100644 --- a/packages/cli/e2e/src/presentation/cli/options/run-options.ts +++ b/packages/cli/e2e/src/presentation/cli/options/run-options.ts @@ -18,6 +18,7 @@ export interface RawRunOptions { readonly source?: string; readonly plugins?: string; readonly samples?: boolean; + readonly cache?: boolean; readonly cleanup?: boolean; readonly format?: string; readonly report?: string; @@ -35,6 +36,7 @@ export function mapRunOptions(raw: RawRunOptions): Partial { packageSource: parseSource(raw.source), plugins: parsePlugins(raw.plugins), samples: raw.samples, + cache: raw.cache, cleanup: raw.cleanup, format: parseFormat(raw.format), reportPath: raw.report, diff --git a/packages/cli/e2e/suites/scaffold/capability-suites.ts b/packages/cli/e2e/suites/scaffold/capability-suites.ts index 85aa5321bd..df9232f764 100644 --- a/packages/cli/e2e/suites/scaffold/capability-suites.ts +++ b/packages/cli/e2e/suites/scaffold/capability-suites.ts @@ -6,6 +6,7 @@ import { SCAFFOLD_TITLE, type SuiteId, } from '../../src/domain/cli-surface.ts'; +import { DATABASE } from '../../src/domain/extension-axes.ts'; import type { RunOptions } from '../../src/domain/run-context.ts'; import type { SuiteDefinition } from '../../src/domain/suite-definition.ts'; @@ -14,6 +15,7 @@ export interface ScaffoldCapabilitySuite { readonly id: SuiteId; readonly title: string; readonly gates: readonly GateId[]; + readonly defaults?: Partial; } const SERVICE_GATES = [ @@ -117,6 +119,12 @@ const RUNTIME_GATES = [ GATE.CLEANUP_ASPIRE_STOP, ] as const; +// The generated users service currently probes Prisma with a tagged raw query that +// is not supported by the libSQL adapter. Keep that product-health assertion in +// the Postgres merge-readiness suite while the reduced-container tier exercises +// every provider-neutral runtime behavior. +const RUNTIME_SQLITE_GATES = RUNTIME_GATES.filter((gate) => gate !== GATE.BEHAVIOR_SERVICE_HEALTH); + const PLUGIN_GATES = [ GATE.PREFLIGHT_DENO, GATE.SCAFFOLD_INIT, @@ -163,6 +171,12 @@ export const scaffoldCapabilitySuites: readonly ScaffoldCapabilitySuite[] = [ title: SCAFFOLD_TITLE.RUNTIME, gates: RUNTIME_GATES, }, + { + id: SCAFFOLD.RUNTIME_SQLITE, + title: SCAFFOLD_TITLE.RUNTIME_SQLITE, + gates: RUNTIME_SQLITE_GATES, + defaults: { database: DATABASE.SQLITE, cache: false }, + }, ]; /** Build one scaffold capability smoke suite. */ @@ -170,45 +184,49 @@ export function createScaffoldCapabilitySuite( capability: ScaffoldCapabilitySuite, overrides: Partial = {}, ): SuiteDefinition { + const resolved = { ...capability.defaults, ...overrides }; const suite = defineCliE2eSuite() .withId(capability.id) .withTitle(capability.title) .withWorkspace((workspace) => { let next = workspace; - if (overrides.repoRoot) next = next.withRepoRoot(overrides.repoRoot); - if (overrides.cliEntrypoint) { - next = next.withCliEntrypoint(overrides.cliEntrypoint); + if (resolved.repoRoot) next = next.withRepoRoot(resolved.repoRoot); + if (resolved.cliEntrypoint) { + next = next.withCliEntrypoint(resolved.cliEntrypoint); + } + if (resolved.smokeRoot) next = next.withSmokeRoot(resolved.smokeRoot); + if (resolved.projectName) { + next = next.withProjectName(resolved.projectName); } - if (overrides.smokeRoot) next = next.withSmokeRoot(overrides.smokeRoot); - if (overrides.projectName) { - next = next.withProjectName(overrides.projectName); + if (resolved.database) next = next.withDatabase(resolved.database); + if (resolved.packageSource) { + next = next.withPackageSource(resolved.packageSource); } - if (overrides.database) next = next.withDatabase(overrides.database); - if (overrides.packageSource) { - next = next.withPackageSource(overrides.packageSource); + if (resolved.cache !== undefined) { + next = next.withCache(resolved.cache); } - if (overrides.cleanup !== undefined) { - next = next.withCleanup(overrides.cleanup); + if (resolved.cleanup !== undefined) { + next = next.withCleanup(resolved.cleanup); } return next; }) .withScaffold((scaffold) => scaffold.withOfficialPluginSuite((plugins) => { - let next = plugins.withSamples(overrides.samples ?? true); - if (overrides.plugins) { - next = next.withSamples(overrides.samples ?? true); - for (const kind of overrides.plugins) next = next.withOfficial(kind); + let next = plugins.withSamples(resolved.samples ?? true); + if (resolved.plugins) { + next = next.withSamples(resolved.samples ?? true); + for (const kind of resolved.plugins) next = next.withOfficial(kind); } return next; }) ) .withReporting((reporting) => { let next = reporting; - if (overrides.format === 'pretty') next = next.withPretty(); - if (overrides.format === 'json') next = next.withJson(); - if (overrides.format === 'ndjson') next = next.withNdjson(); - if (overrides.reportPath) next = next.withReport(overrides.reportPath); - if (overrides.logFile) next = next.withLogFile(overrides.logFile); + if (resolved.format === 'pretty') next = next.withPretty(); + if (resolved.format === 'json') next = next.withJson(); + if (resolved.format === 'ndjson') next = next.withNdjson(); + if (resolved.reportPath) next = next.withReport(resolved.reportPath); + if (resolved.logFile) next = next.withLogFile(resolved.logFile); return next; }) .build(); diff --git a/packages/cli/e2e/tests/adapters/commands/docker-resource-cleaner_test.ts b/packages/cli/e2e/tests/adapters/commands/docker-resource-cleaner_test.ts new file mode 100644 index 0000000000..7703e5f617 --- /dev/null +++ b/packages/cli/e2e/tests/adapters/commands/docker-resource-cleaner_test.ts @@ -0,0 +1,70 @@ +import { assertEquals, assertRejects, assertStringIncludes } from '@std/assert'; +import { DockerCliResourceCleaner } from '../../../src/adapters/commands/docker-resource-cleaner.ts'; + +const encoder = new TextEncoder(); + +Deno.test('docker cleaner treats an absent docker binary as an empty snapshot', async () => { + const warnings: string[] = []; + const cleaner = new DockerCliResourceCleaner( + () => Promise.reject(new Deno.errors.NotFound('docker')), + (warning) => { + warnings.push(warning); + return Promise.resolve(); + }, + ); + + assertEquals(await cleaner.captureSnapshot(), { containerIds: [] }); + assertEquals(warnings.length, 1); + assertStringIncludes(warnings[0], 'docker executable was not found'); +}); + +Deno.test('docker cleaner treats a non-zero docker ps as an empty snapshot', async () => { + const warnings: string[] = []; + const cleaner = new DockerCliResourceCleaner( + () => Promise.resolve(commandOutput(1, '', 'daemon unavailable')), + (warning) => { + warnings.push(warning); + return Promise.resolve(); + }, + ); + + assertEquals(await cleaner.captureSnapshot(), { containerIds: [] }); + assertEquals(warnings.length, 1); + assertStringIncludes(warnings[0], 'docker ps failed: daemon unavailable'); +}); + +Deno.test('docker cleaner returns no resources when the snapshot has no new containers', async () => { + const commands: string[][] = []; + const cleaner = new DockerCliResourceCleaner((args) => { + commands.push([...args]); + return Promise.resolve(commandOutput(0, 'existing-a\nexisting-b\n')); + }); + + const removed = await cleaner.pruneCreatedResources({ + containerIds: ['existing-a', 'existing-b'], + }); + + assertEquals(removed, []); + assertEquals(commands, [['ps', '-a', '--format', '{{.ID}}']]); +}); + +Deno.test('docker cleaner still throws when removing a created container fails', async () => { + const cleaner = new DockerCliResourceCleaner((args) => { + if (args[0] === 'ps') return Promise.resolve(commandOutput(0, 'existing-a\ncreated-b\n')); + return Promise.resolve(commandOutput(1, '', 'permission denied')); + }); + + await assertRejects( + () => cleaner.pruneCreatedResources({ containerIds: ['existing-a'] }), + Error, + 'docker rm -f created-b failed: permission denied', + ); +}); + +function commandOutput(code: number, stdout = '', stderr = '') { + return { + code, + stdout: encoder.encode(stdout), + stderr: encoder.encode(stderr), + }; +} diff --git a/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts b/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts index 0b23a0fc34..a64a5c46a6 100644 --- a/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts +++ b/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts @@ -143,6 +143,31 @@ Deno.test('runtime gates prove MCP Aspire endpoint discovery against the live Ap ]); }); +Deno.test('workers wait gate requires runtime startup evidence before behavior gates', () => { + const gate = createRuntimeGates(DATABASE.SQLITE).find((entry) => + entry.id === GATE.RUNTIME_WAIT_WORKERS + ); + if (gate?.kind !== 'command') { + throw new Error('Expected workers wait gate to be a command gate.'); + } + + assertEquals( + gate.command({ + project: { + repoRoot: '/repo', + appHost: '/workspace/app/aspire/apphost.mts', + }, + } as RunContext), + [ + 'deno', + 'run', + '--allow-run=aspire', + '/repo/packages/cli/e2e/src/application/gates/scaffold/wait-for-workers-runtime.ts', + '/workspace/app/aspire/apphost.mts', + ], + ); +}); + Deno.test('AI chat route gate captures generated registry import failures', () => { const gate = createRuntimeGates().find((entry) => entry.id === GATE.BEHAVIOR_AI_CHAT_ROUTE); if (gate?.kind !== 'command') { diff --git a/packages/cli/e2e/tests/application/gates/command-gate_test.ts b/packages/cli/e2e/tests/application/gates/command-gate_test.ts index 85667224cb..3db7e72a13 100644 --- a/packages/cli/e2e/tests/application/gates/command-gate_test.ts +++ b/packages/cli/e2e/tests/application/gates/command-gate_test.ts @@ -122,6 +122,7 @@ function createContext(): RunContext { packageSource: 'local', plugins: [], samples: true, + cache: true, cleanup: true, format: 'json', commandTimeoutMs: 30_000, diff --git a/packages/cli/e2e/tests/application/gates/http-gate_test.ts b/packages/cli/e2e/tests/application/gates/http-gate_test.ts index 6b96404abd..4dd42f8b4b 100644 --- a/packages/cli/e2e/tests/application/gates/http-gate_test.ts +++ b/packages/cli/e2e/tests/application/gates/http-gate_test.ts @@ -40,6 +40,7 @@ function createContext(): RunContext { packageSource: 'local', plugins: [], samples: true, + cache: true, cleanup: true, format: 'json', commandTimeoutMs: 30_000, diff --git a/packages/cli/e2e/tests/application/gates/scaffold-gates_test.ts b/packages/cli/e2e/tests/application/gates/scaffold-gates_test.ts index 55dc40367f..ec577623cd 100644 --- a/packages/cli/e2e/tests/application/gates/scaffold-gates_test.ts +++ b/packages/cli/e2e/tests/application/gates/scaffold-gates_test.ts @@ -23,6 +23,33 @@ Deno.test('--source jsr accepts the local public CLI binary', () => { assertEquals(command.slice(0, 4), ['deno', 'run', '-A', '/repo/packages/cli/bin/netscript.ts']); }); +Deno.test('scaffold init default command remains byte-identical', () => { + assertEquals( + scaffoldInitGate().command( + createContext('/repo/packages/cli/bin/netscript.ts', PACKAGE_SOURCE.LOCAL), + ), + [ + 'deno', + 'run', + '-A', + '/repo/packages/cli/bin/netscript.ts', + 'init', + 'prod-local-test', + '--path', + '/repo/.llm/tmp/cli-e2e', + '--db', + 'postgres', + '--service', + '--service-name', + 'users', + '--ci', + '--yes', + '--no-git', + '--force', + ], + ); +}); + Deno.test('scaffold runtime exercises the generated service port default', () => { const command = scaffoldInitGate().command( createContext('/repo/packages/cli/bin/netscript.ts', PACKAGE_SOURCE.JSR), @@ -32,6 +59,14 @@ Deno.test('scaffold runtime exercises the generated service port default', () => assertEquals(command.includes('3001'), false); }); +Deno.test('scaffold init disables the cache exactly once', () => { + const command = scaffoldInitGate().command( + createContext('/repo/packages/cli/bin/netscript.ts', PACKAGE_SOURCE.LOCAL, false), + ); + + assertEquals(command.filter((argument) => argument === '--cache=false'), ['--cache=false']); +}); + Deno.test('--source jsr rejects the local contributor CLI binary', () => { assertThrows( () => @@ -124,6 +159,7 @@ function scaffoldInitGate(): CommandGateDefinition { function createContext( cliEntrypoint: string, packageSource: RunOptions['packageSource'], + cache = true, ): RunContext { const options: RunOptions = { repoRoot: '/repo', @@ -134,6 +170,7 @@ function createContext( packageSource, plugins: [], samples: false, + cache, cleanup: true, format: REPORT_FORMAT.PRETTY, commandTimeoutMs: 1, diff --git a/packages/cli/e2e/tests/application/runner/gate-runner_test.ts b/packages/cli/e2e/tests/application/runner/gate-runner_test.ts index ad80e6d364..1918bdde2d 100644 --- a/packages/cli/e2e/tests/application/runner/gate-runner_test.ts +++ b/packages/cli/e2e/tests/application/runner/gate-runner_test.ts @@ -58,6 +58,7 @@ function createContext(): RunContext { packageSource: 'local', plugins: [], samples: false, + cache: true, cleanup: false, format: 'json', commandTimeoutMs: 1_000, diff --git a/packages/cli/e2e/tests/application/runner/suite-runner_test.ts b/packages/cli/e2e/tests/application/runner/suite-runner_test.ts index 5aa8b0187d..99995e490b 100644 --- a/packages/cli/e2e/tests/application/runner/suite-runner_test.ts +++ b/packages/cli/e2e/tests/application/runner/suite-runner_test.ts @@ -13,9 +13,15 @@ import type { HttpClient, HttpRequest, HttpResult } from '../../../src/ports/htt import type { Reporter } from '../../../src/ports/reporter.ts'; import type { PlatformPort } from '../../../src/ports/platform.ts'; import { createSuiteRunner } from '../../../src/application/runner/suite-runner.ts'; -import type { SuiteLease, SuiteLeaseManager } from '../../../src/application/runner/suite-lease.ts'; +import { DockerCliResourceCleaner } from '../../../src/adapters/commands/docker-resource-cleaner.ts'; +import { + type SuiteLease, + SuiteLeaseContentionError, + type SuiteLeaseManager, + type SuiteLeaseRecord, +} from '../../../src/application/runner/suite-lease.ts'; import { GATE, SCAFFOLD } from '../../../src/domain/cli-surface.ts'; -import type { SuiteId } from '../../../src/domain/cli-surface.ts'; +import type { ExpensiveRuntimeSuiteId, SuiteId } from '../../../src/domain/cli-surface.ts'; import type { RunOptions } from '../../../src/domain/run-context.ts'; import { createScaffoldCapabilitySuite, @@ -114,6 +120,39 @@ Deno.test('suite runner skips cleanup phase when cleanup is disabled', async () assertEquals(commands.some((request) => request.command.includes('stop')), false); }); +Deno.test('suite runner completes cleanup with a Docker-less cleaner', async () => { + const warnings: string[] = []; + const cleaner = new DockerCliResourceCleaner( + () => Promise.reject(new Deno.errors.NotFound('docker')), + (warning) => { + warnings.push(warning); + return Promise.resolve(); + }, + ); + const runtimeSuite = createScaffoldRuntimeSuite({ + repoRoot: '.', + projectName: 'runner-docker-less-cleanup-test', + cleanup: true, + format: 'json', + }); + const suite = { ...runtimeSuite, gates: [] }; + const options = { ...suite.defaultOptions, cleanup: true }; + + const report = await createSuiteRunner({ + clock: new FakeClock(), + commandExecutor: new SuccessfulCommandExecutor(), + httpClient: new FakeHttpClient(), + dockerCleaner: cleaner, + reporter: new NullReporter(), + platform: new FakePlatform(), + suiteLeaseManager: new RecordingSuiteLeaseManager(), + }).run(suite, { suiteId: suite.id, options }); + + assertEquals(report.ok, true); + assertEquals(report.steps, []); + assertEquals(warnings.length, 2); +}); + Deno.test('suite runner cleans up after a targeted non-cleanup gate when cleanup is enabled', async () => { const commands: CommandRequest[] = []; const executor: CommandExecutor = { @@ -216,6 +255,40 @@ Deno.test('suite runner releases the expensive-suite lease when suite execution await nextLease.release(); }); +Deno.test('expensive runtime suites contend for one lease in both directions', async () => { + const cases = [ + [SCAFFOLD.RUNTIME, SCAFFOLD.RUNTIME_SQLITE], + [SCAFFOLD.RUNTIME_SQLITE, SCAFFOLD.RUNTIME], + ] as const; + + for (const [holderId, contenderId] of cases) { + const leaseManager = new RecordingSuiteLeaseManager(); + const heldLease = await leaseManager.acquire(holderId, '/worktrees/holder'); + const suite = createScaffoldRuntimeSuite( + { repoRoot: '/worktrees/contender', format: 'json' }, + contenderId, + ); + const runner = createSuiteRunner({ + clock: new FakeClock(), + commandExecutor: new SuccessfulCommandExecutor(), + httpClient: new FakeHttpClient(), + reporter: new NullReporter(), + platform: new FakePlatform(), + suiteLeaseManager: leaseManager, + }); + + try { + const error = await assertRejects( + () => runner.run(suite, { suiteId: suite.id, options: suite.defaultOptions }), + SuiteLeaseContentionError, + ); + assertEquals(error.holder.suiteId, holderId); + } finally { + await heldLease.release(); + } + } +}); + Deno.test('suite runner does not interact with the lease for a cheap suite', async () => { const leaseManager = new RecordingSuiteLeaseManager(); const capability = scaffoldCapabilitySuites.find((suite) => suite.id === SCAFFOLD.SERVICE); @@ -251,9 +324,12 @@ class FakePlatform implements PlatformPort { } } -function createScaffoldRuntimeSuite(overrides: Partial) { - const capability = scaffoldCapabilitySuites.find((suite) => suite.id === SCAFFOLD.RUNTIME); - if (!capability) throw new Error('scaffold.runtime suite is not registered.'); +function createScaffoldRuntimeSuite( + overrides: Partial, + suiteId: ExpensiveRuntimeSuiteId = SCAFFOLD.RUNTIME, +) { + const capability = scaffoldCapabilitySuites.find((suite) => suite.id === suiteId); + if (!capability) throw new Error(`${suiteId} suite is not registered.`); return createScaffoldCapabilitySuite(capability, overrides); } @@ -285,15 +361,24 @@ class SuccessfulCommandExecutor implements CommandExecutor { class RecordingSuiteLeaseManager implements SuiteLeaseManager { acquisitions = 0; releases = 0; - #held = false; + #holder: SuiteLeaseRecord | undefined; - acquire(_suiteId: SuiteId, _worktree: string): Promise { - if (this.#held) return Promise.reject(new Error('lease already held')); - this.#held = true; + acquire(suiteId: SuiteId, worktree: string): Promise { + if (this.#holder) { + return Promise.reject( + new SuiteLeaseContentionError(this.#holder, '/tmp/netscript-e2e-runner-test.lease'), + ); + } + this.#holder = { + pid: 4242, + startedAt: '2026-08-04T00:00:00.000Z', + suiteId, + worktree, + }; this.acquisitions += 1; return Promise.resolve({ release: () => { - this.#held = false; + this.#holder = undefined; this.releases += 1; return Promise.resolve(); }, diff --git a/packages/cli/e2e/tests/presentation/cli-options_test.ts b/packages/cli/e2e/tests/presentation/cli-options_test.ts index 0efc12d6ed..65c622a8cf 100644 --- a/packages/cli/e2e/tests/presentation/cli-options_test.ts +++ b/packages/cli/e2e/tests/presentation/cli-options_test.ts @@ -10,6 +10,14 @@ Deno.test('mapRunOptions accepts sqlite database axis', () => { assertEquals(mapRunOptions({ db: DATABASE.SQLITE }), { database: DATABASE.SQLITE }); }); +Deno.test('mapRunOptions maps --no-cache', () => { + assertEquals(mapRunOptions({ cache: false }), { cache: false }); +}); + +Deno.test('mapRunOptions maps --cache', () => { + assertEquals(mapRunOptions({ cache: true }), { cache: true }); +}); + Deno.test('mapRunOptions rejects unsupported database values', () => { assertThrows( () => mapRunOptions({ db: 'oracle' }), diff --git a/packages/cli/e2e/tests/presentation/cli-program_test.ts b/packages/cli/e2e/tests/presentation/cli-program_test.ts index bf170c661f..3942f14122 100644 --- a/packages/cli/e2e/tests/presentation/cli-program_test.ts +++ b/packages/cli/e2e/tests/presentation/cli-program_test.ts @@ -1,6 +1,7 @@ import { assertEquals } from '@std/assert'; import { createCliProgram } from '../../src/presentation/cli/cli-program.ts'; import { SCAFFOLD } from '../../src/domain/cli-surface.ts'; +import { DATABASE } from '../../src/domain/extension-axes.ts'; import type { RunOptions, RunRequest } from '../../src/domain/run-context.ts'; import type { RunReport } from '../../src/domain/report.ts'; import type { SuiteDefinition } from '../../src/domain/suite-definition.ts'; @@ -53,6 +54,58 @@ Deno.test('full CLI command accepts run options and runs runtime suite', async ( assertEquals(calls.length, 1); assertEquals(calls[0].suite.id, SCAFFOLD.RUNTIME); assertEquals(calls[0].request.suiteId, SCAFFOLD.RUNTIME); + assertEquals(calls[0].request.options.database, DATABASE.POSTGRES); + assertEquals(calls[0].request.options.cache, true); assertEquals(calls[0].request.options.format, 'pretty'); assertEquals(calls[0].request.options.cleanup, true); }); + +Deno.test('run command lets sqlite runtime suite defaults win unless flags override them', async () => { + const calls: Array<{ suite: SuiteDefinition; request: RunRequest; options: RunOptions }> = []; + const command = createCliProgram((options) => ({ + run(suite, request): Promise { + calls.push({ suite, request, options }); + return Promise.resolve({ + ok: true, + suiteId: suite.id, + projectRoot: options.smokeRoot, + startedAt: new Date(0).toISOString(), + durationMs: 0, + steps: [], + summary: { passed: 0, failed: 0, skipped: 0 }, + }); + }, + })); + + await command.parse(['run', SCAFFOLD.RUNTIME_SQLITE]); + + assertEquals(calls.length, 1); + assertEquals(calls[0].suite.id, SCAFFOLD.RUNTIME_SQLITE); + assertEquals(calls[0].request.options.database, DATABASE.SQLITE); + assertEquals(calls[0].request.options.cache, false); +}); + +Deno.test('run command keeps explicit postgres above sqlite runtime suite default', async () => { + const calls: Array<{ request: RunRequest; options: RunOptions }> = []; + const command = createCliProgram((options) => ({ + run(_suite, request): Promise { + calls.push({ request, options }); + return Promise.resolve({ + ok: true, + suiteId: request.suiteId, + projectRoot: options.smokeRoot, + startedAt: new Date(0).toISOString(), + durationMs: 0, + steps: [], + summary: { passed: 0, failed: 0, skipped: 0 }, + }); + }, + })); + + await command.parse(['run', SCAFFOLD.RUNTIME_SQLITE, '--db', DATABASE.POSTGRES]); + + assertEquals(calls.length, 1); + assertEquals(calls[0].request.options.database, DATABASE.POSTGRES); + assertEquals(calls[0].options.database, DATABASE.POSTGRES); + assertEquals(calls[0].options.cache, false); +}); diff --git a/packages/cli/e2e/tests/presentation/suite-registry_test.ts b/packages/cli/e2e/tests/presentation/suite-registry_test.ts index 215cc6411e..2b52c05297 100644 --- a/packages/cli/e2e/tests/presentation/suite-registry_test.ts +++ b/packages/cli/e2e/tests/presentation/suite-registry_test.ts @@ -1,7 +1,19 @@ import { assertEquals } from '@std/assert'; import { DEPLOY, GATE, SCAFFOLD } from '../../src/domain/cli-surface.ts'; -import { DATABASE } from '../../src/domain/extension-axes.ts'; +import { + DATABASE, + PACKAGE_SOURCE, + PLUGIN, + REPORT_FORMAT, +} from '../../src/domain/extension-axes.ts'; +import type { RunOptions } from '../../src/domain/run-context.ts'; +import { runtimeResources } from '../../src/application/gates/scaffold/runtime-gates.ts'; import { builtInSuites, resolveSuite } from '../../src/presentation/cli/suites/registry.ts'; +import { + createScaffoldCapabilitySuite, + type ScaffoldCapabilitySuite, + scaffoldCapabilitySuites, +} from '../../suites/scaffold/capability-suites.ts'; Deno.test('registry exposes scaffold capability suites from constants', () => { assertEquals(builtInSuites.map((suite) => suite.id), [ @@ -10,6 +22,7 @@ Deno.test('registry exposes scaffold capability suites from constants', () => { SCAFFOLD.INFRASTRUCTURE, SCAFFOLD.PLUGIN, SCAFFOLD.RUNTIME, + SCAFFOLD.RUNTIME_SQLITE, SCAFFOLD.USERLAND_INSTALL, DEPLOY.TARGETS, DEPLOY.DESKTOP_NATIVE, @@ -158,6 +171,93 @@ Deno.test('runtime suite omits database resource wait for sqlite', () => { assertEquals(runtime.gates.some((gate) => gate.id === GATE.RUNTIME_WAIT_GARNET), true); }); +Deno.test('sqlite runtime suite resolves its reduced-container defaults without mutating cache mode', () => { + const environmentVariable = 'NETSCRIPT_CACHE_MODE'; + const previous = Deno.env.get(environmentVariable); + const operatorCacheMode = 'Container'; + try { + Deno.env.delete(environmentVariable); + const sqlite = resolveSuite(SCAFFOLD.RUNTIME_SQLITE); + assertEquals(sqlite.defaultOptions.database, DATABASE.SQLITE); + assertEquals(sqlite.defaultOptions.cache, false); + assertEquals(Deno.env.get(environmentVariable), undefined); + + Deno.env.set(environmentVariable, operatorCacheMode); + resolveSuite(SCAFFOLD.RUNTIME_SQLITE); + assertEquals(Deno.env.get(environmentVariable), operatorCacheMode); + } finally { + if (previous === undefined) Deno.env.delete(environmentVariable); + else Deno.env.set(environmentVariable, previous); + } +}); + +Deno.test('sqlite runtime suite excludes only the libSQL-incompatible users health gate', () => { + const sqlite = resolveSuite(SCAFFOLD.RUNTIME_SQLITE); + const postgres = resolveSuite(SCAFFOLD.RUNTIME); + const runtimeCapability = scaffoldCapabilitySuites.find((suite) => suite.id === SCAFFOLD.RUNTIME); + const sqliteCapability = scaffoldCapabilitySuites.find((suite) => + suite.id === SCAFFOLD.RUNTIME_SQLITE + ); + if (!runtimeCapability || !sqliteCapability) { + throw new Error('Runtime capability suites are not registered.'); + } + + assertEquals(sqlite.gates.some((gate) => gate.id === GATE.BEHAVIOR_SERVICE_HEALTH), false); + assertEquals(postgres.gates.some((gate) => gate.id === GATE.BEHAVIOR_SERVICE_HEALTH), true); + assertEquals( + sqliteCapability.gates, + runtimeCapability.gates.filter((gate) => gate !== GATE.BEHAVIOR_SERVICE_HEALTH), + ); +}); + +Deno.test('sqlite runtime suite keeps explicit database overrides above suite defaults', () => { + const sqlite = resolveSuite(SCAFFOLD.RUNTIME_SQLITE, { + database: DATABASE.POSTGRES, + }); + assertEquals(sqlite.defaultOptions.database, DATABASE.POSTGRES); + assertEquals(sqlite.defaultOptions.cache, false); +}); + +Deno.test('runtime suite wait matrices match runtime resources for postgres and sqlite', () => { + const runtimeCapability = scaffoldCapabilitySuites.find((suite) => suite.id === SCAFFOLD.RUNTIME); + const sqliteCapability = scaffoldCapabilitySuites.find((suite) => + suite.id === SCAFFOLD.RUNTIME_SQLITE + ); + if (!runtimeCapability || !sqliteCapability) { + throw new Error('Runtime capability suites are not registered.'); + } + assertEquals( + sqliteCapability.gates, + runtimeCapability.gates.filter((gate) => gate !== GATE.BEHAVIOR_SERVICE_HEALTH), + ); + + const cases = [ + [resolveSuite(SCAFFOLD.RUNTIME), DATABASE.POSTGRES], + [resolveSuite(SCAFFOLD.RUNTIME_SQLITE), DATABASE.SQLITE], + ] as const; + for (const [suite, database] of cases) { + const waitGateIds = suite.gates + .map((gate) => gate.id) + .filter((id) => id.startsWith('runtime.wait.')); + assertEquals( + waitGateIds, + runtimeResources(database).map((resource) => `runtime.wait.${resource}`), + suite.id, + ); + } + + const sqliteGateIds = cases[1][0].gates.map((gate) => gate.id); + assertEquals(sqliteGateIds.includes(GATE.RUNTIME_WAIT_GARNET), true); + assertEquals(sqliteGateIds.includes(GATE.RUNTIME_WAIT_POSTGRES), false); + assertEquals(sqliteGateIds.includes(GATE.RUNTIME_WAIT_MYSQL), false); + assertEquals(sqliteGateIds.includes(GATE.RUNTIME_WAIT_MSSQL), false); + + const runtimeGateIds = cases[0][0].gates.map((gate) => gate.id); + assertEquals(runtimeGateIds.includes(GATE.RUNTIME_WAIT_POSTGRES), true); + assertEquals(runtimeGateIds.includes(GATE.RUNTIME_WAIT_MYSQL), false); + assertEquals(runtimeGateIds.includes(GATE.RUNTIME_WAIT_MSSQL), false); +}); + Deno.test('runtime suite selects mssql database resource wait for mssql', () => { const runtime = resolveSuite(SCAFFOLD.RUNTIME, { database: DATABASE.MSSQL }); assertEquals(runtime.defaultOptions.database, DATABASE.MSSQL); @@ -165,3 +265,101 @@ Deno.test('runtime suite selects mssql database resource wait for mssql', () => assertEquals(runtime.gates.some((gate) => gate.id === GATE.RUNTIME_WAIT_MYSQL), false); assertEquals(runtime.gates.some((gate) => gate.id === GATE.RUNTIME_WAIT_MSSQL), true); }); + +Deno.test('capability defaults are a baseline and caller overrides select database gates', () => { + const capability: ScaffoldCapabilitySuite = { + id: SCAFFOLD.RUNTIME, + title: 'Synthetic runtime default precedence', + gates: [ + GATE.RUNTIME_WAIT_POSTGRES, + GATE.RUNTIME_WAIT_MYSQL, + GATE.RUNTIME_WAIT_MSSQL, + GATE.RUNTIME_WAIT_GARNET, + ], + defaults: { database: DATABASE.SQLITE }, + }; + + const defaulted = createScaffoldCapabilitySuite(capability); + assertEquals(defaulted.defaultOptions.database, DATABASE.SQLITE); + assertEquals(defaulted.gates.map((gate) => gate.id), [GATE.RUNTIME_WAIT_GARNET]); + + const overridden = createScaffoldCapabilitySuite(capability, { + database: DATABASE.POSTGRES, + }); + assertEquals(overridden.defaultOptions.database, DATABASE.POSTGRES); + assertEquals(overridden.gates.map((gate) => gate.id), [ + GATE.RUNTIME_WAIT_POSTGRES, + GATE.RUNTIME_WAIT_GARNET, + ]); +}); + +Deno.test('existing built-in suites preserve their exact resolved options', () => { + assertEquals( + scaffoldCapabilitySuites + .filter((suite) => suite.id !== SCAFFOLD.RUNTIME_SQLITE) + .map((suite) => suite.defaults), + [undefined, undefined, undefined, undefined, undefined], + ); + + const overrides: Partial = { + repoRoot: '/repo', + cliEntrypoint: '/cli.ts', + smokeRoot: '/smoke', + projectName: 'existing-suite-baseline', + logFile: '/log.ndjson', + }; + const common: RunOptions = { + ...overrides, + repoRoot: '/repo', + cliEntrypoint: '/cli.ts', + smokeRoot: '/smoke', + projectName: 'existing-suite-baseline', + database: DATABASE.POSTGRES, + packageSource: PACKAGE_SOURCE.LOCAL, + plugins: [PLUGIN.WORKER, PLUGIN.SAGA, PLUGIN.TRIGGER, PLUGIN.STREAM, PLUGIN.AUTH], + samples: true, + cache: true, + cleanup: false, + format: REPORT_FORMAT.NDJSON, + reportPath: undefined, + logFile: '/log.ndjson', + commandTimeoutMs: 900_000, + httpTimeoutMs: 30_000, + }; + const expected = new Map([ + [SCAFFOLD.SERVICE, common], + [SCAFFOLD.CONTRACTS, common], + [SCAFFOLD.INFRASTRUCTURE, common], + [SCAFFOLD.PLUGIN, common], + [SCAFFOLD.RUNTIME, common], + [ + SCAFFOLD.RUNTIME_SQLITE, + { + ...common, + database: DATABASE.SQLITE, + cache: false, + }, + ], + [ + SCAFFOLD.USERLAND_INSTALL, + { + ...common, + packageSource: PACKAGE_SOURCE.AUTO, + plugins: [PLUGIN.WORKER, PLUGIN.SAGA, PLUGIN.TRIGGER, PLUGIN.STREAM], + samples: false, + }, + ], + [DEPLOY.TARGETS, { ...common, plugins: [...common.plugins, PLUGIN.AI], samples: false }], + [DEPLOY.DESKTOP_NATIVE, { + ...common, + plugins: [...common.plugins, PLUGIN.AI], + samples: false, + }], + ]); + + for (const suite of builtInSuites) { + const options = expected.get(suite.id); + if (!options) throw new Error(`Missing options baseline for "${suite.id}".`); + assertEquals(resolveSuite(suite.id, overrides).defaultOptions, options, suite.id); + } +}); diff --git a/packages/cli/src/kernel/templates/aspire/helpers/helpers-generator-pipeline.ts b/packages/cli/src/kernel/templates/aspire/helpers/helpers-generator-pipeline.ts index a968964658..dd330272b4 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/helpers-generator-pipeline.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/helpers-generator-pipeline.ts @@ -47,6 +47,9 @@ export class HelpersGeneratorPipeline { const { config, configPath, generateAppHost } = options; const templates = await loadAspireHelperTemplateAssets(); const files: GeneratedFile[] = []; + const databaseEngine = config.PrimaryDatabase + ? config.Databases[config.PrimaryDatabase]?.Engine + : undefined; // 0. Tier 2: Aspire compat shim (D-7 Node.js workaround) files.push({ @@ -92,9 +95,7 @@ export class HelpersGeneratorPipeline { services: config.Services, version: config.Version, denoDefaults: config.Defaults.Deno, - databaseEngine: config.PrimaryDatabase - ? config.Databases[config.PrimaryDatabase]?.Engine - : undefined, + databaseEngine, })); // 6. Tier 1: Register plugins (two-pass cross-ref) @@ -102,6 +103,7 @@ export class HelpersGeneratorPipeline { plugins: config.Plugins, version: config.Version, denoDefaults: config.Defaults.Deno, + databaseEngine, })); // 7. Tier 1: Register background processors @@ -109,6 +111,7 @@ export class HelpersGeneratorPipeline { processors: config.BackgroundProcessors, version: config.Version, denoDefaults: config.Defaults.Deno, + databaseEngine, })); // 8. Tier 1: Register apps diff --git a/packages/cli/src/kernel/templates/aspire/helpers/register/database-permissions.ts b/packages/cli/src/kernel/templates/aspire/helpers/register/database-permissions.ts new file mode 100644 index 0000000000..8387482311 --- /dev/null +++ b/packages/cli/src/kernel/templates/aspire/helpers/register/database-permissions.ts @@ -0,0 +1,14 @@ +import type { DatabaseEntry } from '@netscript/aspire/types'; + +/** Adds permissions required by the selected database engine without duplicates. */ +export function withDatabasePermissions( + permissions: readonly string[], + databaseEngine?: DatabaseEntry['Engine'], +): readonly string[] { + const ffiAlreadyGranted = permissions.includes('--allow-all') || + permissions.includes('--allow-ffi'); + + return databaseEngine === 'Sqlite' && !ffiAlreadyGranted + ? [...permissions, '--allow-ffi'] + : permissions; +} diff --git a/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-background.ts b/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-background.ts index 474591a8cc..2d90cb4437 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-background.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-background.ts @@ -21,6 +21,7 @@ import { SCAFFOLD_ASPIRE_MODULES } from '../../../../constants/scaffold/scaffold import { RESOURCE_DEFAULTS } from '@netscript/aspire/constants'; import { TEMPLATE_KEYS } from '../../../../assets/manifest.ts'; import { renderTemplateAssetSync } from '../../../../adapters/templates/template-asset.ts'; +import { withDatabasePermissions } from './database-permissions.ts'; /** * Generates the register-background.mts file content. @@ -29,7 +30,7 @@ import { renderTemplateAssetSync } from '../../../../adapters/templates/template * @returns Generated TypeScript source as a string */ export function generateRegisterBackground(options: RegisterBackgroundOptions): string { - const { processors, version: _version, denoDefaults } = options; + const { processors, version: _version, denoDefaults, databaseEngine } = options; const entries = Object.entries(processors); const registrationBlocks: string[] = []; @@ -40,6 +41,12 @@ export function generateRegisterBackground(options: RegisterBackgroundOptions): const entrypoint = entry.Entrypoint ?? `${name}/runtime.ts`; const telemetry = entry.Telemetry !== false; const watchMode = entry.WatchMode ?? false; + const entryPermissions = entry.Permissions + ? withDatabasePermissions(entry.Permissions, databaseEngine) + : undefined; + const defaultPermissions = entryPermissions + ? denoDefaults.Permissions + : withDatabasePermissions(denoDefaults.Permissions, databaseEngine); const lines: string[] = []; lines.push(` // --- ${name} ---`); @@ -49,12 +56,12 @@ export function generateRegisterBackground(options: RegisterBackgroundOptions): // Resolve permissions — background uses --watch (NOT --watch-hmr) lines.push(` const ${id}_perms = resolvePermissions(`); - if (entry.Permissions) { - lines.push(` ${JSON.stringify(entry.Permissions)},`); + if (entryPermissions) { + lines.push(` ${JSON.stringify(entryPermissions)},`); } else { lines.push(` undefined,`); } - lines.push(` ${JSON.stringify(denoDefaults.Permissions)},`); + lines.push(` ${JSON.stringify(defaultPermissions)},`); lines.push(` ${watchMode},`); lines.push(` '${RESOURCE_DEFAULTS.WatchFlag}',`); lines.push(` );`); diff --git a/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-plugins.ts b/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-plugins.ts index bf3cd3fecc..dbb1a8322e 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-plugins.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-plugins.ts @@ -25,6 +25,7 @@ import { RESOURCE_DEFAULTS } from '@netscript/aspire/constants'; import { TEMPLATE_KEYS } from '../../../../assets/manifest.ts'; import { renderTemplateAssetSync } from '../../../../adapters/templates/template-asset.ts'; import { netscriptJsrSpecifier } from '../../../../constants/jsr-specifiers.ts'; +import { withDatabasePermissions } from './database-permissions.ts'; import { renderHttpEndpointCall } from './render-http-endpoint.ts'; const DENO_NO_LEGACY_ABORT_FLAG = '--unstable-no-legacy-abort'; @@ -40,7 +41,7 @@ const DENO_NO_LEGACY_ABORT_FLAG = '--unstable-no-legacy-abort'; * @returns Generated TypeScript source as a string */ export function generateRegisterPlugins(options: RegisterPluginsOptions): string { - const { plugins, version: _version, denoDefaults } = options; + const { plugins, version: _version, denoDefaults, databaseEngine } = options; const entries = Object.entries(plugins); // --- Pass 1 blocks: create all plugin resources --- @@ -49,6 +50,12 @@ export function generateRegisterPlugins(options: RegisterPluginsOptions): string for (const [name, entry] of entries) { const workdir = entry.Workdir ?? '.'; const entrypoint = entry.Entrypoint ?? netscriptJsrSpecifier(`plugin-${name}`, '/services'); + const entryPermissions = entry.Permissions + ? withDatabasePermissions(entry.Permissions, databaseEngine) + : undefined; + const defaultPermissions = entryPermissions + ? denoDefaults.Permissions + : withDatabasePermissions(denoDefaults.Permissions, databaseEngine); const lines: string[] = []; lines.push(` // --- ${name} ---`); @@ -56,12 +63,12 @@ export function generateRegisterPlugins(options: RegisterPluginsOptions): string // Resolve permissions — plugins use --watch-hmr lines.push(` const perms = resolvePermissions(`); - if (entry.Permissions) { - lines.push(` ${JSON.stringify(entry.Permissions)},`); + if (entryPermissions) { + lines.push(` ${JSON.stringify(entryPermissions)},`); } else { lines.push(` undefined,`); } - lines.push(` ${JSON.stringify(denoDefaults.Permissions)},`); + lines.push(` ${JSON.stringify(defaultPermissions)},`); lines.push(` config.Defaults.Deno.WatchMode,`); lines.push(` '${RESOURCE_DEFAULTS.WatchHmrFlag}',`); lines.push(` );`); diff --git a/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-services.ts b/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-services.ts index 4ed62ee459..055ea84c32 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-services.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-services.ts @@ -26,17 +26,9 @@ import { SCAFFOLD_DIRS } from '../../../../constants/scaffold/scaffold-dirs.ts'; import { RESOURCE_DEFAULTS } from '@netscript/aspire/constants'; import { TEMPLATE_KEYS } from '../../../../assets/manifest.ts'; import { renderTemplateAssetSync } from '../../../../adapters/templates/template-asset.ts'; +import { withDatabasePermissions } from './database-permissions.ts'; import { renderHttpEndpointCall } from './render-http-endpoint.ts'; -function withRequiredServicePermissions( - permissions: readonly string[], - databaseEngine: RegisterServicesOptions['databaseEngine'], -): readonly string[] { - return databaseEngine === 'Sqlite' && !permissions.includes('--allow-ffi') - ? [...permissions, '--allow-ffi'] - : permissions; -} - /** * Generates the `register-services.mts` file content for a scaffolded Aspire * project. Produces a two-pass registration function that creates all service @@ -57,11 +49,11 @@ export function generateRegisterServices(options: RegisterServicesOptions): stri const workdir = entry.Workdir ?? `${SCAFFOLD_DIRS.SERVICES}/${name}`; const watchMode = denoDefaults.WatchMode; const entryPermissions = entry.Permissions - ? withRequiredServicePermissions(entry.Permissions, databaseEngine) + ? withDatabasePermissions(entry.Permissions, databaseEngine) : undefined; const defaultPermissions = entryPermissions ? denoDefaults.Permissions - : withRequiredServicePermissions(denoDefaults.Permissions, databaseEngine); + : withDatabasePermissions(denoDefaults.Permissions, databaseEngine); const lines: string[] = []; lines.push(` // --- ${name} ---`); diff --git a/packages/cli/src/kernel/templates/aspire/helpers/tests/database-permissions_test.ts b/packages/cli/src/kernel/templates/aspire/helpers/tests/database-permissions_test.ts new file mode 100644 index 0000000000..60f27c9aae --- /dev/null +++ b/packages/cli/src/kernel/templates/aspire/helpers/tests/database-permissions_test.ts @@ -0,0 +1,14 @@ +import { assertEquals, assertStrictEquals } from '@std/assert'; + +import { withDatabasePermissions } from '../register/database-permissions.ts'; + +Deno.test('SQLite permissions add FFI only when it is not already granted', () => { + const scoped = ['--allow-net'] as const; + const explicitFfi = ['--allow-net', '--allow-ffi'] as const; + const allowAll = ['--unstable-kv', '--allow-all'] as const; + + assertEquals(withDatabasePermissions(scoped, 'Sqlite'), ['--allow-net', '--allow-ffi']); + assertStrictEquals(withDatabasePermissions(explicitFfi, 'Sqlite'), explicitFfi); + assertStrictEquals(withDatabasePermissions(allowAll, 'Sqlite'), allowAll); + assertStrictEquals(withDatabasePermissions(scoped, 'Postgres'), scoped); +}); diff --git a/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-background-app_test.ts b/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-background-app_test.ts index c34513cdd0..62e501a4e9 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-background-app_test.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-background-app_test.ts @@ -3,7 +3,7 @@ */ import { describe, it } from 'jsr:@std/testing@^1/bdd'; -import { assert, assertStringIncludes } from 'jsr:@std/assert@^1'; +import { assert, assertEquals, assertStringIncludes } from 'jsr:@std/assert@^1'; import type { BackgroundProcessorEntry } from '@netscript/aspire/types'; import { RESOURCE_DEFAULTS } from '@netscript/aspire/constants'; import { generateRegisterBackground } from '../register/generate-register-background.ts'; @@ -59,6 +59,44 @@ describe('generateRegisterBackground', () => { assertStringIncludes(output, "backgroundProcessors.set('workers'"); }); + it('emits SQLite FFI exactly once for background processors', () => { + const output = generateRegisterBackground({ + ...emptyOptions, + processors: { workers: fixtures.MINIMAL_BACKGROUND }, + databaseEngine: 'Sqlite', + }); + const explicitOutput = generateRegisterBackground({ + ...emptyOptions, + processors: { + workers: { + ...fixtures.MINIMAL_BACKGROUND, + Permissions: ['--allow-net', '--allow-ffi'], + }, + }, + databaseEngine: 'Sqlite', + }); + + assertEquals(output.match(/--allow-ffi/g)?.length, 1); + assertEquals(explicitOutput.match(/--allow-ffi/g)?.length, 1); + }); + + it('keeps non-SQLite background output byte-identical', () => { + const options = { + ...emptyOptions, + processors: { workers: fixtures.MINIMAL_BACKGROUND }, + }; + const baseline = generateRegisterBackground(options); + const engines = [undefined, 'Postgres', 'Mysql', 'Mssql'] as const; + + for (const databaseEngine of engines) { + assertEquals( + generateRegisterBackground({ ...options, databaseEngine }), + baseline, + `${databaseEngine ?? 'none'} background output`, + ); + } + }); + it('should use --watch flag (not --watch-hmr) for background processors', () => { const output = generateRegisterBackground({ ...emptyOptions, diff --git a/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-pipeline_test.ts b/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-pipeline_test.ts index ccae55ca9a..49a4389478 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-pipeline_test.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-pipeline_test.ts @@ -35,6 +35,34 @@ describe('HelpersGeneratorPipeline', () => { } }); + it('threads SQLite FFI to every permission-bearing register output', async () => { + const pipeline = new HelpersGeneratorPipeline(); + const files = await pipeline.execute({ + config: { + ...fixtures.POPULATED_CONFIG, + PrimaryDatabase: 'main', + Databases: { + main: { ...fixtures.MINIMAL_DATABASE, Engine: 'Sqlite' }, + }, + }, + }); + const permissionBearingPaths = [ + '.helpers/register-services.mts', + '.helpers/register-plugins.mts', + '.helpers/register-background.mts', + ]; + + for (const path of permissionBearingPaths) { + const generated = files.find((file) => file.path === path); + assert(generated, `${path} should be generated`); + assertEquals( + generated.content.match(/--allow-ffi/g)?.length, + 1, + `${path} should contain SQLite FFI exactly once`, + ); + } + }); + it('should emit local import specifiers that resolve to generated files', async () => { const pipeline = new HelpersGeneratorPipeline(); const files = await pipeline.execute({ config: fixtures.POPULATED_CONFIG }); diff --git a/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-service-plugin_test.ts b/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-service-plugin_test.ts index 9e19107850..0e11ba4a7f 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-service-plugin_test.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/tests/generators-service-plugin_test.ts @@ -162,6 +162,23 @@ describe('generateRegisterServices', () => { assertEquals(output.match(/--allow-ffi/g)?.length, 1); }); + it('keeps non-SQLite service output byte-identical', () => { + const options = { + ...emptyOptions, + services: { users: fixtures.MINIMAL_SERVICE }, + }; + const baseline = generateRegisterServices(options); + const engines = [undefined, 'Postgres', 'Mysql', 'Mssql'] as const; + + for (const databaseEngine of engines) { + assertEquals( + generateRegisterServices({ ...options, databaseEngine }), + baseline, + `${databaseEngine ?? 'none'} service output`, + ); + } + }); + it('should wire primary database dependency for all services', () => { const output = generateRegisterServices({ ...emptyOptions, @@ -270,6 +287,44 @@ describe('generateRegisterPlugins', () => { assertStringIncludes(output, "plugins.set('auth'"); }); + it('emits SQLite FFI exactly once for plugin services', () => { + const output = generateRegisterPlugins({ + ...emptyOptions, + plugins: { auth: fixtures.MINIMAL_PLUGIN }, + databaseEngine: 'Sqlite', + }); + const explicitOutput = generateRegisterPlugins({ + ...emptyOptions, + plugins: { + auth: { + ...fixtures.MINIMAL_PLUGIN, + Permissions: ['--allow-net', '--allow-ffi'], + }, + }, + databaseEngine: 'Sqlite', + }); + + assertEquals(output.match(/--allow-ffi/g)?.length, 1); + assertEquals(explicitOutput.match(/--allow-ffi/g)?.length, 1); + }); + + it('keeps non-SQLite plugin output byte-identical', () => { + const options = { + ...emptyOptions, + plugins: { auth: fixtures.MINIMAL_PLUGIN }, + }; + const baseline = generateRegisterPlugins(options); + const engines = [undefined, 'Postgres', 'Mysql', 'Mssql'] as const; + + for (const databaseEngine of engines) { + assertEquals( + generateRegisterPlugins({ ...options, databaseEngine }), + baseline, + `${databaseEngine ?? 'none'} plugin output`, + ); + } + }); + it('should include full executable OTEL env vars for each plugin', () => { const output = generateRegisterPlugins({ ...emptyOptions, diff --git a/packages/cli/src/kernel/templates/aspire/helpers/types.ts b/packages/cli/src/kernel/templates/aspire/helpers/types.ts index ed4fb3b9fb..8761a4b266 100644 --- a/packages/cli/src/kernel/templates/aspire/helpers/types.ts +++ b/packages/cli/src/kernel/templates/aspire/helpers/types.ts @@ -74,6 +74,8 @@ export interface RegisterPluginsOptions { readonly plugins: Record; readonly version: string; readonly denoDefaults: DenoDefaults; + /** Selected primary database engine, used for engine-required runtime permissions. */ + readonly databaseEngine?: DatabaseEntry['Engine']; } /** Options for register-background.mts generation. */ @@ -81,6 +83,8 @@ export interface RegisterBackgroundOptions { readonly processors: Record; readonly version: string; readonly denoDefaults: DenoDefaults; + /** Selected primary database engine, used for engine-required runtime permissions. */ + readonly databaseEngine?: DatabaseEntry['Engine']; } /** Options for register-apps.mts generation. */ diff --git a/packages/cli/src/maintainer/features/init/init-command.ts b/packages/cli/src/maintainer/features/init/init-command.ts index 247acc7fa9..4a77dfecc0 100644 --- a/packages/cli/src/maintainer/features/init/init-command.ts +++ b/packages/cli/src/maintainer/features/init/init-command.ts @@ -59,6 +59,7 @@ export function createMaintainerInitCommand( .option('--service-name ', 'Example service name') .option('--model-name ', 'Prisma model name for the scaffolded CRUD surface') .option('--service-port ', 'Example service port') + .option('--cache [enabled:boolean]', 'Scaffold a shared cache resource') .option('--editor ', `Editor config (${EDITOR_CHOICES.join(' | ')})`) .option('--no-aspire', 'Skip Aspire orchestration layer') .option('--no-git', 'Skip git init after scaffolding') @@ -97,6 +98,7 @@ export function createMaintainerInitCommand( noGit: options.git === false, noAspire: options.aspire === false, dbEngine: parseDbEngine(options.db), + cache: options.cache, includeExampleService: includeService, serviceName: options.serviceName, modelName: options.modelName, diff --git a/packages/cli/src/maintainer/features/init/init-command_test.ts b/packages/cli/src/maintainer/features/init/init-command_test.ts index dc0ac39eaa..536e57495a 100644 --- a/packages/cli/src/maintainer/features/init/init-command_test.ts +++ b/packages/cli/src/maintainer/features/init/init-command_test.ts @@ -14,7 +14,7 @@ describe('createMaintainerInitCommand', () => { detectMonorepoRoot: () => Promise.resolve('C:/repo'), runInit: (request) => { initCalls.push( - `${request.name}:${request.appName}:${request.includeExampleService}:${request.serviceName}:${request.modelName}:${request.dbEngine}:${request.editor}`, + `${request.name}:${request.appName}:${request.includeExampleService}:${request.serviceName}:${request.modelName}:${request.dbEngine}:${request.cache}:${request.editor}`, ); return Promise.resolve({ name: request.name, @@ -47,12 +47,13 @@ describe('createMaintainerInitCommand', () => { 'Account', '--db', 'postgres', + '--cache=false', '--editor', 'zed', '--dry-run', ]); - assertEquals(initCalls, ['smoke-test:frontend:true:user:Account:postgres:zed']); + assertEquals(initCalls, ['smoke-test:frontend:true:user:Account:postgres:false:zed']); assertEquals(printed[0], 'Maintainer scaffold root: C:/repo/smoke-test'); }); }); diff --git a/packages/cli/src/maintainer/features/init/orchestrate-maintainer-init.ts b/packages/cli/src/maintainer/features/init/orchestrate-maintainer-init.ts index 154a909a57..70cf27b0cd 100644 --- a/packages/cli/src/maintainer/features/init/orchestrate-maintainer-init.ts +++ b/packages/cli/src/maintainer/features/init/orchestrate-maintainer-init.ts @@ -33,6 +33,8 @@ export interface MaintainerInitRequest { readonly noAspire: boolean; /** Optional database engine selection. */ readonly dbEngine?: DbEngineChoice; + /** Whether to scaffold a shared cache resource. */ + readonly cache?: boolean; /** Whether to include an example oRPC service. */ readonly includeExampleService?: boolean; /** Example service name override. */